mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-11 19:05:41 -04:00
cleanup prep
This commit is contained in:
171
Dockerfile
171
Dockerfile
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
@@ -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]}"
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
echo -e " Completed loading container"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-adduser/run
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
# This file doesn't do anything, it's just the end of the downstream image init process
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-crontab-config/run
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-custom-files/run
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-envfile/run
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
# make folders
|
||||
mkdir -p \
|
||||
/config/keys \
|
||||
/run \
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-folders/run
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-keygen/run
|
||||
@@ -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"
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-migrations/run
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
# Empty placeholder for end of mod init process
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mods-package-install/run
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
# This file doesn't do anything, it's just the end of the mod init process
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
# permissions
|
||||
aetherxown -R dockerx:dockerx \
|
||||
/config/keys
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-permissions/run
|
||||
@@ -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
|
||||
# #
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-samples/run
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
# This file doesn't do anything, it just signals that services can start
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-version-checks/run
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 }
|
||||
}
|
||||
}],
|
||||
},
|
||||
}
|
||||
];
|
||||
2842
tvapp2/index.js
2842
tvapp2/index.js
File diff suppressed because it is too large
Load Diff
5088
tvapp2/package-lock.json
generated
5088
tvapp2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
1058
tvapp2/www/css/tvapp2.min.css
vendored
1058
tvapp2/www/css/tvapp2.min.css
vendored
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
1
tvapp2/www/js/tvapp2.min.js
vendored
1
tvapp2/www/js/tvapp2.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user