Phase 3: Go hygiene pass, runtime PUID/PGID, fork README
continuous-integration/drone/push Build encountered an error

Go:
- staticcheck 399 -> 0 with staticcheck.conf (style checks ST1000/1003/
  1005/1016/1020/1021/1022 excluded; error strings are shown in the UI).
- io/ioutil and rand.Seed removed; CloseNotifier kept with a lint-ignore
  until the Phase 2 context rewrite.
- Dead code deleted: Auto handler, getStreamByChannelID, updateXEPG,
  indexOfInt, jsonToMapInt64, removeOldSystemData, randomTime, and the
  commented-out blocks in struct-buffer.go and internal/authentication.
- Duplicates folded: cacheImagesInBackground(), one addErrorToStream().
- Bugs found by SA4006/SA5001: os.Create handle leaked per ffmpeg segment
  (buffer.go), http.NewRequest error unchecked (buffer.go), xepg.json
  migration wrote null on read error (migrate.go), WriteUserData errors
  silently dropped (authentication.go), defer Close before error check
  (buffer.go, toolchain.go). checkFilePermission results were discarded;
  an unwritable config or temp dir is now fatal at start-up.
- gofmt applied repo-wide; Drone runs gofmt check and staticcheck.

Docker:
- Entrypoint starts as root, applies PUID/PGID (falls back to XTEVE_UID/
  XTEVE_GID, then image defaults), fixes config ownership only when it
  differs, then drops to xteve via su-exec. --user starts skip all of it.
- /xteve removed from LEGACY_CONFIG_DIRS (it is the parent of the default).
- mwader/static-ffmpeg pinned to 7.1.1; VOLUME /xteve/config.
- Compose files pull registry.coadcorp.com/nathan/xteve:latest, use
  PUID/PGID/TZ, and explain that SSDP needs host networking.
- .dockerignore excludes the npm toolchain (bundle stays in html/js).

Docs: README rewritten for the fork (about, registry, compose, env vars,
security notes); README-DEV gains a container section.
This commit is contained in:
2026-09-26 12:57:43 +10:00
parent ddc70e170a
commit 504ea3f9f4
38 changed files with 1034 additions and 1382 deletions
+7
View File
@@ -16,3 +16,10 @@ agent.md
skill.md skill.md
*.md *.md
!changelog-beta.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
+2
View File
@@ -8,6 +8,8 @@ steps:
commands: commands:
- go mod download - go mod download
- go vet ./... - 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 ./... - go test ./...
- name: webui-check - name: webui-check
+10 -3
View File
@@ -17,14 +17,18 @@ RUN XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed '
&& CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags="-s -w -X main.Version=$XTEVE_VERSION" -o /out/xteve ./xteve.go 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 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_UID=1000
ARG XTEVE_GID=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 \ && addgroup -S -g "${XTEVE_GID}" xteve \
&& adduser -S -D -H -u "${XTEVE_UID}" -G xteve xteve \ && adduser -S -D -H -u "${XTEVE_UID}" -G xteve xteve \
&& mkdir -p /xteve/config \ && 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 --from=ffmpeg /ffprobe /usr/local/bin/ffprobe
COPY docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh 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 EXPOSE 34400/tcp
ENV XTEVE_CONFIG=/xteve/config ENV XTEVE_CONFIG=/xteve/config
ENV XTEVE_PORT=34400 ENV XTEVE_PORT=34400
VOLUME ["/xteve/config"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ 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 CMD wget -qO- "http://127.0.0.1:${XTEVE_PORT}/healthz" > /dev/null || exit 1
+4
View File
@@ -55,3 +55,7 @@ Drone (`.drone.yml`) runs vet, tests, the bundle check, hadolint and compose val
## Endpoints useful for operations ## Endpoints useful for operations
* `GET /healthz` returns `{"status":"ok","version":...,"scanInProgress":...}` with no authentication. The Docker healthcheck uses it. * `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.
+70 -138
View File
@@ -8,9 +8,23 @@
Documentation for setup and configuration is [here](https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md). Documentation for setup and configuration is [here](https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md).
#### Donation ## About this fork
* **Bitcoin:** 1c1iCe4CJPfNUXtqxKBbW2Qd2EtqRPWme
![Bitcoin](html/img/BC-QR.jpg "Bitcoin - xTeVe") 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 ## Requirements
### Plex ### Plex
@@ -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. ### Image
The weakest points are mostly operational and UX-focused:
* UI was historically utility-first and desktop-biased, with limited responsive behavior and visual hierarchy. The image is built by Drone and pushed to a private registry:
* 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.
### Recommended next technical improvements ```
registry.coadcorp.com/nathan/xteve
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 .
``` ```
### Run with Docker Compose (bridge mode) Tags:
* `latest` and `<commit sha>` from the `master` branch.
* `<branch name>` and `<commit sha>` 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 ```bash
docker compose up -d docker compose up -d
``` ```
Compose file: `docker-compose.yml` 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.
Persistent config volume: `./docker-data/config:/xteve/config`
### Run with Docker Compose (host networking, Linux recommended for discovery)
```bash ```bash
docker compose -f docker-compose.host.yml up -d 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://<host-ip>: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`) ### Environment variables
* `XTEVE_PORT` (default: `34400`)
### 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) 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.
* Runs as non-root user (`xteve`)
* Built-in healthcheck against `http://127.0.0.1:${XTEVE_PORT}/healthz` ### 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 ## Security notes
#### 64 Bit Intel / AMD
* [Windows](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_windows_amd64.zip?raw=true) * This fork is meant for a trusted LAN. Do not put it on the internet.
* [OS X](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_darwin_amd64.zip?raw=true) * 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.
* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_amd64.zip?raw=true) * `/healthz` is always unauthenticated. It reveals the version and scan state only.
* [FreeBSD](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_freebsd_amd64.zip?raw=true) * 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.
#### 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
--- ---
### xTeVe Beta branch ## Development
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]
*/
```
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.
+16 -8
View File
@@ -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: services:
xteve: xteve:
build: image: registry.coadcorp.com/nathan/xteve:latest
context: . # Uncomment to build locally instead of pulling from the registry.
dockerfile: Dockerfile # build:
args: # context: .
XTEVE_UID: ${XTEVE_UID:-1000} # dockerfile: Dockerfile
XTEVE_GID: ${XTEVE_GID:-1000}
container_name: xteve container_name: xteve
restart: unless-stopped restart: unless-stopped
network_mode: host network_mode: host
environment: environment:
XTEVE_CONFIG: /xteve/config XTEVE_CONFIG: /xteve/config
XTEVE_PORT: "34400" XTEVE_PORT: "34400"
XTEVE_UID: ${XTEVE_UID:-1000} # Owner of the config directory inside the container. Set these to the
XTEVE_GID: ${XTEVE_GID:-1000} # 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: volumes:
- ./docker-data/config:/xteve/config - ./docker-data/config:/xteve/config
+19 -8
View File
@@ -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://<host-ip>:34400 manually.
services: services:
xteve: xteve:
build: image: registry.coadcorp.com/nathan/xteve:latest
context: . # Uncomment to build locally instead of pulling from the registry.
dockerfile: Dockerfile # build:
args: # context: .
XTEVE_UID: ${XTEVE_UID:-1000} # dockerfile: Dockerfile
XTEVE_GID: ${XTEVE_GID:-1000}
container_name: xteve container_name: xteve
restart: unless-stopped restart: unless-stopped
environment: environment:
XTEVE_CONFIG: /xteve/config XTEVE_CONFIG: /xteve/config
XTEVE_PORT: "34400" XTEVE_PORT: "34400"
XTEVE_UID: ${XTEVE_UID:-1000} # Owner of the config directory inside the container. Set these to the
XTEVE_GID: ${XTEVE_GID:-1000} # 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: ports:
- "34400:34400/tcp" - "34400:34400/tcp"
- "1900:1900/udp" - "1900:1900/udp"
+114 -10
View File
@@ -2,7 +2,16 @@
set -eu set -eu
DEFAULT_CONFIG_DIR="/xteve/config" 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() { resolve_config_dir() {
if [ -n "${XTEVE_CONFIG:-}" ] && [ "${XTEVE_CONFIG}" != "${DEFAULT_CONFIG_DIR}" ]; then if [ -n "${XTEVE_CONFIG:-}" ] && [ "${XTEVE_CONFIG}" != "${DEFAULT_CONFIG_DIR}" ]; then
@@ -48,11 +57,75 @@ settings_initialized() {
return 1 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)" CONFIG_DIR="$(resolve_config_dir)"
PORT="${XTEVE_PORT:-34400}" 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}" 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 if ! settings_initialized "${CONFIG_DIR}/settings.json"; then
for legacy_dir in ${LEGACY_CONFIG_DIRS}; do for legacy_dir in ${LEGACY_CONFIG_DIRS}; do
if [ "${legacy_dir}" = "${CONFIG_DIR}" ]; then if [ "${legacy_dir}" = "${CONFIG_DIR}" ]; then
@@ -60,7 +133,7 @@ if ! settings_initialized "${CONFIG_DIR}/settings.json"; then
fi fi
if settings_initialized "${legacy_dir}/settings.json"; then 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 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 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}" copy_if_missing "${legacy_dir}/${dir_name}" "${CONFIG_DIR}/${dir_name}"
done done
MIGRATED=1
break break
fi fi
done done
fi fi
if ! touch "${CONFIG_DIR}/.xteve-write-test" 2>/dev/null; then if [ "$(id -u)" = "0" ]; then
echo "[entrypoint] ERROR: Config directory is not writable: ${CONFIG_DIR}" >&2 # Started as root (the default): apply PUID/PGID, fix ownership, drop privileges.
echo "[entrypoint] Running as UID:GID $(id -u):$(id -g)" >&2 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 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: 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
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
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 exit 1
fi fi
rm -f "${CONFIG_DIR}/.xteve-write-test" rm -f "${CONFIG_DIR}/.xteve-write-test"
echo "[entrypoint] Using config directory: ${CONFIG_DIR}" log "Using config directory: ${CONFIG_DIR}"
echo "[entrypoint] Running as UID:GID $(id -u):$(id -g)" log "Running as UID:GID $(id -u):$(id -g)"
if [ -f "${CONFIG_DIR}/settings.json" ]; then 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 fi
exec /usr/local/bin/xteve -config "${CONFIG_DIR}" -port "${PORT}" "$@" exec "${XTEVE_BIN}" -config "${CONFIG_DIR}" -port "${PORT}" "$@"
+10 -6
View File
@@ -63,7 +63,7 @@ func createFirstUserForAuthentication(username, password string) (token string,
func tokenAuthentication(token string) (newToken string, err error) { func tokenAuthentication(token string) (newToken string, err error) {
if System.ConfigurationWizard == true { if System.ConfigurationWizard {
return return
} }
@@ -109,7 +109,7 @@ func urlAuth(r *http.Request, requestType string) (err error) {
case "m3u": case "m3u":
level = "authentication.m3u" level = "authentication.m3u"
if Settings.AuthenticationM3U == true { if Settings.AuthenticationM3U {
token, err = authentication.UserAuthentication(username, password) token, err = authentication.UserAuthentication(username, password)
if err != nil { if err != nil {
return return
@@ -119,7 +119,7 @@ func urlAuth(r *http.Request, requestType string) (err error) {
case "xml": case "xml":
level = "authentication.xml" level = "authentication.xml"
if Settings.AuthenticationXML == true { if Settings.AuthenticationXML {
token, err = authentication.UserAuthentication(username, password) token, err = authentication.UserAuthentication(username, password)
if err != nil { if err != nil {
return return
@@ -150,18 +150,22 @@ func checkAuthorizationLevel(token, level string) (err error) {
if v, ok := userData[level].(bool); ok { if v, ok := userData[level].(bool); ok {
if v == false { if !v {
err = errors.New("No authorization") err = errors.New("No authorization")
} }
} else { } else {
userData[level] = false userData[level] = false
err = authentication.WriteUserData(userID, userData) if err = authentication.WriteUserData(userID, userData); err != nil {
ShowError(err, 0)
}
err = errors.New("No authorization") err = errors.New("No authorization")
} }
} else { } else {
err = authentication.WriteUserData(userID, userData) if err = authentication.WriteUserData(userID, userData); err != nil {
ShowError(err, 0)
}
err = errors.New("No authorization") err = errors.New("No authorization")
} }
+1 -2
View File
@@ -4,7 +4,6 @@ import (
b64 "encoding/base64" b64 "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -32,7 +31,7 @@ func xTeVeAutoBackup() (err error) {
} }
// Alte Backups löschen // Alte Backups löschen
files, err := ioutil.ReadDir(System.Folder.Backup) files, err := os.ReadDir(System.Folder.Backup)
if err == nil { if err == nil {
+77 -86
View File
@@ -11,7 +11,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -146,7 +145,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon
} }
// Neuer Stream bei einer bereits aktiven Playlist // Neuer Stream bei einer bereits aktiven Playlist
if newStream == true { if newStream {
// Prüfen ob die Playlist noch einen weiteren Stream erlaubt (Tuner) // Prüfen ob die Playlist noch einen weiteren Stream erlaubt (Tuner)
if len(playlist.Streams) >= playlist.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 // Ü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 // Neuer Buffer wird benötigt
stream = playlist.Streams[streamID] 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, ok := playlist.Streams[streamID]; ok {
if stream.Status == false { if !stream.Status {
timeOut++ timeOut++
@@ -261,6 +260,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon
// HTTP Clientverbindung überwachen // HTTP Clientverbindung überwachen
//lint:ignore SA1019 replaced by request context in Phase 2
cn, ok := w.(http.CloseNotifier) cn, ok := w.(http.CloseNotifier)
if ok { if ok {
@@ -310,10 +310,11 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon
var fileName = stream.Folder + f var fileName = stream.Folder + f
file, err := os.Open(fileName) file, err := os.Open(fileName)
defer file.Close()
if err == nil { if err == nil {
defer file.Close()
l, err := file.Stat() l, err := file.Stat()
if err == nil { if err == nil {
@@ -327,7 +328,7 @@ func bufferingStream(playlistID, streamingURL, channelName string, w http.Respon
file.Seek(0, 0) file.Seek(0, 0)
if streaming == false { if !streaming {
contentType := http.DetectContentType(buffer) contentType := http.DetectContentType(buffer)
_ = contentType _ = contentType
@@ -406,7 +407,7 @@ func getTmpFiles(stream *ThisStream) (tmpFiles []string) {
if _, err := os.Stat(tmpFolder); !os.IsNotExist(err) { if _, err := os.Stat(tmpFolder); !os.IsNotExist(err) {
files, err := ioutil.ReadDir(getPlatformPath(tmpFolder)) files, err := os.ReadDir(getPlatformPath(tmpFolder))
if err != nil { if err != nil {
ShowError(err, 000) ShowError(err, 000)
return return
@@ -455,7 +456,7 @@ func killClientConnection(streamID int, playlistID string, force bool) {
var playlist = p.(Playlist) var playlist = p.(Playlist)
if force == true { if force {
delete(playlist.Streams, streamID) delete(playlist.Streams, streamID)
showInfo(fmt.Sprintf("Streaming Status:Playlist: %s - Tuner: %d / %d", playlist.PlaylistName, len(playlist.Streams), playlist.Tuner)) showInfo(fmt.Sprintf("Streaming Status:Playlist: %s - Tuner: %d / %d", playlist.PlaylistName, len(playlist.Streams), playlist.Tuner))
return return
@@ -531,6 +532,21 @@ func clientConnection(stream ThisStream) (status bool) {
return 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) { func connectToStreamingServer(streamID int, playlistID string) {
if p, ok := BufferInformation.Load(playlistID); ok { 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)) os.RemoveAll(getPlatformPath(tmpFolder))
err := checkFolder(tmpFolder) err := checkFolder(tmpFolder)
if err != nil { if err != nil {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
return return
} }
@@ -609,11 +611,11 @@ func connectToStreamingServer(streamID int, playlistID string) {
return 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]") showInfo("Streaming Type:" + "[HLS / M3U8]")
} else { } else {
showInfo("Streaming Type:" + "[TS]") showInfo("Streaming Type:" + "[TS]")
@@ -631,7 +633,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
for { for {
if clientConnection(stream) == false { if !clientConnection(stream) {
return return
} }
@@ -654,6 +656,14 @@ func connectToStreamingServer(streamID int, playlistID string) {
Redirect: Redirect:
req, err := http.NewRequest("GET", currentURL, nil) 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("User-Agent", getUserAgent())
req.Header.Set("Connection", "close") req.Header.Set("Connection", "close")
//req.Header.Set("Range", "bytes=0-") //req.Header.Set("Range", "bytes=0-")
@@ -679,7 +689,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
fmt.Println("Current URL:", currentURL) fmt.Println("Current URL:", currentURL)
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
killClientConnection(streamID, playlistID, true) killClientConnection(streamID, playlistID, true)
clientConnection(stream) clientConnection(stream)
@@ -708,7 +718,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
err = errors.New("Streaming server") err = errors.New("Streaming server")
ShowError(err, 4002) ShowError(err, 4002)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
defer resp.Body.Close() defer resp.Body.Close()
@@ -719,7 +729,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
} else { } else {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
defer resp.Body.Close() defer resp.Body.Close()
@@ -749,7 +759,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
showDebug(debug, 1) showDebug(debug, 1)
BufferInformation.Store(playlist.PlaylistID, playlist) BufferInformation.Store(playlist.PlaylistID, playlist)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
killClientConnection(streamID, playlistID, true) killClientConnection(streamID, playlistID, true)
clientConnection(stream) clientConnection(stream)
@@ -759,7 +769,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
} }
// Informationen über den Streamingserver auslesen // Informationen über den Streamingserver auslesen
if stream.Status == false { if !stream.Status {
if len(stream.URLStreamingServer) == 0 { if len(stream.URLStreamingServer) == 0 {
@@ -802,10 +812,10 @@ func connectToStreamingServer(streamID int, playlistID string) {
// M3U8 Playlist // M3U8 Playlist
case "application/x-mpegurl", "application/vnd.apple.mpegurl", "audio/mpegurl", "audio/x-mpegurl": 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 { if err != nil {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
} }
stream.Body = string(body) stream.Body = string(body)
@@ -815,7 +825,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
err = parseM3U8(&stream) err = parseM3U8(&stream)
if err != nil { if err != nil {
ShowError(err, 4050) ShowError(err, 4050)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
} }
// Video Stream (TS) // Video Stream (TS)
@@ -835,7 +845,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
var tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment) var tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment)
if clientConnection(stream) == false { if !clientConnection(stream) {
resp.Body.Close() resp.Body.Close()
return return
} }
@@ -843,7 +853,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
bufferFile, err := os.Create(tmpFile) bufferFile, err := os.Create(tmpFile)
if err != nil { if err != nil {
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
bufferFile.Close() bufferFile.Close()
resp.Body.Close() resp.Body.Close()
return return
@@ -866,7 +876,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
resp.Body.Close() resp.Body.Close()
return return
@@ -877,7 +887,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
if _, err := bufferFile.Write(buffer[:n]); err != nil { if _, err := bufferFile.Write(buffer[:n]); err != nil {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
resp.Body.Close() resp.Body.Close()
return return
@@ -887,7 +897,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
fileSize = fileSize + n fileSize = fileSize + n
if clientConnection(stream) == false { if !clientConnection(stream) {
resp.Body.Close() resp.Body.Close()
bufferFile.Close() bufferFile.Close()
@@ -929,7 +939,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment) tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment)
if clientConnection(stream) == false { if !clientConnection(stream) {
bufferFile.Close() bufferFile.Close()
resp.Body.Close() resp.Body.Close()
@@ -944,7 +954,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
bufferFile, err = os.Create(tmpFile) bufferFile, err = os.Create(tmpFile)
if err != nil { if err != nil {
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
resp.Body.Close() resp.Body.Close()
return return
} }
@@ -970,7 +980,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
err = errors.New("Streaming error") err = errors.New("Streaming error")
ShowError(err, 4003) ShowError(err, 4003)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
resp.Body.Close() resp.Body.Close()
return return
} }
@@ -978,7 +988,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
s++ s++
// Wartezeit für den Download das nächste Segments berechnen // Wartezeit für den Download das nächste Segments berechnen
if stream.HLS == true { if stream.HLS {
var sleep float64 var sleep float64
@@ -1143,14 +1153,13 @@ func parseM3U8(stream *ThisStream) (err error) {
} }
return
} }
if strings.Contains(stream.Body, "#EXTM3U") { if strings.Contains(stream.Body, "#EXTM3U") {
var lines = strings.Split(strings.Replace(stream.Body, "\r\n", "\n", -1), "\n") 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) 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) // 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 var dynamicStream DynamicStream
@@ -1217,7 +1226,7 @@ func parseM3U8(stream *ThisStream) (err error) {
noNewSegment = true noNewSegment = true
if stream.Status == false { if !stream.Status {
if len(m3u8Segments) >= 2 { if len(m3u8Segments) >= 2 {
m3u8Segments = m3u8Segments[0 : len(m3u8Segments)-1] m3u8Segments = m3u8Segments[0 : len(m3u8Segments)-1]
@@ -1229,7 +1238,7 @@ func parseM3U8(stream *ThisStream) (err error) {
segment = s segment = s
if stream.Status == false { if !stream.Status {
noNewSegment = false noNewSegment = false
stream.LastSequence = segment.Sequence 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) switchBandwidth(stream)
} else { } else {
stream.Segment = append(stream.Segment, segment) 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 var sleep = lastSegmentDuration * 0.5
@@ -1371,33 +1380,19 @@ func thirdPartyBuffer(streamID int, playlistID string) {
return 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)) os.RemoveAll(getPlatformPath(tmpFolder))
err := checkFolder(tmpFolder) err := checkFolder(tmpFolder)
if err != nil { if err != nil {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
return return
} }
err = checkFile(path) err = checkFile(path)
if err != nil { if err != nil {
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
return return
} }
@@ -1409,7 +1404,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
f, err := os.Create(tmpFile) f, err := os.Create(tmpFile)
f.Close() f.Close()
if err != nil { if err != nil {
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
return return
} }
@@ -1461,7 +1456,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
ShowError(err, 0) ShowError(err, 0)
cmd.Process.Kill() cmd.Process.Kill()
cmd.Wait() cmd.Wait()
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
return return
} }
@@ -1471,11 +1466,11 @@ func thirdPartyBuffer(streamID int, playlistID string) {
ShowError(err, 0) ShowError(err, 0)
cmd.Process.Kill() cmd.Process.Kill()
cmd.Wait() cmd.Wait()
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
return return
} }
if len(buf.Bytes()) == 0 && stream.Status == false { if len(buf.Bytes()) == 0 && !stream.Status {
showInfo(bufferType + ":Processing data") showInfo(bufferType + ":Processing data")
} }
@@ -1543,7 +1538,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
cmd.Process.Kill() cmd.Process.Kill()
err = errors.New("Timout") err = errors.New("Timout")
ShowError(err, 4006) ShowError(err, 4006)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
cmd.Wait() cmd.Wait()
f.Close() f.Close()
return 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) showInfo("Streaming Status:Receive data from " + bufferType)
} }
if clientConnection(stream) == false { if !clientConnection(stream) {
cmd.Process.Kill() cmd.Process.Kill()
f.Close() f.Close()
cmd.Wait() cmd.Wait()
@@ -1574,7 +1569,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
if _, err := f.Write(buffer[:n]); err != nil { if _, err := f.Write(buffer[:n]); err != nil {
cmd.Process.Kill() cmd.Process.Kill()
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
cmd.Wait() cmd.Wait()
f.Close() f.Close()
return return
@@ -1582,7 +1577,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
if fileSize >= bufferSize/2 { if fileSize >= bufferSize/2 {
if tmpSegment == 1 && stream.Status == false { if tmpSegment == 1 && !stream.Status {
close(t) close(t)
close(streamStatus) close(streamStatus)
showInfo(fmt.Sprintf("Streaming Status:Buffering data from %s", bufferType)) showInfo(fmt.Sprintf("Streaming Status:Buffering data from %s", bufferType))
@@ -1591,7 +1586,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
f.Close() f.Close()
tmpSegment++ tmpSegment++
if stream.Status == false { if !stream.Status {
Lock.Lock() Lock.Lock()
stream.Status = true stream.Status = true
playlist.Streams[streamID] = stream playlist.Streams[streamID] = stream
@@ -1603,13 +1598,11 @@ func thirdPartyBuffer(streamID int, playlistID string) {
fileSize = 0 fileSize = 0
var errCreate, errOpen error f, err = os.OpenFile(tmpFile, os.O_CREATE|os.O_TRUNC|os.O_APPEND|os.O_WRONLY, 0666)
f, errCreate = os.Create(tmpFile) if err != nil {
f, errOpen = os.OpenFile(tmpFile, os.O_APPEND|os.O_WRONLY, 0600)
if errCreate != nil || errOpen != nil {
cmd.Process.Kill() cmd.Process.Kill()
ShowError(err, 0) ShowError(err, 0)
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
cmd.Wait() cmd.Wait()
f.Close() f.Close()
return return
@@ -1623,7 +1616,7 @@ func thirdPartyBuffer(streamID int, playlistID string) {
cmd.Wait() cmd.Wait()
err = errors.New(bufferType + " error") err = errors.New(bufferType + " error")
addErrorToStream(err) addErrorToStream(playlist, streamID, playlistID, err)
ShowError(err, 1204) ShowError(err, 1204)
time.Sleep(time.Duration(500) * time.Millisecond) time.Sleep(time.Duration(500) * time.Millisecond)
@@ -1694,7 +1687,6 @@ func debugRequest(req *http.Request) {
debug = "Request:* * * * * * END HTTP(S) REQUEST * * * * * *" debug = "Request:* * * * * * END HTTP(S) REQUEST * * * * * *"
showDebug(debug, debugLevel) showDebug(debug, debugLevel)
return
} }
func debugResponse(resp *http.Response) { func debugResponse(resp *http.Response) {
@@ -1739,5 +1731,4 @@ func debugResponse(resp *http.Response) {
debug = "Pesponse:* * * * * * END RESPONSE * * * * * * " debug = "Pesponse:* * * * * * END RESPONSE * * * * * * "
showDebug(debug, debugLevel) showDebug(debug, debugLevel)
return
} }
+11 -4
View File
@@ -152,8 +152,9 @@ func Init() (err error) {
// Berechtigung aller Ordner überprüfen // Berechtigung aller Ordner überprüfen
err = checkFilePermission(System.Folder.Config) err = checkFilePermission(System.Folder.Config)
if err == nil { if err != nil {
err = checkFilePermission(System.Folder.Temp) ShowError(err, 0)
return
} }
// Separaten tmp Ordner für jede Instanz // Separaten tmp Ordner für jede Instanz
@@ -165,6 +166,12 @@ func Init() (err error) {
return return
} }
err = checkFilePermission(System.Folder.Temp)
if err != nil {
ShowError(err, 0)
return
}
err = removeChildItems(getPlatformPath(System.Folder.Temp)) err = removeChildItems(getPlatformPath(System.Folder.Temp))
if err != nil { if err != nil {
return return
@@ -173,7 +180,7 @@ func Init() (err error) {
// Branch festlegen // Branch festlegen
System.Branch = Settings.Branch System.Branch = Settings.Branch
if System.Dev == true { if System.Dev {
System.Branch = "Development" System.Branch = "Development"
} }
@@ -217,7 +224,7 @@ func StartSystem(updateProviderFiles bool) (err error) {
showInfo(fmt.Sprintf("Unfiltered Chan. Limit:%d", System.UnfilteredChannelLimit)) showInfo(fmt.Sprintf("Unfiltered Chan. Limit:%d", System.UnfilteredChannelLimit))
// Providerdaten aktualisieren // Providerdaten aktualisieren
if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate == true || updateProviderFiles == true { if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate || updateProviderFiles {
err = xTeVeAutoBackup() err = xTeVeAutoBackup()
if err != nil { if err != nil {
+12 -21
View File
@@ -73,10 +73,6 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
} }
if len(newUpdateTimes) == 0 {
//newUpdateTimes = append(newUpdateTimes, "0000")
}
value = newUpdateTimes value = newUpdateTimes
case "cache.images": case "cache.images":
@@ -179,7 +175,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
return return
} }
if Settings.AuthenticationWEB == false { if !Settings.AuthenticationWEB {
Settings.AuthenticationAPI = false Settings.AuthenticationAPI = false
Settings.AuthenticationM3U = false Settings.AuthenticationM3U = false
@@ -221,7 +217,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
settings = Settings settings = Settings
if reloadData == true { if reloadData {
err = buildDatabaseDVR() err = buildDatabaseDVR()
if err != nil { 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 { 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() { go func() {
createXMLTVFile() createXMLTVFile()
@@ -280,7 +276,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
} }
if triggerPlexGuideReload == true { if triggerPlexGuideReload {
queuePlexGuideRefresh("settings change") queuePlexGuideRefresh("settings change")
} }
@@ -378,7 +374,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
return return
} }
if reloadData == true { if reloadData {
err = buildDatabaseDVR() err = buildDatabaseDVR()
if err != nil { if err != nil {
@@ -452,7 +448,6 @@ func deleteLocalProviderFiles(dataID, fileType string) {
os.RemoveAll(System.Folder.Data + dataID + fileExtension) os.RemoveAll(System.Folder.Data + dataID + fileExtension)
} }
return
} }
// Filtereinstellungen speichern (WebUI) // Filtereinstellungen speichern (WebUI)
@@ -505,7 +500,7 @@ func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
if len(filter) == 0 { if len(filter) == 0 {
err = errors.New(getErrMsg(1014)) err = errors.New(getErrMsg(1014))
if newFilter == true { if newFilter {
delete(filterMap, dataID) 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. // Wenn während des erstellen der Datanbank das Mapping erneut gespeichert wird, wird die Datenbank erst später erneut aktualisiert.
go func() { go func() {
if System.BackgroundProcess == true { if System.BackgroundProcess {
return return
} }
@@ -591,7 +586,7 @@ func saveXEpgMapping(request RequestStruct) (err error) {
cleanupXEPG() cleanupXEPG()
System.ScanInProgress = 0 System.ScanInProgress = 0
buildXEPG(false) buildXEPG(false)
showInfo("XEPG:" + fmt.Sprintf("Ready to use")) showInfo("XEPG:" + "Ready to use")
System.BackgroundProcess = false System.BackgroundProcess = false
@@ -632,7 +627,7 @@ func saveUserData(request RequestStruct) (err error) {
return return
} }
if request.DeleteUser == true { if request.DeleteUser {
err = authentication.RemoveUser(userID) err = authentication.RemoveUser(userID)
return return
} }
@@ -859,7 +854,7 @@ func buildDatabaseDVR() (err error) {
for n, i := range playlistFile { for n, i := range playlistFile {
var channels []any 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 keys = []string{"group-title", "tvg-id", "uuid"}
var compatibility = make(map[string]int) var compatibility = make(map[string]int)
@@ -905,11 +900,7 @@ func buildDatabaseDVR() (err error) {
if value, ok := s[key]; ok { if value, ok := s[key]; ok {
if len(value) > 0 { if len(value) > 0 {
if _, ok := tmpGroupsM3U[value]; ok {
tmpGroupsM3U[value]++ tmpGroupsM3U[value]++
} else {
tmpGroupsM3U[value] = 1
}
groupTitle++ groupTitle++
} }
@@ -985,7 +976,7 @@ func buildDatabaseDVR() (err error) {
for group, count := range tmpGroupsM3U { for group, count := range tmpGroupsM3U {
var text = fmt.Sprintf("%s (%d)", group, count) 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.Text = append(Data.Playlist.M3U.Groups.Text, text)
Data.Playlist.M3U.Groups.Value = append(Data.Playlist.M3U.Groups.Value, value) Data.Playlist.M3U.Groups.Value = append(Data.Playlist.M3U.Groups.Value, value)
} }
+1 -1
View File
@@ -154,7 +154,7 @@ func getLineup() (jsonContent []byte, err error) {
return return
} }
if xepgChannel.XActive == true { if xepgChannel.XActive {
var stream LineupStream var stream LineupStream
stream.GuideName = xepgChannel.XName stream.GuideName = xepgChannel.XName
stream.GuideNumber = xepgChannel.XChannelID stream.GuideNumber = xepgChannel.XChannelID
+28 -32
View File
@@ -20,45 +20,41 @@ func ShowSystemInfo() {
fmt.Println("OK") fmt.Println("OK")
println() println()
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build)) fmt.Printf("Version: %s %s.%s\n", System.Name, System.Version, System.Build)
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch)) fmt.Printf("Branch: %s\n", System.Branch)
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config)) 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.Printf("Streams: %d / %d\n", len(Data.Streams.Active), len(Data.Streams.All))
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter))) fmt.Printf("Filter: %d\n", len(Data.Filter))
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount))) fmt.Printf("XEPG Chanels: %d\n", int(Data.XEPG.XEPGCount))
println() println()
fmt.Println(fmt.Sprintf("IPv4 Addresses:")) fmt.Println("IPv4 Addresses:")
for i, ipv4 := range System.IPAddressesV4 { for i, ipv4 := range System.IPAddressesV4 {
switch count := i; { switch count := i; {
case count < 10: case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) fmt.Printf(" %d. %s\n", count, ipv4)
break
case count < 100: case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) fmt.Printf(" %d. %s\n", count, ipv4)
break
} }
} }
println() println()
fmt.Println(fmt.Sprintf("IPv6 Addresses:")) fmt.Println("IPv6 Addresses:")
for i, ipv4 := range System.IPAddressesV6 { for i, ipv4 := range System.IPAddressesV6 {
switch count := i; { switch count := i; {
case count < 10: case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) fmt.Printf(" %d. %s\n", count, ipv4)
break
case count < 100: case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4)) fmt.Printf(" %d. %s\n", count, ipv4)
break
} }
@@ -67,33 +63,33 @@ func ShowSystemInfo() {
println("---") println("---")
fmt.Println("Settings [General]") fmt.Println("Settings [General]")
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID)) fmt.Printf("UUID: %s\n", Settings.UUID)
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner)) fmt.Printf("Tuner (Plex / Emby): %d\n", Settings.Tuner)
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource)) fmt.Printf("EPG Source: %s\n", Settings.EpgSource)
println("---") println("---")
fmt.Println("Settings [Files]") fmt.Println("Settings [Files]")
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ","))) fmt.Printf("Schedule: %s\n", strings.Join(Settings.Update, ","))
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate)) fmt.Printf("Files Update: %t\n", Settings.FilesUpdate)
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath)) fmt.Printf("Folder (tmp): %s\n", Settings.TempPath)
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages)) fmt.Printf("Image Chaching: %t\n", Settings.CacheImages)
fmt.Println(fmt.Sprintf("Missing EPG Mode: %s", Settings.XepgMissingEPGMode)) fmt.Printf("Missing EPG Mode: %s\n", Settings.XepgMissingEPGMode)
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages)) fmt.Printf("Replace EPG Image: %t\n", Settings.XepgReplaceMissingImages)
println("---") println("---")
fmt.Println("Settings [Streaming]") fmt.Println("Settings [Streaming]")
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer)) fmt.Printf("Buffer: %s\n", Settings.Buffer)
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy)) fmt.Printf("UDPxy: %s\n", Settings.UDPxy)
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize)) fmt.Printf("Buffer Size: %d KB\n", Settings.BufferSize)
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout))) fmt.Printf("Timeout: %d ms\n", int(Settings.BufferTimeout))
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent)) fmt.Printf("User Agent: %s\n", Settings.UserAgent)
println("---") println("---")
fmt.Println("Settings [Backup]") fmt.Println("Settings [Backup]")
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath)) fmt.Printf("Folder (backup): %s\n", Settings.BackupPath)
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep)) fmt.Printf("Backup Keep: %d\n", Settings.BackupKeep)
} }
+3 -96
View File
@@ -3,7 +3,6 @@ package authentication
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@@ -44,90 +43,6 @@ type Cookie struct {
RawExpires string 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 // Init : databasePath = Path to authentication.json
func Init(databasePath string, validity int) (err error) { func Init(databasePath string, validity int) (err error) {
database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile
@@ -457,7 +372,7 @@ func CheckTheValidityOfTheTokenFromHTTPHeader(w http.ResponseWriter, r *http.Req
// Framework tools // Framework tools
func checkInit() (err error) { func checkInit() (err error) {
if initAuthentication == false { if !initAuthentication {
err = createError(000) err = createError(000)
} }
@@ -472,7 +387,7 @@ func saveDatabase(tmpMap any) (err error) {
return return
} }
err = ioutil.WriteFile(database, []byte(jsonString), 0600) err = os.WriteFile(database, []byte(jsonString), 0600)
if err != nil { if err != nil {
return return
} }
@@ -481,7 +396,7 @@ func saveDatabase(tmpMap any) (err error) {
} }
func loadDatabase() (err error) { func loadDatabase() (err error) {
jsonString, err := ioutil.ReadFile(database) jsonString, err := os.ReadFile(database)
if err != nil { if err != nil {
return return
} }
@@ -587,14 +502,6 @@ loopToken:
return return
} }
func mapToJSON(tmpMap any) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return "{}"
}
return string(jsonString)
}
// SetCookieToken : set cookie // SetCookieToken : set cookie
func SetCookieToken(w http.ResponseWriter, token string) http.ResponseWriter { func SetCookieToken(w http.ResponseWriter, token string) http.ResponseWriter {
expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity)) expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity))
+3 -4
View File
@@ -3,7 +3,6 @@ package imgcache
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -51,7 +50,7 @@ func New(path, chacheURL string, caching bool) (c *Cache, err error) {
src = strings.Trim(src, "\r\n") src = strings.Trim(src, "\r\n")
if c.caching == false { if !c.caching {
return src return src
} }
@@ -143,7 +142,7 @@ func New(path, chacheURL string, caching bool) (c *Cache, err error) {
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
files, err := ioutil.ReadDir(c.path) files, err := os.ReadDir(c.path)
if err != nil { if err != nil {
return return
} }
@@ -165,7 +164,7 @@ func New(path, chacheURL string, caching bool) (c *Cache, err error) {
} }
files, err := ioutil.ReadDir(c.path) files, err := os.ReadDir(c.path)
if err != nil { if err != nil {
return return
} }
+19 -19
View File
@@ -3,18 +3,18 @@ package m3u
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os"
"testing" "testing"
) )
type M3UStream struct { type M3UStream struct {
GroupTitle string `json:"group-title,required"` GroupTitle string `json:"group-title"`
Name string `json:"name,required"` Name string `json:"name"`
TvgID string `json:"tvg-id,required"` TvgID string `json:"tvg-id"`
TvgLogo string `json:"tvg-logo,required"` TvgLogo string `json:"tvg-logo"`
TvgName string `json:"tvg-name,required"` TvgName string `json:"tvg-name"`
URL string `json:"url,required"` URL string `json:"url"`
UUIDKey string `json:"_uuid.key,omitempty"` UUIDKey string `json:"_uuid.key,omitempty"`
UUIDValue string `json:"_uuid.value,omitempty"` UUIDValue string `json:"_uuid.value,omitempty"`
} }
@@ -22,7 +22,7 @@ type M3UStream struct {
func TestStream1(t *testing.T) { func TestStream1(t *testing.T) {
var file = "test_list_1.m3u" var file = "test_list_1.m3u"
var content, err = ioutil.ReadFile(file) var content, err = os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@@ -58,26 +58,26 @@ func checkStream(streamInterface []any) (err error) {
err = json.Unmarshal(jsonString, &m3uStream) err = json.Unmarshal(jsonString, &m3uStream)
if err == nil { if err == nil {
log.Print(fmt.Sprintf("Stream: %d", i)) log.Printf("Stream: %d", i)
log.Print(fmt.Sprintf("Name*: %s", m3uStream.Name)) log.Printf("Name*: %s", m3uStream.Name)
log.Print(fmt.Sprintf("URL*: %s", m3uStream.URL)) log.Printf("URL*: %s", m3uStream.URL)
log.Print(fmt.Sprintf("tvg-name: %s", m3uStream.TvgName)) log.Printf("tvg-name: %s", m3uStream.TvgName)
log.Print(fmt.Sprintf("tvg-id**: %s", m3uStream.TvgID)) log.Printf("tvg-id**: %s", m3uStream.TvgID)
log.Print(fmt.Sprintf("tvg-logo: %s", m3uStream.TvgLogo)) log.Printf("tvg-logo: %s", m3uStream.TvgLogo)
log.Print(fmt.Sprintf("group-title**: %s", m3uStream.GroupTitle)) log.Printf("group-title**: %s", m3uStream.GroupTitle)
if len(m3uStream.UUIDKey) > 0 { if len(m3uStream.UUIDKey) > 0 {
log.Print(fmt.Sprintf("UUID key***: %s", m3uStream.UUIDKey)) log.Printf("UUID key***: %s", m3uStream.UUIDKey)
log.Print(fmt.Sprintf("UUID value: %s", m3uStream.UUIDValue)) log.Printf("UUID value: %s", m3uStream.UUIDValue)
} else { } 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 return
+1 -2
View File
@@ -2,7 +2,6 @@ package m3u
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"net/url" "net/url"
"regexp" "regexp"
@@ -124,7 +123,7 @@ func MakeInterfaceFromM3U(byteStream []byte) (allChannels []any, err error) {
if strings.Contains(strings.ToLower(key), "id") { if strings.Contains(strings.ToLower(key), "id") {
if indexOfString(value, uuids) != -1 { 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 break
} }
+4 -4
View File
@@ -114,18 +114,18 @@ func filterThisStream(s any) (status bool) {
} }
} }
if match == true { if match {
if len(exclude) > 0 { if len(exclude) > 0 {
var status = checkConditions(search, exclude, "exclude") var status = checkConditions(search, exclude, "exclude")
if status == false { if !status {
return false return false
} }
} }
if len(include) > 0 { if len(include) > 0 {
var status = checkConditions(search, include, "include") var status = checkConditions(search, include, "include")
if status == false { if !status {
return false return false
} }
} }
@@ -191,7 +191,7 @@ func buildM3U(groups []string) (m3u string, err error) {
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err == nil { if err == nil {
if xepgChannel.XActive == true { if xepgChannel.XActive {
if len(groups) > 0 { if len(groups) > 0 {
+1 -9
View File
@@ -1,15 +1,12 @@
package src package src
import ( import (
"math/rand"
"time" "time"
) )
// InitMaintenance : Wartungsprozess initialisieren // InitMaintenance : Wartungsprozess initialisieren
func InitMaintenance() (err error) { func InitMaintenance() (err error) {
rand.Seed(time.Now().Unix())
go maintenance() go maintenance()
return return
@@ -50,7 +47,7 @@ func maintenance() {
ShowError(err, 000) ShowError(err, 000)
} }
if Settings.CacheImages == false && System.ImageCachingInProgress == 0 { if !Settings.CacheImages && System.ImageCachingInProgress == 0 {
removeChildItems(System.Folder.ImagesCache) 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
}
+3
View File
@@ -131,6 +131,9 @@ func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) {
func setValueForUUID() (err error) { func setValueForUUID() (err error) {
xepg, err := loadJSONFileToMap(System.File.XEPG) xepg, err := loadJSONFileToMap(System.File.XEPG)
if err != nil {
return
}
for _, c := range xepg { for _, c := range xepg {
+18 -18
View File
@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@@ -26,7 +26,7 @@ var plexRefreshState = struct {
// queuePlexGuideRefresh schedules a debounced Plex DVR guide refresh. // queuePlexGuideRefresh schedules a debounced Plex DVR guide refresh.
func queuePlexGuideRefresh(reason string) { func queuePlexGuideRefresh(reason string) {
if Settings.UsePlexAPI == false { if !Settings.UsePlexAPI {
return return
} }
@@ -36,7 +36,7 @@ func queuePlexGuideRefresh(reason string) {
plexRefreshState.reason = reason plexRefreshState.reason = reason
} }
if plexRefreshState.scheduled == true || plexRefreshState.inProgress == true { if plexRefreshState.scheduled || plexRefreshState.inProgress {
plexRefreshState.queued = true plexRefreshState.queued = true
plexRefreshState.Unlock() plexRefreshState.Unlock()
return return
@@ -78,13 +78,13 @@ func runPlexGuideRefresh(initialDelay time.Duration) {
plexRefreshState.inProgress = false plexRefreshState.inProgress = false
nextDelay := time.Duration(0) nextDelay := time.Duration(0)
if runAgain == true && Settings.UsePlexAPI == true { if runAgain && Settings.UsePlexAPI {
plexRefreshState.scheduled = true plexRefreshState.scheduled = true
nextDelay = plexRefreshDelayLocked() nextDelay = plexRefreshDelayLocked()
} }
plexRefreshState.Unlock() plexRefreshState.Unlock()
if runAgain == false || Settings.UsePlexAPI == false { if !runAgain || !Settings.UsePlexAPI {
return return
} }
@@ -98,7 +98,7 @@ func plexRefreshDelayLocked() (delay time.Duration) {
const minDelay = 2 * time.Second const minDelay = 2 * time.Second
const cooldown = 20 * time.Second const cooldown = 20 * time.Second
if plexRefreshState.lastRun.IsZero() == true { if plexRefreshState.lastRun.IsZero() {
return minDelay return minDelay
} }
@@ -113,7 +113,7 @@ func plexRefreshDelayLocked() (delay time.Duration) {
func refreshPlexGuide(reason string) (err error) { func refreshPlexGuide(reason string) (err error) {
baseURL, token, ready := getPlexConfig() baseURL, token, ready := getPlexConfig()
if ready == false { if !ready {
return nil return nil
} }
@@ -178,7 +178,7 @@ func getPlexConfig() (baseURL, token string, ready bool) {
baseURL = strings.TrimSpace(Settings.PlexURL) baseURL = strings.TrimSpace(Settings.PlexURL)
token = strings.TrimSpace(Settings.PlexToken) 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 baseURL = "http://" + baseURL
} }
@@ -189,7 +189,7 @@ func getPlexConfig() (baseURL, token string, ready bool) {
if len(baseURL) == 0 || len(token) == 0 { 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") showInfo("Plex API:Skipped refresh because plex.url or plex.token is empty")
plexRefreshState.missingConfigLogged = true 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) mediaContainer, ok := payload["MediaContainer"].(map[string]any)
if ok == false { if !ok {
err = errors.New("Plex API response missing MediaContainer") err = errors.New("Plex API response missing MediaContainer")
return return
} }
for _, key := range []string{"Dvr", "DVR", "Directory", "Metadata"} { for _, key := range []string{"Dvr", "DVR", "Directory", "Metadata"} {
if raw, found := mediaContainer[key]; found == true { if raw, found := mediaContainer[key]; found {
if list, ok := raw.([]any); ok == true { if list, ok := raw.([]any); ok {
dvrs = convertToPlexMapSlice(list) dvrs = convertToPlexMapSlice(list)
if len(dvrs) > 0 { if len(dvrs) > 0 {
return return
@@ -246,7 +246,7 @@ func convertToPlexMapSlice(list []any) (dvrs []map[string]any) {
dvrs = make([]map[string]any, 0, len(list)) dvrs = make([]map[string]any, 0, len(list))
for _, item := range 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) dvrs = append(dvrs, dvr)
} }
} }
@@ -264,10 +264,10 @@ func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) {
if len(endpoint) == 0 { if len(endpoint) == 0 {
return return
} }
if strings.HasPrefix(endpoint, "/") == false { if !strings.HasPrefix(endpoint, "/") {
endpoint = "/" + endpoint endpoint = "/" + endpoint
} }
if added[endpoint] == true { if added[endpoint] {
return return
} }
added[endpoint] = true added[endpoint] = true
@@ -275,7 +275,7 @@ func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) {
} }
if key := getPlexMapString(dvr, "key"); len(key) > 0 { if key := getPlexMapString(dvr, "key"); len(key) > 0 {
if strings.HasPrefix(key, "/") == true { if strings.HasPrefix(key, "/") {
add(strings.TrimRight(key, "/") + "/reloadGuide") add(strings.TrimRight(key, "/") + "/reloadGuide")
} else { } else {
add("/livetv/dvrs/" + url.PathEscape(key) + "/reloadGuide") 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 { func getPlexMapString(data map[string]any, field string) string {
for key, value := range data { for key, value := range data {
if strings.EqualFold(key, field) == false { if !strings.EqualFold(key, field) {
continue continue
} }
@@ -354,7 +354,7 @@ func doPlexRequest(method, baseURL, endpoint, token string) (status int, body []
defer resp.Body.Close() defer resp.Body.Close()
status = resp.StatusCode status = resp.StatusCode
body, err = ioutil.ReadAll(resp.Body) body, err = io.ReadAll(resp.Body)
if err != nil { if err != nil {
return return
} }
+5 -5
View File
@@ -2,7 +2,7 @@ package src
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "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) // 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 { if dataID != fileID {
goto Done goto Done
} }
@@ -204,7 +204,7 @@ func getProviderData(fileType, fileID string) (err error) {
ShowError(err, 000) ShowError(err, 000)
var downloadErr = err var downloadErr = err
if newProvider == false { if !newProvider {
// Prüfen ob ältere Datei vorhanden ist // Prüfen ob ältere Datei vorhanden ist
var file = System.Folder.Data + dataID + fileExtension var file = System.Folder.Data + dataID + fileExtension
@@ -236,7 +236,7 @@ func getProviderData(fileType, fileID string) (err error) {
} }
// Berechnen der Fehlerquote // Berechnen der Fehlerquote
if newProvider == false { if !newProvider {
if value, ok := dataMap[dataID].(map[string]any); ok { 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 { if err != nil {
return return
} }
+58 -64
View File
@@ -12,7 +12,7 @@ import (
func showInfo(str string) { func showInfo(str string) {
if System.Flag.Info == true { if System.Flag.Info {
return return
} }
@@ -39,7 +39,6 @@ func showInfo(str string) {
} }
return
} }
func showDebug(str string, level int) { func showDebug(str string, level int) {
@@ -73,7 +72,6 @@ func showDebug(str string, level int) {
} }
return
} }
func showHighlight(str string) { func showHighlight(str string) {
@@ -105,7 +103,6 @@ func showHighlight(str string) {
addNotification(notification) addNotification(notification)
return
} }
func showWarning(errCode int) { func showWarning(errCode int) {
@@ -121,7 +118,6 @@ func showWarning(errCode int) {
WebScreenLog.Warnings++ WebScreenLog.Warnings++
mutex.Unlock() mutex.Unlock()
return
} }
// ShowError : Zeigt die Fehlermeldungen in der Konsole // ShowError : Zeigt die Fehlermeldungen in der Konsole
@@ -139,7 +135,6 @@ func ShowError(err error, errCode int) {
WebScreenLog.Errors++ WebScreenLog.Errors++
mutex.Unlock() mutex.Unlock()
return
} }
func printLogOnScreen(logMsg string, logType string) { func printLogOnScreen(logMsg string, logType string) {
@@ -211,7 +206,6 @@ func logCleanUp() {
WebScreenLog.Log = logs WebScreenLog.Log = logs
return
} }
// Fehlercodes // Fehlercodes
@@ -224,79 +218,79 @@ func getErrMsg(errCode int) (errMsg string) {
// Errors // Errors
case 1001: case 1001:
errMsg = fmt.Sprintf("Web server could not be started.") errMsg = "Web server could not be started."
case 1002: case 1002:
errMsg = fmt.Sprintf("No local IP address found.") errMsg = "No local IP address found."
case 1003: case 1003:
errMsg = fmt.Sprintf("Invalid xml") errMsg = "Invalid xml"
case 1004: case 1004:
errMsg = fmt.Sprintf("File not found") errMsg = "File not found"
case 1005: 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: case 1006:
errMsg = fmt.Sprintf("No playlist!") errMsg = "No playlist!"
case 1007: case 1007:
errMsg = fmt.Sprintf("XEPG requires an XMLTV file.") errMsg = "XEPG requires an XMLTV file."
case 1010: case 1010:
errMsg = fmt.Sprintf("Invalid file compression") errMsg = "Invalid file compression"
case 1011: case 1011:
errMsg = fmt.Sprintf("Data is corrupt or unavailable, %s now uses an older version of this file", System.Name) errMsg = fmt.Sprintf("Data is corrupt or unavailable, %s now uses an older version of this file", System.Name)
case 1012: case 1012:
errMsg = fmt.Sprintf("Invalid formatting of the time") errMsg = "Invalid formatting of the time"
case 1013: case 1013:
errMsg = fmt.Sprintf("Invalid settings file (settings.json), file must be at least version %s", System.Compatibility) errMsg = fmt.Sprintf("Invalid settings file (settings.json), file must be at least version %s", System.Compatibility)
case 1014: case 1014:
errMsg = fmt.Sprintf("Invalid filter rule") errMsg = "Invalid filter rule"
case 1020: case 1020:
errMsg = fmt.Sprintf("Data could not be saved, invalid keyword") errMsg = "Data could not be saved, invalid keyword"
// Datenbank Update // Datenbank Update
case 1030: case 1030:
errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings) errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings)
case 1031: 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 // M3U Parser
case 1050: case 1050:
errMsg = fmt.Sprintf("Invalid duration specification in the M3U8 playlist.") errMsg = "Invalid duration specification in the M3U8 playlist."
case 1060: 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 // Dateisystem
case 1070: case 1070:
errMsg = fmt.Sprintf("Folder could not be created.") errMsg = "Folder could not be created."
case 1071: case 1071:
errMsg = fmt.Sprintf("File could not be created") errMsg = "File could not be created"
case 1072: case 1072:
errMsg = fmt.Sprintf("File not found") errMsg = "File not found"
// Backup // Backup
case 1090: case 1090:
errMsg = fmt.Sprintf("Automatic backup failed") errMsg = "Automatic backup failed"
// Websockets // Websockets
case 1100: case 1100:
errMsg = fmt.Sprintf("WebUI build error") errMsg = "WebUI build error"
case 1101: case 1101:
errMsg = fmt.Sprintf("WebUI request error") errMsg = "WebUI request error"
case 1102: case 1102:
errMsg = fmt.Sprintf("WebUI response error") errMsg = "WebUI response error"
// PMS Guide Numbers // PMS Guide Numbers
case 1200: case 1200:
errMsg = fmt.Sprintf("Could not create file") errMsg = "Could not create file"
// Stream URL Fehler // Stream URL Fehler
case 1201: case 1201:
errMsg = fmt.Sprintf("Plex stream error") errMsg = "Plex stream error"
case 1202: 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: 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: 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 // Warnings
case 2000: case 2000:
@@ -304,86 +298,86 @@ func getErrMsg(errCode int) (errMsg string) {
case 2001: 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) 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: case 2002:
errMsg = fmt.Sprintf("PMS can not play m3u8 streams") errMsg = "PMS can not play m3u8 streams"
case 2003: case 2003:
errMsg = fmt.Sprintf("PMS can not play streams over RTSP.") errMsg = "PMS can not play streams over RTSP."
case 2004: case 2004:
errMsg = fmt.Sprintf("Buffer is disabled for this stream.") errMsg = "Buffer is disabled for this stream."
case 2005: 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: case 2010:
errMsg = fmt.Sprintf("No valid streaming URL") errMsg = "No valid streaming URL"
case 2020: 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: 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: case 2099:
errMsg = fmt.Sprintf("Updates have been disabled by the developer") errMsg = "Updates have been disabled by the developer"
// Tuner // Tuner
case 2105: case 2105:
errMsg = "The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again." errMsg = "The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again."
case 2106: 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: case 2110:
errMsg = fmt.Sprintf("Don't run this as Root!") errMsg = "Don't run this as Root!"
case 2300: 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: 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: 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 // Benutzerauthentifizierung
case 3000: case 3000:
errMsg = fmt.Sprintf("Database for user authentication could not be initialized.") errMsg = "Database for user authentication could not be initialized."
case 3001: 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 // Buffer
case 4000: case 4000:
errMsg = fmt.Sprintf("Connection to streaming source was interrupted.") errMsg = "Connection to streaming source was interrupted."
case 4001: 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: 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: case 4003:
errMsg = fmt.Sprintf("Server sends an incompatible content-type") errMsg = "Server sends an incompatible content-type"
case 4004: case 4004:
errMsg = fmt.Sprintf("This error message comes from the provider") errMsg = "This error message comes from the provider"
case 4005: case 4005:
errMsg = fmt.Sprintf("Temporary buffer files could not be deleted") errMsg = "Temporary buffer files could not be deleted"
case 4006: case 4006:
errMsg = fmt.Sprintf("Server connection timeout") errMsg = "Server connection timeout"
// Buffer (M3U8) // Buffer (M3U8)
case 4050: case 4050:
errMsg = fmt.Sprintf("Invalid M3U8 file") errMsg = "Invalid M3U8 file"
case 4051: case 4051:
errMsg = fmt.Sprintf("#EXTM3U header is missing") errMsg = "#EXTM3U header is missing"
// Caching // Caching
case 4100: case 4100:
errMsg = fmt.Sprintf("Unknown content type for downloaded image") errMsg = "Unknown content type for downloaded image"
case 4101: 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 // API
case 5000: case 5000:
errMsg = fmt.Sprintf("Invalid API command") errMsg = "Invalid API command"
// Update Server // Update Server
case 6001: case 6001:
errMsg = fmt.Sprintf("Ivalid key") errMsg = "Ivalid key"
case 6002: case 6002:
errMsg = fmt.Sprintf("Update failed") errMsg = "Update failed"
case 6003: case 6003:
errMsg = fmt.Sprintf("Update server not available") errMsg = "Update server not available"
case 6004: case 6004:
errMsg = fmt.Sprintf("xTeVe update available") errMsg = "xTeVe update available"
default: default:
errMsg = fmt.Sprintf("Unknown error / warning (%d)", errCode) errMsg = fmt.Sprintf("Unknown error / warning (%d)", errCode)
+2 -2
View File
@@ -13,7 +13,7 @@ import (
// SSDP : SSPD / DLNA Server // SSDP : SSPD / DLNA Server
func SSDP() (err error) { func SSDP() (err error) {
if Settings.SSDP == false || System.Flag.Info == true { if !Settings.SSDP || System.Flag.Info {
return return
} }
@@ -23,7 +23,7 @@ func SSDP() (err error) {
signal.Notify(quit, os.Interrupt) signal.Notify(quit, os.Interrupt)
ad, err := ssdp.Advertise( ad, err := ssdp.Advertise(
fmt.Sprintf("upnp:rootdevice"), // send as "ST" "upnp:rootdevice", // send as "ST"
fmt.Sprintf("uuid:%s::upnp:rootdevice", System.DeviceID), // send as "USN" fmt.Sprintf("uuid:%s::upnp:rootdevice", System.DeviceID), // send as "USN"
fmt.Sprintf("%s/device.xml", System.URLBase), // send as "LOCATION" fmt.Sprintf("%s/device.xml", System.URLBase), // send as "LOCATION"
System.AppName, // send as "SERVER" System.AppName, // send as "SERVER"
-144
View File
@@ -108,147 +108,3 @@ type BandwidthCalculation struct {
Stop time.Time Stop time.Time
TimeDiff float64 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)
}
}
*/
+53 -54
View File
@@ -155,81 +155,81 @@ type Filter struct {
// XEPGChannelStruct : XEPG Struktur // XEPGChannelStruct : XEPG Struktur
type XEPGChannelStruct struct { type XEPGChannelStruct struct {
FileM3UID string `json:"_file.m3u.id,required"` FileM3UID string `json:"_file.m3u.id"`
FileM3UName string `json:"_file.m3u.name,required"` FileM3UName string `json:"_file.m3u.name"`
FileM3UPath string `json:"_file.m3u.path,required"` FileM3UPath string `json:"_file.m3u.path"`
GroupTitle string `json:"group-title,required"` GroupTitle string `json:"group-title"`
Name string `json:"name,required"` Name string `json:"name"`
TvgID string `json:"tvg-id,required"` TvgID string `json:"tvg-id"`
TvgLogo string `json:"tvg-logo,required"` TvgLogo string `json:"tvg-logo"`
TvgName string `json:"tvg-name,required"` TvgName string `json:"tvg-name"`
URL string `json:"url,required"` URL string `json:"url"`
UUIDKey string `json:"_uuid.key,required"` UUIDKey string `json:"_uuid.key"`
UUIDValue string `json:"_uuid.value,omitempty"` UUIDValue string `json:"_uuid.value,omitempty"`
Values string `json:"_values,required"` Values string `json:"_values"`
XActive bool `json:"x-active,required"` XActive bool `json:"x-active"`
XCategory string `json:"x-category,required"` XCategory string `json:"x-category"`
XChannelID string `json:"x-channelID,required"` XChannelID string `json:"x-channelID"`
XEPG string `json:"x-epg,required"` XEPG string `json:"x-epg"`
XGroupTitle string `json:"x-group-title,required"` XGroupTitle string `json:"x-group-title"`
XMapping string `json:"x-mapping,required"` XMapping string `json:"x-mapping"`
XmltvFile string `json:"x-xmltv-file,required"` XmltvFile string `json:"x-xmltv-file"`
XName string `json:"x-name,required"` XName string `json:"x-name"`
XUpdateChannelIcon bool `json:"x-update-channel-icon,required"` XUpdateChannelIcon bool `json:"x-update-channel-icon"`
XUpdateChannelName bool `json:"x-update-channel-name,required"` XUpdateChannelName bool `json:"x-update-channel-name"`
XDescription string `json:"x-description,required"` XDescription string `json:"x-description"`
} }
// M3UChannelStructXEPG : M3U Struktur für XEPG // M3UChannelStructXEPG : M3U Struktur für XEPG
type M3UChannelStructXEPG struct { type M3UChannelStructXEPG struct {
FileM3UID string `json:"_file.m3u.id,required"` FileM3UID string `json:"_file.m3u.id"`
FileM3UName string `json:"_file.m3u.name,required"` FileM3UName string `json:"_file.m3u.name"`
FileM3UPath string `json:"_file.m3u.path,required"` FileM3UPath string `json:"_file.m3u.path"`
GroupTitle string `json:"group-title,required"` GroupTitle string `json:"group-title"`
Name string `json:"name,required"` Name string `json:"name"`
TvgID string `json:"tvg-id,required"` TvgID string `json:"tvg-id"`
TvgLogo string `json:"tvg-logo,required"` TvgLogo string `json:"tvg-logo"`
TvgName string `json:"tvg-name,required"` TvgName string `json:"tvg-name"`
URL string `json:"url,required"` URL string `json:"url"`
UUIDKey string `json:"_uuid.key,required"` UUIDKey string `json:"_uuid.key"`
UUIDValue string `json:"_uuid.value,required"` UUIDValue string `json:"_uuid.value"`
Values string `json:"_values,required"` Values string `json:"_values"`
} }
// FilterStruct : Filter Struktur // FilterStruct : Filter Struktur
type FilterStruct struct { type FilterStruct struct {
Active bool `json:"active,required"` Active bool `json:"active"`
CaseSensitive bool `json:"caseSensitive,required"` CaseSensitive bool `json:"caseSensitive"`
Description string `json:"description,required"` Description string `json:"description"`
Exclude string `json:"exclude,required"` Exclude string `json:"exclude"`
Filter string `json:"filter,required"` Filter string `json:"filter"`
Include string `json:"include,required"` Include string `json:"include"`
Name string `json:"name,required"` Name string `json:"name"`
Rule string `json:"rule,omitempty"` Rule string `json:"rule,omitempty"`
Type string `json:"type,required"` Type string `json:"type"`
} }
// StreamingURLS : Informationen zu allen streaming URL's // StreamingURLS : Informationen zu allen streaming URL's
type StreamingURLS struct { 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 // StreamInfo : Informationen zum Kanal für die streaming URL
type StreamInfo struct { type StreamInfo struct {
ChannelNumber string `json:"channelNumber,required"` ChannelNumber string `json:"channelNumber"`
Name string `json:"name,required"` Name string `json:"name"`
PlaylistID string `json:"playlistID,required"` PlaylistID string `json:"playlistID"`
URL string `json:"url,required"` URL string `json:"url"`
URLid string `json:"urlID,required"` URLid string `json:"urlID"`
} }
// Notification : Notifikationen im Webinterface // Notification : Notifikationen im Webinterface
type Notification struct { type Notification struct {
Headline string `json:"headline,required"` Headline string `json:"headline"`
Message string `json:"message,required"` Message string `json:"message"`
New bool `json:"new,required"` New bool `json:"new"`
Time string `json:"time,required"` Time string `json:"time"`
Type string `json:"type,required"` Type string `json:"type"`
} }
// SettingsStruct : Inhalt der settings.json // SettingsStruct : Inhalt der settings.json
@@ -283,4 +283,3 @@ type SettingsStruct struct {
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"` XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
WizardCompleted bool `json:"wizard.completed"` WizardCompleted bool `json:"wizard.completed"`
} }
+20 -20
View File
@@ -3,7 +3,7 @@ package src
// RequestStruct : Anfragen über die Websocket Schnittstelle // RequestStruct : Anfragen über die Websocket Schnittstelle
type RequestStruct struct { type RequestStruct struct {
// Befehle an xTeVe // Befehle an xTeVe
Cmd string `json:"cmd,required"` Cmd string `json:"cmd"`
// Benutzer // Benutzer
DeleteUser bool `json:"deleteUser,omitempty"` DeleteUser bool `json:"deleteUser,omitempty"`
@@ -79,46 +79,46 @@ type ResponseStruct struct {
DVR string `json:"DVR"` DVR string `json:"DVR"`
EpgSource string `json:"epgSource"` EpgSource string `json:"epgSource"`
Errors int `json:"errors"` Errors int `json:"errors"`
M3U string `json:"m3u-url,required"` M3U string `json:"m3u-url"`
OS string `json:"os"` OS string `json:"os"`
Streams string `json:"streams"` Streams string `json:"streams"`
UUID string `json:"uuid"` UUID string `json:"uuid"`
Version string `json:"version"` Version string `json:"version"`
Warnings int `json:"warnings"` Warnings int `json:"warnings"`
XEPGCount int64 `json:"xepg"` XEPGCount int64 `json:"xepg"`
XML string `json:"xepg-url,required"` XML string `json:"xepg-url"`
} `json:"clientInfo"` } `json:"clientInfo"`
Data struct { Data struct {
Playlist struct { Playlist struct {
M3U struct { M3U struct {
Groups struct { Groups struct {
Text []string `json:"text,required"` Text []string `json:"text"`
Value []string `json:"value,required"` Value []string `json:"value"`
} `json:"groups,required"` } `json:"groups"`
} `json:"m3u,required"` } `json:"m3u"`
} `json:"playlist,required"` } `json:"playlist"`
StreamPreviewUI struct { StreamPreviewUI struct {
Active []string `json:"activeStreams,required"` Active []string `json:"activeStreams"`
Inactive []string `json:"inactiveStreams,required"` Inactive []string `json:"inactiveStreams"`
} }
} `json:"data,required"` } `json:"data"`
Alert string `json:"alert,omitempty"` Alert string `json:"alert,omitempty"`
ConfigurationWizard bool `json:"configurationWizard,required"` ConfigurationWizard bool `json:"configurationWizard"`
Error string `json:"err,omitempty"` Error string `json:"err,omitempty"`
Log WebScreenLogStruct `json:"log,required"` Log WebScreenLogStruct `json:"log"`
LogoURL string `json:"logoURL,omitempty"` LogoURL string `json:"logoURL,omitempty"`
OpenLink string `json:"openLink,omitempty"` OpenLink string `json:"openLink,omitempty"`
OpenMenu string `json:"openMenu,omitempty"` OpenMenu string `json:"openMenu,omitempty"`
Reload bool `json:"reload,omitempty"` Reload bool `json:"reload,omitempty"`
Settings SettingsStruct `json:"settings,required"` Settings SettingsStruct `json:"settings"`
Status bool `json:"status,required"` Status bool `json:"status"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
Users map[string]any `json:"users,omitempty"` Users map[string]any `json:"users,omitempty"`
Wizard int `json:"wizard,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"` Notification map[string]Notification `json:"notification,omitempty"`
} }
@@ -135,7 +135,7 @@ type APIRequestStruct struct {
type APIResponseStruct struct { type APIResponseStruct struct {
EpgSource string `json:"epg.source,omitempty"` EpgSource string `json:"epg.source,omitempty"`
Error string `json:"err,omitempty"` Error string `json:"err,omitempty"`
Status bool `json:"status,required"` Status bool `json:"status"`
StreamsActive int64 `json:"streams.active,omitempty"` StreamsActive int64 `json:"streams.active,omitempty"`
StreamsAll int64 `json:"streams.all,omitempty"` StreamsAll int64 `json:"streams.all,omitempty"`
StreamsXepg int64 `json:"streams.xepg,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 // WebScreenLogStruct : Logs werden im RAM gespeichert und für das Webinterface bereitgestellt
type WebScreenLogStruct struct { type WebScreenLogStruct struct {
Errors int `json:"errors,required"` Errors int `json:"errors"`
Log []string `json:"log,required"` Log []string `json:"log"`
Warnings int `json:"warnings,required"` Warnings int `json:"warnings"`
} }
+1 -1
View File
@@ -66,7 +66,7 @@ type SubTitle struct {
Value string `xml:",chardata"` Value string `xml:",chardata"`
} }
//Desc : Programmbeschreibung // Desc : Programmbeschreibung
type Desc struct { type Desc struct {
Lang string `xml:"lang,attr"` Lang string `xml:"lang,attr"`
Value string `xml:",chardata"` Value string `xml:",chardata"`
+2 -5
View File
@@ -13,7 +13,7 @@ import (
// Entwicklerinfos anzeigen // Entwicklerinfos anzeigen
func showDevInfo() { func showDevInfo() {
if System.Dev == true { if System.Dev {
fmt.Print("\033[31m") fmt.Print("\033[31m")
fmt.Println("* * * * * D E V M O D E * * * * *") fmt.Println("* * * * * D E V M O D E * * * * *")
@@ -25,7 +25,6 @@ func showDevInfo() {
} }
return
} }
// Alle Systemordner erstellen // Alle Systemordner erstellen
@@ -261,7 +260,7 @@ func saveSettings(settings SettingsStruct) (err error) {
Settings = settings Settings = settings
if System.Dev == true { if System.Dev {
Settings.UUID = "2019-01-DEV-xTeVe!" Settings.UUID = "2019-01-DEV-xTeVe!"
} }
@@ -301,7 +300,6 @@ func setGlobalDomain(domain string) {
System.Addresses.XML = getErrMsg(2106) System.Addresses.XML = getErrMsg(2106)
} }
return
} }
// UUID generieren // UUID generieren
@@ -323,7 +321,6 @@ func setDeviceID() {
System.DeviceID = fmt.Sprintf("%s:%d", id, Settings.Tuner) System.DeviceID = fmt.Sprintf("%s:%d", id, Settings.Tuner)
} }
return
} }
// Provider Streaming-URL zu xTeVe Streaming-URL konvertieren // Provider Streaming-URL zu xTeVe Streaming-URL konvertieren
+14 -39
View File
@@ -7,7 +7,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"net" "net"
"os" "os"
"os/exec" "os/exec"
@@ -59,11 +59,8 @@ func checkFile(filename string) (err error) {
return err return err
} }
switch mode := fi.Mode(); { if fi.Mode().IsDir() {
case mode.IsDir():
err = fmt.Errorf("%s: %s", file, getErrMsg(1072)) err = fmt.Errorf("%s: %s", file, getErrMsg(1072))
case mode.IsRegular():
break
} }
return return
@@ -97,7 +94,7 @@ func checkFilePermission(dir string) (err error) {
var filename = dir + "permission.test" var filename = dir + "permission.test"
err = ioutil.WriteFile(filename, []byte(""), 0644) err = os.WriteFile(filename, []byte(""), 0644)
if err == nil { if err == nil {
err = os.RemoveAll(filename) err = os.RemoveAll(filename)
} }
@@ -125,12 +122,6 @@ func getFilenameFromPath(path string) (file string) {
return filepath.Base(path) 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 // Sucht eine Datei im OS
func searchFileInOS(file string) (path string) { func searchFileInOS(file string) (path string) {
@@ -197,14 +188,6 @@ func jsonToMap(content string) map[string]any {
return (tmpMap) 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) { func jsonToInterface(content string) (tmpMap any, err error) {
err = json.Unmarshal([]byte(content), &tmpMap) err = json.Unmarshal([]byte(content), &tmpMap)
@@ -221,7 +204,7 @@ func saveMapToJSONFile(file string, tmpMap any) error {
return err return err
} }
err = ioutil.WriteFile(filename, []byte(jsonString), 0644) err = os.WriteFile(filename, []byte(jsonString), 0644)
if err != nil { if err != nil {
return err return err
} }
@@ -232,16 +215,17 @@ func saveMapToJSONFile(file string, tmpMap any) error {
func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) { func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) {
f, err := os.Open(getPlatformFile(file)) f, err := os.Open(getPlatformFile(file))
if err != nil {
return
}
defer f.Close() defer f.Close()
content, err := ioutil.ReadAll(f) content, err := io.ReadAll(f)
if err == nil { if err == nil {
err = json.Unmarshal([]byte(content), &tmpMap) err = json.Unmarshal([]byte(content), &tmpMap)
} }
f.Close()
return return
} }
@@ -249,10 +233,12 @@ func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) {
func readByteFromFile(file string) (content []byte, err error) { func readByteFromFile(file string) (content []byte, err error) {
f, err := os.Open(getPlatformFile(file)) f, err := os.Open(getPlatformFile(file))
if err != nil {
return
}
defer f.Close() defer f.Close()
content, err = ioutil.ReadAll(f) content, err = io.ReadAll(f)
f.Close()
return return
} }
@@ -260,7 +246,7 @@ func readByteFromFile(file string) (content []byte, err error) {
func writeByteToFile(file string, data []byte) (err error) { func writeByteToFile(file string, data []byte) (err error) {
var filename = getPlatformFile(file) var filename = getPlatformFile(file)
err = ioutil.WriteFile(filename, data, 0644) err = os.WriteFile(filename, data, 0644)
return return
} }
@@ -275,7 +261,7 @@ func readStringFromFile(file string) (str string, err error) {
return return
} }
content, err = ioutil.ReadFile(filename) content, err = os.ReadFile(filename)
if err != nil { if err != nil {
ShowError(err, 0) ShowError(err, 0)
return return
@@ -395,17 +381,6 @@ func indexOfFloat64(element float64, data []float64) int {
return -1 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 { func getMD5(str string) string {
md5Hasher := md5.New() md5Hasher := md5.New()
+20 -57
View File
@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
@@ -40,8 +40,6 @@ func StartWebserver() (err error) {
http.HandleFunc("/data_images/", DataImages) http.HandleFunc("/data_images/", DataImages)
http.HandleFunc("/healthz", Healthz) http.HandleFunc("/healthz", Healthz)
//http.HandleFunc("/auto/", Auto)
showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port) showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port)
var ips = len(System.IPAddressesV4) + len(System.IPAddressesV6) - 1 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") w.Header().Set("Content-Type", "application/json")
case "/lineup.json": case "/lineup.json":
if Settings.AuthenticationPMS == true { if Settings.AuthenticationPMS {
_, err := basicAuth(r, "authentication.pms") _, err := basicAuth(r, "authentication.pms")
if err != nil { if err != nil {
@@ -123,7 +121,6 @@ func Index(w http.ResponseWriter, r *http.Request) {
httpStatusError(w, r, 500) httpStatusError(w, r, 500)
return
} }
// Stream : Web Server /stream/ // Stream : Web Server /stream/
@@ -151,12 +148,12 @@ func Stream(w http.ResponseWriter, r *http.Request) {
showInfo(fmt.Sprintf("Buffer:false [%s]", Settings.Buffer)) showInfo(fmt.Sprintf("Buffer:false [%s]", Settings.Buffer))
case "xteve": 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") err = errors.New("RTSP and RTP streams are not supported")
ShowError(err, 2004) ShowError(err, 2004)
showInfo("Streaming URL:" + streamInfo.URL) 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:URL was passed to the client")
return return
@@ -181,7 +178,7 @@ func Stream(w http.ResponseWriter, r *http.Request) {
case "-": case "-":
showInfo("Streaming URL:" + streamInfo.URL) 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:URL was passed to the client.")
showInfo("Streaming Info:xTeVe is no longer involved, the client connects directly to the streaming server.") 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/ // xTeVe : Web Server /xmltv/ und /m3u/
@@ -250,7 +221,7 @@ func xTeVe(w http.ResponseWriter, r *http.Request) {
requestType = "m3u" requestType = "m3u"
groupTitle = r.URL.Query().Get("group-title") groupTitle = r.URL.Query().Get("group-title")
if System.Dev == false { if !System.Dev {
// false: Dateiname wird im Header gesetzt // false: Dateiname wird im Header gesetzt
// true: M3U wird direkt im Browser angezeigt // true: M3U wird direkt im Browser angezeigt
w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(path)) 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)) w.Write([]byte(content))
} }
return
} }
// Images : Image Cache /images/ // Images : Image Cache /images/
@@ -306,7 +276,6 @@ func Images(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
w.Write(content) w.Write(content)
return
} }
// DataImages : Image Pfad für Logos / Bilder die hochgeladen wurden /data_images/ // 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.WriteHeader(200)
w.Write(content) w.Write(content)
return
} }
// WS : Web Sockets /ws/ // WS : Web Sockets /ws/
@@ -362,7 +330,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
return return
} }
if System.ConfigurationWizard == false { if !System.ConfigurationWizard {
switch Settings.AuthenticationWEB { switch Settings.AuthenticationWEB {
@@ -425,7 +393,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
response.OpenMenu = strconv.Itoa(indexOfString("settings", System.WEB.Menu)) response.OpenMenu = strconv.Itoa(indexOfString("settings", System.WEB.Menu))
if Settings.AuthenticationWEB == true && authenticationUpdate == false { if Settings.AuthenticationWEB && !authenticationUpdate {
response.Reload = true response.Reload = true
} }
@@ -568,7 +536,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
var requestMap = make(map[string]any) // Debug var requestMap = make(map[string]any) // Debug
_ = requestMap _ = requestMap
if System.Dev == true { if System.Dev {
fmt.Println(mapToJSON(requestMap)) fmt.Println(mapToJSON(requestMap))
} }
@@ -581,7 +549,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
} }
response = setDefaultResponseData(response, true) response = setDefaultResponseData(response, true)
if System.ConfigurationWizard == true { if System.ConfigurationWizard {
response.ConfigurationWizard = 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. // 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 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 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. // Redirect, damit die Daten aus dem Browser gelöscht werden.
w = authentication.SetCookieToken(w, token) w = authentication.SetCookieToken(w, token)
http.Redirect(w, r, "/web", 301) http.Redirect(w, r, "/web", http.StatusMovedPermanently)
return return
} }
@@ -691,11 +658,11 @@ func Web(w http.ResponseWriter, r *http.Request) {
} }
w = authentication.SetCookieToken(w, token) 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 { } else {
w = authentication.SetCookieToken(w, "-") 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 return
@@ -723,7 +690,7 @@ func Web(w http.ResponseWriter, r *http.Request) {
return return
} }
if len(allUserData) == 0 && Settings.AuthenticationWEB == true { if len(allUserData) == 0 && Settings.AuthenticationWEB {
file = "create-first-user.html" file = "create-first-user.html"
} }
@@ -828,13 +795,12 @@ func API(w http.ResponseWriter, r *http.Request) {
response.Status = false response.Status = false
response.Error = err.Error() response.Error = err.Error()
w.Write([]byte(mapToJSON(response))) w.Write([]byte(mapToJSON(response)))
return
} }
response.Status = true response.Status = true
if Settings.API == false { if !Settings.API {
httpStatusError(w, r, 423) httpStatusError(w, r, 423)
return return
} }
@@ -844,7 +810,7 @@ func API(w http.ResponseWriter, r *http.Request) {
return return
} }
b, err := ioutil.ReadAll(r.Body) b, err := io.ReadAll(r.Body)
defer r.Body.Close() defer r.Body.Close()
if err != nil { if err != nil {
httpStatusError(w, r, 400) httpStatusError(w, r, 400)
@@ -860,7 +826,7 @@ func API(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
if Settings.AuthenticationAPI == true { if Settings.AuthenticationAPI {
var token string var token string
switch len(request.Token) { switch len(request.Token) {
case 0: case 0:
@@ -957,7 +923,6 @@ func API(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(mapToJSON(response))) w.Write([]byte(mapToJSON(response)))
return
} }
// Download : Datei Download // Download : Datei Download
@@ -975,7 +940,6 @@ func Download(w http.ResponseWriter, r *http.Request) {
os.RemoveAll(System.Folder.Temp + getFilenameFromPath(path)) os.RemoveAll(System.Folder.Temp + getFilenameFromPath(path))
w.Write([]byte(content)) w.Write([]byte(content))
return
} }
func setDefaultResponseData(response ResponseStruct, data bool) (defaults ResponseStruct) { func setDefaultResponseData(response ResponseStruct, data bool) (defaults ResponseStruct) {
@@ -999,7 +963,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
switch System.Branch { switch System.Branch {
case "master": case "master":
defaults.ClientInfo.Version = fmt.Sprintf("%s", System.Version) defaults.ClientInfo.Version = System.Version
default: default:
defaults.ClientInfo.Version = fmt.Sprintf("%s (%s)", System.Version, System.Build) 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.Users, _ = authentication.GetAllUserData()
//defaults.DVR = System.DVRAddress //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) { func httpStatusError(w http.ResponseWriter, r *http.Request, httpStatusCode int) {
http.Error(w, fmt.Sprintf("%s [%d]", http.StatusText(httpStatusCode), httpStatusCode), httpStatusCode) http.Error(w, fmt.Sprintf("%s [%d]", http.StatusText(httpStatusCode), httpStatusCode), httpStatusCode)
return
} }
func getContentType(filename string) (contentType string) { func getContentType(filename string) (contentType string) {
+39 -138
View File
@@ -5,7 +5,7 @@ import (
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "os"
"path" "path"
"runtime" "runtime"
"sort" "sort"
@@ -71,28 +71,9 @@ func buildXEPG(background bool) {
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg rebuild") queuePlexGuideRefresh("xepg rebuild")
showInfo("XEPG:" + fmt.Sprintf("Ready to use")) showInfo("XEPG:" + "Ready to use")
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
}()
}
System.ScanInProgress = 0 System.ScanInProgress = 0
@@ -118,28 +99,9 @@ func buildXEPG(background bool) {
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg rebuild") queuePlexGuideRefresh("xepg rebuild")
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { cacheImagesInBackground()
go func() { showInfo("XEPG:" + "Ready to use")
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"))
System.ScanInProgress = 0 System.ScanInProgress = 0
@@ -161,52 +123,30 @@ func buildXEPG(background bool) {
} }
// XEPG Daten aktualisieren // cacheImagesInBackground : Bilder im Hintergrund cachen, danach XMLTV- und M3U-Datei neu erstellen
func updateXEPG(background bool) { func cacheImagesInBackground() {
if System.ScanInProgress == 1 { if !Settings.CacheImages || System.ImageCachingInProgress != 0 {
return return
} }
System.ScanInProgress = 1
if Settings.EpgSource == "XEPG" {
switch background {
case false:
createXEPGDatabase()
mapping()
cleanupXEPG()
go func() { 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() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg update") queuePlexGuideRefresh("xepg image cache refresh")
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0 System.ImageCachingInProgress = 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 // Mapping Menü für die XMLTV Dateien erstellen
@@ -219,7 +159,6 @@ func createXEPGMapping() {
var friendlyDisplayName = func(channel Channel) (displayName string) { var friendlyDisplayName = func(channel Channel) (displayName string) {
var dn = channel.DisplayName var dn = channel.DisplayName
displayName = dn[0].Value
switch len(dn) { switch len(dn) {
case 1: case 1:
@@ -277,11 +216,10 @@ func createXEPGMapping() {
} }
Data.XMLTV.Mapping = tmpMap Data.XMLTV.Mapping = tmpMap
tmpMap = make(map[string]any)
} else { } else {
if System.ConfigurationWizard == false { if !System.ConfigurationWizard {
showWarning(1007) showWarning(1007)
} }
@@ -304,7 +242,6 @@ func createXEPGMapping() {
Data.XMLTV.Mapping["xTeVe Dummy"] = dummy Data.XMLTV.Mapping["xTeVe Dummy"] = dummy
return
} }
// XEPG Datenbank erstellen / aktualisieren // XEPG Datenbank erstellen / aktualisieren
@@ -339,7 +276,7 @@ func createXEPGDatabase() (err error) {
sort.Float64s(allChannelNumbers) sort.Float64s(allChannelNumbers)
var firstFreeNumber float64 = Settings.MappingFirstChannel var firstFreeNumber = Settings.MappingFirstChannel
for { for {
@@ -469,14 +406,14 @@ func createXEPGDatabase() (err error) {
xepgChannel.Name = m3uChannel.Name xepgChannel.Name = m3uChannel.Name
// Kanalname aktualisieren, nur mit Kanal ID's möglich // Kanalname aktualisieren, nur mit Kanal ID's möglich
if channelHasUUID == true { if channelHasUUID {
if xepgChannel.XUpdateChannelName == true { if xepgChannel.XUpdateChannelName {
xepgChannel.XName = m3uChannel.Name xepgChannel.XName = m3uChannel.Name
} }
} }
// Kanallogo aktualisieren. Wird bei vorhandenem Logo in der XMLTV Datei wieder überschrieben // Kanallogo aktualisieren. Wird bei vorhandenem Logo in der XMLTV Datei wieder überschrieben
if xepgChannel.XUpdateChannelIcon == true { if xepgChannel.XUpdateChannelIcon {
xepgChannel.TvgLogo = m3uChannel.TvgLogo xepgChannel.TvgLogo = m3uChannel.TvgLogo
} }
@@ -602,7 +539,7 @@ func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGCh
for id, data := range xmltvChannels { for id, data := range xmltvChannels {
xmltvChannel, castOK := data.(map[string]any) xmltvChannel, castOK := data.(map[string]any)
if castOK == false { if !castOK {
continue continue
} }
@@ -615,7 +552,7 @@ func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGCh
} }
displayName, hasDisplayName := xmltvChannel["display-name"].(string) displayName, hasDisplayName := xmltvChannel["display-name"].(string)
if hasDisplayName == false { if !hasDisplayName {
continue 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. // 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 // Werte kann "-" sein, deswegen len < 1
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XMapping) < 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. // Überprüfen, ob die zugeordneten XMLTV Dateien und Kanäle noch existieren.
if xepgChannel.XActive == true { if xepgChannel.XActive {
var mapping = xepgChannel.XMapping var mapping = xepgChannel.XMapping
var file = xepgChannel.XmltvFile var file = xepgChannel.XmltvFile
@@ -718,7 +655,7 @@ func mapping() (err error) {
// Kanallogo aktualisieren // Kanallogo aktualisieren
if logo, ok := channel["icon"].(string); ok { if logo, ok := channel["icon"].(string); ok {
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 { if xepgChannel.XUpdateChannelIcon && len(logo) > 0 {
xepgChannel.TvgLogo = logo xepgChannel.TvgLogo = logo
} }
@@ -729,7 +666,7 @@ func mapping() (err error) {
xepgChannel.XMapping = channelID xepgChannel.XMapping = channelID
if logo, ok := replacementChannel["icon"].(string); ok { if logo, ok := replacementChannel["icon"].(string); ok {
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 { if xepgChannel.XUpdateChannelIcon && len(logo) > 0 {
xepgChannel.TvgLogo = logo xepgChannel.TvgLogo = logo
} }
} }
@@ -752,7 +689,7 @@ func mapping() (err error) {
name = xepg name = xepg
} }
if strictMissingEPGMode == true { if strictMissingEPGMode {
missingEPGCount++ missingEPGCount++
appendXEPGIssueSample(missingEPGSamples, name) appendXEPGIssueSample(missingEPGSamples, name)
xepgChannel.XActive = false xepgChannel.XActive = false
@@ -772,7 +709,7 @@ func mapping() (err error) {
providerName = file providerName = file
} }
if strictMissingEPGMode == true { if strictMissingEPGMode {
missingXMLTVCount++ missingXMLTVCount++
appendXEPGIssueSample(missingXMLTVSamples, providerName) appendXEPGIssueSample(missingXMLTVSamples, providerName)
xepgChannel.XActive = false xepgChannel.XActive = false
@@ -849,7 +786,7 @@ func createXMLTVFile() (err error) {
Data.Cache.ImagesURLS = []string{} Data.Cache.ImagesURLS = []string{}
Data.Cache.ImagesCache = []string{} Data.Cache.ImagesCache = []string{}
files, err := ioutil.ReadDir(System.Folder.ImagesCache) files, err := os.ReadDir(System.Folder.ImagesCache)
if err == nil { if err == nil {
for _, file := range files { for _, file := range files {
@@ -887,7 +824,7 @@ func createXMLTVFile() (err error) {
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err == nil { if err == nil {
if xepgChannel.XActive == true { if xepgChannel.XActive {
// Kanäle // Kanäle
var channel Channel var channel Channel
@@ -902,9 +839,7 @@ func createXMLTVFile() (err error) {
*tmpProgram, err = getProgramData(xepgChannel) *tmpProgram, err = getProgramData(xepgChannel)
if err == nil { if err == nil {
for _, program := range tmpProgram.Program { xepgXML.Program = append(xepgXML.Program, tmpProgram.Program...)
xepgXML.Program = append(xepgXML.Program, program)
}
} }
@@ -947,7 +882,7 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
err = getLocalXMLTV(xmltvFile, &xmltv) err = getLocalXMLTV(xmltvFile, &xmltv)
if err != nil { if err != nil {
if relaxedMissingEPGMode == true { if relaxedMissingEPGMode {
fallbackToDummy() fallbackToDummy()
err = nil 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() fallbackToDummy()
} }
@@ -1072,7 +1007,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
epg.Desc = append(epg.Desc, &Desc{Value: xepgChannel.XDescription, Lang: "en"}) 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) poster.Src = imgc.Image.GetURL(xepgChannel.TvgLogo)
epg.Poster = append(epg.Poster, poster) 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 // 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) program.Poster = append(program.Poster, poster)
} }
if Settings.XepgReplaceMissingImages == true { if Settings.XepgReplaceMissingImages {
if len(xmltvProgram.Poster) == 0 { if len(xmltvProgram.Poster) == 0 {
var poster Poster var poster Poster
@@ -1161,7 +1095,6 @@ func getEpisodeNum(program *Program, xmltvProgram *Program, xepgChannel XEPGChan
} }
return
} }
// Videoparameter erstellen (createXMLTVFile) // Videoparameter erstellen (createXMLTVFile)
@@ -1187,7 +1120,6 @@ func getVideo(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelSt
program.Video = video program.Video = video
return
} }
// Lokale Provider XMLTV Datei laden // Lokale Provider XMLTV Datei laden
@@ -1236,7 +1168,6 @@ func createM3UFile() {
saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS) saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS)
return
} }
// XEPG Datenbank bereinigen // XEPG Datenbank bereinigen
@@ -1254,7 +1185,7 @@ func cleanupXEPG() {
sourceIDs = append(sourceIDs, source) sourceIDs = append(sourceIDs, source)
} }
showInfo("XEPG:" + fmt.Sprintf("Cleanup database")) showInfo("XEPG:" + "Cleanup database")
Data.XEPG.XEPGCount = 0 Data.XEPG.XEPGCount = 0
for id, dxc := range Data.XEPG.Channels { for id, dxc := range Data.XEPG.Channels {
@@ -1266,7 +1197,7 @@ func cleanupXEPG() {
if indexOfString(xepgChannel.Name+xepgChannel.FileM3UID, Data.Cache.Streams.Active) == -1 { if indexOfString(xepgChannel.Name+xepgChannel.FileM3UID, Data.Cache.Streams.Active) == -1 {
delete(Data.XEPG.Channels, id) delete(Data.XEPG.Channels, id)
} else { } else {
if xepgChannel.XActive == true { if xepgChannel.XActive {
Data.XEPG.XEPGCount++ Data.XEPG.XEPGCount++
} }
} }
@@ -1291,34 +1222,4 @@ func cleanupXEPG() {
showWarning(2005) 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
} }
+4
View File
@@ -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"]
+5 -5
View File
@@ -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] `//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] 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 - [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) - [ ] 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 - [x] `/healthz` endpoint; Dockerfile healthcheck uses it
- [ ] `README-DEV.md` (done) and fork section in `README.md` - [x] `README-DEV.md` and fork-specific `README.md` (about, container usage, env vars, security notes)
- [ ] Deduplicate and delete dead code; drop `ioutil`, `osext`, `rand.Seed` (after go:embed) - [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` - [ ] 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 ## Phase 4: Data model and performance
Not planned (lineup is ~170 channels, decided 2026-09-25). See plan for the reference list. Not planned (lineup is ~170 channels, decided 2026-09-25). See plan for the reference list.