cleanup prep

This commit is contained in:
iFlip721
2026-06-11 16:37:09 -04:00
parent 30aa901b4d
commit 986e83632b
93 changed files with 0 additions and 13097 deletions

View File

View File

@@ -1,171 +0,0 @@
# syntax=docker/dockerfile:1
# #
# @project TVApp2
# @usage docker image which allows you to download a m3u playlist and EPG guide data from
# multiple IPTV services.
# @file Dockerfile
# @repo https://github.com/TheBinaryNinja/tvapp2
# https://git.binaryninja.net/BinaryNinja/tvapp2
# https://github.com/aetherinox/docker-base-alpine
#
# build your own image by running
# amd64 docker build --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 -t tvapp2:latest -t tvapp2:1.5.0 -t tvapp2:1.5.0-amd64 -f Dockerfile .
# arm64 docker build --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 -t tvapp2:1.5.0-arm64 -f Dockerfile.aarch64 .
#
# OR; build using `docker buildx`
# create docker buildx create --driver docker-container --name container --bootstrap --use
# amd64 docker buildx build --build-arg ARCH=amd64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=stable --tag ghcr.io/thebinaryninja/tvapp2:1.5.0-amd64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/amd64 --output type=docker --allow network.host --network host --no-cache --pull --push .
# arm64 docker buildx build --build-arg ARCH=arm64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=stable --tag ghcr.io/thebinaryninja/tvapp2:1.5.0-arm64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/arm64 --output type=docker --allow network.host --network host --no-cache --pull --push .
#
# OR; build single amd64 image
# create docker buildx create --driver docker-container --name container --bootstrap --use
# amd64 docker buildx build --build-arg ARCH=amd64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=stable --tag ghcr.io/thebinaryninja/tvapp2:1.5.0 --tag ghcr.io/thebinaryninja/tvapp2:1.5 --tag ghcr.io/thebinaryninja/tvapp2:1 --tag ghcr.io/thebinaryninja/tvapp2:latest --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/amd64 --output type=docker --allow network.host --network host --no-cache --push .
#
# OR; build official image (publish)
# create docker buildx create --driver docker-container --name container --bootstrap --use
# amd64-stable docker buildx build --build-arg ARCH=amd64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=stable --tag ghcr.io/thebinaryninja/tvapp2:1.5.0-amd64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/amd64 --output type=docker --allow network.host --network host --no-cache --pull --push .
# arm64-stable docker buildx build --build-arg ARCH=arm64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=stable --tag ghcr.io/thebinaryninja/tvapp2:1.5.0-arm64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/arm64 --output type=docker --allow network.host --network host --no-cache --pull --push .
# amd64-dev docker buildx build --build-arg ARCH=amd64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=development --tag ghcr.io/thebinaryninja/tvapp2:development-amd64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/amd64 --output type=docker --allow network.host --network host --no-cache --pull --push .
# arm64-dev docker buildx build --build-arg ARCH=arm64 --build-arg VERSION=1.5.0 --build-arg BUILDDATE=20260812 --build-arg RELEASE=development --tag ghcr.io/thebinaryninja/tvapp2:development-arm64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/arm64 --output type=docker --allow network.host --network host --no-cache --pull --push .
# amd64-stable-hash docker buildx imagetools inspect ghcr.io/thebinaryninja/tvapp2:1.5.0-amd64
# arm64-stable-hash docker buildx imagetools inspect ghcr.io/thebinaryninja/tvapp2:1.5.0-arm64
# amd64-dev-hash docker buildx imagetools inspect ghcr.io/thebinaryninja/tvapp2:development-amd64
# arm64-dev-hash docker buildx imagetools inspect ghcr.io/thebinaryninja/tvapp2:development-arm64
# merge-stable docker buildx imagetools create --tag ghcr.io/thebinaryninja/tvapp2:1.5.0 --tag ghcr.io/thebinaryninja/tvapp2:1.5 --tag ghcr.io/thebinaryninja/tvapp2:1 --tag ghcr.io/thebinaryninja/tvapp2:latest sha256:0abe1b1c119959b3b1ccc23c56a7ee2c4c908c6aaef290d4ab2993859d807a3b sha256:e68b9de8669eac64d4e4d2a8343c56705e05e9a907cf0b542343f9b536d9c473
# merge-dev docker buildx imagetools create --tag ghcr.io/thebinaryninja/tvapp2:development sha256:8f36385a28c8f6eb7394d903c9a7a2765b06f94266b32628389ee9e3e3d7e69d sha256:c719ccb034946e3f0625003f25026d001768794e38a1ba8aafc9146291d548c5
# #
# #
# FROM
# any args defined before FROM cannot be called after FROM and the ARE is classified outside the build process.
# You will have to re-define the arg after FROM to utilize it anywhere else in the build process.
#
# @ref https://docs.docker.com/reference/dockerfile/#understand-how-arg-and-from-interact
# #
ARG ARCH=amd64
ARG ALPINE_VERSION=3.22
FROM --platform=linux/${ARCH} ghcr.io/aetherinox/alpine-base:${ALPINE_VERSION}
# #
# Set Args
# #
ARG ARCH=amd64
ARG ALPINE_VERSION=3.22
ARG BUILDDATE
ARG VERSION
ARG RELEASE
ARG GIT_SHA1=0000000000000000000000000000000000000000
ARG REGISTRY=local
# #
# Set Labels
# #
LABEL org.opencontainers.image.authors="Aetherinox, iFlip721, Optx"
LABEL org.opencontainers.image.vendor="BinaryNinja"
LABEL org.opencontainers.image.title="TVApp2"
LABEL org.opencontainers.image.description="Automatic m3u and xml guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client."
LABEL org.opencontainers.image.source="https://github.com/thebinaryninja/tvapp2"
LABEL org.opencontainers.image.repo.1="https://github.com/thebinaryninja/tvapp2"
LABEL org.opencontainers.image.repo.2="https://git.binaryninja.net/binaryninja/tvapp2"
LABEL org.opencontainers.image.repo.3="https://github.com/aetherinox/docker-base-alpine"
LABEL org.opencontainers.image.documentation="https://thebinaryninja.github.io/tvapp2"
LABEL org.opencontainers.image.url="https://github.com/thebinaryninja/tvapp2/pkgs/container/tvapp2"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.architecture="${ARCH:-amd64}"
LABEL org.opencontainers.image.ref.name="main"
LABEL org.opencontainers.image.registry="${REGISTRY:-local}"
LABEL org.opencontainers.image.release="${RELEASE:-stable}"
LABEL org.tvapp2.image.maintainers="Aetherinox, iFlip721, Optx"
LABEL org.tvapp2.image.build-version="Version:- ${VERSION} Date:- ${BUILDDATE:-3.21}"
LABEL org.tvapp2.image.build-version-alpine="${ALPINE_VERSION:-3.21}"
LABEL org.tvapp2.image.build-architecture="${ARCH:-amd64}"
LABEL org.tvapp2.image.build-release="${RELEASE:-stable}"
LABEL org.tvapp2.image.build-sha1="${GIT_SHA1:-0000000000000000000000000000000000000000}"
# #
# Set Env Var
# #
ENV NODE_VERSION=22.16.0
ENV YARN_VERSION=1.22.22
ENV NPM_VERSION=10.9.2
ENV RELEASE="${RELEASE:-stable}"
ENV DIR_BUILD=/usr/src/app
ENV DIR_RUN=/usr/bin/app
ENV URL_REPO="https://git.binaryninja.net/binaryninja/"
ENV WEB_IP="0.0.0.0"
ENV WEB_PORT=4124
ENV HDHR_PORT=6077
ENV WEB_ENCODING="deflate, br"
ENV WEB_PROXY_HEADER="x-forwarded-for"
ENV STREAM_QUALITY="hd"
ENV FILE_URL="urls.txt"
ENV FILE_M3U="playlist.m3u8"
ENV FILE_EPG="xmltv.xml"
ENV FILE_TAR="xmltv.xml.gz"
ENV HEALTH_TIMER=600000
ENV TASK_CRON_SYNC="0 0 */3 * *"
ENV LOG_LEVEL=4
ENV TZ="Etc/UTC"
ENV GIT_SHA1=${GIT_SHA1:-0000000000000000000000000000000000000000}
# #
# Install
# #
RUN \
apk add --no-cache \
wget \
curl \
bash \
nano \
git \
npm \
openssl
# #
# Copy docker-entrypoint
# #
COPY docker-entrypoint.sh /usr/local/bin/
# #
# copy s6-overlays root to image root
# #
COPY root/ /
# #
# set work directory
# #
WORKDIR ${DIR_BUILD}
# #
# copy tvapp2 project to workdir
# #
COPY tvapp2/ ./
# #
# set work dir to built app
# #
WORKDIR ${DIR_RUN}
# #
# Ports and volumes
# #
EXPOSE ${WEB_PORT}/tcp
# #
# In case user sets up the cron for a longer duration, do a first run
# and then keep the container running. Hacky, but whatever.
# #
ENTRYPOINT ["/init"]

View File

@@ -1,50 +0,0 @@
# #
# TVApp2 Docker-compose.yml
#
# Automatic M3U playlist and XML guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
#
# @url https://github.com/TheBinaryNinja/tvapp2
# https://git.binaryninja.net/BinaryNinja/tvapp2
#
# @image:github ghcr.io/thebinaryninja/tvapp2:latest
# ghcr.io/thebinaryninja/tvapp2:amd64
# ghcr.io/thebinaryninja/tvapp2:arm64
#
# @image:dockerhub thebinaryninja/tvapp2:latest
# thebinaryninja/tvapp2:1.0.0-amd64
# thebinaryninja/tvapp2:1.0.0-arm64
#
# @image:gitea git.binaryninja.net/binaryninja/tvapp2:latest
# git.binaryninja.net/binaryninja/tvapp2:1.0.0-amd64
# git.binaryninja.net/binaryninja/tvapp2:1.0.0-arm64
# #
services:
# #
# Service > TVApp2
# #
tvapp2:
container_name: tvapp2
image: ghcr.io/thebinaryninja/tvapp2:latest # Image: Github
# image: thebinaryninja/tvapp2:latest # Image: Dockerhub
# image: git.binaryninja.net/binaryninja/tvapp2:latest # Image: Gitea
# image: tvapp2:latest # Image: Locally built
hostname: tvapp2
environment:
TZ: "Etc/UTC"
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/config
- ./app:/usr/bin/app
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test: [ "CMD", "curl", "--fail", "http://127.0.0.1:4124/api/health?silent=true" ]
interval: 30s
retries: 5

View File

@@ -1,31 +0,0 @@
#!/bin/sh
# #
# @project TVApp2
# @usage docker image which allows you to download a m3u playlist and EPG guide data from
# multiple IPTV services.
# @file docker-entrypoint.sh
# @repo https://github.com/TheBinaryNinja/tvapp2
# https://git.binaryninja.net/BinaryNinja/tvapp2
# https://github.com/aetherinox/docker-base-alpine
#
# you can build your own image by running
# amd64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 -t tvapp2:1.0.0-amd64 -f Dockerfile .
# arm64 docker build --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:1.0.0-arm64 -f Dockerfile.aarch64 .
#
# if you prefer to use `docker buildx`
# create docker buildx create --driver docker-container --name container --bootstrap --use
# amd64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/amd64 --output type=docker --output type=docker .
# arm64 docker buildx build --no-cache --pull --build-arg VERSION=1.0.0 --build-arg BUILDDATE=20250218 -t tvapp2:latest -t tvapp2:1.0.0 --platform=linux/arm64 --output type=docker --output type=docker .
# #
set -e
# Run command with node if the first argument contains a "-" or is not a system command. The last
# part inside the "{}" is a workaround for the following bug in ash/dash:
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then
set -- node "$@"
fi
exec "$@"

View File

@@ -1,88 +0,0 @@
#!/bin/bash
# #
# @project TVApp2
# @usage Automatic m3u and xml guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
# @file plugins
# @repo.1 https://github.com/TheBinaryNinja/tvapp2
# @repo.2 https://git.binaryninja.net/BinaryNinja/tvapp2
# @repo.3 https://github.com/aetherinox/docker-base-alpine
# #
# #
# define > colors
#
# Use the color table at:
# - https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
# #
declare -A c=(
[end]=$'\e[0m'
[white]=$'\e[97m'
[bold]=$'\e[1m'
[dim]=$'\e[2m'
[underline]=$'\e[4m'
[strike]=$'\e[9m'
[blink]=$'\e[5m'
[inverted]=$'\e[7m'
[hidden]=$'\e[8m'
[black]=$'\e[0;30m'
[redl]=$'\e[0;91m'
[redd]=$'\e[0;31m'
[magental]=$'\e[0;95m'
[magentad]=$'\e[0;35mm'
[bluel]=$'\e[0;94m'
[blued]=$'\e[0;34m'
[cyanl]=$'\e[0;96m'
[cyand]=$'\e[0;36m'
[greenl]=$'\e[0;92m'
[greend]=$'\e[0;32m'
[yellowl]=$'\e[0;93m'
[yellowd]=$'\e[0;33m'
[greyl]=$'\e[0;37m'
[greyd]=$'\e[0;90m'
[navy]=$'\e[38;5;62m'
[olive]=$'\e[38;5;144m'
[peach]=$'\e[38;5;210m'
)
# #
# unicode for emojis
# https://apps.timwhitlock.info/emoji/tables/unicode
# #
declare -A icon=(
["symbolic link"]=$'\xF0\x9F\x94\x97' # 🔗
["regular file"]=$'\xF0\x9F\x93\x84' # 📄
["directory"]=$'\xF0\x9F\x93\x81' # 📁
["regular empty file"]=$'\xe2\xad\x95' # ⭕
["log"]=$'\xF0\x9F\x93\x9C' # 📜
["1"]=$'\xF0\x9F\x93\x9C' # 📜
["2"]=$'\xF0\x9F\x93\x9C' # 📜
["3"]=$'\xF0\x9F\x93\x9C' # 📜
["4"]=$'\xF0\x9F\x93\x9C' # 📜
["5"]=$'\xF0\x9F\x93\x9C' # 📜
["pem"]=$'\xF0\x9F\x94\x92' # 🔑
["pub"]=$'\xF0\x9F\x94\x91' # 🔒
["pfx"]=$'\xF0\x9F\x94\x92' # 🔑
["p12"]=$'\xF0\x9F\x94\x92' # 🔑
["key"]=$'\xF0\x9F\x94\x91' # 🔒
["crt"]=$'\xF0\x9F\xAA\xAA ' # 🪪
["gz"]=$'\xF0\x9F\x93\xA6' # 📦
["zip"]=$'\xF0\x9F\x93\xA6' # 📦
["gzip"]=$'\xF0\x9F\x93\xA6' # 📦
["deb"]=$'\xF0\x9F\x93\xA6' # 📦
["sh"]=$'\xF0\x9F\x97\x94' # 🗔
)
# #
# define > general
# #
PLUGINS_PATH="/config/www/plugins"
# #
# Plugins > Start
# #
printf '%-29s %-65s\n' " ${c[bluel]}Loader${c[end]}" "${c[end]}Checking tvapp2-plugins${c[end]}"

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
echo -e " Completed loading container"

View File

@@ -1,21 +0,0 @@
──────────────────────────────────────────────────────────────────────────────────────────
TVApp2 Docker Image
──────────────────────────────────────────────────────────────────────────────────────────
The TvApp2 image allows you to fetch M3U playlist and EPG data for numerous
IPTV services online.
Once the files are fetched by the image, you can visit the self-hosted webpage,
copy the links to the M3U and EPG files; and add them to your favorite IPTV app
such as Jellyfin, Plex, or Emby.
For more information about this project; visit the links below. This app is
served on multiple repositories as backup. Use any of the repo links below:
TVApp2 Repo 1 https://github.com/TheBinaryNinja/tvapp2
TVApp2 Repo 2 https://git.binaryninja.net/BinaryNinja/tvapp2
Base Alpine Image https://github.com/Aetherinox/docker-base-alpine
If you are making this container available on a public-facing domain,
please consider using Traefik and Authentik to protect this container from
outside access. Your M3U and EPG files will be available for the public to
download and use.

View File

@@ -1,176 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# #
# defaults
# #
PUID=${PUID:-911}
PGID=${PGID:-911}
DIR_BUILD=${DIR_BUILD:-/usr/src/app}
DIR_RUN=${DIR_RUN:-/usr/bin/app}
bHasError=false
# #
# define > colors
#
# Use the color table at:
# - https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
# #
declare -A c=(
[end]=$'\e[0m'
[white]=$'\e[97m'
[bold]=$'\e[1m'
[dim]=$'\e[2m'
[underline]=$'\e[4m'
[strike]=$'\e[9m'
[blink]=$'\e[5m'
[inverted]=$'\e[7m'
[hidden]=$'\e[8m'
[black]=$'\e[0;30m'
[redl]=$'\e[0;91m'
[redd]=$'\e[0;31m'
[magental]=$'\e[0;95m'
[magentad]=$'\e[0;35mm'
[bluel]=$'\e[0;94m'
[blued]=$'\e[0;34m'
[cyanl]=$'\e[0;96m'
[cyand]=$'\e[0;36m'
[greenl]=$'\e[0;92m'
[greend]=$'\e[0;32m'
[yellowl]=$'\e[0;93m'
[yellowd]=$'\e[0;33m'
[greyl]=$'\e[0;37m'
[greyd]=$'\e[0;90m'
[navy]=$'\e[38;5;62m'
[olive]=$'\e[38;5;144m'
[peach]=$'\e[38;5;210m'
)
# #
# unicode for emojis
# https://apps.timwhitlock.info/emoji/tables/unicode
# #
declare -A icon=(
["symbolic link"]=$'\xF0\x9F\x94\x97' # 🔗
["regular file"]=$'\xF0\x9F\x93\x84' # 📄
["directory"]=$'\xF0\x9F\x93\x81' # 📁
["regular empty file"]=$'\xe2\xad\x95' # ⭕
["log"]=$'\xF0\x9F\x93\x9C' # 📜
["1"]=$'\xF0\x9F\x93\x9C' # 📜
["2"]=$'\xF0\x9F\x93\x9C' # 📜
["3"]=$'\xF0\x9F\x93\x9C' # 📜
["4"]=$'\xF0\x9F\x93\x9C' # 📜
["5"]=$'\xF0\x9F\x93\x9C' # 📜
["pem"]=$'\xF0\x9F\x94\x92' # 🔑
["pub"]=$'\xF0\x9F\x94\x91' # 🔒
["pfx"]=$'\xF0\x9F\x94\x92' # 🔑
["p12"]=$'\xF0\x9F\x94\x92' # 🔑
["key"]=$'\xF0\x9F\x94\x91' # 🔒
["crt"]=$'\xF0\x9F\xAA\xAA ' # 🪪
["gz"]=$'\xF0\x9F\x93\xA6' # 📦
["zip"]=$'\xF0\x9F\x93\xA6' # 📦
["gzip"]=$'\xF0\x9F\x93\xA6' # 📦
["deb"]=$'\xF0\x9F\x93\xA6' # 📦
["sh"]=$'\xF0\x9F\x97\x94' # 🗔
)
# #
# distro info
# #
sys_os_name="Unknown"
sys_os_ver="1.0.0"
if [ -e /etc/alpine-release ]; then
sys_os_name="Alpine"
sys_os_ver="$(cat /etc/alpine-release)"
fi
# #
# get container ips
# #
IP_GATEWAY=$(/sbin/ip route|awk '/default/ { print $3 }')
IP_CONTAINER=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1')
# #
# usermod
# -o, --non-unique allow using duplicate (non-unique) UID
# -g, --gid GROUP force use GROUP as new primary group
# -G, --groups GROUPS new list of supplementary GROUPS
# -u, --uid UID new UID for the user account
# -U, --unlock unlock the user account
#
# groupmod
# -g, --gid GID change the group ID to GID
# -o, --non-unique allow to use a duplicate (non-unique) GID
# #
if [[ -z ${TVAPP_READ_ONLY_FS} ]] && [[ -z ${TVAPP_NON_ROOT_USER} ]]; then
groupmod -o -g "$PGID" dockerx
usermod -o -u "$PUID" dockerx
fi
# #
# s6 > branding
# #
printf '%-1s\n' " ${c[greyd]}──────────────────────────────────────────────────────────────────────────────────────────${c[end]}"
printf '%-1s\n' " ${c[greyd]} TVApp2 Docker Image${c[end]}"
printf '%-1s\n' " ${c[greyd]}──────────────────────────────────────────────────────────────────────────────────────────${c[end]}"
printf '%-2s\n' " ${c[greyd]}The TvApp2 image allows you to fetch M3U playlist and EPG data for numerous IPTV ${c[end]}"
printf '%-2s\n' " ${c[greyd]}services online. ${c[end]}"
echo -e
printf '%-2s\n' " ${c[greyd]}Once the files are fetched by the image, you can visit the self-hosted webpage, copy ${c[end]}"
printf '%-2s\n' " ${c[greyd]}the links to the M3U and EPG files; and add them to your favorite IPTV app such as ${c[end]}"
printf '%-2s\n' " ${c[greyd]}Jellyfin, Plex, or Emby. ${c[end]}"
echo -e
printf '%-2s\n' " ${c[greyd]}For more information about this project; visit the links below. This app is served on ${c[end]}"
printf '%-2s\n' " ${c[greyd]}multiple repositories as backup. Use any of the repo links below: ${c[end]}"
echo -e
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}TVApp2 Repo 1${c[end]}" "${c[end]}https://github.com/TheBinaryNinja/tvapp2 ${c[end]}"
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}TVApp2 Repo 2${c[end]}" "${c[end]}https://git.binaryninja.net/BinaryNinja/tvapp2 ${c[end]}"
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}Base Alpine Image${c[end]}" "${c[end]}https://github.com/Aetherinox/docker-base-alpine ${c[end]}"
echo -e
printf '%-2s\n' " ${c[greyd]}If you are making this container available on a public-facing domain, please consider ${c[end]}"
printf '%-2s\n' " ${c[greyd]}using Traefik and Authentik to protect this container from outside access. Your M3U ${c[end]}"
printf '%-2s\n' " ${c[greyd]}and EPG files will be available for the public to download and use. ${c[end]}"
# if { [[ -z ${TVAPP_READ_ONLY_FS} ]] && [[ -z ${TVAPP_NON_ROOT_USER} ]]; } || [[ ! ${TVAPP_FIRST_PARTY} = "true" ]]; then
# cat /etc/s6-overlay/s6-rc.d/init-adduser/branding
# else
# cat /run/branding
# fi
# #
# branding > non-root user
# #
if [[ -z ${TVAPP_NON_ROOT_USER} ]]; then
echo -e
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}Distro${c[end]}" "${c[end]}${sys_os_name} ${sys_os_ver}${c[end]}"
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}User:Group${c[end]}" "${c[end]}$(id -u dockerx):$(id -g dockerx)${c[end]}"
else
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}User:Group${c[end]}" "${c[end]}$(stat /run -c %u):$(stat /run -c %g)${c[end]}"
fi
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}Port(s)${c[end]}" "${c[end]}$(echo $WEB_PORT)${c[end]}"
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}Gateway${c[end]}" "${c[end]}$(echo $IP_GATEWAY)${c[end]}"
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}Web Server${c[end]}" "${c[end]}$(echo $IP_CONTAINER:$WEB_PORT)${c[end]}"
printf '%-6s %-35s %-65s\n' "" " ${c[greenl]}App Folder${c[end]}" "${c[end]}$(echo $DIR_RUN)${c[end]}"
echo -e
printf '%-1s\n' " ${c[greyd]}──────────────────────────────────────────────────────────────────────────────────────────${c[end]}"
# #
# set permissions
# #
if [[ -z ${TVAPP_READ_ONLY_FS} ]] && [[ -z ${TVAPP_NON_ROOT_USER} ]]; then
aetherxown dockerx:dockerx /app
aetherxown dockerx:dockerx /config
aetherxown dockerx:dockerx $(echo $DIR_BUILD)
fi

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-adduser/run

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
# This file doesn't do anything, it's just the end of the downstream image init process

View File

@@ -1,45 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
for cron_user in dockerx root; do
if [[ -z ${TVAPP_READ_ONLY_FS} ]] && [[ -z ${TVAPP_NON_ROOT_USER} ]]; then
if [[ -f "/etc/crontabs/${cron_user}" ]]; then
aetherxown "${cron_user}":"${cron_user}" "/etc/crontabs/${cron_user}"
crontab -u "${cron_user}" "/etc/crontabs/${cron_user}"
fi
fi
if [[ -f "/defaults/crontabs/${cron_user}" ]]; then
mkdir -p /config/crontabs
# #
# if crontabs do not exist in config
# #
if [[ ! -f "/config/crontabs/${cron_user}" ]]; then
# #
# copy crontab from system
# #
if crontab -l -u "${cron_user}" >/dev/null 2>&1; then
crontab -l -u "${cron_user}" >"/config/crontabs/${cron_user}"
fi
# #
# if crontabs still do not exist in config (were not copied from system)
# copy crontab from image defaults (using -n, do not overwrite an existing file)
# #
cp -n "/defaults/crontabs/${cron_user}" /config/crontabs/
fi
# #
# set perms and import user crontabs
# #
aetherxown "${cron_user}":"${cron_user}" "/config/crontabs/${cron_user}"
crontab -u "${cron_user}" "/config/crontabs/${cron_user}"
fi
done

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-crontab-config/run

View File

@@ -1,88 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# #
# define > colors
#
# Use the color table at:
# - https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
# #
declare -A c=(
[end]=$'\e[0m'
[white]=$'\e[97m'
[bold]=$'\e[1m'
[dim]=$'\e[2m'
[underline]=$'\e[4m'
[strike]=$'\e[9m'
[blink]=$'\e[5m'
[inverted]=$'\e[7m'
[hidden]=$'\e[8m'
[black]=$'\e[0;30m'
[redl]=$'\e[0;91m'
[redd]=$'\e[0;31m'
[magental]=$'\e[0;95m'
[magentad]=$'\e[0;35mm'
[bluel]=$'\e[0;94m'
[blued]=$'\e[0;34m'
[cyanl]=$'\e[0;96m'
[cyand]=$'\e[0;36m'
[greenl]=$'\e[0;92m'
[greend]=$'\e[0;32m'
[yellowl]=$'\e[0;93m'
[yellowd]=$'\e[0;33m'
[greyl]=$'\e[0;37m'
[greyd]=$'\e[0;90m'
[navy]=$'\e[38;5;62m'
[olive]=$'\e[38;5;144m'
[peach]=$'\e[38;5;210m'
)
# #
# unicode for emojis
# https://apps.timwhitlock.info/emoji/tables/unicode
# #
declare -A icon=(
["symbolic link"]=$'\xF0\x9F\x94\x97' # 🔗
["regular file"]=$'\xF0\x9F\x93\x84' # 📄
["directory"]=$'\xF0\x9F\x93\x81' # 📁
["regular empty file"]=$'\xe2\xad\x95' # ⭕
["log"]=$'\xF0\x9F\x93\x9C' # 📜
["1"]=$'\xF0\x9F\x93\x9C' # 📜
["2"]=$'\xF0\x9F\x93\x9C' # 📜
["3"]=$'\xF0\x9F\x93\x9C' # 📜
["4"]=$'\xF0\x9F\x93\x9C' # 📜
["5"]=$'\xF0\x9F\x93\x9C' # 📜
["pem"]=$'\xF0\x9F\x94\x92' # 🔑
["pub"]=$'\xF0\x9F\x94\x91' # 🔒
["pfx"]=$'\xF0\x9F\x94\x92' # 🔑
["p12"]=$'\xF0\x9F\x94\x92' # 🔑
["key"]=$'\xF0\x9F\x94\x91' # 🔒
["crt"]=$'\xF0\x9F\xAA\xAA ' # 🪪
["gz"]=$'\xF0\x9F\x93\xA6' # 📦
["zip"]=$'\xF0\x9F\x93\xA6' # 📦
["gzip"]=$'\xF0\x9F\x93\xA6' # 📦
["deb"]=$'\xF0\x9F\x93\xA6' # 📦
["sh"]=$'\xF0\x9F\x97\x94' # 🗔
)
# Directories
SCRIPTS_DIR="/custom-cont-init.d"
# Make sure custom init directory exists and has files in it
if [[ -e "${SCRIPTS_DIR}" ]] && [[ -n "$(/bin/ls -A ${SCRIPTS_DIR} 2>/dev/null)" ]]; then
printf '%-29s %-65s\n' " ${c[bluel]}Loader${c[end]}" "${c[end]}Loading any found plugins${c[end]}"
for SCRIPT in "${SCRIPTS_DIR}"/*; do
NAME="$(basename "${SCRIPT}")"
if [[ -f "${SCRIPT}" ]]; then
printf '%-29s %-65s\n' " ${c[bluel]}Loader${c[end]}" "${c[end]}Executing${c[end]}"
/bin/bash "${SCRIPT}"
printf '%-29s %-65s\n' " ${c[bluel]}Loader${c[end]}" "${c[end]}Successfully ran with code ${c[bluel]}[$?]${c[end]}"
elif [[ ! -f "${SCRIPT}" ]]; then
printf '%-29s %-65s\n' " ${c[bluel]}Loader${c[end]}" "${c[end]}${c[bluel]}${NAME}${c[end]} is not a valid file${c[end]}"
fi
done
else
printf '%-29s %-65s\n' " ${c[bluel]}Loader${c[end]}" "${c[end]}No plugins found; skipping${c[end]}"
fi

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-custom-files/run

View File

@@ -1,19 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
if find /run/s6/container_environment/FILE__* -maxdepth 1 > /dev/null 2>&1; then
for FILENAME in /run/s6/container_environment/FILE__*; do
SECRETFILE=$(cat "${FILENAME}")
if [[ -f ${SECRETFILE} ]]; then
FILESTRIP=${FILENAME//FILE__/}
if [[ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]]; then
echo "[env-init] Your secret: ${FILENAME##*/}"
echo " contains a trailing newline and may not work as expected"
fi
cat "${SECRETFILE}" >"${FILESTRIP}"
echo "[env-init] ${FILESTRIP##*/} set from ${FILENAME##*/}"
else
echo "[env-init] cannot find secret in ${FILENAME##*/}"
fi
done
fi

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-envfile/run

View File

@@ -1,7 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# make folders
mkdir -p \
/config/keys \
/run \

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-folders/run

View File

@@ -1,25 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# #
# @project TVApp2
# @usage Automatic m3u and xml guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
# @file run
# @repo.1 https://github.com/TheBinaryNinja/tvapp2
# @repo.2 https://git.binaryninja.net/BinaryNinja/tvapp2
# @repo.3 https://github.com/aetherinox/docker-base-alpine
# #
SUBJECT="/C=NA/ST=NA/L=NA/O=BinaryNinja/OU=TVApp2 Docker Image/CN=*"
if [[ -f /config/keys/cert.key && -f /config/keys/cert.crt ]]; then
echo -e " SSL : Using existing keys found in /config/keys"
else
echo -e " SSL : Generating self-signed keys in folder/config/keys. Replace if needed."
rm -f \
/config/keys/cert.key \
/config/keys/cert.crt || true
mkdir -p /config/keys
OUT=$(openssl req -new -x509 -days 3650 -nodes -out /config/keys/cert.crt -keyout /config/keys/cert.key -subj "$SUBJECT" 2>/dev/null)
fi

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-keygen/run

View File

@@ -1,39 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
MIGRATIONS_DIR="/migrations"
MIGRATIONS_HISTORY="/config/.migrations"
echo -e " Migrations : Started"
if [[ ! -d ${MIGRATIONS_DIR} ]]; then
echo -e " Migrations : No migrations found"
exit
fi
for MIGRATION in $(find ${MIGRATIONS_DIR}/* | sort -n); do
NAME="$(basename "${MIGRATION}")"
if [[ -f ${MIGRATIONS_HISTORY} ]] && grep -Fxq "${NAME}" ${MIGRATIONS_HISTORY}; then
echo -e " Migrations : ${NAME} Skipped"
continue
fi
echo -e " Migrations : ${NAME} Executing"
chmod +x "${MIGRATION}"
# #
# Execute migration script in a subshell to prevent it from modifying the current environment
# #
("${MIGRATION}")
EXIT_CODE=$?
if [[ ${EXIT_CODE} -ne 0 ]]; then
echo -e " Migrations : ${NAME} Failed with exit code ${EXIT_CODE}"
exit "${EXIT_CODE}"
fi
echo "${NAME}" >>${MIGRATIONS_HISTORY}
echo -e " Migrations : ${NAME} Success"
done
echo -e " Migrations : Complete"

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-migrations/run

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
# Empty placeholder for end of mod init process

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-mods-package-install/run

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
# This file doesn't do anything, it's just the end of the mod init process

View File

@@ -1,6 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# permissions
aetherxown -R dockerx:dockerx \
/config/keys

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-permissions/run

View File

@@ -1,11 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# #
# @project TVApp2
# @usage Automatic m3u and xml guide updater for TheTvApp, TVPass, and MoveOnJoy utilized within your IPTV client.
# @file run
# @repo.1 https://github.com/TheBinaryNinja/tvapp2
# @repo.2 https://git.binaryninja.net/BinaryNinja/tvapp2
# @repo.3 https://github.com/aetherinox/docker-base-alpine
# #

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-samples/run

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
# This file doesn't do anything, it just signals that services can start

View File

@@ -1,24 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# detect nginx configs with dates not matching the provided sample files
# active_confs=$(find /config/nginx/ -name "*.conf" -type f 2>/dev/null)
# for i in ${active_confs}; do
# if [ -f "${i}.sample" ]; then
# if [ "$(sed -nE 's|^## Version ([0-9]{4}\/[0-9]{2}\/[0-9]{2}).*|\1|p' "${i}")" != "$(sed -nE 's|^## Version ([0-9]{4}\/[0-9]{2}\/[0-9]{2}).*|\1|p' "${i}.sample")" ]; then
# active_confs_changed="│ $(printf '%10s' "$(sed -nE 's|^## Version ([0-9]{4}\/[0-9]{2}\/[0-9]{2}).*|\1|p' "${i}" | tr / -)") │ $(printf '%10s' "$(sed -nE 's|^## Version ([0-9]{4}\/[0-9]{2}\/[0-9]{2}).*|\1|p' "${i}.sample" | tr / -)") │ $(printf '%-70s' "${i}") │\n${active_confs_changed}"
# fi
# fi
# done
# detect site-confs with wrong extension
# site_confs_wrong_ext=$(find /config/nginx/site-confs/ -type f -not -name "*.conf" -not -name "*.conf.sample" 2>/dev/null)
# if [ -n "${site_confs_wrong_ext}" ]; then
# echo "**** The following site-confs have extensions other than .conf ****"
# echo "**** This may be due to user customization. ****"
# echo "**** You should review the files and rename them to use the .conf extension or remove them. ****"
# echo "**** nginx.conf will only include site-confs with the .conf extension. ****"
# echo -e "${site_confs_wrong_ext}"
# fi

View File

@@ -1 +0,0 @@
oneshot

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-version-checks/run

View File

@@ -1,15 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
if builtin command -v crontab >/dev/null 2>&1 && [[ -n "$(crontab -l -u dockerx 2>/dev/null || true)" || -n "$(crontab -l -u root 2>/dev/null || true)" ]]; then
if builtin command -v busybox >/dev/null 2>&1 && [[ $(busybox || true) =~ [[:space:]](crond)([,]|$) ]]; then
exec busybox crond -f -S -l 5
elif [[ -f /usr/bin/apt ]] && [[ -f /usr/sbin/cron ]]; then
exec /usr/sbin/cron -f -L 5
else
echo "**** cron not found ****"
sleep infinity
fi
else
sleep infinity
fi

View File

@@ -1 +0,0 @@
longrun

View File

@@ -1,195 +0,0 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# #
# defaults
# #
PUID=${PUID:-911}
PGID=${PGID:-911}
DIR_BUILD=${DIR_BUILD:-/usr/src/app}
DIR_RUN=${DIR_RUN:-/usr/bin/app}
bHasError=false
# #
# define > colors
#
# Use the color table at:
# - https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
# #
declare -A c=(
[end]=$'\e[0m'
[white]=$'\e[97m'
[bold]=$'\e[1m'
[dim]=$'\e[2m'
[underline]=$'\e[4m'
[strike]=$'\e[9m'
[blink]=$'\e[5m'
[inverted]=$'\e[7m'
[hidden]=$'\e[8m'
[black]=$'\e[0;30m'
[redl]=$'\e[0;91m'
[redd]=$'\e[0;31m'
[magental]=$'\e[0;95m'
[magentad]=$'\e[0;35mm'
[bluel]=$'\e[0;94m'
[blued]=$'\e[0;34m'
[cyanl]=$'\e[0;96m'
[cyand]=$'\e[0;36m'
[greenl]=$'\e[0;92m'
[greend]=$'\e[0;32m'
[yellowl]=$'\e[0;93m'
[yellowd]=$'\e[0;33m'
[greyl]=$'\e[0;37m'
[greyd]=$'\e[0;90m'
[navy]=$'\e[38;5;62m'
[olive]=$'\e[38;5;144m'
[peach]=$'\e[38;5;210m'
)
# #
# unicode for emojis
# https://apps.timwhitlock.info/emoji/tables/unicode
# #
declare -A icon=(
["symbolic link"]=$'\xF0\x9F\x94\x97' # 🔗
["regular file"]=$'\xF0\x9F\x93\x84' # 📄
["directory"]=$'\xF0\x9F\x93\x81' # 📁
["regular empty file"]=$'\xe2\xad\x95' # ⭕
["log"]=$'\xF0\x9F\x93\x9C' # 📜
["1"]=$'\xF0\x9F\x93\x9C' # 📜
["2"]=$'\xF0\x9F\x93\x9C' # 📜
["3"]=$'\xF0\x9F\x93\x9C' # 📜
["4"]=$'\xF0\x9F\x93\x9C' # 📜
["5"]=$'\xF0\x9F\x93\x9C' # 📜
["pem"]=$'\xF0\x9F\x94\x92' # 🔑
["pub"]=$'\xF0\x9F\x94\x91' # 🔒
["pfx"]=$'\xF0\x9F\x94\x92' # 🔑
["p12"]=$'\xF0\x9F\x94\x92' # 🔑
["key"]=$'\xF0\x9F\x94\x91' # 🔒
["crt"]=$'\xF0\x9F\xAA\xAA ' # 🪪
["gz"]=$'\xF0\x9F\x93\xA6' # 📦
["zip"]=$'\xF0\x9F\x93\xA6' # 📦
["gzip"]=$'\xF0\x9F\x93\xA6' # 📦
["deb"]=$'\xF0\x9F\x93\xA6' # 📦
["sh"]=$'\xF0\x9F\x97\x94' # 🗔
)
# #
# distro info
# #
sys_os_name="Unknown"
sys_os_ver="1.0.0"
if [ -e /etc/alpine-release ]; then
sys_os_name="Alpine"
sys_os_ver="$(cat /etc/alpine-release)"
fi
# #
# s6 > store env variables
# #
printf '%-29s %-65s\n' " ${c[bluel]}STATUS${c[end]}" "${c[end]}Fetching docker container and gateway addresses${c[end]}"
# #
# get container ips
# #
ip_gateway=$(/sbin/ip route|awk '/default/ { print $3 }')
ip_container=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1')
if [ -d "/var/run/s6/container_environment/" ]; then
printf "$ip_gateway" > /var/run/s6/container_environment/IP_GATEWAY
printf "$ip_container" > /var/run/s6/container_environment/IP_CONTAINER
else
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot generate s6-overlay env files; folder ${c[redl]}/var/run/s6/container_environment/${c[end]} does not exist${c[end]}"
bHasError=true
fi
# #
# s6 > export env vars
# #
export IP_GATEWAY=$ip_gateway
export IP_GATEWAY=$ip_container
# #
# install and startup for tvapp2
# #
printf '%-29s %-65s\n' " ${c[bluel]}STATUS${c[end]}" "${c[end]}Copying ${c[bluel]}${DIR_BUILD}${c[end]} to ${c[bluel]}${DIR_RUN}${c[end]}"
if [ -z "${DIR_BUILD}" ]; then
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot copy; env var ${c[redl]}\${DIR_BUILD}${c[end]} missing${c[end]}"
bHasError=true
else
if [ -d "${DIR_BUILD}/" ]; then
cp -r ${DIR_BUILD}/* ${DIR_RUN}
else
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot copy folder ${c[redl]}${DIR_BUILD}${c[end]} to ${c[redl]}${DIR_RUN}${c[end]}; build folder ${c[redl]}${DIR_BUILD}${c[end]} does not exist${c[end]}"
bHasError=true
fi
fi
# #
# remove build directory
# #
printf '%-29s %-65s\n' " ${c[bluel]}STATUS${c[end]}" "${c[end]}Remove ${c[bluel]}${DIR_BUILD}/${c[end]}"
if [ -z "${DIR_BUILD}" ]; then
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot remove; env var ${c[redl]}\${DIR_BUILD}${c[end]} missing${c[end]}"
else
if [ -d "${DIR_BUILD}" ]; then
rm -rf "${DIR_BUILD}/"
else
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot remove; build folder ${c[redl]}${DIR_BUILD}${c[end]} does not exist. Restart the container to re-initialize build folder.${c[end]}"
fi
fi
# #
# cd to BUILD_RUN directory
# #
printf '%-29s %-65s\n' " ${c[bluel]}STATUS${c[end]}" "${c[end]}Changing to run directory ${c[bluel]}${DIR_RUN}/${c[end]}"
if [ -z "${DIR_RUN}" ]; then
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot cd; env var ${c[redl]}\${DIR_RUN}${c[end]} missing${c[end]}"
bHasError=true
else
if [ -d "${DIR_RUN}" ]; then
cd ${DIR_RUN}
else
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot cd; run folder ${c[redl]}${DIR_RUN}${c[end]} does not exist${c[end]}"
bHasError=true
fi
fi
# #
# install tvapp2 via npm
# #
printf '%-29s %-65s\n' " ${c[bluel]}STATUS${c[end]}" "${c[end]}Running command ${c[bluel]}npm install --omit=dev${c[end]}"
if ! command -v npm; then
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Cannot install TVApp2 with npm because package ${c[redl]}npm${c[end]} not installed${c[end]}"
bHasError=true
else
npm install --omit=dev
printf '%-29s %-65s\n' " ${c[bluel]}STATUS${c[end]}" "${c[end]}Running command ${c[bluel]}npm start${c[end]}"
npm start
fi
# #
# finished run script
# #
printf '%-29s %-65s\n' " ${c[greenl]}OK${c[end]}" "${c[end]}Finished initializing script${c[end]}"
if [ "$bHasError" = true ] ; then
printf '%-29s %-65s\n' "" ""
printf '%-29s %-65s\n' " ${c[redl]}ERROR${c[end]}" "${c[end]}Fatal errors were detected${c[end]}"
printf '%-29s %-65s\n' " ${c[redl]}${c[end]}" "${c[end]}The run script detected that certain steps failed. This app may not${c[end]}"
printf '%-29s %-65s\n' " ${c[redl]}${c[end]}" "${c[end]}work properly. Try restarting the container.${c[end]}"
printf '%-29s %-65s\n' "" ""
fi

View File

@@ -1,169 +0,0 @@
/*
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;

View File

@@ -1,121 +0,0 @@
/*
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

@@ -1,47 +0,0 @@
/*
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;

View File

@@ -1,520 +0,0 @@
/*
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;

View File

@@ -1,455 +0,0 @@
/*
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;

View File

@@ -1,47 +0,0 @@
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

@@ -1,273 +0,0 @@
/*
Eslint 9 Flat Config
old eslint < 8 .rc files are no longer supported! do not place .eslintrc files in subfolders.
eslint developers are currently working on an experimental feature to allow for sub-folder
override rules
@ref https://github.com/eslint/eslint/discussions/18574#discussioncomment-9729092
https://eslint.org/docs/latest/use/configure/configuration-files#experimental-configuration-file-resolution
eslint config migration docs
@ref https://eslint.org/docs/latest/use/configure/migration-guide
*/
import path from 'path';
import globals from 'globals';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
/*
Plugins
*/
import pluginImport from 'eslint-plugin-import';
import pluginNode from 'eslint-plugin-n'
import pluginChaiFriendly from 'eslint-plugin-chai-friendly';
import pluginStylistic from '@stylistic/eslint-plugin'
/*
Globals
*/
const customGlobals =
{
guid: 'readable',
uuid: 'readable',
Buffer: "readonly",
BufferEncoding: "readonly"
};
/*
Compatibility
*/
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); // get resolved path to file
const __dirname = path.dirname(__filename); // get name of directory
const compat = new FlatCompat({
baseDirectory: __dirname, // optional; default: process.cwd()
resolvePluginsRelativeTo: __dirname, // optional
recommendedConfig: js.configs.recommended, // optional unless using 'eslint:recommended'
allConfig: js.configs.all, // optional unless using 'eslint:all'
});
/*
Eslint > Flat Config
*/
export default
[
{
ignores: [
'coverage/**',
'node_modules/**',
'**/dist/**/*',
'**/__tmp__/**/*',
'eslint.config.mjs',
'eslint.config.cjs',
"root.js",
"www/**/*"
]
},
...compat.extends('eslint:recommended'),
{
plugins: {
'n': pluginNode,
'import': pluginImport,
'@stylistic': pluginStylistic,
'chai-friendly': pluginChaiFriendly
},
linterOptions: {
reportUnusedDisableDirectives: false
},
languageOptions: {
globals: {
...customGlobals,
...globals.browser,
process: true, // Node.js global
_: true,
$: true
},
sourceType: 'module',
ecmaVersion: 'latest',
parserOptions: {
requireConfigFile: false
}
},
rules: {
// eslint/js rules
'one-var': 'off',
'no-throw-literal': 'off',
'camelcase': [
'error',
{
'properties': 'always'
}
],
'no-unused-vars': 'off',
'no-console': 'off',
'no-alert': 'error',
'no-debugger': 'error',
'prefer-arrow-callback': 'error',
'no-useless-escape': 'off',
'no-var': 'error',
'prefer-const': 'error',
'no-unused-expressions': 0,
'chai-friendly/no-unused-expressions': 'off',
'strict': ['error', 'never'],
'prefer-promise-reject-errors': 'off',
'no-object-constructor': 'error',
'object-shorthand': 'off',
'no-array-constructor': 'error',
'array-callback-return': 'error',
'no-eval': 'error',
'no-new-func': 'error',
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'no-useless-constructor': 'error',
'no-dupe-class-members': 'error',
'no-duplicate-imports': 'error',
'eqeqeq': 'error',
'no-unneeded-ternary': 'error',
'curly': 'off',
'no-empty': 'off',
'no-restricted-syntax': [
'error',
{
'selector': 'ExportDefaultDeclaration',
'message': 'Prefer named exports'
}
],
'import/no-webpack-loader-syntax': 'off',
'import/no-relative-parent-imports': 'error',
'import/first': 'error',
'import/no-default-export': 'off',
'node/no-callback-literal': 0,
/*
@plugin eslint-plugin-n
*/
'n/no-callback-literal': 0,
'n/no-deprecated-api': 'error',
'n/no-exports-assign': 'error',
'n/no-extraneous-import': 'error',
'n/no-extraneous-require': [
'error',
{
'allowModules': ['electron', 'electron-notarize'],
'resolvePaths': [],
'tryExtensions': []
}
],
'n/no-missing-import': 'off',
'n/no-missing-require': 'off',
'n/no-mixed-requires': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/no-process-env': 'off',
'n/no-process-exit': 'off',
'n/no-restricted-import': 'error',
'n/no-restricted-require': 'error',
'n/no-sync': 'off',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'error',
'n/no-unpublished-require': 'error',
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'off',
'n/prefer-global/buffer': 'error',
'n/prefer-global/console': 'error',
'n/prefer-global/process': 'error',
'n/prefer-global/text-decoder': 'error',
'n/prefer-global/text-encoder': 'error',
'n/prefer-global/url': 'error',
'n/prefer-global/url-search-params': 'error',
'n/prefer-node-protocol': 'off',
'n/prefer-promises/dns': 'off',
'n/prefer-promises/fs': 'off',
'n/process-exit-as-throw': 'error',
'@stylistic/object-property-newline': 'off',
'@stylistic/no-multi-spaces': [ 0, { ignoreEOLComments: true } ],
'@stylistic/arrow-spacing': [ 'error', { before: true, after: true } ],
'@stylistic/semi-spacing': ['error', {
before: false,
after: false,
}],
"@stylistic/space-before-function-paren": ["error", {
anonymous: "always",
asyncArrow: "never",
named: "never"
}],
'@stylistic/padded-blocks': ['error', {
blocks: 'never',
switches: 'never',
classes: 'never',
}],
'@stylistic/arrow-parens': [ 'error', 'always' ],
'@stylistic/block-spacing': [ 'error', 'always' ],
'@stylistic/comma-dangle': [ 'error', 'never' ],
'@stylistic/comma-spacing': [ 'error', { before: false, after: true }],
'@stylistic/computed-property-spacing': ['error', 'never'],
'@stylistic/no-mixed-operators': ['off'],
'@stylistic/eol-last': ['error', 'always'],
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
'@stylistic/linebreak-style': ['error', 'unix'],
'@stylistic/no-mixed-spaces-and-tabs': ['error'],
'@stylistic/no-tabs': ['error'],
'@stylistic/no-trailing-spaces': ['error', { skipBlankLines: true, ignoreComments: true }],
'@stylistic/no-whitespace-before-property': ['error'],
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/quote-props': ['error', 'as-needed'],
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/semi': ['error', 'always'],
'@stylistic/space-infix-ops': ['error'],
'@stylistic/template-curly-spacing': ['error', 'always'],
'@stylistic/template-tag-spacing': ['error', 'always'],
'@stylistic/space-in-parens': [ 'error', 'always',
{
exceptions: ["{}", "[]"]
}],
'@stylistic/spaced-comment': [ 'error', 'always',
{
markers: ['/']
}],
'@stylistic/array-bracket-newline': [ 'warn',
{
multiline: true,
minItems: 5,
}],
'@stylistic/brace-style': [ 'error', 'allman',
{
allowSingleLine: true,
}],
'@stylistic/array-bracket-spacing': [ 'error', 'always',
{
arraysInArrays: false,
objectsInArrays: false,
singleValue: false,
}],
'@stylistic/wrap-iife': [2, 'inside', { functionPrototypeMethods: true }],
'@stylistic/keyword-spacing': [ 'error',
{
before: true,
after: true,
overrides:
{
return: { before: true, after: true },
throw: { before: true, after: true },
case: { before: true, after: true },
as: { before: true, after: true },
if: { before: true, after: true },
for: { before: true, after: true },
while: { before: true, after: true },
static: { before: true, after: true }
}
}],
},
}
];

File diff suppressed because it is too large Load Diff

5088
tvapp2/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,121 +0,0 @@
{
"name": "tvapp2",
"version": "1.5.9",
"description": "This package allows you to generate M3U playlists and EPG guides from various online IPTV services.",
"author": "BinaryNinja",
"license": "MIT",
"main": "index.js",
"type": "module",
"build": {
"appId": "com.tvapp2.id"
},
"bugs": {
"url": "https://github.com/TheBinaryNinja/tvapp2/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/TheBinaryNinja/tvapp2.git"
},
"discord": {
"type": "invite",
"code": "HGv4eGr2kg",
"url": "https://discord.gg/HGv4eGr2kg"
},
"docs": {
"url": "https://thebinaryninja.github.io/tvapp2/"
},
"contributors": [
{
"name": "Aetherinox",
"email": "118329232+Aetherinox@users.noreply.github.com",
"url": "https://github.com/Aetherinox"
},
{
"name": "iFlip721",
"email": "161414668+iFlip721@users.noreply.github.com",
"url": "https://github.com/iFlip721"
},
{
"name": "Optx",
"email": "161414668+Optx@users.noreply.github.com",
"url": "https://github.com/Nvmdfth"
},
{
"name": "EuropaServ",
"email": "161414668+EuropaServ@users.noreply.github.com",
"url": "https://github.com/EuropaServ"
}
],
"scripts": {
"start": "node index.js",
"start:debug": "noxenv LOG_LEVEL=6 node index.js",
"docker:buildx:amd64": "docker buildx create --driver docker-container --name container --bootstrap --use && docker buildx build --build-arg ARCH=amd64 --build-arg VERSION=$npm_config_version --build-arg BUILDDATE=$npm_config_builddate --tag tvapp2:$npm_config_version-amd64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/amd64 --output type=docker --allow network.host --network host --no-cache --pull .",
"docker:buildx:arm64": "docker buildx create --driver docker-container --name container --bootstrap --use && docker buildx build --build-arg ARCH=arm64 --build-arg VERSION=$npm_config_version --build-arg BUILDDATE=$npm_config_builddate --tag tvapp2:$npm_config_version-arm64 --attest type=provenance,disabled=true --attest type=sbom,disabled=true --file Dockerfile --platform linux/arm64 --output type=docker --allow network.host --network host --no-cache --pull .",
"contrib:add": "all-contributors add",
"contrib:generate": "all-contributors generate",
"root": "node root.js",
"root:generate": "node root.js generate",
"env:root": "npx --quiet env-cmd --no-override node root.js",
"env:uuid": "npx --quiet env-cmd --no-override node root.js uuid",
"env:guid": "npx --quiet env-cmd --no-override node root.js guid",
"env:version": "node -p require('./package.json').version;",
"lint": "npx eslint"
},
"keywords": [
"nodejs",
"playlist",
"m3u",
"epg",
"thetvapp",
"TVPass",
"MoveOnJoy",
"token",
"iptv"
],
"dependencies": {
"cron": "^4.3.1",
"node-cron": "^4.1.0",
"chalk": "^5.4.1",
"ejs": "^3.1.10",
"moment": "^2.30.1",
"express": "^5.1.0",
"nconf": "^1.0.0-beta.2",
"javascript-time-ago": "^2.5.11",
"os-name": "^6.1.0",
"getos": "^3.2.1"
},
"devDependencies": {
"@aetherinox/noxenv": "^1.1.1",
"@types/uuid": "^11.0.0",
"all-contributors-cli": "^6.26.1",
"uuid": "^13.0.0",
"env-cmd": "^10.1.0",
"eslint": "^9.28.0",
"eslint-plugin-chai-friendly": "^1.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^17.19.0",
"eslint-plugin-promise": "^7.2.1",
"@stylistic/eslint-plugin": "^5.0.0"
},
"engines": {
"node": ">=20"
},
"files": [
"LICENSE",
"README.md",
"root.js",
"index.js",
"manifest.json",
"classes/CLib.js",
"classes/Log.js",
"classes/Semaphore.js",
"classes/Storage.js",
"classes/Tuner.js",
"classes/Utils.js",
"www/index.html",
"www/hdhomerun.html",
"www/favicon.ico",
"www/css/tvapp2.min.css",
"www/js/tvapp2.min.js"
]
}

View File

@@ -1,84 +0,0 @@
#!/usr/bin/env node
/*
build by running
npm run build
guid and uuid will be automatically generated and placed
inside .env file which will then be read by the github workflow
build script.
*/
/*
This script handles the following:
- read package.json
- create .env file
- return uuid, guid, version
can be called with the following external commands:
- node root.js returns version of root
- node root.js generate generates uuid / guid and shows all env vars in console
- node root.js uuid returns root uuid
- node root.js guid returns root guid
- node root.js versiom returns version of root
can be called with the following root commands:
- npm run root
- npm run root:generate
- npm run env-root
- npm run env-uuid
- npm run env-guid
- npm run env-version
*/
import fs from 'fs';
import { v5 as uuidv5 } from 'uuid';
/*
* declarations package.json
*/
const { version, repository } = JSON.parse(fs.readFileSync('package.json'))
const args = process.argv.slice(2, process.argv.length)
const action = args[0]
// const a = args[ 1 ];
// const b = args[ 2 ];
if (action === 'guid') {
console.log(`${process.env.GUID}`)
} else if (action === 'setup') {
fs.writeFileSync('.env', '', (err) => {
if (err) {
console.error(err)
} else {
console.log(`Wrote to .env successfully`)
}
})
} else if (action === 'generate') {
const buildGuid = uuidv5(`${repository.url}`, uuidv5.URL)
const buildUuid = uuidv5(version, buildGuid)
const ids = `
VERSION=${version}
GUID=${buildGuid}
UUID=${buildUuid}
`
console.log(version)
console.log(buildGuid)
console.log(buildUuid)
fs.writeFileSync('.env', ids, (err) => {
if (err) {
console.error(`Could not write env vars: ${err}`)
} else {
console.log(`Wrote env vars to .env`)
}
})
} else if (action === 'uuid') {
console.log(`${process.env.UUID}`)
} else {
console.log(version)
}
process.exit(0)

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,579 +0,0 @@
<!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

@@ -1,648 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<title><%= appName %> - 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">
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>
<!-- Header Fontawesome Icons -->
<div class="container main-container">
<table id="list" class="table table-dark table-striped">
<thead>
<tr class="d-none d-md-table-row">
<td class="icon cell-icon"></td>
<th class="file cell-file">
File Name
</th>
<th class="link cell-link">
Download
</th>
<th class="size cell-size">
Size
</th>
<th class="date cell-size">
Date
</th>
<th class="desc cell-desc">
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="icon cell-icon">
<svg class="svg-inline--fa fa-file-lines fa-fw" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-lines" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-fa-i2svg="">
<path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM112 256H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16z"></path>
</svg>
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td>
<td class="file cell-file">
<a id="m3u-name" target="_blank" data-bs-toggle="tooltip" title="IPTV channel list"></a>
</td>
<td class="link cell-link"><a id="m3u-link" target="_blank"></a></td>
<td class="size cell-size"><span id="m3u-size"></span></td>
<td class="date cell-date"><span id="m3u-date"></span></td>
<td class="desc cell-desc">M3U playlist which lists all channels, their associated group, and logo.</td>
</tr>
<tr>
<td class="icon cell-icon">
<svg class="svg-inline--fa fa-file-xml fa-fw" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-xml" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg="">
<path fill="currentColor" d="M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V304H176c-35.3 0-64 28.7-64 64V512H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128zM192 368c0 7.3 2.2 14.4 6.2 20.4l9.8 14.7 9.8-14.7c4-6.1 6.2-13.2 6.2-20.4c0-8.8 7.2-16 16-16s16 7.2 16 16c0 13.6-4 26.9-11.6 38.2L227.2 432l17.2 25.8C252 469.1 256 482.4 256 496c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-7.3-2.2-14.4-6.2-20.4L208 460.8l-9.8 14.7c-4 6.1-6.2 13.2-6.2 20.4c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-13.6 4-26.9 11.6-38.2L188.8 432l-17.2-25.8C164 394.9 160 381.6 160 368c0-8.8 7.2-16 16-16s16 7.2 16 16zM448 496V368c0-8.8 7.2-16 16-16s16 7.2 16 16V480h16c8.8 0 16 7.2 16 16s-7.2 16-16 16H464c-8.8 0-16-7.2-16-16zM299.7 352.6c6.9-1.9 14.3 1 18 7.2L352 416.9l34.3-57.1c3.7-6.2 11.1-9.1 18-7.2s11.7 8.2 11.7 15.4V496c0 8.8-7.2 16-16 16s-16-7.2-16-16V425.8l-18.3 30.5c-2.9 4.8-8.1 7.8-13.7 7.8s-10.8-2.9-13.7-7.8L320 425.8V496c0 8.8-7.2 16-16 16s-16-7.2-16-16V368c0-7.2 4.8-13.5 11.7-15.4z"></path>
</svg>
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td>
<td class="file cell-file">
<a id="xml-name" target="_blank" data-bs-toggle="tooltip" title="Uncompressed TV guide data"></a>
</td>
<td class="link cell-link"><a id="xml-link" target="_blank"></a></td>
<td class="size cell-size"><span id="xml-size"></span></td>
<td class="date cell-date"><span id="xml-date"></span></td>
<td class="desc cell-desc">XML / EPG guide data <code>(uncompressed)</code></td>
</tr>
<tr>
<td class="icon cell-icon">
<svg class="svg-inline--fa fa-file-zipper fa-fw" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-zipper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-fa-i2svg="">
<path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM96 48c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16zm-6.3 71.8c3.7-14 16.4-23.8 30.9-23.8h14.8c14.5 0 27.2 9.7 30.9 23.8l23.5 88.2c1.4 5.4 2.1 10.9 2.1 16.4c0 35.2-28.8 63.7-64 63.7s-64-28.5-64-63.7c0-5.5 .7-11.1 2.1-16.4l23.5-88.2zM112 336c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H112z"></path>
</svg>
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td>
<td class="file cell-gzp">
<a id="gzp-name" target="_blank" data-bs-toggle="tooltip" title="Compressed TV guide data"></a>
</td>
<td class="link cell-link"><a id="gzp-link" target="_blank"></a></td>
<td class="size cell-size"><span id="gzp-size"></span></td>
<td class="date cell-date"><span id="gzp-date"></span></td>
<td class="desc cell-desc">XML / EPG guide data <code>(compressed)</code></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';
document.getElementById('m3u-name').textContent = '<%= fileM3U %>';
document.getElementById('m3u-name').href = urlM3U;
document.getElementById('m3u-link').textContent = urlM3U;
document.getElementById('m3u-link').href = urlM3U;
document.getElementById('m3u-size').textContent = '<%= sizeM3U %>';
document.getElementById('m3u-date').textContent = '<%= dateM3U %>';
document.getElementById('xml-name').textContent = '<%= fileXML %>';
document.getElementById('xml-name').href = urlXML;
document.getElementById('xml-link').textContent = urlXML;
document.getElementById('xml-link').href = urlXML;
document.getElementById('xml-size').textContent = '<%= sizeXML %>';
document.getElementById('xml-date').textContent = '<%= dateXML %>';
document.getElementById('gzp-name').textContent = '<%= fileGZP %>';
document.getElementById('gzp-name').href = urlGZP;
document.getElementById('gzp-link').textContent = urlGZP;
document.getElementById('gzp-link').href = urlGZP;
document.getElementById('gzp-size').textContent = '<%= sizeGZP %>';
document.getElementById('gzp-date').textContent = '<%= dateGZP %>';
</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
*/
$(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': `App 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>

File diff suppressed because one or more lines are too long