Compare commits

...

99 Commits

Author SHA1 Message Date
renovate[bot]
06064b53e2 chore(deps): update dependency env-cmd to v11 2026-04-29 11:41:06 +00:00
renovate[bot]
321a0e8540 chore(deps): lock file maintenance (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 04:55:03 +00:00
renovate[bot]
cb8f769e34 chore(deps): lock file maintenance (#164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 04:24:45 +00:00
renovate[bot]
38ff77a04e chore(deps): lock file maintenance (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 02:01:35 +00:00
renovate[bot]
292cd8dd94 chore(deps): lock file maintenance (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 04:52:58 +00:00
renovate[bot]
3878059314 chore(deps): lock file maintenance (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 02:14:45 +00:00
renovate[bot]
7a7e50c7ba chore(deps): lock file maintenance (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 01:49:18 +00:00
renovate[bot]
564dd536fc chore(deps): lock file maintenance (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 05:28:15 +00:00
renovate[bot]
0e29805351 chore(deps): lock file maintenance (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 09:38:43 +00:00
dc76267da3 Merge pull request #151 from TheBinaryNinja/m3u_xmltv_feature
PR 151: bump: version 1.5.9
2026-02-27 14:32:17 -05:00
147b11b22d bump: version 1.5.9 2026-02-27 14:30:03 -05:00
renovate[bot]
b992e4ff01 chore(deps): lock file maintenance (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 05:25:33 +00:00
renovate[bot]
b46a922464 chore(deps): lock file maintenance (#147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 04:56:43 +00:00
d4abc705a0 Merge pull request #145 from TheBinaryNinja/m3u_xmltv_feature
PR 145: Update XML and M3U file paths to version 2.0.0
2026-02-09 09:51:04 -05:00
454d13c608 Update XML and M3U file paths to version 2.0.0 2026-02-09 09:22:14 -05:00
renovate[bot]
6086dbbad2 chore(deps): lock file maintenance (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 06:02:12 +00:00
renovate[bot]
b9607dddce chore(deps): lock file maintenance (#140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 04:26:06 +00:00
renovate[bot]
1a7aeb4450 chore(deps): lock file maintenance (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 05:43:30 +00:00
renovate[bot]
d973af6a8d chore(deps): lock file maintenance (#126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 20:51:12 +00:00
2dae279f93 ci: switch runner in workflow 2026-01-08 12:34:23 -07:00
09d17717ab ci: update gitea workflow 2026-01-08 12:23:24 -07:00
bf4454f635 ci: update workflow network 2026-01-08 12:05:21 -07:00
9e531d823f ci: update release workflow 2026-01-08 11:57:25 -07:00
d17aa23e98 Merge pull request #133 from TheBinaryNinja/m3u-format
[FEATURE]: Add m3u playlist automated generation and validator
2026-01-07 23:44:37 -05:00
63f7c1d665 Change extM3U URL to XML EPG path
Updated extM3U URL to point to XML EPG location.
2026-01-07 23:20:47 -05:00
c5c2f741f0 ci: remove old index.js 2025-10-08 07:44:18 -07:00
ec24c51eea chore(lint): run linter 2025-10-08 07:41:19 -07:00
fa2c4073e3 ci: update gitea workflow 2025-10-08 06:54:56 -07:00
255d093269 ci: update gitea workflow 2025-10-08 06:51:24 -07:00
73a264b1c2 build: bump version to 1.5.8 2025-10-08 06:27:17 -07:00
iFlip721
c112230e05 update EPG endpoint with new pull location 2025-10-06 19:53:44 -04:00
renovate[bot]
02dd911e93 chore(deps): lock file maintenance (#123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 05:45:30 +00:00
9c3ee3d146 refactor: update eslint corrections 2025-10-05 00:57:02 -07:00
4c8d5d03d9 chore(deps): update dependency uuid to v13 2025-10-05 00:07:47 -07:00
c729594864 build: populate channel urls from gitea repo 2025-10-05 00:06:12 -07:00
713626810b fix: update binaryninja gitea url 2025-10-04 23:37:05 -07:00
6e5c261065 ci: update docker flow 2025-10-01 03:29:38 -07:00
2f1027e068 ci: update docker build flow 2025-10-01 03:27:02 -07:00
739f547731 ci: update docker parameters 2025-10-01 03:22:19 -07:00
3f7ecdb84e ci: update docker buildx flags 2025-10-01 03:15:55 -07:00
e037764c3f ci: edit buildx installation parameters 2025-10-01 03:13:48 -07:00
c8aa866dfd ci: update docker build workflow for invalid / insecure ssl certs 2025-10-01 02:58:40 -07:00
84b1199878 ci: update docker restart command 2025-10-01 02:54:18 -07:00
04150d5320 ci: update bootstrap for docker buildx 2025-10-01 02:50:29 -07:00
11ccf2909f ci: update gitea workflow 2025-10-01 02:41:52 -07:00
631942ca75 docs(readme): add HDHR_PORT to env variables 2025-10-01 02:00:55 -07:00
4ee603d7a2 fix(hdhr): animated uptime now counting 2025-10-01 00:48:17 -07:00
7cfe22b72e feat: add HDHomeRun lineup.json to api 2025-10-01 00:47:55 -07:00
e6701cda95 feat: add silence health check to HDHomeRun server 2025-10-01 00:40:35 -07:00
865a2fd645 feat: add Hdhr.SlotsConnected and Hdhr:SlotsMax
add slot count to template and class
2025-10-01 00:34:07 -07:00
05f362153f docs(traefik): update traefik configs
add HDHomeRun
2025-10-01 00:11:04 -07:00
997eb72378 build(package): bump version v1.5.6 2025-09-30 23:54:39 -07:00
69805151c8 fix(hdhr): assign new var to tuner instance 2025-09-30 23:52:15 -07:00
47ec5267ec feat: add HDHomeRun website page when accessing port 6077 2025-09-30 23:50:27 -07:00
3a87b51f41 refactor(hdhr): add customizable HDHomeRun port 2025-09-30 23:07:43 -07:00
ffc8cfe68e build(dockerfile): add new env var HDHR_PORT 2025-09-30 23:04:29 -07:00
7f5fffa5e6 docs(license): update 2025-09-30 23:02:51 -07:00
b16f4a9fb3 refactor: remove docker.sock from examples/docker-compose.yml - fixes #105 2025-09-30 23:01:29 -07:00
ebf0b84a05 refactor: move classes migrated to dedicated class files 2025-09-30 22:58:37 -07:00
b724930c6a feat: add HDHomeRun core server functionality 2025-09-30 22:58:06 -07:00
603e444d35 refactor: migrate classes to dedicated class files; add new imports 2025-09-30 22:56:30 -07:00
f274b807f2 refactor: change the way domain checks validate a domain 2025-09-30 22:55:14 -07:00
d0c8920b98 fix(m3u): source 3 offline due to dns change 2025-09-30 22:52:56 -07:00
4c0d49508f feat: add git.binaryninja.net health check; stop overwriting m3u/epg when down
stops the app from wiping the m3u and xml file if the bit.binaryninja.net repo /website is down
2025-09-30 22:52:21 -07:00
2a09bc1ea3 feat: add new classes structure 2025-09-30 22:49:37 -07:00
259d27a2ce build: publish utils class 2025-09-30 22:47:57 -07:00
renovate[bot]
8aefbb39e0 chore(deps): lock file maintenance (#120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 04:33:48 +00:00
renovate[bot]
e417b9f5d8 chore(deps): lock file maintenance (#118)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 05:30:37 +00:00
renovate[bot]
9458587d59 chore(deps): lock file maintenance (#114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 23:27:53 +00:00
renovate[bot]
468c8c10fc chore(deps): lock file maintenance (#109)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 05:07:05 +00:00
renovate[bot]
6d90a88b60 chore(deps): lock file maintenance (#104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 13:33:04 +00:00
renovate[bot]
7231199f9e chore(deps): lock file maintenance (#101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 06:40:11 +00:00
renovate[bot]
41c0c9f685 chore(deps): lock file maintenance (#100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 10:51:11 +00:00
renovate[bot]
79c5c648c9 chore(deps): lock file maintenance (#94)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 05:56:54 +00:00
renovate[bot]
0ba2e23171 chore(deps): lock file maintenance (#91)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 06:54:22 +00:00
renovate[bot]
b0f3869621 chore(deps): lock file maintenance (#89)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 06:08:51 +00:00
renovate[bot]
b709d53e40 chore(deps): lock file maintenance (#87)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 07:34:57 +00:00
renovate[bot]
b198168d75 chore(deps): lock file maintenance (#84)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 05:35:34 +00:00
bd41ab603d build: update docker-compose.yml example
added new quiet param to healthcheck
2025-06-23 03:25:37 -07:00
0059431fbb ci: update release workflows 2025-06-23 03:19:17 -07:00
863addce39 ci: update release workflow; automatic versioning 2025-06-23 03:08:03 -07:00
07b7272eb1 build: bump version v1.5.5 2025-06-23 02:52:39 -07:00
c59de1fcf9 feat: notifications fade out after first health check 2025-06-23 02:51:35 -07:00
2d24d8e379 feat: update webui interface 2025-06-23 02:38:02 -07:00
60fd32e4d5 feat: add new query param silent 2025-06-22 21:24:08 -07:00
f32504e76b fix: tooltip positioning in webui now shows in the correct location 2025-06-22 21:23:03 -07:00
8eed126fa4 build: remove packages playwright, user-agents 2025-06-22 21:04:20 -07:00
9242cbccc4 Merge pull request #79 from TheBinaryNinja/renovate/major-eslint-stylistic-monorepo 2025-06-22 21:03:20 -07:00
renovate[bot]
a7d209b370 chore(deps): lock file maintenance (#80)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 04:01:04 +00:00
29c1b6286f ci: update workflow release 2025-06-22 20:58:46 -07:00
renovate[bot]
1ae4ab46d4 chore(deps): update dependency @stylistic/eslint-plugin to v5 2025-06-23 01:52:17 +00:00
cd33470b12 docs(readme): update 2025-06-21 15:55:50 -07:00
083feeef90 docs(readme): update 2025-06-21 15:54:23 -07:00
42f6267539 docs(readme): update 2025-06-21 15:51:45 -07:00
renovate[bot]
5f669092c2 chore(deps): lock file maintenance (#75)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 02:11:47 +00:00
9a36aad9cb docs(readme): update 2025-06-14 01:42:05 -07:00
898983f724 Update README.md 2025-06-13 18:47:56 -07:00
c35a726e93 ci: update workflows 2025-06-13 15:56:55 -07:00
9cda4061d5 ci: update roadmap issue template 2025-06-11 21:29:52 -07:00
32 changed files with 4227 additions and 1693 deletions

View File

@@ -41,6 +41,7 @@ body:
- Distribution - Distribution
- Documentation - Documentation
- M3U / EPG Functionality - M3U / EPG Functionality
- Refactor (Code)
- Repository - Repository
- S6-Overlay - S6-Overlay
default: 0 default: 0

View File

@@ -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

View File

@@ -62,18 +62,6 @@ on:
default: 'tvapp2' default: 'tvapp2'
type: string type: string
# #
# Image Version
#
# used to create new release tag, and add version to docker image name
# #
IMAGE_VERSION:
description: '🏷️ Image Version'
required: true
default: '1.0.0'
type: string
# # # #
# Registry Name # Registry Name
# #
@@ -163,8 +151,7 @@ on:
env: env:
IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }} IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }}
IMAGE_VERSION: ${{ github.event.inputs.IMAGE_VERSION || '1.0.0' }} IMAGE_REGISTRY: ${{ github.event.inputs.IMAGE_REGISTRY || 'dockerhub' }}
IMAGE_REGISTRY: ${{ github.event.inputs.IMAGE_VERSION || 'dockerhub' }}
IMAGE_DOCKERHUB_AUTHOR: ${{ github.event.inputs.IMAGE_DOCKERHUB_AUTHOR || 'thebinaryninja' }} IMAGE_DOCKERHUB_AUTHOR: ${{ github.event.inputs.IMAGE_DOCKERHUB_AUTHOR || 'thebinaryninja' }}
IMAGE_DOCKERHUB_USERNAME: ${{ github.event.inputs.IMAGE_DOCKERHUB_USERNAME || 'thebinaryninja' }} IMAGE_DOCKERHUB_USERNAME: ${{ github.event.inputs.IMAGE_DOCKERHUB_USERNAME || 'thebinaryninja' }}
IMAGE_ALPINE_VERSION: ${{ github.event.inputs.IMAGE_ALPINE_VERSION || '3.22' }} IMAGE_ALPINE_VERSION: ${{ github.event.inputs.IMAGE_ALPINE_VERSION || '3.22' }}
@@ -195,9 +182,11 @@ 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:
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
permissions: permissions:
contents: write contents: write
packages: write packages: write
@@ -223,6 +212,26 @@ jobs:
uses: qoomon/actions--context@v4 uses: qoomon/actions--context@v4
id: 'context' id: 'context'
# #
# Release Tags Set Package.json Version
# #
- name: '👁️‍🗨️ Package Version Set'
id: task_initialize_package_getversion
working-directory: ./tvapp2
run: |
VER=$(cat package.json | jq -r '.version')
echo "PACKAGE_VERSION=${VER}" >> $GITHUB_OUTPUT
echo "PACKAGE_VERSION=${VER}" >> $GITHUB_ENV
# #
# Initialize Get Package.json Version
# #
- name: '👁️‍🗨️ Package Version Get'
run: |
echo "VERSION: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}"
# # # #
# Release Tags Start # Release Tags Start
# # # #
@@ -259,10 +268,10 @@ jobs:
SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627 SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627
echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV
PKG_VER_1DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3 PKG_VER_1DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3
echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV
PKG_VER_2DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2 PKG_VER_2DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2
echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV
echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@@ -352,9 +361,9 @@ jobs:
id: task_release_tags_create id: task_release_tags_create
if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false ) if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false )
with: with:
tag: "${{ env.IMAGE_VERSION }}" tag: "${{ env.PACKAGE_VERSION }}"
tag_exists_error: false tag_exists_error: false
message: '${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}' message: '${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}'
gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }} gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }}
gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }} gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }}
@@ -365,15 +374,17 @@ 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 ]
permissions: permissions:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
id-token: write id-token: write
needs: [ job-docker-release-tags-create ] env:
PACKAGE_VERSION: ${{ needs.job-docker-release-tags-create.outputs.package_version }}
steps: steps:
# # # #
@@ -430,10 +441,10 @@ jobs:
SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627 SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627
echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV
PKG_VER_1DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3 PKG_VER_1DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3
echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV
PKG_VER_2DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2 PKG_VER_2DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2
echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV
echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@@ -616,7 +627,7 @@ jobs:
type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=,event=tag type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=,event=tag
# tag add 1.0.0 ( dispatch only + no dev ) # tag add 1.0.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.IMAGE_VERSION }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.PACKAGE_VERSION }}
# tag add 1.0 ( dispatch only + no dev ) # tag add 1.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }}
@@ -634,28 +645,28 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
@@ -684,14 +695,14 @@ jobs:
build-args: |- build-args: |-
ARCH=amd64 ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }} RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.PACKAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER }} BUILDDATE=${{ env.NOW_DOCKER }}
GIT_SHA1=${{ env.SHA1 }} GIT_SHA1=${{ env.SHA1 }}
ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }} ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.architecture=amd64 org.opencontainers.image.architecture=amd64
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
@@ -699,7 +710,7 @@ jobs:
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-architecture=amd64 org.tvapp2.image.build-architecture=amd64
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
@@ -757,14 +768,14 @@ jobs:
build-args: |- build-args: |-
ARCH=arm64 ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }} RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.PACKAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER }} BUILDDATE=${{ env.NOW_DOCKER }}
GIT_SHA1=${{ env.SHA1 }} GIT_SHA1=${{ env.SHA1 }}
ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }} ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.architecture=arm64 org.opencontainers.image.architecture=arm64
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
@@ -772,7 +783,7 @@ jobs:
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-architecture=arm64 org.tvapp2.image.build-architecture=arm64
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
@@ -824,7 +835,6 @@ jobs:
echo "" echo ""
echo "---- [ INPUTS ] ----------------------------------------------------------------------------------------" echo "---- [ INPUTS ] ----------------------------------------------------------------------------------------"
echo "inputs.IMAGE_NAME ........................ ${{ inputs.IMAGE_NAME }}" echo "inputs.IMAGE_NAME ........................ ${{ inputs.IMAGE_NAME }}"
echo "inputs.IMAGE_VERSION ..................... ${{ inputs.IMAGE_VERSION }}"
echo "inputs.IMAGE_DOCKERHUB_AUTHOR ............ ${{ inputs.IMAGE_DOCKERHUB_AUTHOR }}" echo "inputs.IMAGE_DOCKERHUB_AUTHOR ............ ${{ inputs.IMAGE_DOCKERHUB_AUTHOR }}"
echo "inputs.IMAGE_DOCKERHUB_USERNAME .......... ${{ inputs.IMAGE_DOCKERHUB_USERNAME }}" echo "inputs.IMAGE_DOCKERHUB_USERNAME .......... ${{ inputs.IMAGE_DOCKERHUB_USERNAME }}"
echo "inputs.DEV_RELEASE ....................... ${{ inputs.DEV_RELEASE }}" echo "inputs.DEV_RELEASE ....................... ${{ inputs.DEV_RELEASE }}"
@@ -832,7 +842,7 @@ jobs:
echo "" echo ""
echo "---- [ ENV ] -------------------------------------------------------------------------------------------" echo "---- [ ENV ] -------------------------------------------------------------------------------------------"
echo "env.IMAGE_NAME ........................... ${{ env.IMAGE_NAME }}" echo "env.IMAGE_NAME ........................... ${{ env.IMAGE_NAME }}"
echo "env.IMAGE_VERSION ........................ ${{ env.IMAGE_VERSION }}" echo "env.PACKAGE_VERSION ...................... ${{ env.PACKAGE_VERSION }}"
echo "env.PKG_VER_1DIGIT ....................... ${{ env.PKG_VER_1DIGIT }}" echo "env.PKG_VER_1DIGIT ....................... ${{ env.PKG_VER_1DIGIT }}"
echo "env.PKG_VER_2DIGIT ....................... ${{ env.PKG_VER_2DIGIT }}" echo "env.PKG_VER_2DIGIT ....................... ${{ env.PKG_VER_2DIGIT }}"
echo "env.IMAGE_DOCKERHUB_AUTHOR ............... ${{ env.IMAGE_DOCKERHUB_AUTHOR }}" echo "env.IMAGE_DOCKERHUB_AUTHOR ............... ${{ env.IMAGE_DOCKERHUB_AUTHOR }}"
@@ -906,20 +916,20 @@ jobs:
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-thumbnail-url: ${{ env.DISCORD_BOT_EMBED_THUMBNAIL }} embed-thumbnail-url: ${{ env.DISCORD_BOT_EMBED_THUMBNAIL }}
embed-description: | embed-description: |
### 📦 Deploy (Dockerhub) ${{ job.status == 'success' && '✅' || '❌' }} `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}` ### 📦 Deploy (Dockerhub) ${{ job.status == 'success' && '✅' || '❌' }} `${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}`
${{ inputs.DEV_RELEASE == true && '### ⚠️⚠️ Development / Pre-release ⚠️⚠️' || '' }} ${{ inputs.DEV_RELEASE == true && '### ⚠️⚠️ Development / Pre-release ⚠️⚠️' || '' }}
A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github to Dockerhub. The image is available at: A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github to Dockerhub. The image is available at:
- https://hub.docker.com/r/${{ env.IMAGE_DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} - https://hub.docker.com/r/${{ env.IMAGE_DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}
- Version: `${{ env.IMAGE_VERSION }}` - Version: `${{ env.PACKAGE_VERSION }}`
- Release Type: `${{ inputs.DEV_RELEASE == true && '⚠️⚠️ Development / Pre-release ⚠️⚠️' || 'Stable' }}` - Release Type: `${{ inputs.DEV_RELEASE == true && '⚠️⚠️ Development / Pre-release ⚠️⚠️' || 'Stable' }}`
- Pull: `docker pull ${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}:${{ inputs.DEV_RELEASE == true && 'development' || env.IMAGE_VERSION }}` - Pull: `docker pull ${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}:${{ inputs.DEV_RELEASE == true && 'development' || env.PACKAGE_VERSION }}`
- Pull (amd64): `docker pull ${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_dh_push_amd64.outputs.digest }}` - Pull (amd64): `docker pull ${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_dh_push_amd64.outputs.digest }}`
- Pull (arm64): `docker pull ${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_dh_push_arm64.outputs.digest }}` - Pull (arm64): `docker pull ${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_dh_push_arm64.outputs.digest }}`
- Dry Run: `${{ inputs.DRY_RUN }}` - Dry Run: `${{ inputs.DRY_RUN }}`
- Source: `Dockerhub` https://hub.docker.com/r/${{ env.IMAGE_DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} - Source: `Dockerhub` https://hub.docker.com/r/${{ env.IMAGE_DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}
- Docker Image: `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}` - Docker Image: `${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}`
- Branch: `${{ github.ref_name }}` - Branch: `${{ github.ref_name }}`
- Workflow: `${{ github.workflow }} (#${{github.run_number}})` - Workflow: `${{ github.workflow }} (#${{github.run_number}})`
- Runner: `${{ runner.name }}` - Runner: `${{ runner.name }}`

View File

@@ -62,18 +62,6 @@ on:
default: 'tvapp2' default: 'tvapp2'
type: string type: string
# #
# Image Version
#
# used to create new release tag, and add version to docker image name
# #
IMAGE_VERSION:
description: '🏷️ Image Version'
required: true
default: '1.0.0'
type: string
# # # #
# Registry Name # Registry Name
# #
@@ -174,8 +162,7 @@ on:
env: env:
IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }} IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }}
IMAGE_VERSION: ${{ github.event.inputs.IMAGE_VERSION || '1.0.0' }} IMAGE_REGISTRY: ${{ github.event.inputs.IMAGE_REGISTRY || 'gitea' }}
IMAGE_REGISTRY: ${{ github.event.inputs.IMAGE_VERSION || 'gitea' }}
IMAGE_GITEA_AUTHOR: ${{ github.event.inputs.IMAGE_GITEA_AUTHOR || 'BinaryNinja' }} IMAGE_GITEA_AUTHOR: ${{ github.event.inputs.IMAGE_GITEA_AUTHOR || 'BinaryNinja' }}
IMAGE_GITEA_USERNAME: ${{ github.event.inputs.IMAGE_GITEA_USERNAME || 'BinaryNinja' }} IMAGE_GITEA_USERNAME: ${{ github.event.inputs.IMAGE_GITEA_USERNAME || 'BinaryNinja' }}
IMAGE_GITEA_WEBSITE: ${{ github.event.inputs.IMAGE_GITEA_WEBSITE || 'git.binaryninja.net' }} IMAGE_GITEA_WEBSITE: ${{ github.event.inputs.IMAGE_GITEA_WEBSITE || 'git.binaryninja.net' }}
@@ -207,9 +194,11 @@ 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:
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
permissions: permissions:
contents: write contents: write
packages: write packages: write
@@ -235,6 +224,26 @@ jobs:
uses: qoomon/actions--context@v4 uses: qoomon/actions--context@v4
id: 'context' id: 'context'
# #
# Release Tags Set Package.json Version
# #
- name: '👁️‍🗨️ Package Version Set'
id: task_initialize_package_getversion
working-directory: ./tvapp2
run: |
VER=$(cat package.json | jq -r '.version')
echo "PACKAGE_VERSION=${VER}" >> $GITHUB_OUTPUT
echo "PACKAGE_VERSION=${VER}" >> $GITHUB_ENV
# #
# Initialize Get Package.json Version
# #
- name: '👁️‍🗨️ Package Version Get'
run: |
echo "VERSION: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}"
# # # #
# Release Tags Start # Release Tags Start
# # # #
@@ -271,10 +280,10 @@ jobs:
SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627 SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627
echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV
PKG_VER_1DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3 PKG_VER_1DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3
echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV
PKG_VER_2DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2 PKG_VER_2DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2
echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV
echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@@ -364,9 +373,9 @@ jobs:
id: task_release_tags_create id: task_release_tags_create
if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false ) if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false )
with: with:
tag: "${{ env.IMAGE_VERSION }}" tag: "${{ env.PACKAGE_VERSION }}"
tag_exists_error: false tag_exists_error: false
message: '${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}' message: '${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}'
gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }} gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }}
gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }} gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }}
@@ -377,15 +386,17 @@ 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 ]
permissions: permissions:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
id-token: write id-token: write
needs: [ job-docker-release-tags-create ] env:
PACKAGE_VERSION: ${{ needs.job-docker-release-tags-create.outputs.package_version }}
steps: steps:
# # # #
@@ -442,10 +453,10 @@ jobs:
SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627 SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627
echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV
PKG_VER_1DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3 PKG_VER_1DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3
echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV
PKG_VER_2DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2 PKG_VER_2DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2
echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV
echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@@ -560,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
# # # #
@@ -570,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
@@ -629,10 +670,10 @@ jobs:
type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=-amd64,event=tag type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=-amd64,event=tag
# tag add 1.0.0-amd64 ( dispatch only + no dev ) # tag add 1.0.0-amd64 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=500,prefix=,suffix=-amd64,value=${{ env.IMAGE_VERSION }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=500,prefix=,suffix=-amd64,value=${{ env.PACKAGE_VERSION }}
# tag add 1.0.0 ( dispatch only + no dev ) # tag add 1.0.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.IMAGE_VERSION }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.PACKAGE_VERSION }}
# tag add 1.0 ( dispatch only + no dev ) # tag add 1.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }}
@@ -650,28 +691,28 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
@@ -697,7 +738,7 @@ jobs:
type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=-arm64,event=tag type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=-arm64,event=tag
# tag add 1.0.0-arm64 ( dispatch only + no dev ) # tag add 1.0.0-arm64 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=500,prefix=,suffix=-arm64,value=${{ env.IMAGE_VERSION }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=500,prefix=,suffix=-arm64,value=${{ env.PACKAGE_VERSION }}
# dispatch add development-arm64 ( dispatch only + only dev ) # dispatch add development-arm64 ( dispatch only + only dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == true }},priority=300,prefix=,suffix=-arm64,value=development type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == true }},priority=300,prefix=,suffix=-arm64,value=development
@@ -706,28 +747,28 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
@@ -756,7 +797,7 @@ jobs:
type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=,event=tag type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=,event=tag
# tag add 1.0.0 ( dispatch only + no dev ) # tag add 1.0.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.IMAGE_VERSION }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.PACKAGE_VERSION }}
# tag add 1.0 ( dispatch only + no dev ) # tag add 1.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }}
@@ -774,28 +815,28 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
@@ -824,14 +865,14 @@ jobs:
build-args: |- build-args: |-
ARCH=amd64 ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }} RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.PACKAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER }} BUILDDATE=${{ env.NOW_DOCKER }}
GIT_SHA1=${{ env.SHA1 }} GIT_SHA1=${{ env.SHA1 }}
ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }} ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.architecture=amd64 org.opencontainers.image.architecture=amd64
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
@@ -839,7 +880,7 @@ jobs:
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-architecture=amd64 org.tvapp2.image.build-architecture=amd64
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
@@ -897,14 +938,14 @@ jobs:
build-args: |- build-args: |-
ARCH=arm64 ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }} RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.PACKAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER }} BUILDDATE=${{ env.NOW_DOCKER }}
GIT_SHA1=${{ env.SHA1 }} GIT_SHA1=${{ env.SHA1 }}
ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }} ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.architecture=arm64 org.opencontainers.image.architecture=arm64
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
@@ -912,7 +953,7 @@ jobs:
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-architecture=arm64 org.tvapp2.image.build-architecture=arm64
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
@@ -964,7 +1005,6 @@ jobs:
echo "" echo ""
echo "---- [ INPUTS ] ----------------------------------------------------------------------------------------" echo "---- [ INPUTS ] ----------------------------------------------------------------------------------------"
echo "inputs.IMAGE_NAME ........................ ${{ inputs.IMAGE_NAME }}" echo "inputs.IMAGE_NAME ........................ ${{ inputs.IMAGE_NAME }}"
echo "inputs.IMAGE_VERSION ..................... ${{ inputs.IMAGE_VERSION }}"
echo "inputs.IMAGE_GITEA_USERNAME .............. ${{ inputs.IMAGE_GITEA_AUTHOR }}" echo "inputs.IMAGE_GITEA_USERNAME .............. ${{ inputs.IMAGE_GITEA_AUTHOR }}"
echo "inputs.IMAGE_GITEA_USERNAME .............. ${{ inputs.IMAGE_GITEA_USERNAME }}" echo "inputs.IMAGE_GITEA_USERNAME .............. ${{ inputs.IMAGE_GITEA_USERNAME }}"
echo "inputs.IMAGE_GITEA_WEBSITE ............... ${{ inputs.IMAGE_GITEA_WEBSITE }}" echo "inputs.IMAGE_GITEA_WEBSITE ............... ${{ inputs.IMAGE_GITEA_WEBSITE }}"
@@ -973,7 +1013,7 @@ jobs:
echo "" echo ""
echo "---- [ ENV ] -------------------------------------------------------------------------------------------" echo "---- [ ENV ] -------------------------------------------------------------------------------------------"
echo "env.IMAGE_NAME ........................... ${{ env.IMAGE_NAME }}" echo "env.IMAGE_NAME ........................... ${{ env.IMAGE_NAME }}"
echo "env.IMAGE_VERSION ........................ ${{ env.IMAGE_VERSION }}" echo "env.PACKAGE_VERSION ...................... ${{ env.PACKAGE_VERSION }}"
echo "env.PKG_VER_1DIGIT ....................... ${{ env.PKG_VER_1DIGIT }}" echo "env.PKG_VER_1DIGIT ....................... ${{ env.PKG_VER_1DIGIT }}"
echo "env.PKG_VER_2DIGIT ....................... ${{ env.PKG_VER_2DIGIT }}" echo "env.PKG_VER_2DIGIT ....................... ${{ env.PKG_VER_2DIGIT }}"
echo "env.IMAGE_GITEA_AUTHOR ................... ${{ env.IMAGE_GITEA_AUTHOR }}" echo "env.IMAGE_GITEA_AUTHOR ................... ${{ env.IMAGE_GITEA_AUTHOR }}"
@@ -1052,20 +1092,20 @@ jobs:
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-thumbnail-url: ${{ env.DISCORD_BOT_EMBED_THUMBNAIL }} embed-thumbnail-url: ${{ env.DISCORD_BOT_EMBED_THUMBNAIL }}
embed-description: | embed-description: |
### 📦 Deploy (Gitea) ${{ job.status == 'success' && '✅' || '❌' }} `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}` ### 📦 Deploy (Gitea) ${{ job.status == 'success' && '✅' || '❌' }} `${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}`
${{ inputs.DEV_RELEASE == true && '### ⚠️⚠️ Development / Pre-release ⚠️⚠️' || '' }} ${{ inputs.DEV_RELEASE == true && '### ⚠️⚠️ Development / Pre-release ⚠️⚠️' || '' }}
A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github to Gitea. The image is available at: A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github to Gitea. The image is available at:
- https://${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.IMAGE_GITEA_USERNAME }}/${{ env.IMAGE_NAME }}/packages - https://${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.IMAGE_GITEA_USERNAME }}/${{ env.IMAGE_NAME }}/packages
- Version: `${{ env.IMAGE_VERSION }}` - Version: `${{ env.PACKAGE_VERSION }}`
- Release Type: `${{ inputs.DEV_RELEASE == true && '⚠️⚠️ Development / Pre-release ⚠️⚠️' || 'Stable' }}` - Release Type: `${{ inputs.DEV_RELEASE == true && '⚠️⚠️ Development / Pre-release ⚠️⚠️' || 'Stable' }}`
- Pull: `docker pull ${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}:${{ inputs.DEV_RELEASE == true && 'development' || env.IMAGE_VERSION }}` - Pull: `docker pull ${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}:${{ inputs.DEV_RELEASE == true && 'development' || env.PACKAGE_VERSION }}`
- Pull (amd64): `docker pull ${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gi_push_amd64.outputs.digest }}` - Pull (amd64): `docker pull ${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gi_push_amd64.outputs.digest }}`
- Pull (arm64): `docker pull ${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gi_push_arm64.outputs.digest }}` - Pull (arm64): `docker pull ${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gi_push_arm64.outputs.digest }}`
- Dry Run: `${{ inputs.DRY_RUN }}` - Dry Run: `${{ inputs.DRY_RUN }}`
- Source: `Gitea` https://${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.IMAGE_GITEA_USERNAME }}/${{ env.IMAGE_NAME }}/packages - Source: `Gitea` https://${{ env.IMAGE_GITEA_WEBSITE }}/${{ env.IMAGE_GITEA_USERNAME }}/${{ env.IMAGE_NAME }}/packages
- Docker Image: `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}` - Docker Image: `${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}`
- Branch: `${{ github.ref_name }}` - Branch: `${{ github.ref_name }}`
- Workflow: `${{ github.workflow }} (#${{github.run_number}})` - Workflow: `${{ github.workflow }} (#${{github.run_number}})`
- Runner: `${{ runner.name }}` - Runner: `${{ runner.name }}`

View File

@@ -62,18 +62,6 @@ on:
default: 'tvapp2' default: 'tvapp2'
type: string type: string
# #
# Image Version
#
# used to create new release tag, and add version to docker image name
# #
IMAGE_VERSION:
description: '🏷️ Image Version'
required: true
default: '1.0.0'
type: string
# # # #
# Registry Name # Registry Name
# #
@@ -162,8 +150,7 @@ on:
env: env:
IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }} IMAGE_NAME: ${{ github.event.inputs.IMAGE_NAME || 'tvapp2' }}
IMAGE_VERSION: ${{ github.event.inputs.IMAGE_VERSION || '1.0.0' }} IMAGE_REGISTRY: ${{ github.event.inputs.IMAGE_REGISTRY || 'github' }}
IMAGE_REGISTRY: ${{ github.event.inputs.IMAGE_VERSION || 'github' }}
IMAGE_GHCR_AUTHOR: ${{ github.event.inputs.IMAGE_GHCR_AUTHOR || 'BinaryNinja' }} IMAGE_GHCR_AUTHOR: ${{ github.event.inputs.IMAGE_GHCR_AUTHOR || 'BinaryNinja' }}
IMAGE_GHCR_USERNAME: ${{ github.event.inputs.IMAGE_GHCR_USERNAME || 'BinaryNinja' }} IMAGE_GHCR_USERNAME: ${{ github.event.inputs.IMAGE_GHCR_USERNAME || 'BinaryNinja' }}
IMAGE_ALPINE_VERSION: ${{ github.event.inputs.IMAGE_ALPINE_VERSION || '3.22' }} IMAGE_ALPINE_VERSION: ${{ github.event.inputs.IMAGE_ALPINE_VERSION || '3.22' }}
@@ -194,9 +181,11 @@ 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:
package_version: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}
permissions: permissions:
contents: write contents: write
packages: write packages: write
@@ -222,6 +211,26 @@ jobs:
uses: qoomon/actions--context@v4 uses: qoomon/actions--context@v4
id: 'context' id: 'context'
# #
# Release Tags Set Package.json Version
# #
- name: '👁️‍🗨️ Package Version Set'
id: task_initialize_package_getversion
working-directory: ./tvapp2
run: |
VER=$(cat package.json | jq -r '.version')
echo "PACKAGE_VERSION=${VER}" >> $GITHUB_OUTPUT
echo "PACKAGE_VERSION=${VER}" >> $GITHUB_ENV
# #
# Initialize Get Package.json Version
# #
- name: '👁️‍🗨️ Package Version Get'
run: |
echo "VERSION: ${{ steps.task_initialize_package_getversion.outputs.PACKAGE_VERSION }}"
# # # #
# Release Tags Start # Release Tags Start
# # # #
@@ -258,10 +267,10 @@ jobs:
SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627 SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627
echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV
PKG_VER_1DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3 PKG_VER_1DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3
echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV
PKG_VER_2DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2 PKG_VER_2DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2
echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV
echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@@ -323,7 +332,7 @@ jobs:
echo "" echo ""
# # # #
# Tags Tags Fix Permissions # Release Tags Fix Permissions
# # # #
- name: '#️⃣ Manage Permissions' - name: '#️⃣ Manage Permissions'
@@ -349,9 +358,9 @@ jobs:
- uses: rickstaa/action-create-tag@v1 - uses: rickstaa/action-create-tag@v1
if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false ) if: ( github.event_name != 'workflow_dispatch' && inputs.DRY_RUN == false )
with: with:
tag: "${{ env.IMAGE_VERSION }}" tag: "${{ env.PACKAGE_VERSION }}"
tag_exists_error: false tag_exists_error: false
message: '${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}' message: '${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}'
gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }} gpg_private_key: ${{ secrets.ADMINSERV_GPG_KEY_ASC }}
gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }} gpg_passphrase: ${{ secrets.ADMINSERV_GPG_PASSPHRASE }}
@@ -362,15 +371,17 @@ 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 ]
permissions: permissions:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
id-token: write id-token: write
needs: [ job-docker-release-tags-create ] env:
PACKAGE_VERSION: ${{ needs.job-docker-release-tags-create.outputs.package_version }}
steps: steps:
# # # #
@@ -427,10 +438,10 @@ jobs:
SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627 SHA1_GH="$(echo ${GITHUB_SHA})" # 71fad013cfce9116ec62779e4a7e627fe4c33627
echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV echo "SHA1_GH=${SHA1_GH}" >> $GITHUB_ENV
PKG_VER_1DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3 PKG_VER_1DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -d '.' -f1-1)" # 3.22 > 3
echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV echo "PKG_VER_1DIGIT=${PKG_VER_1DIGIT}" >> $GITHUB_ENV
PKG_VER_2DIGIT="$(echo ${{ env.IMAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2 PKG_VER_2DIGIT="$(echo ${{ env.PACKAGE_VERSION }} | cut -f2 -d ":" | cut -c1-3)" # 3.22 > 3.2
echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV echo "PKG_VER_2DIGIT=${PKG_VER_2DIGIT}" >> $GITHUB_ENV
echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@@ -611,7 +622,7 @@ jobs:
type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=,event=tag type=ref,enable=${{ github.event_name == 'pull_request' || github.event_name == 'push' }},priority=600,prefix=,suffix=,event=tag
# tag add 1.0.0 ( dispatch only + no dev ) # tag add 1.0.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.IMAGE_VERSION }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=450,prefix=,suffix=,value=${{ env.PACKAGE_VERSION }}
# tag add 1.0 ( dispatch only + no dev ) # tag add 1.0 ( dispatch only + no dev )
type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }} type=raw,enable=${{ github.event_name == 'workflow_dispatch' && inputs.DEV_RELEASE == false }},priority=425,prefix=,suffix=,value=${{ env.PKG_VER_2DIGIT }}
@@ -629,28 +640,28 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }} org.opencontainers.image.vendor=${{ env.REGISTRY_REPO_AUTHOR_LC }}
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
org.tvapp2.image.build-sha1=${{ env.SHA1 }} org.tvapp2.image.build-sha1=${{ env.SHA1 }}
@@ -679,14 +690,14 @@ jobs:
build-args: |- build-args: |-
ARCH=amd64 ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }} RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.PACKAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER }} BUILDDATE=${{ env.NOW_DOCKER }}
GIT_SHA1=${{ env.SHA1 }} GIT_SHA1=${{ env.SHA1 }}
ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }} ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.architecture=amd64 org.opencontainers.image.architecture=amd64
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
@@ -694,7 +705,7 @@ jobs:
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-architecture=amd64 org.tvapp2.image.build-architecture=amd64
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
@@ -750,14 +761,14 @@ jobs:
build-args: |- build-args: |-
ARCH=arm64 ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }} RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.PACKAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER }} BUILDDATE=${{ env.NOW_DOCKER }}
GIT_SHA1=${{ env.SHA1 }} GIT_SHA1=${{ env.SHA1 }}
ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }} ALPINE_VERSION=${{ env.IMAGE_ALPINE_VERSION }}
annotations: |- annotations: |-
org.opencontainers.image.description=TVApp2 org.opencontainers.image.description=TVApp2
org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }} org.opencontainers.image.created=${{ env.NOW_DOCKER_TS }}
org.opencontainers.image.version=${{ env.IMAGE_VERSION }} org.opencontainers.image.version=${{ env.PACKAGE_VERSION }}
org.opencontainers.image.licenses=MIT org.opencontainers.image.licenses=MIT
org.opencontainers.image.architecture=arm64 org.opencontainers.image.architecture=arm64
org.opencontainers.image.revision=${{ env.SHA1 }} org.opencontainers.image.revision=${{ env.SHA1 }}
@@ -765,7 +776,7 @@ jobs:
org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.ref.name=${{ github.ref_name }}
org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }} org.opencontainers.image.development=${{ inputs.DEV_RELEASE == true && 'true' || 'false' }}
org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }} org.opencontainers.image.registry=${{ env.IMAGE_REGISTRY }}
org.tvapp2.image.build-version="Version:- ${{ env.IMAGE_VERSION }} Date:- ${{ env.NOW_DOCKER }}" org.tvapp2.image.build-version="Version: ${{ env.PACKAGE_VERSION }} Date: ${{ env.NOW_DOCKER }}"
org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }} org.tvapp2.image.build-version-alpine=${{ env.IMAGE_ALPINE_VERSION }}
org.tvapp2.image.build-architecture=arm64 org.tvapp2.image.build-architecture=arm64
org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}" org.tvapp2.image.build-release="${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}"
@@ -815,7 +826,6 @@ jobs:
echo "" echo ""
echo "---- [ INPUTS ] ----------------------------------------------------------------------------------------" echo "---- [ INPUTS ] ----------------------------------------------------------------------------------------"
echo "inputs.IMAGE_NAME ........................ ${{ inputs.IMAGE_NAME }}" echo "inputs.IMAGE_NAME ........................ ${{ inputs.IMAGE_NAME }}"
echo "inputs.IMAGE_VERSION ..................... ${{ inputs.IMAGE_VERSION }}"
echo "inputs.IMAGE_GHCR_AUTHOR ................. ${{ inputs.IMAGE_GHCR_AUTHOR }}" echo "inputs.IMAGE_GHCR_AUTHOR ................. ${{ inputs.IMAGE_GHCR_AUTHOR }}"
echo "inputs.IMAGE_GHCR_USERNAME ............... ${{ inputs.IMAGE_GHCR_USERNAME }}" echo "inputs.IMAGE_GHCR_USERNAME ............... ${{ inputs.IMAGE_GHCR_USERNAME }}"
echo "inputs.DEV_RELEASE ....................... ${{ inputs.DEV_RELEASE }}" echo "inputs.DEV_RELEASE ....................... ${{ inputs.DEV_RELEASE }}"
@@ -823,7 +833,7 @@ jobs:
echo "" echo ""
echo "---- [ ENV ] -------------------------------------------------------------------------------------------" echo "---- [ ENV ] -------------------------------------------------------------------------------------------"
echo "env.IMAGE_NAME ........................... ${{ env.IMAGE_NAME }}" echo "env.IMAGE_NAME ........................... ${{ env.IMAGE_NAME }}"
echo "env.IMAGE_VERSION ........................ ${{ env.IMAGE_VERSION }}" echo "env.PACKAGE_VERSION ...................... ${{ env.PACKAGE_VERSION }}"
echo "env.PKG_VER_1DIGIT ....................... ${{ env.PKG_VER_1DIGIT }}" echo "env.PKG_VER_1DIGIT ....................... ${{ env.PKG_VER_1DIGIT }}"
echo "env.PKG_VER_2DIGIT ....................... ${{ env.PKG_VER_2DIGIT }}" echo "env.PKG_VER_2DIGIT ....................... ${{ env.PKG_VER_2DIGIT }}"
echo "env.IMAGE_GHCR_AUTHOR .................... ${{ env.IMAGE_GHCR_AUTHOR }}" echo "env.IMAGE_GHCR_AUTHOR .................... ${{ env.IMAGE_GHCR_AUTHOR }}"
@@ -897,20 +907,20 @@ jobs:
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-thumbnail-url: ${{ env.DISCORD_BOT_EMBED_THUMBNAIL }} embed-thumbnail-url: ${{ env.DISCORD_BOT_EMBED_THUMBNAIL }}
embed-description: | embed-description: |
### 📦 Deploy (Github) ${{ job.status == 'success' && '✅' || '❌' }} `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}` ### 📦 Deploy (Github) ${{ job.status == 'success' && '✅' || '❌' }} `${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}`
${{ inputs.DEV_RELEASE == true && '### ⚠️⚠️ Development / Pre-release ⚠️⚠️' || '' }} ${{ inputs.DEV_RELEASE == true && '### ⚠️⚠️ Development / Pre-release ⚠️⚠️' || '' }}
A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github to Github GHCR. The image is available at: A new version of the docker container `${{ env.IMAGE_NAME }}` has been released from Github to Github GHCR. The image is available at:
- https://github.com/${{ github.repository }}/pkgs/container/${{ env.IMAGE_NAME }} - https://github.com/${{ github.repository }}/pkgs/container/${{ env.IMAGE_NAME }}
- Version: `${{ env.IMAGE_VERSION }}` - Version: `${{ env.PACKAGE_VERSION }}`
- Release Type: `${{ inputs.DEV_RELEASE == true && '⚠️⚠️ Development / Pre-release ⚠️⚠️' || 'Stable' }}` - Release Type: `${{ inputs.DEV_RELEASE == true && '⚠️⚠️ Development / Pre-release ⚠️⚠️' || 'Stable' }}`
- Pull: `docker pull ghcr.io/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}:${{ inputs.DEV_RELEASE == true && 'development' || env.IMAGE_VERSION }}` - Pull: `docker pull ghcr.io/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}:${{ inputs.DEV_RELEASE == true && 'development' || env.PACKAGE_VERSION }}`
- Pull (amd64): `docker pull ghcr.io/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gh_push_amd64.outputs.digest }}` - Pull (amd64): `docker pull ghcr.io/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gh_push_amd64.outputs.digest }}`
- Pull (arm64): `docker pull ghcr.io/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gh_push_arm64.outputs.digest }}` - Pull (arm64): `docker pull ghcr.io/${{ env.REGISTRY_REPO_ORG_AUTHOR_LC }}@${{ steps.task_release_gh_push_arm64.outputs.digest }}`
- Dry Run: `${{ inputs.DRY_RUN }}` - Dry Run: `${{ inputs.DRY_RUN }}`
- Source: `Github` https://github.com/${{ github.repository }} - Source: `Github` https://github.com/${{ github.repository }}
- Docker Image: `${{ env.IMAGE_NAME }}-${{ env.IMAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}` - Docker Image: `${{ env.IMAGE_NAME }}-${{ env.PACKAGE_VERSION }}${{ inputs.DEV_RELEASE == true && '-development' || '' }}`
- Branch: `${{ github.ref_name }}` - Branch: `${{ github.ref_name }}`
- Workflow: `${{ github.workflow }} (#${{github.run_number}})` - Workflow: `${{ github.workflow }} (#${{github.run_number}})`
- Runner: `${{ runner.name }}` - Runner: `${{ runner.name }}`
@@ -944,15 +954,17 @@ 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 ]
permissions: permissions:
contents: write contents: write
packages: write packages: write
attestations: write attestations: write
id-token: write id-token: write
needs: [ job-docker-release-tags-create, job-docker-release-github ] env:
PACKAGE_VERSION: ${{ needs.job-docker-release-tags-create.outputs.package_version }}
steps: steps:
# # # #
@@ -964,6 +976,14 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
# #
# Release Cleanup Print Version Debug
# #
- name: '🪪 Get Package Version'
run: |
echo "VERSION: ${{ env.PACKAGE_VERSION }}"
# # # #
# Release Cleanup Clean Untagged Images # Release Cleanup Clean Untagged Images
# # # #

View File

@@ -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

View File

@@ -67,37 +67,25 @@ on:
type: string type: string
# # # #
# Main Branch # Branch
# #
# main branch re-recreate # select branch to clean
# you must also run the workflow from that branch
# # # #
BRANCH_MAIN: BRANCH:
description: '🌳 Main Branch' description: '🌳 Branch'
required: true required: true
default: 'main' default: 'main'
type: string type: string
# #
# Deployment Environment Name
#
# this is the name of the deployment item
# #
DEPLOYMENT_ENV:
description: '📦 Deployment Environment'
required: true
default: 'orion'
type: string
# # # #
# environment variables # environment variables
# # # #
env: env:
COMMIT_LABEL: ${{ github.event.inputs.COMMIT_LABEL || 'cleanup' }} COMMIT_LABEL: ${{ github.event.inputs.COMMIT_LABEL || 'cleanup' }}
BRANCH_MAIN: ${{ github.event.inputs.BRANCH_MAIN || 'main' }} BRANCH: ${{ github.event.inputs.BRANCH || 'main' }}
DEPLOYMENT_ENV: ${{ github.event.inputs.DEPLOYMENT_ENV || 'orion' }}
BOT_NAME_1: EuropaServ BOT_NAME_1: EuropaServ
BOT_NAME_2: BinaryServ BOT_NAME_2: BinaryServ
BOT_NAME_DEPENDABOT: dependabot[bot] BOT_NAME_DEPENDABOT: dependabot[bot]
@@ -111,9 +99,11 @@ jobs:
history-clean: history-clean:
name: >- name: >-
🧹 History Clean 🧹 History Clean
# runs-on: ubuntu-latest runs-on: ubuntu-latest
runs-on: apollo-x64 # runs-on: apollo-x64
timeout-minutes: 5 timeout-minutes: 15
permissions:
contents: write
steps: steps:
# # # #
@@ -254,7 +244,7 @@ jobs:
now=$(date -u '+%m/%d/%Y %H:%M') now=$(date -u '+%m/%d/%Y %H:%M')
commit_label="${{ env.COMMIT_LABEL }}" >> $GITHUB_ENV commit_label="${{ env.COMMIT_LABEL }}" >> $GITHUB_ENV
echo -e "$commit_label" echo -e "$commit_label"
commit_message="\\\`️️🧹 $commit_label 🧹\\\` \\\`$now UTC\\\`" >> $GITHUB_ENV commit_message="chore(maint): \\\`️️🧹 $commit_label 🧹\\\` \\\`$now UTC\\\`" >> $GITHUB_ENV
echo -e "$commit_message" echo -e "$commit_message"
echo "COMMIT_MESSAGE=$(echo $commit_message)" >> $GITHUB_ENV echo "COMMIT_MESSAGE=$(echo $commit_message)" >> $GITHUB_ENV
echo "NOW=$(echo $now)" >> $GITHUB_ENV echo "NOW=$(echo $now)" >> $GITHUB_ENV
@@ -290,24 +280,25 @@ jobs:
git commit -m "${{ env.COMMIT_MESSAGE }}" git commit -m "${{ env.COMMIT_MESSAGE }}"
# Delete the old main branch # Delete the old main branch
git branch -D ${{ env.BRANCH_MAIN }} git branch -D ${{ env.BRANCH }}
# Rename the new orphan branch to main # Rename the new orphan branch to main
git branch -m ${{ env.BRANCH_MAIN }} git branch -m ${{ env.BRANCH }}
# Force push the new main branch to the remote repository # Force push the new main branch to the remote repository
git push -f origin ${{ env.BRANCH_MAIN }} git push -f origin ${{ env.BRANCH }}
# # # #
# History Clean References # History Clean References
# # # #
- name: >- - name: >-
🗑️ Clean References 🗑️ Garbage Collection (Aggressive)
run: | run: |
# Remove remote-tracking references to deleted branches (optional) # Remove remote-tracking references to deleted branches (optional)
git fetch origin --prune git fetch origin --prune
git repack
git prune-packed
git reflog expire --expire=now --all git reflog expire --expire=now --all
git gc --prune=now --aggressive git gc --prune=now --aggressive

View File

@@ -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:
@@ -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'
@@ -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'
@@ -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

View File

@@ -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

View File

@@ -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:
@@ -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:
@@ -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

View File

@@ -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'

View File

@@ -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'

View File

@@ -53,8 +53,8 @@ on:
type: string type: string
# # # #
# ENABLE: the changelog generated in releases tab will only display single commits. # true the changelog generated in releases tab will only display single commits.
# DISABLE: the changelog shows pull requests completed based on their labels # false the changelog shows pull requests completed based on their labels
# # # #
CHANGELOG_MODE_COMMIT: CHANGELOG_MODE_COMMIT:
@@ -64,10 +64,10 @@ on:
type: boolean type: boolean
# # # #
# ENABLE: Will show all types of commits, including uncategorized # true Will show all types of commits, including uncategorized
# DISABLE: WIll only show actions that have been categorized using the format # false WIll only show actions that have been categorized using the format
# type(scope): description # type(scope): description
# type: description # type: description
# # # #
SHOW_UNCATEGORIZED: SHOW_UNCATEGORIZED:
@@ -256,7 +256,6 @@ jobs:
# # # #
- name: '🪪 Test Next Job Version' - name: '🪪 Test Next Job Version'
id: task_release_debug_print_ver
run: | run: |
echo "VERSION: ${{ env.PACKAGE_VERSION }}" echo "VERSION: ${{ env.PACKAGE_VERSION }}"
@@ -265,7 +264,6 @@ jobs:
# # # #
- name: '🪪 NPM Install & Lint' - name: '🪪 NPM Install & Lint'
id: task_release_npm_install
working-directory: ./tvapp2 working-directory: ./tvapp2
run: | run: |
npm ci npm ci
@@ -278,7 +276,6 @@ jobs:
# # # #
- name: '🪪 Generate IDs' - name: '🪪 Generate IDs'
id: task_release_npm_env_generate
working-directory: ./tvapp2 working-directory: ./tvapp2
run: | run: |
npm run root:generate npm run root:generate
@@ -300,7 +297,6 @@ jobs:
# # # #
- name: '🪪 .ENV Read' - name: '🪪 .ENV Read'
id: task_dotenv_debug_print
run: | run: |
echo "GUID: ${{ steps.task_release_dotenv_get.outputs.GUID }}" echo "GUID: ${{ steps.task_release_dotenv_get.outputs.GUID }}"
echo "UUID: ${{ steps.task_release_dotenv_get.outputs.UUID }}" echo "UUID: ${{ steps.task_release_dotenv_get.outputs.UUID }}"
@@ -310,7 +306,6 @@ jobs:
# # # #
- name: '🔨 Build Stable ${{ env.PROJECT_NAME }}-${{ env.PACKAGE_VERSION }}.zip' - name: '🔨 Build Stable ${{ env.PROJECT_NAME }}-${{ env.PACKAGE_VERSION }}.zip'
id: task_release_build_st
if: | if: |
startsWith( inputs.RC_RELEASE, false ) || startsWith( inputs.RC_RELEASE, false ) ||
startsWith( env.RC_RELEASE, false ) startsWith( env.RC_RELEASE, false )
@@ -320,6 +315,7 @@ jobs:
echo Building STABLE Package ${{ env.PROJECT_NAME }}-${{ env.PACKAGE_VERSION }}-docker-compose.zip echo Building STABLE Package ${{ env.PROJECT_NAME }}-${{ env.PACKAGE_VERSION }}-docker-compose.zip
zip -r ${{ env.PROJECT_NAME }}-${{ env.PACKAGE_VERSION }}-docker-compose.zip docker-compose.yml README.md LICENSE zip -r ${{ env.PROJECT_NAME }}-${{ env.PACKAGE_VERSION }}-docker-compose.zip docker-compose.yml README.md LICENSE
ls
env: env:
NODE_AUTH_TOKEN: ${{ secrets.ADMINSERV_TOKEN_CL }} NODE_AUTH_TOKEN: ${{ secrets.ADMINSERV_TOKEN_CL }}
@@ -366,7 +362,6 @@ jobs:
# # # #
- name: '🔖 Tag Confirm ${{ env.PACKAGE_VERSION }}' - name: '🔖 Tag Confirm ${{ env.PACKAGE_VERSION }}'
id: task_release_tag_get
run: | run: |
echo "Tag already present: ${{ env.TAG_EXISTS }}" echo "Tag already present: ${{ env.TAG_EXISTS }}"
echo "Tag already present: ${{ steps.task_release_tag_create.outputs.tag_exists }}" echo "Tag already present: ${{ steps.task_release_tag_create.outputs.tag_exists }}"
@@ -378,7 +373,6 @@ jobs:
# # # #
- name: '🪪 GPG Import Signing Key W/o Passphrase' - name: '🪪 GPG Import Signing Key W/o Passphrase'
id: task_release_gpg_import_nopass
if: env.GPG_KEY_BASE64 != '' && env.GPG_KEY_PASSPHRASE == '' if: env.GPG_KEY_BASE64 != '' && env.GPG_KEY_PASSPHRASE == ''
run: | run: |
echo $GPG_KEY_BASE64 | base64 -di | gpg --import echo $GPG_KEY_BASE64 | base64 -di | gpg --import
@@ -390,7 +384,6 @@ jobs:
# # # #
- name: '🪪 GPG Import Signing Key w/ Passphrase' - name: '🪪 GPG Import Signing Key w/ Passphrase'
id: task_release_gpg_import_withpass
if: env.GPG_KEY_BASE64 != '' && env.GPG_KEY_PASSPHRASE != '' if: env.GPG_KEY_BASE64 != '' && env.GPG_KEY_PASSPHRASE != ''
run: | run: |
echo "$GPG_KEY_BASE64" | base64 -di > /tmp/signing-key.gpg echo "$GPG_KEY_BASE64" | base64 -di > /tmp/signing-key.gpg
@@ -472,16 +465,15 @@ jobs:
# # # #
- name: '🆔 Checksum Print' - name: '🆔 Checksum Print'
id: task_release_checksum_st_get
run: | run: |
echo "${{ env.SHA256SUM }}" echo SHA1SUM ............... ${{ env.SHA1SUM }}
echo SHA256SUM ............. ${{ env.SHA256SUM }}
# # # #
# Release Contributor Images # Release Contributor Images
# # # #
- name: '🥸 Contributors Generate' - name: '🥸 Contributors Generate'
id: task_release_contribs_generate
uses: jaywcjlove/github-action-contributors@main uses: jaywcjlove/github-action-contributors@main
with: with:
filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\]) filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\])
@@ -491,7 +483,7 @@ jobs:
# # # #
# Release Changelog Generate Tags # Release Changelog Generate Tags
# #
# generates a changelog from the github api. requires a PREVIOUS_TAG in order to figure # generates a changelog from the github api. requires a TAG_LAST in order to figure
# out the changes made between the two versions. # out the changes made between the two versions.
# #
# outputs: # outputs:
@@ -499,7 +491,6 @@ jobs:
# # # #
- name: '📝 Changelog Pre Setup (Categorized Commits)' - name: '📝 Changelog Pre Setup (Categorized Commits)'
id: task_release_changelog_categorized_sha_set
run: | run: |
echo "TAG_LAST=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV echo "TAG_LAST=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
echo "COMMIT_LAST=$(git rev-parse HEAD)" >> $GITHUB_ENV echo "COMMIT_LAST=$(git rev-parse HEAD)" >> $GITHUB_ENV
@@ -674,7 +665,7 @@ jobs:
if: | if: |
startsWith( inputs.RC_RELEASE, false ) || startsWith( inputs.RC_RELEASE, false ) ||
startsWith( env.RC_RELEASE, false ) startsWith( env.RC_RELEASE, false )
uses: softprops/action-gh-release@v2.2.2 uses: softprops/action-gh-release@v2
env: env:
CHANGELOG_CATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }} CHANGELOG_CATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }}
CHANGELOG_UNCATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }} CHANGELOG_UNCATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }}
@@ -712,7 +703,7 @@ jobs:
if: | if: |
startsWith( inputs.RC_RELEASE, true ) || startsWith( inputs.RC_RELEASE, true ) ||
startsWith( env.RC_RELEASE, true ) startsWith( env.RC_RELEASE, true )
uses: softprops/action-gh-release@v2.2.2 uses: softprops/action-gh-release@v2
env: env:
CHANGELOG_CATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }} CHANGELOG_CATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }}
CHANGELOG_UNCATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }} CHANGELOG_UNCATEGORIZED: ${{ steps.task_release_changelog_categorized.outputs.changelog }}
@@ -811,7 +802,7 @@ jobs:
job-complete: job-complete:
name: >- name: >-
🆗 Successful Deployment 🆗 Successful Deployment
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ job-initialize, job-release ] needs: [ job-initialize, job-release ]
env: env:
@@ -872,7 +863,7 @@ jobs:
echo "This is the main release. It contains all required docker files, and the TVApp2 `package.json`" >> $GITHUB_STEP_SUMMARY echo "This is the main release. It contains all required docker files, and the TVApp2 `package.json`" >> $GITHUB_STEP_SUMMARY
echo "| File | Result |" >> $GITHUB_STEP_SUMMARY echo "| File | Result |" >> $GITHUB_STEP_SUMMARY
echo "| ------------------------------- | ----------------------- |" >> $GITHUB_STEP_SUMMARY echo "| ------------------------------- | ----------------------- |" >> $GITHUB_STEP_SUMMARY
echo "| 🏷️ **SHA256** | ${{ env.SHA_STABLE }} |" >> $GITHUB_STEP_SUMMARY echo "| 🏷️ **SHA256** | ${{ env.SHA_STABLE }} |" >> $GITHUB_STEP_SUMMARY
echo "| 🏷️ **GUID** | ${{ env.GUID }} |" >> $GITHUB_STEP_SUMMARY echo "| 🏷️ **GUID** | ${{ env.GUID }} |" >> $GITHUB_STEP_SUMMARY
echo "| 🏷️ **UUID** | ${{ env.UUID }} |" >> $GITHUB_STEP_SUMMARY echo "| 🏷️ **UUID** | ${{ env.UUID }} |" >> $GITHUB_STEP_SUMMARY

View File

@@ -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"

View File

@@ -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

101
README.md
View File

@@ -86,10 +86,11 @@
- [Labels](#labels-1) - [Labels](#labels-1)
- [Dynamic.yml](#dynamicyml-1) - [Dynamic.yml](#dynamicyml-1)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Run Error: `Error serving playlist: ENOENT: no such file or directory, open /usr/src/app/xmltv.1.xml`](#run-error-error-serving-playlist-enoent-no-such-file-or-directory-open-usrsrcappxmltv1xml) - [Run Error: `Warning: Step size 60 higher than possible maximum of 59`](#run-error-warning-step-size-60-higher-than-possible-maximum-of-59)
- [Build Error: `s6-rc-compile: fatal: invalid /etc/s6-overlay/s6-rc.d/certsync/type: must be oneshot, longrun, or bundle`](#build-error-s6-rc-compile-fatal-invalid-etcs6-overlays6-rcdcertsynctype-must-be-oneshot-longrun-or-bundle) - [Run Error: `Error serving playlist: ENOENT: no such file or directory, open /usr/src/app/xmltv.1.xml`](#run-error-error-serving-playlist-enoent-no-such-file-or-directory-open-usrsrcappxmltv1xml)
- [Build Error: `unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied`](#build-error-unable-to-exec-etcs6-overlays6-rcdinit-envfilerun-permission-denied) - [Build Error: `s6-rc-compile: fatal: invalid /etc/s6-overlay/s6-rc.d/certsync/type: must be oneshot, longrun, or bundle`](#build-error-s6-rc-compile-fatal-invalid-etcs6-overlays6-rcdcertsynctype-must-be-oneshot-longrun-or-bundle)
- [Build Error: `[ERR] [27] Jellyfin.LiveTv.Guide.GuideManager: Error getting programs for channel XXXXXXXXXXXXXXX (Source 2) System.Xml.XmlException: '', hexadecimal value 0x1F, is an invalid character. Line 1, position 1.`](#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) - [Build Error: `unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied`](#build-error-unable-to-exec-etcs6-overlays6-rcdinit-envfilerun-permission-denied)
- [Build Error: `[ERR] [27] Jellyfin.LiveTv.Guide.GuideManager: Error getting programs for channel XXXXXXXXXXXXXXX (Source 2) System.Xml.XmlException: '', hexadecimal value 0x1F, is an invalid character. Line 1, position 1.`](#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)
- [Extra Notes](#extra-notes) - [Extra Notes](#extra-notes)
- [Accessing Container Shell](#accessing-container-shell) - [Accessing Container Shell](#accessing-container-shell)
- [ash](#ash) - [ash](#ash)
@@ -183,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 |
@@ -411,13 +413,14 @@ If the listed tasks above are not performed, your docker container will throw th
- `/etc/s6-overlay/s6-rc.d/init-adduser/run: line 34: aetherxown: command not found` - `/etc/s6-overlay/s6-rc.d/init-adduser/run: line 34: aetherxown: command not found`
- `/etc/s6-overlay/s6-rc.d/init-adduser/run: /usr/bin/aetherxown: cannot execute: required file not found` - `/etc/s6-overlay/s6-rc.d/init-adduser/run: /usr/bin/aetherxown: cannot execute: required file not found`
<br />
<br /> <br />
#### LF over CRLF #### LF over CRLF
You cannot utilize Windows' `Carriage Return Line Feed`. All files must be converted to Unix' `Line Feed`. This can be done with **[Visual Studio Code](https://code.visualstudio.com/)**. OR; you can run the Linux terminal command `🗔 dos2unix` to convert these files. You cannot utilize Windows' `Carriage Return Line Feed`. All files must be converted to Unix' `Line Feed`. This can be done with **[Visual Studio Code](https://code.visualstudio.com/)**. OR; you can run the Linux terminal command `🗔 dos2unix` to convert these files.
If you cloned the files from the official repository [🔆 gitea:binaryninja/tvapp2](https://git.binaryninja.net/binaryninja/tvapp2) and have not edited them, then you should not need to do this step. For the branches **[docker/alpine-base](https://github.com/Aetherinox/docker-base-alpine/tree/docker/alpine-base)** and your main app image, you can use the following recursive commands:
<br /> <br />
@@ -437,14 +440,57 @@ If you cloned the files from the official repository [🔆 gitea:binaryninja/tva
find ./ -type f | grep -Ev 'docs|node_modules|.git|*.jpg|*.jpeg|*.png' | xargs dos2unix -- find ./ -type f | grep -Ev 'docs|node_modules|.git|*.jpg|*.jpeg|*.png' | xargs dos2unix --
# Change run / binaries # Change run / binaries
find ./ -type f -name 'run' | xargs dos2unix -- find ./ -type f -name 'run' -print | xargs dos2unix --
``` ```
<br /> <br />
For the branch **[docker/core](https://github.com/Aetherinox/docker-base-alpine/tree/docker/core)**, you can use the following commands:
```shell
dos2unix docker-images.v3
dos2unix aetherxown.v1
dos2unix package-install.v1
dos2unix with-contenv.v1
```
<br />
If you do not have dos2unix; you may use `sed:
```shell
sed -i 's/\r$//' /etc/s6-overlay/s6-rc.d/ci-service-check/file
```
<br />
You may pre-check if a file is using Windows CRLF or Linux LF by running the command `file <filename>` on the file:
```shell
$ file ./root//etc/s6-overlay/s6-rc.d/ci-service-check/type
./root//etc/s6-overlay/s6-rc.d/ci-service-check/type: ASCII text
```
<br />
You will get one of three messages listed below:
1. ASCII text, with CRLF, LF line terminators
2. ASCII text, with CRLF line terminators
3. ASCII text
<br />
If you get messages `1` or `2`, then you need to run `dos2unix` on the file; otherwise when you bring the container up, you will get errors.
<br />
<br />
#### Set `+x / 0755` Permissions #### Set `+x / 0755` Permissions
The files contained within this repo **MUST** have `chmod 755` / `+x` executable permissions. The files contained within this repo **MUST** have `chmod 755` / `+x` executable permissions. If you are using our Github workflow sample **[deploy-docker-github.yml](https://github.com/Aetherinox/docker-base-alpine/blob/workflows/samples/deploy-docker-github.yml)**, this is done automatically. If you are building the images manually; you need to do this. Ensure those files have the correct permissions prior to building the Alpine base docker image.
If you are building the **[docker/alpine-base](https://github.com/Aetherinox/docker-base-alpine/tree/docker/alpine-base)** or your main application images, you must ensure the files in those branches have the proper permissions. All of the executable files are named `run`:
```shell ```shell
find ./ -name 'run' -print -exec sudo chmod +x {} \; find ./ -name 'run' -print -exec sudo chmod +x {} \;
@@ -472,6 +518,17 @@ sudo chmod +x ./root/etc/s6-overlay/s6-rc.d/init-adduser/run \
./root/etc/s6-overlay/s6-rc.d/init-nginx/run ./root/etc/s6-overlay/s6-rc.d/init-nginx/run
``` ```
<br />
For the branch **[docker/core](https://github.com/Aetherinox/docker-base-alpine/tree/docker/core)**, there are a few files to change. The ending version number may change, but the commands to change the permissions are as follows:
```shell
sudo chmod +x docker-images.v3 \
chmod +x aetherxown.v1 \
chmod +x package-install.v1 \
chmod +x with-contenv.v1
```
<br /> <br />
<br /> <br />
@@ -1262,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 |
@@ -1978,7 +2036,28 @@ If you have issues building your TVApp2 docker image, please refer to the follow
<br /> <br />
<br /> <br />
#### Run Error: `Error serving playlist: ENOENT: no such file or directory, open /usr/src/app/xmltv.1.xml` ### Run Error: `Warning: Step size 60 higher than possible maximum of 59`
This error means that you have placed an incorrect value for a cron job. This error can show if you've set:
```shell
environment:
TASK_CRON_SYNC: "*/60 * * * *"
```
<br />
To correctly set the value, change your cron to:
```shell
environment:
TASK_CRON_SYNC: "0 */1 * * *"
```
<br />
<br />
### Run Error: `Error serving playlist: ENOENT: no such file or directory, open /usr/src/app/xmltv.1.xml`
This error occurs at run-time when attempting to spin up your TVApp2 docker container. If you receive this error, restart your TVApp2 docker container. Ensure that your docker container also has access to your docker network so that it can connect to our repository and fetch the data files it needs to generate your playlist. This error occurs at run-time when attempting to spin up your TVApp2 docker container. If you receive this error, restart your TVApp2 docker container. Ensure that your docker container also has access to your docker network so that it can connect to our repository and fetch the data files it needs to generate your playlist.
@@ -1989,7 +2068,7 @@ If the error continues after doing the above; delete the existing image, and re-
<br /> <br />
<br /> <br />
#### Build Error: `s6-rc-compile: fatal: invalid /etc/s6-overlay/s6-rc.d/certsync/type: must be oneshot, longrun, or bundle` ### Build Error: `s6-rc-compile: fatal: invalid /etc/s6-overlay/s6-rc.d/certsync/type: must be oneshot, longrun, or bundle`
This error means that you are attempting to combine files which are utilizing CRLF over LF; which is **CR** = Carriage Return and **LF** = Line Feed This error means that you are attempting to combine files which are utilizing CRLF over LF; which is **CR** = Carriage Return and **LF** = Line Feed
@@ -2029,7 +2108,7 @@ find ./ -type f | grep -Ev '.git|*.jpg|*.jpeg|*.png' | sudo xargs dos2unix --
<br /> <br />
<br /> <br />
#### Build Error: `unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied` ### Build Error: `unable to exec /etc/s6-overlay/s6-rc.d/init-envfile/run: Permission denied`
There are multiple errors you can receive when attempting to run your TVApp2 docker image. You may receive any of the following errors: There are multiple errors you can receive when attempting to run your TVApp2 docker image. You may receive any of the following errors:
@@ -2054,7 +2133,7 @@ After you have set these permissions, re-build your docker image using `docker b
<br /> <br />
<br /> <br />
#### Build Error: `[ERR] [27] Jellyfin.LiveTv.Guide.GuideManager: Error getting programs for channel XXXXXXXXXXXXXXX (Source 2) System.Xml.XmlException: '', hexadecimal value 0x1F, is an invalid character. Line 1, position 1.` ### Build Error: `[ERR] [27] Jellyfin.LiveTv.Guide.GuideManager: Error getting programs for channel XXXXXXXXXXXXXXX (Source 2) System.Xml.XmlException: '', hexadecimal value 0x1F, is an invalid character. Line 1, position 1.`
This error may be seen if you are attempting to import our EPG guide data directly into Jellyfin. The cause of this is due to you having **GZIP Compression** enabled in your header request and response. See the example below; which is in your TVApp2 `📄 docker-compose.yml` file: This error may be seen if you are attempting to import our EPG guide data directly into Jellyfin. The cause of this is due to you having **GZIP Compression** enabled in your header request and response. See the example below; which is in your TVApp2 `📄 docker-compose.yml` file:

View File

@@ -45,6 +45,6 @@ services:
soft: -1 soft: -1
hard: -1 hard: -1
healthcheck: healthcheck:
test: [ "CMD", "curl", "--fail", "http://127.0.0.1:4124" ] test: [ "CMD", "curl", "--fail", "http://127.0.0.1:4124/api/health?silent=true" ]
interval: 30s interval: 30s
retries: 5 retries: 5

View 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

View File

@@ -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
View 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
View 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;

View 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
View 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
View 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
View 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;

View File

@@ -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'],

View File

@@ -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,89 +219,6 @@ getos( ( e, json ) =>
return serverOs; return serverOs;
}); });
/*
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
*/ */
@@ -281,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 }` );
@@ -292,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 }` );
@@ -299,7 +249,7 @@ else
} }
/* /*
helper > sleep helper sleep
*/ */
function sleep( ms ) function sleep( ms )
@@ -311,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
*/ */
@@ -382,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
@@ -546,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
*/ */
@@ -608,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 )
{ {
@@ -1479,13 +1423,14 @@ async function serveHealthCheck( req, res )
try try
{ {
const paramUrl = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'api' ); const paramUrl = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'api' );
const paramQuery = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'query' ); const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' );
if ( !paramUrl ) if ( !paramUrl )
{ {
if ( paramQuery !== 'uptime' ) 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` ) );
} }
} }
@@ -1509,7 +1454,7 @@ async function serveHealthCheck( req, res )
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
if ( paramQuery !== 'uptime' ) 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` ),
@@ -1627,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',
@@ -2062,11 +2009,11 @@ const server = http.createServer( ( req, resp ) =>
loadFile channel?url=https%3A%2F%2Ftvpass.org%2Fchannel%2Fabc-wabc-new-york-ny%2F loadFile channel?url=https%3A%2F%2Ftvpass.org%2Fchannel%2Fabc-wabc-new-york-ny%2F
*/ */
const paramQuery = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'query' ); const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' );
if ( paramQuery !== 'uptime' ) 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 }` ),
@@ -2251,11 +2198,27 @@ const server = http.createServer( ( req, resp ) =>
return; return;
} }
/*
Endpoint > Health Check
paramQuery specifies what type of query is triggered
options:
uptime
healthcheck
sync
paramSilent specifies if logs should be silenced. useful for docker-compose.yml healthcheck so that console
is not spammed every 30 seconds.
*/
if ( subdomainHealth.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) if ( subdomainHealth.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
{ {
const paramQuery = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'query' ); const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' );
if ( paramQuery !== 'uptime' ) // 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( `` ), 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` ),
@@ -2384,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 =
@@ -2458,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
@@ -2486,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 } ` ) );
});
})(); })();
/* /*

1329
tvapp2/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "tvapp2", "name": "tvapp2",
"version": "1.5.4", "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",
@@ -75,8 +75,6 @@
"dependencies": { "dependencies": {
"cron": "^4.3.1", "cron": "^4.3.1",
"node-cron": "^4.1.0", "node-cron": "^4.1.0",
"playwright": "^1.52.0",
"user-agents": "^1.1.557",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"moment": "^2.30.1", "moment": "^2.30.1",
@@ -88,16 +86,16 @@
}, },
"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": "^11.0.0",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-plugin-chai-friendly": "^1.1.0", "eslint-plugin-chai-friendly": "^1.1.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^17.19.0", "eslint-plugin-n": "^17.19.0",
"eslint-plugin-promise": "^7.2.1", "eslint-plugin-promise": "^7.2.1",
"@stylistic/eslint-plugin": "^4.4.1" "@stylistic/eslint-plugin": "^5.0.0"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
@@ -113,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",

File diff suppressed because it is too large Load Diff

579
tvapp2/www/hdhomerun.html Normal file
View 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>

View File

@@ -16,11 +16,11 @@
<!-- Header --> <!-- Header -->
<div class="header"> <div class="header">
<nav class="navbar sticky-top container"> <nav class="navbar sticky-top container">
<div class="navbar-brand"> <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> <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> <a target="_blank" data-bs-toggle="tooltip" title="View Github Repository" class="header-name" href="<%= appUrlGithub %>">TVApp2 for Docker</a>
</div> </div>
<div class="navbar-social"> <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> <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 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="<%= 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>
@@ -32,10 +32,27 @@
<!-- Header Notification: description --> <!-- Header Notification: description -->
<div class="container"> <div class="container">
<div class="container header-container"> <div class="introduction">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="about">This page displays your most recent copies of the <code><%= fileM3U %></code> playlist and <code><%= fileXML %></code> EPG guide data. Right-click each file, select <span class="text-accent">Copy Link</span> and paste the URLs within an IPTV app such as Jellyfin. The <code><%= fileXML %></code> and <code><%= fileGZP %></code> have identical guide data, however the <code><%= fileGZP %></code> is compressed and will import into your IPTV application much faster.</div> <div class="introduction-body">
<div class="desc">
This page displays your most recent copies of the <code><%= fileM3U %></code> playlist and <code><%= fileXML %></code>
EPG guide data. Right-click each file, select <span class="accent">Copy Link</span> and paste the URLs within an IPTV
app such as Jellyfin. The <code><%= fileXML %></code> and <code><%= fileGZP %></code> have identical guide data,
however the <code><%= fileGZP %></code> is compressed and will import into your IPTV application much faster.
<br />
<div class="badges">
<img src="https://img.shields.io/github/v/tag/TheBinaryNinja/tvapp2?logo=GitHub&label=Version&color=ba5225">
<img src="https://img.shields.io/github/downloads/TheBinaryNinja/tvapp2/total?logo=github&logoColor=FFFFFF&label=Downloads&color=376892">
<img src="https://img.shields.io/github/repo-size/TheBinaryNinja/tvapp2?logo=github&label=Size&color=59702a">
<img src="https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fgithub.com%2Faetherinox%2Fbackage%2Fraw%2Findex%2FTheBinaryNinja%2Ftvapp2%2Ftvapp2.xml&query=%2Fxml%2Fdownloads&label=Pulls&logo=github&color=de1f5e">
<img src="https://img.shields.io/github/last-commit/TheBinaryNinja/tvapp2?logo=conventionalcommits&logoColor=FFFFFF&label=Last%20Commit&color=313131">
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -116,12 +133,13 @@
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container" style="padding-bottom:20px;"> <div class="container notifications" style="padding-bottom:20px;">
<div id="ntfy-restart" class="ntfy ntfy-success sticky-bottom"></div> <div id="ntfy-restart" class="ntfy-parent indicator-success sticky-bottom"></div>
<div id="ntfy-firewall" class="ntfy ntfy-warning sticky-bottom"></div> <div id="ntfy-firewall" class="ntfy-parent indicator-warning sticky-bottom"></div>
<div id="ntfy-localhost" class="ntfy ntfy-danger sticky-bottom"></div> <div id="ntfy-localhost" class="ntfy-parent indicator-danger sticky-bottom"></div>
</div> </div>
<div class="footer-inner">
<div class="sub">
<div class="container"> <div class="container">
<div class="col text-center text-muted text-small text-nowrap"> <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 /> <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 />
@@ -218,6 +236,53 @@
<script> <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 tooltipList = [].slice.call( document.querySelectorAll( '[data-bs-toggle="tooltip"]' ) )
var tooltipList = tooltipList.map( function ( el )
{
return new bootstrap.Tooltip(el,
{
placement: "bottom",
trigger: "hover",
html: true
});
})
/*
Helper > Get Multiple Elements by ID
*/
function getElementsById( ids )
{
const idList = ids.split(" ");
let results = [], item;
for ( let i = 0; i < idList.length; i++ )
{
item = document.getElementById( idList[ i ] );
if (item)
{
results.push( item );
}
}
return( results );
}
/* /*
Document Ready Document Ready
*/ */
@@ -244,21 +309,26 @@
Notify > Localhost Notify > Localhost
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener( 'DOMContentLoaded', function()
{
const host = window.location.hostname; const host = window.location.hostname;
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80'); const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
if (host === 'localhost' || host === '127.0.0.1') if (host === 'localhost' || host === '127.0.0.1')
{ {
const msg = "<p><span class='warning'>Warning</span> If you are accessing this page via 127.0.0.1 or localhost, proxying will not work on other devices.Please load \
this page using your computer's IP address (e.g., 192.168.x.x) and port in order to access the playlist from other devices on your network.</p> \
<br> \
<p> 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>.</p>";
document.getElementById('ntfy-localhost').innerHTML = msg; const msg = "<div class='ntfy-child'><span class='danger'>Danger</span> \
document.getElementById('ntfy-localhost').style.display = 'block'; <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 { } else {
document.getElementById('ntfy-localhost').style.display = 'none'; document.getElementById( 'ntfy-localhost' ).style.display = 'none';
} }
}); });
@@ -266,27 +336,38 @@
Notify > Firewall Notify > Firewall
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener( 'DOMContentLoaded', function()
{
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80'); const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
const msg = "<p><span class='notice'>Notice</span> 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> \ const msg = "<div class='ntfy-child'><span class='warning'>Warning</span> \
or <a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a> OS firewall settings \ <span class='msg'> \
This action enables devices such as Firestick or Android to connect to the server and request the playlist through the proxy.</p>"; 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' ).innerHTML = msg;
document.getElementById('ntfy-firewall').style.display = 'block'; document.getElementById( 'ntfy-firewall' ).style.display = 'block';
}); });
/* /*
Notify > Restart / Resync Notify > Restart / Resync
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener( 'DOMContentLoaded', function()
{
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80'); const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
const msg = "<p><span class='success'>Success</span> Your IPTV m3u channels and xml guide data has been successfully re-synced. \ const msg = "<div class='ntfy-child'><span class='success'>Success</span> \
Please refresh this window to see new data</p>"; <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' ).innerHTML = msg;
document.getElementById('ntfy-restart').style.display = 'none'; document.getElementById( 'ntfy-restart' ).style.display = 'none';
}); });
/* /*
@@ -318,11 +399,12 @@
url: 'api/health', url: 'api/health',
type: 'GET', type: 'GET',
data: { data: {
query: 'healthcheck' query: 'healthcheck',
silent: false
}, },
beforeSend: function( data ) beforeSend: function( data )
{ {
console.log('Sending health check ...') console.log( 'Sending health check ...' )
}, },
success: function( data ) success: function( data )
{ {
@@ -337,6 +419,16 @@
$('.toast #toast-title').html(`<%= appName %> is ${ status }`); $('.toast #toast-title').html(`<%= appName %> is ${ status }`);
$('.toast #toast-message').html(`Health check returned ${ status } (${ code })`); $('.toast #toast-message').html(`Health check returned ${ status } (${ code })`);
$('#tvapp2Toast').toast('show'); $('#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';
});
} }
}, },
@@ -376,7 +468,8 @@
url: 'api/health', url: 'api/health',
type: 'GET', type: 'GET',
data: { data: {
query: 'uptime' query: 'uptime',
silent: true
}, },
success: function( data ) success: function( data )
{ {
@@ -422,7 +515,8 @@
url: 'api/restart', url: 'api/restart',
type: 'GET', type: 'GET',
data: { data: {
query: 'sync' query: 'sync',
silent: false
}, },
beforeSend: function( data ) beforeSend: function( data )
{ {