mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-04 15:15:41 -04:00
Compare commits
78 Commits
1.5.5
...
renovate/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffbfad206e | ||
|
|
321a0e8540 | ||
|
|
cb8f769e34 | ||
|
|
38ff77a04e | ||
|
|
292cd8dd94 | ||
|
|
3878059314 | ||
|
|
7a7e50c7ba | ||
|
|
564dd536fc | ||
|
|
0e29805351 | ||
| dc76267da3 | |||
| 147b11b22d | |||
|
|
b992e4ff01 | ||
|
|
b46a922464 | ||
| d4abc705a0 | |||
| 454d13c608 | |||
|
|
6086dbbad2 | ||
|
|
b9607dddce | ||
|
|
1a7aeb4450 | ||
|
|
d973af6a8d | ||
|
2dae279f93
|
|||
|
09d17717ab
|
|||
|
bf4454f635
|
|||
|
9e531d823f
|
|||
| d17aa23e98 | |||
| 63f7c1d665 | |||
|
c5c2f741f0
|
|||
|
ec24c51eea
|
|||
|
fa2c4073e3
|
|||
|
255d093269
|
|||
|
73a264b1c2
|
|||
|
|
c112230e05 | ||
|
|
02dd911e93 | ||
|
9c3ee3d146
|
|||
|
4c8d5d03d9
|
|||
|
c729594864
|
|||
|
713626810b
|
|||
|
6e5c261065
|
|||
|
2f1027e068
|
|||
|
739f547731
|
|||
|
3f7ecdb84e
|
|||
|
e037764c3f
|
|||
|
c8aa866dfd
|
|||
|
84b1199878
|
|||
|
04150d5320
|
|||
|
11ccf2909f
|
|||
|
631942ca75
|
|||
|
4ee603d7a2
|
|||
|
7cfe22b72e
|
|||
|
e6701cda95
|
|||
|
865a2fd645
|
|||
|
05f362153f
|
|||
|
997eb72378
|
|||
|
69805151c8
|
|||
|
47ec5267ec
|
|||
|
3a87b51f41
|
|||
|
ffc8cfe68e
|
|||
|
7f5fffa5e6
|
|||
|
b16f4a9fb3
|
|||
|
ebf0b84a05
|
|||
|
b724930c6a
|
|||
|
603e444d35
|
|||
|
f274b807f2
|
|||
|
d0c8920b98
|
|||
|
4c0d49508f
|
|||
|
2a09bc1ea3
|
|||
|
259d27a2ce
|
|||
|
|
8aefbb39e0 | ||
|
|
e417b9f5d8 | ||
|
|
9458587d59 | ||
|
|
468c8c10fc | ||
|
|
6d90a88b60 | ||
|
|
7231199f9e | ||
|
|
41c0c9f685 | ||
|
|
79c5c648c9 | ||
|
|
0ba2e23171 | ||
|
|
b0f3869621 | ||
|
|
b709d53e40 | ||
|
|
b198168d75 |
2
.github/workflows/cache-clean.yml
vendored
2
.github/workflows/cache-clean.yml
vendored
@@ -114,7 +114,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/deploy-clean.yml
vendored
6
.github/workflows/deploy-clean.yml
vendored
@@ -152,8 +152,8 @@ jobs:
|
|||||||
cleanup:
|
cleanup:
|
||||||
name: >-
|
name: >-
|
||||||
🧹 Deployments › Clean
|
🧹 Deployments › Clean
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/deploy-docker-all.yml
vendored
8
.github/workflows/deploy-docker-all.yml
vendored
@@ -255,7 +255,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_tags_checkout
|
id: task_release_tags_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_gh_checkout
|
id: task_release_gh_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -787,7 +787,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_dh_checkout
|
id: task_release_dh_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -1232,7 +1232,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_gi_checkout
|
id: task_release_gi_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/deploy-docker-dockerhub.yml
vendored
12
.github/workflows/deploy-docker-dockerhub.yml
vendored
@@ -182,8 +182,8 @@ jobs:
|
|||||||
job-docker-release-tags-create:
|
job-docker-release-tags-create:
|
||||||
name: >-
|
name: >-
|
||||||
📦 Release › Create Tag
|
📦 Release › Create Tag
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 4
|
timeout-minutes: 4
|
||||||
outputs:
|
outputs:
|
||||||
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
|
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
|
||||||
@@ -199,7 +199,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -374,8 +374,8 @@ jobs:
|
|||||||
job-docker-release-dockerhub:
|
job-docker-release-dockerhub:
|
||||||
name: >-
|
name: >-
|
||||||
📦 Release › Dockerhub
|
📦 Release › Dockerhub
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
needs: [ job-docker-release-tags-create ]
|
needs: [ job-docker-release-tags-create ]
|
||||||
permissions:
|
permissions:
|
||||||
@@ -392,7 +392,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
42
.github/workflows/deploy-docker-gitea.yml
vendored
42
.github/workflows/deploy-docker-gitea.yml
vendored
@@ -194,8 +194,8 @@ jobs:
|
|||||||
job-docker-release-tags-create:
|
job-docker-release-tags-create:
|
||||||
name: >-
|
name: >-
|
||||||
📦 Release › Create Tag
|
📦 Release › Create Tag
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 4
|
timeout-minutes: 4
|
||||||
outputs:
|
outputs:
|
||||||
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
|
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -386,8 +386,8 @@ jobs:
|
|||||||
job-docker-release-gitea:
|
job-docker-release-gitea:
|
||||||
name: >-
|
name: >-
|
||||||
📦 Release › Gitea
|
📦 Release › Gitea
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
needs: [ job-docker-release-tags-create ]
|
needs: [ job-docker-release-tags-create ]
|
||||||
permissions:
|
permissions:
|
||||||
@@ -404,7 +404,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -571,6 +571,32 @@ jobs:
|
|||||||
id: task_release_gi_qemu
|
id: task_release_gi_qemu
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
# #
|
||||||
|
# Required to fix insecure SSL error with docker buildx
|
||||||
|
# #
|
||||||
|
|
||||||
|
- name: '⚙️ Configure Docker daemon to allow insecure registry'
|
||||||
|
run: |
|
||||||
|
echo "Configuring daemon to treat ${REGISTRY_HOST} as insecure"
|
||||||
|
sudo mkdir -p /etc/docker
|
||||||
|
sudo tee /etc/docker/daemon.json > /dev/null <<'JSON'
|
||||||
|
{
|
||||||
|
"insecure-registries": ["git.binaryninja.net:443"]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
# Restart Docker
|
||||||
|
sudo service docker restart
|
||||||
|
env:
|
||||||
|
REGISTRY_HOST: git.binaryninja.net
|
||||||
|
|
||||||
|
# #
|
||||||
|
# Make sure change in docker daemon config successful
|
||||||
|
# #
|
||||||
|
|
||||||
|
- name: '⚙️ Check Docker Daemon Configuration'
|
||||||
|
run: cat /etc/docker/daemon.json
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Release › Gitea › Setup BuildX › Amd64
|
# Release › Gitea › Setup BuildX › Amd64
|
||||||
# #
|
# #
|
||||||
@@ -581,6 +607,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
driver-opts: 'image=moby/buildkit:latest'
|
driver-opts: 'image=moby/buildkit:latest'
|
||||||
|
driver: docker
|
||||||
|
buildkitd-flags: --allow-insecure-entitlement
|
||||||
|
install: true
|
||||||
|
use: true
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Release › Gitea › Registry Login › Amd64
|
# Release › Gitea › Registry Login › Amd64
|
||||||
|
|||||||
6
.github/workflows/deploy-docker-giteacom.yml
vendored
6
.github/workflows/deploy-docker-giteacom.yml
vendored
@@ -200,7 +200,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_tags_checkout
|
id: task_release_tags_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_gh_checkout
|
id: task_release_gh_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Release › Gitea › Get Timestamp
|
# Release › Gitea › Get Timestamp
|
||||||
@@ -540,7 +540,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
id: task_release_gh_checkout
|
id: task_release_gh_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Release › Gitea › Get Timestamp
|
# Release › Gitea › Get Timestamp
|
||||||
|
|||||||
18
.github/workflows/deploy-docker-github.yml
vendored
18
.github/workflows/deploy-docker-github.yml
vendored
@@ -181,8 +181,8 @@ jobs:
|
|||||||
job-docker-release-tags-create:
|
job-docker-release-tags-create:
|
||||||
name: >-
|
name: >-
|
||||||
📦 Release › Create Tag
|
📦 Release › Create Tag
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 4
|
timeout-minutes: 4
|
||||||
outputs:
|
outputs:
|
||||||
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
|
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
|
||||||
@@ -198,7 +198,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -371,8 +371,8 @@ jobs:
|
|||||||
job-docker-release-github:
|
job-docker-release-github:
|
||||||
name: >-
|
name: >-
|
||||||
📦 Release › Github
|
📦 Release › Github
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
needs: [ job-docker-release-tags-create ]
|
needs: [ job-docker-release-tags-create ]
|
||||||
permissions:
|
permissions:
|
||||||
@@ -389,7 +389,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -954,8 +954,8 @@ jobs:
|
|||||||
job-docker-release-cleanup:
|
job-docker-release-cleanup:
|
||||||
name: >-
|
name: >-
|
||||||
🧹 Release › Cleanup
|
🧹 Release › Cleanup
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
needs: [ job-docker-release-tags-create, job-docker-release-github ]
|
needs: [ job-docker-release-tags-create, job-docker-release-github ]
|
||||||
permissions:
|
permissions:
|
||||||
@@ -972,7 +972,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '✅ Checkout'
|
- name: '✅ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/documentation.yml
vendored
6
.github/workflows/documentation.yml
vendored
@@ -170,8 +170,8 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docs:
|
build-docs:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/gpg-tests.yml
vendored
2
.github/workflows/gpg-tests.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/history-clean.yml
vendored
2
.github/workflows/history-clean.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
22
.github/workflows/issues-new.yml
vendored
22
.github/workflows/issues-new.yml
vendored
@@ -165,8 +165,8 @@ jobs:
|
|||||||
job-labels-create:
|
job-labels-create:
|
||||||
name: >-
|
name: >-
|
||||||
🎫 Labels › Verify Existing
|
🎫 Labels › Verify Existing
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -335,8 +335,8 @@ jobs:
|
|||||||
🏷️ Labels › Assign
|
🏷️ Labels › Assign
|
||||||
needs:
|
needs:
|
||||||
- job-labels-create
|
- job-labels-create
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
permissions:
|
permissions:
|
||||||
contents: 'read'
|
contents: 'read'
|
||||||
@@ -349,7 +349,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -1191,8 +1191,8 @@ jobs:
|
|||||||
🏷️ Labels › Phrase Search
|
🏷️ Labels › Phrase Search
|
||||||
needs:
|
needs:
|
||||||
- job-labels-create
|
- job-labels-create
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
permissions:
|
permissions:
|
||||||
contents: 'read'
|
contents: 'read'
|
||||||
@@ -1207,7 +1207,7 @@ jobs:
|
|||||||
- name: >-
|
- name: >-
|
||||||
☑️ Prepare
|
☑️ Prepare
|
||||||
id: issues-labels-check-checkout
|
id: issues-labels-check-checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -1280,8 +1280,8 @@ jobs:
|
|||||||
job-assign-assignees:
|
job-assign-assignees:
|
||||||
name: >-
|
name: >-
|
||||||
✍️ Issue › Assignees
|
✍️ Issue › Assignees
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
needs: [ job-assign-labels ]
|
needs: [ job-assign-labels ]
|
||||||
# disable
|
# disable
|
||||||
|
|||||||
8
.github/workflows/issues-scan.yml
vendored
8
.github/workflows/issues-scan.yml
vendored
@@ -160,8 +160,8 @@ jobs:
|
|||||||
job-pr-scan:
|
job-pr-scan:
|
||||||
name: >-
|
name: >-
|
||||||
🎫 Issues › Autoscan
|
🎫 Issues › Autoscan
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -175,7 +175,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ jobs:
|
|||||||
|
|
||||||
- name: >-
|
- name: >-
|
||||||
☑️ Checkout
|
☑️ Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
if: |
|
if: |
|
||||||
( github.event_name == 'pull_request_target' ) || ( github.event_name == 'pull_request' ) || ( github.event_name == 'issue_comment' && contains( github.event.comment.html_url, '/pull/' ) && contains( github.event.comment.body, '/rescan' ) )
|
( github.event_name == 'pull_request_target' ) || ( github.event_name == 'pull_request' ) || ( github.event_name == 'issue_comment' && contains( github.event.comment.html_url, '/pull/' ) && contains( github.event.comment.body, '/rescan' ) )
|
||||||
with:
|
with:
|
||||||
|
|||||||
20
.github/workflows/issues-stale.yml
vendored
20
.github/workflows/issues-stale.yml
vendored
@@ -181,8 +181,8 @@ jobs:
|
|||||||
job-labels-create:
|
job-labels-create:
|
||||||
name: >-
|
name: >-
|
||||||
🎫 Labels › Verify Existing
|
🎫 Labels › Verify Existing
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -360,8 +360,8 @@ jobs:
|
|||||||
job-issues-nolabel:
|
job-issues-nolabel:
|
||||||
name: >-
|
name: >-
|
||||||
🎫 Labels › Assign Missing
|
🎫 Labels › Assign Missing
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 4
|
timeout-minutes: 4
|
||||||
needs: job-labels-create
|
needs: job-labels-create
|
||||||
steps:
|
steps:
|
||||||
@@ -371,7 +371,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -961,8 +961,8 @@ jobs:
|
|||||||
job-issues-stale:
|
job-issues-stale:
|
||||||
name: >-
|
name: >-
|
||||||
💤 Scan › Check Stale
|
💤 Scan › Check Stale
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
needs:
|
needs:
|
||||||
- job-labels-create
|
- job-labels-create
|
||||||
@@ -1005,8 +1005,8 @@ jobs:
|
|||||||
job-issues-lock:
|
job-issues-lock:
|
||||||
name: >-
|
name: >-
|
||||||
🔒 Scan › Lock Inactive
|
🔒 Scan › Lock Inactive
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
needs:
|
needs:
|
||||||
- job-labels-create
|
- job-labels-create
|
||||||
|
|||||||
6
.github/workflows/labels-clean.yml
vendored
6
.github/workflows/labels-clean.yml
vendored
@@ -146,8 +146,8 @@ jobs:
|
|||||||
issues-labels-clean:
|
issues-labels-clean:
|
||||||
name: >-
|
name: >-
|
||||||
🧹 Labels › Clean
|
🧹 Labels › Clean
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
permissions:
|
permissions:
|
||||||
contents: 'read'
|
contents: 'read'
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/labels-create.yml
vendored
6
.github/workflows/labels-create.yml
vendored
@@ -193,8 +193,8 @@ jobs:
|
|||||||
issues-labels-create:
|
issues-labels-create:
|
||||||
name: >-
|
name: >-
|
||||||
🎫 Labels › Create
|
🎫 Labels › Create
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
runs-on: apollo-x64
|
# runs-on: apollo-x64
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
permissions:
|
permissions:
|
||||||
contents: 'read'
|
contents: 'read'
|
||||||
@@ -207,7 +207,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/ping-developer.yml
vendored
2
.github/workflows/ping-developer.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
|||||||
# #
|
# #
|
||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -166,7 +166,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
id: task_initialize_checkout
|
id: task_initialize_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ jobs:
|
|||||||
|
|
||||||
- name: '☑️ Checkout'
|
- name: '☑️ Checkout'
|
||||||
id: task_release_checkout
|
id: task_release_checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ ENV DIR_RUN=/usr/bin/app
|
|||||||
ENV URL_REPO="https://git.binaryninja.net/binaryninja/"
|
ENV URL_REPO="https://git.binaryninja.net/binaryninja/"
|
||||||
ENV WEB_IP="0.0.0.0"
|
ENV WEB_IP="0.0.0.0"
|
||||||
ENV WEB_PORT=4124
|
ENV WEB_PORT=4124
|
||||||
|
ENV HDHR_PORT=6077
|
||||||
ENV WEB_ENCODING="deflate, br"
|
ENV WEB_ENCODING="deflate, br"
|
||||||
ENV WEB_PROXY_HEADER="x-forwarded-for"
|
ENV WEB_PROXY_HEADER="x-forwarded-for"
|
||||||
ENV STREAM_QUALITY="hd"
|
ENV STREAM_QUALITY="hd"
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 BinaryNinja
|
Copyright (c) 2025-2026 BinaryNinja
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ The following is a list of environment variables you can declare within your `do
|
|||||||
| `WEB_FOLDER` | `www` | Internal container folder to keep TVApp2 web files in. <br /><br /> <sup>⚠️ This should not be used unless you know what you're doing</sup> |
|
| `WEB_FOLDER` | `www` | Internal container folder to keep TVApp2 web files in. <br /><br /> <sup>⚠️ This should not be used unless you know what you're doing</sup> |
|
||||||
| `WEB_ENCODING` | `deflate, br` | Defines the HTTP `Accept-Encoding` request and response header. This value specifies what content encoding the sender can understand<br /><br />Gzip compression can be enabled by specifying `'gzip, deflate, br'`, however, [it may break Jellyfin users](#build-error-err-27-jellyfinlivetvguideguidemanager-error-getting-programs-for-channel-xxxxxxxxxxxxxxx-source-2-systemxmlxmlexception--hexadecimal-value-0x1f-is-an-invalid-character-line-1-position-1). |
|
| `WEB_ENCODING` | `deflate, br` | Defines the HTTP `Accept-Encoding` request and response header. This value specifies what content encoding the sender can understand<br /><br />Gzip compression can be enabled by specifying `'gzip, deflate, br'`, however, [it may break Jellyfin users](#build-error-err-27-jellyfinlivetvguideguidemanager-error-getting-programs-for-channel-xxxxxxxxxxxxxxx-source-2-systemxmlxmlexception--hexadecimal-value-0x1f-is-an-invalid-character-line-1-position-1). |
|
||||||
| `WEB_PROXY_HEADER` | `x-forwarded-for` | Defines the header to look for when finding a client's IP address. Used to get a client's IP when behind a reverse proxy or Cloudflare |
|
| `WEB_PROXY_HEADER` | `x-forwarded-for` | Defines the header to look for when finding a client's IP address. Used to get a client's IP when behind a reverse proxy or Cloudflare |
|
||||||
|
| `HDHR_PORT` | `6077` | HDHomeRun server default listening port |
|
||||||
| `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. |
|
| `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. |
|
||||||
| `FILE_URL` | `urls.txt` | Filename for `urls.txt` cache file |
|
| `FILE_URL` | `urls.txt` | Filename for `urls.txt` cache file |
|
||||||
| `FILE_M3U` | `playlist.m3u8` | Filename for M3U playlist file |
|
| `FILE_M3U` | `playlist.m3u8` | Filename for M3U playlist file |
|
||||||
@@ -1318,6 +1319,7 @@ This docker container contains the following env variables:
|
|||||||
| `WEB_FOLDER` | `www` | Internal container folder to keep TVApp2 web files in. <br /><br /> <sup>⚠️ This should not be used unless you know what you're doing</sup> |
|
| `WEB_FOLDER` | `www` | Internal container folder to keep TVApp2 web files in. <br /><br /> <sup>⚠️ This should not be used unless you know what you're doing</sup> |
|
||||||
| `WEB_ENCODING` | `deflate, br` | Defines the HTTP `Accept-Encoding` request and response header. This value specifies what content encoding the sender can understand<br /><br />Gzip compression can be enabled by specifying `'gzip, deflate, br'`, however, [it may break Jellyfin users](#build-error-err-27-jellyfinlivetvguideguidemanager-error-getting-programs-for-channel-xxxxxxxxxxxxxxx-source-2-systemxmlxmlexception--hexadecimal-value-0x1f-is-an-invalid-character-line-1-position-1). |
|
| `WEB_ENCODING` | `deflate, br` | Defines the HTTP `Accept-Encoding` request and response header. This value specifies what content encoding the sender can understand<br /><br />Gzip compression can be enabled by specifying `'gzip, deflate, br'`, however, [it may break Jellyfin users](#build-error-err-27-jellyfinlivetvguideguidemanager-error-getting-programs-for-channel-xxxxxxxxxxxxxxx-source-2-systemxmlxmlexception--hexadecimal-value-0x1f-is-an-invalid-character-line-1-position-1). |
|
||||||
| `WEB_PROXY_HEADER` | `x-forwarded-for` | Defines the header to look for when finding a client's IP address. Used to get a client's IP when behind a reverse proxy or Cloudflare |
|
| `WEB_PROXY_HEADER` | `x-forwarded-for` | Defines the header to look for when finding a client's IP address. Used to get a client's IP when behind a reverse proxy or Cloudflare |
|
||||||
|
| `HDHR_PORT` | `6077` | HDHomeRun server default listening port |
|
||||||
| `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. |
|
| `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. |
|
||||||
| `FILE_URL` | `urls.txt` | Filename for `urls.txt` cache file |
|
| `FILE_URL` | `urls.txt` | Filename for `urls.txt` cache file |
|
||||||
| `FILE_M3U` | `playlist.m3u8` | Filename for M3U playlist file |
|
| `FILE_M3U` | `playlist.m3u8` | Filename for M3U playlist file |
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Service › TVApp2 › Traefik Labels
|
# Service › TVApp2
|
||||||
# #
|
# #
|
||||||
|
|
||||||
tvapp2:
|
tvapp2:
|
||||||
@@ -40,7 +40,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./config:/config
|
- ./config:/config
|
||||||
- ./app:/usr/bin/app
|
- ./app:/usr/bin/app
|
||||||
ulimits:
|
ulimits:
|
||||||
@@ -56,7 +55,7 @@ services:
|
|||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Scope > http
|
# Routers › Web Interface › http
|
||||||
# #
|
# #
|
||||||
|
|
||||||
- traefik.http.routers.tvapp2-http.rule=Host(`tvapp2.localhost`) || Host(`tvapp2.domain.lan`) || Host(`www.tvapp2.domain.lan`) || Host(`${SERVICE_IP}`)
|
- traefik.http.routers.tvapp2-http.rule=Host(`tvapp2.localhost`) || Host(`tvapp2.domain.lan`) || Host(`www.tvapp2.domain.lan`) || Host(`${SERVICE_IP}`)
|
||||||
@@ -66,7 +65,7 @@ services:
|
|||||||
- traefik.http.routers.tvapp2-http.middlewares=https-redirect@file
|
- traefik.http.routers.tvapp2-http.middlewares=https-redirect@file
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Scope > https
|
# Routers › Web Interface › https
|
||||||
#
|
#
|
||||||
# remove the authentik@file line if you do not wish to use Authentik or middleware
|
# remove the authentik@file line if you do not wish to use Authentik or middleware
|
||||||
# - traefik.http.routers.tvapp2-https.middlewares=authentik@file
|
# - traefik.http.routers.tvapp2-https.middlewares=authentik@file
|
||||||
@@ -83,8 +82,26 @@ services:
|
|||||||
- traefik.http.routers.tvapp2-https.middlewares=authentik@file
|
- traefik.http.routers.tvapp2-https.middlewares=authentik@file
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# Load Balancer
|
# Routers › HDHomeRun
|
||||||
# #
|
# #
|
||||||
|
|
||||||
- traefik.http.services.tvapp2.loadbalancer.server.port=http
|
- traefik.http.routers.hdhr-https.rule=Host(`hdhr.domain.lan`)
|
||||||
- traefik.http.services.tvapp2.loadbalancer.server.scheme=4124
|
- traefik.http.routers.hdhr-https.service=hdhr
|
||||||
|
- traefik.http.routers.hdhr-https.entrypoints=https
|
||||||
|
- traefik.http.routers.hdhr-https.priority=1
|
||||||
|
- traefik.http.routers.hdhr-https.tls=true
|
||||||
|
- traefik.http.routers.hdhr-https.tls.certresolver=cloudflare
|
||||||
|
|
||||||
|
# #
|
||||||
|
# Services › Main Web Interface
|
||||||
|
# #
|
||||||
|
|
||||||
|
- traefik.http.services.tvapp2.loadbalancer.server.port=4124
|
||||||
|
- traefik.http.services.tvapp2.loadbalancer.server.scheme=http
|
||||||
|
|
||||||
|
# #
|
||||||
|
# Services › HDHomeRun Server (optional)
|
||||||
|
# #
|
||||||
|
|
||||||
|
- traefik.http.services.hdhr.loadbalancer.server.port=6077
|
||||||
|
- traefik.http.services.hdhr.loadbalancer.server.scheme=http
|
||||||
|
|||||||
@@ -295,24 +295,24 @@ http:
|
|||||||
- "*.domain.lan"
|
- "*.domain.lan"
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# @container TVApp2
|
# @container TVApp2 › Main
|
||||||
# @desc utomatic M3U playlist and XML guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
|
# @desc automatic M3U playlist and XML guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
|
||||||
# @url https://github.com/TheBinaryNinja/tvapp2
|
# @url https://github.com/TheBinaryNinja/tvapp2
|
||||||
#
|
#
|
||||||
# remove / comment out the authentik line if you do not plan to use authentik:
|
# remove / comment out the authentik line if you do not plan to use authentik:
|
||||||
# - authentik@file
|
# - authentik@file
|
||||||
# #
|
# #
|
||||||
|
|
||||||
tvapp2-http:
|
tvapp2-server-http:
|
||||||
service: "tvapp2"
|
service: "tvapp2-server"
|
||||||
rule: "Host(`tvapp2.localhost`) || Host(`tvapp2.domain.lan`)"
|
rule: "Host(`tvapp2.localhost`) || Host(`tvapp2.domain.lan`)"
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- http
|
- http
|
||||||
middlewares:
|
middlewares:
|
||||||
- https-redirect@file
|
- https-redirect@file
|
||||||
|
|
||||||
tvapp2-https:
|
tvapp2-server-https:
|
||||||
service: "tvapp2"
|
service: "tvapp2-server"
|
||||||
rule: "Host(`tvapp2.localhost`) || Host(`tvapp2.domain.lan`)"
|
rule: "Host(`tvapp2.localhost`) || Host(`tvapp2.domain.lan`)"
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- https
|
- https
|
||||||
@@ -325,6 +325,37 @@ http:
|
|||||||
- main: "domain.lan"
|
- main: "domain.lan"
|
||||||
sans:
|
sans:
|
||||||
- "*.domain.lan"
|
- "*.domain.lan"
|
||||||
|
# #
|
||||||
|
# @container TVApp2 › HDHomeRun
|
||||||
|
# @desc automatic M3U playlist and XML guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
|
||||||
|
# @url https://github.com/TheBinaryNinja/tvapp2
|
||||||
|
#
|
||||||
|
# remove / comment out the authentik line if you do not plan to use authentik:
|
||||||
|
# - authentik@file
|
||||||
|
# #
|
||||||
|
|
||||||
|
tvapp2-hdhr-http:
|
||||||
|
service: "tvapp2-hdhr"
|
||||||
|
rule: "Host(`hdhr.localhost`) || Host(`hdhr.domain.lan`)"
|
||||||
|
entryPoints:
|
||||||
|
- http
|
||||||
|
middlewares:
|
||||||
|
- https-redirect@file
|
||||||
|
|
||||||
|
tvapp2-hdhr-https:
|
||||||
|
service: "tvapp2-hdhr"
|
||||||
|
rule: "Host(`hdhr.localhost`) || Host(`hdhr.domain.lan`)"
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
middlewares:
|
||||||
|
- redirect-www@file
|
||||||
|
- authentik@file
|
||||||
|
tls:
|
||||||
|
certResolver: cloudflare
|
||||||
|
domains:
|
||||||
|
- main: "domain.lan"
|
||||||
|
sans:
|
||||||
|
- "*.domain.lan"
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# http › Services
|
# http › Services
|
||||||
@@ -351,7 +382,12 @@ http:
|
|||||||
servers:
|
servers:
|
||||||
- url: "http://plex:32400"
|
- url: "http://plex:32400"
|
||||||
|
|
||||||
tvapp2:
|
tvapp2-server:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://tvapp2:4124"
|
||||||
|
|
||||||
|
tvapp2-hdhr:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://tvapp2:4124"
|
- url: "http://tvapp2:4124"
|
||||||
|
|||||||
169
tvapp2/classes/CLib.js
Normal file
169
tvapp2/classes/CLib.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
Compress / Uncompress String with base64
|
||||||
|
|
||||||
|
these functions use a unique character table. moving the letters around will cause strings to not
|
||||||
|
be in the correct order once uncompressed.
|
||||||
|
|
||||||
|
@usage new CLib().compress( 'https://daddylive.mp/' )
|
||||||
|
new CLib().uncompress( 'burS7u6FvUHhZfrhkfJoYz8CswTD=' )
|
||||||
|
new CLib().translate( '=', plugin.defTrans, plugin.tvaTrans )
|
||||||
|
|
||||||
|
a custom character set can be specified with two additional parameters. however, anything prior
|
||||||
|
that was encoded will not be decoded by the new character set.
|
||||||
|
|
||||||
|
const strCompress = new CLib().compress( 'test.com' );
|
||||||
|
const strUncompress = new CLib().uncompress( strCompress );
|
||||||
|
|
||||||
|
new CLib().compress( 'test.com', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'rXzxP9ZdvehYlstwiTuV1c07j45Abo2Ama6k3gqpyf8n+/NMSEIUHBQRJDLFCGKO' )
|
||||||
|
new CLib().uncompress( 'oZcUozDkAQH=', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'rXzxP9ZdvehYlstwiTuV1c07j45Abo2Ama6k3gqpyf8n+/NMSEIUHBQRJDLFCGKO' )
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import Log from './Log.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class > CLib
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CLib
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.defTrans = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
this.tvaTrans = 'TVAPp29uqXiv6g5adr1j8nfwZ0bs7Ykm3xl4hczAtoey+/CDKJULSEMBQRFGIHNO';
|
||||||
|
}
|
||||||
|
|
||||||
|
compress( data, defTrans, tvaTrans )
|
||||||
|
{
|
||||||
|
if ( typeof data === 'string' )
|
||||||
|
data = Buffer.from( data, 'utf8' );
|
||||||
|
|
||||||
|
const transDef = defTrans || this.defTrans;
|
||||||
|
const transTva = tvaTrans || this.tvaTrans;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const dataCompress = this.translate( data.toString( 'base64' ), transDef, transTva );
|
||||||
|
|
||||||
|
Log.ok( `clib`, chalk.yellow( `[compress]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Compress string` ),
|
||||||
|
chalk.blueBright( `<strRaw>` ), chalk.gray( `${ data }` ),
|
||||||
|
chalk.blueBright( `<strCompress>` ), chalk.gray( `${ dataCompress }` ) );
|
||||||
|
|
||||||
|
return dataCompress;
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `clib`, chalk.redBright( `[compress]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not compress string; bad string ${ data }` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<strCompress>` ), chalk.gray( `${ data }` ) );
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uncompress( data, defTrans, tvaTrans )
|
||||||
|
{
|
||||||
|
if ( Buffer.isBuffer( data ) )
|
||||||
|
data = data.toString();
|
||||||
|
|
||||||
|
const transDef = defTrans || this.defTrans;
|
||||||
|
const transTva = tvaTrans || this.tvaTrans;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const dataTranslated = this.translate( data, transTva, transDef );
|
||||||
|
const dataUncompress = Buffer.from( dataTranslated, 'base64' ).toString( 'utf8' );
|
||||||
|
|
||||||
|
Log.ok( `clib`, chalk.yellow( `[decompss]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Uncompress string` ),
|
||||||
|
chalk.blueBright( `<strCompress>` ), chalk.gray( `${ data }` ),
|
||||||
|
chalk.blueBright( `<strRaw>` ), chalk.gray( `${ dataUncompress }` ) );
|
||||||
|
|
||||||
|
return dataUncompress;
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `clib`, chalk.redBright( `[decompss]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not uncompress string; bad string ${ data }` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<strCompress>` ), chalk.gray( `${ data }` ) );
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Translate
|
||||||
|
|
||||||
|
compresses or decompresses encoded strings for the functions:
|
||||||
|
- compress
|
||||||
|
- uncompress
|
||||||
|
*/
|
||||||
|
|
||||||
|
translate( str, fromChars, toChars )
|
||||||
|
{
|
||||||
|
let res = '';
|
||||||
|
for ( let i = 0;i < str.length;i++ )
|
||||||
|
{
|
||||||
|
const char = str[i];
|
||||||
|
const index = fromChars.indexOf( char );
|
||||||
|
if ( index !== -1 )
|
||||||
|
res += toChars[index];
|
||||||
|
else
|
||||||
|
res += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Encode: String > Hex > Base64
|
||||||
|
|
||||||
|
encodes a human-readable string into a hex value, and then to base64
|
||||||
|
|
||||||
|
@usage const clib = new CLib()
|
||||||
|
const encoded = clib.encodeToHexBase64('hello'); // Njg2NTZjNmM2Zg==
|
||||||
|
const decoded = clib.decodeFromHexBase64(`${ encoded }`); // hello
|
||||||
|
*/
|
||||||
|
|
||||||
|
encodeToHexBase64( str )
|
||||||
|
{
|
||||||
|
const hex = [...str].map( ( char ) =>
|
||||||
|
{
|
||||||
|
const code = char.charCodeAt( 0 ).toString( 16 );
|
||||||
|
return code.padStart( 2, '0' );
|
||||||
|
}).join( '' );
|
||||||
|
|
||||||
|
const base64 = btoa( hex );
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decode: Base64 > Hex > String
|
||||||
|
|
||||||
|
decodes a base64 value to hex, and then back into a human readable string
|
||||||
|
|
||||||
|
@usage const clib = new CLib()
|
||||||
|
const encoded = clib.encodeToHexBase64('hello'); // Njg2NTZjNmM2Zg==
|
||||||
|
const decoded = clib.decodeFromHexBase64(`${ encoded }`); // hello
|
||||||
|
*/
|
||||||
|
|
||||||
|
decodeFromHexBase64( base64Str )
|
||||||
|
{
|
||||||
|
const hex = atob( base64Str );
|
||||||
|
const chars = hex.match( /.{1,2}/g ); // every 2 hex chars = 1 byte
|
||||||
|
|
||||||
|
return chars.map( ( byte ) => String.fromCharCode( parseInt( byte, 16 ) ) ).join( '' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@usage import CLib from './classes/CLib.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default CLib;
|
||||||
121
tvapp2/classes/Log.js
Normal file
121
tvapp2/classes/Log.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Define > Logs
|
||||||
|
|
||||||
|
When assigning text colors, terminals and the windows command prompt can display any color; however apps
|
||||||
|
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
|
||||||
|
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
|
||||||
|
|
||||||
|
Various levels of logs with the following usage:
|
||||||
|
Log.verbose(`This is verbose`)
|
||||||
|
Log.debug(`This is debug`)
|
||||||
|
Log.info(`This is info`)
|
||||||
|
Log.ok(`This is ok`)
|
||||||
|
Log.notice(`This is notice`)
|
||||||
|
Log.warn(`This is warn`)
|
||||||
|
Log.error(
|
||||||
|
`Error fetching sports data with error:`,
|
||||||
|
chalk.white(`→`),
|
||||||
|
chalk.grey(`This is the error message`)
|
||||||
|
);
|
||||||
|
|
||||||
|
Level Type
|
||||||
|
-----------------------------------
|
||||||
|
6 Trace
|
||||||
|
5 Debug
|
||||||
|
4 Info
|
||||||
|
3 Notice
|
||||||
|
2 Warn
|
||||||
|
1 Error
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
/*
|
||||||
|
chalk.level
|
||||||
|
|
||||||
|
@ref https://npmjs.com/package/chalk
|
||||||
|
- 0 All colors disabled
|
||||||
|
- 1 Basic color support (16 colors)
|
||||||
|
- 2 256 color support
|
||||||
|
- 3 Truecolor support (16 million colors)
|
||||||
|
|
||||||
|
When assigning text colors, terminals and the windows command prompt can display any color; however apps
|
||||||
|
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
|
||||||
|
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
chalk.level = 3;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LOG_LEVEL = process.env.LOG_LEVEL || 4;
|
||||||
|
const { name } = JSON.parse( fs.readFileSync( './package.json' ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class > Log
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Log
|
||||||
|
{
|
||||||
|
static now()
|
||||||
|
{
|
||||||
|
const now = new Date();
|
||||||
|
return chalk.gray( `[${ now.toLocaleTimeString() }]` );
|
||||||
|
}
|
||||||
|
|
||||||
|
static verbose( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 6 )
|
||||||
|
console.debug( chalk.white.bgBlack.blackBright.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static debug( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 7 )
|
||||||
|
console.trace( chalk.white.bgMagenta.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.magentaBright( msg.join( ' ' ) ) );
|
||||||
|
else if ( LOG_LEVEL >= 5 )
|
||||||
|
console.debug( chalk.white.bgGray.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static info( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 4 )
|
||||||
|
console.info( chalk.white.bgBlueBright.bold( ` ${ name } ` ), chalk.white( `ℹ️` ), this.now(), chalk.blueBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static ok( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 4 )
|
||||||
|
console.log( chalk.white.bgGreen.bold( ` ${ name } ` ), chalk.white( `✅` ), this.now(), chalk.greenBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static notice( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 3 )
|
||||||
|
console.log( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `📌` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static warn( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 2 )
|
||||||
|
console.warn( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `⚠️` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static error( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 1 )
|
||||||
|
console.error( chalk.white.bgRedBright.bold( ` ${ name } ` ), chalk.white( `❌` ), this.now(), chalk.redBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@usage import Log from './classes/Log.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Log;
|
||||||
47
tvapp2/classes/Semaphore.js
Normal file
47
tvapp2/classes/Semaphore.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
Semaphore > Declare
|
||||||
|
|
||||||
|
allows multiple threads to work with the same shared resources
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Semaphore
|
||||||
|
{
|
||||||
|
constructor( max )
|
||||||
|
{
|
||||||
|
this.max = max;
|
||||||
|
this.queue = [];
|
||||||
|
this.active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquire()
|
||||||
|
{
|
||||||
|
if ( this.active < this.max )
|
||||||
|
{
|
||||||
|
this.active++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise( ( resolve ) => this.queue.push( resolve ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
release()
|
||||||
|
{
|
||||||
|
this.active--;
|
||||||
|
if ( this.queue.length > 0 )
|
||||||
|
{
|
||||||
|
const resolve = this.queue.shift();
|
||||||
|
this.active++;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@usage import Log from './classes/Log.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Semaphore;
|
||||||
|
|
||||||
520
tvapp2/classes/Storage.js
Normal file
520
tvapp2/classes/Storage.js
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
/*
|
||||||
|
Class › Storage
|
||||||
|
|
||||||
|
The storage classes allows you to save specific settings into a json file. These settings are better off being stored in
|
||||||
|
a local file, instead of using up the resources being saved in a database.
|
||||||
|
|
||||||
|
Class supports multiple storage files, but by default, it will save settings in `www/config.json`.
|
||||||
|
|
||||||
|
Settings include Tuner / HDHomeRun device information, etc.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage( envWebFolder, FILE_CFG );
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import path from 'path';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import fs from 'fs';
|
||||||
|
import Log from './Log.js';
|
||||||
|
import Utils from './Utils.js';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
/*
|
||||||
|
CJS › ESM
|
||||||
|
*/
|
||||||
|
|
||||||
|
const __filename = fileURLToPath( import.meta.url ); // get resolved path to file
|
||||||
|
const __dirname = path.dirname( __filename ); // get name of directory
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class › Storage
|
||||||
|
|
||||||
|
constructor ( str:folder, str:file )
|
||||||
|
Initialize ( bool:bForceNew )
|
||||||
|
Setup ( bool:bForceNew )
|
||||||
|
Get ( str:key )
|
||||||
|
Set ( str:key, any:value )
|
||||||
|
Save ( )
|
||||||
|
GetConfig ( )
|
||||||
|
isJsonString ( json:str )
|
||||||
|
isJsonEmpty ( obj:json )
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Storage
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Constructor › Storage
|
||||||
|
|
||||||
|
Initializes a Storage instance for managing the config.json file.
|
||||||
|
Determines the full path to the config file based on folder and file arguments,
|
||||||
|
or uses the default static fileConfig if none are provided.
|
||||||
|
|
||||||
|
Handles Node.js packaged apps (process.pkg) by adjusting paths accordingly.
|
||||||
|
|
||||||
|
@args
|
||||||
|
folder (str) Optional folder where config.json will be stored. Defaults to 'www'.
|
||||||
|
file (str) Optional config file name. Defaults to static Storage.fileConfig.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage(envWebFolder, FILE_CFG);
|
||||||
|
*/
|
||||||
|
|
||||||
|
static fileConfig = path.resolve( process.cwd( ), 'www', 'config.json' );
|
||||||
|
|
||||||
|
constructor( folder, file )
|
||||||
|
{
|
||||||
|
this.folderWeb = folder || 'www';
|
||||||
|
this.fileConfig = file ? path.resolve( folder, file ) : Storage.fileConfig;
|
||||||
|
|
||||||
|
if ( process.pkg )
|
||||||
|
this.fileConfig = path.join( path.dirname( process.execPath ), this.folderWeb, this.fileConfig );
|
||||||
|
else
|
||||||
|
this.fileConfig = path.resolve( process.cwd( ), this.folderWeb, this.fileConfig );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize › Activate Config Setup with Logging
|
||||||
|
|
||||||
|
Activates the Storage.Setup( ) function while providing detailed logging.
|
||||||
|
Ensures the user's config.json file exists, is valid, and is initialized
|
||||||
|
with default values if missing or corrupt.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Logs the start of initialization.
|
||||||
|
- Calls Setup( ) with optional force flag to recreate config.
|
||||||
|
- Catches and logs any errors during setup.
|
||||||
|
|
||||||
|
@args
|
||||||
|
bForceNew (bool) Optional. If true, forces the config file to be removed
|
||||||
|
and regenerated from defaults.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(Promise) Resolves when initialization completes, or logs an error if setup fails.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage(envWebFolder, FILE_CFG);
|
||||||
|
await storage.Initialize(false);
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Initialize( bForceNew )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const bForce = bForceNew || false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.info( `conf`, chalk.yellow( `[initiate]` ), chalk.white( `ℹ️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Initializing config file` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
await new Storage( ).Setup( bForce );
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
console.log( 'Error writing Metadata.json:' + err.message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize › Setup User Config File
|
||||||
|
|
||||||
|
Sets up a user's config.json file, ensuring it exists and is valid JSON.
|
||||||
|
If the file is missing, empty, or invalid, it will be created or replaced.
|
||||||
|
Typically, you should call this via Storage( ).Initialize( ) rather than Setup( ) directly.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Creates parent directory if it doesn't exist.
|
||||||
|
- Removes existing config if bForceNew is true.
|
||||||
|
- Validates existing JSON; backs up invalid files.
|
||||||
|
- Creates default config if missing.
|
||||||
|
- Wires up nconf with argv, env, file, and default values.
|
||||||
|
|
||||||
|
@args
|
||||||
|
bForceNew (bool) Optional flag to force recreate the config file, wiping all existing data.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(Promise) Resolves true when initialization completes successfully.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage(envWebFolder, FILE_CFG);
|
||||||
|
await storage.Initialize(false);
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Setup( bForceNew )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return new Promise( ( resolve, reject ) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.info( `conf`, chalk.yellow( `[generate]` ), chalk.white( `ℹ️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Initializing storage setup` ),
|
||||||
|
chalk.blueBright( `<force>` ), chalk.gray( `${ bForceNew }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
ensure parent directory exists
|
||||||
|
*/
|
||||||
|
const dirPath = path.dirname( this.fileConfig );
|
||||||
|
|
||||||
|
if ( !fs.existsSync( dirPath ) )
|
||||||
|
{
|
||||||
|
fs.mkdirSync( dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if force flag is true, remove existing config file (force)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( bForceNew === true && fs.existsSync( this.fileConfig ) )
|
||||||
|
{
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[generate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Remove original config; force new` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.unlinkSync( this.fileConfig );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Failed to unlink existing config` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ e.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if config exists, validate JSON; if invalid, move to backup and recreate
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( fs.existsSync( this.fileConfig ) )
|
||||||
|
{
|
||||||
|
let raw = null;
|
||||||
|
let parsed = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
raw = fs.readFileSync( this.fileConfig, { encoding: 'utf8' });
|
||||||
|
|
||||||
|
if ( typeof raw !== 'string' || raw.trim( ).length === 0 )
|
||||||
|
{
|
||||||
|
throw new Error( 'Empty config file' );
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = JSON.parse( raw );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
const backupPath = `${ this.fileConfig }.corrupt.${ Date.now( ) }`;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.renameSync( this.fileConfig, backupPath );
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Config file invalid; moved to backup` ),
|
||||||
|
chalk.redBright( `<backup>` ), chalk.gray( `${ backupPath }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
}
|
||||||
|
catch ( renameErr )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Unable to backup invalid config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ renameErr.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
if ( this.rejected )
|
||||||
|
{
|
||||||
|
reject( renameErr );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if config does not exist (or was just moved because it was corrupt), create it atomically
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !fs.existsSync( this.fileConfig ) )
|
||||||
|
{
|
||||||
|
const defaults =
|
||||||
|
{
|
||||||
|
deviceId: 'FFFFFFFF'
|
||||||
|
};
|
||||||
|
|
||||||
|
const tempPath = `${ this.fileConfig }.tmp`;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.writeFileSync( tempPath, JSON.stringify( defaults, null, 4 ), { encoding: 'utf8' });
|
||||||
|
fs.renameSync( tempPath, this.fileConfig );
|
||||||
|
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[generate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Created new config file with defaults` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
}
|
||||||
|
catch ( writeErr )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Failed to create config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ writeErr.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
if ( this.rejected )
|
||||||
|
{
|
||||||
|
reject( writeErr );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
now that file exists and is valid JSON, wire up nconf
|
||||||
|
*/
|
||||||
|
|
||||||
|
nconf.argv( ).env({ parseValues: true }).file({ file: this.fileConfig }).defaults(
|
||||||
|
{
|
||||||
|
deviceId: 'FFFFFFFF'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not generate and write to new config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
if ( this.rejected )
|
||||||
|
{
|
||||||
|
reject( err );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve( true );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get › Retrieve Configuration Value
|
||||||
|
|
||||||
|
Fetches a stored value from the application's persistent configuration
|
||||||
|
using the provided key via the nconf module.
|
||||||
|
|
||||||
|
This function is static, so it can be called without creating a Storage instance.
|
||||||
|
|
||||||
|
@args
|
||||||
|
key (str) The configuration key to retrieve.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(any) The value associated with the key, or undefined if the key does not exist.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const deviceId = Storage.Get('deviceId');
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Get( key )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return nconf.get( key );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set › Store Configuration Value
|
||||||
|
|
||||||
|
Stores a value in the application's persistent configuration using
|
||||||
|
the provided key via the nconf module. Automatically saves the
|
||||||
|
updated configuration to disk by calling Storage.Save( ).
|
||||||
|
|
||||||
|
This function is static, so it can be called without creating a Storage instance.
|
||||||
|
|
||||||
|
@args
|
||||||
|
key (str) The configuration key to set.
|
||||||
|
value (any) The value to store under the specified key.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(void) No return value.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
Storage.Set('deviceId', '105B35EF');
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Set( key, value )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ),
|
||||||
|
chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
nconf.set( key, value );
|
||||||
|
Storage.Save( );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Save › Persist Configuration to Disk
|
||||||
|
|
||||||
|
Saves the current configuration stored in nconf to disk.
|
||||||
|
After saving, the method reads back the file to verify it is valid JSON
|
||||||
|
and logs detailed status messages about success or errors.
|
||||||
|
|
||||||
|
@purpose
|
||||||
|
- Calls nconf.save() to write the current configuration.
|
||||||
|
- Reads back the saved file.
|
||||||
|
- Parses the file as JSON to confirm validity.
|
||||||
|
- Logs success or detailed error messages for failures.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(void) Logs success or error; does not return a value.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
Storage.Save();
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Save( )
|
||||||
|
{
|
||||||
|
const filePath = this.fileConfig;
|
||||||
|
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ),
|
||||||
|
chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
nconf.save( ( err ) =>
|
||||||
|
{
|
||||||
|
if ( err )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[snapshot]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not save config` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile( filePath, ( err, data ) =>
|
||||||
|
{
|
||||||
|
if ( err )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[snapshot]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Unable to read config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const parsed = JSON.parse( data.toString( ) );
|
||||||
|
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[snapshot]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Save to config file successful` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
|
||||||
|
Log.debug( `conf`, chalk.yellow( `[snapshot]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Read values from saved config file` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ filePath }` ),
|
||||||
|
chalk.blueBright( `<values>` ), chalk.gray( `${ JSON.stringify( parsed ) }` ) );
|
||||||
|
}
|
||||||
|
catch ( parseErr )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[snapshot]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Config file is not valid JSON` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ parseErr.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetConfig › Return Full Path to Config File
|
||||||
|
|
||||||
|
Returns the full path to the currently used config.json file for this Storage instance.
|
||||||
|
This is useful when you need to know the exact file location without reading its contents.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) Absolute path to the config.json file.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage_config = Storage.GetConfig();
|
||||||
|
*/
|
||||||
|
|
||||||
|
static GetConfig( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return this.fileConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
isJsonString › Check if Input is Valid JSON
|
||||||
|
|
||||||
|
Determines whether a given string is valid JSON by attempting
|
||||||
|
to parse it. Returns true if parsing succeeds, false if it throws
|
||||||
|
an error.
|
||||||
|
|
||||||
|
@args
|
||||||
|
json (str) The string to test for valid JSON.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(bool) True if input is valid JSON, false otherwise.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const valid = Storage.isJsonString('{"key":"value"}'); // returns true
|
||||||
|
*/
|
||||||
|
|
||||||
|
static isJsonString( json )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JSON.parse( json );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
helper › json object empty
|
||||||
|
*/
|
||||||
|
|
||||||
|
static isJsonEmpty( json )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
if ( Object.keys( json ).length === 0 )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ( JSON.stringify( json ) === '\"{}\"' )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for ( const key in json )
|
||||||
|
{
|
||||||
|
if ( ! Object.prototype.hasOwnProperty.call( json, key ) )
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@import
|
||||||
|
import Storage from './classes/Storage.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Storage;
|
||||||
455
tvapp2/classes/Tuner.js
Normal file
455
tvapp2/classes/Tuner.js
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
/*
|
||||||
|
Class › Tuner
|
||||||
|
|
||||||
|
Handles HDHomeRun device management and deviceId lifecycle.
|
||||||
|
|
||||||
|
@purpose
|
||||||
|
- Generate / format HDHomeRun device IDs.
|
||||||
|
- Validate device IDs against HDHomeRun rules (length, hex chars, checksum).
|
||||||
|
- Persist device IDs using Storage class.
|
||||||
|
- Automatically generate new device ID if missing, invalid, or uninitialized (FFFFFFFF).
|
||||||
|
- Initialize tuner instances with validated device IDs.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
await new Tuner( Storage.Get( 'deviceId' ) ).Initialize( );
|
||||||
|
const tuner = new Tuner( );
|
||||||
|
await tuner.Initialize( );
|
||||||
|
const validId = await tuner.VerifyDeviceId( );
|
||||||
|
|
||||||
|
@notes
|
||||||
|
- Device IDs are persisted via the Storage class (config.json).
|
||||||
|
- User's device id must be valid before HDHomeRun will initialize.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import Storage from './Storage.js';
|
||||||
|
import Utils from './Utils.js';
|
||||||
|
import Log from './Log.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class › Tuner
|
||||||
|
|
||||||
|
constructor ( str:deviceId )
|
||||||
|
Initialize ( )
|
||||||
|
Start ( )
|
||||||
|
_GenerateDeviceId ( int:len )
|
||||||
|
GenerateDeviceId ( )
|
||||||
|
GetDeviceId ( )
|
||||||
|
FormatDeviceId ( str:deviceid )
|
||||||
|
IsDeviceIdValid ( )
|
||||||
|
VerifyDeviceId ( )
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Tuner
|
||||||
|
{
|
||||||
|
constructor( deviceId )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getConstructorName( ) }` ) );
|
||||||
|
|
||||||
|
this.Name = `HDHomeRun`;
|
||||||
|
this.FriendlyName = `TVApp2`;
|
||||||
|
this.ModelNumber = `HDHR5-4US`;
|
||||||
|
this.FirmwareName = `hdhomerun5_atsc`;
|
||||||
|
this.FirmwareVersion = `0.9.15.00-RC04`;
|
||||||
|
this.SlotsConnected = 0;
|
||||||
|
this.SlotsMax = 10;
|
||||||
|
this.DeviceId = deviceId || Storage.Get( 'deviceId' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize › Setup and Start Tuner
|
||||||
|
|
||||||
|
Initializes the tuner by calling the Start( ) method.
|
||||||
|
Catches and logs any errors encountered during startup.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(void) Logs status; does not return a value.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
await tuner.Initialize( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Initialize( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.Start( );
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[initiate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Failure initializing tuner` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Start › Initialize and Verify Device ID
|
||||||
|
|
||||||
|
Starts the tuner by verifying the current deviceId.
|
||||||
|
If the deviceId is missing or invalid, it will be regenerated and validated.
|
||||||
|
Logs the status of the deviceId once verification completes.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(bool) true if deviceId is valid after verification, false otherwise.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
await tuner.Start( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Start( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const verifiedId = await new Tuner( ).VerifyDeviceId( this.DeviceId );
|
||||||
|
|
||||||
|
if ( await this.IsDeviceIdValid( verifiedId ) )
|
||||||
|
{
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[validate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `User has valid deviceId` ),
|
||||||
|
chalk.greenBright( `<deviceId>` ), chalk.gray( `${ verifiedId }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_GenerateDeviceId › Generate Raw Random Hexadecimal String
|
||||||
|
|
||||||
|
Generates a raw random hexadecimal string using Node.js crypto module.
|
||||||
|
This is typically used as the random portion of a deviceId.
|
||||||
|
|
||||||
|
@args
|
||||||
|
len (int) Optional number of bytes to generate. Defaults to 4 bytes.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) Uppercase hexadecimal string, length = len * 2 characters.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const randomHex = Tuner._GenerateDeviceId( 4 ); // 8-character hex string
|
||||||
|
*/
|
||||||
|
|
||||||
|
static _GenerateDeviceId( len )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return crypto.randomBytes( len || 4 ).toString( 'hex' ).toUpperCase( );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GenerateDeviceId › Generate New HDHomeRun Device ID
|
||||||
|
|
||||||
|
Generates a new, properly formatted HDHomeRun deviceId.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Generates 4 random hexadecimal characters.
|
||||||
|
- Prepends '105' and appends '0' to form base deviceId.
|
||||||
|
- Passes baseId to Tuner.FormatDeviceId( ) to ensure correct checksum and 8-character format.
|
||||||
|
|
||||||
|
@args
|
||||||
|
None
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) A valid, 8-character HDHomeRun deviceId in uppercase hexadecimal.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const newDeviceId = Tuner.GenerateDeviceId( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
static GenerateDeviceId( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const chars = '0123456789ABCDEF';
|
||||||
|
let randomHex = '';
|
||||||
|
|
||||||
|
// generate 4 random hexadecimal chars
|
||||||
|
for ( let i = 0;i < 4;i++ )
|
||||||
|
{
|
||||||
|
randomHex += chars[Math.floor( Math.random( ) * chars.length )];
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseId = '105' + randomHex + '0';
|
||||||
|
return this.FormatDeviceId( baseId );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetDeviceId › Retrieve Stored HDHomeRun Device ID
|
||||||
|
|
||||||
|
Fetches the current deviceId from persistent storage (via Storage.Get).
|
||||||
|
|
||||||
|
@args
|
||||||
|
None
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) The current deviceId stored in configuration.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const deviceId = await tuner.GetDeviceId( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
GetDeviceId( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return Storage.Get( 'deviceId' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FormatDeviceId › Validate and Format HDHomeRun Device ID
|
||||||
|
|
||||||
|
Fetches the provided deviceId (or instance default) and ensures it is valid
|
||||||
|
according to HDHomeRun rules, then returns a properly formatted ID.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Input must be exactly 8 hexadecimal characters.
|
||||||
|
- All characters must be 0-9 or A-F/a-f.
|
||||||
|
- Computes checksum using HDHomeRun-specific lookup table.
|
||||||
|
- Generates a new deviceId integer with checksum applied.
|
||||||
|
- Converts back to 8-character uppercase hexadecimal string.
|
||||||
|
|
||||||
|
Logs detailed errors if the input deviceId is invalid.
|
||||||
|
|
||||||
|
@args
|
||||||
|
deviceid (str) Optional deviceId to format. Defaults to instance deviceId.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str|int) Formatted 8-character hex deviceId, or 0 if input invalid.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const formattedId = Tuner.FormatDeviceId( someDeviceId );
|
||||||
|
*/
|
||||||
|
|
||||||
|
static FormatDeviceId( deviceid )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const deviceId = deviceid || this.DeviceId;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Validate input length
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !deviceId || deviceId.length !== 8 )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must be 8 hexadecimals` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
All chars should be valid hexadecimal
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hexPattern = /^[0-9A-Fa-f]+$/;
|
||||||
|
if ( !hexPattern.test( deviceId ) )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must contain all hex (0-9, A-F, a-f)` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hex string to integer
|
||||||
|
*/
|
||||||
|
|
||||||
|
const deviceIdInt = parseInt( deviceId, 16 );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checksum lookup table
|
||||||
|
*/
|
||||||
|
|
||||||
|
const checksumLookup =
|
||||||
|
[
|
||||||
|
0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calc checksum
|
||||||
|
*/
|
||||||
|
|
||||||
|
let checksum = 0;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 28 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >> 24 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 20 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >> 16 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 12 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >> 8 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 4 ) & 0x0F];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calc new device ID
|
||||||
|
*/
|
||||||
|
|
||||||
|
const newDevId = ( deviceIdInt & 0xFFFFFFF0 ) + checksum;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert back to hex string; ensure we get 8 characters with leading zeros; convert to uppercase
|
||||||
|
*/
|
||||||
|
|
||||||
|
return newDevId.toString( 16 ).toUpperCase( ).padStart( 8, '0' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IsDeviceIdValid › Validate HDHomeRun Device ID
|
||||||
|
|
||||||
|
Checks if the current deviceId on this instance is valid according to HDHomeRun rules.
|
||||||
|
|
||||||
|
Validation steps:
|
||||||
|
- Must be exactly 8 characters long.
|
||||||
|
- All characters must be hexadecimal (0-9, A-F, a-f).
|
||||||
|
- Computes checksum using HDHomeRun-specific lookup table; must equal 0.
|
||||||
|
|
||||||
|
Logs detailed errors if the deviceId fails any validation step.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(bool) true if deviceId is valid, false otherwise.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const isValid = await tuner.IsDeviceIdValid( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async IsDeviceIdValid( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define Hexadecimal charset (0-9, A-F, a-f)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hexDigits = new Set( '0123456789ABCDEFabcdef' );
|
||||||
|
const deviceId = this.DeviceId;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Check if device ID is exactly 8 characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !deviceId || deviceId.length !== 8 )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must be 8 hexadecimals` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Check if all characters are hexadecimal
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !Array.from( deviceId ).every( ( c ) => hexDigits.has( c ) ) )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must contain all hex (0-A)` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert hex string to integer (equivalent to int.from_bytes with big endian)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const deviceIdInt = parseInt( deviceId, 16 );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checksum lookup table
|
||||||
|
*/
|
||||||
|
|
||||||
|
const checksumLookup =
|
||||||
|
[
|
||||||
|
0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calc checksum
|
||||||
|
*/
|
||||||
|
|
||||||
|
let checksum = 0;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 28 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 24 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 20 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 16 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 12 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 8 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 4 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 0 ) & 0x0F;
|
||||||
|
|
||||||
|
return checksum === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
VerifyDeviceId › Validate / Generate Device ID
|
||||||
|
|
||||||
|
Checks if the current deviceId on this instance is valid.
|
||||||
|
|
||||||
|
If missing, uninitialized ('FFFFFFFF'), or fails validation:
|
||||||
|
a new deviceId is generated via the static Tuner.GenerateDeviceId( ) method.
|
||||||
|
|
||||||
|
New deviceId is saved to persistent storage via Storage.Set( ) and
|
||||||
|
updated on the instance.
|
||||||
|
|
||||||
|
Function also recursively verifies until a valid deviceId is established.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) A valid deviceId for this tuner instance.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const validId = await tuner.VerifyDeviceId( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async VerifyDeviceId( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const deviceId = this.DeviceId;
|
||||||
|
|
||||||
|
if ( !deviceId || deviceId === 'FFFFFFFF' || !await this.IsDeviceIdValid( ) )
|
||||||
|
{
|
||||||
|
const deviceIdNew = Tuner.GenerateDeviceId( ); // static generates a properly formatted ID
|
||||||
|
if ( deviceId === 'FFFFFFFF' )
|
||||||
|
{
|
||||||
|
Log.info( `conf`, chalk.yellow( `[generate]` ), chalk.white( `📣` ),
|
||||||
|
chalk.yellow( `<msg>` ), chalk.gray( `Generating HDHomeRun deviceId for the first time` ),
|
||||||
|
chalk.yellow( `<deviceId>` ), chalk.gray( `${ deviceIdNew }` ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Invalid deviceId; generating new` ),
|
||||||
|
chalk.redBright( `<oldDeviceId>` ), chalk.gray( `${ deviceId }` ),
|
||||||
|
chalk.redBright( `<deviceIdNew>` ), chalk.gray( `${ deviceIdNew }` ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.Set( 'deviceId', deviceIdNew ); // save to JSON via nconf
|
||||||
|
this.DeviceId = deviceIdNew; // update the instance so validation works
|
||||||
|
|
||||||
|
// verify recursively until valid
|
||||||
|
const verifiedId = await this.VerifyDeviceId( );
|
||||||
|
return verifiedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@image
|
||||||
|
import Tuner from './classes/Tuner.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Tuner;
|
||||||
47
tvapp2/classes/Utils.js
Normal file
47
tvapp2/classes/Utils.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
class Utils
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Returns the name of the function that this function was called from.
|
||||||
|
used for Log.verbose
|
||||||
|
*/
|
||||||
|
|
||||||
|
static getFuncName()
|
||||||
|
{
|
||||||
|
return ( new Error() ).stack.match( /at (\S+)/g )[1].slice( 3 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns the name of the constructor that this function was called from.
|
||||||
|
used for Log.verbose
|
||||||
|
*/
|
||||||
|
|
||||||
|
static getConstructorName()
|
||||||
|
{
|
||||||
|
return ( new Error() ).stack.match( /new\s+(\w+)/g )[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
helper > str2bool
|
||||||
|
*/
|
||||||
|
|
||||||
|
static str2bool( str )
|
||||||
|
{
|
||||||
|
if ( typeof str === 'string' )
|
||||||
|
{
|
||||||
|
const lower = str.toLowerCase();
|
||||||
|
if ([
|
||||||
|
'1', 'true', 'yes', 'y', 't'
|
||||||
|
].includes( lower ) )
|
||||||
|
str = true;
|
||||||
|
if ([
|
||||||
|
'0', 'false', 'no', 'n', 'f'
|
||||||
|
].includes( lower ) )
|
||||||
|
str = false;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
else return Boolean( str );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Utils;
|
||||||
@@ -223,7 +223,7 @@ export default
|
|||||||
'@stylistic/no-whitespace-before-property': ['error'],
|
'@stylistic/no-whitespace-before-property': ['error'],
|
||||||
'@stylistic/object-curly-spacing': ['error', 'always'],
|
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||||
'@stylistic/quote-props': ['error', 'as-needed'],
|
'@stylistic/quote-props': ['error', 'as-needed'],
|
||||||
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always' }],
|
||||||
'@stylistic/semi': ['error', 'always'],
|
'@stylistic/semi': ['error', 'always'],
|
||||||
'@stylistic/space-infix-ops': ['error'],
|
'@stylistic/space-infix-ops': ['error'],
|
||||||
'@stylistic/template-curly-spacing': ['error', 'always'],
|
'@stylistic/template-curly-spacing': ['error', 'always'],
|
||||||
|
|||||||
689
tvapp2/index.js
689
tvapp2/index.js
@@ -17,8 +17,12 @@ import ejs from 'ejs';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import TimeAgo from 'javascript-time-ago';
|
import TimeAgo from 'javascript-time-ago';
|
||||||
import en from 'javascript-time-ago/locale/en';
|
import en from 'javascript-time-ago/locale/en';
|
||||||
import nconf from 'nconf';
|
import Log from './classes/Log.js';
|
||||||
import crypto from 'node:crypto';
|
import Storage from './classes/Storage.js';
|
||||||
|
import Utils from './classes/Utils.js';
|
||||||
|
import CLib from './classes/CLib.js';
|
||||||
|
import Semaphore from './classes/Semaphore.js';
|
||||||
|
import Tuner from './classes/Tuner.js';
|
||||||
import cron, { schedule } from 'node-cron';
|
import cron, { schedule } from 'node-cron';
|
||||||
import * as child from 'child_process';
|
import * as child from 'child_process';
|
||||||
import * as crons from 'cron';
|
import * as crons from 'cron';
|
||||||
@@ -28,17 +32,27 @@ import * as crons from 'cron';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize classes
|
||||||
|
*/
|
||||||
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
const clib = new CLib();
|
||||||
|
|
||||||
|
const encoded = clib.encodeToHexBase64( 'tvapp2' );
|
||||||
|
const decoded = clib.decodeFromHexBase64( `${ encoded }` );
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Import package.json values
|
Import package.json values
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { name, author, version, repository, discord, docs } = JSON.parse( fs.readFileSync( './package.json' ) );
|
const { name, author, version, repository, discord, docs } = JSON.parse( fs.readFileSync( './package.json' ) );
|
||||||
const __filename = fileURLToPath( import.meta.url ); // get resolved path to file
|
const __filename = fileURLToPath( import.meta.url ); // get resolved path to file
|
||||||
const __dirname = path.dirname( __filename ); // get name of directory
|
const __dirname = path.dirname( __filename ); // get name of directory
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const gitHash = child.execSync( 'git rev-parse HEAD' ).toString().trim();
|
const gitHash = child.execSync( 'git rev-parse HEAD' ).toString().trim();
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -58,19 +72,20 @@ const gitHash = child.execSync( 'git rev-parse HEAD' ).toString().trim();
|
|||||||
chalk.level = 3;
|
chalk.level = 3;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
timeAgo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
TimeAgo.addDefaultLocale( en );
|
TimeAgo.addDefaultLocale( en );
|
||||||
const timeAgo = new TimeAgo( );
|
const timeAgo = new TimeAgo( );
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Define > General
|
Define › General
|
||||||
|
|
||||||
@note if you change `envWebFolder`; ensure you re-name the folder where the
|
@note if you change `envWebFolder`; ensure you re-name the folder where the
|
||||||
website assets are stored.
|
website assets are stored.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
let FILE_CFG;
|
||||||
let FILE_URL;
|
let FILE_URL;
|
||||||
let FILE_M3U;
|
let FILE_M3U;
|
||||||
let FILE_XML;
|
let FILE_XML;
|
||||||
@@ -83,11 +98,12 @@ let FILE_XML_MODIFIED = 0;
|
|||||||
let FILE_GZP_MODIFIED = 0;
|
let FILE_GZP_MODIFIED = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Define > Environment Variables || Defaults
|
Define › Environment Variables || Defaults
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const envAppRelease = process.env.RELEASE || 'stable';
|
const envAppRelease = process.env.RELEASE || 'stable';
|
||||||
const envUrlRepo = process.env.URL_REPO || 'https://git.binaryninja.net/binaryninja';
|
const envUrlRepo = process.env.URL_REPO || 'https://git.binaryninja.net/binaryninja';
|
||||||
|
const envXmlEpg = process.env.URL_EPG || 'https://epg.binaryninja.net/XMLTV-EPG';
|
||||||
const envStreamQuality = process.env.STREAM_QUALITY || 'hd';
|
const envStreamQuality = process.env.STREAM_QUALITY || 'hd';
|
||||||
const envFileURL = process.env.FILE_URL || 'urls.txt';
|
const envFileURL = process.env.FILE_URL || 'urls.txt';
|
||||||
const envFileM3U = process.env.FILE_M3U || 'playlist.m3u8';
|
const envFileM3U = process.env.FILE_M3U || 'playlist.m3u8';
|
||||||
@@ -97,6 +113,7 @@ const envApiKey = process.env.API_KEY || null;
|
|||||||
const envWebIP = process.env.WEB_IP || '0.0.0.0';
|
const envWebIP = process.env.WEB_IP || '0.0.0.0';
|
||||||
const envWebPort = process.env.WEB_PORT || `4124`;
|
const envWebPort = process.env.WEB_PORT || `4124`;
|
||||||
const envWebFolder = process.env.WEB_FOLDER || 'www';
|
const envWebFolder = process.env.WEB_FOLDER || 'www';
|
||||||
|
const envHdhrPort = process.env.HDHR_PORT || `6077`;
|
||||||
const envWebEncoding = process.env.WEB_ENCODING || 'deflate, br';
|
const envWebEncoding = process.env.WEB_ENCODING || 'deflate, br';
|
||||||
const envProxyHeader = process.env.WEB_PROXY_HEADER || 'x-forwarded-for';
|
const envProxyHeader = process.env.WEB_PROXY_HEADER || 'x-forwarded-for';
|
||||||
const envHealthTimer = process.env.HEALTH_TIMER || 600000;
|
const envHealthTimer = process.env.HEALTH_TIMER || 600000;
|
||||||
@@ -112,15 +129,16 @@ let serverOs = 'Unknown';
|
|||||||
let serverStartup = 0;
|
let serverStartup = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Define > Externals
|
Define › Externals
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const extURL = `${ envUrlRepo }/tvapp2-externals/raw/branch/main/urls.txt`;
|
const extURL = `${ envUrlRepo }/tvapp2-externals/raw/branch/main/urls.txt`;
|
||||||
const extXML = `${ envUrlRepo }/XMLTV-EPG/raw/branch/main/xmltv.1.xml`;
|
const extXML = `${ envXmlEpg }/xmltv_v2.0.0.xml`;
|
||||||
const extM3U = `${ envUrlRepo }/tvapp2-externals/raw/branch/main/formatted.dat`;
|
const extM3U = `${ envXmlEpg }/formatted_v2.0.0.dat`;
|
||||||
|
//const extM3U = `${ envUrlRepo }/tvapp2-externals/raw/branch/main/formatted.dat`;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Define > Defaults
|
Define › Defaults
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let urls = [];
|
let urls = [];
|
||||||
@@ -132,12 +150,12 @@ const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/201
|
|||||||
|
|
||||||
using any of the following subdomains / subpaths will trigger the download for that specific file
|
using any of the following subdomains / subpaths will trigger the download for that specific file
|
||||||
|
|
||||||
@example http://127.0.0.1:4124/gzip
|
@example http://127.0.0.1:4124/gzip
|
||||||
http://127.0.0.1:4124/gz
|
http://127.0.0.1:4124/gz
|
||||||
http://127.0.0.1:4124/playlist
|
http://127.0.0.1:4124/playlist
|
||||||
http://127.0.0.1:4124/key
|
http://127.0.0.1:4124/key
|
||||||
http://127.0.0.1:4124/channel?url=https://thetvapp.to/tv/bbc-america-live-stream/
|
http://127.0.0.1:4124/channel?url=https://thetvapp.to/tv/bbc-america-live-stream/
|
||||||
http://127.0.0.1:4124/api/health
|
http://127.0.0.1:4124/api/health
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const subdomainGZP = [ 'gzip', 'gz' ];
|
const subdomainGZP = [ 'gzip', 'gz' ];
|
||||||
@@ -159,6 +177,19 @@ const fileIpContainer = '/var/run/s6/container_environment/IP_CONTAINER';
|
|||||||
const envIpGateway = fs.existsSync( fileIpGateway ) ? fs.readFileSync( fileIpGateway, 'utf8' ) : `0.0.0.0`;
|
const envIpGateway = fs.existsSync( fileIpGateway ) ? fs.readFileSync( fileIpGateway, 'utf8' ) : `0.0.0.0`;
|
||||||
const envIpContainer = fs.existsSync( fileIpContainer ) ? fs.readFileSync( fileIpContainer, 'utf8' ) : `0.0.0.0`;
|
const envIpContainer = fs.existsSync( fileIpContainer ) ? fs.readFileSync( fileIpContainer, 'utf8' ) : `0.0.0.0`;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hosts
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hosts =
|
||||||
|
[
|
||||||
|
{ name: 'TVPass.org', url: 'https://tvpass.org' },
|
||||||
|
{ name: 'TheTVApp.to', url: 'https://thetvapp.to' },
|
||||||
|
{ name: 'MoveOnJoy.com', url: 'http://moveonjoy.com' },
|
||||||
|
{ name: 'Daddylive.dad', url: 'https://daddylivestream.com' },
|
||||||
|
{ name: 'git.binaryninja.net', url: envUrlRepo }
|
||||||
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Get Server OS
|
Get Server OS
|
||||||
|
|
||||||
@@ -188,111 +219,6 @@ getos( ( e, json ) =>
|
|||||||
return serverOs;
|
return serverOs;
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
helper > str2bool
|
|
||||||
*/
|
|
||||||
|
|
||||||
function str2bool( str )
|
|
||||||
{
|
|
||||||
if ( typeof str === 'string' )
|
|
||||||
{
|
|
||||||
const lower = str.toLowerCase();
|
|
||||||
if ([
|
|
||||||
'1', 'true', 'yes', 'y', 't'
|
|
||||||
].includes( lower ) )
|
|
||||||
str = true;
|
|
||||||
if ([
|
|
||||||
'0', 'false', 'no', 'n', 'f'
|
|
||||||
].includes( lower ) )
|
|
||||||
str = false;
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
else return Boolean( str );
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Define > Logs
|
|
||||||
|
|
||||||
When assigning text colors, terminals and the windows command prompt can display any color; however apps
|
|
||||||
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
|
|
||||||
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
|
|
||||||
|
|
||||||
Various levels of logs with the following usage:
|
|
||||||
Log.verbose(`This is verbose`)
|
|
||||||
Log.debug(`This is debug`)
|
|
||||||
Log.info(`This is info`)
|
|
||||||
Log.ok(`This is ok`)
|
|
||||||
Log.notice(`This is notice`)
|
|
||||||
Log.warn(`This is warn`)
|
|
||||||
Log.error(
|
|
||||||
`Error fetching sports data with error:`,
|
|
||||||
chalk.white(`→`),
|
|
||||||
chalk.grey(`This is the error message`)
|
|
||||||
);
|
|
||||||
|
|
||||||
Level Type
|
|
||||||
-----------------------------------
|
|
||||||
6 Trace
|
|
||||||
5 Debug
|
|
||||||
4 Info
|
|
||||||
3 Notice
|
|
||||||
2 Warn
|
|
||||||
1 Error
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Log
|
|
||||||
{
|
|
||||||
static now()
|
|
||||||
{
|
|
||||||
const now = new Date();
|
|
||||||
return chalk.gray( `[${ now.toLocaleTimeString() }]` );
|
|
||||||
}
|
|
||||||
|
|
||||||
static verbose( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 6 )
|
|
||||||
console.debug( chalk.white.bgBlack.blackBright.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
static debug( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 7 )
|
|
||||||
console.trace( chalk.white.bgMagenta.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.magentaBright( msg.join( ' ' ) ) );
|
|
||||||
else if ( LOG_LEVEL >= 5 )
|
|
||||||
console.debug( chalk.white.bgGray.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
static info( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 4 )
|
|
||||||
console.info( chalk.white.bgBlueBright.bold( ` ${ name } ` ), chalk.white( `ℹ️` ), this.now(), chalk.blueBright( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
static ok( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 4 )
|
|
||||||
console.log( chalk.white.bgGreen.bold( ` ${ name } ` ), chalk.white( `✅` ), this.now(), chalk.greenBright( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
static notice( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 3 )
|
|
||||||
console.log( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `📌` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
static warn( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 2 )
|
|
||||||
console.warn( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `⚠️` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
static error( ...msg )
|
|
||||||
{
|
|
||||||
if ( LOG_LEVEL >= 1 )
|
|
||||||
console.error( chalk.white.bgRedBright.bold( ` ${ name } ` ), chalk.white( `❌` ), this.now(), chalk.redBright( msg.join( ' ' ) ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Process
|
Process
|
||||||
*/
|
*/
|
||||||
@@ -303,6 +229,7 @@ if ( process.pkg )
|
|||||||
chalk.blueBright( `<msg>` ), chalk.gray( `Starting server utilizing process.execPath` ) );
|
chalk.blueBright( `<msg>` ), chalk.gray( `Starting server utilizing process.execPath` ) );
|
||||||
|
|
||||||
const basePath = path.dirname( process.execPath );
|
const basePath = path.dirname( process.execPath );
|
||||||
|
FILE_CFG = path.join( basePath, envWebFolder, `config.json` );
|
||||||
FILE_URL = path.join( basePath, envWebFolder, `${ envFileURL }` );
|
FILE_URL = path.join( basePath, envWebFolder, `${ envFileURL }` );
|
||||||
FILE_M3U = path.join( basePath, envWebFolder, `${ envFileM3U }` );
|
FILE_M3U = path.join( basePath, envWebFolder, `${ envFileM3U }` );
|
||||||
FILE_XML = path.join( basePath, envWebFolder, `${ envFileXML }` );
|
FILE_XML = path.join( basePath, envWebFolder, `${ envFileXML }` );
|
||||||
@@ -314,6 +241,7 @@ else
|
|||||||
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `ℹ️` ),
|
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `ℹ️` ),
|
||||||
chalk.blueBright( `<msg>` ), chalk.gray( `Starting server utilizing processed locals` ) );
|
chalk.blueBright( `<msg>` ), chalk.gray( `Starting server utilizing processed locals` ) );
|
||||||
|
|
||||||
|
FILE_CFG = path.resolve( __dirname, envWebFolder, `config.json` );
|
||||||
FILE_URL = path.resolve( __dirname, envWebFolder, `${ envFileURL }` );
|
FILE_URL = path.resolve( __dirname, envWebFolder, `${ envFileURL }` );
|
||||||
FILE_M3U = path.resolve( __dirname, envWebFolder, `${ envFileM3U }` );
|
FILE_M3U = path.resolve( __dirname, envWebFolder, `${ envFileM3U }` );
|
||||||
FILE_XML = path.resolve( __dirname, envWebFolder, `${ envFileXML }` );
|
FILE_XML = path.resolve( __dirname, envWebFolder, `${ envFileXML }` );
|
||||||
@@ -321,7 +249,7 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
helper > sleep
|
helper › sleep
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function sleep( ms )
|
function sleep( ms )
|
||||||
@@ -333,45 +261,7 @@ function sleep( ms )
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Semaphore > Declare
|
Semaphore › Initialize
|
||||||
|
|
||||||
allows multiple threads to work with the same shared resources
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Semaphore
|
|
||||||
{
|
|
||||||
constructor( max )
|
|
||||||
{
|
|
||||||
this.max = max;
|
|
||||||
this.queue = [];
|
|
||||||
this.active = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquire()
|
|
||||||
{
|
|
||||||
if ( this.active < this.max )
|
|
||||||
{
|
|
||||||
this.active++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise( ( resolve ) => this.queue.push( resolve ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
release()
|
|
||||||
{
|
|
||||||
this.active--;
|
|
||||||
if ( this.queue.length > 0 )
|
|
||||||
{
|
|
||||||
const resolve = this.queue.shift();
|
|
||||||
this.active++;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Semaphore > Initialize
|
|
||||||
|
|
||||||
@arg int threads_max
|
@arg int threads_max
|
||||||
*/
|
*/
|
||||||
@@ -404,90 +294,94 @@ const clientIp = ( req ) =>
|
|||||||
if try 2 fails with the opposite protocol; domain is considered down
|
if try 2 fails with the opposite protocol; domain is considered down
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function serviceCheck( service, uri )
|
async function hostCheck( service, uri )
|
||||||
{
|
{
|
||||||
/* try 1 */
|
/* try 1 */
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const resp = await fetch( uri );
|
const resp = await fetch( uri );
|
||||||
|
|
||||||
/* try 1 > domain down */
|
/* try 1 › domain down */
|
||||||
if ( resp.status !== 200 )
|
if ( resp.status !== 200 )
|
||||||
{
|
{
|
||||||
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ) );
|
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Try: Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ) );
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* try 1 > domain up */
|
/* try 1 › domain up */
|
||||||
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `<msg>` ), chalk.gray( `Service Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uri }` ) );
|
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `<msg>` ), chalk.gray( `Domain Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uri }` ) );
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
try 2 > http
|
try 2 › https
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( /^https:\/\//i.test( uri ) )
|
if ( /^https:\/\//i.test( uri ) )
|
||||||
{
|
{
|
||||||
const uriRetry = uri.replace( /^https:\/\//ig, 'http://' );
|
const uriRetry = uri.replace( /^https:\/\//ig, 'http://' );
|
||||||
Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service via SSL; trying http protocol` ), chalk.yellowBright( `<service>` ), chalk.gray( `${ service }` ), chalk.yellowBright( `<uriAttempt1>` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `<uriAttempt2>` ), chalk.gray( `${ uriRetry }` ), chalk.blueBright( `(pending)` ) );
|
Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `<msg>` ), chalk.gray( `Try: Failed via HTTPS; trying HTTP protocol` ), chalk.yellowBright( `<service>` ), chalk.gray( `${ service }` ), chalk.yellowBright( `<uriAttempt1>` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `<uriAttempt2>` ), chalk.gray( `${ uriRetry }` ), chalk.blueBright( `(pending)` ) );
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const resp = await fetch( uriRetry );
|
const resp = await fetch( uriRetry );
|
||||||
|
|
||||||
/* try 2 > http > domain down */
|
/* try 2 › https › domain down */
|
||||||
if ( resp.status !== 200 )
|
if ( resp.status !== 200 )
|
||||||
{
|
{
|
||||||
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Try: Domain Offline; failed to communicate with domain, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* try 2 > http > domain up */
|
/* try 2 › https › domain up */
|
||||||
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `<msg>` ), chalk.gray( `Service Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `<msg>` ), chalk.gray( `Domain Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
/* try 2 > http > domain not exist */
|
/* try 2 › https › domain not exist */
|
||||||
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, address does not exist` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ), chalk.redBright( `<message>` ), chalk.gray( `${ err }` ) );
|
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Try: Domain Offline; failed to communicate with domain, address does not exist` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ), chalk.redBright( `<message>` ), chalk.gray( `${ err }` ) );
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
try 2 > https
|
try 2 › http
|
||||||
*/
|
*/
|
||||||
|
|
||||||
else if ( /^http:\/\//i.test( uri ) )
|
else if ( /^http:\/\//i.test( uri ) )
|
||||||
{
|
{
|
||||||
const uriRetry = uri.replace( /^http:\/\//ig, 'https://' );
|
const uriRetry = uri.replace( /^http:\/\//ig, 'https://' );
|
||||||
Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service via SSL; trying https protocol` ), chalk.yellowBright( `<service>` ), chalk.gray( `${ service }` ), chalk.yellowBright( `<uriAttempt1>` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `<uriAttempt2>` ), chalk.gray( `${ uriRetry }` ), chalk.blueBright( `(pending)` ) );
|
Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `<msg>` ), chalk.gray( `Try: Failed via HTTP; trying HTTPS protocol` ), chalk.yellowBright( `<service>` ), chalk.gray( `${ service }` ), chalk.yellowBright( `<uriAttempt1>` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `<uriAttempt2>` ), chalk.gray( `${ uriRetry }` ), chalk.blueBright( `(pending)` ) );
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const resp = await fetch( uriRetry );
|
const resp = await fetch( uriRetry );
|
||||||
|
|
||||||
/* try 2 > https > domain down */
|
/* try 2 › http › domain down */
|
||||||
if ( resp.status !== 200 )
|
if ( resp.status !== 200 )
|
||||||
{
|
{
|
||||||
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Domain Offline; failed to communicate with domain, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* try 2 > https > domain up */
|
/* try 2 › http › domain up */
|
||||||
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `<msg>` ), chalk.gray( `Service Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `<msg>` ), chalk.gray( `Domain Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
/* try 2 > https > domain not exist */
|
/* try 2 › http › domain not exist */
|
||||||
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, address does not exist` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ), chalk.redBright( `<message>` ), chalk.gray( `${ err }` ) );
|
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `<msg>` ), chalk.gray( `Domain Offline; failed to communicate with domain, address does not exist` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ), chalk.redBright( `<message>` ), chalk.gray( `${ err }` ) );
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Func > Download File
|
Func › Download File
|
||||||
|
|
||||||
@arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt
|
@arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt
|
||||||
@arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt
|
@arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt
|
||||||
@@ -568,10 +462,10 @@ function getFileModified( filename )
|
|||||||
Takes the total number of bytes in a file's size and converts it into
|
Takes the total number of bytes in a file's size and converts it into
|
||||||
a human readable format.
|
a human readable format.
|
||||||
|
|
||||||
@arg str filename filename to get size in bytes for
|
@arg str filename filename to get size in bytes for
|
||||||
@arg bool si divides the bytes of a file by 1000 instead of 2024
|
@arg bool si divides the bytes of a file by 1000 instead of 2024
|
||||||
@arg int decimal specifies the decimal point
|
@arg int decimal specifies the decimal point
|
||||||
@ret str 111.9 KB
|
@ret str 111.9 KB
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -630,7 +524,35 @@ async function getFile( url, filePath )
|
|||||||
chalk.blueBright( `<src>` ), chalk.gray( `${ url }` ),
|
chalk.blueBright( `<src>` ), chalk.gray( `${ url }` ),
|
||||||
chalk.blueBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
|
chalk.blueBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
|
||||||
await downloadFile( url, filePath );
|
const ok = await hostCheck( 'git.binaryninja.com', `${ envUrlRepo }` );
|
||||||
|
|
||||||
|
if ( ok )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await downloadFile( url, filePath );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `file`, chalk.redBright( `[download]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Download attempt failed after service check succeeded` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<src>` ), chalk.gray( `${ url }` ),
|
||||||
|
chalk.redBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.info( `file`, chalk.yellow( `[download]` ), chalk.white( `ℹ️` ),
|
||||||
|
chalk.yellowBright( `<msg>` ), chalk.gray( `Skipping download because service is offline; using existing local file` ),
|
||||||
|
chalk.yellowBright( `<url>` ), chalk.gray( `${ url }` ),
|
||||||
|
chalk.yellowBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
@@ -1505,7 +1427,7 @@ async function serveHealthCheck( req, res )
|
|||||||
|
|
||||||
if ( !paramUrl )
|
if ( !paramUrl )
|
||||||
{
|
{
|
||||||
if ( str2bool( paramSilent ) !== true )
|
if ( Utils.str2bool( paramSilent ) !== true )
|
||||||
{
|
{
|
||||||
Log.debug( `/api`, chalk.yellow( `[health]` ), chalk.white( `⚙️` ),
|
Log.debug( `/api`, chalk.yellow( `[health]` ), chalk.white( `⚙️` ),
|
||||||
chalk.blueBright( `<msg>` ), chalk.gray( `No api-key passed to health check` ) );
|
chalk.blueBright( `<msg>` ), chalk.gray( `No api-key passed to health check` ) );
|
||||||
@@ -1532,7 +1454,7 @@ async function serveHealthCheck( req, res )
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( str2bool( paramSilent ) !== true )
|
if ( Utils.str2bool( paramSilent ) !== true )
|
||||||
{
|
{
|
||||||
Log.ok( `/api`, chalk.yellow( `[health]` ), chalk.white( `✅` ),
|
Log.ok( `/api`, chalk.yellow( `[health]` ), chalk.white( `✅` ),
|
||||||
chalk.greenBright( `<msg>` ), chalk.gray( `Response` ),
|
chalk.greenBright( `<msg>` ), chalk.gray( `Response` ),
|
||||||
@@ -1650,18 +1572,20 @@ async function serveM3U( res, req )
|
|||||||
chalk.blueBright( `<to>` ), chalk.gray( `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }` ) );
|
chalk.blueBright( `<to>` ), chalk.gray( `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }` ) );
|
||||||
|
|
||||||
return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`;
|
return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`;
|
||||||
})
|
});
|
||||||
.replace( /(https?:\/\/[^\s]*fl2.moveonjoy[^\s]*)/g, ( fullUrl ) =>
|
/*
|
||||||
|
.replace( /(https?:\/\/fl\d+\.moveonjoy\.com[^\s]*)/g, ( fullUrl ) =>
|
||||||
{
|
{
|
||||||
const urlRewrite = fullUrl.replace( 'fl2.moveonjoy', 'fl6.moveonjoy' );
|
const urlRewrite = fullUrl.replace( /fl\d+\.moveonjoy\.com/, 'fl25.moveonjoy.com' );
|
||||||
Log.debug( `.m3u`, chalk.yellow( `[rewriter]` ), chalk.white( `⚙️` ),
|
Log.debug( `.m3u`, chalk.yellow( `[rewriter]` ), chalk.white( `⚙️` ),
|
||||||
chalk.blueBright( `<msg>` ), chalk.gray( `Rewriting url for keyword` ),
|
chalk.blueBright( `<msg>` ), chalk.gray( `Rewriting url for keyword` ),
|
||||||
chalk.blueBright( `<keyword>` ), chalk.gray( `*fl2.moveonjoy` ),
|
chalk.blueBright( `<keyword>` ), chalk.gray( `*fl1.moveonjoy` ),
|
||||||
chalk.blueBright( `<from>` ), chalk.gray( `${ fullUrl }` ),
|
chalk.blueBright( `<from>` ), chalk.gray( `${ fullUrl }` ),
|
||||||
chalk.blueBright( `<to>` ), chalk.gray( `${ urlRewrite }` ) );
|
chalk.blueBright( `<to>` ), chalk.gray( `${ urlRewrite }` ) );
|
||||||
|
|
||||||
return `${ urlRewrite }`;
|
return `${ urlRewrite }`;
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
res.writeHead( 200, {
|
res.writeHead( 200, {
|
||||||
'Content-Type': 'application/x-mpegURL',
|
'Content-Type': 'application/x-mpegURL',
|
||||||
@@ -2086,10 +2010,10 @@ const server = http.createServer( ( req, resp ) =>
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' );
|
const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' );
|
||||||
if ( str2bool( paramSilent ) !== true )
|
if ( Utils.str2bool( paramSilent ) !== true )
|
||||||
{
|
{
|
||||||
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
|
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
|
||||||
chalk.blueBright( `<msg>` ), chalk.gray( `Request started` ),
|
chalk.blueBright( `<msg>` ), chalk.gray( `New request` ),
|
||||||
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
chalk.blueBright( `<request.url>` ), chalk.gray( `${ req.url }` ),
|
chalk.blueBright( `<request.url>` ), chalk.gray( `${ req.url }` ),
|
||||||
chalk.blueBright( `<reqUrl>` ), chalk.gray( `${ reqUrl }` ),
|
chalk.blueBright( `<reqUrl>` ), chalk.gray( `${ reqUrl }` ),
|
||||||
@@ -2294,7 +2218,7 @@ const server = http.createServer( ( req, resp ) =>
|
|||||||
// do not show log if query is `uptime`, since uptime runs every 1 second.
|
// do not show log if query is `uptime`, since uptime runs every 1 second.
|
||||||
// do not show logs if query has striggered `silent?=true` in url
|
// do not show logs if query has striggered `silent?=true` in url
|
||||||
|
|
||||||
if ( str2bool( paramSilent ) !== true )
|
if ( Utils.str2bool( paramSilent ) !== true )
|
||||||
{
|
{
|
||||||
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `ℹ️` ),
|
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `ℹ️` ),
|
||||||
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access health api` ),
|
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access health api` ),
|
||||||
@@ -2423,10 +2347,353 @@ const server = http.createServer( ( req, resp ) =>
|
|||||||
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main Server › Discovery.json
|
||||||
|
*/
|
||||||
|
|
||||||
if ( loadFile === 'discovery.json' )
|
if ( loadFile === 'discovery.json' )
|
||||||
{
|
{
|
||||||
Log.notice( `http`, chalk.yellowBright( `[notice]` ), chalk.white( `📌` ),
|
Log.notice( `http`, chalk.yellowBright( `[notice]` ), chalk.white( `📌` ),
|
||||||
chalk.yellowBright( `<msg>` ), chalk.gray( `If you are attempting to load TVApp2 using an HDHomeRun tuner, please switch to the` ), chalk.yellowBright( `M3U Tuner` ) );
|
chalk.yellowBright( `<msg>` ), chalk.gray( `If you are attempting to load TVApp2 using an HDHomeRun tuner, please switch to the` ), chalk.yellowBright( `M3U Tuner` ) );
|
||||||
|
|
||||||
|
const tunerInstance = new Tuner(); // <-- use a different name
|
||||||
|
const hdHomeRun =
|
||||||
|
{
|
||||||
|
FriendlyName: tunerInstance.FriendlyName,
|
||||||
|
ModelNumber: tunerInstance.ModelNumber,
|
||||||
|
FirmwareName: tunerInstance.FirmwareName,
|
||||||
|
FirmwareVersion: tunerInstance.FirmwareVersion,
|
||||||
|
DeviceID: tunerInstance.GetDeviceId(),
|
||||||
|
TunerCount: tunerInstance.SlotsMax,
|
||||||
|
BaseURL: `${ envIpContainer }:${ envHdhrPort }`,
|
||||||
|
LineupURL: `${ envIpContainer }:${ envHdhrPort }/lineup.json`,
|
||||||
|
client: clientIp( req ),
|
||||||
|
message: 'Connected to HDHomeRun server',
|
||||||
|
status: 'healthy',
|
||||||
|
code: 200,
|
||||||
|
uptime: Math.round( process.uptime() ),
|
||||||
|
uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ),
|
||||||
|
uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ),
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
resp.writeHead( hdHomeRun.code, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
resp.end( JSON.stringify( hdHomeRun ) );
|
||||||
|
|
||||||
|
return; // <- Prevent further code from executing
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusCheck =
|
||||||
|
{
|
||||||
|
ip: envIpContainer,
|
||||||
|
gateway: envIpGateway,
|
||||||
|
client: clientIp( req ),
|
||||||
|
message: 'Page not found',
|
||||||
|
status: 'healthy',
|
||||||
|
ref: req.url,
|
||||||
|
method: method || 'GET',
|
||||||
|
code: 404,
|
||||||
|
uptime: Math.round( process.uptime() ),
|
||||||
|
uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ),
|
||||||
|
uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ),
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
resp.writeHead( statusCheck.code, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.error( `http`, chalk.redBright( `[requests]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
|
||||||
|
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.redBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
|
||||||
|
resp.end( JSON.stringify( statusCheck ) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
handleRequest().catch( ( err ) =>
|
||||||
|
{
|
||||||
|
resp.writeHead( 500, {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.error( `http`, chalk.redBright( `[requests]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Cannot handle request` ),
|
||||||
|
chalk.redBright( `<code>` ), chalk.gray( `500` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ) );
|
||||||
|
|
||||||
|
resp.end( 'Internal Server Error' );
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Server > HDHomeRun
|
||||||
|
|
||||||
|
this server will serve up the HDHomeRun lineup.json for people wishing to
|
||||||
|
see the IPTV streams using the HDHomeRun tuner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const serverHdHomeRun = http.createServer( ( req, resp ) =>
|
||||||
|
{
|
||||||
|
const method = req.method || 'GET';
|
||||||
|
let reqUrl = req.url;
|
||||||
|
if ( reqUrl === '/' )
|
||||||
|
reqUrl = 'hdhomerun.html';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove leading forward slash
|
||||||
|
*/
|
||||||
|
|
||||||
|
const loadFile = reqUrl.replace( /^\/+/, '' );
|
||||||
|
|
||||||
|
const handleRequest = async() =>
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Define the different routes.
|
||||||
|
Place the template system last. Getting TVApp data should take priority.
|
||||||
|
|
||||||
|
subdomainM3U array []
|
||||||
|
loadFile channel?url=https%3A%2F%2Ftvpass.org%2Fchannel%2Fabc-wabc-new-york-ny%2F
|
||||||
|
*/
|
||||||
|
|
||||||
|
Log.debug( `hdjr`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Request sent to HDHomeRun` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<request.url>` ), chalk.gray( `${ req.url }` ),
|
||||||
|
chalk.blueBright( `<reqUrl>` ), chalk.gray( `${ reqUrl }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
|
||||||
|
if ( subdomainHealth.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
|
||||||
|
{
|
||||||
|
const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' );
|
||||||
|
|
||||||
|
// do not show log if query is `uptime`, since uptime runs every 1 second.
|
||||||
|
// do not show logs if query has striggered `silent?=true` in url
|
||||||
|
|
||||||
|
if ( Utils.str2bool( paramSilent ) !== true )
|
||||||
|
{
|
||||||
|
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `ℹ️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access health api` ),
|
||||||
|
chalk.blueBright( `<type>` ), chalk.gray( `api/health` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
await serveHealthCheck( req, resp );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
General Template & .html / .css / .js
|
||||||
|
read the loaded asset file
|
||||||
|
*/
|
||||||
|
|
||||||
|
const tunerInstance = new Tuner();
|
||||||
|
ejs.renderFile( `./${ envWebFolder }/${ loadFile }`,
|
||||||
|
{
|
||||||
|
friendlyName: tunerInstance.FriendlyName,
|
||||||
|
modelNumber: tunerInstance.ModelNumber,
|
||||||
|
firmwareName: tunerInstance.FirmwareName,
|
||||||
|
firmwareVersion: tunerInstance.FirmwareVersion,
|
||||||
|
slotsConnected: tunerInstance.SlotsConnected,
|
||||||
|
slotsMax: tunerInstance.SlotsMax,
|
||||||
|
deviceId: tunerInstance.GetDeviceId( ),
|
||||||
|
hdhrIp: `${ envIpContainer }`,
|
||||||
|
hdhrPort: `${ envHdhrPort }`,
|
||||||
|
|
||||||
|
healthTimer: envHealthTimer,
|
||||||
|
appRelease: envAppRelease,
|
||||||
|
appName: name,
|
||||||
|
appVersion: version,
|
||||||
|
appUrlGithub: repository.url.substr( 0, repository.url.lastIndexOf( '.' ) ),
|
||||||
|
appUrlDiscord: discord.url,
|
||||||
|
appUrlDocs: docs.url,
|
||||||
|
appGitHashShort: envGitSHA1.substring( 0, 9 ),
|
||||||
|
appGitHashLong: envGitSHA1,
|
||||||
|
appUptimeShort: timeAgo.format( Date.now() - Math.round( process.uptime() ) * 1000, 'twitter' ),
|
||||||
|
appUptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ),
|
||||||
|
appUptimeFull: timeAgo.format( Date.now() - process.uptime() * 1000 ),
|
||||||
|
appStartup: Math.round( serverStartup ) / 1000,
|
||||||
|
serverOs: serverOs
|
||||||
|
}, ( err, data ) =>
|
||||||
|
{
|
||||||
|
if ( !err )
|
||||||
|
{
|
||||||
|
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Request accepted by ejs` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
This allows us to serve all files locally: css, js, etc.
|
||||||
|
the file loaded is dependent on what comes to the right of the period.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fileExt = loadFile.lastIndexOf( '.' );
|
||||||
|
const fileMime = fileExt === -1
|
||||||
|
? 'text/plain'
|
||||||
|
: {
|
||||||
|
'.html' : 'text/html',
|
||||||
|
'.htm' : 'text/html',
|
||||||
|
'.ico' : 'image/x-icon',
|
||||||
|
'.jpg' : 'image/jpeg',
|
||||||
|
'.png' : 'image/png',
|
||||||
|
'.gif' : 'image/gif',
|
||||||
|
'.css' : 'text/css',
|
||||||
|
'.scss' : 'text/x-sass',
|
||||||
|
'.gz' : 'application/gzip',
|
||||||
|
'.js' : 'text/javascript',
|
||||||
|
'.txt' : 'text/plain',
|
||||||
|
'.xml' : 'application/xml',
|
||||||
|
'.json' : 'application/json',
|
||||||
|
'.m3u' : 'text/plain',
|
||||||
|
'.m3u8' : 'text/plain'
|
||||||
|
}[loadFile.substring( fileExt )];
|
||||||
|
|
||||||
|
/*
|
||||||
|
ejs is only for templates; if we want to load an binary data (like images); we must use fs.readFile
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( fileMime !== 'text/html' )
|
||||||
|
data = fs.readFileSync( `./${ envWebFolder }/${ loadFile }` );
|
||||||
|
|
||||||
|
resp.setHeader( 'Content-type', fileMime );
|
||||||
|
resp.end( data );
|
||||||
|
|
||||||
|
/*
|
||||||
|
silence logs if loading css or js files; otherwise they'll spam console each time you load
|
||||||
|
a page by the client.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( fileMime === 'text/html' || fileMime === 'application/xml' || fileMime === 'application/json' )
|
||||||
|
{
|
||||||
|
Log.ok( `http`, chalk.yellow( `[requests]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Request to load file` ),
|
||||||
|
chalk.greenBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.greenBright( `<mime>` ), chalk.gray( `${ fileMime }` ),
|
||||||
|
chalk.greenBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Request to load file` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.blueBright( `<mime>` ), chalk.gray( `${ fileMime }` ),
|
||||||
|
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Request rejected by ejs` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<error>` ), chalk.gray( `${ err }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
|
||||||
|
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
HDHomeRun › Discovery.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( loadFile === 'discovery.json' )
|
||||||
|
{
|
||||||
|
Log.notice( `http`, chalk.yellowBright( `[notice]` ), chalk.white( `📌` ),
|
||||||
|
chalk.yellowBright( `<msg>` ), chalk.gray( `If you are attempting to load TVApp2 using an HDHomeRun tuner, please switch to the` ), chalk.yellowBright( `M3U Tuner` ) );
|
||||||
|
|
||||||
|
const tunerInstance = new Tuner();
|
||||||
|
const hdHomeRun =
|
||||||
|
{
|
||||||
|
FriendlyName: tunerInstance.FriendlyName,
|
||||||
|
ModelNumber: tunerInstance.ModelNumber,
|
||||||
|
FirmwareName: tunerInstance.FirmwareName,
|
||||||
|
FirmwareVersion: tunerInstance.FirmwareVersion,
|
||||||
|
DeviceID: tunerInstance.GetDeviceId(),
|
||||||
|
TunerCount: tunerInstance.SlotsMax,
|
||||||
|
BaseURL: `${ envIpContainer }:${ envHdhrPort }`,
|
||||||
|
LineupURL: `${ envIpContainer }:${ envHdhrPort }/lineup.json`,
|
||||||
|
client: clientIp( req ),
|
||||||
|
message: 'Connected to HDHomeRun server',
|
||||||
|
status: 'healthy',
|
||||||
|
code: 200,
|
||||||
|
uptime: Math.round( process.uptime() ),
|
||||||
|
uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ),
|
||||||
|
uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ),
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
resp.writeHead( hdHomeRun.code, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.ok( `http`, chalk.yellow( `[requests]` ), chalk.white( `✅` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Established connection to HDHomeRun` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<friendlyName>` ), chalk.gray( `${ hdHomeRun.FriendlyName }` ),
|
||||||
|
chalk.blueBright( `<modelNumber>` ), chalk.gray( `${ hdHomeRun.ModelNumber }` ),
|
||||||
|
chalk.blueBright( `<deviceID>` ), chalk.gray( `${ hdHomeRun.DeviceID }` ),
|
||||||
|
chalk.blueBright( `<tunerCount>` ), chalk.gray( `${ hdHomeRun.TunerCount }` ),
|
||||||
|
chalk.blueBright( `<urlBase>` ), chalk.whiteBright.bgBlack( ` ${ hdHomeRun.BaseURL } ` ),
|
||||||
|
chalk.blueBright( `<urlLineup>` ), chalk.whiteBright.bgBlack( ` ${ hdHomeRun.LineupURL } ` ) );
|
||||||
|
|
||||||
|
resp.end( JSON.stringify( hdHomeRun ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HDHomeRun › Lineup.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( loadFile === 'lineup.json' )
|
||||||
|
{
|
||||||
|
const tunerInstance = new Tuner();
|
||||||
|
const hdHomeRun =
|
||||||
|
{
|
||||||
|
FriendlyName: tunerInstance.FriendlyName,
|
||||||
|
ModelNumber: tunerInstance.ModelNumber,
|
||||||
|
FirmwareName: tunerInstance.FirmwareName,
|
||||||
|
FirmwareVersion: tunerInstance.FirmwareVersion,
|
||||||
|
DeviceID: tunerInstance.GetDeviceId(),
|
||||||
|
TunerCount: tunerInstance.SlotsMax,
|
||||||
|
BaseURL: `${ envIpContainer }:${ envHdhrPort }`,
|
||||||
|
LineupURL: `${ envIpContainer }:${ envHdhrPort }/lineup.json`,
|
||||||
|
client: clientIp( req ),
|
||||||
|
status: 'healthy',
|
||||||
|
code: 200,
|
||||||
|
uptime: Math.round( process.uptime() ),
|
||||||
|
uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ),
|
||||||
|
uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
channels: `[]`
|
||||||
|
};
|
||||||
|
|
||||||
|
resp.writeHead( hdHomeRun.code, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.ok( `http`, chalk.yellow( `[requests]` ), chalk.white( `✅` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Lineup requested` ),
|
||||||
|
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
|
||||||
|
chalk.blueBright( `<friendlyName>` ), chalk.gray( `${ hdHomeRun.FriendlyName }` ),
|
||||||
|
chalk.blueBright( `<modelNumber>` ), chalk.gray( `${ hdHomeRun.ModelNumber }` ),
|
||||||
|
chalk.blueBright( `<deviceID>` ), chalk.gray( `${ hdHomeRun.DeviceID }` ),
|
||||||
|
chalk.blueBright( `<tunerCount>` ), chalk.gray( `${ hdHomeRun.TunerCount }` ),
|
||||||
|
chalk.blueBright( `<urlBase>` ), chalk.whiteBright.bgBlack( ` ${ hdHomeRun.BaseURL } ` ),
|
||||||
|
chalk.blueBright( `<urlLineup>` ), chalk.whiteBright.bgBlack( ` ${ hdHomeRun.LineupURL } ` ) );
|
||||||
|
|
||||||
|
resp.end( JSON.stringify( hdHomeRun ) );
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusCheck =
|
const statusCheck =
|
||||||
@@ -2497,17 +2764,15 @@ const server = http.createServer( ( req, resp ) =>
|
|||||||
initialize
|
initialize
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
await new Storage( envWebFolder, FILE_CFG ).Initialize();
|
||||||
|
await new Tuner( Storage.Get( 'deviceId' ) ).Initialize();
|
||||||
await initialize();
|
await initialize();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
check service status that we depend on
|
check service status that we depend on
|
||||||
*/
|
*/
|
||||||
|
|
||||||
serviceCheck( 'TVPass.org', 'https://tvpass.org' );
|
hosts.forEach( ( host ) => hostCheck( host.name, host.url ) );
|
||||||
serviceCheck( 'TheTVApp.to', 'https://thetvapp.to' );
|
|
||||||
serviceCheck( 'MoveOnJoy.com', 'http://moveonjoy.com' );
|
|
||||||
serviceCheck( 'Daddylive.dad', 'https://daddylive.dad' );
|
|
||||||
serviceCheck( 'Newkso.ru', 'https://zekonew.newkso.ru/zeko' );
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
start web server
|
start web server
|
||||||
@@ -2525,6 +2790,14 @@ const server = http.createServer( ( req, resp ) =>
|
|||||||
chalk.blueBright( `<version>` ), chalk.gray( ` ${ version } ` ),
|
chalk.blueBright( `<version>` ), chalk.gray( ` ${ version } ` ),
|
||||||
chalk.blueBright( `<release>` ), chalk.gray( ` ${ envAppRelease } ` ) );
|
chalk.blueBright( `<release>` ), chalk.gray( ` ${ envAppRelease } ` ) );
|
||||||
});
|
});
|
||||||
|
|
||||||
|
serverHdHomeRun.listen( `${ envHdhrPort }`, envWebIP, () =>
|
||||||
|
{
|
||||||
|
Log.ok( `core`, chalk.yellow( `[initiate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Starting HDHomeRun server on` ),
|
||||||
|
chalk.blueBright( `<ipPublic>` ), chalk.whiteBright.bgBlack( ` ${ envWebIP }:${ envHdhrPort } ` ),
|
||||||
|
chalk.blueBright( `<ipDocker>` ), chalk.whiteBright.bgBlack( ` ${ envIpContainer }:${ envHdhrPort } ` ) );
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
1272
tvapp2/package-lock.json
generated
1272
tvapp2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tvapp2",
|
"name": "tvapp2",
|
||||||
"version": "1.5.5",
|
"version": "1.5.9",
|
||||||
"description": "This package allows you to generate M3U playlists and EPG guides from various online IPTV services.",
|
"description": "This package allows you to generate M3U playlists and EPG guides from various online IPTV services.",
|
||||||
"author": "BinaryNinja",
|
"author": "BinaryNinja",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -86,9 +86,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aetherinox/noxenv": "^1.1.1",
|
"@aetherinox/noxenv": "^1.1.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"all-contributors-cli": "^6.26.1",
|
"all-contributors-cli": "^6.26.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-plugin-chai-friendly": "^1.1.0",
|
"eslint-plugin-chai-friendly": "^1.1.0",
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
"classes/Semaphore.js",
|
"classes/Semaphore.js",
|
||||||
"classes/Storage.js",
|
"classes/Storage.js",
|
||||||
"classes/Tuner.js",
|
"classes/Tuner.js",
|
||||||
|
"classes/Utils.js",
|
||||||
"www/index.html",
|
"www/index.html",
|
||||||
"www/hdhomerun.html",
|
"www/hdhomerun.html",
|
||||||
"www/favicon.ico",
|
"www/favicon.ico",
|
||||||
|
|||||||
579
tvapp2/www/hdhomerun.html
Normal file
579
tvapp2/www/hdhomerun.html
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<title><%= appName %> | HDHomeRun Tuner | v<%= appVersion %></title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="css/tvapp2.min.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<script src='https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js' integrity='sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq' crossorigin='anonymous'></script>
|
||||||
|
<script src=' https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js '></script>
|
||||||
|
<script src='js/tvapp2.min.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<nav class="navbar sticky-top container">
|
||||||
|
<div class="brand">
|
||||||
|
<i data-bs-toggle="tooltip" title="v<%= appVersion %>" class="logo fa-sharp-duotone fa-regular fa-tv" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i>
|
||||||
|
<a target="_blank" data-bs-toggle="tooltip" title="View Github Repository" class="header-name" href="<%= appUrlGithub %>">TVApp2 for Docker</a>
|
||||||
|
</div>
|
||||||
|
<div class="social">
|
||||||
|
<i id="action-health" data-bs-toggle="tooltip" title="Health" class="heart logo health fa-duotone fa-solid fa-heart" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i>
|
||||||
|
<a href="javascript:runResync();"><i id="action-resync" data-bs-toggle="tooltip" title="Resync" class="restart fa-solid fa-rotate" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
|
||||||
|
<a target="_blank" href="<%= appUrlDocs %>"><i data-bs-toggle="tooltip" title="Documentation" class="logo fa-duotone fa-solid fa-book-open-cover" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
|
||||||
|
<a target="_blank" href="<%= appUrlGithub %>"><i data-bs-toggle="tooltip" title="Github" class="logo fa-logos fa-github" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
|
||||||
|
<a target="_blank" href="<%= appUrlDiscord %>"><i data-bs-toggle="tooltip" title="Discord" class="logo fa-logos fa-discord" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header Notification: description -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="introduction">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="introduction-body">
|
||||||
|
<div class="desc">
|
||||||
|
<div class="about" style="font-size: 13px;"><code>HDHomeRun</code> is a network-attached digital television tuner box, produced by the company SiliconDust USA, Inc. Self-hosted multimedia applications such as Jellyfin allow for you to add IPTV channels either using a <code>M3U8 tuner</code>, or also with the option of specifying a <code>HDHomeRun</code> tuner.</div>
|
||||||
|
<div class="about" style="font-size: 13px;">The TVApp2 app allows you to host your own HDHomeRun tuner, and then utilize this tuner within apps like Jellyfin in order to stream IPTV using the integrated server. Your HDHomeRun tuner settings are provided below:</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header Fontawesome Icons -->
|
||||||
|
<div class="container main-container">
|
||||||
|
<table id="list" class="table table-dark table-striped" style="width:60%; margin: 0 auto;">
|
||||||
|
<thead>
|
||||||
|
<tr class="d-none d-md-table-row">
|
||||||
|
<th class="file cell-file">
|
||||||
|
Property
|
||||||
|
</th>
|
||||||
|
<th class="link cell-link">
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<th class="desc cell-desc">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
FriendlyName
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= friendlyName %></td>
|
||||||
|
<td class="desc cell-desc">Name of tuner</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
ModelNumber
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= modelNumber %></td>
|
||||||
|
<td class="desc cell-desc">Virtual tuner model number</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
FirmwareName
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= firmwareName %></td>
|
||||||
|
<td class="desc cell-desc">Firmware name for tuner</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
FirmwareVersion
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= firmwareVersion %></td>
|
||||||
|
<td class="desc cell-desc">Firmware version running on tuner</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
DeviceID
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= deviceId %></td>
|
||||||
|
<td class="desc cell-desc">Tuner device id</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
TunerCount
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= slotsConnected %> / <%= slotsMax %></td>
|
||||||
|
<td class="desc cell-desc">Number of connection slots to view IPTV</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
BaseURL
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><a href="https://<%= hdhrIp %>:<%= hdhrPort %>" id="m3u-link" target="_blank"><%= hdhrIp %>:<%= hdhrPort %></a></td>
|
||||||
|
<td class="desc cell-desc">Base URL where HDHomeRun is hosted</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
LineupURL
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><a href="https://<%= hdhrIp %>:<%= hdhrPort %>/lineup.json" id="m3u-link" target="_blank"><%= hdhrIp %>:<%= hdhrPort %>/lineup.json</a></td>
|
||||||
|
<td class="desc cell-desc">URL to IPTV channel & guide lineups</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="file cell-file" style="color: #919191;font-weight: 400;">
|
||||||
|
Uptime
|
||||||
|
</td>
|
||||||
|
<td class="link cell-link"><%= appUptimeFull %></td>
|
||||||
|
<td class="desc cell-desc">Duration that tuner has been online</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container notifications" style="padding-bottom:20px;">
|
||||||
|
<div id="ntfy-restart" class="ntfy-parent indicator-success sticky-bottom"></div>
|
||||||
|
<div id="ntfy-firewall" class="ntfy-parent indicator-warning sticky-bottom"></div>
|
||||||
|
<div id="ntfy-localhost" class="ntfy-parent indicator-danger sticky-bottom"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sub">
|
||||||
|
<div class="container">
|
||||||
|
<div class="col text-center text-muted text-small text-nowrap">
|
||||||
|
<small>Developed by BinaryNinja - <a data-bs-toggle="tooltip" title="v<%= appVersion %> <%= appRelease %> (<%= appGitHashShort %>)" href="<%= appUrlGithub %>"><%= appName %> (<%= appRelease %>)</a> v<%= appVersion %> <a target="_blank" data-bs-toggle="tooltip" title="View Github commit" href="<%= appUrlGithub %>/commit/<%= appGitHashLong %>"><%= appGitHashShort %></a></small><br />
|
||||||
|
<span class="footer-sub"><small>Uptime <a id="uptime" href="" data-bs-toggle="tooltip" title="<%= appUptimeLong %>"> <%= appUptimeShort %> </a> | Startup <a id="startup" data-bs-toggle="tooltip" title="Startup time" href=""><%= appStartup %>s</a> | OS <a id="os" href="" data-bs-toggle="tooltip" title="Server operating system" href=""><%= serverOs %></a></small></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Toast Notifications -->
|
||||||
|
<!-- <button type="button" class="btn btn-primary" id="btnTestToasts">Show toast</button> -->
|
||||||
|
<div style="z-index: 9999;" class="toast position-fixed bottom-0 end-0 p-8 m-3" id="tvapp2Toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="10000">
|
||||||
|
<div class="toast-body">
|
||||||
|
<div class="d-flex gap-4">
|
||||||
|
<span><i class="fa-solid fa-circle-check fa-lg icon-success"></i></span>
|
||||||
|
<div class="d-flex flex-column flex-grow-1 gap-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span id="toast-title" class="fw-semibold">Toast Title</span>
|
||||||
|
<button type="button" class="btn-close btn-close-sm ms-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<span id="toast-message">Dismiss in 6 seconds</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="modalTvapp2" tabindex="-1" data-bs-backdrop="static" aria-labelledby="modalTvapp2Label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalTvapp2Label">Modal title</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="btn-secondary" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="button" id="btn-primary" class="btn btn-primary">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/*
|
||||||
|
this is test code. enable the "Show Toast" button and then uncomment this code.
|
||||||
|
|
||||||
|
document.getElementById("btnTestToasts").onclick = function()
|
||||||
|
{
|
||||||
|
var toastElList = [].slice.call(document.querySelectorAll('.toast'))
|
||||||
|
var toastList = toastElList.map(function(toastEl)
|
||||||
|
{
|
||||||
|
return new bootstrap.Toast(toastEl)
|
||||||
|
});
|
||||||
|
|
||||||
|
toastList.forEach(toast => toast.show());
|
||||||
|
console.log(toastList);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlBase = window.location.origin;
|
||||||
|
const urlM3U = urlBase + '/playlist';
|
||||||
|
const urlXML = urlBase + '/epg';
|
||||||
|
const urlGZP = urlBase + '/gzip';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
/*
|
||||||
|
Document Ready
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(function(){
|
||||||
|
$("[data-bs-toggle=tooltip]").tooltip({ placement: 'bottom'});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Action > DOM Status
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentReady", function() {
|
||||||
|
$("#tvapp2Toast").toast();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
$('#tvapp2Toast').toast("show");
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Notify > Localhost
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener( 'DOMContentLoaded', function()
|
||||||
|
{
|
||||||
|
const host = window.location.hostname;
|
||||||
|
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
||||||
|
if (host === 'localhost' || host === '127.0.0.1')
|
||||||
|
{
|
||||||
|
|
||||||
|
const msg = "<div class='ntfy-child'><span class='danger'>Danger</span> \
|
||||||
|
<span class='msg'> \
|
||||||
|
If accessing this page via 127.0.0.1 / localhost, proxying will not work on other devices. Load this page using \
|
||||||
|
your computer's IP address (e.g., 192.168.x.x) and port to access the playlist from other devices on your network. \
|
||||||
|
<br> \
|
||||||
|
Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> \
|
||||||
|
or <a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.\
|
||||||
|
</span></div>";
|
||||||
|
|
||||||
|
document.getElementById( 'ntfy-localhost' ).innerHTML = msg;
|
||||||
|
document.getElementById( 'ntfy-localhost' ).style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById( 'ntfy-localhost' ).style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Notify > Firewall
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener( 'DOMContentLoaded', function()
|
||||||
|
{
|
||||||
|
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
||||||
|
const msg = "<div class='ntfy-child'><span class='warning'>Warning</span> \
|
||||||
|
<span class='msg'> \
|
||||||
|
Port <strong> " + port + " </strong> must be open and allowed through your \
|
||||||
|
<a href='https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18 target='_blank'>Windows</a> \
|
||||||
|
or \
|
||||||
|
<a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a> \
|
||||||
|
OS firewall settings. This action enables devices such as Firestick or Android to connect \
|
||||||
|
to the server and request the playlist through the proxy. \
|
||||||
|
</span></div>";
|
||||||
|
|
||||||
|
document.getElementById( 'ntfy-firewall' ).innerHTML = msg;
|
||||||
|
document.getElementById( 'ntfy-firewall' ).style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Notify > Restart / Resync
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener( 'DOMContentLoaded', function()
|
||||||
|
{
|
||||||
|
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
||||||
|
const msg = "<div class='ntfy-child'><span class='success'>Success</span> \
|
||||||
|
<span class='msg'> \
|
||||||
|
Your IPTV m3u channels and xml guide data has been successfully re-synced. \
|
||||||
|
Please refresh this window to see new data \
|
||||||
|
</span></div>";
|
||||||
|
|
||||||
|
document.getElementById( 'ntfy-restart' ).innerHTML = msg;
|
||||||
|
document.getElementById( 'ntfy-restart' ).style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set initial health check sync time
|
||||||
|
|
||||||
|
first health check runs after 10 seconds
|
||||||
|
all future health checks run after <%= healthTimer %>
|
||||||
|
*/
|
||||||
|
|
||||||
|
let timerDelayMS = 10000;
|
||||||
|
let timerStartMS = Date.now(); // returns milliseconds
|
||||||
|
const timerHealthRun = '<%= healthTimer %>'; // time in milliseconds until health check ran AFTER initial run
|
||||||
|
const timerUptime = 1000;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Action > Healthcheck
|
||||||
|
*/
|
||||||
|
|
||||||
|
function runHealthCheck()
|
||||||
|
{
|
||||||
|
const toastTypeClass = [];
|
||||||
|
toastTypeClass[ 'DEFAULT' ] = 'text-bg-primary';
|
||||||
|
toastTypeClass[ 'UNHEALTHY' ] = 'text-bg-warning';
|
||||||
|
toastTypeClass[ 'HEALTHY' ] = 'text-bg-success';
|
||||||
|
toastTypeClass[ 'ERROR' ] = 'text-bg-danger';
|
||||||
|
|
||||||
|
$.ajax(
|
||||||
|
{
|
||||||
|
url: 'api/health',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
query: 'healthcheck',
|
||||||
|
silent: false
|
||||||
|
},
|
||||||
|
beforeSend: function( data )
|
||||||
|
{
|
||||||
|
console.log( 'Sending health check ...' )
|
||||||
|
},
|
||||||
|
success: function( data )
|
||||||
|
{
|
||||||
|
const status = data.message;
|
||||||
|
const code = data.code;
|
||||||
|
if ( status )
|
||||||
|
{
|
||||||
|
const toastClass = toastTypeClass[status.toUpperCase()];
|
||||||
|
const toastElm = document.getElementById('tvapp2Toast');
|
||||||
|
toastElm.classList.add(toastClass);
|
||||||
|
|
||||||
|
$('.toast #toast-title').html(`<%= appName %> is ${ status }`);
|
||||||
|
$('.toast #toast-message').html(`Health check returned ${ status } (${ code })`);
|
||||||
|
$('#tvapp2Toast').toast('show');
|
||||||
|
|
||||||
|
const elementsList = document.querySelectorAll( '#ntfy-firewall, #ntfy-localhost, #ntfy-restart' );
|
||||||
|
const elementsArray = [...elementsList];
|
||||||
|
|
||||||
|
elementsArray.forEach(element =>
|
||||||
|
{
|
||||||
|
element.style.transition = '1s';
|
||||||
|
element.style.opacity = '0';
|
||||||
|
element.style.visibility = 'hidden';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function( data )
|
||||||
|
{
|
||||||
|
const toastClass = toastTypeClass['ERROR'];
|
||||||
|
const toastElm = document.getElementById('tvapp2Toast');
|
||||||
|
toastElm.classList.add(toastClass);
|
||||||
|
|
||||||
|
$('.toast #toast-title').html(`Could not connect to health check api`);
|
||||||
|
$('.toast #toast-message').html(`Failed to communicate with health check api. Try restarting the docker container to restore connection.`);
|
||||||
|
$('#tvapp2Toast').toast('show');
|
||||||
|
|
||||||
|
}
|
||||||
|
}).always(function()
|
||||||
|
{
|
||||||
|
timerDelayMS = parseInt(timerHealthRun);
|
||||||
|
timerStartMS = Date.now();
|
||||||
|
|
||||||
|
setTimeout(function()
|
||||||
|
{
|
||||||
|
runHealthCheck();
|
||||||
|
}, parseInt(timerHealthRun));
|
||||||
|
}).responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runUptime()
|
||||||
|
{
|
||||||
|
const toastTypeClass = [];
|
||||||
|
toastTypeClass[ 'DEFAULT' ] = 'text-bg-primary';
|
||||||
|
toastTypeClass[ 'UNHEALTHY' ] = 'text-bg-warning';
|
||||||
|
toastTypeClass[ 'HEALTHY' ] = 'text-bg-success';
|
||||||
|
toastTypeClass[ 'ERROR' ] = 'text-bg-danger';
|
||||||
|
|
||||||
|
$.ajax(
|
||||||
|
{
|
||||||
|
url: 'api/health',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
query: 'uptime',
|
||||||
|
silent: true
|
||||||
|
},
|
||||||
|
success: function( data )
|
||||||
|
{
|
||||||
|
const status = data.message;
|
||||||
|
const code = data.code;
|
||||||
|
const uptimeShort = data.uptimeShort;
|
||||||
|
const uptimeLong = data.uptimeLong;
|
||||||
|
if ( status )
|
||||||
|
{
|
||||||
|
$('a#uptime').text(`${ uptimeShort }`);
|
||||||
|
|
||||||
|
const tooltip = bootstrap.Tooltip.getInstance('#uptime') // Returns a Bootstrap tooltip instance
|
||||||
|
tooltip.setContent( { '.tooltip-inner': `HDHomeRun server started ${ uptimeLong }` } )
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function( data )
|
||||||
|
{
|
||||||
|
const toastClass = toastTypeClass['ERROR'];
|
||||||
|
const toastElm = document.getElementById('tvapp2Toast');
|
||||||
|
toastElm.classList.add(toastClass);
|
||||||
|
|
||||||
|
$('.toast #toast-title').html(`Could not get uptime from api`);
|
||||||
|
$('.toast #toast-message').html(`Failed to communicate with the api. Try restarting the docker container to restore connection.`);
|
||||||
|
$('#tvapp2Toast').toast('show');
|
||||||
|
}
|
||||||
|
}).always(function()
|
||||||
|
{
|
||||||
|
setTimeout(function()
|
||||||
|
{
|
||||||
|
runUptime();
|
||||||
|
}, parseInt(timerUptime));
|
||||||
|
}).responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Action > Do Resync
|
||||||
|
*/
|
||||||
|
|
||||||
|
function runResync()
|
||||||
|
{
|
||||||
|
$.ajax(
|
||||||
|
{
|
||||||
|
url: 'api/restart',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
query: 'sync',
|
||||||
|
silent: false
|
||||||
|
},
|
||||||
|
beforeSend: function( data )
|
||||||
|
{
|
||||||
|
const dimmer = document.createElement('div');
|
||||||
|
dimmer.setAttribute('id', 'dimmer');
|
||||||
|
dimmer.style.visibility = 'visible';
|
||||||
|
dimmer.classList.add('dimmer-in');
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(dimmer);
|
||||||
|
document.getElementById('ntfy-firewall').style.display = 'none';
|
||||||
|
document.getElementById('ntfy-localhost').style.display = 'none';
|
||||||
|
document.getElementById('ntfy-restart').style.display = 'none';
|
||||||
|
|
||||||
|
const iconResync = document.getElementsByClassName('fa-rotate');
|
||||||
|
iconResync[0].classList.remove('restart');
|
||||||
|
iconResync[0].classList.add('spin');
|
||||||
|
|
||||||
|
$('.modal-content .modal-body').html('<small>The M3U and EPG data will now be re-downloaded and synced with your TVApp2 container. Afterward, this page will be refreshed automatically.</small><br /><br /><small>Please wait...</small>')
|
||||||
|
$('.modal-content .modal-title').html('Resyncing Data')
|
||||||
|
$('#modalTvapp2').modal('show');
|
||||||
|
|
||||||
|
const modalBtnPrimary = document.querySelector('#btn-primary');
|
||||||
|
modalBtnPrimary.style.display = 'none';
|
||||||
|
modalBtnPrimary.style.visibility= 'hidden';
|
||||||
|
|
||||||
|
},
|
||||||
|
success: function( data )
|
||||||
|
{
|
||||||
|
|
||||||
|
/*
|
||||||
|
On successful restart, wait 1 second, remove dimmer, reload page in 5 seconds
|
||||||
|
*/
|
||||||
|
|
||||||
|
setTimeout( () =>
|
||||||
|
{
|
||||||
|
document.getElementById('ntfy-restart').style.display = 'block'
|
||||||
|
const dimmer = document.getElementById('dimmer');
|
||||||
|
dimmer.classList.remove('dimmer-in');
|
||||||
|
dimmer.classList.add('dimmer-out');
|
||||||
|
dimmer.remove();
|
||||||
|
|
||||||
|
setTimeout( function()
|
||||||
|
{
|
||||||
|
const iconResync = document.getElementsByClassName('fa-rotate'); // resync favicon
|
||||||
|
iconResync[0].classList.remove('spin'); // stop spinning
|
||||||
|
iconResync[0].classList.add('restart'); // normal spinner class
|
||||||
|
document.location.reload() // reload page
|
||||||
|
}, 5000 ); // how long until refresh page
|
||||||
|
}, 1000 ); // how long until dimmer is removed / reload page activated (also on delay)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Health check > Show time remaining as tooltip
|
||||||
|
*/
|
||||||
|
|
||||||
|
function runTooltipCountdown( )
|
||||||
|
{
|
||||||
|
let timerHours, timerMins, timerRemainsLS;
|
||||||
|
|
||||||
|
function twoDigits( n )
|
||||||
|
{
|
||||||
|
return (n <= 9 ? "0" + n : n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Update Tooltip Countdown
|
||||||
|
|
||||||
|
MS = milliseconds
|
||||||
|
LS = long string (Wed Dec 31 1969 10:01:42 (Coordinated Universal Time))
|
||||||
|
*/
|
||||||
|
|
||||||
|
function updateTooltipCountdown()
|
||||||
|
{
|
||||||
|
const timerElapsedMS = Date.now() - timerStartMS; // ( 2091 )
|
||||||
|
const timerRemainsMS = timerDelayMS - timerElapsedMS; // ( 7909 ) divide by 1000 for seconds
|
||||||
|
|
||||||
|
timerRemainsLS = new Date( timerRemainsMS ); // (Wed Dec 31 1969 10:01:42 (Coordinated Universal Time))
|
||||||
|
timerHours = timerRemainsLS.getUTCHours(); // ( 0 )
|
||||||
|
timerMins = timerRemainsLS.getUTCMinutes(); // ( 9 )
|
||||||
|
const timeLeft = (timerHours ? timerHours + ':' + twoDigits( timerMins ) : timerMins) + ':' + twoDigits( timerRemainsLS.getUTCSeconds() );
|
||||||
|
|
||||||
|
jQuery(function($)
|
||||||
|
{
|
||||||
|
const tooltip = bootstrap.Tooltip.getInstance('#action-health') // Returns a Bootstrap tooltip instance
|
||||||
|
tooltip.setContent({ '.tooltip-inner': `Health check in ${ timeLeft }` })
|
||||||
|
});
|
||||||
|
|
||||||
|
const Heart = document.getElementsByClassName('fa-heart');
|
||||||
|
Heart[0].style.color = '#FFF';
|
||||||
|
|
||||||
|
setTimeout( function()
|
||||||
|
{
|
||||||
|
const Heart = document.getElementsByClassName('fa-heart');
|
||||||
|
Heart[0].style.color = '#FFF';
|
||||||
|
|
||||||
|
setTimeout( function()
|
||||||
|
{
|
||||||
|
Heart[0].style.color = '#FF6593';
|
||||||
|
}, timerRemainsLS.getUTCMilliseconds() + 100 );
|
||||||
|
|
||||||
|
}, timerRemainsLS.getUTCMilliseconds() + 500 );
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout( function()
|
||||||
|
{
|
||||||
|
updateTooltipCountdown();
|
||||||
|
}, timerRemainsLS.getUTCMilliseconds() + 500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTooltipCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Action > Healthcheck > Initialize
|
||||||
|
*/
|
||||||
|
|
||||||
|
setTimeout( function() { runHealthCheck(); }, timerDelayMS );
|
||||||
|
setTimeout( function() { runUptime(); }, 1000 );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Action > Tooltip Resync Timers
|
||||||
|
*/
|
||||||
|
|
||||||
|
runTooltipCountdown( );
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -318,10 +318,9 @@
|
|||||||
|
|
||||||
const msg = "<div class='ntfy-child'><span class='danger'>Danger</span> \
|
const msg = "<div class='ntfy-child'><span class='danger'>Danger</span> \
|
||||||
<span class='msg'> \
|
<span class='msg'> \
|
||||||
If you are accessing this page via 127.0.0.1 or localhost, proxying will not work on other devices.Please load \
|
If accessing this page via 127.0.0.1 / localhost, proxying will not work on other devices. Load this page using \
|
||||||
this page using your computer's IP address (e.g., 192.168.x.x) and port in order to access the playlist from other \
|
your computer's IP address (e.g., 192.168.x.x) and port to access the playlist from other devices on your network. \
|
||||||
devices on your network. \
|
<br> \
|
||||||
<br> <br> \
|
|
||||||
Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> \
|
Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> \
|
||||||
or <a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.\
|
or <a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.\
|
||||||
</span></div>";
|
</span></div>";
|
||||||
|
|||||||
Reference in New Issue
Block a user