diff --git a/.dockerignore b/.dockerignore index e0187c7..247b853 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,3 +16,10 @@ agent.md skill.md *.md !changelog-beta.md + +# Web UI toolchain. The compiled bundle is committed under html/js and is +# embedded by the Go build, so the TypeScript sources and npm tree are not +# needed in the build context. html/ itself must stay in the context. +node_modules/ +package.json +package-lock.json diff --git a/.drone.yml b/.drone.yml index 0b43d02..5afeab0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,6 +8,8 @@ steps: commands: - go mod download - go vet ./... + - test -z "$(gofmt -l .)" || (echo "gofmt needed:" && gofmt -l . && exit 1) + - go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./... - go test ./... - name: webui-check diff --git a/Dockerfile b/Dockerfile index dbd3b26..7d28b2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,14 +17,18 @@ RUN XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed ' && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ go build -trimpath -ldflags="-s -w -X main.Version=$XTEVE_VERSION" -o /out/xteve ./xteve.go -FROM mwader/static-ffmpeg:latest AS ffmpeg +# Static ffmpeg/ffprobe binaries. To bump, pick a tag from +# https://hub.docker.com/r/mwader/static-ffmpeg/tags and change it here. +FROM mwader/static-ffmpeg:7.1.1 AS ffmpeg FROM alpine:3.23 +# Build-time defaults for the xteve user. The entrypoint can change them at +# runtime with PUID/PGID, so these only matter when neither is set. ARG XTEVE_UID=1000 ARG XTEVE_GID=1000 -RUN apk add --no-cache ca-certificates tzdata \ +RUN apk add --no-cache ca-certificates tzdata su-exec \ && addgroup -S -g "${XTEVE_GID}" xteve \ && adduser -S -D -H -u "${XTEVE_UID}" -G xteve xteve \ && mkdir -p /xteve/config \ @@ -37,13 +41,16 @@ COPY --from=ffmpeg /ffmpeg /usr/local/bin/ffmpeg COPY --from=ffmpeg /ffprobe /usr/local/bin/ffprobe COPY docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh -USER xteve +# The entrypoint starts as root so it can apply PUID/PGID and fix ownership of +# the config directory, then drops to the xteve user with su-exec. EXPOSE 34400/tcp ENV XTEVE_CONFIG=/xteve/config ENV XTEVE_PORT=34400 +VOLUME ["/xteve/config"] + HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD wget -qO- "http://127.0.0.1:${XTEVE_PORT}/healthz" > /dev/null || exit 1 diff --git a/README-DEV.md b/README-DEV.md index fb3893e..bc9bb49 100644 --- a/README-DEV.md +++ b/README-DEV.md @@ -55,3 +55,7 @@ Drone (`.drone.yml`) runs vet, tests, the bundle check, hadolint and compose val ## Endpoints useful for operations * `GET /healthz` returns `{"status":"ok","version":...,"scanInProgress":...}` with no authentication. The Docker healthcheck uses it. + +## Container image + +`Dockerfile` builds the binary in a `golang:1.27.1-alpine` stage, copies static `ffmpeg`/`ffprobe` from a pinned `mwader/static-ffmpeg` tag (bump the tag in the Dockerfile; there is no other place to change) and runs on `alpine`. The runtime stage installs `su-exec` and does not set `USER`: `docker/entrypoint.sh` starts as root, applies `PUID`/`PGID` to the `xteve` user by editing `/etc/passwd` and `/etc/group`, fixes ownership of the config directory, and execs `su-exec xteve:xteve xteve`. With `--user` it skips all of that. `ARG XTEVE_UID`/`XTEVE_GID` remain the build-time defaults so Drone's build args still work. Check the script with `sh -n` and `shellcheck -s sh` after editing. diff --git a/README.md b/README.md index 44e9de8..9810401 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,27 @@
# xTeVe -## M3U Proxy for Plex DVR and Emby Live TV. +## M3U Proxy for Plex DVR and Emby Live TV. Documentation for setup and configuration is [here](https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md). -#### Donation -* **Bitcoin:** 1c1iCe4CJPfNUXtqxKBbW2Qd2EtqRPWme -![Bitcoin](html/img/BC-QR.jpg "Bitcoin - xTeVe") +## About this fork + +The upstream xTeVe project has been inactive since 2021. This fork is maintained privately for a single trusted LAN. It is Docker-only and built for amd64 only. There are no release archives and no self-update. + +What it adds over upstream: + +* Web UI redesign with a mobile and accessibility pass (responsive navigation, keyboard flow, focus visibility, ARIA announcements, contrast). +* Plex API guide refresh: after a lineup or XEPG update xTeVe can ask Plex to reload the DVR guide. New settings: `use_plexAPI`, `plex.url`, `plex.token`. +* Strict or relaxed handling of channels whose EPG source went missing in XEPG, with automatic re-mapping. +* A wizard-completed flag so the setup wizard is not shown again on restart. +* First-party Docker image with a static ffmpeg, runtime `PUID`/`PGID`, and a healthcheck. +* Drone CI: vet, tests, web bundle check, Dockerfile lint, compose validation, image publishing. +* Web UI embedded in the binary with `go:embed`. +* Unauthenticated `GET /healthz` endpoint for container health checks. +* The self-updater and the GitHub branch switching are removed. + +The plan for further work is in `tasks/improvement-plan.md`. ## Requirements ### Plex @@ -23,7 +37,7 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro * Emby Client with Live-TV support * Emby Premiere ---- +--- ## Features @@ -48,164 +62,82 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro --- -## Project Analysis (UI + Operations) +## Running the container -The core architecture is strong: a Go backend with websocket-driven UI updates, filesystem-based state, and very low runtime overhead. -The weakest points are mostly operational and UX-focused: +### Image -* UI was historically utility-first and desktop-biased, with limited responsive behavior and visual hierarchy. -* Container usage was documented externally but there was no first-party Dockerfile/compose setup in this repository. -* Static web assets are generated into `src/webUI.go`, which works, but creates large diffs and a heavier edit/build cycle. +The image is built by Drone and pushed to a private registry: -### Recommended next technical improvements - -1. Replace generated `src/webUI.go` with Go `embed` for simpler static asset management and cleaner PR diffs. -2. Add CI checks (`go test ./...`, build on Linux/arm64/amd64, docker build smoke test). -3. Add a dedicated health endpoint (for example `/healthz`) to decouple health checks from HDHomeRun endpoints. -4. Add integration tests around websocket commands that mutate settings/files to reduce regression risk. - ---- - -## Container-First Run (Included In This Repo) - -### Build image -```bash -docker build -t xteve:local . +``` +registry.coadcorp.com/nathan/xteve ``` -### Run with Docker Compose (bridge mode) +Tags: + +* `latest` and `` from the `master` branch. +* `` and `` from every other branch. + +```bash +docker pull registry.coadcorp.com/nathan/xteve:latest +``` + +The image is linux/amd64 only. + +### Start with Docker Compose + +Bridge mode (default). The web UI is on port 34400. + ```bash docker compose up -d ``` -Compose file: `docker-compose.yml` -Persistent config volume: `./docker-data/config:/xteve/config` +Host networking. Use this if you want Plex or Emby to discover the tuner by SSDP/DLNA. Multicast does not cross the Docker bridge, so publishing UDP 1900 in bridge mode is not enough on its own. Linux only. -### Run with Docker Compose (host networking, Linux recommended for discovery) ```bash docker compose -f docker-compose.host.yml up -d ``` -Host networking improves LAN discovery behavior (SSDP/DLNA) for Plex/Emby in many setups. +In bridge mode you can still add the tuner manually in Plex or Emby with `http://:34400`. -### Container environment variables +Both compose files pull `registry.coadcorp.com/nathan/xteve:latest`. To build locally instead, uncomment the `build:` block in the compose file, or run `docker build -t xteve:local .`. -* `XTEVE_CONFIG` (default: `/xteve/config`) -* `XTEVE_PORT` (default: `34400`) +### Environment variables -### Image details +| Variable | Default | Meaning | +|---|---|---| +| `XTEVE_CONFIG` | `/xteve/config` | Config directory inside the container. | +| `XTEVE_PORT` | `34400` | HTTP port xTeVe listens on. | +| `PUID` | `1000` | User id xTeVe runs as. Set it to the owner of the host config directory. | +| `PGID` | `1000` | Group id xTeVe runs as. | +| `TZ` | unset (UTC) | Time zone, for example `Australia/Sydney`. | -* Multi-stage build (Go builder + minimal Alpine runtime) -* Runs as non-root user (`xteve`) -* Built-in healthcheck against `http://127.0.0.1:${XTEVE_PORT}/healthz` +The container starts as root, sets the `xteve` user to `PUID`/`PGID`, fixes the ownership of the config directory if needed, and then drops to that user before starting xTeVe. If the container is started with `--user`, the ids are left alone and xTeVe runs as the given user. The old `XTEVE_UID`/`XTEVE_GID` variables are still accepted as aliases. + +### Volume + +The config lives in `/xteve/config` (declared as a volume in the image). The compose files bind it to `./docker-data/config`. It holds `settings.json`, the mapping and EPG data, backups and the cache, so it is small enough to back up as a whole. + +If an older container kept its config in `/config` or `/home/xteve/.xteve`, the entrypoint copies it to the new location on first start. + +### Healthcheck + +The image has a `HEALTHCHECK` that requests `http://127.0.0.1:${XTEVE_PORT}/healthz` every 30 seconds. The endpoint needs no authentication and returns the version and whether a scan is in progress. `docker ps` shows the state as `healthy` or `unhealthy`. + +### ffmpeg + +A static ffmpeg and ffprobe are in the image at `/usr/local/bin/ffmpeg` and `/usr/local/bin/ffprobe`, taken from the pinned `mwader/static-ffmpeg` image. In a container xTeVe defaults `ffmpeg.path` to that location. VLC is not included. --- -## Downloads v2 | 64 Bit only -#### 64 Bit Intel / AMD +## Security notes -* [Windows](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_windows_amd64.zip?raw=true) -* [OS X](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_darwin_amd64.zip?raw=true) -* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_amd64.zip?raw=true) -* [FreeBSD](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_freebsd_amd64.zip?raw=true) - -#### 64 Bit ARM -* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_arm64.zip?raw=true) - -#### Recommended Docker Image (Linux 64 Bit) -Thanks to @alturismo and @LeeD for creating the Docker Images. - -**Created by alturismo:** -[xTeVe](https://hub.docker.com/r/alturismo/xteve) -[xTeVe / Guide2go](https://hub.docker.com/r/alturismo/xteve_guide2go) -[xTeVe / Guide2go / owi2plex](https://hub.docker.com/r/alturismo/xteve_g2g_owi) - -Including: -- Guide2go: XMLTV grabber for Schedules Direct -- owi2plex: XMLTV file grabber for Enigma receivers - -**Created by LeeD:** -[xTeVe / Guide2go / Zap2XML](https://hub.docker.com/r/dnsforge/xteve) - -Including: -- Guide2go: XMLTV grabber for Schedules Direct -- Zap2XML: Perl based zap2it XMLTV grabber -- Bash: A Unix / Linux shell -- Crond: Daemon to execute scheduled commands -- Perl: Programming language +* This fork is meant for a trusted LAN. Do not put it on the internet. +* Web authentication is off by default. Turn it on under Settings > Authentication before you expose it any further than the LAN, for example through a reverse proxy. +* `/healthz` is always unauthenticated. It reveals the version and scan state only. +* The container starts as root to apply `PUID`/`PGID` and then drops privileges. Start it with `--user` if you prefer it never to run as root. --- -### xTeVe Beta branch -New features and bug fixes are only available in beta branch. Only after successful testing are they are merged into the master branch. - -**It is not recommended to use the beta version in a production system.** - -With the command line argument `branch` the Git Branch can be changed. xTeVe must be started via the terminal. - -#### Switch from master to beta branch: -``` -xteve -branch beta - -... -[xTeVe] GitHub: https://github.com/xteve-project -[xTeVe] Git Branch: beta [xteve-project] -... -``` - -#### Switch from beta to master branch: -``` -xteve -branch master - -... -[xTeVe] GitHub: https://github.com/xteve-project -[xTeVe] Git Branch: master [xteve-project] -... -``` - -When the branch is changed, an update is only performed if there is a new version and the update function is activated in the settings. - ---- - -## Build from source code [Go / Golang] - -#### Requirements -* [Go](https://golang.org) (go1.16.2 or newer) - -#### Dependencies -* [go-ssdp](https://github.com/koron/go-ssdp) -* [websocket](https://github.com/gorilla/websocket) -* [osext](https://github.com/kardianos/osext) - -#### Build -1. Download source code -2. Install dependencies -``` -go get github.com/koron/go-ssdp -go get github.com/gorilla/websocket -go get github.com/kardianos/osext -``` -3. Build xTeVe -``` -go build xteve.go -``` - ---- - -## Fork without pull request :mega: -When creating a fork, the xTeVe GitHub account must be changed from the source code or the update function disabled. -Future updates of the xteve-project would update your fork. :wink: - -xteve.go - Line: 29 -```Go -var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-Downloads", Update: true} - -/* - Branch: GitHub Branch - User: GitHub Username - Repo: GitHub Repository - Update: Automatic updates from the GitHub repository [true|false] -*/ - -``` +## Development +See [README-DEV.md](README-DEV.md) for the layout, how to build the binary and the web UI, and how CI publishes the image. diff --git a/docker-compose.host.yml b/docker-compose.host.yml index eb58f26..3a2c3dd 100644 --- a/docker-compose.host.yml +++ b/docker-compose.host.yml @@ -1,18 +1,26 @@ +# Host-network compose file for the private xTeVe image. +# +# Host networking lets SSDP/DLNA multicast reach the container, so Plex and +# Emby can discover the tuner automatically. Ports are not published in this +# mode; xTeVe listens on XTEVE_PORT directly on the host. Linux only. services: xteve: - build: - context: . - dockerfile: Dockerfile - args: - XTEVE_UID: ${XTEVE_UID:-1000} - XTEVE_GID: ${XTEVE_GID:-1000} + image: registry.coadcorp.com/nathan/xteve:latest + # Uncomment to build locally instead of pulling from the registry. + # build: + # context: . + # dockerfile: Dockerfile container_name: xteve restart: unless-stopped network_mode: host environment: XTEVE_CONFIG: /xteve/config XTEVE_PORT: "34400" - XTEVE_UID: ${XTEVE_UID:-1000} - XTEVE_GID: ${XTEVE_GID:-1000} + # Owner of the config directory inside the container. Set these to the + # uid/gid that owns ./docker-data/config on the host. + PUID: ${PUID:-1000} + PGID: ${PGID:-1000} + # Time zone for log timestamps and the EPG, for example Australia/Sydney. + TZ: ${TZ:-UTC} volumes: - ./docker-data/config:/xteve/config diff --git a/docker-compose.yml b/docker-compose.yml index bf25840..dbeb3a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,29 @@ +# Bridge-mode compose file for the private xTeVe image. +# +# SSDP/DLNA discovery (how Plex and Emby find the tuner automatically) uses +# multicast on UDP 1900. Multicast does not cross the Docker bridge, so +# publishing 1900/udp below is not enough on its own. If you want discovery +# to work, use host networking instead: docker-compose.host.yml. In bridge +# mode you can still add the tuner in Plex/Emby by entering +# http://:34400 manually. services: xteve: - build: - context: . - dockerfile: Dockerfile - args: - XTEVE_UID: ${XTEVE_UID:-1000} - XTEVE_GID: ${XTEVE_GID:-1000} + image: registry.coadcorp.com/nathan/xteve:latest + # Uncomment to build locally instead of pulling from the registry. + # build: + # context: . + # dockerfile: Dockerfile container_name: xteve restart: unless-stopped environment: XTEVE_CONFIG: /xteve/config XTEVE_PORT: "34400" - XTEVE_UID: ${XTEVE_UID:-1000} - XTEVE_GID: ${XTEVE_GID:-1000} + # Owner of the config directory inside the container. Set these to the + # uid/gid that owns ./docker-data/config on the host. + PUID: ${PUID:-1000} + PGID: ${PGID:-1000} + # Time zone for log timestamps and the EPG, for example Australia/Sydney. + TZ: ${TZ:-UTC} ports: - "34400:34400/tcp" - "1900:1900/udp" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 58280b2..f857a2e 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,7 +2,16 @@ set -eu DEFAULT_CONFIG_DIR="/xteve/config" -LEGACY_CONFIG_DIRS="/config /xteve /home/xteve/.xteve" +# Older images and other Docker images kept the config in one of these. +# /xteve itself is not listed because it is the parent of the default dir. +LEGACY_CONFIG_DIRS="/config /home/xteve/.xteve" + +RUN_USER="xteve" +XTEVE_BIN="/usr/local/bin/xteve" + +log() { + echo "[entrypoint] $*" +} resolve_config_dir() { if [ -n "${XTEVE_CONFIG:-}" ] && [ "${XTEVE_CONFIG}" != "${DEFAULT_CONFIG_DIR}" ]; then @@ -48,11 +57,75 @@ settings_initialized() { return 1 } +is_number() { + case "$1" in + ''|*[!0-9]*) return 1 ;; + *) return 0 ;; + esac +} + +# Apply PUID/PGID to the xteve user and group. Only called when running as root. +# Falls back to XTEVE_UID/XTEVE_GID (the old variable names), then to the ids +# baked into the image at build time. +apply_ids() { + current_uid="$(id -u "${RUN_USER}")" + current_gid="$(id -g "${RUN_USER}")" + + want_uid="${PUID:-${XTEVE_UID:-${current_uid}}}" + want_gid="${PGID:-${XTEVE_GID:-${current_gid}}}" + + if ! is_number "${want_uid}" || ! is_number "${want_gid}"; then + log "ERROR: PUID/PGID must be numeric (got PUID=${want_uid} PGID=${want_gid})" >&2 + exit 1 + fi + + if [ "${want_gid}" != "${current_gid}" ]; then + # Edit /etc/group in place. deluser/delgroup on busybox refuse to remove + # a group that still has members, so sed is the safest option. + sed -i "s/^${RUN_USER}:\([^:]*\):${current_gid}:/${RUN_USER}:\1:${want_gid}:/" /etc/group + fi + + if [ "${want_uid}" != "${current_uid}" ] || [ "${want_gid}" != "${current_gid}" ]; then + sed -i "s/^${RUN_USER}:\([^:]*\):${current_uid}:${current_gid}:/${RUN_USER}:\1:${want_uid}:${want_gid}:/" /etc/passwd + fi + + new_uid="$(id -u "${RUN_USER}")" + new_gid="$(id -g "${RUN_USER}")" + if [ "${new_uid}" != "${want_uid}" ] || [ "${new_gid}" != "${want_gid}" ]; then + log "ERROR: failed to set ${RUN_USER} to UID:GID ${want_uid}:${want_gid} (now ${new_uid}:${new_gid})" >&2 + exit 1 + fi +} + +# Make sure the config directory belongs to the xteve user. The directory is +# small (settings, cache, backups), so a recursive chown is acceptable, but it +# is skipped when the ownership is already right. +fix_config_owner() { + dir="$1" + uid="$2" + gid="$3" + + owner="$(stat -c '%u:%g' "${dir}" 2>/dev/null || echo "")" + if [ "${owner}" != "${uid}:${gid}" ] || [ "${MIGRATED}" = "1" ]; then + log "Setting ownership of ${dir} to ${uid}:${gid}" + chown -R "${uid}:${gid}" "${dir}" || log "WARNING: chown of ${dir} failed; continuing" + fi +} + CONFIG_DIR="$(resolve_config_dir)" PORT="${XTEVE_PORT:-34400}" +# The binary uses XTEVE_CONFIG to detect that it runs in a container and to +# default ffmpeg.path to /usr/local/bin/ffmpeg. Keep it in sync with the +# resolved directory so both agree. +export XTEVE_CONFIG="${CONFIG_DIR}" + mkdir -p "${CONFIG_DIR}" +# Set to 1 when files were copied from a legacy directory. When that happens as +# root the copies are root-owned, so the ownership fix below must not be skipped. +MIGRATED=0 + if ! settings_initialized "${CONFIG_DIR}/settings.json"; then for legacy_dir in ${LEGACY_CONFIG_DIRS}; do if [ "${legacy_dir}" = "${CONFIG_DIR}" ]; then @@ -60,7 +133,7 @@ if ! settings_initialized "${CONFIG_DIR}/settings.json"; then fi if settings_initialized "${legacy_dir}/settings.json"; then - echo "[entrypoint] Migrating existing configuration from ${legacy_dir} to ${CONFIG_DIR}" + log "Migrating existing configuration from ${legacy_dir} to ${CONFIG_DIR}" for file in authentication.json pms.json settings.json xepg.json urls.json; do if [ "${file}" = "settings.json" ] || [ "${file}" = "xepg.json" ] || [ "${file}" = "urls.json" ]; then @@ -74,24 +147,55 @@ if ! settings_initialized "${CONFIG_DIR}/settings.json"; then copy_if_missing "${legacy_dir}/${dir_name}" "${CONFIG_DIR}/${dir_name}" done + MIGRATED=1 break fi done fi +if [ "$(id -u)" = "0" ]; then + # Started as root (the default): apply PUID/PGID, fix ownership, drop privileges. + apply_ids + RUN_UID="$(id -u "${RUN_USER}")" + RUN_GID="$(id -g "${RUN_USER}")" + fix_config_owner "${CONFIG_DIR}" "${RUN_UID}" "${RUN_GID}" + + if ! su-exec "${RUN_USER}:${RUN_USER}" touch "${CONFIG_DIR}/.xteve-write-test" 2>/dev/null; then + log "ERROR: Config directory is not writable: ${CONFIG_DIR}" >&2 + log "xteve would run as UID:GID ${RUN_UID}:${RUN_GID}" >&2 + ls -ld "${CONFIG_DIR}" >&2 || true + log "Hint: set PUID/PGID to the owner of the host directory, or fix its ownership/permissions on the host." >&2 + exit 1 + fi + rm -f "${CONFIG_DIR}/.xteve-write-test" + + log "Using config directory: ${CONFIG_DIR}" + log "Running as UID:GID ${RUN_UID}:${RUN_GID} (PUID/PGID)" + if [ -f "${CONFIG_DIR}/settings.json" ]; then + log "settings.json details: $(ls -l "${CONFIG_DIR}/settings.json" | awk '{print $1, $3, $4, $5, $9}')" + fi + + exec su-exec "${RUN_USER}:${RUN_USER}" "${XTEVE_BIN}" -config "${CONFIG_DIR}" -port "${PORT}" "$@" +fi + +# Started with --user: keep the given identity, no id or ownership changes. +if [ -n "${PUID:-}${PGID:-}${XTEVE_UID:-}${XTEVE_GID:-}" ]; then + log "Container started as non-root; PUID/PGID are ignored" +fi + if ! touch "${CONFIG_DIR}/.xteve-write-test" 2>/dev/null; then - echo "[entrypoint] ERROR: Config directory is not writable: ${CONFIG_DIR}" >&2 - echo "[entrypoint] Running as UID:GID $(id -u):$(id -g)" >&2 + log "ERROR: Config directory is not writable: ${CONFIG_DIR}" >&2 + log "Running as UID:GID $(id -u):$(id -g)" >&2 ls -ld "${CONFIG_DIR}" >&2 || true - echo "[entrypoint] Hint: ensure host path ownership/permissions allow this UID:GID to write, or set matching container UID/GID at build time." >&2 + log "Hint: ensure the host path ownership/permissions allow this UID:GID to write, or drop --user and set PUID/PGID instead." >&2 exit 1 fi rm -f "${CONFIG_DIR}/.xteve-write-test" -echo "[entrypoint] Using config directory: ${CONFIG_DIR}" -echo "[entrypoint] Running as UID:GID $(id -u):$(id -g)" +log "Using config directory: ${CONFIG_DIR}" +log "Running as UID:GID $(id -u):$(id -g)" if [ -f "${CONFIG_DIR}/settings.json" ]; then - echo "[entrypoint] settings.json details: $(ls -l "${CONFIG_DIR}/settings.json" | awk '{print $1, $3, $4, $5, $9}')" + log "settings.json details: $(ls -l "${CONFIG_DIR}/settings.json" | awk '{print $1, $3, $4, $5, $9}')" fi -exec /usr/local/bin/xteve -config "${CONFIG_DIR}" -port "${PORT}" "$@" +exec "${XTEVE_BIN}" -config "${CONFIG_DIR}" -port "${PORT}" "$@" diff --git a/src/authentication.go b/src/authentication.go index ebfee34..7720f29 100644 --- a/src/authentication.go +++ b/src/authentication.go @@ -63,7 +63,7 @@ func createFirstUserForAuthentication(username, password string) (token string, func tokenAuthentication(token string) (newToken string, err error) { - if System.ConfigurationWizard == true { + if System.ConfigurationWizard { return } @@ -109,7 +109,7 @@ func urlAuth(r *http.Request, requestType string) (err error) { case "m3u": level = "authentication.m3u" - if Settings.AuthenticationM3U == true { + if Settings.AuthenticationM3U { token, err = authentication.UserAuthentication(username, password) if err != nil { return @@ -119,7 +119,7 @@ func urlAuth(r *http.Request, requestType string) (err error) { case "xml": level = "authentication.xml" - if Settings.AuthenticationXML == true { + if Settings.AuthenticationXML { token, err = authentication.UserAuthentication(username, password) if err != nil { return @@ -150,18 +150,22 @@ func checkAuthorizationLevel(token, level string) (err error) { if v, ok := userData[level].(bool); ok { - if v == false { + if !v { err = errors.New("No authorization") } } else { userData[level] = false - err = authentication.WriteUserData(userID, userData) + if err = authentication.WriteUserData(userID, userData); err != nil { + ShowError(err, 0) + } err = errors.New("No authorization") } } else { - err = authentication.WriteUserData(userID, userData) + if err = authentication.WriteUserData(userID, userData); err != nil { + ShowError(err, 0) + } err = errors.New("No authorization") } diff --git a/src/backup.go b/src/backup.go index 437fbcb..10c6102 100644 --- a/src/backup.go +++ b/src/backup.go @@ -1,274 +1,273 @@ package src import ( - b64 "encoding/base64" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" + b64 "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" ) func xTeVeAutoBackup() (err error) { - var archiv = "xteve_auto_backup_" + time.Now().Format("20060102_1504") + ".zip" - var target string - var sourceFiles = make([]string, 0) - var oldBackupFiles = make([]string, 0) - var debug string + var archiv = "xteve_auto_backup_" + time.Now().Format("20060102_1504") + ".zip" + var target string + var sourceFiles = make([]string, 0) + var oldBackupFiles = make([]string, 0) + var debug string - if len(Settings.BackupPath) > 0 { - System.Folder.Backup = Settings.BackupPath - } + if len(Settings.BackupPath) > 0 { + System.Folder.Backup = Settings.BackupPath + } - showInfo("Backup Path:" + System.Folder.Backup) + showInfo("Backup Path:" + System.Folder.Backup) - err = checkFolder(System.Folder.Backup) - if err != nil { - ShowError(err, 1070) - return - } + err = checkFolder(System.Folder.Backup) + if err != nil { + ShowError(err, 1070) + return + } - // Alte Backups löschen - files, err := ioutil.ReadDir(System.Folder.Backup) + // Alte Backups löschen + files, err := os.ReadDir(System.Folder.Backup) - if err == nil { + if err == nil { - for _, file := range files { + for _, file := range files { - if filepath.Ext(file.Name()) == ".zip" && strings.Contains(file.Name(), "xteve_auto_backup") { - oldBackupFiles = append(oldBackupFiles, file.Name()) - } + if filepath.Ext(file.Name()) == ".zip" && strings.Contains(file.Name(), "xteve_auto_backup") { + oldBackupFiles = append(oldBackupFiles, file.Name()) + } - } + } - // Alle Backups löschen - var end int - switch Settings.BackupKeep { - case 0: - end = 0 - default: - end = Settings.BackupKeep - 1 - } + // Alle Backups löschen + var end int + switch Settings.BackupKeep { + case 0: + end = 0 + default: + end = Settings.BackupKeep - 1 + } - for i := 0; i < len(oldBackupFiles)-end; i++ { + for i := 0; i < len(oldBackupFiles)-end; i++ { - os.RemoveAll(System.Folder.Backup + oldBackupFiles[i]) - debug = fmt.Sprintf("Delete backup file:%s", oldBackupFiles[i]) - showDebug(debug, 1) + os.RemoveAll(System.Folder.Backup + oldBackupFiles[i]) + debug = fmt.Sprintf("Delete backup file:%s", oldBackupFiles[i]) + showDebug(debug, 1) - } + } - if Settings.BackupKeep == 0 { - return - } + if Settings.BackupKeep == 0 { + return + } - } else { + } else { - return + return - } + } - // Backup erstellen - if err == nil { + // Backup erstellen + if err == nil { - target = System.Folder.Backup + archiv + target = System.Folder.Backup + archiv - for _, i := range SystemFiles { - sourceFiles = append(sourceFiles, System.Folder.Config+i) - } + for _, i := range SystemFiles { + sourceFiles = append(sourceFiles, System.Folder.Config+i) + } - sourceFiles = append(sourceFiles, System.Folder.ImagesUpload) + sourceFiles = append(sourceFiles, System.Folder.ImagesUpload) - err = zipFiles(sourceFiles, target) + err = zipFiles(sourceFiles, target) - if err == nil { + if err == nil { - debug = fmt.Sprintf("Create backup file:%s", target) - showDebug(debug, 1) + debug = fmt.Sprintf("Create backup file:%s", target) + showDebug(debug, 1) - showInfo("Backup file:" + target) + showInfo("Backup file:" + target) - } + } - } + } - return + return } func xteveBackup() (archiv string, err error) { - err = checkFolder(System.Folder.Temp) - if err != nil { - return - } + err = checkFolder(System.Folder.Temp) + if err != nil { + return + } - archiv = "xteve_backup_" + time.Now().Format("20060102_1504") + ".zip" + archiv = "xteve_backup_" + time.Now().Format("20060102_1504") + ".zip" - var target = System.Folder.Temp + archiv - var sourceFiles = make([]string, 0) + var target = System.Folder.Temp + archiv + var sourceFiles = make([]string, 0) - for _, i := range SystemFiles { - sourceFiles = append(sourceFiles, System.Folder.Config+i) - } + for _, i := range SystemFiles { + sourceFiles = append(sourceFiles, System.Folder.Config+i) + } - sourceFiles = append(sourceFiles, System.Folder.Data) + sourceFiles = append(sourceFiles, System.Folder.Data) - err = zipFiles(sourceFiles, target) - if err != nil { - ShowError(err, 0) - return - } + err = zipFiles(sourceFiles, target) + if err != nil { + ShowError(err, 0) + return + } - return + return } func xteveRestore(archive string) (newWebURL string, err error) { - var newPort, oldPort, backupVersion, tmpRestore string + var newPort, oldPort, backupVersion, tmpRestore string - tmpRestore = System.Folder.Temp + "restore" + string(os.PathSeparator) + tmpRestore = System.Folder.Temp + "restore" + string(os.PathSeparator) - err = checkFolder(tmpRestore) - if err != nil { - return - } + err = checkFolder(tmpRestore) + if err != nil { + return + } - // Zip Archiv in tmp entpacken - err = extractZIP(archive, tmpRestore) - if err != nil { - return - } + // Zip Archiv in tmp entpacken + err = extractZIP(archive, tmpRestore) + if err != nil { + return + } - // Neue Config laden um den Port und die Version zu überprüfen - newConfig, err := loadJSONFileToMap(tmpRestore + "settings.json") - if err != nil { - ShowError(err, 0) - return - } + // Neue Config laden um den Port und die Version zu überprüfen + newConfig, err := loadJSONFileToMap(tmpRestore + "settings.json") + if err != nil { + ShowError(err, 0) + return + } - backupVersion = newConfig["version"].(string) - if backupVersion < System.Compatibility { - err = errors.New(getErrMsg(1013)) - return - } + backupVersion = newConfig["version"].(string) + if backupVersion < System.Compatibility { + err = errors.New(getErrMsg(1013)) + return + } - // Zip Archiv in den Config Ordner entpacken - err = extractZIP(archive, System.Folder.Config) - if err != nil { - return - } + // Zip Archiv in den Config Ordner entpacken + err = extractZIP(archive, System.Folder.Config) + if err != nil { + return + } - // Neue Config laden um den Port und die Version zu überprüfen - newConfig, err = loadJSONFileToMap(System.Folder.Config + "settings.json") - if err != nil { - ShowError(err, 0) - return - } + // Neue Config laden um den Port und die Version zu überprüfen + newConfig, err = loadJSONFileToMap(System.Folder.Config + "settings.json") + if err != nil { + ShowError(err, 0) + return + } - newPort = newConfig["port"].(string) - oldPort = Settings.Port + newPort = newConfig["port"].(string) + oldPort = Settings.Port - if newPort == oldPort { + if newPort == oldPort { - if err != nil { - ShowError(err, 0) - } + if err != nil { + ShowError(err, 0) + } - loadSettings() + loadSettings() - err := Init() - if err != nil { - ShowError(err, 0) - return "", err - } + err := Init() + if err != nil { + ShowError(err, 0) + return "", err + } - err = StartSystem(true) - if err != nil { - ShowError(err, 0) - return "", err - } + err = StartSystem(true) + if err != nil { + ShowError(err, 0) + return "", err + } - return "", err - } + return "", err + } - var url = System.URLBase + "/web/" - newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1) + var url = System.URLBase + "/web/" + newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1) - os.RemoveAll(tmpRestore) + os.RemoveAll(tmpRestore) - return + return } func xteveRestoreFromWeb(input string) (newWebURL string, err error) { - // Base64 Json String in base64 umwandeln - b64data := input[strings.IndexByte(input, ',')+1:] + // Base64 Json String in base64 umwandeln + b64data := input[strings.IndexByte(input, ',')+1:] - // Base64 in bytes umwandeln und speichern - sDec, err := b64.StdEncoding.DecodeString(b64data) + // Base64 in bytes umwandeln und speichern + sDec, err := b64.StdEncoding.DecodeString(b64data) - if err != nil { - return - } + if err != nil { + return + } - var archive = System.Folder.Temp + "restore.zip" + var archive = System.Folder.Temp + "restore.zip" - err = writeByteToFile(archive, sDec) - if err != nil { - return - } + err = writeByteToFile(archive, sDec) + if err != nil { + return + } - newWebURL, err = xteveRestore(archive) + newWebURL, err = xteveRestore(archive) - return + return } // XteveRestoreFromCLI : Wiederherstellung über die Kommandozeile func XteveRestoreFromCLI(archive string) (err error) { - var confirm string + var confirm string - println() - showInfo(fmt.Sprintf("Version:%s Build: %s", System.Version, System.Build)) - showInfo(fmt.Sprintf("Backup File:%s", archive)) - showInfo(fmt.Sprintf("System Folder:%s", getPlatformPath(System.Folder.Config))) - println() + println() + showInfo(fmt.Sprintf("Version:%s Build: %s", System.Version, System.Build)) + showInfo(fmt.Sprintf("Backup File:%s", archive)) + showInfo(fmt.Sprintf("System Folder:%s", getPlatformPath(System.Folder.Config))) + println() - fmt.Print("All data will be replaced with those from the backup. Should the files be restored? [yes|no]:") + fmt.Print("All data will be replaced with those from the backup. Should the files be restored? [yes|no]:") - fmt.Scanln(&confirm) + fmt.Scanln(&confirm) - switch strings.ToLower(confirm) { + switch strings.ToLower(confirm) { - case "yes": - break + case "yes": + break - case "no": - return + case "no": + return - default: - fmt.Println("Invalid input") - return + default: + fmt.Println("Invalid input") + return - } + } - if len(System.Folder.Config) > 0 { + if len(System.Folder.Config) > 0 { - err = checkFilePermission(System.Folder.Config) - if err != nil { - return - } + err = checkFilePermission(System.Folder.Config) + if err != nil { + return + } - _, err = xteveRestore(archive) - if err != nil { - return - } + _, err = xteveRestore(archive) + if err != nil { + return + } - showHighlight(fmt.Sprintf("Restor:Backup was successfully restored. %s can now be started normally", System.Name)) + showHighlight(fmt.Sprintf("Restor:Backup was successfully restored. %s can now be started normally", System.Name)) - } - return + } + return } diff --git a/src/buffer.go b/src/buffer.go index edd4b9d..68fff80 100644 --- a/src/buffer.go +++ b/src/buffer.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -146,7 +145,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon } // Neuer Stream bei einer bereits aktiven Playlist - if newStream == true { + if newStream { // Prüfen ob die Playlist noch einen weiteren Stream erlaubt (Tuner) if len(playlist.Streams) >= playlist.Tuner { @@ -193,7 +192,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon } // Überprüfen ob der Stream breits von einem anderen Client abgespielt wird - if playlist.Streams[streamID].Status == false && newStream == true { + if !playlist.Streams[streamID].Status && newStream { // Neuer Buffer wird benötigt stream = playlist.Streams[streamID] @@ -235,7 +234,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon if stream, ok := playlist.Streams[streamID]; ok { - if stream.Status == false { + if !stream.Status { timeOut++ @@ -261,6 +260,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon // HTTP Clientverbindung überwachen + //lint:ignore SA1019 replaced by request context in Phase 2 cn, ok := w.(http.CloseNotifier) if ok { @@ -310,10 +310,11 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon var fileName = stream.Folder + f file, err := os.Open(fileName) - defer file.Close() if err == nil { + defer file.Close() + l, err := file.Stat() if err == nil { @@ -327,7 +328,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon file.Seek(0, 0) - if streaming == false { + if !streaming { contentType := http.DetectContentType(buffer) _ = contentType @@ -406,7 +407,7 @@ func getTmpFiles(stream *ThisStream) (tmpFiles []string) { if _, err := os.Stat(tmpFolder); !os.IsNotExist(err) { - files, err := ioutil.ReadDir(getPlatformPath(tmpFolder)) + files, err := os.ReadDir(getPlatformPath(tmpFolder)) if err != nil { ShowError(err, 000) return @@ -455,7 +456,7 @@ func killClientConnection(streamID int, playlistID string, force bool) { var playlist = p.(Playlist) - if force == true { + if force { delete(playlist.Streams, streamID) showInfo(fmt.Sprintf("Streaming Status:Playlist: %s - Tuner: %d / %d", playlist.PlaylistName, len(playlist.Streams), playlist.Tuner)) return @@ -531,6 +532,21 @@ func clientConnection(stream ThisStream) (status bool) { return } +// addErrorToStream : Fehler an die Clientverbindung des Streams weitergeben +func addErrorToStream(playlist Playlist, streamID int, playlistID string, err error) { + + var stream = playlist.Streams[streamID] + + if c, ok := BufferClients.Load(playlistID + stream.MD5); ok { + + var clients = c.(ClientConnection) + clients.Error = err + BufferClients.Store(playlistID+stream.MD5, clients) + + } + +} + func connectToStreamingServer(streamID int, playlistID string) { if p, ok := BufferInformation.Load(playlistID); ok { @@ -575,26 +591,12 @@ func connectToStreamingServer(streamID int, playlistID string) { } - var addErrorToStream = func(err error) { - - var stream = playlist.Streams[streamID] - - if c, ok := BufferClients.Load(playlistID + stream.MD5); ok { - - var clients = c.(ClientConnection) - clients.Error = err - BufferClients.Store(playlistID+stream.MD5, clients) - - } - - } - os.RemoveAll(getPlatformPath(tmpFolder)) err := checkFolder(tmpFolder) if err != nil { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) return } @@ -609,11 +611,11 @@ func connectToStreamingServer(streamID int, playlistID string) { return } - var stream ThisStream = playlist.Streams[streamID] + var stream = playlist.Streams[streamID] - if stream.Status == false { + if !stream.Status { - if strings.Index(stream.URL, ".m3u8") != -1 { + if strings.Contains(stream.URL, ".m3u8") { showInfo("Streaming Type:" + "[HLS / M3U8]") } else { showInfo("Streaming Type:" + "[TS]") @@ -631,7 +633,7 @@ func connectToStreamingServer(streamID int, playlistID string) { for { - if clientConnection(stream) == false { + if !clientConnection(stream) { return } @@ -654,6 +656,14 @@ func connectToStreamingServer(streamID int, playlistID string) { Redirect: req, err := http.NewRequest("GET", currentURL, nil) + if err != nil { + ShowError(err, 0) + addErrorToStream(playlist, streamID, playlistID, err) + killClientConnection(streamID, playlistID, true) + clientConnection(stream) + return + } + req.Header.Set("User-Agent", getUserAgent()) req.Header.Set("Connection", "close") //req.Header.Set("Range", "bytes=0-") @@ -679,7 +689,7 @@ func connectToStreamingServer(streamID int, playlistID string) { fmt.Println("Current URL:", currentURL) ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) killClientConnection(streamID, playlistID, true) clientConnection(stream) @@ -708,7 +718,7 @@ func connectToStreamingServer(streamID int, playlistID string) { err = errors.New("Streaming server") ShowError(err, 4002) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) defer resp.Body.Close() @@ -719,7 +729,7 @@ func connectToStreamingServer(streamID int, playlistID string) { } else { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) defer resp.Body.Close() @@ -749,7 +759,7 @@ func connectToStreamingServer(streamID int, playlistID string) { showDebug(debug, 1) BufferInformation.Store(playlist.PlaylistID, playlist) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) killClientConnection(streamID, playlistID, true) clientConnection(stream) @@ -759,7 +769,7 @@ func connectToStreamingServer(streamID int, playlistID string) { } // Informationen über den Streamingserver auslesen - if stream.Status == false { + if !stream.Status { if len(stream.URLStreamingServer) == 0 { @@ -802,10 +812,10 @@ func connectToStreamingServer(streamID int, playlistID string) { // M3U8 Playlist case "application/x-mpegurl", "application/vnd.apple.mpegurl", "audio/mpegurl", "audio/x-mpegurl": - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) } stream.Body = string(body) @@ -815,7 +825,7 @@ func connectToStreamingServer(streamID int, playlistID string) { err = parseM3U8(&stream) if err != nil { ShowError(err, 4050) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) } // Video Stream (TS) @@ -835,7 +845,7 @@ func connectToStreamingServer(streamID int, playlistID string) { var tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment) - if clientConnection(stream) == false { + if !clientConnection(stream) { resp.Body.Close() return } @@ -843,7 +853,7 @@ func connectToStreamingServer(streamID int, playlistID string) { bufferFile, err := os.Create(tmpFile) if err != nil { - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) bufferFile.Close() resp.Body.Close() return @@ -866,7 +876,7 @@ func connectToStreamingServer(streamID int, playlistID string) { if err != nil && err != io.EOF { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) resp.Body.Close() return @@ -877,7 +887,7 @@ func connectToStreamingServer(streamID int, playlistID string) { if _, err := bufferFile.Write(buffer[:n]); err != nil { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) resp.Body.Close() return @@ -887,7 +897,7 @@ func connectToStreamingServer(streamID int, playlistID string) { fileSize = fileSize + n - if clientConnection(stream) == false { + if !clientConnection(stream) { resp.Body.Close() bufferFile.Close() @@ -929,7 +939,7 @@ func connectToStreamingServer(streamID int, playlistID string) { tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment) - if clientConnection(stream) == false { + if !clientConnection(stream) { bufferFile.Close() resp.Body.Close() @@ -944,7 +954,7 @@ func connectToStreamingServer(streamID int, playlistID string) { bufferFile, err = os.Create(tmpFile) if err != nil { - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) resp.Body.Close() return } @@ -970,7 +980,7 @@ func connectToStreamingServer(streamID int, playlistID string) { err = errors.New("Streaming error") ShowError(err, 4003) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) resp.Body.Close() return } @@ -978,7 +988,7 @@ func connectToStreamingServer(streamID int, playlistID string) { s++ // Wartezeit für den Download das nächste Segments berechnen - if stream.HLS == true { + if stream.HLS { var sleep float64 @@ -1143,14 +1153,13 @@ func parseM3U8(stream *ThisStream) (err error) { } - return } if strings.Contains(stream.Body, "#EXTM3U") { var lines = strings.Split(strings.Replace(stream.Body, "\r\n", "\n", -1), "\n") - if stream.DynamicBandwidth == false { + if !stream.DynamicBandwidth { stream.DynamicStream = make(map[int]DynamicStream) } @@ -1173,7 +1182,7 @@ func parseM3U8(stream *ThisStream) (err error) { } // M3U8 enthällt mehrere Links zu weiteren M3U8 Wiedergabelisten (Bandbreitenoption) - if segment.Info == true && len(line) > 0 && line[0:1] != "#" { + if segment.Info && len(line) > 0 && line[0:1] != "#" { var dynamicStream DynamicStream @@ -1217,7 +1226,7 @@ func parseM3U8(stream *ThisStream) (err error) { noNewSegment = true - if stream.Status == false { + if !stream.Status { if len(m3u8Segments) >= 2 { m3u8Segments = m3u8Segments[0 : len(m3u8Segments)-1] @@ -1229,7 +1238,7 @@ func parseM3U8(stream *ThisStream) (err error) { segment = s - if stream.Status == false { + if !stream.Status { noNewSegment = false stream.LastSequence = segment.Sequence @@ -1255,9 +1264,9 @@ func parseM3U8(stream *ThisStream) (err error) { } - if noNewSegment == false { + if !noNewSegment { - if stream.DynamicBandwidth == true { + if stream.DynamicBandwidth { switchBandwidth(stream) } else { stream.Segment = append(stream.Segment, segment) @@ -1265,7 +1274,7 @@ func parseM3U8(stream *ThisStream) (err error) { } - if noNewSegment == true { + if noNewSegment { var sleep = lastSegmentDuration * 0.5 @@ -1371,33 +1380,19 @@ func thirdPartyBuffer(streamID int, playlistID string) { return } - var addErrorToStream = func(err error) { - - var stream = playlist.Streams[streamID] - - if c, ok := BufferClients.Load(playlistID + stream.MD5); ok { - - var clients = c.(ClientConnection) - clients.Error = err - BufferClients.Store(playlistID+stream.MD5, clients) - - } - - } - os.RemoveAll(getPlatformPath(tmpFolder)) err := checkFolder(tmpFolder) if err != nil { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) return } err = checkFile(path) if err != nil { ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) return } @@ -1409,7 +1404,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { f, err := os.Create(tmpFile) f.Close() if err != nil { - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) return } @@ -1461,7 +1456,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { ShowError(err, 0) cmd.Process.Kill() cmd.Wait() - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) return } @@ -1471,11 +1466,11 @@ func thirdPartyBuffer(streamID int, playlistID string) { ShowError(err, 0) cmd.Process.Kill() cmd.Wait() - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) return } - if len(buf.Bytes()) == 0 && stream.Status == false { + if len(buf.Bytes()) == 0 && !stream.Status { showInfo(bufferType + ":Processing data") } @@ -1543,7 +1538,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { cmd.Process.Kill() err = errors.New("Timout") ShowError(err, 4006) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) cmd.Wait() f.Close() return @@ -1553,11 +1548,11 @@ func thirdPartyBuffer(streamID int, playlistID string) { } - if fileSize == 0 && stream.Status == false { + if fileSize == 0 && !stream.Status { showInfo("Streaming Status:Receive data from " + bufferType) } - if clientConnection(stream) == false { + if !clientConnection(stream) { cmd.Process.Kill() f.Close() cmd.Wait() @@ -1574,7 +1569,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { if _, err := f.Write(buffer[:n]); err != nil { cmd.Process.Kill() ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) cmd.Wait() f.Close() return @@ -1582,7 +1577,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { if fileSize >= bufferSize/2 { - if tmpSegment == 1 && stream.Status == false { + if tmpSegment == 1 && !stream.Status { close(t) close(streamStatus) showInfo(fmt.Sprintf("Streaming Status:Buffering data from %s", bufferType)) @@ -1591,7 +1586,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { f.Close() tmpSegment++ - if stream.Status == false { + if !stream.Status { Lock.Lock() stream.Status = true playlist.Streams[streamID] = stream @@ -1603,13 +1598,11 @@ func thirdPartyBuffer(streamID int, playlistID string) { fileSize = 0 - var errCreate, errOpen error - f, errCreate = os.Create(tmpFile) - f, errOpen = os.OpenFile(tmpFile, os.O_APPEND|os.O_WRONLY, 0600) - if errCreate != nil || errOpen != nil { + f, err = os.OpenFile(tmpFile, os.O_CREATE|os.O_TRUNC|os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { cmd.Process.Kill() ShowError(err, 0) - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) cmd.Wait() f.Close() return @@ -1623,7 +1616,7 @@ func thirdPartyBuffer(streamID int, playlistID string) { cmd.Wait() err = errors.New(bufferType + " error") - addErrorToStream(err) + addErrorToStream(playlist, streamID, playlistID, err) ShowError(err, 1204) time.Sleep(time.Duration(500) * time.Millisecond) @@ -1694,7 +1687,6 @@ func debugRequest(req *http.Request) { debug = "Request:* * * * * * END HTTP(S) REQUEST * * * * * *" showDebug(debug, debugLevel) - return } func debugResponse(resp *http.Response) { @@ -1739,5 +1731,4 @@ func debugResponse(resp *http.Response) { debug = "Pesponse:* * * * * * END RESPONSE * * * * * * " showDebug(debug, debugLevel) - return } diff --git a/src/config.go b/src/config.go index bfea24b..6c819fe 100644 --- a/src/config.go +++ b/src/config.go @@ -152,8 +152,9 @@ func Init() (err error) { // Berechtigung aller Ordner überprüfen err = checkFilePermission(System.Folder.Config) - if err == nil { - err = checkFilePermission(System.Folder.Temp) + if err != nil { + ShowError(err, 0) + return } // Separaten tmp Ordner für jede Instanz @@ -165,6 +166,12 @@ func Init() (err error) { return } + err = checkFilePermission(System.Folder.Temp) + if err != nil { + ShowError(err, 0) + return + } + err = removeChildItems(getPlatformPath(System.Folder.Temp)) if err != nil { return @@ -173,7 +180,7 @@ func Init() (err error) { // Branch festlegen System.Branch = Settings.Branch - if System.Dev == true { + if System.Dev { System.Branch = "Development" } @@ -217,7 +224,7 @@ func StartSystem(updateProviderFiles bool) (err error) { showInfo(fmt.Sprintf("Unfiltered Chan. Limit:%d", System.UnfilteredChannelLimit)) // Providerdaten aktualisieren - if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate == true || updateProviderFiles == true { + if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate || updateProviderFiles { err = xTeVeAutoBackup() if err != nil { diff --git a/src/data.go b/src/data.go index 71448aa..a5871d1 100644 --- a/src/data.go +++ b/src/data.go @@ -73,10 +73,6 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e } - if len(newUpdateTimes) == 0 { - //newUpdateTimes = append(newUpdateTimes, "0000") - } - value = newUpdateTimes case "cache.images": @@ -179,7 +175,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e return } - if Settings.AuthenticationWEB == false { + if !Settings.AuthenticationWEB { Settings.AuthenticationAPI = false Settings.AuthenticationM3U = false @@ -221,7 +217,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e settings = Settings - if reloadData == true { + if reloadData { err = buildDatabaseDVR() if err != nil { @@ -232,7 +228,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e } - if cacheImages == true { + if cacheImages { if Settings.EpgSource == "XEPG" && System.ImageCachingInProgress == 0 { @@ -271,7 +267,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e } - if createXEPGFiles == true { + if createXEPGFiles { go func() { createXMLTVFile() @@ -280,7 +276,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e } - if triggerPlexGuideReload == true { + if triggerPlexGuideReload { queuePlexGuideRefresh("settings change") } @@ -378,7 +374,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) { return } - if reloadData == true { + if reloadData { err = buildDatabaseDVR() if err != nil { @@ -452,7 +448,6 @@ func deleteLocalProviderFiles(dataID, fileType string) { os.RemoveAll(System.Folder.Data + dataID + fileExtension) } - return } // Filtereinstellungen speichern (WebUI) @@ -505,7 +500,7 @@ func saveFilter(request RequestStruct) (settings SettingsStruct, err error) { if len(filter) == 0 { err = errors.New(getErrMsg(1014)) - if newFilter == true { + if newFilter { delete(filterMap, dataID) } @@ -573,7 +568,7 @@ func saveXEpgMapping(request RequestStruct) (err error) { // Wenn während des erstellen der Datanbank das Mapping erneut gespeichert wird, wird die Datenbank erst später erneut aktualisiert. go func() { - if System.BackgroundProcess == true { + if System.BackgroundProcess { return } @@ -591,7 +586,7 @@ func saveXEpgMapping(request RequestStruct) (err error) { cleanupXEPG() System.ScanInProgress = 0 buildXEPG(false) - showInfo("XEPG:" + fmt.Sprintf("Ready to use")) + showInfo("XEPG:" + "Ready to use") System.BackgroundProcess = false @@ -632,7 +627,7 @@ func saveUserData(request RequestStruct) (err error) { return } - if request.DeleteUser == true { + if request.DeleteUser { err = authentication.RemoveUser(userID) return } @@ -859,7 +854,7 @@ func buildDatabaseDVR() (err error) { for n, i := range playlistFile { var channels []any - var groupTitle, tvgID, uuid int = 0, 0, 0 + var groupTitle, tvgID, uuid = 0, 0, 0 var keys = []string{"group-title", "tvg-id", "uuid"} var compatibility = make(map[string]int) @@ -905,11 +900,7 @@ func buildDatabaseDVR() (err error) { if value, ok := s[key]; ok { if len(value) > 0 { - if _, ok := tmpGroupsM3U[value]; ok { - tmpGroupsM3U[value]++ - } else { - tmpGroupsM3U[value] = 1 - } + tmpGroupsM3U[value]++ groupTitle++ } @@ -985,7 +976,7 @@ func buildDatabaseDVR() (err error) { for group, count := range tmpGroupsM3U { var text = fmt.Sprintf("%s (%d)", group, count) - var value = fmt.Sprintf("%s", group) + var value = group Data.Playlist.M3U.Groups.Text = append(Data.Playlist.M3U.Groups.Text, text) Data.Playlist.M3U.Groups.Value = append(Data.Playlist.M3U.Groups.Value, value) } diff --git a/src/hdhr.go b/src/hdhr.go index d98eca0..2cce878 100644 --- a/src/hdhr.go +++ b/src/hdhr.go @@ -154,7 +154,7 @@ func getLineup() (jsonContent []byte, err error) { return } - if xepgChannel.XActive == true { + if xepgChannel.XActive { var stream LineupStream stream.GuideName = xepgChannel.XName stream.GuideNumber = xepgChannel.XChannelID diff --git a/src/images.go b/src/images.go index 04dd256..2f33445 100644 --- a/src/images.go +++ b/src/images.go @@ -1,30 +1,30 @@ package src import ( - b64 "encoding/base64" - "fmt" - "strings" + b64 "encoding/base64" + "fmt" + "strings" ) func uploadLogo(input, filename string) (logoURL string, err error) { - b64data := input[strings.IndexByte(input, ',')+1:] + b64data := input[strings.IndexByte(input, ',')+1:] - // BAse64 in bytes umwandeln un speichern - sDec, err := b64.StdEncoding.DecodeString(b64data) - if err != nil { - return - } + // BAse64 in bytes umwandeln un speichern + sDec, err := b64.StdEncoding.DecodeString(b64data) + if err != nil { + return + } - var file = fmt.Sprintf("%s%s", System.Folder.ImagesUpload, filename) + var file = fmt.Sprintf("%s%s", System.Folder.ImagesUpload, filename) - err = writeByteToFile(file, sDec) - if err != nil { - return - } + err = writeByteToFile(file, sDec) + if err != nil { + return + } - logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.XML, System.Domain, filename) + logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.XML, System.Domain, filename) - return + return } diff --git a/src/info.go b/src/info.go index 6d09ee6..db913dc 100644 --- a/src/info.go +++ b/src/info.go @@ -20,45 +20,41 @@ func ShowSystemInfo() { fmt.Println("OK") println() - fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build)) - fmt.Println(fmt.Sprintf("Branch: %s", System.Branch)) - fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config)) + fmt.Printf("Version: %s %s.%s\n", System.Name, System.Version, System.Build) + fmt.Printf("Branch: %s\n", System.Branch) + fmt.Printf("Folder (config): %s\n", System.Folder.Config) - fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All))) - fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter))) - fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount))) + fmt.Printf("Streams: %d / %d\n", len(Data.Streams.Active), len(Data.Streams.All)) + fmt.Printf("Filter: %d\n", len(Data.Filter)) + fmt.Printf("XEPG Chanels: %d\n", int(Data.XEPG.XEPGCount)) println() - fmt.Println(fmt.Sprintf("IPv4 Addresses:")) + fmt.Println("IPv4 Addresses:") for i, ipv4 := range System.IPAddressesV4 { switch count := i; { case count < 10: - fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) - break + fmt.Printf(" %d. %s\n", count, ipv4) case count < 100: - fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) - break + fmt.Printf(" %d. %s\n", count, ipv4) } } println() - fmt.Println(fmt.Sprintf("IPv6 Addresses:")) + fmt.Println("IPv6 Addresses:") for i, ipv4 := range System.IPAddressesV6 { switch count := i; { case count < 10: - fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) - break + fmt.Printf(" %d. %s\n", count, ipv4) case count < 100: - fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) - break + fmt.Printf(" %d. %s\n", count, ipv4) } @@ -67,33 +63,33 @@ func ShowSystemInfo() { println("---") fmt.Println("Settings [General]") - fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID)) - fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner)) - fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource)) + fmt.Printf("UUID: %s\n", Settings.UUID) + fmt.Printf("Tuner (Plex / Emby): %d\n", Settings.Tuner) + fmt.Printf("EPG Source: %s\n", Settings.EpgSource) println("---") fmt.Println("Settings [Files]") - fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ","))) - fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate)) - fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath)) - fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages)) - fmt.Println(fmt.Sprintf("Missing EPG Mode: %s", Settings.XepgMissingEPGMode)) - fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages)) + fmt.Printf("Schedule: %s\n", strings.Join(Settings.Update, ",")) + fmt.Printf("Files Update: %t\n", Settings.FilesUpdate) + fmt.Printf("Folder (tmp): %s\n", Settings.TempPath) + fmt.Printf("Image Chaching: %t\n", Settings.CacheImages) + fmt.Printf("Missing EPG Mode: %s\n", Settings.XepgMissingEPGMode) + fmt.Printf("Replace EPG Image: %t\n", Settings.XepgReplaceMissingImages) println("---") fmt.Println("Settings [Streaming]") - fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer)) - fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy)) - fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize)) - fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout))) - fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent)) + fmt.Printf("Buffer: %s\n", Settings.Buffer) + fmt.Printf("UDPxy: %s\n", Settings.UDPxy) + fmt.Printf("Buffer Size: %d KB\n", Settings.BufferSize) + fmt.Printf("Timeout: %d ms\n", int(Settings.BufferTimeout)) + fmt.Printf("User Agent: %s\n", Settings.UserAgent) println("---") fmt.Println("Settings [Backup]") - fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath)) - fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep)) + fmt.Printf("Folder (backup): %s\n", Settings.BackupPath) + fmt.Printf("Backup Keep: %d\n", Settings.BackupKeep) } diff --git a/src/internal/authentication/authentication.go b/src/internal/authentication/authentication.go index a908394..0fc0870 100755 --- a/src/internal/authentication/authentication.go +++ b/src/internal/authentication/authentication.go @@ -3,7 +3,6 @@ package authentication import ( "encoding/json" "errors" - "io/ioutil" "net/http" "os" "path/filepath" @@ -44,90 +43,6 @@ type Cookie struct { RawExpires string } -// Framework examples - -/* -func main() { - var err error - - var checkErr = func(err error) { - log.Println(err) - os.Exit(0) - } - - err = Init("", 10) // Path to save the data, Validity of tokens in minutes | (error) - if err != nil { - checkErr(err) - } - - - err = CreateDefaultUser("admin", "123") - if err != nil { - checkErr(err) - } - - - - - err = CreateNewUser("xteve", "xteve") // Username, Password | (error) - if err != nil { - checkErr(err) - } - - - - err, token := UserAuthentication("xteve", "xteve") // Username, Password | (error, token) - if err != nil { - checkErr(err) - } else { - fmt.Println("UserAuthentication()") - fmt.Println("Token:", token) - fmt.Println("---") - } - - err, newToken := CheckTheValidityOfTheToken(token) // Current token | (error, new token) - if err != nil { - checkErr(err) - } else { - fmt.Println("CheckTheValidityOfTheToken()") - fmt.Println("New Token:", newToken) - fmt.Println("---") - } - - err, userID := GetUserID(newToken) // Current token | (error, user id) - if err != nil { - checkErr(err) - } else { - fmt.Println("GetUserID()") - fmt.Println("User ID:", userID) - fmt.Println("---") - } - - - var userData = make(map[string]interface{}) - userData["type"] = "Administrator" - err = WriteUserData(userID, userData) // User id, user data | (error) - if err != nil { - checkErr(err) - } - - err, userData = ReadUserData(userID) // User id | (error, userData) - if err != nil { - checkErr(err) - } else { - fmt.Println("ReadUserData()") - fmt.Println("User data:", userData) - fmt.Println("---") - } - - err = RemoveUser(userID) - if err != nil { - checkErr(err) - } - -} -*/ - // Init : databasePath = Path to authentication.json func Init(databasePath string, validity int) (err error) { database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile @@ -457,7 +372,7 @@ func CheckTheValidityOfTheTokenFromHTTPHeader(w http.ResponseWriter, r *http.Req // Framework tools func checkInit() (err error) { - if initAuthentication == false { + if !initAuthentication { err = createError(000) } @@ -472,7 +387,7 @@ func saveDatabase(tmpMap any) (err error) { return } - err = ioutil.WriteFile(database, []byte(jsonString), 0600) + err = os.WriteFile(database, []byte(jsonString), 0600) if err != nil { return } @@ -481,7 +396,7 @@ func saveDatabase(tmpMap any) (err error) { } func loadDatabase() (err error) { - jsonString, err := ioutil.ReadFile(database) + jsonString, err := os.ReadFile(database) if err != nil { return } @@ -587,14 +502,6 @@ loopToken: return } -func mapToJSON(tmpMap any) string { - jsonString, err := json.MarshalIndent(tmpMap, "", " ") - if err != nil { - return "{}" - } - return string(jsonString) -} - // SetCookieToken : set cookie func SetCookieToken(w http.ResponseWriter, token string) http.ResponseWriter { expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity)) diff --git a/src/internal/imgcache/cache.go b/src/internal/imgcache/cache.go index 0337dcc..94a5451 100644 --- a/src/internal/imgcache/cache.go +++ b/src/internal/imgcache/cache.go @@ -1,178 +1,177 @@ package imgcache import ( - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "sync" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" ) // Cache : Cache strcut type Cache struct { - path string - cacheURL string - caching bool - images map[string]string - Queue []string - Cache []string - Image imageFunc - sync.RWMutex + path string + cacheURL string + caching bool + images map[string]string + Queue []string + Cache []string + Image imageFunc + sync.RWMutex } type imageFunc struct { - GetURL func(string) string - Caching func() - Remove func() + GetURL func(string) string + Caching func() + Remove func() } // New : New cahce func New(path, chacheURL string, caching bool) (c *Cache, err error) { - c = &Cache{} + c = &Cache{} - c.images = make(map[string]string) - c.path = path - c.cacheURL = chacheURL - c.caching = caching - c.Queue = []string{} - c.Cache = []string{} + c.images = make(map[string]string) + c.path = path + c.cacheURL = chacheURL + c.caching = caching + c.Queue = []string{} + c.Cache = []string{} - var queue []string + var queue []string - c.Image.GetURL = func(src string) (cacheURL string) { + c.Image.GetURL = func(src string) (cacheURL string) { - c.Lock() - defer c.Unlock() + c.Lock() + defer c.Unlock() - src = strings.Trim(src, "\r\n") + src = strings.Trim(src, "\r\n") - if c.caching == false { - return src - } + if !c.caching { + return src + } - u, err := url.Parse(src) - if err != nil || len(filepath.Ext(u.Path)) == 0 { - return src - } + u, err := url.Parse(src) + if err != nil || len(filepath.Ext(u.Path)) == 0 { + return src + } - var filename = fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path)) - if cacheURL, ok := c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))]; ok { - return cacheURL - } + var filename = fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path)) + if cacheURL, ok := c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))]; ok { + return cacheURL + } - if indexOfString(filename, c.Cache) == -1 { + if indexOfString(filename, c.Cache) == -1 { - if indexOfString(src, c.Queue) == -1 { - c.Queue = append(c.Queue, src) - } + if indexOfString(src, c.Queue) == -1 { + c.Queue = append(c.Queue, src) + } - } else { - c.images[filename] = c.cacheURL + filename - src = c.cacheURL + filename - } + } else { + c.images[filename] = c.cacheURL + filename + src = c.cacheURL + filename + } - /* - if _, err := os.Stat(c.path + filename); err != nil { - //c.images[filename] = c.cacheURL + filename - if indexOfString(src, c.Queue) == -1 { - c.Queue = append(c.Queue, src) - } - } else { - c.images[filename] = c.cacheURL + filename - } - */ + /* + if _, err := os.Stat(c.path + filename); err != nil { + //c.images[filename] = c.cacheURL + filename + if indexOfString(src, c.Queue) == -1 { + c.Queue = append(c.Queue, src) + } + } else { + c.images[filename] = c.cacheURL + filename + } + */ - return src - } + return src + } - c.Image.Caching = func() { + c.Image.Caching = func() { - c.Lock() - defer c.Unlock() + c.Lock() + defer c.Unlock() - var filename string + var filename string - for _, src := range c.Queue { + for _, src := range c.Queue { - resp, err := http.Get(src) - if err != nil { - continue - } - defer resp.Body.Close() + resp, err := http.Get(src) + if err != nil { + continue + } + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - continue - } + if resp.StatusCode != http.StatusOK { + continue + } - filename = fmt.Sprintf("%s%s%s%s", c.path, string(os.PathSeparator), strToMD5(src), filepath.Ext(src)) + filename = fmt.Sprintf("%s%s%s%s", c.path, string(os.PathSeparator), strToMD5(src), filepath.Ext(src)) - file, err := os.Create(filename) - if err != nil { - continue - } + file, err := os.Create(filename) + if err != nil { + continue + } - defer file.Close() + defer file.Close() - _, err = io.Copy(file, resp.Body) - if err != nil { - continue - } + _, err = io.Copy(file, resp.Body) + if err != nil { + continue + } - u, err := url.Parse(src) - if err == nil { - c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))] = c.cacheURL + filename - } + u, err := url.Parse(src) + if err == nil { + c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))] = c.cacheURL + filename + } - queue = append(queue, src) + queue = append(queue, src) - } + } - for _, q := range queue { - c.Queue = removeStringFromSlice(q, c.Queue) - } + for _, q := range queue { + c.Queue = removeStringFromSlice(q, c.Queue) + } - } + } - c.Image.Remove = func() { + c.Image.Remove = func() { - c.Lock() - defer c.Unlock() + c.Lock() + defer c.Unlock() - files, err := ioutil.ReadDir(c.path) - if err != nil { - return - } + files, err := os.ReadDir(c.path) + if err != nil { + return + } - for _, file := range files { + for _, file := range files { - switch c.caching { + switch c.caching { - case true: - if _, ok := c.images[file.Name()]; !ok { - os.RemoveAll(c.path + file.Name()) - } + case true: + if _, ok := c.images[file.Name()]; !ok { + os.RemoveAll(c.path + file.Name()) + } - case false: - os.RemoveAll(c.path + file.Name()) - } + case false: + os.RemoveAll(c.path + file.Name()) + } - } + } - } + } - files, err := ioutil.ReadDir(c.path) - if err != nil { - return - } + files, err := os.ReadDir(c.path) + if err != nil { + return + } - for _, file := range files { - c.Cache = append(c.Cache, file.Name()) - } + for _, file := range files { + c.Cache = append(c.Cache, file.Name()) + } - return + return } diff --git a/src/internal/imgcache/tools.go b/src/internal/imgcache/tools.go index 9d4b622..f30e277 100644 --- a/src/internal/imgcache/tools.go +++ b/src/internal/imgcache/tools.go @@ -1,34 +1,34 @@ package imgcache import ( - "crypto/md5" - "encoding/hex" + "crypto/md5" + "encoding/hex" ) func strToMD5(str string) string { - md5Hasher := md5.New() - md5Hasher.Write([]byte(str)) - return hex.EncodeToString(md5Hasher.Sum(nil)) + md5Hasher := md5.New() + md5Hasher.Write([]byte(str)) + return hex.EncodeToString(md5Hasher.Sum(nil)) } func indexOfString(str string, slice []string) int { - for i, v := range slice { - if str == v { - return i - } - } + for i, v := range slice { + if str == v { + return i + } + } - return -1 + return -1 } func removeStringFromSlice(str string, slice []string) []string { - var i = indexOfString(str, slice) + var i = indexOfString(str, slice) - if i != -1 { - slice = append(slice[:i], slice[i+1:]...) - } + if i != -1 { + slice = append(slice[:i], slice[i+1:]...) + } - return slice + return slice } diff --git a/src/internal/m3u-parser/m3u-parser_test.go b/src/internal/m3u-parser/m3u-parser_test.go index c59cba7..38c352d 100644 --- a/src/internal/m3u-parser/m3u-parser_test.go +++ b/src/internal/m3u-parser/m3u-parser_test.go @@ -3,18 +3,18 @@ package m3u import ( "encoding/json" "fmt" - "io/ioutil" "log" + "os" "testing" ) type M3UStream struct { - GroupTitle string `json:"group-title,required"` - Name string `json:"name,required"` - TvgID string `json:"tvg-id,required"` - TvgLogo string `json:"tvg-logo,required"` - TvgName string `json:"tvg-name,required"` - URL string `json:"url,required"` + GroupTitle string `json:"group-title"` + Name string `json:"name"` + TvgID string `json:"tvg-id"` + TvgLogo string `json:"tvg-logo"` + TvgName string `json:"tvg-name"` + URL string `json:"url"` UUIDKey string `json:"_uuid.key,omitempty"` UUIDValue string `json:"_uuid.value,omitempty"` } @@ -22,7 +22,7 @@ type M3UStream struct { func TestStream1(t *testing.T) { var file = "test_list_1.m3u" - var content, err = ioutil.ReadFile(file) + var content, err = os.ReadFile(file) if err != nil { t.Error(err) return @@ -58,26 +58,26 @@ func checkStream(streamInterface []any) (err error) { err = json.Unmarshal(jsonString, &m3uStream) if err == nil { - log.Print(fmt.Sprintf("Stream: %d", i)) - log.Print(fmt.Sprintf("Name*: %s", m3uStream.Name)) - log.Print(fmt.Sprintf("URL*: %s", m3uStream.URL)) - log.Print(fmt.Sprintf("tvg-name: %s", m3uStream.TvgName)) - log.Print(fmt.Sprintf("tvg-id**: %s", m3uStream.TvgID)) - log.Print(fmt.Sprintf("tvg-logo: %s", m3uStream.TvgLogo)) - log.Print(fmt.Sprintf("group-title**: %s", m3uStream.GroupTitle)) + log.Printf("Stream: %d", i) + log.Printf("Name*: %s", m3uStream.Name) + log.Printf("URL*: %s", m3uStream.URL) + log.Printf("tvg-name: %s", m3uStream.TvgName) + log.Printf("tvg-id**: %s", m3uStream.TvgID) + log.Printf("tvg-logo: %s", m3uStream.TvgLogo) + log.Printf("group-title**: %s", m3uStream.GroupTitle) if len(m3uStream.UUIDKey) > 0 { - log.Print(fmt.Sprintf("UUID key***: %s", m3uStream.UUIDKey)) - log.Print(fmt.Sprintf("UUID value: %s", m3uStream.UUIDValue)) + log.Printf("UUID key***: %s", m3uStream.UUIDKey) + log.Printf("UUID value: %s", m3uStream.UUIDValue) } else { - log.Print(fmt.Sprintf("UUID key: false")) + log.Print("UUID key: false") } } } - log.Println(fmt.Sprintf("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -")) + log.Println("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -") } return diff --git a/src/internal/m3u-parser/xteve_m3uParser.go b/src/internal/m3u-parser/xteve_m3uParser.go index d8dc05f..c7aea4d 100755 --- a/src/internal/m3u-parser/xteve_m3uParser.go +++ b/src/internal/m3u-parser/xteve_m3uParser.go @@ -2,7 +2,6 @@ package m3u import ( "errors" - "fmt" "log" "net/url" "regexp" @@ -124,7 +123,7 @@ func MakeInterfaceFromM3U(byteStream []byte) (allChannels []any, err error) { if strings.Contains(strings.ToLower(key), "id") { if indexOfString(value, uuids) != -1 { - log.Println(fmt.Sprintf("Channel: %s - %s = %s ", stream["name"], key, value)) + log.Printf("Channel: %s - %s = %s ", stream["name"], key, value) break } diff --git a/src/m3u.go b/src/m3u.go index 3ae48ab..8d5d81c 100644 --- a/src/m3u.go +++ b/src/m3u.go @@ -114,18 +114,18 @@ func filterThisStream(s any) (status bool) { } } - if match == true { + if match { if len(exclude) > 0 { var status = checkConditions(search, exclude, "exclude") - if status == false { + if !status { return false } } if len(include) > 0 { var status = checkConditions(search, include, "include") - if status == false { + if !status { return false } } @@ -191,7 +191,7 @@ func buildM3U(groups []string) (m3u string, err error) { err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) if err == nil { - if xepgChannel.XActive == true { + if xepgChannel.XActive { if len(groups) > 0 { diff --git a/src/maintenance.go b/src/maintenance.go index b915205..5c86314 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -1,15 +1,12 @@ package src import ( - "math/rand" "time" ) // InitMaintenance : Wartungsprozess initialisieren func InitMaintenance() (err error) { - rand.Seed(time.Now().Unix()) - go maintenance() return @@ -50,7 +47,7 @@ func maintenance() { ShowError(err, 000) } - if Settings.CacheImages == false && System.ImageCachingInProgress == 0 { + if !Settings.CacheImages && System.ImageCachingInProgress == 0 { removeChildItems(System.Folder.ImagesCache) } @@ -68,8 +65,3 @@ func maintenance() { } } - -func randomTime(min, max int) int { - rand.Seed(time.Now().Unix()) - return rand.Intn(max-min) + min -} diff --git a/src/migrate.go b/src/migrate.go index a1e7c47..4124e4d 100644 --- a/src/migrate.go +++ b/src/migrate.go @@ -131,6 +131,9 @@ func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) { func setValueForUUID() (err error) { xepg, err := loadJSONFileToMap(System.File.XEPG) + if err != nil { + return + } for _, c := range xepg { diff --git a/src/plex_api.go b/src/plex_api.go index 7b80901..e1169f7 100644 --- a/src/plex_api.go +++ b/src/plex_api.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strconv" @@ -26,7 +26,7 @@ var plexRefreshState = struct { // queuePlexGuideRefresh schedules a debounced Plex DVR guide refresh. func queuePlexGuideRefresh(reason string) { - if Settings.UsePlexAPI == false { + if !Settings.UsePlexAPI { return } @@ -36,7 +36,7 @@ func queuePlexGuideRefresh(reason string) { plexRefreshState.reason = reason } - if plexRefreshState.scheduled == true || plexRefreshState.inProgress == true { + if plexRefreshState.scheduled || plexRefreshState.inProgress { plexRefreshState.queued = true plexRefreshState.Unlock() return @@ -78,13 +78,13 @@ func runPlexGuideRefresh(initialDelay time.Duration) { plexRefreshState.inProgress = false nextDelay := time.Duration(0) - if runAgain == true && Settings.UsePlexAPI == true { + if runAgain && Settings.UsePlexAPI { plexRefreshState.scheduled = true nextDelay = plexRefreshDelayLocked() } plexRefreshState.Unlock() - if runAgain == false || Settings.UsePlexAPI == false { + if !runAgain || !Settings.UsePlexAPI { return } @@ -98,7 +98,7 @@ func plexRefreshDelayLocked() (delay time.Duration) { const minDelay = 2 * time.Second const cooldown = 20 * time.Second - if plexRefreshState.lastRun.IsZero() == true { + if plexRefreshState.lastRun.IsZero() { return minDelay } @@ -113,7 +113,7 @@ func plexRefreshDelayLocked() (delay time.Duration) { func refreshPlexGuide(reason string) (err error) { baseURL, token, ready := getPlexConfig() - if ready == false { + if !ready { return nil } @@ -178,7 +178,7 @@ func getPlexConfig() (baseURL, token string, ready bool) { baseURL = strings.TrimSpace(Settings.PlexURL) token = strings.TrimSpace(Settings.PlexToken) - if strings.HasPrefix(baseURL, "http://") == false && strings.HasPrefix(baseURL, "https://") == false && len(baseURL) > 0 { + if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") && len(baseURL) > 0 { baseURL = "http://" + baseURL } @@ -189,7 +189,7 @@ func getPlexConfig() (baseURL, token string, ready bool) { if len(baseURL) == 0 || len(token) == 0 { - if plexRefreshState.missingConfigLogged == false { + if !plexRefreshState.missingConfigLogged { showInfo("Plex API:Skipped refresh because plex.url or plex.token is empty") plexRefreshState.missingConfigLogged = true } @@ -221,14 +221,14 @@ func discoverPlexDVRs(baseURL, token string) (dvrs []map[string]any, err error) } mediaContainer, ok := payload["MediaContainer"].(map[string]any) - if ok == false { + if !ok { err = errors.New("Plex API response missing MediaContainer") return } for _, key := range []string{"Dvr", "DVR", "Directory", "Metadata"} { - if raw, found := mediaContainer[key]; found == true { - if list, ok := raw.([]any); ok == true { + if raw, found := mediaContainer[key]; found { + if list, ok := raw.([]any); ok { dvrs = convertToPlexMapSlice(list) if len(dvrs) > 0 { return @@ -246,7 +246,7 @@ func convertToPlexMapSlice(list []any) (dvrs []map[string]any) { dvrs = make([]map[string]any, 0, len(list)) for _, item := range list { - if dvr, ok := item.(map[string]any); ok == true { + if dvr, ok := item.(map[string]any); ok { dvrs = append(dvrs, dvr) } } @@ -264,10 +264,10 @@ func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) { if len(endpoint) == 0 { return } - if strings.HasPrefix(endpoint, "/") == false { + if !strings.HasPrefix(endpoint, "/") { endpoint = "/" + endpoint } - if added[endpoint] == true { + if added[endpoint] { return } added[endpoint] = true @@ -275,7 +275,7 @@ func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) { } if key := getPlexMapString(dvr, "key"); len(key) > 0 { - if strings.HasPrefix(key, "/") == true { + if strings.HasPrefix(key, "/") { add(strings.TrimRight(key, "/") + "/reloadGuide") } else { add("/livetv/dvrs/" + url.PathEscape(key) + "/reloadGuide") @@ -294,7 +294,7 @@ func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) { func getPlexMapString(data map[string]any, field string) string { for key, value := range data { - if strings.EqualFold(key, field) == false { + if !strings.EqualFold(key, field) { continue } @@ -354,7 +354,7 @@ func doPlexRequest(method, baseURL, endpoint, token string) (status int, body [] defer resp.Body.Close() status = resp.StatusCode - body, err = ioutil.ReadAll(resp.Body) + body, err = io.ReadAll(resp.Body) if err != nil { return } diff --git a/src/provider.go b/src/provider.go index d985cf1..d7cf508 100644 --- a/src/provider.go +++ b/src/provider.go @@ -2,7 +2,7 @@ package src import ( "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -152,7 +152,7 @@ func getProviderData(fileType, fileID string) (err error) { } // Wenn eine ID vorhanden ist und nicht mit der aus der Datanbank übereinstimmt, wird die Aktualisierung übersprungen (goto) - if len(fileID) > 0 && newProvider == false { + if len(fileID) > 0 && !newProvider { if dataID != fileID { goto Done } @@ -204,7 +204,7 @@ func getProviderData(fileType, fileID string) (err error) { ShowError(err, 000) var downloadErr = err - if newProvider == false { + if !newProvider { // Prüfen ob ältere Datei vorhanden ist var file = System.Folder.Data + dataID + fileExtension @@ -236,7 +236,7 @@ func getProviderData(fileType, fileID string) (err error) { } // Berechnen der Fehlerquote - if newProvider == false { + if !newProvider { if value, ok := dataMap[dataID].(map[string]any); ok { @@ -322,7 +322,7 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e } - body, err = ioutil.ReadAll(resp.Body) + body, err = io.ReadAll(resp.Body) if err != nil { return } diff --git a/src/screen.go b/src/screen.go index 64bf5ce..55c6ec7 100644 --- a/src/screen.go +++ b/src/screen.go @@ -12,7 +12,7 @@ import ( func showInfo(str string) { - if System.Flag.Info == true { + if System.Flag.Info { return } @@ -39,7 +39,6 @@ func showInfo(str string) { } - return } func showDebug(str string, level int) { @@ -73,7 +72,6 @@ func showDebug(str string, level int) { } - return } func showHighlight(str string) { @@ -105,7 +103,6 @@ func showHighlight(str string) { addNotification(notification) - return } func showWarning(errCode int) { @@ -121,7 +118,6 @@ func showWarning(errCode int) { WebScreenLog.Warnings++ mutex.Unlock() - return } // ShowError : Zeigt die Fehlermeldungen in der Konsole @@ -139,7 +135,6 @@ func ShowError(err error, errCode int) { WebScreenLog.Errors++ mutex.Unlock() - return } func printLogOnScreen(logMsg string, logType string) { @@ -211,7 +206,6 @@ func logCleanUp() { WebScreenLog.Log = logs - return } // Fehlercodes @@ -224,79 +218,79 @@ func getErrMsg(errCode int) (errMsg string) { // Errors case 1001: - errMsg = fmt.Sprintf("Web server could not be started.") + errMsg = "Web server could not be started." case 1002: - errMsg = fmt.Sprintf("No local IP address found.") + errMsg = "No local IP address found." case 1003: - errMsg = fmt.Sprintf("Invalid xml") + errMsg = "Invalid xml" case 1004: - errMsg = fmt.Sprintf("File not found") + errMsg = "File not found" case 1005: - errMsg = fmt.Sprintf("Invalid M3U file, an extended M3U file is required.") + errMsg = "Invalid M3U file, an extended M3U file is required." case 1006: - errMsg = fmt.Sprintf("No playlist!") + errMsg = "No playlist!" case 1007: - errMsg = fmt.Sprintf("XEPG requires an XMLTV file.") + errMsg = "XEPG requires an XMLTV file." case 1010: - errMsg = fmt.Sprintf("Invalid file compression") + errMsg = "Invalid file compression" case 1011: errMsg = fmt.Sprintf("Data is corrupt or unavailable, %s now uses an older version of this file", System.Name) case 1012: - errMsg = fmt.Sprintf("Invalid formatting of the time") + errMsg = "Invalid formatting of the time" case 1013: errMsg = fmt.Sprintf("Invalid settings file (settings.json), file must be at least version %s", System.Compatibility) case 1014: - errMsg = fmt.Sprintf("Invalid filter rule") + errMsg = "Invalid filter rule" case 1020: - errMsg = fmt.Sprintf("Data could not be saved, invalid keyword") + errMsg = "Data could not be saved, invalid keyword" // Datenbank Update case 1030: errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings) case 1031: - errMsg = fmt.Sprintf("Database error. The database version of your settings is not compatible with this version.") + errMsg = "Database error. The database version of your settings is not compatible with this version." // M3U Parser case 1050: - errMsg = fmt.Sprintf("Invalid duration specification in the M3U8 playlist.") + errMsg = "Invalid duration specification in the M3U8 playlist." case 1060: - errMsg = fmt.Sprintf("Invalid characters found in the tvg parameters, streams with invalid parameters were skipped.") + errMsg = "Invalid characters found in the tvg parameters, streams with invalid parameters were skipped." // Dateisystem case 1070: - errMsg = fmt.Sprintf("Folder could not be created.") + errMsg = "Folder could not be created." case 1071: - errMsg = fmt.Sprintf("File could not be created") + errMsg = "File could not be created" case 1072: - errMsg = fmt.Sprintf("File not found") + errMsg = "File not found" // Backup case 1090: - errMsg = fmt.Sprintf("Automatic backup failed") + errMsg = "Automatic backup failed" // Websockets case 1100: - errMsg = fmt.Sprintf("WebUI build error") + errMsg = "WebUI build error" case 1101: - errMsg = fmt.Sprintf("WebUI request error") + errMsg = "WebUI request error" case 1102: - errMsg = fmt.Sprintf("WebUI response error") + errMsg = "WebUI response error" // PMS Guide Numbers case 1200: - errMsg = fmt.Sprintf("Could not create file") + errMsg = "Could not create file" // Stream URL Fehler case 1201: - errMsg = fmt.Sprintf("Plex stream error") + errMsg = "Plex stream error" case 1202: - errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist") + errMsg = "Steaming URL could not be found in any playlist" case 1203: - errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist") + errMsg = "Steaming URL could not be found in any playlist" case 1204: - errMsg = fmt.Sprintf("Streaming was stopped by third party transcoder (FFmpeg / VLC)") + errMsg = "Streaming was stopped by third party transcoder (FFmpeg / VLC)" // Warnings case 2000: @@ -304,86 +298,86 @@ func getErrMsg(errCode int) (errMsg string) { case 2001: errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.UnfilteredChannelLimit) case 2002: - errMsg = fmt.Sprintf("PMS can not play m3u8 streams") + errMsg = "PMS can not play m3u8 streams" case 2003: - errMsg = fmt.Sprintf("PMS can not play streams over RTSP.") + errMsg = "PMS can not play streams over RTSP." case 2004: - errMsg = fmt.Sprintf("Buffer is disabled for this stream.") + errMsg = "Buffer is disabled for this stream." case 2005: - errMsg = fmt.Sprintf("There are no channels mapped, use the mapping menu to assign EPG data to the channels.") + errMsg = "There are no channels mapped, use the mapping menu to assign EPG data to the channels." case 2010: - errMsg = fmt.Sprintf("No valid streaming URL") + errMsg = "No valid streaming URL" case 2020: - errMsg = fmt.Sprintf("FFmpeg binary was not found. Check the FFmpeg binary path in the xTeVe settings.") + errMsg = "FFmpeg binary was not found. Check the FFmpeg binary path in the xTeVe settings." case 2021: - errMsg = fmt.Sprintf("VLC binary was not found. Check the VLC path binary in the xTeVe settings.") + errMsg = "VLC binary was not found. Check the VLC path binary in the xTeVe settings." case 2099: - errMsg = fmt.Sprintf("Updates have been disabled by the developer") + errMsg = "Updates have been disabled by the developer" // Tuner case 2105: errMsg = "The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again." case 2106: - errMsg = fmt.Sprintf("This function is only available with XEPG as EPG source") + errMsg = "This function is only available with XEPG as EPG source" case 2110: - errMsg = fmt.Sprintf("Don't run this as Root!") + errMsg = "Don't run this as Root!" case 2300: - errMsg = fmt.Sprintf("No channel logo found in the XMLTV or M3U file.") + errMsg = "No channel logo found in the XMLTV or M3U file." case 2301: - errMsg = fmt.Sprintf("XMLTV file no longer available, channel has been deactivated.") + errMsg = "XMLTV file no longer available, channel has been deactivated." case 2302: - errMsg = fmt.Sprintf("Channel ID in the XMLTV file has changed. Channel has been deactivated.") + errMsg = "Channel ID in the XMLTV file has changed. Channel has been deactivated." // Benutzerauthentifizierung case 3000: - errMsg = fmt.Sprintf("Database for user authentication could not be initialized.") + errMsg = "Database for user authentication could not be initialized." case 3001: - errMsg = fmt.Sprintf("The user has no authorization to load the channels.") + errMsg = "The user has no authorization to load the channels." // Buffer case 4000: - errMsg = fmt.Sprintf("Connection to streaming source was interrupted.") + errMsg = "Connection to streaming source was interrupted." case 4001: - errMsg = fmt.Sprintf("Too many errors connecting to the provider. Streaming is canceled.") + errMsg = "Too many errors connecting to the provider. Streaming is canceled." case 4002: - errMsg = fmt.Sprintf("New URL for the redirect to the streaming server is missing") + errMsg = "New URL for the redirect to the streaming server is missing" case 4003: - errMsg = fmt.Sprintf("Server sends an incompatible content-type") + errMsg = "Server sends an incompatible content-type" case 4004: - errMsg = fmt.Sprintf("This error message comes from the provider") + errMsg = "This error message comes from the provider" case 4005: - errMsg = fmt.Sprintf("Temporary buffer files could not be deleted") + errMsg = "Temporary buffer files could not be deleted" case 4006: - errMsg = fmt.Sprintf("Server connection timeout") + errMsg = "Server connection timeout" // Buffer (M3U8) case 4050: - errMsg = fmt.Sprintf("Invalid M3U8 file") + errMsg = "Invalid M3U8 file" case 4051: - errMsg = fmt.Sprintf("#EXTM3U header is missing") + errMsg = "#EXTM3U header is missing" // Caching case 4100: - errMsg = fmt.Sprintf("Unknown content type for downloaded image") + errMsg = "Unknown content type for downloaded image" case 4101: - errMsg = fmt.Sprintf("Invalid URL, original URL is used for this image") + errMsg = "Invalid URL, original URL is used for this image" // API case 5000: - errMsg = fmt.Sprintf("Invalid API command") + errMsg = "Invalid API command" // Update Server case 6001: - errMsg = fmt.Sprintf("Ivalid key") + errMsg = "Ivalid key" case 6002: - errMsg = fmt.Sprintf("Update failed") + errMsg = "Update failed" case 6003: - errMsg = fmt.Sprintf("Update server not available") + errMsg = "Update server not available" case 6004: - errMsg = fmt.Sprintf("xTeVe update available") + errMsg = "xTeVe update available" default: errMsg = fmt.Sprintf("Unknown error / warning (%d)", errCode) diff --git a/src/ssdp.go b/src/ssdp.go index 63ce77c..85e38f2 100644 --- a/src/ssdp.go +++ b/src/ssdp.go @@ -1,72 +1,72 @@ package src import ( - "fmt" - "log" - "os" - "os/signal" - "time" + "fmt" + "log" + "os" + "os/signal" + "time" - "github.com/koron/go-ssdp" + "github.com/koron/go-ssdp" ) // SSDP : SSPD / DLNA Server func SSDP() (err error) { - if Settings.SSDP == false || System.Flag.Info == true { - return - } + if !Settings.SSDP || System.Flag.Info { + return + } - showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP)) + showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP)) - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) - ad, err := ssdp.Advertise( - fmt.Sprintf("upnp:rootdevice"), // send as "ST" - fmt.Sprintf("uuid:%s::upnp:rootdevice", System.DeviceID), // send as "USN" - fmt.Sprintf("%s/device.xml", System.URLBase), // send as "LOCATION" - System.AppName, // send as "SERVER" - 1800) // send as "maxAge" in "CACHE-CONTROL" + ad, err := ssdp.Advertise( + "upnp:rootdevice", // send as "ST" + fmt.Sprintf("uuid:%s::upnp:rootdevice", System.DeviceID), // send as "USN" + fmt.Sprintf("%s/device.xml", System.URLBase), // send as "LOCATION" + System.AppName, // send as "SERVER" + 1800) // send as "maxAge" in "CACHE-CONTROL" - if err != nil { - return - } + if err != nil { + return + } - // Debug SSDP - if System.Flag.Debug == 3 { - ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags) - } + // Debug SSDP + if System.Flag.Debug == 3 { + ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags) + } - go func(adv *ssdp.Advertiser) { + go func(adv *ssdp.Advertiser) { - aliveTick := time.Tick(300 * time.Second) + aliveTick := time.Tick(300 * time.Second) - loop: - for { + loop: + for { - select { + select { - case <-aliveTick: - err = adv.Alive() - if err != nil { - ShowError(err, 0) - adv.Bye() - adv.Close() - break loop - } + case <-aliveTick: + err = adv.Alive() + if err != nil { + ShowError(err, 0) + adv.Bye() + adv.Close() + break loop + } - case <-quit: - adv.Bye() - adv.Close() - os.Exit(0) - break loop + case <-quit: + adv.Bye() + adv.Close() + os.Exit(0) + break loop - } + } - } + } - }(ad) + }(ad) - return + return } diff --git a/src/struct-buffer.go b/src/struct-buffer.go index b56e2d5..68332d6 100644 --- a/src/struct-buffer.go +++ b/src/struct-buffer.go @@ -108,147 +108,3 @@ type BandwidthCalculation struct { Stop time.Time TimeDiff float64 } - -/* -var args = "-hide_banner -loglevel panic -re -i " + url + " -codec copy -f mpegts pipe:1" - //var args = "-re -i " + url + " -codec copy -f mpegts pipe:1" - cmd := exec.Command("/usr/local/bin/ffmpeg", strings.Split(args, " ")...) - - //run := exec.Command("/usr/local/bin/ffmpeg", "-hide_banner", "-loglevel", "panic", "-re", "-i", url, "-codec", "copy", "-f", "mpegts", "pipe:1") - //run := exec.Command("/usr/local/bin/ffmpeg", "-re", "-i", url, "-codec", "copy", "-f", "mpegts", "pipe:1") - - stderr, _ := cmd.StderrPipe() - cmd.Start() - - scanner := bufio.NewScanner(stderr) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - m := scanner.Text() - fmt.Println(m) - } - cmd.Wait() - - os.Exit(0) -*/ - -/* - -ffmpegOut, _ := run.StderrPipe() - //run.Start() - - scanner = bufio.NewScanner(ffmpegOut) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - m := scanner.Text() - fmt.Println(m) - } - - ffmpegOut, err = run.StdoutPipe() - if err != nil { - ShowError(err, 0) - return - } - - stderr, stderrErr := run.StderrPipe() - if stderrErr != nil { - fmt.Println(stderrErr) - } - - _ = stderr - - if startErr := run.Start(); startErr != nil { - fmt.Println(startErr) - - return - } - - n, err := ffmpegOut.Read(buffer) - _ = n - _ = stream - _ = fileSize - - if err != nil && err != io.EOF { - - ShowError(err, 0) - addErrorToStream(err) - return - - } - - defer bufferFile.Close() - - scanner = bufio.NewScanner(ffmpegOut) - - for scanner.Scan() { - //fmt.Printf("%s\n", scanner.Text()) - //fmt.Println(scanner) - thisLine := scanner.Text() - line := make([]byte, len(thisLine)) - - buffer = append(buffer, line...) - - fmt.Println(len(buffer)) - - if len(buffer) > tmpFileSize { - - if _, err := bufferFile.Write(buffer[:]); err != nil { - - ShowError(err, 0) - addErrorToStream(err) - run.Process.Kill() - return - - } - - buffer = make([]byte, 1024*Settings.BufferSize*2) - - debug = fmt.Sprintf("Buffer Status:Done (%s)", tmpFile) - showDebug(debug, 2) - - bufferFile.Close() - - stream.Status = true - playlist.Streams[streamID] = stream - BufferInformation.Store(playlistID, playlist) - - tmpSegment++ - - tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment) - - if clientConnection(stream) == false { - - bufferFile.Close() - run.Process.Kill() - - err = os.RemoveAll(stream.Folder) - if err != nil { - ShowError(err, 4005) - } - - return - } - - bufferFile, err = os.Create(tmpFile) - if err != nil { - addErrorToStream(err) - run.Process.Kill() - return - } - - fileSize = 0 - - if n == 0 { - bufferFile.Close() - run.Process.Kill() - break - } - - os.Exit(0) - - } - - - - } - -*/ diff --git a/src/struct-system.go b/src/struct-system.go index b384ced..61f1f1f 100644 --- a/src/struct-system.go +++ b/src/struct-system.go @@ -155,81 +155,81 @@ type Filter struct { // XEPGChannelStruct : XEPG Struktur type XEPGChannelStruct struct { - FileM3UID string `json:"_file.m3u.id,required"` - FileM3UName string `json:"_file.m3u.name,required"` - FileM3UPath string `json:"_file.m3u.path,required"` - GroupTitle string `json:"group-title,required"` - Name string `json:"name,required"` - TvgID string `json:"tvg-id,required"` - TvgLogo string `json:"tvg-logo,required"` - TvgName string `json:"tvg-name,required"` - URL string `json:"url,required"` - UUIDKey string `json:"_uuid.key,required"` + FileM3UID string `json:"_file.m3u.id"` + FileM3UName string `json:"_file.m3u.name"` + FileM3UPath string `json:"_file.m3u.path"` + GroupTitle string `json:"group-title"` + Name string `json:"name"` + TvgID string `json:"tvg-id"` + TvgLogo string `json:"tvg-logo"` + TvgName string `json:"tvg-name"` + URL string `json:"url"` + UUIDKey string `json:"_uuid.key"` UUIDValue string `json:"_uuid.value,omitempty"` - Values string `json:"_values,required"` - XActive bool `json:"x-active,required"` - XCategory string `json:"x-category,required"` - XChannelID string `json:"x-channelID,required"` - XEPG string `json:"x-epg,required"` - XGroupTitle string `json:"x-group-title,required"` - XMapping string `json:"x-mapping,required"` - XmltvFile string `json:"x-xmltv-file,required"` - XName string `json:"x-name,required"` - XUpdateChannelIcon bool `json:"x-update-channel-icon,required"` - XUpdateChannelName bool `json:"x-update-channel-name,required"` - XDescription string `json:"x-description,required"` + Values string `json:"_values"` + XActive bool `json:"x-active"` + XCategory string `json:"x-category"` + XChannelID string `json:"x-channelID"` + XEPG string `json:"x-epg"` + XGroupTitle string `json:"x-group-title"` + XMapping string `json:"x-mapping"` + XmltvFile string `json:"x-xmltv-file"` + XName string `json:"x-name"` + XUpdateChannelIcon bool `json:"x-update-channel-icon"` + XUpdateChannelName bool `json:"x-update-channel-name"` + XDescription string `json:"x-description"` } // M3UChannelStructXEPG : M3U Struktur für XEPG type M3UChannelStructXEPG struct { - FileM3UID string `json:"_file.m3u.id,required"` - FileM3UName string `json:"_file.m3u.name,required"` - FileM3UPath string `json:"_file.m3u.path,required"` - GroupTitle string `json:"group-title,required"` - Name string `json:"name,required"` - TvgID string `json:"tvg-id,required"` - TvgLogo string `json:"tvg-logo,required"` - TvgName string `json:"tvg-name,required"` - URL string `json:"url,required"` - UUIDKey string `json:"_uuid.key,required"` - UUIDValue string `json:"_uuid.value,required"` - Values string `json:"_values,required"` + FileM3UID string `json:"_file.m3u.id"` + FileM3UName string `json:"_file.m3u.name"` + FileM3UPath string `json:"_file.m3u.path"` + GroupTitle string `json:"group-title"` + Name string `json:"name"` + TvgID string `json:"tvg-id"` + TvgLogo string `json:"tvg-logo"` + TvgName string `json:"tvg-name"` + URL string `json:"url"` + UUIDKey string `json:"_uuid.key"` + UUIDValue string `json:"_uuid.value"` + Values string `json:"_values"` } // FilterStruct : Filter Struktur type FilterStruct struct { - Active bool `json:"active,required"` - CaseSensitive bool `json:"caseSensitive,required"` - Description string `json:"description,required"` - Exclude string `json:"exclude,required"` - Filter string `json:"filter,required"` - Include string `json:"include,required"` - Name string `json:"name,required"` + Active bool `json:"active"` + CaseSensitive bool `json:"caseSensitive"` + Description string `json:"description"` + Exclude string `json:"exclude"` + Filter string `json:"filter"` + Include string `json:"include"` + Name string `json:"name"` Rule string `json:"rule,omitempty"` - Type string `json:"type,required"` + Type string `json:"type"` } // StreamingURLS : Informationen zu allen streaming URL's type StreamingURLS struct { - Streams map[string]StreamInfo `json:"channels,required"` + Streams map[string]StreamInfo `json:"channels"` } // StreamInfo : Informationen zum Kanal für die streaming URL type StreamInfo struct { - ChannelNumber string `json:"channelNumber,required"` - Name string `json:"name,required"` - PlaylistID string `json:"playlistID,required"` - URL string `json:"url,required"` - URLid string `json:"urlID,required"` + ChannelNumber string `json:"channelNumber"` + Name string `json:"name"` + PlaylistID string `json:"playlistID"` + URL string `json:"url"` + URLid string `json:"urlID"` } // Notification : Notifikationen im Webinterface type Notification struct { - Headline string `json:"headline,required"` - Message string `json:"message,required"` - New bool `json:"new,required"` - Time string `json:"time,required"` - Type string `json:"type,required"` + Headline string `json:"headline"` + Message string `json:"message"` + New bool `json:"new"` + Time string `json:"time"` + Type string `json:"type"` } // SettingsStruct : Inhalt der settings.json @@ -283,4 +283,3 @@ type SettingsStruct struct { XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"` WizardCompleted bool `json:"wizard.completed"` } - diff --git a/src/struct-webserver.go b/src/struct-webserver.go index e0bde99..e6f7cb2 100644 --- a/src/struct-webserver.go +++ b/src/struct-webserver.go @@ -3,7 +3,7 @@ package src // RequestStruct : Anfragen über die Websocket Schnittstelle type RequestStruct struct { // Befehle an xTeVe - Cmd string `json:"cmd,required"` + Cmd string `json:"cmd"` // Benutzer DeleteUser bool `json:"deleteUser,omitempty"` @@ -79,46 +79,46 @@ type ResponseStruct struct { DVR string `json:"DVR"` EpgSource string `json:"epgSource"` Errors int `json:"errors"` - M3U string `json:"m3u-url,required"` + M3U string `json:"m3u-url"` OS string `json:"os"` Streams string `json:"streams"` UUID string `json:"uuid"` Version string `json:"version"` Warnings int `json:"warnings"` XEPGCount int64 `json:"xepg"` - XML string `json:"xepg-url,required"` + XML string `json:"xepg-url"` } `json:"clientInfo"` Data struct { Playlist struct { M3U struct { Groups struct { - Text []string `json:"text,required"` - Value []string `json:"value,required"` - } `json:"groups,required"` - } `json:"m3u,required"` - } `json:"playlist,required"` + Text []string `json:"text"` + Value []string `json:"value"` + } `json:"groups"` + } `json:"m3u"` + } `json:"playlist"` StreamPreviewUI struct { - Active []string `json:"activeStreams,required"` - Inactive []string `json:"inactiveStreams,required"` + Active []string `json:"activeStreams"` + Inactive []string `json:"inactiveStreams"` } - } `json:"data,required"` + } `json:"data"` Alert string `json:"alert,omitempty"` - ConfigurationWizard bool `json:"configurationWizard,required"` + ConfigurationWizard bool `json:"configurationWizard"` Error string `json:"err,omitempty"` - Log WebScreenLogStruct `json:"log,required"` + Log WebScreenLogStruct `json:"log"` LogoURL string `json:"logoURL,omitempty"` OpenLink string `json:"openLink,omitempty"` OpenMenu string `json:"openMenu,omitempty"` Reload bool `json:"reload,omitempty"` - Settings SettingsStruct `json:"settings,required"` - Status bool `json:"status,required"` + Settings SettingsStruct `json:"settings"` + Status bool `json:"status"` Token string `json:"token,omitempty"` Users map[string]any `json:"users,omitempty"` Wizard int `json:"wizard,omitempty"` - XEPG map[string]any `json:"xepg,required"` + XEPG map[string]any `json:"xepg"` Notification map[string]Notification `json:"notification,omitempty"` } @@ -135,7 +135,7 @@ type APIRequestStruct struct { type APIResponseStruct struct { EpgSource string `json:"epg.source,omitempty"` Error string `json:"err,omitempty"` - Status bool `json:"status,required"` + Status bool `json:"status"` StreamsActive int64 `json:"streams.active,omitempty"` StreamsAll int64 `json:"streams.all,omitempty"` StreamsXepg int64 `json:"streams.xepg,omitempty"` @@ -149,7 +149,7 @@ type APIResponseStruct struct { // WebScreenLogStruct : Logs werden im RAM gespeichert und für das Webinterface bereitgestellt type WebScreenLogStruct struct { - Errors int `json:"errors,required"` - Log []string `json:"log,required"` - Warnings int `json:"warnings,required"` + Errors int `json:"errors"` + Log []string `json:"log"` + Warnings int `json:"warnings"` } diff --git a/src/struct-xml.go b/src/struct-xml.go index c79f385..b10b3a0 100644 --- a/src/struct-xml.go +++ b/src/struct-xml.go @@ -66,7 +66,7 @@ type SubTitle struct { Value string `xml:",chardata"` } -//Desc : Programmbeschreibung +// Desc : Programmbeschreibung type Desc struct { Lang string `xml:"lang,attr"` Value string `xml:",chardata"` diff --git a/src/system.go b/src/system.go index 5d17a77..6db7c7b 100644 --- a/src/system.go +++ b/src/system.go @@ -13,7 +13,7 @@ import ( // Entwicklerinfos anzeigen func showDevInfo() { - if System.Dev == true { + if System.Dev { fmt.Print("\033[31m") fmt.Println("* * * * * D E V M O D E * * * * *") @@ -25,7 +25,6 @@ func showDevInfo() { } - return } // Alle Systemordner erstellen @@ -261,7 +260,7 @@ func saveSettings(settings SettingsStruct) (err error) { Settings = settings - if System.Dev == true { + if System.Dev { Settings.UUID = "2019-01-DEV-xTeVe!" } @@ -301,7 +300,6 @@ func setGlobalDomain(domain string) { System.Addresses.XML = getErrMsg(2106) } - return } // UUID generieren @@ -323,7 +321,6 @@ func setDeviceID() { System.DeviceID = fmt.Sprintf("%s:%d", id, Settings.Tuner) } - return } // Provider Streaming-URL zu xTeVe Streaming-URL konvertieren diff --git a/src/toolchain.go b/src/toolchain.go index de34794..baffa25 100644 --- a/src/toolchain.go +++ b/src/toolchain.go @@ -7,7 +7,7 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" + "io" "net" "os" "os/exec" @@ -59,11 +59,8 @@ func checkFile(filename string) (err error) { return err } - switch mode := fi.Mode(); { - case mode.IsDir(): + if fi.Mode().IsDir() { err = fmt.Errorf("%s: %s", file, getErrMsg(1072)) - case mode.IsRegular(): - break } return @@ -97,7 +94,7 @@ func checkFilePermission(dir string) (err error) { var filename = dir + "permission.test" - err = ioutil.WriteFile(filename, []byte(""), 0644) + err = os.WriteFile(filename, []byte(""), 0644) if err == nil { err = os.RemoveAll(filename) } @@ -125,12 +122,6 @@ func getFilenameFromPath(path string) (file string) { return filepath.Base(path) } -// Nicht mehr verwendete Systemdaten löschen -func removeOldSystemData() { - // Temporären Ordner löschen - os.RemoveAll(System.Folder.Temp) -} - // Sucht eine Datei im OS func searchFileInOS(file string) (path string) { @@ -197,14 +188,6 @@ func jsonToMap(content string) map[string]any { return (tmpMap) } -func jsonToMapInt64(content string) map[int64]any { - - var tmpMap = make(map[int64]any) - json.Unmarshal([]byte(content), &tmpMap) - - return (tmpMap) -} - func jsonToInterface(content string) (tmpMap any, err error) { err = json.Unmarshal([]byte(content), &tmpMap) @@ -221,7 +204,7 @@ func saveMapToJSONFile(file string, tmpMap any) error { return err } - err = ioutil.WriteFile(filename, []byte(jsonString), 0644) + err = os.WriteFile(filename, []byte(jsonString), 0644) if err != nil { return err } @@ -232,16 +215,17 @@ func saveMapToJSONFile(file string, tmpMap any) error { func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) { f, err := os.Open(getPlatformFile(file)) + if err != nil { + return + } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err == nil { err = json.Unmarshal([]byte(content), &tmpMap) } - f.Close() - return } @@ -249,10 +233,12 @@ func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) { func readByteFromFile(file string) (content []byte, err error) { f, err := os.Open(getPlatformFile(file)) + if err != nil { + return + } defer f.Close() - content, err = ioutil.ReadAll(f) - f.Close() + content, err = io.ReadAll(f) return } @@ -260,7 +246,7 @@ func readByteFromFile(file string) (content []byte, err error) { func writeByteToFile(file string, data []byte) (err error) { var filename = getPlatformFile(file) - err = ioutil.WriteFile(filename, data, 0644) + err = os.WriteFile(filename, data, 0644) return } @@ -275,7 +261,7 @@ func readStringFromFile(file string) (str string, err error) { return } - content, err = ioutil.ReadFile(filename) + content, err = os.ReadFile(filename) if err != nil { ShowError(err, 0) return @@ -395,17 +381,6 @@ func indexOfFloat64(element float64, data []float64) int { return -1 } -func indexOfInt(element int, data []int) int { - - for k, v := range data { - if element == v { - return (k) - } - } - - return -1 -} - func getMD5(str string) string { md5Hasher := md5.New() diff --git a/src/webserver.go b/src/webserver.go index f23c874..15a467b 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "os" "strconv" @@ -40,8 +40,6 @@ func StartWebserver() (err error) { http.HandleFunc("/data_images/", DataImages) http.HandleFunc("/healthz", Healthz) - //http.HandleFunc("/auto/", Auto) - showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port) var ips = len(System.IPAddressesV4) + len(System.IPAddressesV6) - 1 @@ -90,7 +88,7 @@ func Index(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") case "/lineup.json": - if Settings.AuthenticationPMS == true { + if Settings.AuthenticationPMS { _, err := basicAuth(r, "authentication.pms") if err != nil { @@ -123,7 +121,6 @@ func Index(w http.ResponseWriter, r *http.Request) { httpStatusError(w, r, 500) - return } // Stream : Web Server /stream/ @@ -151,12 +148,12 @@ func Stream(w http.ResponseWriter, r *http.Request) { showInfo(fmt.Sprintf("Buffer:false [%s]", Settings.Buffer)) case "xteve": - if strings.Index(streamInfo.URL, "rtsp://") != -1 || strings.Index(streamInfo.URL, "rtp://") != -1 { + if strings.Contains(streamInfo.URL, "rtsp://") || strings.Contains(streamInfo.URL, "rtp://") { err = errors.New("RTSP and RTP streams are not supported") ShowError(err, 2004) showInfo("Streaming URL:" + streamInfo.URL) - http.Redirect(w, r, streamInfo.URL, 302) + http.Redirect(w, r, streamInfo.URL, http.StatusFound) showInfo("Streaming Info:URL was passed to the client") return @@ -181,7 +178,7 @@ func Stream(w http.ResponseWriter, r *http.Request) { case "-": showInfo("Streaming URL:" + streamInfo.URL) - http.Redirect(w, r, streamInfo.URL, 302) + http.Redirect(w, r, streamInfo.URL, http.StatusFound) showInfo("Streaming Info:URL was passed to the client.") showInfo("Streaming Info:xTeVe is no longer involved, the client connects directly to the streaming server.") @@ -191,32 +188,6 @@ func Stream(w http.ResponseWriter, r *http.Request) { } - return -} - -// Auto : HDHR routing (wird derzeit nicht benutzt) -func Auto(w http.ResponseWriter, r *http.Request) { - - var channelID = strings.Replace(r.RequestURI, "/auto/v", "", 1) - fmt.Println(channelID) - - /* - switch Settings.Buffer { - - case true: - var playlistID, streamURL, err = getStreamByChannelID(channelID) - if err == nil { - bufferingStream(playlistID, streamURL, w, r) - } else { - httpStatusError(w, r, 404) - } - - case false: - httpStatusError(w, r, 423) - } - */ - - return } // xTeVe : Web Server /xmltv/ und /m3u/ @@ -250,7 +221,7 @@ func xTeVe(w http.ResponseWriter, r *http.Request) { requestType = "m3u" groupTitle = r.URL.Query().Get("group-title") - if System.Dev == false { + if !System.Dev { // false: Dateiname wird im Header gesetzt // true: M3U wird direkt im Browser angezeigt w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(path)) @@ -286,7 +257,6 @@ func xTeVe(w http.ResponseWriter, r *http.Request) { w.Write([]byte(content)) } - return } // Images : Image Cache /images/ @@ -306,7 +276,6 @@ func Images(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write(content) - return } // DataImages : Image Pfad für Logos / Bilder die hochgeladen wurden /data_images/ @@ -326,7 +295,6 @@ func DataImages(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write(content) - return } // WS : Web Sockets /ws/ @@ -362,7 +330,7 @@ func WS(w http.ResponseWriter, r *http.Request) { return } - if System.ConfigurationWizard == false { + if !System.ConfigurationWizard { switch Settings.AuthenticationWEB { @@ -425,7 +393,7 @@ func WS(w http.ResponseWriter, r *http.Request) { response.OpenMenu = strconv.Itoa(indexOfString("settings", System.WEB.Menu)) - if Settings.AuthenticationWEB == true && authenticationUpdate == false { + if Settings.AuthenticationWEB && !authenticationUpdate { response.Reload = true } @@ -568,7 +536,7 @@ func WS(w http.ResponseWriter, r *http.Request) { var requestMap = make(map[string]any) // Debug _ = requestMap - if System.Dev == true { + if System.Dev { fmt.Println(mapToJSON(requestMap)) } @@ -581,7 +549,7 @@ func WS(w http.ResponseWriter, r *http.Request) { } response = setDefaultResponseData(response, true) - if System.ConfigurationWizard == true { + if System.ConfigurationWizard { response.ConfigurationWizard = System.ConfigurationWizard } @@ -593,7 +561,6 @@ func WS(w http.ResponseWriter, r *http.Request) { } - return } // Healthz : Liveness endpoint for container healthchecks and monitoring. @@ -629,7 +596,7 @@ func Web(w http.ResponseWriter, r *http.Request) { if System.ScanInProgress == 0 { - if Settings.WizardCompleted == false && len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 { + if !Settings.WizardCompleted && len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 { System.ConfigurationWizard = true } @@ -675,7 +642,7 @@ func Web(w http.ResponseWriter, r *http.Request) { } // Redirect, damit die Daten aus dem Browser gelöscht werden. w = authentication.SetCookieToken(w, token) - http.Redirect(w, r, "/web", 301) + http.Redirect(w, r, "/web", http.StatusMovedPermanently) return } @@ -691,11 +658,11 @@ func Web(w http.ResponseWriter, r *http.Request) { } w = authentication.SetCookieToken(w, token) - http.Redirect(w, r, "/web", 301) // Redirect, damit die Daten aus dem Browser gelöscht werden. + http.Redirect(w, r, "/web", http.StatusMovedPermanently) // Redirect, damit die Daten aus dem Browser gelöscht werden. } else { w = authentication.SetCookieToken(w, "-") - http.Redirect(w, r, "/web", 301) // Redirect, damit die Daten aus dem Browser gelöscht werden. + http.Redirect(w, r, "/web", http.StatusMovedPermanently) // Redirect, damit die Daten aus dem Browser gelöscht werden. } return @@ -723,7 +690,7 @@ func Web(w http.ResponseWriter, r *http.Request) { return } - if len(allUserData) == 0 && Settings.AuthenticationWEB == true { + if len(allUserData) == 0 && Settings.AuthenticationWEB { file = "create-first-user.html" } @@ -828,13 +795,12 @@ func API(w http.ResponseWriter, r *http.Request) { response.Status = false response.Error = err.Error() w.Write([]byte(mapToJSON(response))) - return } response.Status = true - if Settings.API == false { + if !Settings.API { httpStatusError(w, r, 423) return } @@ -844,7 +810,7 @@ func API(w http.ResponseWriter, r *http.Request) { return } - b, err := ioutil.ReadAll(r.Body) + b, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { httpStatusError(w, r, 400) @@ -860,7 +826,7 @@ func API(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") - if Settings.AuthenticationAPI == true { + if Settings.AuthenticationAPI { var token string switch len(request.Token) { case 0: @@ -957,7 +923,6 @@ func API(w http.ResponseWriter, r *http.Request) { w.Write([]byte(mapToJSON(response))) - return } // Download : Datei Download @@ -975,7 +940,6 @@ func Download(w http.ResponseWriter, r *http.Request) { os.RemoveAll(System.Folder.Temp + getFilenameFromPath(path)) w.Write([]byte(content)) - return } func setDefaultResponseData(response ResponseStruct, data bool) (defaults ResponseStruct) { @@ -999,7 +963,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon switch System.Branch { case "master": - defaults.ClientInfo.Version = fmt.Sprintf("%s", System.Version) + defaults.ClientInfo.Version = System.Version default: defaults.ClientInfo.Version = fmt.Sprintf("%s (%s)", System.Version, System.Build) @@ -1007,7 +971,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon } - if data == true { + if data { defaults.Users, _ = authentication.GetAllUserData() //defaults.DVR = System.DVRAddress @@ -1048,7 +1012,6 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon func httpStatusError(w http.ResponseWriter, r *http.Request, httpStatusCode int) { http.Error(w, fmt.Sprintf("%s [%d]", http.StatusText(httpStatusCode), httpStatusCode), httpStatusCode) - return } func getContentType(filename string) (contentType string) { diff --git a/src/xepg.go b/src/xepg.go index f098b2e..b3eb318 100644 --- a/src/xepg.go +++ b/src/xepg.go @@ -5,7 +5,7 @@ import ( "encoding/xml" "errors" "fmt" - "io/ioutil" + "os" "path" "runtime" "sort" @@ -71,28 +71,9 @@ func buildXEPG(background bool) { createM3UFile() queuePlexGuideRefresh("xepg rebuild") - showInfo("XEPG:" + fmt.Sprintf("Ready to use")) + showInfo("XEPG:" + "Ready to use") - if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { - - go func() { - - System.ImageCachingInProgress = 1 - showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue))) - - Data.Cache.Images.Image.Caching() - Data.Cache.Images.Image.Remove() - showInfo("Image Caching:Done") - - createXMLTVFile() - createM3UFile() - queuePlexGuideRefresh("xepg image cache refresh") - - System.ImageCachingInProgress = 0 - - }() - - } + cacheImagesInBackground() System.ScanInProgress = 0 @@ -118,28 +99,9 @@ func buildXEPG(background bool) { createM3UFile() queuePlexGuideRefresh("xepg rebuild") - if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { + cacheImagesInBackground() - go func() { - - System.ImageCachingInProgress = 1 - showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue))) - - Data.Cache.Images.Image.Caching() - Data.Cache.Images.Image.Remove() - showInfo("Image Caching:Done") - - createXMLTVFile() - createM3UFile() - queuePlexGuideRefresh("xepg image cache refresh") - - System.ImageCachingInProgress = 0 - - }() - - } - - showInfo("XEPG:" + fmt.Sprintf("Ready to use")) + showInfo("XEPG:" + "Ready to use") System.ScanInProgress = 0 @@ -161,52 +123,30 @@ func buildXEPG(background bool) { } -// XEPG Daten aktualisieren -func updateXEPG(background bool) { +// cacheImagesInBackground : Bilder im Hintergrund cachen, danach XMLTV- und M3U-Datei neu erstellen +func cacheImagesInBackground() { - if System.ScanInProgress == 1 { + if !Settings.CacheImages || System.ImageCachingInProgress != 0 { return } - System.ScanInProgress = 1 + go func() { - if Settings.EpgSource == "XEPG" { + System.ImageCachingInProgress = 1 + showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue))) - switch background { + Data.Cache.Images.Image.Caching() + Data.Cache.Images.Image.Remove() + showInfo("Image Caching:Done") - case false: + createXMLTVFile() + createM3UFile() + queuePlexGuideRefresh("xepg image cache refresh") - createXEPGDatabase() - mapping() - cleanupXEPG() + System.ImageCachingInProgress = 0 - go func() { + }() - createXMLTVFile() - createM3UFile() - queuePlexGuideRefresh("xepg update") - showInfo("XEPG:" + fmt.Sprintf("Ready to use")) - - System.ScanInProgress = 0 - - }() - - case true: - System.ScanInProgress = 0 - - } - - } else { - - System.ScanInProgress = 0 - - } - - // Cache löschen - //Data.Cache.XMLTV = nil //make(map[string]XMLTV) - //Data.Cache.XMLTV = make(map[string]XMLTV) - - return } // Mapping Menü für die XMLTV Dateien erstellen @@ -219,7 +159,6 @@ func createXEPGMapping() { var friendlyDisplayName = func(channel Channel) (displayName string) { var dn = channel.DisplayName - displayName = dn[0].Value switch len(dn) { case 1: @@ -277,11 +216,10 @@ func createXEPGMapping() { } Data.XMLTV.Mapping = tmpMap - tmpMap = make(map[string]any) } else { - if System.ConfigurationWizard == false { + if !System.ConfigurationWizard { showWarning(1007) } @@ -304,7 +242,6 @@ func createXEPGMapping() { Data.XMLTV.Mapping["xTeVe Dummy"] = dummy - return } // XEPG Datenbank erstellen / aktualisieren @@ -339,7 +276,7 @@ func createXEPGDatabase() (err error) { sort.Float64s(allChannelNumbers) - var firstFreeNumber float64 = Settings.MappingFirstChannel + var firstFreeNumber = Settings.MappingFirstChannel for { @@ -469,14 +406,14 @@ func createXEPGDatabase() (err error) { xepgChannel.Name = m3uChannel.Name // Kanalname aktualisieren, nur mit Kanal ID's möglich - if channelHasUUID == true { - if xepgChannel.XUpdateChannelName == true { + if channelHasUUID { + if xepgChannel.XUpdateChannelName { xepgChannel.XName = m3uChannel.Name } } // Kanallogo aktualisieren. Wird bei vorhandenem Logo in der XMLTV Datei wieder überschrieben - if xepgChannel.XUpdateChannelIcon == true { + if xepgChannel.XUpdateChannelIcon { xepgChannel.TvgLogo = m3uChannel.TvgLogo } @@ -602,7 +539,7 @@ func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGCh for id, data := range xmltvChannels { xmltvChannel, castOK := data.(map[string]any) - if castOK == false { + if !castOK { continue } @@ -615,7 +552,7 @@ func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGCh } displayName, hasDisplayName := xmltvChannel["display-name"].(string) - if hasDisplayName == false { + if !hasDisplayName { continue } @@ -660,7 +597,7 @@ func mapping() (err error) { } // Automatische Mapping für neue Kanäle. Wird nur ausgeführt, wenn der Kanal deaktiviert ist und keine XMLTV Datei und kein XMLTV Kanal zugeordnet ist. - if xepgChannel.XActive == false { + if !xepgChannel.XActive { // Werte kann "-" sein, deswegen len < 1 if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XMapping) < 1 { @@ -704,7 +641,7 @@ func mapping() (err error) { } // Überprüfen, ob die zugeordneten XMLTV Dateien und Kanäle noch existieren. - if xepgChannel.XActive == true { + if xepgChannel.XActive { var mapping = xepgChannel.XMapping var file = xepgChannel.XmltvFile @@ -718,7 +655,7 @@ func mapping() (err error) { // Kanallogo aktualisieren if logo, ok := channel["icon"].(string); ok { - if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 { + if xepgChannel.XUpdateChannelIcon && len(logo) > 0 { xepgChannel.TvgLogo = logo } @@ -729,7 +666,7 @@ func mapping() (err error) { xepgChannel.XMapping = channelID if logo, ok := replacementChannel["icon"].(string); ok { - if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 { + if xepgChannel.XUpdateChannelIcon && len(logo) > 0 { xepgChannel.TvgLogo = logo } } @@ -752,7 +689,7 @@ func mapping() (err error) { name = xepg } - if strictMissingEPGMode == true { + if strictMissingEPGMode { missingEPGCount++ appendXEPGIssueSample(missingEPGSamples, name) xepgChannel.XActive = false @@ -772,7 +709,7 @@ func mapping() (err error) { providerName = file } - if strictMissingEPGMode == true { + if strictMissingEPGMode { missingXMLTVCount++ appendXEPGIssueSample(missingXMLTVSamples, providerName) xepgChannel.XActive = false @@ -849,7 +786,7 @@ func createXMLTVFile() (err error) { Data.Cache.ImagesURLS = []string{} Data.Cache.ImagesCache = []string{} - files, err := ioutil.ReadDir(System.Folder.ImagesCache) + files, err := os.ReadDir(System.Folder.ImagesCache) if err == nil { for _, file := range files { @@ -887,7 +824,7 @@ func createXMLTVFile() (err error) { err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) if err == nil { - if xepgChannel.XActive == true { + if xepgChannel.XActive { // Kanäle var channel Channel @@ -902,9 +839,7 @@ func createXMLTVFile() (err error) { *tmpProgram, err = getProgramData(xepgChannel) if err == nil { - for _, program := range tmpProgram.Program { - xepgXML.Program = append(xepgXML.Program, program) - } + xepgXML.Program = append(xepgXML.Program, tmpProgram.Program...) } @@ -947,7 +882,7 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) { err = getLocalXMLTV(xmltvFile, &xmltv) if err != nil { - if relaxedMissingEPGMode == true { + if relaxedMissingEPGMode { fallbackToDummy() err = nil } @@ -1024,7 +959,7 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) { } - if len(xepgXML.Program) == 0 && relaxedMissingEPGMode == true && xmltvFile != System.Folder.Data+"xTeVe Dummy" { + if len(xepgXML.Program) == 0 && relaxedMissingEPGMode && xmltvFile != System.Folder.Data+"xTeVe Dummy" { fallbackToDummy() } @@ -1072,7 +1007,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) { epg.Desc = append(epg.Desc, &Desc{Value: xepgChannel.XDescription, Lang: "en"}) } - if Settings.XepgReplaceMissingImages == true { + if Settings.XepgReplaceMissingImages { poster.Src = imgc.Image.GetURL(xepgChannel.TvgLogo) epg.Poster = append(epg.Poster, poster) } @@ -1114,7 +1049,6 @@ func getCategory(program *Program, xmltvProgram *Program, xepgChannel XEPGChanne } - return } // Programm Poster Cover aus der XMLTV Datei laden @@ -1127,7 +1061,7 @@ func getPoster(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelS program.Poster = append(program.Poster, poster) } - if Settings.XepgReplaceMissingImages == true { + if Settings.XepgReplaceMissingImages { if len(xmltvProgram.Poster) == 0 { var poster Poster @@ -1161,7 +1095,6 @@ func getEpisodeNum(program *Program, xmltvProgram *Program, xepgChannel XEPGChan } - return } // Videoparameter erstellen (createXMLTVFile) @@ -1187,7 +1120,6 @@ func getVideo(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelSt program.Video = video - return } // Lokale Provider XMLTV Datei laden @@ -1236,7 +1168,6 @@ func createM3UFile() { saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS) - return } // XEPG Datenbank bereinigen @@ -1254,7 +1185,7 @@ func cleanupXEPG() { sourceIDs = append(sourceIDs, source) } - showInfo("XEPG:" + fmt.Sprintf("Cleanup database")) + showInfo("XEPG:" + "Cleanup database") Data.XEPG.XEPGCount = 0 for id, dxc := range Data.XEPG.Channels { @@ -1266,7 +1197,7 @@ func cleanupXEPG() { if indexOfString(xepgChannel.Name+xepgChannel.FileM3UID, Data.Cache.Streams.Active) == -1 { delete(Data.XEPG.Channels, id) } else { - if xepgChannel.XActive == true { + if xepgChannel.XActive { Data.XEPG.XEPGCount++ } } @@ -1291,34 +1222,4 @@ func cleanupXEPG() { showWarning(2005) } - return -} - -// Streaming URL für die Channels App generieren -func getStreamByChannelID(channelID string) (playlistID, streamURL string, err error) { - - err = errors.New("Channel not found") - - for _, dxc := range Data.XEPG.Channels { - - var xepgChannel XEPGChannelStruct - err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) - - fmt.Println(xepgChannel.XChannelID) - - if err == nil { - - if channelID == xepgChannel.XChannelID { - - playlistID = xepgChannel.FileM3UID - streamURL = xepgChannel.URL - - return playlistID, streamURL, nil - } - - } - - } - - return } diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..79554e4 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,4 @@ +# Style checks deliberately kept out: +# ST1005 - error strings are shown verbatim in the web UI; wording is intentional +# ST1000/ST1003/ST1016/ST1020/ST1021/ST1022 - staticcheck's own defaults +checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] diff --git a/tasks/todo.md b/tasks/todo.md index 2ccc765..e301ee0 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -46,13 +46,13 @@ Status: Phase 0 committed on branch `improvements` 2026-09-26. - [x] `//go:embed` via `html/embed.go`; deleted `webUI.go`, `html-build.go`, `cmd/webui-gen`; ETag + Cache-Control on static assets - [x] i18n dropped: 254 placeholders inlined, `en.json` deleted, only HTML pages templated (`authenticationErr`) - [x] `package.json` + `ts/tsconfig.json` (tsc 5.9.3, ES2020, single outFile `html/js/app.js`, committed, CI-verified); 10 dead JS files deleted; 6 tsc errors fixed incl. a real ASI bug in the search shortcut -- [ ] CI: vet (done), lint, gofmt, `test -race`; bundle freshness check (done) +- [x] CI: vet, gofmt check, staticcheck v0.8.1 (config in `staticcheck.conf`), bundle freshness check. `go test -race` still to add once tests exist - [ ] Version-tagged images (amd64 only) -- [ ] PUID/PGID via su-exec; pin static-ffmpeg; `VOLUME`; fix `LEGACY_CONFIG_DIRS` +- [x] PUID/PGID via su-exec at runtime; static-ffmpeg pinned to 7.1.1; `VOLUME /xteve/config`; `/xteve` removed from `LEGACY_CONFIG_DIRS`; compose files pull from the registry and document SSDP/host networking - [x] `/healthz` endpoint; Dockerfile healthcheck uses it -- [ ] `README-DEV.md` (done) and fork section in `README.md` -- [ ] Deduplicate and delete dead code; drop `ioutil`, `osext`, `rand.Seed` (after go:embed) -- [ ] Resolve missing `docs/design-system/` referenced by `agent.md` +- [x] `README-DEV.md` and fork-specific `README.md` (about, container usage, env vars, security notes) +- [x] staticcheck 399 -> 0; dead code and duplicates removed; `ioutil`/`rand.Seed` gone. Real bugs fixed on the way: leaked file handle per ffmpeg segment, unchecked `http.NewRequest`, migration writing `null` xepg.json, silent user-write failures, unwritable config/temp dir now fatal at start +- [ ] Resolve missing `docs/design-system/` referenced by `agent.md` (user decision: agent.md is a personal, git-ignored file) ## Phase 4: Data model and performance Not planned (lineup is ~170 channels, decided 2026-09-25). See plan for the reference list.