Phase 3: Go hygiene pass, runtime PUID/PGID, fork README
continuous-integration/drone/push Build encountered an error
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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||

|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user