Compare commits
143
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d385e68b31 | ||
|
|
e1a6956657 | ||
|
|
6a23996c38 | ||
|
|
0b26aa2029 | ||
|
|
014a3b04d2 | ||
|
|
3532679235 | ||
|
|
0b7a54d7da | ||
|
|
3a3ba861dc | ||
|
|
cbc5bc5da2 | ||
|
|
3bbea8e952 | ||
|
|
320eaa1b28 | ||
|
|
36303fecea | ||
|
|
4976219857 | ||
|
|
504ea3f9f4 | ||
|
|
ddc70e170a | ||
|
|
51c7830067 | ||
|
|
f558a855ae | ||
|
|
125b0bb35f | ||
|
|
32c3d779c0 | ||
|
|
ce5b12d8b8 | ||
|
|
76183bfaa2 | ||
|
|
c577d354e7 | ||
|
|
e48a061ca0 | ||
|
|
ffd43d5217 | ||
|
|
9bd2b32003 | ||
|
|
8698983bbf | ||
|
|
60af423335 | ||
|
|
57b6be74e2 | ||
|
|
c5545cbf08 | ||
|
|
339d2d0aa5 | ||
|
|
a04b0ede50 | ||
|
|
84de46a2f2 | ||
|
|
5eaf5efdb6 | ||
|
|
fb62353de4 | ||
|
|
fa9d41e10c | ||
|
|
e07d4dd878 | ||
|
|
6053207034 | ||
|
|
6a8b4bed28 | ||
|
|
45685ce592 | ||
|
|
43a9cf5a7e | ||
|
|
a23cc7a183 | ||
|
|
b069d5bee8 | ||
|
|
8cb9e43a72 | ||
|
|
0e999b85b9 | ||
|
|
e4b3667fe0 | ||
|
|
e44eff0645 | ||
|
|
884b427754 | ||
|
|
25bad13800 | ||
|
|
d2456b0506 | ||
|
|
19b9a259b1 | ||
|
|
441cefa3a4 | ||
|
|
014f5b7218 | ||
|
|
0310b7e738 | ||
|
|
6d890cfd33 | ||
|
|
410cc3648f | ||
|
|
07f9cac3c5 | ||
|
|
f43ce0f7c5 | ||
|
|
db59f7ef37 | ||
|
|
b407ec5bf0 | ||
|
|
d8fc1aea97 | ||
|
|
38333d65cb | ||
|
|
a6a9b90937 | ||
|
|
2b3fe6a09d | ||
|
|
a09eca59a7 | ||
|
|
0df3b4d755 | ||
|
|
5552514a1f | ||
|
|
5fceb1d34f | ||
|
|
fb5d0a3904 | ||
|
|
28fe4dcf1c | ||
|
|
bc307a7cd4 | ||
|
|
fd3024d7ff | ||
|
|
d3f6725ba6 | ||
|
|
c266012db3 | ||
|
|
d602b60710 | ||
|
|
67b7ba6df9 | ||
|
|
71dfe91272 | ||
|
|
534510a4ec | ||
|
|
2d10fc9313 | ||
|
|
a63a9c0d8f | ||
|
|
dd911d6e5d | ||
|
|
c1970a8393 | ||
|
|
493d612d52 | ||
|
|
4fc4330a94 | ||
|
|
a683533824 | ||
|
|
4b9f5826cf | ||
|
|
ca49d70910 | ||
|
|
1b425018d4 | ||
|
|
87b36c283b | ||
|
|
6da26ff4fb | ||
|
|
cd08985e79 | ||
|
|
dc04519229 | ||
|
|
c4ad96b715 | ||
|
|
45b5e602bb | ||
|
|
1cefbf022d | ||
|
|
d5328f6b1a | ||
|
|
91b80bc8bb | ||
|
|
aa763726a3 | ||
|
|
66c01dd1fb | ||
|
|
03e1abbe90 | ||
|
|
469581e280 | ||
|
|
019c98996a | ||
|
|
36db927794 | ||
|
|
f0a49788cc | ||
|
|
72767d7dbd | ||
|
|
8eecbf2b78 | ||
|
|
1a1e37fe15 | ||
|
|
08f6fb60e3 | ||
|
|
eded490ac7 | ||
|
|
ed770b9dbc | ||
|
|
6129b4911a | ||
|
|
477c5f30c1 | ||
|
|
65ddc6f301 | ||
|
|
3ef95c1950 | ||
|
|
3a3798cd2d | ||
|
|
72eb7fb599 | ||
|
|
4b969b8cee | ||
|
|
3d9266dabe | ||
|
|
48218cda50 | ||
|
|
dc42afcd05 | ||
|
|
20e5e1b545 | ||
|
|
11dd830110 | ||
|
|
7c87d1d5bd | ||
|
|
6cdd44357b | ||
|
|
ad992eb615 | ||
|
|
11453c6053 | ||
|
|
81e8ae33d7 | ||
|
|
f3be0fca47 | ||
|
|
1c1c89cd74 | ||
|
|
3d73dba422 | ||
|
|
c6e74fe11c | ||
|
|
18dba46c02 | ||
|
|
efa55b39a9 | ||
|
|
717fa68b7e | ||
|
|
878531ff79 | ||
|
|
792fd9a373 | ||
|
|
a1ec0287ef | ||
|
|
a79e824ef8 | ||
|
|
c843f424fe | ||
|
|
5c6637c048 | ||
|
|
1062e072d6 | ||
|
|
d831a099f0 | ||
|
|
a06baef4d3 | ||
|
|
f9d1a45bbd |
No files matched your search
@@ -0,0 +1,25 @@
|
||||
.git
|
||||
.github
|
||||
.DS_Store
|
||||
|
||||
docker-data
|
||||
|
||||
README-DEV.md
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
.gocache/
|
||||
ts/
|
||||
tasks/
|
||||
agent.md
|
||||
skill.md
|
||||
*.md
|
||||
!changelog-beta.md
|
||||
|
||||
# Web UI toolchain. The compiled bundle is committed under html/js and is
|
||||
# embedded by the Go build, so the TypeScript sources and npm tree are not
|
||||
# needed in the build context. html/ itself must stay in the context.
|
||||
node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: go-test
|
||||
image: cache.coadcorp.com/library/golang:1.27.1
|
||||
commands:
|
||||
- go mod download
|
||||
- go vet ./...
|
||||
- 'test -z "$(gofmt -l .)" || (echo "gofmt needed" && gofmt -l . && exit 1)'
|
||||
- go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...
|
||||
- go test ./...
|
||||
|
||||
- name: webui-check
|
||||
image: cache.coadcorp.com/library/node:22-alpine
|
||||
commands:
|
||||
- cp html/js/app.js /tmp/app.js.committed
|
||||
- npm ci --no-audit --no-fund
|
||||
- npm run build
|
||||
- npm test
|
||||
- cmp /tmp/app.js.committed html/js/app.js || (echo "html/js/app.js is stale - run 'npm run build' and commit the result" && exit 1)
|
||||
|
||||
- name: go-build
|
||||
image: cache.coadcorp.com/library/golang:1.27.1
|
||||
commands:
|
||||
- XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed 's/-beta$//')"
|
||||
- test -n "$XTEVE_VERSION" || (echo "Could not parse version from changelog-beta.md" && exit 1)
|
||||
- SRC_VERSION="$(grep -m1 '^var Version = ' xteve.go | cut -d'"' -f2)"
|
||||
- 'test "$SRC_VERSION" = "$XTEVE_VERSION" || (echo "Version drift - xteve.go has $SRC_VERSION, changelog-beta.md has $XTEVE_VERSION" && exit 1)'
|
||||
- echo "Building xTeVe version $XTEVE_VERSION from changelog-beta.md"
|
||||
- go build -v -ldflags "-X main.Version=$XTEVE_VERSION" ./...
|
||||
|
||||
- name: dockerfile-lint
|
||||
image: cache.coadcorp.com/hadolint/hadolint:v2.12.0-alpine
|
||||
commands:
|
||||
- hadolint -t error Dockerfile
|
||||
|
||||
- name: compose-validate
|
||||
image: cache.coadcorp.com/library/docker:27-cli
|
||||
commands:
|
||||
- apk add --no-cache docker-cli-compose
|
||||
- docker compose -f docker-compose.yml config -q
|
||||
- docker compose -f docker-compose.host.yml config -q
|
||||
|
||||
- name: docker-build-validate
|
||||
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
commands:
|
||||
- /kaniko/executor --context "${DRONE_WORKSPACE}" --dockerfile "${DRONE_WORKSPACE}/Dockerfile" --no-push --destination xteve:validate --build-arg TARGETOS=linux --build-arg TARGETARCH=amd64 --build-arg XTEVE_UID=1000 --build-arg XTEVE_GID=1000
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
|
||||
# plugins/docker reads its tag list from a .tags file when no "tags"
|
||||
# setting is given. Master images get latest, the commit SHA and the
|
||||
# release version from changelog-beta.md, so a release can be pulled by name.
|
||||
- name: image-tags
|
||||
image: cache.coadcorp.com/library/golang:1.27.1
|
||||
commands:
|
||||
- XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed 's/-beta$//')"
|
||||
- test -n "$XTEVE_VERSION"
|
||||
- printf '%s' "latest,${DRONE_COMMIT_SHA},$XTEVE_VERSION" > .tags
|
||||
- cat .tags
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
- master
|
||||
|
||||
- name: docker-publish
|
||||
image: plugins/docker
|
||||
environment:
|
||||
XTEVE_UID: "1000"
|
||||
XTEVE_GID: "1000"
|
||||
settings:
|
||||
registry: registry.coadcorp.com
|
||||
repo: registry.coadcorp.com/nathan/xteve
|
||||
dockerfile: Dockerfile
|
||||
username: nathan
|
||||
password:
|
||||
from_secret: registry_password
|
||||
build_args:
|
||||
- TARGETOS=linux
|
||||
- TARGETARCH=amd64
|
||||
build_args_from_env:
|
||||
- XTEVE_UID
|
||||
- XTEVE_GID
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
- master
|
||||
|
||||
- name: docker-publish-branch
|
||||
image: plugins/docker
|
||||
environment:
|
||||
XTEVE_UID: "1000"
|
||||
XTEVE_GID: "1000"
|
||||
settings:
|
||||
registry: registry.coadcorp.com
|
||||
repo: registry.coadcorp.com/nathan/xteve
|
||||
dockerfile: Dockerfile
|
||||
username: nathan
|
||||
password:
|
||||
from_secret: registry_password
|
||||
tags:
|
||||
- ${DRONE_BRANCH}
|
||||
- ${DRONE_COMMIT_SHA}
|
||||
build_args:
|
||||
- TARGETOS=linux
|
||||
- TARGETARCH=amd64
|
||||
build_args_from_env:
|
||||
- XTEVE_UID
|
||||
- XTEVE_GID
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- master
|
||||
@@ -1,7 +1,15 @@
|
||||
.DS_Store
|
||||
demo
|
||||
dev
|
||||
compiler
|
||||
files
|
||||
update_xteve*.sh
|
||||
xteve
|
||||
xteve.exe
|
||||
de.json
|
||||
|
||||
.gocache/
|
||||
agent.md
|
||||
skill.md
|
||||
node_modules/
|
||||
.claude/
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM golang:1.27.1-alpine AS builder
|
||||
WORKDIR /src
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
RUN XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed 's/-beta$//')" \
|
||||
&& test -n "$XTEVE_VERSION" \
|
||||
&& CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
go build -trimpath -ldflags="-s -w -X main.Version=$XTEVE_VERSION" -o /out/xteve ./xteve.go
|
||||
|
||||
# Static ffmpeg/ffprobe binaries. To bump, pick a tag from
|
||||
# https://hub.docker.com/r/mwader/static-ffmpeg/tags and change it here.
|
||||
# Do not go below 8.x: 7.1.1 rejects HLS segments with extensions such as .ec3
|
||||
# ("not in allowed_extensions"), which breaks streams with E-AC-3 audio.
|
||||
FROM mwader/static-ffmpeg:9.0.2 AS ffmpeg
|
||||
|
||||
FROM alpine:3.23
|
||||
|
||||
# Build-time defaults for the xteve user. The entrypoint can change them at
|
||||
# runtime with PUID/PGID, so these only matter when neither is set.
|
||||
ARG XTEVE_UID=1000
|
||||
ARG XTEVE_GID=1000
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata su-exec \
|
||||
&& addgroup -S -g "${XTEVE_GID}" xteve \
|
||||
&& adduser -S -D -H -u "${XTEVE_UID}" -G xteve xteve \
|
||||
&& mkdir -p /xteve/config \
|
||||
&& chown -R xteve:xteve /xteve
|
||||
|
||||
WORKDIR /xteve
|
||||
|
||||
COPY --from=builder /out/xteve /usr/local/bin/xteve
|
||||
COPY --from=ffmpeg /ffmpeg /usr/local/bin/ffmpeg
|
||||
COPY --from=ffmpeg /ffprobe /usr/local/bin/ffprobe
|
||||
COPY docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# The entrypoint starts as root so it can apply PUID/PGID and fix ownership of
|
||||
# the config directory, then drops to the xteve user with su-exec.
|
||||
|
||||
EXPOSE 34400/tcp
|
||||
|
||||
ENV XTEVE_CONFIG=/xteve/config
|
||||
ENV XTEVE_PORT=34400
|
||||
|
||||
VOLUME ["/xteve/config"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD wget -qO- "http://127.0.0.1:${XTEVE_PORT}/healthz" > /dev/null || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
+64
-1
@@ -1 +1,64 @@
|
||||
# Information for the developers will come soon
|
||||
# Developer notes
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `xteve.go` | `main`: flags, version, start-up |
|
||||
| `src/` | the application (one Go package `src`) |
|
||||
| `src/internal/` | authentication, image cache, M3U parser |
|
||||
| `html/` | web interface: HTML pages, CSS, images, the compiled `js/app.js`, and `embed.go` which embeds the lot |
|
||||
| `ts/` | TypeScript sources for the web interface |
|
||||
| `docker/` | container entrypoint |
|
||||
| `tasks/` | improvement plan and checklist |
|
||||
|
||||
## Build the binary
|
||||
|
||||
Go 1.27.1 is pinned in `go.mod`; the go command downloads it automatically.
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go vet ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
The web interface is embedded with `go:embed` from `html/`, so a bare clone builds a complete binary.
|
||||
|
||||
## Web interface
|
||||
|
||||
The UI is plain TypeScript compiled as global scripts (no modules yet) into a single bundle, `html/js/app.js`, which is committed. CI rebuilds it and fails if the committed file is stale.
|
||||
|
||||
```bash
|
||||
npm ci # installs the pinned TypeScript compiler and jsdom
|
||||
npm run build # ts/*.ts -> html/js/app.js
|
||||
npm run check # type-check only
|
||||
npm test # jsdom tests under tests/ (Node's built-in runner)
|
||||
```
|
||||
|
||||
File order in the bundle is fixed in `ts/tsconfig.json`: `network`, `menu`, `content`, `popup`, `xmltv`, `settings`, `logs`, `base`, `configuration`, `authentication`. Classes must be defined before the top-level statements in later files that instantiate them. All pages load the same bundle.
|
||||
|
||||
The tests load the built bundle into a jsdom window (`tests/harness.js`) with a fixture server payload (`tests/fixture.js`), so they exercise the real rendering code. Run `npm run build` before `npm test`.
|
||||
|
||||
English strings are inlined in the TypeScript. There is no language layer.
|
||||
|
||||
## Run from source
|
||||
|
||||
```bash
|
||||
go run . -config /path/to/config -port 34400
|
||||
```
|
||||
|
||||
Add `-dev` to serve the web interface from the local `html/` directory instead of the embedded copy, so CSS and HTML edits show up on reload. JavaScript still needs `npm run build`.
|
||||
|
||||
## Versioning and release
|
||||
|
||||
The version lives in two places that must agree: `var Version` in `xteve.go` and the first `#### ` heading in `changelog-beta.md` (without the `-beta` suffix). CI fails on drift.
|
||||
|
||||
Drone (`.drone.yml`) runs vet, tests, the bundle check, hadolint and compose validation on every push. Pushes to `master` publish `registry.coadcorp.com/nathan/xteve:latest`, `:<version>` and `:<sha>`; other branches publish `:<branch>` and `:<sha>`.
|
||||
|
||||
## Endpoints useful for operations
|
||||
|
||||
* `GET /healthz` returns `{"status":"ok","version":...,"scanInProgress":...}` with no authentication. The Docker healthcheck uses it.
|
||||
|
||||
## Container image
|
||||
|
||||
`Dockerfile` builds the binary in a `golang:1.27.1-alpine` stage, copies static `ffmpeg`/`ffprobe` from a pinned `mwader/static-ffmpeg` tag (bump the tag in the Dockerfile; there is no other place to change) and runs on `alpine`. The runtime stage installs `su-exec` and does not set `USER`: `docker/entrypoint.sh` starts as root, applies `PUID`/`PGID` to the `xteve` user by editing `/etc/passwd` and `/etc/group`, fixes ownership of the config directory, and execs `su-exec xteve:xteve xteve`. With `--user` it skips all of that. `ARG XTEVE_UID`/`XTEVE_GID` remain the build-time defaults so Drone's build args still work. Check the script with `sh -n` and `shellcheck -s sh` after editing.
|
||||
@@ -8,9 +8,23 @@
|
||||
|
||||
Documentation for setup and configuration is [here](https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md).
|
||||
|
||||
#### Donation
|
||||
* **Bitcoin:** 1c1iCe4CJPfNUXtqxKBbW2Qd2EtqRPWme
|
||||

|
||||
## About this fork
|
||||
|
||||
The upstream xTeVe project has been inactive since 2021. This fork is maintained privately for a single trusted LAN. It is Docker-only and built for amd64 only. There are no release archives and no self-update.
|
||||
|
||||
What it adds over upstream:
|
||||
|
||||
* Web UI redesign with a mobile and accessibility pass (responsive navigation, keyboard flow, focus visibility, ARIA announcements, contrast).
|
||||
* Plex API guide refresh: after a lineup or XEPG update xTeVe can ask Plex to reload the DVR guide. New settings: `use_plexAPI`, `plex.url`, `plex.token`.
|
||||
* Strict or relaxed handling of channels whose EPG source went missing in XEPG, with automatic re-mapping.
|
||||
* A wizard-completed flag so the setup wizard is not shown again on restart.
|
||||
* First-party Docker image with a static ffmpeg, runtime `PUID`/`PGID`, and a healthcheck.
|
||||
* Drone CI: vet, tests, web bundle check, Dockerfile lint, compose validation, image publishing.
|
||||
* Web UI embedded in the binary with `go:embed`.
|
||||
* Unauthenticated `GET /healthz` endpoint for container health checks.
|
||||
* The self-updater and the GitHub branch switching are removed.
|
||||
|
||||
The plan for further work is in `tasks/improvement-plan.md`.
|
||||
|
||||
## Requirements
|
||||
### Plex
|
||||
@@ -31,7 +45,7 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
|
||||
* Merge external M3U files
|
||||
* Merge external XMLTV files
|
||||
* Automatic M3U and XMLTV update
|
||||
* M3U und XMLTV export
|
||||
* M3U and XMLTV export
|
||||
|
||||
#### Channel management
|
||||
* Filtering streams
|
||||
@@ -48,117 +62,82 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
|
||||
|
||||
---
|
||||
|
||||
## Downloads v2 | 64 Bit only
|
||||
#### 64 Bit Intel / AMD
|
||||
## Running the container
|
||||
|
||||
* [Windows](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_windows_amd64.zip?raw=true)
|
||||
* [OS X](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_darwin_amd64.zip?raw=true)
|
||||
* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_amd64.zip?raw=true)
|
||||
* [FreeBSD](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_freebsd_amd64.zip?raw=true)
|
||||
### Image
|
||||
|
||||
#### 64 Bit ARM
|
||||
* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_arm64.zip?raw=true)
|
||||
The image is built by Drone and pushed to a private registry:
|
||||
|
||||
#### Recommended Docker Image (Linux 64 Bit)
|
||||
Thanks to @alturismo and @LeeD for creating the Docker Images.
|
||||
```
|
||||
registry.coadcorp.com/nathan/xteve
|
||||
```
|
||||
|
||||
**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)
|
||||
Tags:
|
||||
|
||||
Including:
|
||||
- Guide2go: XMLTV grabber for Schedules Direct
|
||||
- owi2plex: XMLTV file grabber for Enigma receivers
|
||||
* `latest`, the release version (for example `2.3.0.0202`) and `<commit sha>` from the `master` branch. Pin the version tag if you want to control when you upgrade, or to roll back.
|
||||
* `<branch name>` and `<commit sha>` from every other branch.
|
||||
|
||||
**Created by LeeD:**
|
||||
[xTeVe / Guide2go / Zap2XML](https://hub.docker.com/r/dnsforge/xteve)
|
||||
```bash
|
||||
docker pull registry.coadcorp.com/nathan/xteve:latest
|
||||
```
|
||||
|
||||
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
|
||||
The image is linux/amd64 only.
|
||||
|
||||
## Beta version
|
||||
### Start with Docker Compose
|
||||
|
||||
New features are first available in the beta version and will be added to the master branch after successful testing
|
||||
If you prefer to use the beta version, you can always switch between master and beta branch.
|
||||
Bridge mode (default). The web UI is on port 34400.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.host.yml up -d
|
||||
```
|
||||
|
||||
In bridge mode you can still add the tuner manually in Plex or Emby with `http://<host-ip>:34400`.
|
||||
|
||||
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 .`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| 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`. |
|
||||
|
||||
The container starts as root, sets the `xteve` user to `PUID`/`PGID`, fixes the ownership of the config directory if needed, and then drops to that user before starting xTeVe. If the container is started with `--user`, the ids are left alone and xTeVe runs as the given user. The old `XTEVE_UID`/`XTEVE_GID` variables are still accepted as aliases.
|
||||
|
||||
### Volume
|
||||
|
||||
The config lives in `/xteve/config` (declared as a volume in the image). The compose files bind it to `./docker-data/config`. It holds `settings.json`, the mapping and EPG data, backups and the cache, so it is small enough to back up as a whole.
|
||||
|
||||
If an older container kept its config in `/config` or `/home/xteve/.xteve`, the entrypoint copies it to the new location on first start.
|
||||
|
||||
### Healthcheck
|
||||
|
||||
The image has a `HEALTHCHECK` that requests `http://127.0.0.1:${XTEVE_PORT}/healthz` every 30 seconds. The endpoint needs no authentication and returns the version and whether a scan is in progress. `docker ps` shows the state as `healthy` or `unhealthy`.
|
||||
|
||||
### ffmpeg
|
||||
|
||||
A static ffmpeg and ffprobe are in the image at `/usr/local/bin/ffmpeg` and `/usr/local/bin/ffprobe`, taken from the pinned `mwader/static-ffmpeg` image. In a container xTeVe defaults `ffmpeg.path` to that location. VLC is not included.
|
||||
|
||||
---
|
||||
|
||||
### xTeVe Beta branch
|
||||
New features and bug fixes are only available in beta brunch. Only after successful testing, they are merged into the master branch.
|
||||
## Security notes
|
||||
|
||||
**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.
|
||||
* This fork is meant for a trusted LAN. Do not put it on the internet.
|
||||
* Web authentication is off by default. Turn it on under Settings > Authentication before you expose it any further than the LAN, for example through a reverse proxy.
|
||||
* `/healthz` is always unauthenticated. It reveals the version and scan state only.
|
||||
* The container starts as root to apply `PUID`/`PGID` and then drops privileges. Start it with `--user` if you prefer it never to run as root.
|
||||
|
||||
---
|
||||
|
||||
## Build from source code [Go / Golang]
|
||||
|
||||
#### Requirements
|
||||
* [Go](https://golang.org) (go1.12.4 or newer)
|
||||
|
||||
#### Dependencies
|
||||
* [go-ssdp](https://github.com/koron/go-ssdp)
|
||||
* [websocket](https://github.com/gorilla/websocket)
|
||||
* [osext](https://github.com/kardianos/osext)
|
||||
|
||||
#### Build
|
||||
1. Download source code
|
||||
2. Install dependencies
|
||||
```
|
||||
go get github.com/koron/go-ssdp
|
||||
go get github.com/gorilla/websocket
|
||||
go get github.com/kardianos/osext
|
||||
```
|
||||
3. Build xTeVe
|
||||
```
|
||||
go build xteve.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fork without pull request :mega:
|
||||
When creating a fork, the xTeVe GitHub account must be changed from the source code or the update function disabled.
|
||||
Future updates of the xteve-project would update your fork. :wink:
|
||||
|
||||
xteve.go - Line: 29
|
||||
```Go
|
||||
var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-Downloads", Update: true}
|
||||
|
||||
/*
|
||||
Branch: GitHub Branch
|
||||
User: GitHub Username
|
||||
Repo: GitHub Repository
|
||||
Update: Automatic updates from the GitHub repository [true|false]
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
See [README-DEV.md](README-DEV.md) for the layout, how to build the binary and the web UI, and how CI publishes the image.
|
||||
@@ -1,3 +1,99 @@
|
||||
#### 2.3.0.0202-beta
|
||||
```diff
|
||||
+ Security: provider-supplied text is rendered as text in the web UI; websocket requests must come from the same origin; the session cookie is HttpOnly and SameSite=Strict; logo uploads, backup restores, the ffmpeg/VLC path and stream URLs are validated; backups can only be downloaded with a web session.
|
||||
+ Passwords are stored with bcrypt. Existing accounts are upgraded on their next login.
|
||||
+ Streaming: buffer state rewritten around a single lock with an atomic tuner limit; ffmpeg/VLC are stopped when the last client leaves or when no data arrives within 20 seconds; long streams no longer accumulate open handles.
|
||||
+ Per-playlist buffer choice (default, none, xTeVe, FFmpeg, VLC) in the playlist dialog.
|
||||
+ New filter type: regular expression.
|
||||
+ Web UI: one persistent websocket with automatic reconnect, light theme following the OS setting, focus is kept when the menu refreshes, errors are announced to screen readers.
|
||||
+ The web interface is embedded with go:embed and built from a pinned TypeScript toolchain; the language file is gone (English only).
|
||||
+ New /healthz endpoint, used by the container healthcheck.
|
||||
+ Container: PUID/PGID are applied at start-up; ffmpeg 9.0.2; images are also tagged with the release version.
|
||||
+ Settings and other state files are written atomically; settings.json is readable by the owner only.
|
||||
+ HTTP server and provider downloads have timeouts.
|
||||
- Removed the built-in self-updater. Updates are delivered through the container image.
|
||||
- Removed XTEVE_UID/XTEVE_GID as the documented variables (still accepted as a fallback for PUID/PGID).
|
||||
```
|
||||
|
||||
#### 2.2.0.0201-beta
|
||||
Add UI enhancement for range selecting checkboxes
|
||||
|
||||
#### 2.2.0.0200-beta
|
||||
```diff
|
||||
+ Major web UI redesign focused on cleaner layout, better information hierarchy, and faster daily workflows.
|
||||
+ Large mobile UX overhaul: responsive navigation, improved spacing, touch-friendly controls, and better small-screen mapping/config flows.
|
||||
+ Accessibility pass across auth, configuration, and main app screens (focus visibility, keyboard flow, ARIA/announcer updates, contrast and status feedback improvements).
|
||||
+ Settings UX improvements and additional polish in frontend behavior for menu, configuration, and authentication interactions.
|
||||
+ Added optional Plex API refresh integration with new settings: use_plexAPI, plex.url, plex.token.
|
||||
+ Added debounced/queued Plex DVR guide reload workflow after lineup and XEPG updates to reduce manual refresh work in Plex.
|
||||
+ Container/runtime improvements and validation updates for easier, safer container usage.
|
||||
+ Added Drone CI pipeline for tests/build checks, Docker/Compose validation, and Docker image publishing to registry.coadcorp.com via plugins/docker.
|
||||
+ Pipeline build version is now derived from changelog-beta.md and validated against source version to prevent release/version drift.
|
||||
```
|
||||
|
||||
#### 2.1.1.0116-beta
|
||||
If no user agent is specified, the default FFmpeg or VLC user agent is used.
|
||||
|
||||
#### 2.1.1.0115-beta
|
||||
```diff
|
||||
+ GZIP compression for xteve.xml file. (http://xteve.ip:34400/xmltv/xteve.xml.gz)
|
||||
- Removed protocol setting for reverse proxy. HTTPS can also be configured in the proxy, where it makes more sense.
|
||||
```
|
||||
|
||||
#### 2.1.0.0106-beta
|
||||
```diff
|
||||
+ User-Agent is now also used by VLC and FFmpeg.
|
||||
```
|
||||
|
||||
#### 2.1.0.0105-beta
|
||||
```diff
|
||||
+ Fixed wrong buffer value in log
|
||||
+ New setting: URL protocol for M3U and XML file
|
||||
+ Add xml tag premiere to xteve.xml
|
||||
```
|
||||
|
||||
#### 2.1.0.0101-beta
|
||||
```diff
|
||||
+ Reverse proxy fix
|
||||
```
|
||||
|
||||
#### 2.0.3.0042-beta
|
||||
**Version 2.0.3.0042 changes the settings.json.**
|
||||
Settings from the current beta can not be used for the current master version 2.0.3
|
||||
- New default options for VLC and FFmpeg
|
||||
- VLC and FFmpeg log entries in the xTeVe log
|
||||
- Less CPU load with VLC and FFmpeg
|
||||
|
||||
#### 2.0.3.0035-beta
|
||||
```diff
|
||||
+ FFmpeg support
|
||||
+ VLC support
|
||||
```
|
||||
**Version 2.0.3.0035 changes the settings.json.**
|
||||
Settings from the current beta can not be used for the current master version 2.0.3
|
||||
|
||||
#### 2.0.2.0024-beta
|
||||
```diff
|
||||
+ Improved monitoring of the buffer process
|
||||
+ Update the XEPG database a bit faster
|
||||
```
|
||||
|
||||
##### Fixes
|
||||
- Error message if filter rule is missing
|
||||
- Channels are lost when saving again (Mapping)
|
||||
- Plex log, invalid source: IPTV
|
||||
|
||||
#### 2.0.1.0012-beta
|
||||
```diff
|
||||
+ Add support for "video/m2ts" video streams (Pull request #14)
|
||||
```
|
||||
#### 2.0.1.0011-beta
|
||||
```diff
|
||||
+ Original group title is shown in the Mapping Editor
|
||||
```
|
||||
##### Fixes
|
||||
- incorrect original-air-date
|
||||
|
||||
#### 2.0.1.0010-beta
|
||||
```diff
|
||||
+ Set timestamp to <episode-num system="original-air-date">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Host-network compose file for the private xTeVe image.
|
||||
#
|
||||
# Host networking lets SSDP/DLNA multicast reach the container, so Plex and
|
||||
# Emby can discover the tuner automatically. Ports are not published in this
|
||||
# mode; xTeVe listens on XTEVE_PORT directly on the host. Linux only.
|
||||
services:
|
||||
xteve:
|
||||
image: registry.coadcorp.com/nathan/xteve:latest
|
||||
# Uncomment to build locally instead of pulling from the registry.
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
container_name: xteve
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
environment:
|
||||
XTEVE_CONFIG: /xteve/config
|
||||
XTEVE_PORT: "34400"
|
||||
# Owner of the config directory inside the container. Set these to the
|
||||
# uid/gid that owns ./docker-data/config on the host.
|
||||
PUID: ${PUID:-1000}
|
||||
PGID: ${PGID:-1000}
|
||||
# Time zone for log timestamps and the EPG, for example Australia/Sydney.
|
||||
TZ: ${TZ:-UTC}
|
||||
volumes:
|
||||
- ./docker-data/config:/xteve/config
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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:
|
||||
xteve:
|
||||
image: registry.coadcorp.com/nathan/xteve:latest
|
||||
# Uncomment to build locally instead of pulling from the registry.
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
container_name: xteve
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
XTEVE_CONFIG: /xteve/config
|
||||
XTEVE_PORT: "34400"
|
||||
# Owner of the config directory inside the container. Set these to the
|
||||
# uid/gid that owns ./docker-data/config on the host.
|
||||
PUID: ${PUID:-1000}
|
||||
PGID: ${PGID:-1000}
|
||||
# Time zone for log timestamps and the EPG, for example Australia/Sydney.
|
||||
TZ: ${TZ:-UTC}
|
||||
ports:
|
||||
- "34400:34400/tcp"
|
||||
- "1900:1900/udp"
|
||||
volumes:
|
||||
- ./docker-data/config:/xteve/config
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
DEFAULT_CONFIG_DIR="/xteve/config"
|
||||
# Older images and other Docker images kept the config in one of these.
|
||||
# /xteve itself is not listed because it is the parent of the default dir.
|
||||
LEGACY_CONFIG_DIRS="/config /home/xteve/.xteve"
|
||||
|
||||
RUN_USER="xteve"
|
||||
XTEVE_BIN="/usr/local/bin/xteve"
|
||||
|
||||
log() {
|
||||
echo "[entrypoint] $*"
|
||||
}
|
||||
|
||||
resolve_config_dir() {
|
||||
if [ -n "${XTEVE_CONFIG:-}" ] && [ "${XTEVE_CONFIG}" != "${DEFAULT_CONFIG_DIR}" ]; then
|
||||
printf "%s" "${XTEVE_CONFIG}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -f "${DEFAULT_CONFIG_DIR}/settings.json" ]; then
|
||||
printf "%s" "${DEFAULT_CONFIG_DIR}"
|
||||
return
|
||||
fi
|
||||
|
||||
for dir in ${LEGACY_CONFIG_DIRS}; do
|
||||
if [ -f "${dir}/settings.json" ]; then
|
||||
printf "%s" "${dir}"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
printf "%s" "${DEFAULT_CONFIG_DIR}"
|
||||
}
|
||||
|
||||
copy_if_missing() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
|
||||
if [ -e "${src}" ] && [ ! -e "${dst}" ]; then
|
||||
cp -R "${src}" "${dst}"
|
||||
fi
|
||||
}
|
||||
|
||||
settings_initialized() {
|
||||
file="$1"
|
||||
|
||||
if [ ! -s "${file}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if grep -q '"uuid"' "${file}" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_number() {
|
||||
case "$1" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Apply PUID/PGID to the xteve user and group. Only called when running as root.
|
||||
# Falls back to XTEVE_UID/XTEVE_GID (the old variable names), then to the ids
|
||||
# baked into the image at build time.
|
||||
apply_ids() {
|
||||
current_uid="$(id -u "${RUN_USER}")"
|
||||
current_gid="$(id -g "${RUN_USER}")"
|
||||
|
||||
want_uid="${PUID:-${XTEVE_UID:-${current_uid}}}"
|
||||
want_gid="${PGID:-${XTEVE_GID:-${current_gid}}}"
|
||||
|
||||
if ! is_number "${want_uid}" || ! is_number "${want_gid}"; then
|
||||
log "ERROR: PUID/PGID must be numeric (got PUID=${want_uid} PGID=${want_gid})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${want_gid}" != "${current_gid}" ]; then
|
||||
# Edit /etc/group in place. deluser/delgroup on busybox refuse to remove
|
||||
# a group that still has members, so sed is the safest option.
|
||||
sed -i "s/^${RUN_USER}:\([^:]*\):${current_gid}:/${RUN_USER}:\1:${want_gid}:/" /etc/group
|
||||
fi
|
||||
|
||||
if [ "${want_uid}" != "${current_uid}" ] || [ "${want_gid}" != "${current_gid}" ]; then
|
||||
sed -i "s/^${RUN_USER}:\([^:]*\):${current_uid}:${current_gid}:/${RUN_USER}:\1:${want_uid}:${want_gid}:/" /etc/passwd
|
||||
fi
|
||||
|
||||
new_uid="$(id -u "${RUN_USER}")"
|
||||
new_gid="$(id -g "${RUN_USER}")"
|
||||
if [ "${new_uid}" != "${want_uid}" ] || [ "${new_gid}" != "${want_gid}" ]; then
|
||||
log "ERROR: failed to set ${RUN_USER} to UID:GID ${want_uid}:${want_gid} (now ${new_uid}:${new_gid})" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Make sure the config directory belongs to the xteve user. The directory is
|
||||
# small (settings, cache, backups), so a recursive chown is acceptable, but it
|
||||
# is skipped when the ownership is already right.
|
||||
fix_config_owner() {
|
||||
dir="$1"
|
||||
uid="$2"
|
||||
gid="$3"
|
||||
|
||||
owner="$(stat -c '%u:%g' "${dir}" 2>/dev/null || echo "")"
|
||||
if [ "${owner}" != "${uid}:${gid}" ] || [ "${MIGRATED}" = "1" ]; then
|
||||
log "Setting ownership of ${dir} to ${uid}:${gid}"
|
||||
chown -R "${uid}:${gid}" "${dir}" || log "WARNING: chown of ${dir} failed; continuing"
|
||||
fi
|
||||
}
|
||||
|
||||
CONFIG_DIR="$(resolve_config_dir)"
|
||||
PORT="${XTEVE_PORT:-34400}"
|
||||
|
||||
# The binary uses XTEVE_CONFIG to detect that it runs in a container and to
|
||||
# default ffmpeg.path to /usr/local/bin/ffmpeg. Keep it in sync with the
|
||||
# resolved directory so both agree.
|
||||
export XTEVE_CONFIG="${CONFIG_DIR}"
|
||||
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
# Set to 1 when files were copied from a legacy directory. When that happens as
|
||||
# root the copies are root-owned, so the ownership fix below must not be skipped.
|
||||
MIGRATED=0
|
||||
|
||||
if ! settings_initialized "${CONFIG_DIR}/settings.json"; then
|
||||
for legacy_dir in ${LEGACY_CONFIG_DIRS}; do
|
||||
if [ "${legacy_dir}" = "${CONFIG_DIR}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if settings_initialized "${legacy_dir}/settings.json"; then
|
||||
log "Migrating existing configuration from ${legacy_dir} to ${CONFIG_DIR}"
|
||||
|
||||
for file in authentication.json pms.json settings.json xepg.json urls.json; do
|
||||
if [ "${file}" = "settings.json" ] || [ "${file}" = "xepg.json" ] || [ "${file}" = "urls.json" ]; then
|
||||
cp -f "${legacy_dir}/${file}" "${CONFIG_DIR}/${file}" 2>/dev/null || true
|
||||
else
|
||||
copy_if_missing "${legacy_dir}/${file}" "${CONFIG_DIR}/${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
for dir_name in data cache backup tmp; do
|
||||
copy_if_missing "${legacy_dir}/${dir_name}" "${CONFIG_DIR}/${dir_name}"
|
||||
done
|
||||
|
||||
MIGRATED=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
# Started as root (the default): apply PUID/PGID, fix ownership, drop privileges.
|
||||
apply_ids
|
||||
RUN_UID="$(id -u "${RUN_USER}")"
|
||||
RUN_GID="$(id -g "${RUN_USER}")"
|
||||
fix_config_owner "${CONFIG_DIR}" "${RUN_UID}" "${RUN_GID}"
|
||||
|
||||
if ! su-exec "${RUN_USER}:${RUN_USER}" touch "${CONFIG_DIR}/.xteve-write-test" 2>/dev/null; then
|
||||
log "ERROR: Config directory is not writable: ${CONFIG_DIR}" >&2
|
||||
log "xteve would run as UID:GID ${RUN_UID}:${RUN_GID}" >&2
|
||||
ls -ld "${CONFIG_DIR}" >&2 || true
|
||||
log "Hint: set PUID/PGID to the owner of the host directory, or fix its ownership/permissions on the host." >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${CONFIG_DIR}/.xteve-write-test"
|
||||
|
||||
log "Using config directory: ${CONFIG_DIR}"
|
||||
log "Running as UID:GID ${RUN_UID}:${RUN_GID} (PUID/PGID)"
|
||||
if [ -f "${CONFIG_DIR}/settings.json" ]; then
|
||||
log "settings.json details: $(ls -l "${CONFIG_DIR}/settings.json" | awk '{print $1, $3, $4, $5, $9}')"
|
||||
fi
|
||||
|
||||
exec su-exec "${RUN_USER}:${RUN_USER}" "${XTEVE_BIN}" -config "${CONFIG_DIR}" -port "${PORT}" "$@"
|
||||
fi
|
||||
|
||||
# Started with --user: keep the given identity, no id or ownership changes.
|
||||
if [ -n "${PUID:-}${PGID:-}${XTEVE_UID:-}${XTEVE_GID:-}" ]; then
|
||||
log "Container started as non-root; PUID/PGID are ignored"
|
||||
fi
|
||||
|
||||
if ! touch "${CONFIG_DIR}/.xteve-write-test" 2>/dev/null; then
|
||||
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
|
||||
fi
|
||||
rm -f "${CONFIG_DIR}/.xteve-write-test"
|
||||
|
||||
log "Using config directory: ${CONFIG_DIR}"
|
||||
log "Running as UID:GID $(id -u):$(id -g)"
|
||||
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 "${XTEVE_BIN}" -config "${CONFIG_DIR}" -port "${PORT}" "$@"
|
||||
@@ -0,0 +1,14 @@
|
||||
module xteve
|
||||
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/koron/go-ssdp v0.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
|
||||
github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
+15
-16
@@ -1,28 +1,26 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>xTeVe</title>
|
||||
<link rel="stylesheet" href="css/screen.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
<script language="javascript" type="text/javascript" src="js/configuration_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/menu_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/settings_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
|
||||
<link rel="stylesheet" href="css/tokens.css">
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<script src="js/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="javascript: readyForConfiguration(0);">
|
||||
<body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);">
|
||||
|
||||
<div id="loading" class="block">
|
||||
<div id="loading" class="block" role="status" aria-live="polite" aria-label="Loading" aria-hidden="false">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
<div id="box">
|
||||
<main id="box" aria-labelledby="head-text">
|
||||
|
||||
<table id="clientInfo" class="visible">
|
||||
<table id="clientInfo" class="visible" aria-label="Server information">
|
||||
<tr>
|
||||
<td class="tdKey">Version:</td>
|
||||
<td id="version" class="tdVal"> </td>
|
||||
@@ -46,13 +44,14 @@
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">Configuration</h1>
|
||||
</div>
|
||||
<p id="err" class="errorMsg center"></p>
|
||||
<div id="content">
|
||||
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
|
||||
<div id="content" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Configuration step">
|
||||
|
||||
</div>
|
||||
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
|
||||
<div id="box-footer">
|
||||
<input id="next" class="" type="button" name="next" value="Next" onclick="javascript: saveWizard();">
|
||||
<input id="next" class="" type="button" name="next" value="Next" aria-controls="content" onclick="javascript: saveWizard();">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+19
-18
@@ -1,47 +1,48 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>xTeVe</title>
|
||||
<link rel="stylesheet" href="css/screen.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
|
||||
<link rel="stylesheet" href="css/tokens.css">
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<script src="js/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="auth-screen">
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
|
||||
<div id="box">
|
||||
<main id="box" aria-labelledby="head-text">
|
||||
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">{{.account.headline}}</h1>
|
||||
<h1 id="head-text" class="center">Create user account</h1>
|
||||
</div>
|
||||
|
||||
<p id="err" class="errorMsg center"></p>
|
||||
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<form id="authentication" action="/web/" method="post">
|
||||
<form id="authentication" action="" method="post" aria-describedby="err" novalidate>
|
||||
|
||||
<h5>{{.account.username.title}}:</h5>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="">
|
||||
<h5>{{.account.password.title}}:</h5>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="">
|
||||
<h5>{{.account.confirm.title}}:</h5>
|
||||
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value="">
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="new-password">
|
||||
<label for="confirm">Confirm:</label>
|
||||
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value="" autocomplete="new-password">
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="box-footer">
|
||||
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" onclick="javascript: login();">
|
||||
<input id="submit" class="" type="button" value="Create Account" aria-label="Create Account" onclick="javascript: login();">
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+209
-305
@@ -1,254 +1,240 @@
|
||||
/*
|
||||
* base.css - reset, typography, links, focus styles, skip link, generic
|
||||
* form elements and small utility classes. Loaded after tokens.css.
|
||||
*/
|
||||
|
||||
* {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-ms-appearance: none;
|
||||
font-family: "Arial", sans-serif;
|
||||
letter-spacing: 2px;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--font-sans);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/*
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
*/
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||
border-radius: 5px;
|
||||
|
||||
border-radius: 999px;
|
||||
background: var(--scroll-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0,0.6);
|
||||
background-color: #444;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--scroll-track);
|
||||
background: linear-gradient(180deg, var(--scroll-thumb-a), var(--scroll-thumb-b));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #333;
|
||||
background: linear-gradient(180deg, var(--scroll-thumb-hover-a), var(--scroll-thumb-hover-b));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00E6FF;
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
background:
|
||||
radial-gradient(circle at 15% 12%, var(--bg-glow-1), transparent 32%),
|
||||
radial-gradient(circle at 88% 2%, var(--bg-glow-2), transparent 30%),
|
||||
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 56%, var(--bg-deep) 100%);
|
||||
}
|
||||
|
||||
html, body {
|
||||
color: #fff;
|
||||
margin: 0px auto;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
body {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.72rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
letter-spacing: 2px;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 22px;
|
||||
letter-spacing: 1px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.5em;
|
||||
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2em;
|
||||
margin: 25px 0px 10px 0px;
|
||||
font-size: 0.92rem;
|
||||
margin: 8px 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.55;
|
||||
letter-spacing: 0.015em;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: #333;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 2px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0px 0px 5px 0px;
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
letter-spacing: 1px;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
line-height: 1.6em;
|
||||
margin: 12px 0;
|
||||
background: linear-gradient(90deg, transparent, var(--line), transparent);
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
background-color: #111;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-left: solid 2px #111;
|
||||
transition: all 0.3;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
border-color: #00E6FF
|
||||
select,
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
outline: none;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--focus-ring-soft);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
cursor: pointer;
|
||||
width: calc(100% + 2px);
|
||||
border: solid 0px #00E6FF;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
padding: 9px 10px;
|
||||
display:block;
|
||||
background-color: #333;
|
||||
font-size: 14px;
|
||||
margin: 5px 0px 5px 0px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none;
|
||||
input[type=text],
|
||||
input[type=search],
|
||||
input[type=password],
|
||||
input[type=number],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid var(--line);
|
||||
background-color: var(--surface-input);
|
||||
color: var(--text);
|
||||
padding: 10px 11px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
margin: 5px 0px;
|
||||
padding: 2.5px 10px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
input[type=text]:focus,
|
||||
input[type=search]:focus,
|
||||
input[type=password]:focus,
|
||||
input[type=number]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-ring);
|
||||
background-color: var(--surface-input-focus);
|
||||
}
|
||||
|
||||
input[type=button], input[type=submit] {
|
||||
cursor: pointer;
|
||||
background-color: #000;
|
||||
margin: 10px 10px;
|
||||
padding: 10px 25px;
|
||||
border: solid 0px;
|
||||
border-color: #000;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
input[type=text]::placeholder,
|
||||
input[type=search]::placeholder,
|
||||
input[type=password]::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--text-placeholder);
|
||||
}
|
||||
|
||||
input[type=button]:focus {
|
||||
outline: none;
|
||||
input[type=button]:focus-visible,
|
||||
input[type=submit]:focus-visible,
|
||||
button:focus-visible,
|
||||
input[type=checkbox]:focus-visible,
|
||||
a:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible,
|
||||
input[type=text]:focus-visible,
|
||||
input[type=search]:focus-visible,
|
||||
input[type=password]:focus-visible,
|
||||
input[type=number]:focus-visible {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
input[type=button]:hover {
|
||||
background-color: #00E6FF;
|
||||
color: #000;
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2600;
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-on-accent);
|
||||
background: var(--accent-gradient);
|
||||
transform: translateY(-180%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
input[type=button]:hover.delete {
|
||||
background-color: red;
|
||||
color: #fff;
|
||||
.skip-link:focus-visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
input[type=text], input[type=search], input[type=password] {
|
||||
color: #fff;
|
||||
width: -webkit-calc(100% - 0px);
|
||||
width: -moz-calc(100% - 0px);
|
||||
width: calc(100% - 0px);
|
||||
outline: none;
|
||||
border: solid 1px transparent;
|
||||
background-color: transparent;
|
||||
border-bottom-color: #555;
|
||||
border-radius: 0px;
|
||||
padding: 8px 10px;
|
||||
.sr-only {
|
||||
border: 0 !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
height: 1px !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
white-space: nowrap !important;
|
||||
width: 1px !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
border: solid 1px #00E6FF;
|
||||
background-color: #333;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
cursor: pointer;
|
||||
/*
|
||||
-webkit-appearance: checkbox;
|
||||
*/
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
color: #fff;
|
||||
background-color: #00E6FF;
|
||||
/*display: inline-block;*/
|
||||
}
|
||||
|
||||
input[type="checkbox"]:before {
|
||||
position: initial;
|
||||
left: 0px;
|
||||
margin-left: -4px;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked:before {
|
||||
position: initial;
|
||||
left: 0px;
|
||||
margin-left: -3px;
|
||||
content: "✓";
|
||||
color: #000;
|
||||
}
|
||||
|
||||
|
||||
input[type=button].cancel {
|
||||
|
||||
background-color: transparent;
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
input[type=button].save{
|
||||
background-color: #111;
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
input[type=button].black, input[type=submit].black{
|
||||
background-color: #000;
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
input[type=button].center{
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
background-color: #000;
|
||||
border-color: #000;
|
||||
}
|
||||
/* Utility classes */
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pointer:hover {
|
||||
color: #00E6FF;
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.sortThis {
|
||||
color: #00E6FF;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.w40px {
|
||||
@@ -271,14 +257,9 @@ input[type=button].center{
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
width: 200px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.w300px {
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.w220px {
|
||||
@@ -286,46 +267,18 @@ input[type=button].center{
|
||||
cursor: alias;
|
||||
}
|
||||
|
||||
.w300px {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screenLogHidden {
|
||||
transform: translate(0px, -110px);
|
||||
}
|
||||
|
||||
.borderSpace {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.block {
|
||||
|
||||
}
|
||||
|
||||
.none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.notVisible {
|
||||
height: 0px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
border-bottom: #000 solid 0px;
|
||||
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
border-bottom: #444 solid 1px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.floatRight {
|
||||
float: right;
|
||||
}
|
||||
@@ -334,115 +287,66 @@ input[type=button].center{
|
||||
float: left;
|
||||
}
|
||||
|
||||
.menu-active {
|
||||
background-color: #00E6FF;
|
||||
.borderSpace {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.menu-notActive {
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#branch {
|
||||
display: table;
|
||||
margin: auto;
|
||||
color: red;
|
||||
.none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#interaction {
|
||||
margin-bottom: 100px;
|
||||
text-align: center;
|
||||
border-bottom: solid 0px #777;
|
||||
.showBulk {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.hideBulk {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notVisible {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.half {
|
||||
display: block;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.menu {
|
||||
border: solid 1px #00E6FF;
|
||||
}
|
||||
|
||||
.infoMsg {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.errorMsg {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.warningMsg {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.debugMsg {
|
||||
color: magenta;
|
||||
}
|
||||
|
||||
.News, .Movie, .Series, .Sports, .Kids {
|
||||
border-left: solid 2px
|
||||
}
|
||||
|
||||
.News {
|
||||
border-color: tomato
|
||||
}
|
||||
|
||||
.Movie {
|
||||
border-color: royalblue;
|
||||
}
|
||||
|
||||
.Series {
|
||||
border-color: gold;
|
||||
}
|
||||
|
||||
.Sports {
|
||||
border-color: yellowgreen;
|
||||
}
|
||||
|
||||
.Kids {
|
||||
border-color: mediumpurple;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
#loading {
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
background-color: rgba(0,0,0, 0.8);
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.loader {
|
||||
border: 5px solid transparent;
|
||||
border-radius: 50%;
|
||||
border-top: 5px solid #00E6FF;
|
||||
border-bottom: 5px solid #00E6FF;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
-webkit-animation: spin 1.2s linear infinite;
|
||||
animation: spin 1.2s linear infinite;
|
||||
|
||||
position: fixed;
|
||||
margin: auto;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% { -webkit-transform: rotate(0deg); }
|
||||
100% { -webkit-transform: rotate(360deg); }
|
||||
.tableEllipsis {
|
||||
width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popupIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(7px) scale(0.99);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,775 @@
|
||||
/*
|
||||
* components.css - buttons, inputs, tables, status cards, connection
|
||||
* indicator, popups, menu, log lines and badges. Loaded after base.css.
|
||||
* Responsive overrides for these components live in layout.css.
|
||||
*/
|
||||
|
||||
/* Buttons */
|
||||
|
||||
input[type=button],
|
||||
input[type=submit],
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
color: var(--text-on-accent);
|
||||
padding: 10px 18px;
|
||||
margin: 8px 8px 8px 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
background: var(--accent-gradient);
|
||||
box-shadow: 0 10px 22px var(--accent-shadow);
|
||||
transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
input[type=button]:hover,
|
||||
input[type=submit]:hover,
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.05);
|
||||
box-shadow: 0 12px 24px var(--accent-shadow-strong);
|
||||
}
|
||||
|
||||
input[type=button]:active,
|
||||
input[type=submit]:active,
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
input[type=button].delete {
|
||||
color: var(--text-on-error);
|
||||
background: linear-gradient(135deg, var(--error-bright) 0%, var(--error-strong) 100%);
|
||||
box-shadow: 0 10px 20px var(--error-shadow);
|
||||
}
|
||||
|
||||
input[type=button].cancel {
|
||||
color: var(--error-text-soft);
|
||||
border-color: var(--error-soft);
|
||||
background: var(--error-surface);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type=button].black,
|
||||
input[type=submit].black {
|
||||
color: var(--text-heading);
|
||||
border-color: var(--line);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type=button].center {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
input[type=button].save {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
|
||||
input[type=checkbox] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background-color: var(--surface-input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked {
|
||||
border-color: var(--checkbox-checked-border);
|
||||
background-color: var(--checkbox-checked);
|
||||
}
|
||||
|
||||
input[type=checkbox]:before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked:before {
|
||||
content: "\2713";
|
||||
color: var(--text-on-accent);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
/* Input states */
|
||||
|
||||
.changed {
|
||||
border-color: var(--accent-hover) !important;
|
||||
box-shadow: 0 0 0 3px var(--accent-soft) !important;
|
||||
}
|
||||
|
||||
.notAvailable {
|
||||
opacity: 0.58;
|
||||
cursor: not-allowed;
|
||||
border-color: var(--error-soft) !important;
|
||||
}
|
||||
|
||||
.wizard {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Sidebar menu */
|
||||
|
||||
#branch {
|
||||
min-height: 18px;
|
||||
margin: 2px 0 6px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--error-text);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#main-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
#main-menu li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 42px;
|
||||
padding: 8px 11px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--surface-1);
|
||||
}
|
||||
|
||||
#main-menu li:hover {
|
||||
border-color: var(--accent-soft);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
#main-menu li:focus-visible {
|
||||
border-color: var(--accent-border);
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
}
|
||||
|
||||
#main-menu li.menu-active {
|
||||
border-color: var(--accent-border);
|
||||
background: var(--accent-gradient);
|
||||
box-shadow: 0 10px 18px var(--accent-shadow);
|
||||
}
|
||||
|
||||
#main-menu li.menu-active p {
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
#main-menu li.menu-active img {
|
||||
filter: brightness(0.04) saturate(120%);
|
||||
}
|
||||
|
||||
nav img {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
nav p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-heading);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-active {
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.menu {
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
/* Connection indicator */
|
||||
|
||||
#connection-indicator {
|
||||
margin-left: auto;
|
||||
padding: 5px 11px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#connection-indicator.status-idle {
|
||||
color: var(--text-heading);
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
|
||||
#connection-indicator.status-busy {
|
||||
color: var(--text-on-accent);
|
||||
border: 1px solid var(--accent-bright);
|
||||
background: var(--accent-gradient);
|
||||
}
|
||||
|
||||
#connection-indicator.status-online {
|
||||
color: var(--text-on-ok);
|
||||
border: 1px solid var(--ok-border);
|
||||
background: linear-gradient(135deg, var(--ok-bright) 0%, var(--ok-mid) 100%);
|
||||
}
|
||||
|
||||
#connection-indicator.status-offline {
|
||||
color: var(--text-on-error);
|
||||
border: 1px solid var(--error-border);
|
||||
background: linear-gradient(135deg, var(--error-bright) 0%, var(--error-strong) 100%);
|
||||
}
|
||||
|
||||
/* Server info and stream tables */
|
||||
|
||||
#clientInfo,
|
||||
#activeStreams,
|
||||
#inactiveStreams {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 7px;
|
||||
background: var(--surface-0);
|
||||
}
|
||||
|
||||
#clientInfo {
|
||||
padding: 7px 10px 6px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
#clientInfo tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#clientInfo .tdKey,
|
||||
#clientInfo .tdVal,
|
||||
#activeStreams .tdKey,
|
||||
#inactiveStreams .tdKey,
|
||||
#activeStreams .tdVal,
|
||||
#inactiveStreams .tdVal,
|
||||
#logInfo .tdVal,
|
||||
#mappingInfo .tdVal {
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 8px;
|
||||
padding: 7px 8px;
|
||||
vertical-align: middle;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
#clientInfo .tdKey,
|
||||
#activeStreams .tdKey,
|
||||
#inactiveStreams .tdKey {
|
||||
color: var(--accent-text);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#clientInfo .tdVal,
|
||||
#activeStreams .tdVal,
|
||||
#inactiveStreams .tdVal {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#clientInfo .tdVal a {
|
||||
color: var(--accent-text);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--link-underline);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#clientInfo .tdVal a:hover,
|
||||
#clientInfo .tdVal a:focus {
|
||||
color: var(--accent-text-hover);
|
||||
border-bottom-color: var(--accent-text-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#myStreamsBox {
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
background: var(--surface-1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#allStreams {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#activeStreams,
|
||||
#inactiveStreams {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#inactiveStreams .tdKey {
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
/* Status cards */
|
||||
|
||||
.dashboard-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(130px, 1fr));
|
||||
gap: 9px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
background: var(--surface-0);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-2);
|
||||
padding: 10px;
|
||||
min-height: 74px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-card-label {
|
||||
color: var(--text-soft);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
}
|
||||
|
||||
.status-card-value {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-card.status-card-ok {
|
||||
border-color: var(--ok-soft);
|
||||
background: linear-gradient(180deg, var(--ok-surface-top) 0%, var(--ok-surface-bottom) 100%);
|
||||
}
|
||||
|
||||
.status-card.status-card-warn {
|
||||
border-color: var(--warn-soft);
|
||||
background: linear-gradient(180deg, var(--warn-surface-top) 0%, var(--warn-surface-bottom) 100%);
|
||||
}
|
||||
|
||||
.status-card.status-card-error {
|
||||
border-color: var(--error-soft);
|
||||
background: linear-gradient(180deg, var(--error-surface-top) 0%, var(--error-surface-bottom) 100%);
|
||||
}
|
||||
|
||||
/* Content table */
|
||||
|
||||
#box-wrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-1);
|
||||
}
|
||||
|
||||
#content_table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#content_table .content_table_header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: linear-gradient(180deg, var(--table-head-top), var(--table-head-bottom));
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
#content_table .content_table_header td {
|
||||
color: var(--accent-text);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
#content_table tr {
|
||||
border-left: 3px solid transparent;
|
||||
border-bottom: 1px solid var(--row-line);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
#content_table tr:hover {
|
||||
background-color: var(--row-hover);
|
||||
}
|
||||
|
||||
#content_table tr[tabindex],
|
||||
#content_table td[tabindex] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#content_table tr[tabindex]:focus-visible,
|
||||
#content_table td[tabindex]:focus-visible,
|
||||
.keyboard-clickable:focus-visible {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--row-focus);
|
||||
}
|
||||
|
||||
#content_table td {
|
||||
padding: 7px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#content_table img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 34px;
|
||||
max-height: 30px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#content_table input[type=text] {
|
||||
min-width: 48px;
|
||||
max-width: 76px;
|
||||
margin: 0;
|
||||
padding: 6px 7px;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#content_table input[type=checkbox] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#content_table .mapping-edit-button {
|
||||
min-height: 32px;
|
||||
min-width: 64px;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#content_table tr.activeEPG {
|
||||
border-left-color: var(--ok);
|
||||
}
|
||||
|
||||
#content_table tr.notActiveEPG {
|
||||
border-left-color: var(--error);
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
|
||||
#settings {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#content_settings h4 {
|
||||
margin: 15px 0 8px;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
#content_settings table {
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#content_settings td {
|
||||
border-bottom: 1px solid var(--row-line);
|
||||
}
|
||||
|
||||
#content_settings tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
#content_settings pre {
|
||||
color: var(--text-soft);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Popup and detail tables */
|
||||
|
||||
#popup-custom table,
|
||||
#content_settings table,
|
||||
#mapping-detail-table,
|
||||
#user-detail-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
}
|
||||
|
||||
#popup-custom td,
|
||||
#content_settings td,
|
||||
#mapping-detail-table td,
|
||||
#user-detail-table td {
|
||||
padding: 2px 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#popup-custom td.left,
|
||||
#mapping-detail-table td.left,
|
||||
#user-detail-table td.left {
|
||||
width: 38%;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#popup-custom input[type=text],
|
||||
#popup-custom input[type=password],
|
||||
#mapping-detail input[type=text],
|
||||
#content_settings input[type=text],
|
||||
#content_settings input[type=password] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mapping-detail img {
|
||||
display: block;
|
||||
max-height: 44px;
|
||||
margin: 8px auto 12px;
|
||||
}
|
||||
|
||||
#file-detail input[type=text] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Loading and popups */
|
||||
|
||||
#loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background-color: var(--overlay);
|
||||
backdrop-filter: blur(1.5px);
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid transparent;
|
||||
border-top-color: var(--loader);
|
||||
border-right-color: var(--accent-strong);
|
||||
animation: spin 0.9s linear infinite;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: var(--overlay);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 1500;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#popup-custom,
|
||||
#mapping-detail,
|
||||
#user-detail,
|
||||
#file-detail {
|
||||
margin: 0 auto;
|
||||
max-width: 760px;
|
||||
max-height: calc(100vh - 36px);
|
||||
overflow: auto;
|
||||
border-radius: var(--radius-l);
|
||||
border: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, var(--panel-top) 0%, var(--panel-bottom) 100%);
|
||||
box-shadow: var(--shadow-1);
|
||||
padding: 16px;
|
||||
animation: popupIn 0.2s ease;
|
||||
}
|
||||
|
||||
#popup-custom h3 {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popup-title h3 {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#popup-custom .popup-title h3 {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text-heading);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: none;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.popup-close:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.interaction,
|
||||
#interaction {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.interaction input[type=button],
|
||||
.interaction input[type=submit] {
|
||||
margin: 0;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
#popup-interaction {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
|
||||
#notification {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
width: 260px;
|
||||
max-height: calc(100vh - 24px);
|
||||
overflow: auto;
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface-solid);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
#notification .element {
|
||||
margin: 8px;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid var(--ok);
|
||||
background: var(--surface-3);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#notification h5 {
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#notification p {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Log lines */
|
||||
|
||||
#content_log {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#content_log pre {
|
||||
margin-bottom: 5px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-1);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
#logScreen p {
|
||||
white-space: pre;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.screenLogHidden {
|
||||
transform: translate(0, -110px);
|
||||
}
|
||||
|
||||
.infoMsg {
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.errorMsg {
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.warningMsg {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.debugMsg {
|
||||
color: var(--debug);
|
||||
}
|
||||
|
||||
/* Category badges */
|
||||
|
||||
.News,
|
||||
.Movie,
|
||||
.Series,
|
||||
.Sports,
|
||||
.Kids {
|
||||
border-left: 3px solid transparent;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.News {
|
||||
border-color: #ff8f6e;
|
||||
}
|
||||
|
||||
.Movie {
|
||||
border-color: #46a4ff;
|
||||
}
|
||||
|
||||
.Series {
|
||||
border-color: #ffd267;
|
||||
}
|
||||
|
||||
.Sports {
|
||||
border-color: #5ee495;
|
||||
}
|
||||
|
||||
.Kids {
|
||||
border-color: #f6a8ff;
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
/*
|
||||
* layout.css - shell layout (sidebar, header, content wrapper), the auth /
|
||||
* wizard box pages and every responsive breakpoint. Loaded last.
|
||||
*/
|
||||
|
||||
/* Application shell */
|
||||
|
||||
#layout {
|
||||
display: grid;
|
||||
grid-template-columns: 290px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#layout-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-left {
|
||||
min-width: 0;
|
||||
border-radius: var(--radius-l);
|
||||
border: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, var(--panel-top) 0%, var(--panel-bottom) 100%),
|
||||
radial-gradient(circle at 20% 0%, var(--panel-glow), transparent 48%);
|
||||
box-shadow: var(--shadow-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#menu-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 100%;
|
||||
height: 76px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line-soft);
|
||||
background:
|
||||
linear-gradient(180deg, var(--logo-tint-top), var(--logo-tint-bottom)),
|
||||
url("../img/logo_w_600x200.png") center no-repeat;
|
||||
background-size: contain;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.layout-right {
|
||||
min-width: 0;
|
||||
border-radius: var(--radius-l);
|
||||
border: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, var(--panel-top) 0%, var(--panel-bottom) 100%),
|
||||
radial-gradient(circle at 100% 0%, var(--panel-glow), transparent 34%);
|
||||
box-shadow: var(--shadow-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#shell-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
background: var(--surface-0);
|
||||
}
|
||||
|
||||
#shell-title {
|
||||
margin: 0;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-heading);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
display: none;
|
||||
min-width: 72px;
|
||||
padding: 8px 14px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Content wrapper */
|
||||
|
||||
#content {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#content h3 {
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
#content-interaction {
|
||||
margin: 0 0 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#content-interaction .search {
|
||||
width: min(260px, 100%);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mobile-only-control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Login, first-user, wizard and maintenance box */
|
||||
|
||||
#box {
|
||||
width: min(680px, calc(100% - 24px));
|
||||
margin: 14px auto;
|
||||
border-radius: var(--radius-l);
|
||||
border: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, var(--panel-top) 0%, var(--panel-bottom) 100%);
|
||||
box-shadow: var(--shadow-2);
|
||||
min-height: 430px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#header,
|
||||
#header_config {
|
||||
width: min(680px, calc(100% - 24px));
|
||||
height: 84px;
|
||||
margin: 24px auto 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, var(--logo-tint-top), var(--logo-tint-bottom)),
|
||||
url("../img/logo_w_600x200.png") center no-repeat;
|
||||
background-size: 300px auto;
|
||||
}
|
||||
|
||||
#headline {
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--surface-0);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
#box #content {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
#box p {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#box-footer {
|
||||
margin-top: auto;
|
||||
padding: 8px 14px 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#box input[type=text],
|
||||
#box input[type=password],
|
||||
#box select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#box input[type=submit],
|
||||
#box input[type=button] {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.auth-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.auth-screen #clientInfo {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.phone {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
/* Breakpoints */
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
.dashboard-cards {
|
||||
grid-template-columns: repeat(3, minmax(130px, 1fr));
|
||||
}
|
||||
|
||||
#clientInfo {
|
||||
border-spacing: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
#layout {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#shell-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#clientInfo .tdKey,
|
||||
#clientInfo .tdVal {
|
||||
font-size: 10px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
#content-interaction {
|
||||
gap: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
#layout {
|
||||
grid-template-columns: 1fr;
|
||||
padding-top: max(10px, env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
#layout-overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
background: var(--overlay-soft);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
body.menu-open #layout-overlay {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-left {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: min(320px, calc(100vw - 34px));
|
||||
max-width: none;
|
||||
z-index: 1100;
|
||||
transform: translateX(-116%);
|
||||
transition: transform 0.25s ease;
|
||||
border-radius: 0 16px 16px 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
body.menu-open .layout-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#main-menu li {
|
||||
min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
#main-menu li p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.layout-right {
|
||||
min-height: calc(100vh - 24px);
|
||||
}
|
||||
|
||||
#shell-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 8;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 42px;
|
||||
min-width: 92px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#clientInfo,
|
||||
.dashboard-cards {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 10px;
|
||||
padding-bottom: calc(14px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
#content-interaction {
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
z-index: 6;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-solid);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#content-interaction input[type=button],
|
||||
#content-interaction select {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
#allStreams {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#activeStreams,
|
||||
#inactiveStreams {
|
||||
max-height: 96px;
|
||||
}
|
||||
|
||||
body.menu-log-focus #clientInfo,
|
||||
body.menu-log-focus .dashboard-cards,
|
||||
body.menu-log-focus #myStreamsBox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.menu-log-focus #content {
|
||||
padding: 8px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
body.menu-log-focus #content h3 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
body.menu-log-focus #content-interaction {
|
||||
top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
body.menu-log-focus #content-interaction .search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.menu-log-focus #content-interaction input[type=button] {
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
body.menu-log-focus #content_log {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
body.menu-log-focus #content_log pre {
|
||||
margin-bottom: 4px;
|
||||
padding: 7px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.phone {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 760px) {
|
||||
#shell-header {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
#clientInfo {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
border-spacing: 0;
|
||||
white-space: normal;
|
||||
background: var(--surface-0);
|
||||
}
|
||||
|
||||
#clientInfo tr {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
#clientInfo .tdKey {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#clientInfo .tdVal {
|
||||
font-size: 12px;
|
||||
min-height: 54px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 10px 9px;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
#clientInfo .tdVal::before {
|
||||
content: attr(data-label);
|
||||
color: var(--text-soft);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.dashboard-cards {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
#content_table {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#content_table .content_table_header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#content_table tbody,
|
||||
#content_table tr,
|
||||
#content_table td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content_table tr {
|
||||
border: 1px solid var(--line-soft);
|
||||
border-left-width: 3px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px;
|
||||
background: var(--surface-3);
|
||||
}
|
||||
|
||||
#content_table td {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
margin: 4px 0;
|
||||
padding: 7px 8px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(92px, 36%) 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
#content_table td::before {
|
||||
content: attr(data-label);
|
||||
color: var(--text-soft);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#content_table td[data-cell-type="checkbox"],
|
||||
#content_table td[data-cell-type="image"],
|
||||
#content_table td[data-cell-type="action"] {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
#content_table td[data-cell-type="checkbox"]::before,
|
||||
#content_table td[data-cell-type="image"]::before,
|
||||
#content_table td[data-cell-type="action"]::before {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#content_table td[data-cell-type="image"] img {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#content_table input[type=text] {
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
#content_table input[type=checkbox] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#content_table .mapping-edit-button {
|
||||
min-height: 38px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-only-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 620px) {
|
||||
#layout {
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.layout-right {
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
#shell-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#shell-title {
|
||||
flex: 1 1 auto;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
#connection-indicator {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#clientInfo {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.dashboard-cards {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
#content-interaction {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#content-interaction .search {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#content-interaction input[type=button] {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
#box {
|
||||
width: calc(100% - 14px);
|
||||
min-height: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#header,
|
||||
#header_config {
|
||||
width: calc(100% - 14px);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
#box-footer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#box-footer input[type=button],
|
||||
#box-footer input[type=submit] {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 619px) {
|
||||
h1 {
|
||||
font-size: 1.46rem;
|
||||
}
|
||||
|
||||
#popup {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#popup-custom,
|
||||
#mapping-detail,
|
||||
#user-detail,
|
||||
#file-detail {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
padding: 10px 10px calc(14px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
#popup-custom table {
|
||||
border-spacing: 0 10px;
|
||||
}
|
||||
|
||||
#popup-custom tr {
|
||||
display: block;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 10px;
|
||||
padding: 8px 8px 10px;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
#popup-custom td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
#popup-custom td.left {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
#popup-interaction {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
background: linear-gradient(180deg, var(--sticky-fade-from) 0%, var(--sticky-fade-to) 30%);
|
||||
}
|
||||
|
||||
.interaction,
|
||||
#interaction {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.interaction input[type=button],
|
||||
.interaction input[type=submit],
|
||||
#popup-interaction input[type=button] {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.popup-title h3 {
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 430px) {
|
||||
#clientInfo {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
nav img {
|
||||
display: block;
|
||||
max-height: 20px;
|
||||
max-width: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
nav p {
|
||||
text-align: left;
|
||||
padding: 0px 30px;
|
||||
}
|
||||
|
||||
#layout {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.layout-left {
|
||||
display: block;
|
||||
min-width: 150px;
|
||||
max-width: 20%;
|
||||
background-color: #111;
|
||||
height: inherit;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.layout-right {
|
||||
display: block;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
#menu-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
min-width: 180px;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: url("../img/logo_w_600x200.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
|
||||
#page {
|
||||
max-width: 950px;
|
||||
margin: auto;
|
||||
background-color: #444;
|
||||
|
||||
/*
|
||||
height: -webkit-calc(100% - 130px);
|
||||
height: -moz-calc(100% - 130px);
|
||||
height: calc(100% - 130px);
|
||||
*/
|
||||
|
||||
min-height: -webkit-calc(100% - 120px);
|
||||
min-height: -moz-calc(100% - 120px);
|
||||
min-height: calc(100% - 120px);
|
||||
|
||||
|
||||
box-shadow: 0px 5px 5px #222;
|
||||
|
||||
}
|
||||
|
||||
#uiSetting {
|
||||
float: right;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
#box input[type=text], #box input[type=password] {
|
||||
width: -webkit-calc(100% - 20px);
|
||||
width: -moz-calc(100% - 20px);
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#box input[type=submit]{
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
#settings {
|
||||
display: block;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
#settings h5 {
|
||||
margin: 50px 0px 10px 0px;
|
||||
}
|
||||
|
||||
#content-interaction .search {
|
||||
width: 200px;
|
||||
border: 1px solid #000;
|
||||
padding: 9px;
|
||||
background-color: #333;
|
||||
margin: 10px;
|
||||
float: right;
|
||||
border-radius: 3px;
|
||||
|
||||
}
|
||||
|
||||
#myStreams {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
background-color: #111;
|
||||
width: 100%;
|
||||
max-width: 950px;
|
||||
|
||||
/*
|
||||
max-height: 100px;
|
||||
*/
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#myStreams img {
|
||||
width: 4%;
|
||||
padding: 2px 5px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#settings-footer {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Wizard*/
|
||||
#box {
|
||||
background-color: #444;
|
||||
min-height: 400px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#box p{
|
||||
padding: 10px 0px;
|
||||
}
|
||||
|
||||
#box-footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
#box-footer {
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#headline {
|
||||
background-color: #222;
|
||||
border-bottom: solid 2px #222;
|
||||
transition: all 0.5s;
|
||||
padding: 10px 0px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
||||
|
||||
#clientInfo, #activeStreams, #inactiveStreams {
|
||||
font-family: monospace;
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
background-color: #111;
|
||||
color: #00E6FF;
|
||||
border-bottom: solid 0px;;
|
||||
padding: 0px;
|
||||
letter-spacing: 1px;
|
||||
overflow-x: hidden;
|
||||
border-spacing: 4px 4px;
|
||||
border-bottom: solid 1px #444;
|
||||
}
|
||||
|
||||
#myStreamsBox {
|
||||
position: relative;
|
||||
padding: 0px;
|
||||
/*height: 100px;*/
|
||||
max-height: 150px;
|
||||
background-color: #111;
|
||||
color: white;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
#openStreams {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background: url("../img/touch.png");
|
||||
background-color: #111;
|
||||
|
||||
background-position: bottom right;
|
||||
}
|
||||
|
||||
#allStreams {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#activeStreams, #inactiveStreams {
|
||||
overflow-y: scroll;
|
||||
width: 50%;
|
||||
max-height: 100px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#activeStreams .tdKey, #inactiveStreams .tdKey {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#inactiveStreams .tdKey {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#clientInfo .tdVal, #logInfo .tdVal, #activeStreams .tdVal, #inactiveStreams .tdVal, #mappingInfo .tdVal{
|
||||
color: #aaa;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
#box-wrapper {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#content_table, #mapping-detail-table, #content_table {
|
||||
display: table;
|
||||
|
||||
border-collapse: collapse;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
#content_table .content_table_header {
|
||||
background-color: #333;
|
||||
height: 50px;
|
||||
border-bottom: solid 1px #111;
|
||||
border-left: solid 3px #333;
|
||||
cursor: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.tableEllipsis {
|
||||
width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#content_table img {
|
||||
display: block;
|
||||
max-height: 28px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
#content_table tr{
|
||||
border-left: solid 3px 444;
|
||||
border-bottom: solid 1px #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#content_table tr:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
#content_table td {
|
||||
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
#content_table input[type=text]{
|
||||
width: 80%;
|
||||
min-width: 35px;
|
||||
max-width: 60px;
|
||||
border: 0px;
|
||||
background-color: #333;
|
||||
margin-left: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#content_table input[type=checkbox]{
|
||||
max-width: 25px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
||||
.showBulk {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hideBulk {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.noBulk {
|
||||
|
||||
}
|
||||
|
||||
#content_table tr.activeEPG{
|
||||
border-left: solid 3px lawngreen;
|
||||
}
|
||||
|
||||
#content_table tr.notActiveEPG{
|
||||
border-left: solid 3px red;
|
||||
}
|
||||
|
||||
|
||||
#logScreen p{
|
||||
white-space: pre;
|
||||
font-size: 10px;
|
||||
/*
|
||||
line-height: 1.6em;
|
||||
font-family: "Arial", sans-serif;
|
||||
*/
|
||||
letter-spacing: 1px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
#popup {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#mapping-detail, #user-detail, #file-detail, #popup-custom {
|
||||
box-shadow: 0px 5px 40px #000;
|
||||
margin-top: 20px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
max-width: 600px;
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
#popup-custom h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#file-detail input[type=text] {
|
||||
width: -webkit-calc(100% - 20px);
|
||||
width: -moz-calc(100% - 20px);
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#mapping-detail img {
|
||||
display: block;
|
||||
max-height: 30px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#popup-custom input[type=text], #popup-custom input[type=password], #mapping-detail input[type=text], #content_settings input[type=text], #content_settings input[type=password]{
|
||||
border: solid 1px;
|
||||
border-color: transparent;
|
||||
background-color: #333;
|
||||
text-align: left;
|
||||
width: -webkit-calc(100% - 20px);
|
||||
width: -moz-calc(100% - 20px);
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#popup-custom input[type=text].notAvailable {
|
||||
border-color: red;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#mapping-detail-table, #user-detail-table {
|
||||
display: inline-table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#popup-custom table, #content_settings table {
|
||||
display: inline-table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
#mapping-detail-table td, #user-detail-table td {
|
||||
padding: 10px 0px;
|
||||
|
||||
}
|
||||
|
||||
#mapping-detail-table td.left, #user-detail-table td.left, #popup-custom td.left {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.interaction, #interaction {
|
||||
margin-top: 20px;
|
||||
display: inline-flex;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.interaction input[type=button], .interaction input[type=submit] {
|
||||
background-color: #000;
|
||||
min-width: 100px;
|
||||
margin: 0px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#notification {
|
||||
display: block;
|
||||
position: fixed;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
width: 250px;
|
||||
|
||||
background-color: #222;
|
||||
box-shadow: 0px 0px 20px #000;
|
||||
}
|
||||
|
||||
#notification h5 {
|
||||
background-color: #121212;
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
|
||||
#notification pre {
|
||||
padding: 0px 10px 0px 10px;
|
||||
}
|
||||
|
||||
#notification p {
|
||||
font-size: 10 px;
|
||||
margin: 0px;
|
||||
padding: 0px 10px 5px 10px;
|
||||
}
|
||||
|
||||
#notification .element {
|
||||
/*padding: 0px 5px;*/
|
||||
margin: 5px 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #181818;
|
||||
border-left: 10px solid green;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (min-width: 620px){
|
||||
body {
|
||||
width: 100%;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
nav p {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#header_config {
|
||||
display: block;
|
||||
height: 100px;
|
||||
background: url("../img/logo_w_600x200.png");
|
||||
background-repeat: no-repeat;
|
||||
|
||||
background-size: 300px 100px;
|
||||
}
|
||||
|
||||
#screenLog {
|
||||
margin-left: 300px;
|
||||
|
||||
transition: none;
|
||||
background-color: transparent;
|
||||
border-bottom: solid 1px transparent;
|
||||
box-shadow: 0px 0px 0px #222;
|
||||
}
|
||||
|
||||
#settings {
|
||||
/*
|
||||
height: -webkit-calc(100% - 100px);
|
||||
height: -moz-calc(100% - 100px);
|
||||
height: calc(100% - 100px);
|
||||
*/
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.screenLogHidden {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
|
||||
|
||||
#box {
|
||||
display: block;
|
||||
min-height: 500px;
|
||||
max-width: 500px;
|
||||
margin: 10px auto;
|
||||
background-color: #444;
|
||||
box-shadow: 0px 5px 5px #222;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#settings, #settings-footer {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* tokens.css - design tokens (colours, spacing, radii, fonts, shadows).
|
||||
*
|
||||
* Load order: tokens.css, base.css, components.css, layout.css.
|
||||
*
|
||||
* The dark palette is the default. A light palette is applied when the user
|
||||
* agent prefers a light colour scheme, and either palette can be forced with
|
||||
* <html data-theme="dark"> or <html data-theme="light">, which always wins
|
||||
* over the media query. Only colour tokens change between themes.
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: "Space Grotesk", "Avenir Next", "Trebuchet MS", sans-serif;
|
||||
--font-mono: "IBM Plex Mono", "SFMono-Regular", "Consolas", monospace;
|
||||
|
||||
/* Radii and shadows */
|
||||
--radius-s: 10px;
|
||||
--radius-m: 14px;
|
||||
--radius-l: 18px;
|
||||
--shadow-1: 0 20px 45px rgba(0, 0, 0, 0.32);
|
||||
--shadow-2: 0 12px 30px rgba(0, 0, 0, 0.28);
|
||||
|
||||
/* Page background */
|
||||
--bg-0: #07111d;
|
||||
--bg-1: #0c1a2b;
|
||||
--bg-2: #12233a;
|
||||
--bg-deep: #060d17;
|
||||
--bg-glow-1: rgba(64, 132, 183, 0.22);
|
||||
--bg-glow-2: rgba(18, 185, 255, 0.16);
|
||||
|
||||
/* Panels and surfaces (translucent tints layered over the page) */
|
||||
--panel-top: rgba(17, 34, 56, 0.97);
|
||||
--panel-bottom: rgba(12, 27, 44, 0.97);
|
||||
--panel-glow: rgba(53, 210, 255, 0.12);
|
||||
--surface-0: rgba(8, 20, 33, 0.54);
|
||||
--surface-1: rgba(10, 22, 36, 0.72);
|
||||
--surface-2: rgba(13, 31, 49, 0.8);
|
||||
--surface-3: rgba(17, 35, 56, 0.84);
|
||||
--surface-solid: rgba(10, 22, 36, 0.92);
|
||||
--surface-raised: rgba(14, 32, 50, 0.85);
|
||||
--surface-input: rgba(10, 22, 36, 0.78);
|
||||
--surface-input-focus: rgba(9, 25, 40, 0.95);
|
||||
--logo-tint-top: rgba(26, 49, 73, 0.55);
|
||||
--logo-tint-bottom: rgba(13, 28, 45, 0.6);
|
||||
--table-head-top: rgba(23, 46, 72, 0.98);
|
||||
--table-head-bottom: rgba(16, 32, 50, 0.98);
|
||||
--row-line: rgba(29, 55, 80, 0.72);
|
||||
--row-hover: rgba(28, 53, 79, 0.5);
|
||||
--row-focus: rgba(39, 73, 106, 0.56);
|
||||
--overlay: rgba(3, 9, 16, 0.72);
|
||||
--overlay-soft: rgba(3, 9, 15, 0.48);
|
||||
--sticky-fade-from: rgba(12, 27, 44, 0.1);
|
||||
--sticky-fade-to: rgba(12, 27, 44, 0.96);
|
||||
--scroll-track: rgba(7, 17, 29, 0.65);
|
||||
--scroll-thumb-a: #2d597f;
|
||||
--scroll-thumb-b: #1e4060;
|
||||
--scroll-thumb-hover-a: #3678ac;
|
||||
--scroll-thumb-hover-b: #20537c;
|
||||
|
||||
/* Lines */
|
||||
--line: #274462;
|
||||
--line-soft: #1b334d;
|
||||
--line-strong: #3a5877;
|
||||
|
||||
/* Text */
|
||||
--text: #e9f5ff;
|
||||
--text-heading: #d6efff;
|
||||
--text-muted: #b7cee1;
|
||||
--text-soft: #8fb3cf;
|
||||
--text-placeholder: #7e9ab4;
|
||||
--text-on-accent: #03101a;
|
||||
--text-on-ok: #052219;
|
||||
--text-on-error: #ffe5e5;
|
||||
|
||||
/* Accent */
|
||||
--accent: #35d2ff;
|
||||
--accent-strong: #12b9ff;
|
||||
--accent-bright: #67e8ff;
|
||||
--accent-hover: #8de7ff;
|
||||
--accent-text: #8fd8ff;
|
||||
--accent-text-hover: #d7f4ff;
|
||||
--accent-soft: rgba(53, 210, 255, 0.2);
|
||||
--accent-ring: rgba(53, 210, 255, 0.17);
|
||||
--accent-border: rgba(103, 232, 255, 0.55);
|
||||
--accent-shadow: rgba(18, 185, 255, 0.3);
|
||||
--accent-shadow-strong: rgba(18, 185, 255, 0.36);
|
||||
--accent-gradient: linear-gradient(135deg, var(--accent-bright) 0%, var(--accent-strong) 100%);
|
||||
--link-underline: rgba(143, 220, 255, 0.6);
|
||||
--focus-ring: #7de5ff;
|
||||
--focus-ring-soft: #91eaff;
|
||||
--checkbox-checked: #39d6ff;
|
||||
--checkbox-checked-border: #89ebff;
|
||||
--loader: #58ddff;
|
||||
|
||||
/* Status */
|
||||
--ok: #2fd18a;
|
||||
--ok-bright: #92f0c0;
|
||||
--ok-mid: #4fce93;
|
||||
--ok-border: #9ff0c8;
|
||||
--ok-soft: rgba(67, 191, 130, 0.48);
|
||||
--ok-surface-top: rgba(20, 54, 44, 0.9);
|
||||
--ok-surface-bottom: rgba(12, 38, 31, 0.95);
|
||||
--warn: #ffc15a;
|
||||
--warn-soft: rgba(255, 193, 90, 0.5);
|
||||
--warn-surface-top: rgba(68, 50, 20, 0.86);
|
||||
--warn-surface-bottom: rgba(44, 32, 14, 0.96);
|
||||
--error: #ff6f6f;
|
||||
--error-text: #ff8d8d;
|
||||
--error-text-soft: #ffd8d8;
|
||||
--error-bright: #ff7f7f;
|
||||
--error-strong: #e94343;
|
||||
--error-border: #ff9e9e;
|
||||
--error-soft: rgba(255, 111, 111, 0.45);
|
||||
--error-surface: rgba(87, 22, 22, 0.45);
|
||||
--error-surface-top: rgba(73, 26, 26, 0.84);
|
||||
--error-surface-bottom: rgba(47, 19, 19, 0.96);
|
||||
--error-shadow: rgba(233, 67, 67, 0.35);
|
||||
--info: #87b0d1;
|
||||
--debug: #d687ff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Light palette. Only tokens that differ from the dark palette are listed.
|
||||
* The same block appears twice on purpose: once for the media query (skipped
|
||||
* when dark is forced with data-theme="dark") and once for the explicit
|
||||
* data-theme="light" override. Keep the two copies identical.
|
||||
*/
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme="dark"]) {
|
||||
color-scheme: light;
|
||||
|
||||
--bg-0: #e9eff5;
|
||||
--bg-1: #f5f8fb;
|
||||
--bg-2: #dfe7ef;
|
||||
--bg-deep: #dfe7ef;
|
||||
--bg-glow-1: rgba(64, 132, 183, 0.14);
|
||||
--bg-glow-2: rgba(18, 185, 255, 0.12);
|
||||
|
||||
--panel-top: rgba(255, 255, 255, 0.97);
|
||||
--panel-bottom: rgba(246, 249, 252, 0.97);
|
||||
--panel-glow: rgba(18, 185, 255, 0.08);
|
||||
--surface-0: rgba(255, 255, 255, 0.55);
|
||||
--surface-1: rgba(255, 255, 255, 0.75);
|
||||
--surface-2: rgba(236, 243, 249, 0.85);
|
||||
--surface-3: rgba(232, 240, 247, 0.9);
|
||||
--surface-solid: rgba(255, 255, 255, 0.94);
|
||||
--surface-raised: rgba(228, 236, 244, 0.9);
|
||||
--surface-input: rgba(255, 255, 255, 0.85);
|
||||
--surface-input-focus: rgba(255, 255, 255, 1);
|
||||
--logo-tint-top: rgba(15, 31, 48, 0.96);
|
||||
--logo-tint-bottom: rgba(11, 23, 39, 0.98);
|
||||
--table-head-top: rgba(232, 240, 248, 0.98);
|
||||
--table-head-bottom: rgba(222, 232, 243, 0.98);
|
||||
--row-line: rgba(100, 130, 160, 0.3);
|
||||
--row-hover: rgba(0, 90, 150, 0.07);
|
||||
--row-focus: rgba(0, 90, 150, 0.14);
|
||||
--overlay: rgba(20, 35, 52, 0.5);
|
||||
--overlay-soft: rgba(20, 35, 52, 0.35);
|
||||
--sticky-fade-from: rgba(246, 249, 252, 0.1);
|
||||
--sticky-fade-to: rgba(246, 249, 252, 0.96);
|
||||
--scroll-track: rgba(200, 214, 228, 0.6);
|
||||
--scroll-thumb-a: #9fb6cb;
|
||||
--scroll-thumb-b: #7f9bb3;
|
||||
--scroll-thumb-hover-a: #7f9bb3;
|
||||
--scroll-thumb-hover-b: #61819c;
|
||||
|
||||
--line: #b9cadb;
|
||||
--line-soft: #d3dfea;
|
||||
--line-strong: #9fb4c8;
|
||||
|
||||
--text: #142434;
|
||||
--text-heading: #0f2033;
|
||||
--text-muted: #4a6379;
|
||||
--text-soft: #526a80;
|
||||
--text-placeholder: #58708a;
|
||||
--text-on-error: #4a0c0c;
|
||||
|
||||
--accent: #0069a8;
|
||||
--accent-hover: #00517f;
|
||||
--accent-text: #0b6a95;
|
||||
--accent-text-hover: #003a5c;
|
||||
--accent-soft: rgba(0, 105, 168, 0.2);
|
||||
--accent-ring: rgba(0, 105, 168, 0.18);
|
||||
--accent-border: rgba(0, 105, 168, 0.5);
|
||||
--accent-shadow: rgba(0, 105, 168, 0.22);
|
||||
--accent-shadow-strong: rgba(0, 105, 168, 0.3);
|
||||
--link-underline: rgba(11, 106, 149, 0.5);
|
||||
--focus-ring: #0a7fc0;
|
||||
--focus-ring-soft: #0a7fc0;
|
||||
--checkbox-checked: #12b9ff;
|
||||
--checkbox-checked-border: #0a7fc0;
|
||||
--loader: #0a7fc0;
|
||||
|
||||
--ok: #12734a;
|
||||
--ok-bright: #a9efcc;
|
||||
--ok-mid: #5fd39b;
|
||||
--ok-border: #4fb884;
|
||||
--ok-soft: rgba(18, 115, 74, 0.45);
|
||||
--ok-surface-top: rgba(226, 246, 235, 0.95);
|
||||
--ok-surface-bottom: rgba(208, 238, 222, 0.98);
|
||||
--warn: #7f5300;
|
||||
--warn-soft: rgba(183, 121, 31, 0.5);
|
||||
--warn-surface-top: rgba(253, 243, 224, 0.95);
|
||||
--warn-surface-bottom: rgba(248, 232, 200, 0.98);
|
||||
--error: #c62828;
|
||||
--error-text: #b02020;
|
||||
--error-text-soft: #8a1a1a;
|
||||
--error-bright: #f6a5a5;
|
||||
--error-strong: #e07575;
|
||||
--error-border: #d95c5c;
|
||||
--error-soft: rgba(198, 40, 40, 0.45);
|
||||
--error-surface: rgba(255, 220, 220, 0.6);
|
||||
--error-surface-top: rgba(253, 232, 232, 0.95);
|
||||
--error-surface-bottom: rgba(248, 214, 214, 0.98);
|
||||
--error-shadow: rgba(198, 40, 40, 0.25);
|
||||
--info: #3f6785;
|
||||
--debug: #7a35b5;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
--bg-0: #e9eff5;
|
||||
--bg-1: #f5f8fb;
|
||||
--bg-2: #dfe7ef;
|
||||
--bg-deep: #dfe7ef;
|
||||
--bg-glow-1: rgba(64, 132, 183, 0.14);
|
||||
--bg-glow-2: rgba(18, 185, 255, 0.12);
|
||||
|
||||
--panel-top: rgba(255, 255, 255, 0.97);
|
||||
--panel-bottom: rgba(246, 249, 252, 0.97);
|
||||
--panel-glow: rgba(18, 185, 255, 0.08);
|
||||
--surface-0: rgba(255, 255, 255, 0.55);
|
||||
--surface-1: rgba(255, 255, 255, 0.75);
|
||||
--surface-2: rgba(236, 243, 249, 0.85);
|
||||
--surface-3: rgba(232, 240, 247, 0.9);
|
||||
--surface-solid: rgba(255, 255, 255, 0.94);
|
||||
--surface-raised: rgba(228, 236, 244, 0.9);
|
||||
--surface-input: rgba(255, 255, 255, 0.85);
|
||||
--surface-input-focus: rgba(255, 255, 255, 1);
|
||||
--logo-tint-top: rgba(15, 31, 48, 0.96);
|
||||
--logo-tint-bottom: rgba(11, 23, 39, 0.98);
|
||||
--table-head-top: rgba(232, 240, 248, 0.98);
|
||||
--table-head-bottom: rgba(222, 232, 243, 0.98);
|
||||
--row-line: rgba(100, 130, 160, 0.3);
|
||||
--row-hover: rgba(0, 90, 150, 0.07);
|
||||
--row-focus: rgba(0, 90, 150, 0.14);
|
||||
--overlay: rgba(20, 35, 52, 0.5);
|
||||
--overlay-soft: rgba(20, 35, 52, 0.35);
|
||||
--sticky-fade-from: rgba(246, 249, 252, 0.1);
|
||||
--sticky-fade-to: rgba(246, 249, 252, 0.96);
|
||||
--scroll-track: rgba(200, 214, 228, 0.6);
|
||||
--scroll-thumb-a: #9fb6cb;
|
||||
--scroll-thumb-b: #7f9bb3;
|
||||
--scroll-thumb-hover-a: #7f9bb3;
|
||||
--scroll-thumb-hover-b: #61819c;
|
||||
|
||||
--line: #b9cadb;
|
||||
--line-soft: #d3dfea;
|
||||
--line-strong: #9fb4c8;
|
||||
|
||||
--text: #142434;
|
||||
--text-heading: #0f2033;
|
||||
--text-muted: #4a6379;
|
||||
--text-soft: #526a80;
|
||||
--text-placeholder: #58708a;
|
||||
--text-on-error: #4a0c0c;
|
||||
|
||||
--accent: #0069a8;
|
||||
--accent-hover: #00517f;
|
||||
--accent-text: #0b6a95;
|
||||
--accent-text-hover: #003a5c;
|
||||
--accent-soft: rgba(0, 105, 168, 0.2);
|
||||
--accent-ring: rgba(0, 105, 168, 0.18);
|
||||
--accent-border: rgba(0, 105, 168, 0.5);
|
||||
--accent-shadow: rgba(0, 105, 168, 0.22);
|
||||
--accent-shadow-strong: rgba(0, 105, 168, 0.3);
|
||||
--link-underline: rgba(11, 106, 149, 0.5);
|
||||
--focus-ring: #0a7fc0;
|
||||
--focus-ring-soft: #0a7fc0;
|
||||
--checkbox-checked: #12b9ff;
|
||||
--checkbox-checked-border: #0a7fc0;
|
||||
--loader: #0a7fc0;
|
||||
|
||||
--ok: #12734a;
|
||||
--ok-bright: #a9efcc;
|
||||
--ok-mid: #5fd39b;
|
||||
--ok-border: #4fb884;
|
||||
--ok-soft: rgba(18, 115, 74, 0.45);
|
||||
--ok-surface-top: rgba(226, 246, 235, 0.95);
|
||||
--ok-surface-bottom: rgba(208, 238, 222, 0.98);
|
||||
--warn: #7f5300;
|
||||
--warn-soft: rgba(183, 121, 31, 0.5);
|
||||
--warn-surface-top: rgba(253, 243, 224, 0.95);
|
||||
--warn-surface-bottom: rgba(248, 232, 200, 0.98);
|
||||
--error: #c62828;
|
||||
--error-text: #b02020;
|
||||
--error-text-soft: #8a1a1a;
|
||||
--error-bright: #f6a5a5;
|
||||
--error-strong: #e07575;
|
||||
--error-border: #d95c5c;
|
||||
--error-soft: rgba(198, 40, 40, 0.45);
|
||||
--error-surface: rgba(255, 220, 220, 0.6);
|
||||
--error-surface-top: rgba(253, 232, 232, 0.95);
|
||||
--error-surface-bottom: rgba(248, 214, 214, 0.98);
|
||||
--error-shadow: rgba(198, 40, 40, 0.25);
|
||||
--info: #3f6785;
|
||||
--debug: #7a35b5;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Package webui embeds the static web interface (HTML, CSS, the compiled
|
||||
// JavaScript bundle, images and the tuner-limit video) into the binary.
|
||||
//
|
||||
// Build the JavaScript bundle with `npm run build` (see README-DEV.md); the
|
||||
// result, html/js/app.js, is committed so `go build` works from a bare clone.
|
||||
package webui
|
||||
|
||||
import "embed"
|
||||
|
||||
// FS holds the web interface files, rooted at the html/ directory.
|
||||
//
|
||||
//go:embed *.html css img js video
|
||||
var FS embed.FS
|
||||
+42
-36
@@ -1,32 +1,30 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!---
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
-->
|
||||
<title>xTeVe</title>
|
||||
<link rel="stylesheet" href="css/screen.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
|
||||
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/menu_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/settings_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/logs_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
|
||||
<link rel="stylesheet" href="css/tokens.css">
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="javascript: PageReady();">
|
||||
<body class="app-shell" onload="javascript: PageReady();">
|
||||
|
||||
<div id="loading" class="none">
|
||||
<a class="skip-link" href="#content">Skip to main content</a>
|
||||
|
||||
<div id="loading" class="none" role="status" aria-live="polite" aria-label="Loading" aria-hidden="true">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<div id="popup" class="none">
|
||||
<div id="popup-custom"></div>
|
||||
<div id="popup" class="none" role="dialog" aria-modal="true" aria-hidden="true" tabindex="-1">
|
||||
<div id="popup-custom" role="document" tabindex="-1"></div>
|
||||
</div>
|
||||
|
||||
<div id="layout-overlay" aria-hidden="true" tabindex="-1"></div>
|
||||
<div id="layout">
|
||||
|
||||
<!--
|
||||
@@ -40,55 +38,62 @@
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div id="menu-wrapper" class="layout-left">
|
||||
<div id= "branch"></div>
|
||||
<aside id="menu-wrapper" class="layout-left" aria-label="Sidebar menu">
|
||||
<div id="branch"></div>
|
||||
<div id="logo"></div>
|
||||
<nav id="main-menu"></nav>
|
||||
</div>
|
||||
<nav id="main-menu" role="menubar" aria-label="Main navigation"></nav>
|
||||
</aside>
|
||||
|
||||
<div class="layout-right">
|
||||
<main id="shell-main" class="layout-right">
|
||||
<header id="shell-header">
|
||||
<button id="menu-toggle" type="button" aria-expanded="false" aria-controls="menu-wrapper" aria-label="Toggle navigation menu">Menu</button>
|
||||
<h1 id="shell-title">xTeVe Control Panel</h1>
|
||||
<p id="connection-indicator" class="status-idle" role="status" aria-live="polite" aria-atomic="true">Connecting...</p>
|
||||
</header>
|
||||
|
||||
<table id="clientInfo" class="">
|
||||
<table id="clientInfo" class="" aria-label="Server information">
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">xTeVe:</td>
|
||||
<td id="version" class="tdVal"> </td>
|
||||
<td id="version" class="tdVal" data-label="xTeVe"> </td>
|
||||
<td class="tdKey">OS:</td>
|
||||
<td id="os" class="tdVal"> </td>
|
||||
<td id="os" class="tdVal" data-label="OS"> </td>
|
||||
<td class="tdKey phone">DVR IP:</td>
|
||||
<td id="DVR" class="tdVal phone"> </td>
|
||||
<td id="DVR" class="tdVal phone" data-label="DVR IP"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">UUID:</td>
|
||||
<td id="uuid" class="tdVal"> </td>
|
||||
<td id="uuid" class="tdVal" data-label="UUID"> </td>
|
||||
<td class="tdKey">Arch:</td>
|
||||
<td id="arch" class="tdVal"> </td>
|
||||
<td id="arch" class="tdVal" data-label="Arch"> </td>
|
||||
<td class="tdKey phone">M3U URL:</td>
|
||||
<td id="m3u-url" class="tdVal phone"> </td>
|
||||
<td id="m3u-url" class="tdVal phone" data-label="M3U URL"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">Available Streams:</td>
|
||||
<td id="streams" class="tdVal"> </td>
|
||||
<td id="streams" class="tdVal" data-label="Available Streams"> </td>
|
||||
<td class="tdKey">EPG Source:</td>
|
||||
<td id="epgSource" class="tdVal"> </td>
|
||||
<td id="epgSource" class="tdVal" data-label="EPG Source"> </td>
|
||||
<td class="tdKey phone">XEPG URL:</td>
|
||||
<td id="xepg-url" class="tdVal phone"> </td>
|
||||
<td id="xepg-url" class="tdVal phone" data-label="XEPG URL"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">XEPG Channels:</td>
|
||||
<td id="xepg" class="tdVal"> </td>
|
||||
<td id="xepg" class="tdVal" data-label="XEPG Channels"> </td>
|
||||
<td class="tdKey">Errors:</td>
|
||||
<td id="errors" class="tdVal"> </td>
|
||||
<td id="errors" class="tdVal" data-label="Errors"> </td>
|
||||
<td class="tdKey">Warnings:</td>
|
||||
<td id="warnings" class="tdVal"> </td>
|
||||
<td id="warnings" class="tdVal" data-label="Warnings"> </td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<div id="myStreamsBox" class="notVisible">
|
||||
<div id="status-cards" class="dashboard-cards" role="list" aria-live="polite" aria-label="System summary"></div>
|
||||
|
||||
<div id="myStreamsBox" class="notVisible" aria-live="polite" aria-label="Stream details">
|
||||
|
||||
<div id="allStreams">
|
||||
<table id="activeStreams"></table>
|
||||
@@ -97,9 +102,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div id="content" class=""></div>
|
||||
<div id="content" class="" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Main content"></div>
|
||||
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
+3333
File diff suppressed because it is too large.
Load diff
@@ -1,42 +0,0 @@
|
||||
function createFirstAccount(elm) {
|
||||
var err = false;
|
||||
var div = document.getElementById(elm);
|
||||
console.log(div);
|
||||
|
||||
var form = document.getElementById('authentication');
|
||||
|
||||
const username = document.getElementById('username');
|
||||
const password = document.getElementById('password');
|
||||
const confirm = document.getElementById('confirm');
|
||||
|
||||
var inputs = div.getElementsByTagName('INPUT')
|
||||
console.log(confirm);
|
||||
|
||||
switch(confirm) {
|
||||
case null: break;
|
||||
|
||||
default:
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].value.length == 0) {
|
||||
inputs[i].style.borderColor = 'red';
|
||||
err = true
|
||||
}
|
||||
}
|
||||
|
||||
switch(err) {
|
||||
case true: return; break;
|
||||
case false:
|
||||
if (password.value != confirm.value) {
|
||||
confirm.style.borderColor = 'red';
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
form.submit();
|
||||
return;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
function login() {
|
||||
var err = false;
|
||||
var data = new Object();
|
||||
var div = document.getElementById("content");
|
||||
var form = document.getElementById("authentication");
|
||||
var inputs = div.getElementsByTagName("INPUT");
|
||||
console.log(inputs);
|
||||
for (var i = inputs.length - 1; i >= 0; i--) {
|
||||
var key = inputs[i].name;
|
||||
var value = inputs[i].value;
|
||||
if (value.length == 0) {
|
||||
inputs[i].style.borderColor = "red";
|
||||
err = true;
|
||||
}
|
||||
data[key] = value;
|
||||
}
|
||||
if (err == true) {
|
||||
data = new Object();
|
||||
return;
|
||||
}
|
||||
if (data.hasOwnProperty("confirm")) {
|
||||
if (data["confirm"] != data["password"]) {
|
||||
alert("sdafsd");
|
||||
document.getElementById('password').style.borderColor = "red";
|
||||
document.getElementById('confirm').style.borderColor = "red";
|
||||
document.getElementById("err").innerHTML = "{{.account.failed}}";
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(data);
|
||||
form.submit();
|
||||
}
|
||||
-331
@@ -1,331 +0,0 @@
|
||||
var config = new Object();
|
||||
var menu = new Object();
|
||||
var subMenu = new Object();
|
||||
var activeStreams = new Object();
|
||||
var xEPG = new Object();
|
||||
var users = new Object();
|
||||
var log = new Object();
|
||||
var undo = new Object();
|
||||
var webSockets = true;
|
||||
var closeLog, version, activeMenu;
|
||||
var columnToSort = 0
|
||||
|
||||
|
||||
if (window.WebSocket === undefined) {
|
||||
alert("Your browser does not support WebSockets");
|
||||
webSockets = false;
|
||||
}
|
||||
|
||||
function pageReady() {
|
||||
var data = new Object();
|
||||
data["cmd"] = "getServerConfig";
|
||||
xTeVe(data);
|
||||
//showLoadingScreen(false);
|
||||
|
||||
var resizeHandle = document.getElementById("openStreams");
|
||||
var box = document.getElementById("myStreamsBox");
|
||||
resizeHandle.addEventListener("mousedown", initialiseResize, false);
|
||||
|
||||
function initialiseResize(e) {
|
||||
window.addEventListener("mousemove", startResizing, false);
|
||||
window.addEventListener("mouseup", stopResizing, false);
|
||||
}
|
||||
|
||||
function startResizing(e) {
|
||||
box.style.height = (e.clientY - box.offsetTop) + "px";
|
||||
|
||||
var elm = document.getElementById("allStreams");
|
||||
if (e.clientY > 120) {
|
||||
elm.className = "visible";
|
||||
} else {
|
||||
elm.className = "notVisible";
|
||||
}
|
||||
|
||||
calculateWrapperHeight();
|
||||
|
||||
}
|
||||
function stopResizing(e) {
|
||||
window.removeEventListener('mousemove', startResizing, false);
|
||||
window.removeEventListener('mouseup', stopResizing, false);
|
||||
calculateWrapperHeight();
|
||||
}
|
||||
|
||||
window.addEventListener("resize", function(){
|
||||
calculateWrapperHeight();
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
||||
function getObjKeys(obj) {
|
||||
var keys = new Array();
|
||||
|
||||
for (var i in obj) {
|
||||
if (obj.hasOwnProperty(i)) {
|
||||
keys.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
function createElement(item) {
|
||||
//console.log(item);
|
||||
var element = document.createElement(item["_element"]);
|
||||
if (item.hasOwnProperty("_text")) {
|
||||
//element.innerHTML = "<p>" + item["_text"] + "</p>";
|
||||
element.innerHTML = item["_text"];
|
||||
}
|
||||
|
||||
var keys = getObjKeys(item);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (keys[i].charAt(0) != "_") {
|
||||
//console.log(keys[i], item[keys[i]]);
|
||||
element.setAttribute(keys[i], item[keys[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
//console.log(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
function modifyOption(id, options, values) {
|
||||
var select = document.getElementById(id);
|
||||
select.innerHTML = "";
|
||||
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
|
||||
var element = document.createElement("OPTION")
|
||||
|
||||
element.value = values[i];
|
||||
element.innerHTML = options[i];
|
||||
|
||||
document.getElementById(id).appendChild(element);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function startWebSocket() {
|
||||
if (webSockets == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
//ws.send('{"cmd": "getServerConfig1"}');
|
||||
|
||||
}
|
||||
|
||||
function checkErr(obj) {
|
||||
//alert(obj["err"])
|
||||
//screenLog(obj["err"], "error")
|
||||
console.log(obj);
|
||||
var newObj = new Object();
|
||||
var newErr = new Object();
|
||||
newErr["key"] = "Error";
|
||||
newErr["value"] = obj["err"];
|
||||
newErr["type"] = "error";
|
||||
|
||||
newObj[0] = newErr
|
||||
showLog(newObj);
|
||||
return
|
||||
}
|
||||
|
||||
function screenLog(msg, msgType, show) {
|
||||
return
|
||||
clearTimeout(closeLog)
|
||||
var div = document.getElementById("screenLog");
|
||||
var newMsg = new Object();
|
||||
|
||||
newMsg["_element"] = "P";
|
||||
|
||||
switch(msgType) {
|
||||
case "error": newMsg["class"] = "errorMsg"; break;
|
||||
case "warning": newMsg["class"] = "warningMsg"; break;
|
||||
//default: newMsg["class"] = "infoMsg"
|
||||
}
|
||||
|
||||
newMsg["_text"] = msg;
|
||||
|
||||
div.appendChild(createElement(newMsg));
|
||||
|
||||
div.scrollTop = div.scrollHeight;
|
||||
|
||||
if (show == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
div.className = ""
|
||||
closeLog = setTimeout(closeScreenLog, 10000);
|
||||
}
|
||||
|
||||
|
||||
function closeScreenLog() {
|
||||
var div = document.getElementById("screenLog");
|
||||
div.className = "screenLogHidden"
|
||||
}
|
||||
|
||||
function showScreenLog() {
|
||||
clearTimeout(closeLog)
|
||||
var div = document.getElementById("screenLog");
|
||||
var currentClass = div.className;
|
||||
div.className = "screenLogHidden"
|
||||
|
||||
switch(currentClass) {
|
||||
case "screenLogHidden": div.className = ""; break;
|
||||
case "": div.className = "screenLogHidden"; break;
|
||||
}
|
||||
}
|
||||
|
||||
function showLoadingScreen(elm) {
|
||||
var div = document.getElementById("loading");
|
||||
switch(elm) {
|
||||
case true: div.className = "block"; break;
|
||||
case false: div.className = "none"; break;
|
||||
|
||||
/*
|
||||
case true: div.style.display = "block"; break;
|
||||
case false: div.style.display = "none"; break;
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
function createClintInfo(obj) {
|
||||
//console.log(obj);
|
||||
var keys = getObjKeys(obj);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if(document.getElementById(keys[i])){
|
||||
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
|
||||
}
|
||||
}
|
||||
//document.getElementById("clientInfo").className = "visible";
|
||||
}
|
||||
|
||||
function showElement(elmID, type) {
|
||||
switch(type) {
|
||||
case true: cssClass = "block"; break;
|
||||
case false: cssClass = "none"; break;
|
||||
}
|
||||
|
||||
document.getElementById(elmID).className = cssClass;
|
||||
}
|
||||
|
||||
function showPopUpElement(elm) {
|
||||
var allElements = new Array("deleteUserDetail", "mapping-detail", "user-detail", "file-detail");
|
||||
|
||||
for (var i = 0; i < allElements.length; i++) {
|
||||
showElement(allElements[i], false)
|
||||
}
|
||||
|
||||
showElement(elm, true)
|
||||
|
||||
setTimeout(function(){
|
||||
showElement("popup", true);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// body...
|
||||
|
||||
function showStreams(force) {
|
||||
|
||||
var elmBox = document.getElementById("myStreamsBox");
|
||||
var elm = document.getElementById("allStreams");
|
||||
//console.log(elm);
|
||||
show = elm.className;
|
||||
|
||||
switch(force) {
|
||||
case true: show = "notVisible"; break;
|
||||
case false: show = "visible"; break;
|
||||
}
|
||||
|
||||
switch(show) {
|
||||
case "notVisible":
|
||||
elm.className = "visible";
|
||||
elmBox.style.height = "100px";
|
||||
break;
|
||||
|
||||
default:
|
||||
elm.className = "notVisible";
|
||||
elmBox.style.height = "20px";
|
||||
break;
|
||||
}
|
||||
|
||||
var show = elm.style.display; {
|
||||
//console.log(elm.style.display);
|
||||
}
|
||||
|
||||
calculateWrapperHeight();
|
||||
}
|
||||
|
||||
function xteveBackup() {
|
||||
console.log("xteveBackup");
|
||||
var data = new Object();
|
||||
data["cmd"] = "xteveBackup";
|
||||
|
||||
xTeVe(data);
|
||||
}
|
||||
|
||||
function xteveRestore(elm) {
|
||||
var restore = document.createElement("INPUT");
|
||||
restore.setAttribute("type", "file");
|
||||
restore.setAttribute("class", "notVisible");
|
||||
restore.setAttribute("name", "");
|
||||
restore.id = "upload";
|
||||
|
||||
document.body.appendChild(restore);
|
||||
restore.click();
|
||||
|
||||
restore.onchange = function() {
|
||||
var filename = restore.files[0].name
|
||||
//console.log(restore.srcElement.files[0]);
|
||||
var check = confirm("File: " + filename + "\nAll data will be replaced with those from the backup.\nShould the files be restored?");
|
||||
if (check == true) {
|
||||
var reader = new FileReader();
|
||||
var file = document.querySelector('input[type=file]').files[0];
|
||||
if (file) {
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function() {
|
||||
console.log(reader.result);
|
||||
var data = new Object();
|
||||
data["cmd"] = "xteveRestore"
|
||||
data["base64"] = reader.result
|
||||
|
||||
xTeVe(data);
|
||||
return
|
||||
};
|
||||
} else {
|
||||
alert("File could not be loaded")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getBase64(file) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function() {
|
||||
console.log(reader.result);
|
||||
};
|
||||
reader.onerror = function(error) {
|
||||
console.log('Error: ', error);
|
||||
};
|
||||
}
|
||||
|
||||
function logout() {
|
||||
document.cookie.split(';').forEach(function(c) {
|
||||
document.cookie = c.trim().split('=')[0] + '=;' + 'expires=Thu, 01 Jan 1970 00:00:00 UTC;';
|
||||
});
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length == 2) return parts.pop().split(";").shift();
|
||||
}
|
||||
|
||||
function setCookie(token) {
|
||||
//console.log(token);
|
||||
document.cookie = "Token=" + token
|
||||
}
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
var SERVER = new Object();
|
||||
var BULK_EDIT = false;
|
||||
var COLUMN_TO_SORT;
|
||||
var SEARCH_MAPPING = new Object();
|
||||
var UNDO = new Object();
|
||||
var SERVER_CONNECTION = false;
|
||||
var WS_AVAILABLE = false;
|
||||
// Menü
|
||||
var menuItems = new Array();
|
||||
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"));
|
||||
//menuItems.push(new MainMenuItem("pmsID", "{{.mainMenu.item.pmsID}}", "number.png", "{{.mainMenu.headline.pmsID}}"))
|
||||
menuItems.push(new MainMenuItem("filter", "{{.mainMenu.item.filter}}", "filter.png", "{{.mainMenu.headline.filter}}"));
|
||||
menuItems.push(new MainMenuItem("xmltv", "{{.mainMenu.item.xmltv}}", "xmltv.png", "{{.mainMenu.headline.xmltv}}"));
|
||||
menuItems.push(new MainMenuItem("mapping", "{{.mainMenu.item.mapping}}", "mapping.png", "{{.mainMenu.headline.mapping}}"));
|
||||
menuItems.push(new MainMenuItem("users", "{{.mainMenu.item.users}}", "users.png", "{{.mainMenu.headline.users}}"));
|
||||
menuItems.push(new MainMenuItem("settings", "{{.mainMenu.item.settings}}", "settings.png", "{{.mainMenu.headline.settings}}"));
|
||||
menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.mainMenu.headline.log}}"));
|
||||
menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"));
|
||||
// Kategorien für die Einstellungen
|
||||
var settingsCategory = new Array();
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"));
|
||||
function showPopUpElement(elm) {
|
||||
var allElements = new Array("popup-custom");
|
||||
for (var i = 0; i < allElements.length; i++) {
|
||||
showElement(allElements[i], false);
|
||||
}
|
||||
showElement(elm, true);
|
||||
setTimeout(function () {
|
||||
showElement("popup", true);
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
function showElement(elmID, type) {
|
||||
var cssClass;
|
||||
switch (type) {
|
||||
case true:
|
||||
cssClass = "block";
|
||||
break;
|
||||
case false:
|
||||
cssClass = "none";
|
||||
break;
|
||||
}
|
||||
document.getElementById(elmID).className = cssClass;
|
||||
}
|
||||
function changeButtonAction(element, buttonID, attribute) {
|
||||
var value = element.options[element.selectedIndex].value;
|
||||
document.getElementById(buttonID).setAttribute(attribute, value);
|
||||
}
|
||||
function getLocalData(dataType, id) {
|
||||
var data = new Object();
|
||||
switch (dataType) {
|
||||
case "m3u":
|
||||
data = SERVER["settings"]["files"][dataType][id];
|
||||
break;
|
||||
case "hdhr":
|
||||
data = SERVER["settings"]["files"][dataType][id];
|
||||
break;
|
||||
case "filter":
|
||||
case "custom-filter":
|
||||
case "group-title":
|
||||
if (id == -1) {
|
||||
data["active"] = true;
|
||||
data["caseSensitive"] = false;
|
||||
data["description"] = "";
|
||||
data["exclude"] = "";
|
||||
data["filter"] = "";
|
||||
data["include"] = "";
|
||||
data["name"] = "";
|
||||
data["type"] = "group-title";
|
||||
SERVER["settings"]["filter"][id] = data;
|
||||
}
|
||||
data = SERVER["settings"]["filter"][id];
|
||||
break;
|
||||
case "xmltv":
|
||||
data = SERVER["settings"]["files"][dataType][id];
|
||||
break;
|
||||
case "users":
|
||||
data = SERVER["users"][id]["data"];
|
||||
break;
|
||||
case "mapping":
|
||||
data = SERVER["xepg"]["epgMapping"][id];
|
||||
break;
|
||||
case "m3uGroups":
|
||||
data = SERVER["data"]["playlist"]["m3u"]["groups"];
|
||||
break;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
function getObjKeys(obj) {
|
||||
var keys = new Array();
|
||||
for (var i in obj) {
|
||||
if (obj.hasOwnProperty(i)) {
|
||||
keys.push(i);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function getAllSelectedChannels() {
|
||||
var channels = new Array();
|
||||
if (BULK_EDIT == false) {
|
||||
return channels;
|
||||
}
|
||||
var trs = document.getElementById("content_table").getElementsByTagName("TR");
|
||||
for (var i = 1; i < trs.length; i++) {
|
||||
if (trs[i].style.display != "none") {
|
||||
if (trs[i].firstChild.firstChild.checked == true) {
|
||||
channels.push(trs[i].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
function selectAllChannels() {
|
||||
var bulk = false;
|
||||
var trs = document.getElementById("content_table").getElementsByTagName("TR");
|
||||
if (trs[0].firstChild.firstChild.checked == true) {
|
||||
bulk = true;
|
||||
}
|
||||
for (var i = 1; i < trs.length; i++) {
|
||||
if (trs[i].style.display != "none") {
|
||||
switch (bulk) {
|
||||
case true:
|
||||
trs[i].firstChild.firstChild.checked = true;
|
||||
break;
|
||||
case false:
|
||||
trs[i].firstChild.firstChild.checked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
function bulkEdit() {
|
||||
BULK_EDIT = !BULK_EDIT;
|
||||
var className;
|
||||
var rows = document.getElementsByClassName("bulk");
|
||||
switch (BULK_EDIT) {
|
||||
case true:
|
||||
className = "bulk showBulk";
|
||||
break;
|
||||
case false:
|
||||
className = "bulk hideBulk";
|
||||
break;
|
||||
}
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
rows[i].className = className;
|
||||
rows[i].checked = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function sortTable(column) {
|
||||
//console.log(columm);
|
||||
if (column == COLUMN_TO_SORT) {
|
||||
return;
|
||||
}
|
||||
var table = document.getElementById("content_table");
|
||||
var tableHead = table.getElementsByTagName("TR")[0];
|
||||
var tableItems = tableHead.getElementsByTagName("TD");
|
||||
var sortObj = new Object();
|
||||
var x, xValue;
|
||||
var tableHeader;
|
||||
var sortByString = false;
|
||||
if (column > 0 && COLUMN_TO_SORT > 0) {
|
||||
tableItems[COLUMN_TO_SORT].className = "pointer";
|
||||
tableItems[column].className = "sortThis";
|
||||
}
|
||||
COLUMN_TO_SORT = column;
|
||||
var rows = table.rows;
|
||||
if (rows[1] != undefined) {
|
||||
tableHeader = rows[0];
|
||||
x = rows[1].getElementsByTagName("TD")[column];
|
||||
for (i = 1; i < rows.length; i++) {
|
||||
x = rows[i].getElementsByTagName("TD")[column];
|
||||
switch (x.childNodes[0].tagName.toLowerCase()) {
|
||||
case "input":
|
||||
xValue = x.getElementsByTagName("INPUT")[0].value.toLowerCase();
|
||||
break;
|
||||
case "p":
|
||||
xValue = x.getElementsByTagName("P")[0].innerText.toLowerCase();
|
||||
break;
|
||||
default: console.log(x.childNodes[0].tagName);
|
||||
}
|
||||
if (xValue == "" || xValue == NaN) {
|
||||
xValue = i;
|
||||
sortObj[i] = rows[i];
|
||||
}
|
||||
else {
|
||||
switch (isNaN(xValue)) {
|
||||
case false:
|
||||
xValue = parseFloat(xValue);
|
||||
sortObj[xValue] = rows[i];
|
||||
break;
|
||||
case true:
|
||||
sortByString = true;
|
||||
sortObj[xValue.toLowerCase() + i] = rows[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (table.firstChild) {
|
||||
table.removeChild(table.firstChild);
|
||||
}
|
||||
var sortValues = getObjKeys(sortObj);
|
||||
if (sortByString == true) {
|
||||
sortValues.sort();
|
||||
console.log(sortValues);
|
||||
}
|
||||
else {
|
||||
function sortFloat(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
sortValues.sort(sortFloat);
|
||||
}
|
||||
table.appendChild(tableHeader);
|
||||
for (var i = 0; i < sortValues.length; i++) {
|
||||
table.appendChild(sortObj[sortValues[i]]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
function createSearchObj() {
|
||||
SEARCH_MAPPING = new Object();
|
||||
var data = SERVER["xepg"]["epgMapping"];
|
||||
var channels = getObjKeys(data);
|
||||
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title"];
|
||||
channels.forEach(function (id) {
|
||||
channelKeys.forEach(function (key) {
|
||||
if (key == "x-active") {
|
||||
switch (data[id][key]) {
|
||||
case true:
|
||||
SEARCH_MAPPING[id] = "online ";
|
||||
break;
|
||||
case false:
|
||||
SEARCH_MAPPING[id] = "offline ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " ";
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
function searchInMapping() {
|
||||
var searchValue = document.getElementById("searchMapping").value;
|
||||
var trs = document.getElementById("content_table").getElementsByTagName("TR");
|
||||
for (var i = 1; i < trs.length; ++i) {
|
||||
var id = trs[i].getAttribute("id");
|
||||
var element = SEARCH_MAPPING[id];
|
||||
switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
case true:
|
||||
document.getElementById(id).style.display = "";
|
||||
break;
|
||||
case false:
|
||||
document.getElementById(id).style.display = "none";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
function calculateWrapperHeight() {
|
||||
if (document.getElementById("box-wrapper")) {
|
||||
var elm = document.getElementById("box-wrapper");
|
||||
var divs = new Array("myStreamsBox", "clientInfo", "content");
|
||||
var elementsHeight = 0 - elm.offsetHeight;
|
||||
for (var i = 0; i < divs.length; i++) {
|
||||
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
|
||||
}
|
||||
elm.style.height = window.innerHeight - elementsHeight + "px";
|
||||
}
|
||||
return;
|
||||
}
|
||||
function changeChannelNumber(element) {
|
||||
var dbID = element.parentNode.parentNode.id;
|
||||
var newNumber = parseFloat(element.value);
|
||||
var channelNumbers = [];
|
||||
var data = SERVER["xepg"]["epgMapping"];
|
||||
var channels = getObjKeys(data);
|
||||
if (isNaN(newNumber)) {
|
||||
alert("{{.alert.invalidChannelNumber}}");
|
||||
return;
|
||||
}
|
||||
channels.forEach(function (id) {
|
||||
var channelNumber = parseFloat(data[id]["x-channelID"]);
|
||||
channelNumbers.push(channelNumber);
|
||||
});
|
||||
for (var i = 0; i < channelNumbers.length; i++) {
|
||||
if (channelNumbers.indexOf(newNumber) == -1) {
|
||||
break;
|
||||
}
|
||||
if (Math.floor(newNumber) == newNumber) {
|
||||
newNumber = newNumber + 1;
|
||||
}
|
||||
else {
|
||||
newNumber = newNumber + 0.1;
|
||||
newNumber.toFixed(1);
|
||||
newNumber = Math.round(newNumber * 10) / 10;
|
||||
}
|
||||
}
|
||||
data[dbID]["x-channelID"] = newNumber.toString();
|
||||
element.value = newNumber;
|
||||
console.log(data[dbID]["x-channelID"]);
|
||||
if (COLUMN_TO_SORT == 1) {
|
||||
COLUMN_TO_SORT = -1;
|
||||
sortTable(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
function backup() {
|
||||
var data = new Object();
|
||||
console.log("Backup data");
|
||||
var cmd = "xteveBackup";
|
||||
console.log("SEND TO SERVER");
|
||||
console.log(data);
|
||||
var server = new Server(cmd);
|
||||
server.request(data);
|
||||
return;
|
||||
}
|
||||
function toggleChannelStatus(id) {
|
||||
var element;
|
||||
var status;
|
||||
if (document.getElementById("active")) {
|
||||
var checkbox = document.getElementById("active");
|
||||
status = (checkbox).checked;
|
||||
}
|
||||
var ids = getAllSelectedChannels();
|
||||
if (ids.length == 0) {
|
||||
ids.push(id);
|
||||
}
|
||||
ids.forEach(function (id) {
|
||||
var channel = SERVER["xepg"]["epgMapping"][id];
|
||||
channel["x-active"] = status;
|
||||
switch (channel["x-active"]) {
|
||||
case true:
|
||||
if (channel["x-xmltv-file"] == "-" || channel["x-mapping"] == "-") {
|
||||
if (BULK_EDIT == false) {
|
||||
alert(channel["x-name"] + ": Missing XMLTV file / channel");
|
||||
checkbox.checked = false;
|
||||
}
|
||||
channel["x-active"] = false;
|
||||
}
|
||||
break;
|
||||
case false:
|
||||
// code...
|
||||
break;
|
||||
}
|
||||
if (channel["x-active"] == false) {
|
||||
document.getElementById(id).className = "notActiveEPG";
|
||||
}
|
||||
else {
|
||||
document.getElementById(id).className = "activeEPG";
|
||||
}
|
||||
});
|
||||
}
|
||||
function restore() {
|
||||
if (document.getElementById('upload')) {
|
||||
document.getElementById('upload').remove();
|
||||
}
|
||||
var restore = document.createElement("INPUT");
|
||||
restore.setAttribute("type", "file");
|
||||
restore.setAttribute("class", "notVisible");
|
||||
restore.setAttribute("name", "");
|
||||
restore.id = "upload";
|
||||
document.body.appendChild(restore);
|
||||
restore.click();
|
||||
restore.onchange = function () {
|
||||
var filename = restore.files[0].name;
|
||||
var check = confirm("File: " + filename + "\n{{.confirm.restore}}");
|
||||
if (check == true) {
|
||||
var reader = new FileReader();
|
||||
var file = document.querySelector('input[type=file]').files[0];
|
||||
if (file) {
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function () {
|
||||
console.log(reader.result);
|
||||
var data = new Object();
|
||||
var cmd = "xteveRestore";
|
||||
data["base64"] = reader.result;
|
||||
var server = new Server(cmd);
|
||||
server.request(data);
|
||||
};
|
||||
}
|
||||
else {
|
||||
alert("File could not be loaded");
|
||||
}
|
||||
restore.remove();
|
||||
return;
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
function uploadLogo() {
|
||||
if (document.getElementById('upload')) {
|
||||
document.getElementById('upload').remove();
|
||||
}
|
||||
var upload = document.createElement("INPUT");
|
||||
upload.setAttribute("type", "file");
|
||||
upload.setAttribute("class", "notVisible");
|
||||
upload.setAttribute("name", "");
|
||||
upload.id = "upload";
|
||||
document.body.appendChild(upload);
|
||||
upload.click();
|
||||
upload.onblur = function () {
|
||||
alert();
|
||||
};
|
||||
upload.onchange = function () {
|
||||
var filename = upload.files[0].name;
|
||||
var reader = new FileReader();
|
||||
var file = document.querySelector('input[type=file]').files[0];
|
||||
if (file) {
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function () {
|
||||
console.log(reader.result);
|
||||
var data = new Object();
|
||||
var cmd = "uploadLogo";
|
||||
data["base64"] = reader.result;
|
||||
data["filename"] = file.name;
|
||||
var server = new Server(cmd);
|
||||
server.request(data);
|
||||
var updateLogo = document.getElementById('update-icon');
|
||||
updateLogo.checked = false;
|
||||
updateLogo.className = "changed";
|
||||
};
|
||||
}
|
||||
else {
|
||||
alert("File could not be loaded");
|
||||
}
|
||||
upload.remove();
|
||||
return;
|
||||
};
|
||||
}
|
||||
function checkUndo(key) {
|
||||
switch (key) {
|
||||
case "epgMapping":
|
||||
if (UNDO.hasOwnProperty(key)) {
|
||||
SERVER["xepg"][key] = JSON.parse(JSON.stringify(UNDO[key]));
|
||||
}
|
||||
else {
|
||||
UNDO[key] = JSON.parse(JSON.stringify(SERVER["xepg"][key]));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function sortSelect(elem) {
|
||||
var tmpAry = [];
|
||||
var selectedValue = elem[elem.selectedIndex].value;
|
||||
for (var i = 0; i < elem.options.length; i++)
|
||||
tmpAry.push(elem.options[i]);
|
||||
tmpAry.sort(function (a, b) { return (a.text < b.text) ? -1 : 1; });
|
||||
while (elem.options.length > 0)
|
||||
elem.options[0] = null;
|
||||
var newSelectedIndex = 0;
|
||||
for (var i = 0; i < tmpAry.length; i++) {
|
||||
elem.options[i] = tmpAry[i];
|
||||
if (elem.options[i].value == selectedValue)
|
||||
newSelectedIndex = i;
|
||||
}
|
||||
elem.selectedIndex = newSelectedIndex; // Set new selected index after sorting
|
||||
return;
|
||||
}
|
||||
function updateLog() {
|
||||
console.log("TOKEN");
|
||||
var server = new Server("updateLog");
|
||||
server.request(new Object());
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var MainMenu = /** @class */ (function () {
|
||||
function MainMenu() {
|
||||
this.DocumentID = "main-menu";
|
||||
this.HTMLTag = "LI";
|
||||
}
|
||||
MainMenu.prototype.create = function () {
|
||||
console.log(this.DocumentID);
|
||||
};
|
||||
return MainMenu;
|
||||
}());
|
||||
var MainMenuItem = /** @class */ (function (_super) {
|
||||
__extends(MainMenuItem, _super);
|
||||
function MainMenuItem() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
MainMenuItem.prototype.create2 = function () {
|
||||
var element = document.createElement(this.HTMLTag);
|
||||
element.innerText = this.Value;
|
||||
console.log(element);
|
||||
};
|
||||
return MainMenuItem;
|
||||
}(MainMenu));
|
||||
function pageReady() {
|
||||
var item = new MainMenuItem();
|
||||
item.Value = "Test";
|
||||
item.create2();
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
var configMenu = new Object();
|
||||
var wizard = new Array("key", "tuner", "epgSource", "m3u", "complete");
|
||||
var activeWizard;
|
||||
var dvrIP
|
||||
|
||||
var configMenu_tuner = new Object();
|
||||
configMenu_tuner["_element"] = "SELECT";
|
||||
configMenu_tuner["_menuType"] = "singleInput";
|
||||
configMenu_tuner["_configKey"] = "tuner";
|
||||
configMenu_tuner["_label"] = "Available tuners";
|
||||
configMenu_tuner["name"] = "tuner";
|
||||
configMenu_tuner["id"] = "Tuner";
|
||||
configMenu_tuner["placeholder"] = "Tuner";
|
||||
configMenu_tuner["_usage"] = "This setting is only used by Plex and Emby.<br>The number of concurrent streams allowed by the IPTV provider."
|
||||
|
||||
|
||||
var optionValues = new Array();
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
optionValues.push(i)
|
||||
}
|
||||
configMenu_tuner["_optionValues"] = optionValues;
|
||||
|
||||
var configMenu_epg = new Object();
|
||||
configMenu_epg["_element"] = "SELECT";
|
||||
configMenu_epg["_menuType"] = "singleInput";
|
||||
configMenu_epg["_configKey"] = "epgSource";
|
||||
configMenu_epg["_label"] = "Selection of the EPG source";
|
||||
configMenu_epg["name"] = "epgSource";
|
||||
configMenu_epg["id"] = "EPG source";
|
||||
configMenu_epg["placeholder"] = "EPG source";
|
||||
configMenu_epg["_optionValues"] = new Array("PMS", "XEPG");
|
||||
configMenu_epg["_usage"] = "PMS: Use EPG data from Plex or Emby<br>XEPG: Use of external EPG data (XMLTV)<br> Several XMLTV sources possible<br> Allows editing and order channels<br> M3U / XMLTV export (HTTP link for IPTV apps)"
|
||||
|
||||
var configMenu_m3u = new Object();
|
||||
configMenu_m3u["_element"] = "INPUT";
|
||||
configMenu_m3u["_menuType"] = "inputArray";
|
||||
configMenu_m3u["_configKey"] = "file";
|
||||
configMenu_m3u["_label"] = "M3U File: local or remote";
|
||||
configMenu_m3u["name"] = "file";
|
||||
configMenu_m3u["id"] = "m3u";
|
||||
configMenu_m3u["type"] = "text";
|
||||
configMenu_m3u["placeholder"] = "M3U File";
|
||||
configMenu_m3u["_usage"] = "Remote playlist: http://your.provider.com/file.m3u<br>Local playlist: /path/to/file.m3u"
|
||||
|
||||
|
||||
configMenu_m3u["value"] = "http://websrv.local:8080/kabel.m3u";
|
||||
|
||||
var configMenu_complete = new Object();
|
||||
configMenu_complete["_element"] = "H2";
|
||||
configMenu_complete["_menuType"] = "inputArray";
|
||||
configMenu_complete["_configKey"] = "file";
|
||||
configMenu_complete["_text"] = "xTeVe was successfully set up";
|
||||
configMenu_complete["name"] = "complete";
|
||||
configMenu_complete["id"] = "complete";
|
||||
configMenu_complete["type"] = "text";
|
||||
configMenu_complete["class"] = "center";
|
||||
|
||||
configMenu["tuner"] = configMenu_tuner;
|
||||
configMenu["epgSource"] = configMenu_epg;
|
||||
configMenu["m3u"] = configMenu_m3u;
|
||||
configMenu["complete"] = configMenu_complete;
|
||||
|
||||
function readyForConfiguration() {
|
||||
var data = new Object();
|
||||
data["cmd"] = "getServerConfig";
|
||||
xTeVe(data);
|
||||
showLoadingScreen(false);
|
||||
}
|
||||
|
||||
function createConfiguration(elm) {
|
||||
|
||||
activeWizard = elm;
|
||||
var item = configMenu[elm];
|
||||
|
||||
var div = document.getElementById("content");
|
||||
div.innerHTML = "";
|
||||
div.setAttribute("data-configKey", item["_configKey"]);
|
||||
div.setAttribute("data-menuType", item["_menuType"]);
|
||||
|
||||
switch(item.hasOwnProperty("_label")) {
|
||||
case true:
|
||||
var newItem = new Object();
|
||||
newItem["_element"] = "LABEL";
|
||||
newItem["_text"] = item["_label"];
|
||||
newItem["for"] = item["id"];
|
||||
div.appendChild(createElement(newItem));
|
||||
break
|
||||
}
|
||||
|
||||
switch(item["_element"]) {
|
||||
case "SELECT":
|
||||
div.appendChild(createElement(item));
|
||||
var selectElement = div.getElementsByTagName("SELECT")[0];
|
||||
var values = item["_optionValues"];
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var newEntry = new Object;
|
||||
newEntry["_element"] = "OPTION";
|
||||
newEntry["_text"] = item["id"] + ": " + values[i];
|
||||
newEntry["value"] = values[i];
|
||||
selectElement.appendChild(createElement(newEntry));
|
||||
}
|
||||
//return
|
||||
break;
|
||||
|
||||
default:
|
||||
div.appendChild(createElement(item));
|
||||
break;
|
||||
|
||||
|
||||
}
|
||||
//alert()
|
||||
|
||||
switch(item.hasOwnProperty("_usage")) {
|
||||
case true:
|
||||
var usageItem = new Object();
|
||||
usageItem["_element"] = "PRE"
|
||||
usageItem["_text"] = item["_usage"];
|
||||
div.appendChild(createElement(usageItem));
|
||||
}
|
||||
|
||||
if (activeWizard == "complete") {
|
||||
document.getElementById("next").value = "Finished"
|
||||
//document.getElementById("next").setAttribute("onclick", "javascript: location.reload();")
|
||||
}
|
||||
|
||||
//div.appendChild(createElement(item));
|
||||
}
|
||||
|
||||
function saveData() {
|
||||
|
||||
var div = document.getElementById("content");
|
||||
var inputs = div.getElementsByTagName("INPUT");
|
||||
var selects = div.getElementsByTagName("SELECT");
|
||||
var value;
|
||||
var data = new Object();
|
||||
var valueArr = new Array();
|
||||
var newData = false;
|
||||
|
||||
if (activeWizard == "complete") {
|
||||
data["cmd"] = "wizardCompleted";
|
||||
showLoadingScreen(true)
|
||||
xTeVe(data);
|
||||
return
|
||||
}
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var menuType = inputs[i].parentElement.getAttribute("data-menutype");
|
||||
if (inputs[i].value != undefined && inputs[i].value != "" ) {
|
||||
newData = true;
|
||||
|
||||
console.log(inputs[i].id)
|
||||
switch(inputs[i].id) {
|
||||
case "m3u":
|
||||
var newPlaylist = new Object();
|
||||
newPlaylist["file.source"] = inputs[i].value;
|
||||
//newPlaylist["name"] = inputs[i].value;
|
||||
newPlaylist["type"] = "m3u";
|
||||
newPlaylist["new"] = true;
|
||||
|
||||
data["files"] = new Object();
|
||||
data["files"]["m3u"] = new Object();
|
||||
data["files"]["m3u"]["-"] = newPlaylist;
|
||||
|
||||
data["cmd"] = "saveFilesM3U";
|
||||
xTeVe(data)
|
||||
return
|
||||
}
|
||||
/*
|
||||
switch(menuType) {
|
||||
case "singleInput":
|
||||
data[inputs[i].name] = inputs[i].value; break;
|
||||
case "inputArray":
|
||||
valueArr.push(inputs[i].value);
|
||||
data[inputs[i].name] = valueArr; break
|
||||
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
inputs[i].style.borderBottomColor = "red";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (var i = 0; i < selects.length; i++) {
|
||||
var value = selects[i].options[selects[i].selectedIndex].value;
|
||||
if (isNaN(value) == false) {
|
||||
value = parseInt(value);
|
||||
data[selects[i].name] = value;
|
||||
newData = true;
|
||||
break;
|
||||
}
|
||||
data[selects[i].name] = value;
|
||||
newData = true;
|
||||
}
|
||||
|
||||
|
||||
//console.log(data, newData);
|
||||
if (newData == true) {
|
||||
config = data
|
||||
data["cmd"] = "saveConfig";
|
||||
xTeVe(data);
|
||||
}
|
||||
}
|
||||
|
||||
function xTeVe(data) {
|
||||
|
||||
if (webSockets == false) {
|
||||
alert("Your browser does not support WebSockets");
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeWizard == "m3u" || activeWizard == "epgSource") {
|
||||
showLoadingScreen(true);
|
||||
}
|
||||
|
||||
var protocolWS
|
||||
switch(window.location.protocol) {
|
||||
case "http:": protocolWS = "ws://"; break;
|
||||
case "https:": protocolWS = "wss://"; break;
|
||||
}
|
||||
|
||||
var ws = new WebSocket(protocolWS + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token"));
|
||||
|
||||
ws.onopen = function() {
|
||||
ws.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
ws.onmessage = function (e) {
|
||||
|
||||
var response = JSON.parse(e.data);
|
||||
|
||||
if (response.hasOwnProperty("clientInfo")) {
|
||||
createClintInfo(response["clientInfo"]);
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("status")) {
|
||||
if (response["status"] == false) {
|
||||
document.getElementById("headline").style.borderColor = "red";
|
||||
showErr(response["err"]);
|
||||
showLoadingScreen(false)
|
||||
return
|
||||
} else {
|
||||
document.getElementById("err").innerHTML = "";
|
||||
document.getElementById("headline").style.borderColor = "lawngreen";
|
||||
}
|
||||
|
||||
dvrIP = response["DVR"]
|
||||
switch(response["configurationWizard"]) {
|
||||
case true:
|
||||
if (activeWizard == undefined) {
|
||||
activeWizard = wizard[0]
|
||||
}
|
||||
var n = wizard.indexOf(activeWizard);
|
||||
n++;
|
||||
activeWizard = wizard[n]
|
||||
|
||||
if (activeWizard == undefined) {
|
||||
data["cmd"] = "wizardCompleted";
|
||||
xTeVe(data)
|
||||
} else {
|
||||
//console.log(activeWizard);
|
||||
createConfiguration(activeWizard);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
switch(response["reload"]) {
|
||||
|
||||
|
||||
case true:
|
||||
|
||||
setTimeout(function(){
|
||||
location.reload();
|
||||
}, 100);
|
||||
|
||||
//location.reload();
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
setTimeout(function(){ showLoadingScreen(false); }, 300);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function showErr(elm) {
|
||||
document.getElementById("err").innerHTML = elm;
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var WizardCategory = /** @class */ (function () {
|
||||
function WizardCategory() {
|
||||
this.DocumentID = "content";
|
||||
}
|
||||
WizardCategory.prototype.createCategoryHeadline = function (value) {
|
||||
var element = document.createElement("H4");
|
||||
element.innerHTML = value;
|
||||
return element;
|
||||
};
|
||||
return WizardCategory;
|
||||
}());
|
||||
var WizardItem = /** @class */ (function (_super) {
|
||||
__extends(WizardItem, _super);
|
||||
function WizardItem(key, headline) {
|
||||
var _this = _super.call(this) || this;
|
||||
_this.headline = headline;
|
||||
_this.key = key;
|
||||
return _this;
|
||||
}
|
||||
WizardItem.prototype.createWizard = function () {
|
||||
var headline = this.createCategoryHeadline(this.headline);
|
||||
var key = this.key;
|
||||
var content = new PopupContent();
|
||||
var description;
|
||||
var doc = document.getElementById(this.DocumentID);
|
||||
doc.innerHTML = "";
|
||||
doc.appendChild(headline);
|
||||
switch (key) {
|
||||
case "tuner":
|
||||
var text = new Array();
|
||||
var values = new Array();
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
text.push(i);
|
||||
values.push(i);
|
||||
}
|
||||
var select = content.createSelect(text, values, "1", key);
|
||||
select.setAttribute("class", "wizard");
|
||||
select.id = key;
|
||||
doc.appendChild(select);
|
||||
description = "{{.wizard.tuner.description}}";
|
||||
break;
|
||||
case "epgSource":
|
||||
var text = ["PMS", "XEPG"];
|
||||
var values = ["PMS", "XEPG"];
|
||||
var select = content.createSelect(text, values, "XEPG", key);
|
||||
select.setAttribute("class", "wizard");
|
||||
select.id = key;
|
||||
doc.appendChild(select);
|
||||
description = "{{.wizard.epgSource.description}}";
|
||||
break;
|
||||
case "m3u":
|
||||
var input = content.createInput("text", key, "");
|
||||
input.setAttribute("placeholder", "{{.wizard.m3u.placeholder}}");
|
||||
input.setAttribute("class", "wizard");
|
||||
input.id = key;
|
||||
doc.appendChild(input);
|
||||
description = "{{.wizard.m3u.description}}";
|
||||
break;
|
||||
case "xmltv":
|
||||
var input = content.createInput("text", key, "");
|
||||
input.setAttribute("placeholder", "{{.wizard.xmltv.placeholder}}");
|
||||
input.setAttribute("class", "wizard");
|
||||
input.id = key;
|
||||
doc.appendChild(input);
|
||||
description = "{{.wizard.xmltv.description}}";
|
||||
break;
|
||||
default:
|
||||
console.log(key);
|
||||
break;
|
||||
}
|
||||
var pre = document.createElement("PRE");
|
||||
pre.innerHTML = description;
|
||||
doc.appendChild(pre);
|
||||
console.log(headline, key);
|
||||
};
|
||||
return WizardItem;
|
||||
}(WizardCategory));
|
||||
function readyForConfiguration(wizard) {
|
||||
var server = new Server("getServerConfig");
|
||||
server.request(new Object());
|
||||
showElement("loading", false);
|
||||
configurationWizard[wizard].createWizard();
|
||||
}
|
||||
function saveWizard() {
|
||||
var cmd = "saveWizard";
|
||||
var div = document.getElementById("content");
|
||||
var config = div.getElementsByClassName("wizard");
|
||||
var wizard = new Object();
|
||||
for (var i = 0; i < config.length; i++) {
|
||||
var name;
|
||||
var value;
|
||||
switch (config[i].tagName) {
|
||||
case "SELECT":
|
||||
name = config[i].name;
|
||||
value = config[i].value;
|
||||
// Wenn der Wert eine Zahl ist, wird dieser als Zahl gespeichert
|
||||
if (isNaN(value)) {
|
||||
wizard[name] = value;
|
||||
}
|
||||
else {
|
||||
wizard[name] = parseInt(value);
|
||||
}
|
||||
break;
|
||||
case "INPUT":
|
||||
switch (config[i].type) {
|
||||
case "text":
|
||||
name = config[i].name;
|
||||
value = config[i].value;
|
||||
if (value.length == 0) {
|
||||
var msg = name.toUpperCase() + ": " + "{{.alert.missingInput}}";
|
||||
alert(msg);
|
||||
return;
|
||||
}
|
||||
wizard[name] = value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// code...
|
||||
break;
|
||||
}
|
||||
}
|
||||
var data = new Object();
|
||||
data["wizard"] = wizard;
|
||||
var server = new Server(cmd);
|
||||
server.request(data);
|
||||
console.log(data);
|
||||
}
|
||||
// Wizard
|
||||
var configurationWizard = new Array();
|
||||
configurationWizard.push(new WizardItem("tuner", "{{.wizard.tuner.title}}"));
|
||||
configurationWizard.push(new WizardItem("epgSource", "{{.wizard.epgSource.title}}"));
|
||||
configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}"));
|
||||
configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.title}}"));
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
function showConfig(obj) {
|
||||
config = obj;
|
||||
//setMenuItem();
|
||||
createMenu();
|
||||
//document.getElementById("page").className = "";
|
||||
}
|
||||
|
||||
showMyStreams
|
||||
|
||||
function showMyStreams(allStreamsObj) {
|
||||
|
||||
var streamTypeKeys = getObjKeys(allStreamsObj)
|
||||
|
||||
for (var s = 0; s < streamTypeKeys.length; s++) {
|
||||
var streamType = streamTypeKeys[s];
|
||||
var obj = new Object();
|
||||
obj = allStreamsObj[streamType];
|
||||
switch(streamType) {
|
||||
case "activeStreams": activeStreams = obj; break;
|
||||
}
|
||||
|
||||
document.getElementById(streamType).innerHTML = "";
|
||||
|
||||
var streamsObj = new Object();
|
||||
var streamsNames = new Array();
|
||||
|
||||
var keys = getObjKeys(obj)
|
||||
|
||||
// Create Object (streamsObj) for the streams and sort by name (streamsNames)
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var name = obj[keys[i]]["name"];
|
||||
var tmp = new Object();
|
||||
var streamKey = getObjKeys(obj[keys[i]]);
|
||||
|
||||
for (var j = 0; j < streamKey.length; j++) {
|
||||
tmp[streamKey[j]] = obj[keys[i]][streamKey[j]];
|
||||
}
|
||||
|
||||
streamsObj[name] = tmp;
|
||||
streamsNames.push(name)
|
||||
}
|
||||
|
||||
streamsNames.sort();
|
||||
|
||||
// Create Table for activeStreams
|
||||
var table = document.getElementById(streamType);
|
||||
|
||||
for (var i = 0; i < streamsNames.length; i++) {
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "TR";
|
||||
table.appendChild(createElement(newEntry));
|
||||
var line = table.lastChild;
|
||||
|
||||
var tmp = streamsObj[streamsNames[i]]
|
||||
var keys = getObjKeys(tmp)
|
||||
|
||||
var newKey = new Object()
|
||||
newKey["_element"] = "TD";
|
||||
//newKey["_text"] = streamsNames[i];
|
||||
switch(streamType) {
|
||||
case "activeStreams": newKey["_text"] = "Channel (+):"; break;
|
||||
case "inactiveStreams": newKey["_text"] = "Channel (-):"; break;
|
||||
}
|
||||
|
||||
newKey["class"] = "tdKey";
|
||||
console.log();
|
||||
|
||||
|
||||
var newVal = new Object()
|
||||
newVal["_element"] = "TD";
|
||||
newVal["_text"] = streamsNames[i];
|
||||
newVal["class"] = "tdVal";
|
||||
//newVal["_text"] = value;
|
||||
|
||||
line.appendChild(createElement(newKey));
|
||||
line.appendChild(createElement(newVal));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function showActiveStreams(obj) {
|
||||
document.getElementById("activeStreams").innerHTML = "";
|
||||
activeStreams = obj;
|
||||
var streamsObj = new Object();
|
||||
var streamsNames = new Array();
|
||||
|
||||
var keys = getObjKeys(obj)
|
||||
|
||||
// Create Object (streamsObj) for the streams and sort by name (streamsNames)
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var name = obj[keys[i]]["name"];
|
||||
var tmp = new Object();
|
||||
var streamKey = getObjKeys(obj[keys[i]]);
|
||||
|
||||
for (var j = 0; j < streamKey.length; j++) {
|
||||
tmp[streamKey[j]] = obj[keys[i]][streamKey[j]];
|
||||
}
|
||||
|
||||
streamsObj[name] = tmp;
|
||||
streamsNames.push(name)
|
||||
}
|
||||
|
||||
streamsNames.sort();
|
||||
|
||||
// Create Table for activeStreams
|
||||
var table = document.getElementById("activeStreams");
|
||||
|
||||
for (var i = 0; i < streamsNames.length; i++) {
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "TR";
|
||||
table.appendChild(createElement(newEntry));
|
||||
var line = table.lastChild;
|
||||
|
||||
var tmp = streamsObj[streamsNames[i]]
|
||||
var keys = getObjKeys(tmp)
|
||||
|
||||
var newKey = new Object()
|
||||
newKey["_element"] = "TD";
|
||||
//newKey["_text"] = streamsNames[i];
|
||||
newKey["_text"] = "Channel:";
|
||||
newKey["class"] = "tdKey";
|
||||
console.log();
|
||||
|
||||
|
||||
var newVal = new Object()
|
||||
newVal["_element"] = "TD";
|
||||
newVal["_text"] = streamsNames[i];
|
||||
newVal["class"] = "tdVal";
|
||||
//newVal["_text"] = value;
|
||||
|
||||
line.appendChild(createElement(newKey));
|
||||
line.appendChild(createElement(newVal));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function parseLogs(obj) {
|
||||
log = obj
|
||||
var keys = getObjKeys(obj)
|
||||
|
||||
var msgType;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
switch(keys[i]) {
|
||||
case "warnings": msgType = "warningMsg"; break;
|
||||
case "errors": msgType = "errorMsg"; break;
|
||||
}
|
||||
|
||||
switch(obj[keys[i]]) {
|
||||
case 0: msgType = "tdVal"; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if(document.getElementById(keys[i])){
|
||||
document.getElementById(keys[i]).className = msgType;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
function cancelData(element) {
|
||||
createMenu();
|
||||
}
|
||||
|
||||
function saveData(element) {
|
||||
var data = new Object();
|
||||
var div = element.parentNode.parentNode;
|
||||
var inputs = div.getElementsByTagName("INPUT");
|
||||
|
||||
var configKey = div.getAttribute("data-configkey");
|
||||
var menuType = div.getAttribute("data-menutype");
|
||||
var value;
|
||||
var valueArr = new Array();
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].type == "text" && inputs[i].value != undefined && inputs[i].value != "" ) {
|
||||
console.log(inputs[i].value, menuType)
|
||||
switch(menuType) {
|
||||
case "inputArray": valueArr.push(inputs[i].value); break;
|
||||
case "singleInput": value = inputs[i].value; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch(menuType) {
|
||||
case "inputArray": data[configKey] = valueArr; break;
|
||||
case "singleInput":
|
||||
if (isNaN(value) == false) {
|
||||
value = parseInt(value);
|
||||
data[configKey] = value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (value == undefined) {
|
||||
data["delete"] = configKey;
|
||||
} else {
|
||||
data[configKey] = value
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
data["cmd"] = "saveConfig";
|
||||
console.log(data);
|
||||
xTeVe(data)
|
||||
}
|
||||
|
||||
function xTeVe(data) {
|
||||
if (webSockets == false) {
|
||||
alert("Your browser does not support WebSockets");
|
||||
return;
|
||||
} else {
|
||||
if (data["cmd"] != "getLog") {
|
||||
showLoadingScreen(true)
|
||||
}
|
||||
}
|
||||
delete undo["epgMapping"];
|
||||
|
||||
var protocolWS
|
||||
switch(window.location.protocol) {
|
||||
case "http:": protocolWS = "ws://"; break;
|
||||
case "https:": protocolWS = "wss://"; break;
|
||||
}
|
||||
|
||||
|
||||
var ws = new WebSocket(protocolWS + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token"));
|
||||
ws.onopen = function() {
|
||||
console.log(data)
|
||||
ws.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
ws.onmessage = function (e) {
|
||||
var response = JSON.parse(e.data);
|
||||
console.log(response);
|
||||
|
||||
if (response.hasOwnProperty("clientInfo")) {
|
||||
createClintInfo(response["clientInfo"]);
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("log")) {
|
||||
createClintInfo(response["log"]);
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("status")) {
|
||||
if (response["status"] == false) {
|
||||
alert(response["err"])
|
||||
if(response.hasOwnProperty("reload")) {
|
||||
location.reload();
|
||||
}
|
||||
//checkErr(response)
|
||||
console.log(response);
|
||||
updateXteveStatus(response);
|
||||
setTimeout(function(){ showLoadingScreen(false); }, 300);
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updateXteveStatus(response)
|
||||
|
||||
//console.log(data["cmd"]);
|
||||
switch(data["cmd"]) {
|
||||
case "saveUserData": createMenu(); break;
|
||||
case "saveNewUser": createMenu(); break;
|
||||
case "saveFilesXMLTV": //createMenu(); break;
|
||||
case "saveFilesM3U": //createMenu(); return; break;
|
||||
case "saveConfig":
|
||||
data = new Object();
|
||||
data["cmd"] = "checkToken";
|
||||
xTeVe(data);
|
||||
break;
|
||||
|
||||
case "emptyLog": writeLogInDiv(); break;
|
||||
case "getLog": return; break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (config["files"] == undefined || config["files"].length == 0) {
|
||||
createMenu();
|
||||
document.getElementById(10).click()
|
||||
}
|
||||
|
||||
setTimeout(function(){ showLoadingScreen(false); }, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateXteveStatus(response) {
|
||||
var keys = getObjKeys(response);
|
||||
//console.log(keys);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
switch(keys[i]) {
|
||||
case "alert": alert(response[keys[i]]); break;
|
||||
case "config": showConfig(response[keys[i]]); break;
|
||||
case "log": parseLogs(response[keys[i]]); break;
|
||||
case "myStreams": showMyStreams(response[keys[i]]); break;
|
||||
case "xEPG": xEPG = response[keys[i]]; break;
|
||||
case "users": users = response[keys[i]]; break;
|
||||
case "token": document.cookie = "Token=" + response[keys[i]]; break;
|
||||
case "reload": location.reload(); break;
|
||||
case "openLink": window.location = response["openLink"]; break;
|
||||
//case "version": version = response[keys[i]]; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getValueFromProviderFile(xXmltvFile, fileType, key) {
|
||||
|
||||
var fileID = xXmltvFile.substring(0, xXmltvFile.lastIndexOf('.'))
|
||||
|
||||
if (config["files"][fileType].hasOwnProperty(fileID) == true) {
|
||||
var data = config["files"][fileType][fileID];
|
||||
return data[key]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
function openFiles(elm, fileType) {
|
||||
//document.getElementById("settings").innerHTML = "Test";
|
||||
|
||||
columnToSort = 0;
|
||||
var newDiv = document.getElementById("settings");
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "HR";
|
||||
newDiv.appendChild(createElement(newEntry));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
newEntry["class"] = "button";
|
||||
newEntry["value"] = "New";
|
||||
newEntry["onclick"] = 'fileDetail("-", "' + fileType + '")';
|
||||
newDiv.appendChild(createElement(newEntry));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
newEntry["class"] = "button";
|
||||
newEntry["value"] = "Update";
|
||||
newEntry["onclick"] = "fileDetail(0)";
|
||||
//newDiv.appendChild(createElement(newEntry));
|
||||
|
||||
var div = document.getElementById("settings");
|
||||
|
||||
// Build table
|
||||
var newTable = new Object();
|
||||
newTable["_element"] = "TABLE";
|
||||
newTable["id"] = "id_mapping";
|
||||
newTable["class"] = "table-mapping";
|
||||
div.appendChild(createElement(newTable));
|
||||
|
||||
setTimeout(function(){
|
||||
createFilesTable(fileType);
|
||||
}, 10);
|
||||
|
||||
}
|
||||
|
||||
function createFilesTable(fileType) {
|
||||
var table = document.getElementById("id_mapping");
|
||||
var availableFileTypes = new Array();
|
||||
|
||||
table.innerHTML = "";
|
||||
var newTR = new Object();
|
||||
newTR["_element"] = "TR";
|
||||
newTR["class"] = "table-mapping-header";
|
||||
table.appendChild(createElement(newTR));
|
||||
|
||||
var tr = table.lastChild;
|
||||
|
||||
switch(fileType) {
|
||||
case "xmltv":
|
||||
availableFileTypes = new Array("xmltv");
|
||||
var trHeadlines = new Array("Guide", "Last Update", "Availability %", "Channels", "Programs")
|
||||
var compatibilityKeys = new Array("xmltv.channels", "xmltv.programs")
|
||||
break;
|
||||
|
||||
case "m3u":
|
||||
availableFileTypes = new Array("m3u", "hdhr");
|
||||
var trHeadlines = new Array("Playlist", "Last Update", "Availability %", "Type", "Streams", "group-title %", "tvg-id %", "Unique ID %");
|
||||
var compatibilityKeys = new Array("streams", "group.title", "tvg.id", "stream.id");
|
||||
break;
|
||||
}
|
||||
|
||||
for (var i = 0; i < trHeadlines.length; i++) {
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "TD";
|
||||
newTD["_text"] = trHeadlines[i];
|
||||
tr.appendChild(createElement(newTD));
|
||||
}
|
||||
|
||||
for (var i = 0; i < availableFileTypes.length; i++) {
|
||||
|
||||
var fileType = availableFileTypes[i]
|
||||
|
||||
var data = config["files"][fileType];
|
||||
|
||||
var allFiles = getObjKeys(data)
|
||||
|
||||
for (var f = 0; f < allFiles.length; f++) {
|
||||
var elm = data[allFiles[f]];
|
||||
var table = document.getElementById("id_mapping");
|
||||
var fileID = elm["id.provider"];
|
||||
var name = elm["name"];
|
||||
var lastUpdate = elm["last.update"];
|
||||
var availability = elm["provider.availability"];
|
||||
var type = elm["type"].toUpperCase();
|
||||
var compatibility = elm["compatibility"];
|
||||
|
||||
// Create TR
|
||||
var newTR = new Object();
|
||||
newTR["_element"] = "TR";
|
||||
newTR["class"] = "";
|
||||
newTR["id"] = fileID;
|
||||
newTR["onclick"] = 'javascript: fileDetail("' + fileID + '","' + fileType + '");';
|
||||
table.appendChild(createElement(newTR));
|
||||
|
||||
var tr = table.lastChild;
|
||||
|
||||
// Create file name TD
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = name;
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create last update TD
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = lastUpdate;
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create availability TD
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = availability;
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
if (fileType == "m3u" || fileType == "hdhr") {
|
||||
|
||||
// Create Type TD
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = type;
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
}
|
||||
|
||||
// Create all compatibility TDs
|
||||
|
||||
for (var j = 0; j < compatibilityKeys.length; j++) {
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = compatibility[compatibilityKeys[j]];
|
||||
createNewTD(newTD, tr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
sortTable(0)
|
||||
|
||||
// usage Info
|
||||
var div = document.getElementById("settings");
|
||||
switch(menu[activeMenu.id].hasOwnProperty("_usage")) {
|
||||
case true:
|
||||
var usageItem = new Object();
|
||||
usageItem["_element"] = "PRE"
|
||||
usageItem["_text"] = menu[activeMenu.id]["_usage"];
|
||||
|
||||
var newHR = new Object();
|
||||
newHR["_element"] = "HR"
|
||||
div.appendChild(createElement(newHR));
|
||||
div.appendChild(createElement(usageItem));
|
||||
break;
|
||||
}
|
||||
|
||||
calculateWrapperHeight();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
function fileDetail(fileID, fileType) {
|
||||
|
||||
optionsText = new Array("M3U", "HDHomeRun - [Experimental]")
|
||||
optionsValue = new Array("m3u", "hdhr")
|
||||
|
||||
switch (fileType) {
|
||||
|
||||
case "m3u":
|
||||
document.getElementById("name").setAttribute("placeholder", "Playlist name");
|
||||
document.getElementById("description").setAttribute("placeholder", "Description of this playlist");
|
||||
document.getElementById("file-detail-headline").innerHTML = "M3U Playlist";
|
||||
document.getElementById("file-path").innerHTML = "M3U File:";
|
||||
document.getElementById("file.source").setAttribute("placeholder", "Local or remote");
|
||||
break;
|
||||
|
||||
case "hdhr":
|
||||
document.getElementById("name").setAttribute("placeholder", "HDHomeRun name");
|
||||
document.getElementById("description").setAttribute("placeholder", "Description of this HDHomeRun tuner");
|
||||
document.getElementById("file-detail-headline").innerHTML = "HDHomeRun";
|
||||
document.getElementById("file-path").innerHTML = "HDHomeRun IP:";
|
||||
document.getElementById("file.source").setAttribute("placeholder", "IP address and port of the tuner (192.168.1.10:5004)");
|
||||
break;
|
||||
|
||||
case "xmltv":
|
||||
document.getElementById("name").setAttribute("placeholder", "XMLTV name");
|
||||
document.getElementById("description").setAttribute("placeholder", "Description of this XMLTV file");
|
||||
document.getElementById("file-detail-headline").innerHTML = "XMLTV File";
|
||||
document.getElementById("file-path").innerHTML = "XMLTV File:";
|
||||
document.getElementById("file.source").setAttribute("placeholder", "Local or remote");
|
||||
|
||||
optionsText = new Array("XMLTV")
|
||||
optionsValue = new Array("xmltv")
|
||||
break;
|
||||
}
|
||||
|
||||
modifyOption("type", optionsText, optionsValue)
|
||||
|
||||
showPopUpElement('file-detail');
|
||||
|
||||
document.getElementById("saveFileDetail").setAttribute("onclick", 'javascript: saveFileDetail("' + fileID + '","' + fileType + '", false)');
|
||||
document.getElementById("updateFileDetail").setAttribute("onclick", 'javascript: updateFile("' + fileID + '","' + fileType + '", false)');
|
||||
document.getElementById("deleteFileDetail").setAttribute("onclick", 'javascript: saveFileDetail("' + fileID + '","' + fileType + '", true)');
|
||||
|
||||
var data = new Object();
|
||||
|
||||
switch(fileID) {
|
||||
|
||||
case "-": // New file
|
||||
data["name"] = "";
|
||||
data["description"] = "";
|
||||
data["file.source"] = "";
|
||||
data["type"] = fileType;
|
||||
|
||||
document.getElementById("deleteFileDetail").className = "delete";
|
||||
document.getElementById("type").setAttribute("onchange", "changeFileType(this);")
|
||||
document.getElementById("type").setAttribute("data-id", fileID)
|
||||
|
||||
showElement("deleteFileDetail", false);
|
||||
showElement("updateFileDetail", false);
|
||||
|
||||
if (fileType == "xmltv") {
|
||||
showElement("type", false);
|
||||
showElement("file-type", false);
|
||||
} else {
|
||||
showElement("type", true);
|
||||
showElement("file-type", true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
data = config["files"][fileType][fileID];
|
||||
document.getElementById("deleteFileDetail").className = "delete";
|
||||
|
||||
showElement("updateFileDetail", true);
|
||||
showElement("type", false);
|
||||
showElement("file-type", false);
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
var keys = getObjKeys(data);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
|
||||
if(document.getElementById(keys[i])){
|
||||
document.getElementById(keys[i]).value = data[keys[i]];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function changeFileType(elm) {
|
||||
|
||||
var fileID = elm.getAttribute("data-id");
|
||||
var fileType = elm.options[elm.selectedIndex].value;
|
||||
|
||||
fileDetail(fileID, fileType)
|
||||
|
||||
}
|
||||
|
||||
|
||||
function saveFileDetail(fileID, fileType, deleteFile) {
|
||||
|
||||
if (fileID == undefined) {
|
||||
alert("ID is missing!!!");
|
||||
return
|
||||
}
|
||||
|
||||
var inputs = document.getElementById("file-detail").getElementsByTagName("INPUT");
|
||||
var selects = document.getElementById("file-detail").getElementsByTagName("SELECT");
|
||||
var newFileData = new Object();
|
||||
var data = new Object();
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
switch(inputs[i].type) {
|
||||
case "text": newFileData[inputs[i].name] = inputs[i].value; break;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < selects.length; i++) {
|
||||
newFileData[selects[i].id] = selects[i].options[selects[i].selectedIndex].value;
|
||||
}
|
||||
|
||||
if (deleteFile == true) {
|
||||
switch(fileType) {
|
||||
case "m3u": var alertText = "Delete this playlist?"; break;
|
||||
case "hdhr": var alertText = "Delete this HDHomeRun tuner?"; break;
|
||||
case "xmltv": var alertText = "Delete this XMLTV file?"; break;
|
||||
}
|
||||
|
||||
if (confirm(alertText)) {
|
||||
newFileData["delete"] = true
|
||||
data = buildFilesObj(fileType, fileID, newFileData);
|
||||
console.log(data);
|
||||
|
||||
} else {
|
||||
showElement("popup", false);
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
switch(config["files"][fileType].hasOwnProperty(fileID)) {
|
||||
|
||||
case true:
|
||||
data = config["files"][fileType][fileID];
|
||||
if (data["file.source"] != newFileData["file.source"]) {
|
||||
data["update"] = true
|
||||
} else {
|
||||
data["updatePlaylistName"] = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case false:
|
||||
newFileData["new"] = true;
|
||||
data = buildFilesObj(fileType, fileID, newFileData);
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch(fileType) {
|
||||
|
||||
case "m3u": data["cmd"] = "saveFilesM3U"; break;
|
||||
case "hdhr": data["cmd"] = "saveFilesHDHR"; break;
|
||||
case "xmltv": data["cmd"] = "saveFilesXMLTV"; break;
|
||||
|
||||
}
|
||||
//console.log(data);
|
||||
xTeVe(data);
|
||||
return
|
||||
}
|
||||
|
||||
function updateFile(fileID, fileType, allFiles) {
|
||||
|
||||
switch(config["files"][fileType].hasOwnProperty(fileID)) {
|
||||
|
||||
case true:
|
||||
|
||||
var data = new Object();
|
||||
var data = buildFilesObj(fileType, fileID, config["files"][fileType][fileID])
|
||||
data["new"] = true
|
||||
|
||||
switch(fileType) {
|
||||
|
||||
case "m3u": data["cmd"] = "updateFileM3U"; break;
|
||||
case "hdhr": data["cmd"] = "updateFileHDHR"; break;
|
||||
case "xmltv": data["cmd"] = "updateFileXMLTV"; break;
|
||||
|
||||
}
|
||||
|
||||
xTeVe(data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function buildFilesObj(fileType, fileID, obj) {
|
||||
|
||||
var data = new Object();
|
||||
data["files"] = new Object();
|
||||
data["files"][fileType] = new Object();
|
||||
data["files"][fileType][fileID] = obj
|
||||
return data
|
||||
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
var logInterval
|
||||
|
||||
|
||||
function updateLog() {
|
||||
var data = new Object();
|
||||
data["cmd"] = "getLog";
|
||||
xTeVe(data);
|
||||
writeLogInDiv();
|
||||
return
|
||||
}
|
||||
|
||||
function writeLogInDiv() {
|
||||
var logs = log["log"];
|
||||
var div = document.getElementById("settings").lastChild.lastChild;
|
||||
div.innerHTML = "";
|
||||
|
||||
var max = 50;
|
||||
|
||||
|
||||
for (var i = 0; i < logs.length; i++) {
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "P";
|
||||
|
||||
if (logs[i].includes("ERROR")) {
|
||||
// case "warnings": msgType = "warningMsg"; break;
|
||||
newEntry["class"] = "errorMsg";
|
||||
}
|
||||
|
||||
if (logs[i].includes("WARNING")) {
|
||||
// case "warnings": msgType = "warningMsg"; break;
|
||||
newEntry["class"] = "warningMsg";
|
||||
}
|
||||
|
||||
newEntry["_text"] = logs[i];
|
||||
|
||||
div.appendChild(createElement(newEntry));
|
||||
}
|
||||
|
||||
calculateWrapperHeight();
|
||||
var scrollDiv = document.getElementById("box-wrapper");
|
||||
scrollDiv.scrollTop = scrollDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function showLog(obj) {
|
||||
//logInterval = setInterval(updateLog, 5000);
|
||||
|
||||
var logs = log["log"];
|
||||
|
||||
var div = document.getElementById("settings");
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "HR";
|
||||
div.appendChild(createElement(newEntry));
|
||||
//div = div.lastChild;
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
newEntry["class"] = "button";
|
||||
newEntry["value"] = "Empty Log";
|
||||
newEntry["onclick"] = "emptyLog()";
|
||||
div.appendChild(createElement(newEntry));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "code";
|
||||
newEntry["_text"] = "Update Log: ";
|
||||
div.appendChild(createElement(newEntry));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "checkbox";
|
||||
//newEntry["checked"] = "checkbox";
|
||||
newEntry["onclick"] = "logUpdates(this)";
|
||||
div.appendChild(createElement(newEntry));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "HR";
|
||||
div.appendChild(createElement(newEntry));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var newWrapper = new Object();
|
||||
newWrapper["_element"] = "DIV";
|
||||
newWrapper["id"] = "box-wrapper";
|
||||
div.appendChild(createElement(newWrapper));
|
||||
div = div.lastChild;
|
||||
|
||||
var newPre = new Object();
|
||||
newPre["_element"] = "PRE";
|
||||
newPre["id"] = "logScreen";
|
||||
div.appendChild(createElement(newPre));
|
||||
|
||||
div = div.lastChild;
|
||||
|
||||
writeLogInDiv()
|
||||
return
|
||||
}
|
||||
|
||||
function emptyLog() {
|
||||
var data = new Object();
|
||||
data["cmd"] = "emptyLog";
|
||||
xTeVe(data);
|
||||
return
|
||||
}
|
||||
|
||||
function logUpdates(elm) {
|
||||
switch(elm.checked) {
|
||||
case false: clearInterval(logInterval); break;
|
||||
case true: logInterval = setInterval(updateLog, 5000); break;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
var Log = /** @class */ (function () {
|
||||
function Log() {
|
||||
}
|
||||
Log.prototype.createLog = function (entry) {
|
||||
var element = document.createElement("PRE");
|
||||
if (entry.indexOf("WARNING") != -1) {
|
||||
element.className = "warningMsg";
|
||||
}
|
||||
if (entry.indexOf("ERROR") != -1) {
|
||||
element.className = "errorMsg";
|
||||
}
|
||||
if (entry.indexOf("DEBUG") != -1) {
|
||||
element.className = "debugMsg";
|
||||
}
|
||||
element.innerHTML = entry;
|
||||
return element;
|
||||
};
|
||||
return Log;
|
||||
}());
|
||||
function showLogs(bottom) {
|
||||
var log = new Log();
|
||||
var logs = SERVER["log"]["log"];
|
||||
var div = document.getElementById("content_log");
|
||||
div.innerHTML = "";
|
||||
var keys = getObjKeys(logs);
|
||||
keys.forEach(function (logID) {
|
||||
var entry = log.createLog(logs[logID]);
|
||||
div.append(entry);
|
||||
});
|
||||
setTimeout(function () {
|
||||
if (bottom == true) {
|
||||
var wrapper = document.getElementById("box-wrapper");
|
||||
wrapper.scrollTop = wrapper.scrollHeight;
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
function resetLogs() {
|
||||
var cmd = "resetLogs";
|
||||
var data = new Object();
|
||||
var server = new Server(cmd);
|
||||
server.request(data);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
-754
@@ -1,754 +0,0 @@
|
||||
|
||||
function setMenuItem() {
|
||||
|
||||
menu = new Object();
|
||||
subMenu = new Object();
|
||||
|
||||
var menu_m3u = new Object();
|
||||
menu_m3u["_menuType"] = "inputArray";
|
||||
menu_m3u["_element"] = "LI";
|
||||
menu_m3u["_configKey"] = "files.m3u";
|
||||
menu_m3u["_text"] = "Playlist";
|
||||
menu_m3u["_icon"] = "img/m3u.png";
|
||||
menu_m3u["_headline"] = "Playlists: Local or remote";
|
||||
menu_m3u["_usage"] = "<b>Info</b><br>Availability: File availability in percent<br>Streams: Number of streams in the file.<br>group-title: Streams that are assigned to a group. Simplifies filtering streams<br>tvg-id: This ID is used for automatic mapping, must match with the channel ID in the XMLTV file.<br>Unique ID: Streams with a unique ID to identify them. Allows channel name changes in the M3U without losing the XMLTV mapping (PPV / live events).<br><br><b>Usage M3U:</b><br>Remote playlist: http://your.iptv.provider.com/file.m3u<br>Local playlist: /path/to/file.m3u<br><br><b>Usage HDHomeRun:</b><br>IP: 192.168.1.10:5004<br>"
|
||||
menu_m3u["name"] = "file";
|
||||
menu_m3u["id"] = "file";
|
||||
menu_m3u["value"] = menu_m3u["name"];
|
||||
menu_m3u["placeholder"] = "Playlist: local or remote";
|
||||
menu_m3u["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_m3u["class"] = "menu-notActive";
|
||||
|
||||
|
||||
var menu_filter = new Object();
|
||||
menu_filter["_menuType"] = "inputArray";
|
||||
menu_filter["_element"] = "LI";
|
||||
menu_filter["_configKey"] = "filter";
|
||||
menu_filter["_text"] = "Filter";
|
||||
menu_filter["_icon"] = "img/filter.png";
|
||||
menu_filter["_headline"] = "Filter by M3U parameters, e.g. group-title";
|
||||
menu_filter["_usage"] = "<b>Usage:</b><br>Sport - All sports channels<br>Sport {HD} - All HD sports channels<br>Sport {HD} !{ES,DE} - All HD sports channels, but no Spanish and German<br><br>To filter the streams of a HDHomeRun, the playlist name can be entered:<br>My tuner {HD}"
|
||||
//menu_filter["_usage"] = "<b>Usage:</b><br>All sports channels: Sport<br>All HD sports channels: Sport {HD}<br>All HD sports channels, but no Spanish and German: Sport {HD} !{ES,DE}"
|
||||
menu_filter["name"] = "filter";
|
||||
menu_filter["id"] = "M3U";
|
||||
menu_filter["value"] = menu_filter["name"];
|
||||
menu_filter["placeholder"] = "Filter streams: Sport";
|
||||
menu_filter["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_filter["class"] = "menu-notActive";
|
||||
|
||||
var menu_id = new Object();
|
||||
menu_id["_menuType"] = "inputArray";
|
||||
menu_id["_element"] = "LI";
|
||||
menu_id["_configKey"] = "id";
|
||||
menu_id["_text"] = "PMS ID";
|
||||
menu_id["_icon"] = "img/number.png";
|
||||
menu_id["_headline"] = "Setup PMS guide number";
|
||||
menu_id["_usage"] = 'Some playlists have unique channel IDs.<br>Enter the keyword of the ID. The channel assignment in PMS will change as a result.<br><br>e.g. channelID<br>#EXTINF:0 type="stream" <b>channelId</b>="81", My Streaming Channel HD<br><br>Only enter here if you know what you are doing!'
|
||||
menu_id["name"] = "id";
|
||||
menu_id["id"] = "id";
|
||||
menu_id["value"] = menu_id["name"];
|
||||
menu_id["placeholder"] = "Unique ID from the M3U file";
|
||||
menu_id["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_id["class"] = "menu-notActive";
|
||||
|
||||
|
||||
var menu_xmltv = new Object();
|
||||
menu_xmltv["_menuType"] = "inputArray";
|
||||
menu_xmltv["_element"] = "LI";
|
||||
menu_xmltv["_configKey"] = "files.xmltv";
|
||||
menu_xmltv["_text"] = "XMLTV";
|
||||
menu_xmltv["_icon"] = "img/xmltv.png";
|
||||
menu_xmltv["_headline"] = "XMLTV files: Local or remote";
|
||||
menu_xmltv["_usage"] = "<b>Info:</b><br>Availability: File availability in percent<br>Channels: Number of channels in the file<br>Programs: Number of EPG data<br><br><b>Usage:</b><br>Remote XMLTV file: http://your.epg.provider.com/guide.xml<br>Local XMLTV file: /path/to/guide.xml"
|
||||
menu_xmltv["name"] = "xmltv";
|
||||
menu_xmltv["id"] = "xmltv";
|
||||
menu_xmltv["value"] = menu_xmltv["name"];
|
||||
menu_xmltv["placeholder"] = "XMLTV File: local or remote";
|
||||
menu_xmltv["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_xmltv["class"] = "menu-notActive";
|
||||
|
||||
menu_mapping = new Object();
|
||||
menu_mapping["_element"] = "LI";
|
||||
menu_mapping["_text"] = "Mapping";
|
||||
menu_mapping["_icon"] = "img/mapping.png";
|
||||
menu_mapping["_configKey"] = "mapping";
|
||||
menu_mapping["_headline"] = "XMLTV assignment and sorting of channels";
|
||||
menu_mapping["id"] = "mapping";
|
||||
menu_mapping["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_mapping["class"] = "menu-notActive phone";
|
||||
|
||||
menu_users = new Object();
|
||||
menu_users["_element"] = "LI";
|
||||
menu_users["_text"] = "Users";
|
||||
menu_users["_icon"] = "img/users.png";
|
||||
menu_users["_configKey"] = "users";
|
||||
menu_users["_headline"] = "Administration of users and permissions";
|
||||
menu_users["id"] = "users";
|
||||
menu_users["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_users["class"] = "menu-notActive";
|
||||
menu_users["_usage"] = "<b>Authorization groups:</b><br>WEB: Users can log in to the web interface<br>PMS: Programs like Plex can access the channel list. Login via DVR IP: username:password@xteve.ip:port<br>M3U: Allows clients to download the M3U playlist.<br>XML: Allows clients to download the XMLTV file.<br>API: Allows clients to use the API interface.<br><br>!!! For PMS authentication, only the following special characters are valid: !$()=.,-:;<br><br>The individual authentication groups can be activated / deactivated in the settings menu."
|
||||
|
||||
menu_settings = new Object();
|
||||
menu_settings["_element"] = "LI";
|
||||
menu_settings["_text"] = "Settings";
|
||||
menu_settings["_icon"] = "img/settings.png";
|
||||
menu_settings["_configKey"] = "settings";
|
||||
menu_settings["_headline"] = "Settings";
|
||||
menu_settings["_subMenu"] = "701,702,703,704,705,706,707,708,799,710,711,712,713,714";
|
||||
menu_settings["id"] = "settings";
|
||||
menu_settings["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_settings["class"] = "menu-notActive";
|
||||
|
||||
menu_log = new Object();
|
||||
menu_log["_element"] = "LI";
|
||||
menu_log["_text"] = "Log";
|
||||
menu_log["_icon"] = "img/log.png";
|
||||
menu_log["_headline"] = "Log";
|
||||
menu_log["_configKey"] = "log";
|
||||
menu_log["id"] = "log";
|
||||
menu_log["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_log["class"] = "menu-notActive";
|
||||
|
||||
menu_logout = new Object();
|
||||
menu_logout["_element"] = "LI";
|
||||
menu_logout["_text"] = "Logout";
|
||||
menu_logout["_icon"] = "img/logout.png";
|
||||
menu_logout["id"] = "logout";
|
||||
menu_logout["onclick"] = "javascript: logout();";
|
||||
menu_logout["class"] = "menu-notActive";
|
||||
|
||||
var menu_schedule = new Object();
|
||||
menu_schedule["_menuType"] = "inputArray";
|
||||
menu_schedule["_element"] = "LI";
|
||||
menu_schedule["_configKey"] = "update";
|
||||
menu_schedule["_text"] = "Schedule";
|
||||
menu_schedule["_icon"] = "img/schedule.png";
|
||||
menu_schedule["_headline"] = "Schedule for updating M3U, XMLTV files and creating a local backup";
|
||||
menu_schedule["_usage"] = "<b>Usage:</b><br>0815 = 8:15 am<br>1930 = 7:30 pm"
|
||||
menu_schedule["name"] = "update";
|
||||
menu_schedule["id"] = "update";
|
||||
menu_schedule["value"] = menu_id["name"];
|
||||
menu_schedule["placeholder"]= "time of day (24-hour clock)";
|
||||
menu_schedule["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_schedule["class"] = "menu-notActive";
|
||||
|
||||
var menu_filesUpdate = new Object();
|
||||
menu_filesUpdate["_element"] = "LI";
|
||||
menu_filesUpdate["_menuType"] = "checkbox";
|
||||
menu_filesUpdate["_configKey"] = "files.update";
|
||||
menu_filesUpdate["_label"] = "Update the provider files at system startup";
|
||||
menu_filesUpdate["_headline"] = "Update the provider files at system startup";
|
||||
menu_filesUpdate["_usage"] = "Playlists and XMLTV files are updated by xTeVe at system startup."
|
||||
menu_filesUpdate["name"] = "files.update";
|
||||
menu_filesUpdate["id"] = "files.update";
|
||||
menu_filesUpdate["value"] = menu_filesUpdate["name"];
|
||||
menu_filesUpdate["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_filesUpdate["class"] = "menu-notActive";
|
||||
|
||||
var menu_tuner = new Object();
|
||||
menu_tuner["_element"] = "LI";
|
||||
menu_tuner["_menuType"] = "select";
|
||||
menu_tuner["_configKey"] = "tuner";
|
||||
menu_tuner["_label"] = "Available tuners";
|
||||
menu_tuner["_text"] = "Tuner";
|
||||
menu_tuner["_icon"] = "img/tuner.png";
|
||||
menu_tuner["_headline"] = "Number of tuners";
|
||||
menu_tuner["_usage"] = "This setting is only used by Plex and Emby.<br>The number of concurrent streams allowed by the IPTV provider.<br>After a change, xTeVe must be delete in the PMS DVR settings and set up again."
|
||||
menu_tuner["name"] = "tuner";
|
||||
menu_tuner["id"] = "tuner";
|
||||
menu_tuner["value"] = menu_tuner["name"];
|
||||
menu_tuner["placeholder"] = "Number of tuners";
|
||||
menu_tuner["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_tuner["class"] = "menu-notActive";
|
||||
|
||||
var optionValues = new Array();
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
optionValues.push(i)
|
||||
}
|
||||
menu_tuner["_optionValues"] = optionValues;
|
||||
|
||||
var menu_epg = new Object();
|
||||
menu_epg["_element"] = "LI";
|
||||
menu_epg["_menuType"] = "select";
|
||||
menu_epg["_configKey"] = "epgSource";
|
||||
menu_epg["_label"] = "Selection of the EPG source";
|
||||
menu_epg["_text"] = "EPG source";
|
||||
menu_epg["_headline"] = "Selection of the EPG source";
|
||||
menu_epg["_usage"] = "PMS: Use EPG data from Plex or Emby.<br>XEPG: Use of external EPG data (XMLTV).<br> Several XMLTV sources possible.<br> Allows editing and order channels.<br> M3U / XMLTV export (HTTP link for IPTV apps)."
|
||||
menu_epg["name"] = "epgSource";
|
||||
menu_epg["id"] = "epgSource";
|
||||
menu_epg["value"] = menu_epg["name"];
|
||||
menu_epg["placeholder"] = "EPG source";
|
||||
menu_epg["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_epg["class"] = "menu-notActive";
|
||||
menu_epg["_optionValues"] = new Array("PMS", "XEPG");
|
||||
|
||||
var menu_xepg = new Object();
|
||||
menu_xepg["_element"] = "LI";
|
||||
menu_xepg["_menuType"] = "checkbox";
|
||||
menu_xepg["_configKey"] = "xteveAutoUpdate";
|
||||
menu_xepg["_label"] = "Automatic update of xTeVe";
|
||||
menu_xepg["_headline"] = "Automatic update of xTeVe";
|
||||
menu_xepg["_usage"] = "If a new version of xTeVe is available, it will be automatically installed."
|
||||
menu_xepg["name"] = "xteveAutoUpdate";
|
||||
menu_xepg["id"] = "xteveAutoUpdate";
|
||||
menu_xepg["value"] = menu_xepg["name"];
|
||||
menu_xepg["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_xepg["class"] = "menu-notActive";
|
||||
|
||||
var menu_autoBackupPath = new Object();
|
||||
menu_autoBackupPath["_element"] = "LI";
|
||||
menu_autoBackupPath["_menuType"] = "singleInput";
|
||||
menu_autoBackupPath["_configKey"] = "backup.path";
|
||||
menu_autoBackupPath["_label"] = "Location for automatic backups";
|
||||
menu_autoBackupPath["_headline"] = "Location for automatic backups";
|
||||
menu_autoBackupPath["_usage"] = "Before any update of the provider data by the schedule, xTeVe creates a backup. The path for the automatic backups can be changed. xTeVe requires write permission for this folder."
|
||||
menu_autoBackupPath["name"] = "backup.path";
|
||||
menu_autoBackupPath["id"] = "backup.path";
|
||||
menu_autoBackupPath["value"] = menu_autoBackupPath["name"];
|
||||
menu_autoBackupPath["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_autoBackupPath["class"] = "menu-notActive";
|
||||
|
||||
var menu_autoBackupKeep = new Object();
|
||||
menu_autoBackupKeep["_element"] = "LI";
|
||||
menu_autoBackupKeep["_menuType"] = "select";
|
||||
menu_autoBackupKeep["_configKey"] = "backup.keep";
|
||||
menu_autoBackupKeep["_text"] = "Keep";
|
||||
menu_autoBackupKeep["_label"] = "Number of backups to keep";
|
||||
menu_autoBackupKeep["_headline"] = "Number of backups to keep";
|
||||
menu_autoBackupKeep["_usage"] = ""
|
||||
menu_autoBackupKeep["name"] = "backup.keep";
|
||||
menu_autoBackupKeep["id"] = "backup.keep";
|
||||
menu_autoBackupKeep["value"] = menu_autoBackupKeep["name"];
|
||||
menu_autoBackupKeep["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_autoBackupKeep["class"] = "menu-notActive";
|
||||
|
||||
var optionValues = new Array(5, 10, 20, 30, 40, 50);
|
||||
menu_autoBackupKeep["_optionValues"] = optionValues;
|
||||
|
||||
|
||||
var menu_buffer = new Object();
|
||||
menu_buffer["_element"] = "LI";
|
||||
menu_buffer["_menuType"] = "checkbox";
|
||||
menu_buffer["_configKey"] = "buffer";
|
||||
menu_buffer["_label"] = "Stream buffering [Experimental]";
|
||||
menu_buffer["_headline"] = "Stream buffering [Experimental]";
|
||||
menu_buffer["_usage"] = "With activated buffer, streams can be played and recorded more fluently.<br>The stream is passed from xTeVe to Plex / Emby"
|
||||
menu_buffer["name"] = "buffer";
|
||||
menu_buffer["id"] = "buffer";
|
||||
menu_buffer["value"] = menu_buffer["name"];
|
||||
menu_buffer["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_buffer["class"] = "menu-notActive";
|
||||
|
||||
var menu_api = new Object();
|
||||
menu_api["_element"] = "LI";
|
||||
menu_api["_menuType"] = "checkbox";
|
||||
menu_api["_configKey"] = "api";
|
||||
menu_api["_label"] = "API interface";
|
||||
menu_api["_headline"] = "API interface";
|
||||
menu_api["_usage"] = 'Via API interface it is possible to send commands to xTeVe. API documentation is available <a href="https://xteve.de?scroll=api">here</a> '
|
||||
//menu_api["_usage"] = 'Via API interface it is possible to send commands to xTeVe. API documentation is available <a href="http://localhost:1313?scroll=api">here</a> '
|
||||
menu_api["name"] = "api";
|
||||
menu_api["id"] = "api";
|
||||
menu_api["value"] = menu_api["name"];
|
||||
menu_api["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_api["class"] = "menu-notActive";
|
||||
|
||||
var menu_authenticationWeb = new Object();
|
||||
menu_authenticationWeb["_element"] = "LI";
|
||||
menu_authenticationWeb["_menuType"] = "checkbox";
|
||||
menu_authenticationWeb["_configKey"] = "authentication.web";
|
||||
menu_authenticationWeb["_label"] = "User authentication";
|
||||
menu_authenticationWeb["_headline"] = "User authentication";
|
||||
menu_authenticationWeb["_usage"] = "Access to xTeVe requires authentication."
|
||||
menu_authenticationWeb["name"] = "authentication.web";
|
||||
menu_authenticationWeb["id"] = "authentication.web";
|
||||
menu_authenticationWeb["value"] = menu_authenticationWeb["name"];
|
||||
menu_authenticationWeb["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_authenticationWeb["class"] = "menu-notActive";
|
||||
|
||||
var menu_authenticationPms = new Object();
|
||||
menu_authenticationPms["_element"] = "LI";
|
||||
menu_authenticationPms["_menuType"] = "checkbox";
|
||||
menu_authenticationPms["_configKey"] = "authentication.pms";
|
||||
menu_authenticationPms["_label"] = "Plex authentication.";
|
||||
menu_authenticationPms["_headline"] = "Plex authentication.";
|
||||
menu_authenticationPms["_usage"] = "Plex requests are only possible with authentication.<br>Warning!!! After activating this function xTeVe must be delete in the PMS DVR settings and set up again."
|
||||
menu_authenticationPms["name"] = "authentication.pms";
|
||||
menu_authenticationPms["id"] = "authentication.pms";
|
||||
menu_authenticationPms["value"] = menu_authenticationPms["name"];
|
||||
menu_authenticationPms["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_authenticationPms["class"] = "menu-notActive";
|
||||
|
||||
var menu_authenticationM3u = new Object();
|
||||
menu_authenticationM3u["_element"] = "LI";
|
||||
menu_authenticationM3u["_menuType"] = "checkbox";
|
||||
menu_authenticationM3u["_configKey"] = "authentication.m3u";
|
||||
menu_authenticationM3u["_label"] = "M3U authentication.";
|
||||
menu_authenticationM3u["_headline"] = "M3U authentication.";
|
||||
menu_authenticationM3u["_usage"] = "Downloading the M3U file via an HTTP request is only possible with authentication."
|
||||
menu_authenticationM3u["name"] = "authentication.m3u";
|
||||
menu_authenticationM3u["id"] = "authentication.m3u";
|
||||
menu_authenticationM3u["value"] = menu_authenticationM3u["name"];
|
||||
menu_authenticationM3u["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_authenticationM3u["class"] = "menu-notActive";
|
||||
|
||||
|
||||
var menu_authenticationXml = new Object();
|
||||
menu_authenticationXml["_element"] = "LI";
|
||||
menu_authenticationXml["_menuType"] = "checkbox";
|
||||
menu_authenticationXml["_configKey"] = "authentication.xml";
|
||||
menu_authenticationXml["_label"] = "XEPG authentication";
|
||||
menu_authenticationXml["_headline"] = "XEPG authentication";
|
||||
menu_authenticationXml["_usage"] = "Downloading the XEPG (XMLTV) file via an HTTP request is only possible with authentication."
|
||||
menu_authenticationXml["name"] = "authentication.xml";
|
||||
menu_authenticationXml["id"] = "authentication.xml";
|
||||
menu_authenticationXml["value"] = menu_authenticationXml["name"];
|
||||
menu_authenticationXml["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_authenticationXml["class"] = "menu-notActive";
|
||||
|
||||
var menu_authenticationApi = new Object();
|
||||
menu_authenticationApi["_element"] = "LI";
|
||||
menu_authenticationApi["_menuType"] = "checkbox";
|
||||
menu_authenticationApi["_configKey"] = "authentication.api";
|
||||
menu_authenticationApi["_label"] = "API authentication";
|
||||
menu_authenticationApi["_headline"] = "API authentication";
|
||||
menu_authenticationApi["_usage"] = "Access to the API interface is only possible with authentication."
|
||||
menu_authenticationApi["name"] = "authentication.api";
|
||||
menu_authenticationApi["id"] = "authentication.api";
|
||||
menu_authenticationApi["value"] = menu_authenticationApi["name"];
|
||||
menu_authenticationApi["onclick"] = "javascript: toggleMenu(this);";
|
||||
menu_authenticationApi["class"] = "menu-notActive";
|
||||
|
||||
|
||||
// Main menu
|
||||
menu[10] = menu_m3u;
|
||||
|
||||
switch(config["epgSource"]) {
|
||||
case "PMS":
|
||||
menu[20] = menu_id;
|
||||
break;
|
||||
|
||||
case "XMLTV":
|
||||
menu[40] = menu_xmltv;
|
||||
break;
|
||||
|
||||
case "XEPG":
|
||||
menu[40] = menu_xmltv;
|
||||
menu[50] = menu_mapping;
|
||||
break;
|
||||
}
|
||||
|
||||
menu[30] = menu_filter;
|
||||
|
||||
if (config["authentication.web"] == true) {
|
||||
menu[60] = menu_users;
|
||||
}
|
||||
|
||||
menu[70] = menu_settings;
|
||||
menu[80] = menu_log;
|
||||
if (config["authentication.web"] == true) {
|
||||
menu[100] = menu_logout;
|
||||
}
|
||||
|
||||
|
||||
// Sub-Menu
|
||||
|
||||
subMenu[701] = menu_schedule;
|
||||
subMenu[702] = menu_filesUpdate;
|
||||
subMenu[703] = menu_tuner;
|
||||
subMenu[704] = menu_epg;
|
||||
subMenu[705] = menu_xepg;
|
||||
subMenu[706] = menu_autoBackupPath;
|
||||
subMenu[707] = menu_autoBackupKeep;
|
||||
subMenu[708] = menu_buffer;
|
||||
|
||||
subMenu[710] = menu_authenticationWeb;
|
||||
|
||||
if (config["authentication.web"] == true) {
|
||||
subMenu[711] = menu_authenticationPms;
|
||||
subMenu[712] = menu_authenticationM3u;
|
||||
subMenu[713] = menu_authenticationXml;
|
||||
subMenu[714] = menu_authenticationApi;
|
||||
}
|
||||
|
||||
subMenu[799] = menu_api;
|
||||
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function createMenu() {
|
||||
|
||||
showElement("popup", false);
|
||||
|
||||
//console.log(config);
|
||||
setMenuItem();
|
||||
var menuItems = getObjKeys(menu)
|
||||
var nav = document.getElementsByTagName("NAV")[0];
|
||||
nav.innerHTML = "";
|
||||
var newItem = new Object();
|
||||
|
||||
for (var i = 0; i < menuItems.length; i++) {
|
||||
|
||||
|
||||
var newItem = menu[menuItems[i]];
|
||||
newItem["id"] = menuItems[i];
|
||||
|
||||
|
||||
switch(newItem.hasOwnProperty("_icon")) {
|
||||
case true:
|
||||
var itemText = newItem["_text"];
|
||||
delete newItem["_text"]
|
||||
nav.appendChild(createElement(newItem));
|
||||
newItem["_text"] = itemText;
|
||||
var newIcon = new Object();
|
||||
newIcon["_element"] = "IMG";
|
||||
newIcon["src"] = newItem["_icon"];
|
||||
|
||||
var currentElement = document.getElementById(menuItems[i]);
|
||||
currentElement.appendChild(createElement(newIcon));
|
||||
|
||||
|
||||
var text = new Object();
|
||||
text["_element"] = "P"
|
||||
text["_text"] = itemText;
|
||||
text["class"] = "nav-text"
|
||||
currentElement.appendChild(createElement(text));
|
||||
break;
|
||||
|
||||
default:
|
||||
nav.appendChild(createElement(newIcon));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if (activeMenu != undefined) {
|
||||
//console.log(activeMenu);
|
||||
toggleMenu(activeMenu);
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function toggleMenu(elm) {
|
||||
//showStreams(false);
|
||||
clearInterval(logInterval)
|
||||
activeMenu = elm;
|
||||
var item = menu[elm.id]
|
||||
var div = document.getElementById("settings");
|
||||
div.innerHTML = "";
|
||||
|
||||
// Set Headline
|
||||
var headline = new Object();
|
||||
headline["_element"] = "H4";
|
||||
headline["_text"] = item["_headline"];
|
||||
div.appendChild(createElement(headline));
|
||||
|
||||
// Sub-Menu
|
||||
if (item.hasOwnProperty("_subMenu") == true) {
|
||||
openSubMenu(item);
|
||||
return
|
||||
}
|
||||
|
||||
// Mapping, Users, Log, Files
|
||||
switch(item["_configKey"]) {
|
||||
case "mapping": openMappingEditor(item); return; break;
|
||||
case "users": openUsers(item); return; break;
|
||||
case "log": showLog(item); return; break;
|
||||
case "files.m3u": openFiles(item, "m3u"); return; break;
|
||||
case "files.xmltv": openFiles(item, "xmltv"); return; break;
|
||||
|
||||
case "filter": showStreams(true); break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var newHR = new Object();
|
||||
newHR["_element"] = "HR"
|
||||
div.appendChild(createElement(newHR));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
//newEntry["class"] = "save";
|
||||
newEntry["value"] = "Save";
|
||||
newEntry["onclick"] = "saveData2('settings')"
|
||||
div.appendChild(createElement(newEntry));
|
||||
|
||||
|
||||
var newWrapper = new Object();
|
||||
newWrapper["_element"] = "DIV";
|
||||
newWrapper["id"] = "box-wrapper";
|
||||
div.appendChild(createElement(newWrapper));
|
||||
|
||||
div = div.lastChild;
|
||||
|
||||
div.appendChild(createMenuItem(item))
|
||||
|
||||
// usage Info
|
||||
switch(menu[activeMenu.id].hasOwnProperty("_usage")) {
|
||||
case true:
|
||||
var usageItem = new Object();
|
||||
usageItem["_element"] = "PRE"
|
||||
usageItem["_text"] = menu[activeMenu.id]["_usage"];
|
||||
div.appendChild(createElement(usageItem));
|
||||
}
|
||||
|
||||
calculateWrapperHeight();
|
||||
|
||||
}
|
||||
|
||||
function createMenuItem(item) {
|
||||
var element = document.createElement("DIV");
|
||||
switch(item["_menuType"]) {
|
||||
case "inputArray":
|
||||
if (config.hasOwnProperty(item["_configKey"]) == true) {
|
||||
var value = config[item["_configKey"]];
|
||||
} else {
|
||||
var value = new Array();
|
||||
}
|
||||
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
var newEntry = new Object();
|
||||
newEntry = item
|
||||
delete newEntry["onclick"];
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["value"] = value[i];
|
||||
newEntry["type"] = "search";
|
||||
newEntry["data-menutype"] = item["_menuType"];
|
||||
newEntry["data-menukey"] = item["_configKey"];
|
||||
element.appendChild(createElement(newEntry));
|
||||
|
||||
}
|
||||
// New entry for array
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "search";
|
||||
newEntry["name"] = item["name"];
|
||||
newEntry["placeholder"] = item["placeholder"];
|
||||
newEntry["value"] = "";
|
||||
newEntry["data-menutype"] = item["_menuType"];
|
||||
newEntry["data-menukey"] = item["_configKey"];
|
||||
element.appendChild(createElement(newEntry));
|
||||
break;
|
||||
|
||||
case "singleInput":
|
||||
var value = config[item["_configKey"]];
|
||||
if (value == undefined) {
|
||||
value = "";
|
||||
}
|
||||
var newEntry = new Object();
|
||||
newEntry = item;
|
||||
delete newEntry["onclick"];
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["value"] = value;
|
||||
newEntry["type"] = "search";
|
||||
newEntry["data-menutype"] = item["_menuType"];
|
||||
newEntry["data-menukey"] = item["_configKey"];
|
||||
element.appendChild(createElement(newEntry));
|
||||
break;
|
||||
|
||||
case "checkbox":
|
||||
var value = config[item["_configKey"]];
|
||||
if (value == undefined) {
|
||||
value = false;
|
||||
}
|
||||
var newEntry = new Object();
|
||||
newEntry = item;
|
||||
delete newEntry["onclick"];
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["value"] = value;
|
||||
newEntry["type"] = "checkbox";
|
||||
newEntry["data-menutype"] = item["_menuType"];
|
||||
newEntry["data-menukey"] = item["_configKey"];
|
||||
element.appendChild(createElement(newEntry));
|
||||
element.getElementsByTagName("INPUT")[0].checked = value;
|
||||
break;
|
||||
|
||||
case "select":
|
||||
var value = config[item["_configKey"]];
|
||||
var newEntry = new Object();
|
||||
newEntry = item;
|
||||
delete newEntry["onclick"]
|
||||
newEntry["_element"] = "SELECT";
|
||||
element.appendChild(createElement(newEntry));
|
||||
var selectElement = element.getElementsByTagName("SELECT")[0];
|
||||
var values = item["_optionValues"];
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var newEntry = new Object;
|
||||
newEntry["_element"] = "OPTION";
|
||||
newEntry["_text"] = item["_text"] + ": " + values[i];
|
||||
newEntry["value"] = values[i];
|
||||
selectElement.appendChild(createElement(newEntry));
|
||||
}
|
||||
selectElement.value = value;
|
||||
break;
|
||||
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function openSubMenu(item) {
|
||||
var entrys = item["_subMenu"].split(",");
|
||||
var div = document.getElementById("settings");
|
||||
|
||||
var newHR = new Object();
|
||||
newHR["_element"] = "HR"
|
||||
div.appendChild(createElement(newHR));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
//newEntry["class"] = "save";
|
||||
newEntry["value"] = "Save";
|
||||
newEntry["onclick"] = "saveData2('settings')"
|
||||
div.appendChild(createElement(newEntry));
|
||||
|
||||
if (item["_configKey"] == "settings") {
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
//newEntry["class"] = "save";
|
||||
newEntry["value"] = "Backup";
|
||||
newEntry["onclick"] = "xteveBackup()"
|
||||
div.appendChild(createElement(newEntry));
|
||||
}
|
||||
|
||||
if (item["_configKey"] == "settings") {
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
//newEntry["class"] = "save";
|
||||
newEntry["value"] = "Restore";
|
||||
newEntry["onclick"] = "xteveRestore(this)"
|
||||
div.appendChild(createElement(newEntry));
|
||||
}
|
||||
|
||||
|
||||
var newWrapper = new Object();
|
||||
newWrapper["_element"] = "DIV";
|
||||
newWrapper["id"] = "box-wrapper";
|
||||
div.appendChild(createElement(newWrapper));
|
||||
|
||||
div = div.lastChild;
|
||||
|
||||
|
||||
for (var i = 0; i < entrys.length; i++) {
|
||||
var item = subMenu[entrys[i]];
|
||||
if (item == undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
var container = new Object();
|
||||
container["_element"] = "DIV";
|
||||
div.appendChild(createElement(container));
|
||||
|
||||
var divContainer = div.lastChild;
|
||||
|
||||
var headline = new Object();
|
||||
headline["_element"] = "H5";
|
||||
headline["_text"] = item["_headline"];
|
||||
divContainer.appendChild(createElement(headline));
|
||||
|
||||
divContainer.appendChild(createMenuItem(item))
|
||||
|
||||
switch(item.hasOwnProperty("_usage")) {
|
||||
case true:
|
||||
var usageItem = new Object();
|
||||
usageItem["_element"] = "PRE"
|
||||
usageItem["_text"] = item["_usage"];
|
||||
divContainer.appendChild(createElement(usageItem));
|
||||
}
|
||||
|
||||
var hr = new Object();
|
||||
hr["_element"] = "HR";
|
||||
divContainer.appendChild(createElement(hr));
|
||||
|
||||
}
|
||||
|
||||
calculateWrapperHeight();
|
||||
return
|
||||
}
|
||||
|
||||
function saveData2(elm) {
|
||||
var div = document.getElementById(elm);
|
||||
var inputs = div.getElementsByTagName("INPUT");
|
||||
var selects = div.getElementsByTagName("SELECT");
|
||||
var value, configKey;
|
||||
var data = new Object();
|
||||
var valueArr = new Array();
|
||||
var newData = false;
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].type != "button") {
|
||||
var menuType = inputs[i].getAttribute("data-menutype");
|
||||
|
||||
//console.log(menuType);
|
||||
switch(menuType) {
|
||||
case "singleInput":
|
||||
value = inputs[i].value;
|
||||
if (value == "" || value == undefined) {
|
||||
data = new Object();
|
||||
data["delete"] = inputs[i].name
|
||||
newData = true;
|
||||
} else {
|
||||
newData = true;
|
||||
data[inputs[i].name] = value;
|
||||
console.log(data);
|
||||
}
|
||||
break;
|
||||
case "inputArray":
|
||||
value = inputs[i].value;
|
||||
if (value != "" && value != undefined) {
|
||||
newData = true;
|
||||
valueArr.push(value)
|
||||
data[inputs[i].name] = valueArr;
|
||||
configKey = inputs[i].name;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "checkbox":
|
||||
value = inputs[i].checked
|
||||
data[inputs[i].name] = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Delete config key
|
||||
if (valueArr.length == 0 && newData == false) {
|
||||
newData = true;
|
||||
data = new Object();
|
||||
data["delete"] = configKey;
|
||||
}
|
||||
|
||||
|
||||
for (var i = 0; i < selects.length; i++) {
|
||||
var value = selects[i].options[selects[i].selectedIndex].value;
|
||||
switch(isNaN(value)) {
|
||||
case false: value = parseInt(value); break;
|
||||
}
|
||||
|
||||
data[selects[i].name] = value;
|
||||
newData = true;
|
||||
}
|
||||
|
||||
//console.log(data, newData);
|
||||
|
||||
if (newData == true) {
|
||||
data["cmd"] = "saveConfig";
|
||||
if (!data.hasOwnProperty('filter')) {
|
||||
data["filter"] = config["filter"]
|
||||
}
|
||||
var settings = new Object();
|
||||
settings["cmd"] = data["cmd"];
|
||||
settings["settings"] = data;
|
||||
console.log(settings);
|
||||
xTeVe(settings);
|
||||
}
|
||||
}
|
||||
-1748
File diff suppressed because it is too large.
Load diff
@@ -1,105 +0,0 @@
|
||||
var Server = /** @class */ (function () {
|
||||
function Server(cmd) {
|
||||
this.cmd = cmd;
|
||||
}
|
||||
Server.prototype.request = function (data) {
|
||||
if (SERVER_CONNECTION == true) {
|
||||
return;
|
||||
}
|
||||
SERVER_CONNECTION = true;
|
||||
console.log(data);
|
||||
if (this.cmd != "updateLog") {
|
||||
showElement("loading", true);
|
||||
UNDO = new Object();
|
||||
}
|
||||
switch (window.location.protocol) {
|
||||
case "http:":
|
||||
this.protocol = "ws://";
|
||||
break;
|
||||
case "https:":
|
||||
this.protocol = "wss://";
|
||||
break;
|
||||
}
|
||||
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token");
|
||||
data["cmd"] = this.cmd;
|
||||
var ws = new WebSocket(url);
|
||||
ws.onopen = function () {
|
||||
WS_AVAILABLE = true;
|
||||
console.log("REQUEST (JS):");
|
||||
console.log(data);
|
||||
console.log("REQUEST: (JSON)");
|
||||
console.log(JSON.stringify(data));
|
||||
this.send(JSON.stringify(data));
|
||||
};
|
||||
ws.onerror = function (e) {
|
||||
console.log("No websocket connection to xTeVe could be established. Check your network configuration.");
|
||||
SERVER_CONNECTION = false;
|
||||
if (WS_AVAILABLE == false) {
|
||||
alert("No websocket connection to xTeVe could be established. Check your network configuration.");
|
||||
}
|
||||
};
|
||||
ws.onmessage = function (e) {
|
||||
SERVER_CONNECTION = false;
|
||||
showElement("loading", false);
|
||||
console.log("RESPONSE:");
|
||||
var response = JSON.parse(e.data);
|
||||
console.log(response);
|
||||
if (response.hasOwnProperty("token")) {
|
||||
document.cookie = "Token=" + response["token"];
|
||||
}
|
||||
if (response["status"] == false) {
|
||||
alert(response["err"]);
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
location.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.hasOwnProperty("logoURL")) {
|
||||
var div = document.getElementById("channel-icon");
|
||||
div.value = response["logoURL"];
|
||||
div.className = "changed";
|
||||
return;
|
||||
}
|
||||
switch (data["cmd"]) {
|
||||
case "updateLog":
|
||||
SERVER["log"] = response["log"];
|
||||
if (document.getElementById("content_log")) {
|
||||
showLogs(false);
|
||||
}
|
||||
return;
|
||||
break;
|
||||
default:
|
||||
SERVER = new Object();
|
||||
SERVER = response;
|
||||
break;
|
||||
}
|
||||
if (response.hasOwnProperty("openMenu")) {
|
||||
var menu = document.getElementById(response["openMenu"]);
|
||||
menu.click();
|
||||
showElement("popup", false);
|
||||
}
|
||||
if (response.hasOwnProperty("openLink")) {
|
||||
window.location = response["openLink"];
|
||||
}
|
||||
if (response.hasOwnProperty("alert")) {
|
||||
alert(response["alert"]);
|
||||
}
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
location.reload();
|
||||
}
|
||||
if (response.hasOwnProperty("wizard")) {
|
||||
createLayout();
|
||||
configurationWizard[response["wizard"]].createWizard();
|
||||
return;
|
||||
}
|
||||
createLayout();
|
||||
};
|
||||
};
|
||||
return Server;
|
||||
}());
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length == 2)
|
||||
return parts.pop().split(";").shift();
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var SettingsCategory = /** @class */ (function () {
|
||||
function SettingsCategory() {
|
||||
this.DocumentID = "content_settings";
|
||||
}
|
||||
SettingsCategory.prototype.createCategoryHeadline = function (value) {
|
||||
var element = document.createElement("H4");
|
||||
element.innerHTML = value;
|
||||
return element;
|
||||
};
|
||||
SettingsCategory.prototype.createHR = function () {
|
||||
var element = document.createElement("HR");
|
||||
return element;
|
||||
};
|
||||
SettingsCategory.prototype.createSettings = function (settingsKey) {
|
||||
var setting = document.createElement("TR");
|
||||
var content = new PopupContent();
|
||||
var data = SERVER["settings"][settingsKey];
|
||||
switch (settingsKey) {
|
||||
// Texteingaben
|
||||
case "update":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.update.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "update", data.toString());
|
||||
input.setAttribute("placeholder", "{{.settings.update.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "backup.path":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.backupPath.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "backup.path", data);
|
||||
input.setAttribute("placeholder", "{{.settings.backupPath.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "temp.path":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.tempPath.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "temp.path", data);
|
||||
input.setAttribute("placeholder", "{{.settings.tmpPath.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "user.agent":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.userAgent.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "user.agent", data);
|
||||
input.setAttribute("placeholder", "{{.settings.userAgent.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "buffer.timeout":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "buffer.timeout", data);
|
||||
input.setAttribute("placeholder", "{{.settings.bufferTimeout.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
// Checkboxen
|
||||
case "authentication.web":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.authenticationWEB.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "authentication.pms":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.authenticationPMS.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "authentication.m3u":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.authenticationM3U.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "authentication.xml":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.authenticationXML.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "authentication.api":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.authenticationAPI.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "files.update":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.filesUpdate.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "cache.images":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.cacheImages.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "xepg.replace.missing.images":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.replaceEmptyImages.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "xteveAutoUpdate":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.xteveAutoUpdate.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "buffer":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "api":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.api.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
// Select
|
||||
case "tuner":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var text = new Array();
|
||||
var values = new Array();
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
text.push(i);
|
||||
values.push(i);
|
||||
}
|
||||
var select = content.createSelect(text, values, data, settingsKey);
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(select);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "epgSource":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.epgSource.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var text = ["PMS", "XEPG"];
|
||||
var values = ["PMS", "XEPG"];
|
||||
var select = content.createSelect(text, values, data, settingsKey);
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(select);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "backup.keep":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.backupKeep.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var text = ["5", "10", "20", "30", "40", "50"];
|
||||
var values = ["5", "10", "20", "30", "40", "50"];
|
||||
var select = content.createSelect(text, values, data, settingsKey);
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(select);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "buffer.size.kb":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.bufferSize.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var text = ["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"];
|
||||
var values = ["512", "1024", "2048", "3072", "4096", "5120", "6144", "7168", "8192"];
|
||||
var select = content.createSelect(text, values, data, settingsKey);
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(select);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
}
|
||||
return setting;
|
||||
};
|
||||
SettingsCategory.prototype.createDescription = function (settingsKey) {
|
||||
var description = document.createElement("TR");
|
||||
var text;
|
||||
switch (settingsKey) {
|
||||
case "authentication.web":
|
||||
text = "{{.settings.authenticationWEB.description}}";
|
||||
break;
|
||||
case "authentication.m3u":
|
||||
text = "{{.settings.authenticationM3U.description}}";
|
||||
break;
|
||||
case "authentication.pms":
|
||||
text = "{{.settings.authenticationPMS.description}}";
|
||||
break;
|
||||
case "authentication.xml":
|
||||
text = "{{.settings.authenticationXML.description}}";
|
||||
break;
|
||||
case "authentication.api":
|
||||
if (SERVER["settings"]["authentication.web"] == true) {
|
||||
text = "{{.settings.authenticationAPI.description}}";
|
||||
}
|
||||
break;
|
||||
case "xteveAutoUpdate":
|
||||
text = "{{.settings.xteveAutoUpdate.description}}";
|
||||
break;
|
||||
case "backup.keep":
|
||||
text = "{{.settings.backupKeep.description}}";
|
||||
break;
|
||||
case "backup.path":
|
||||
text = "{{.settings.backupPath.description}}";
|
||||
break;
|
||||
case "temp.path":
|
||||
text = "{{.settings.tempPath.description}}";
|
||||
break;
|
||||
case "buffer":
|
||||
text = "{{.settings.streamBuffering.description}}";
|
||||
break;
|
||||
case "buffer.size.kb":
|
||||
text = "{{.settings.bufferSize.description}}";
|
||||
break;
|
||||
case "buffer.timeout":
|
||||
text = "{{.settings.bufferTimeout.description}}";
|
||||
break;
|
||||
case "user.agent":
|
||||
text = "{{.settings.userAgent.description}}";
|
||||
break;
|
||||
case "epgSource":
|
||||
text = "{{.settings.epgSource.description}}";
|
||||
break;
|
||||
case "tuner":
|
||||
text = "{{.settings.tuner.description}}";
|
||||
break;
|
||||
case "update":
|
||||
text = "{{.settings.update.description}}";
|
||||
break;
|
||||
case "api":
|
||||
text = "{{.settings.api.description}}";
|
||||
break;
|
||||
case "files.update":
|
||||
text = "{{.settings.filesUpdate.description}}";
|
||||
break;
|
||||
case "cache.images":
|
||||
text = "{{.settings.cacheImages.description}}";
|
||||
break;
|
||||
case "xepg.replace.missing.images":
|
||||
text = "{{.settings.replaceEmptyImages.description}}";
|
||||
break;
|
||||
default:
|
||||
text = "";
|
||||
break;
|
||||
}
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "";
|
||||
var tdRight = document.createElement("TD");
|
||||
var pre = document.createElement("PRE");
|
||||
pre.innerHTML = text;
|
||||
tdRight.appendChild(pre);
|
||||
description.appendChild(tdLeft);
|
||||
description.appendChild(tdRight);
|
||||
return description;
|
||||
};
|
||||
return SettingsCategory;
|
||||
}());
|
||||
var SettingsCategoryItem = /** @class */ (function (_super) {
|
||||
__extends(SettingsCategoryItem, _super);
|
||||
function SettingsCategoryItem(headline, settingsKeys) {
|
||||
var _this = _super.call(this) || this;
|
||||
_this.headline = headline;
|
||||
_this.settingsKeys = settingsKeys;
|
||||
return _this;
|
||||
}
|
||||
SettingsCategoryItem.prototype.createCategory = function () {
|
||||
var _this = this;
|
||||
var headline = this.createCategoryHeadline(this.headline);
|
||||
var settingsKeys = this.settingsKeys;
|
||||
var doc = document.getElementById(this.DocumentID);
|
||||
doc.appendChild(headline);
|
||||
// Tabelle für die Kategorie erstellen
|
||||
var table = document.createElement("TABLE");
|
||||
var keys = settingsKeys.split(",");
|
||||
keys.forEach(function (settingsKey) {
|
||||
switch (settingsKey) {
|
||||
case "authentication.pms":
|
||||
case "authentication.m3u":
|
||||
case "authentication.xml":
|
||||
case "authentication.api":
|
||||
if (SERVER["settings"]["authentication.web"] == false) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
var item = _this.createSettings(settingsKey);
|
||||
var description = _this.createDescription(settingsKey);
|
||||
table.appendChild(item);
|
||||
table.appendChild(description);
|
||||
break;
|
||||
}
|
||||
});
|
||||
doc.appendChild(table);
|
||||
doc.appendChild(this.createHR());
|
||||
};
|
||||
return SettingsCategoryItem;
|
||||
}(SettingsCategory));
|
||||
function showSettings() {
|
||||
console.log("SETTINGS");
|
||||
for (var i = 0; i < settingsCategory.length; i++) {
|
||||
settingsCategory[i].createCategory();
|
||||
}
|
||||
}
|
||||
function saveSettings() {
|
||||
console.log("Save Settings");
|
||||
var cmd = "saveSettings";
|
||||
var div = document.getElementById("content_settings");
|
||||
var settings = div.getElementsByClassName("changed");
|
||||
var newSettings = new Object();
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
var name;
|
||||
var value;
|
||||
switch (settings[i].tagName) {
|
||||
case "INPUT":
|
||||
switch (settings[i].type) {
|
||||
case "checkbox":
|
||||
name = settings[i].name;
|
||||
value = settings[i].checked;
|
||||
newSettings[name] = value;
|
||||
break;
|
||||
case "text":
|
||||
name = settings[i].name;
|
||||
value = settings[i].value;
|
||||
switch (name) {
|
||||
case "update":
|
||||
value = value.split(",");
|
||||
value = value.filter(function (e) { return e; });
|
||||
break;
|
||||
case "buffer.timeout":
|
||||
value = parseFloat(value);
|
||||
}
|
||||
newSettings[name] = value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "SELECT":
|
||||
name = settings[i].name;
|
||||
value = settings[i].value;
|
||||
// Wenn der Wert eine Zahl ist, wird dieser als Zahl gespeichert
|
||||
if (isNaN(value)) {
|
||||
newSettings[name] = value;
|
||||
}
|
||||
else {
|
||||
newSettings[name] = parseInt(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
var data = new Object();
|
||||
data["settings"] = newSettings;
|
||||
var server = new Server(cmd);
|
||||
server.request(data);
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
function openUsers(elm) {
|
||||
colomnSort = 0;
|
||||
|
||||
var newDiv = document.getElementById("settings");
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "HR";
|
||||
newDiv.appendChild(createElement(newEntry));
|
||||
|
||||
var newEntry = new Object();
|
||||
newEntry["_element"] = "INPUT";
|
||||
newEntry["type"] = "button";
|
||||
newEntry["class"] = "button";
|
||||
newEntry["value"] = "New";
|
||||
newEntry["onclick"] = "userDetail(0)";
|
||||
newDiv.appendChild(createElement(newEntry));
|
||||
|
||||
var div = document.getElementById("settings");
|
||||
|
||||
// Build table
|
||||
var newTable = new Object();
|
||||
newTable["_element"] = "TABLE";
|
||||
newTable["id"] = "id_mapping";
|
||||
newTable["class"] = "table-mapping";
|
||||
div.appendChild(createElement(newTable));
|
||||
|
||||
setTimeout(function(){
|
||||
createUsersTable();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function createUsersTable() {
|
||||
var table = document.getElementById("id_mapping");
|
||||
table.innerHTML = "";
|
||||
var newTR = new Object();
|
||||
newTR["_element"] = "TR";
|
||||
newTR["class"] = "table-mapping-header";
|
||||
table.appendChild(createElement(newTR));
|
||||
|
||||
var tr = table.lastChild;
|
||||
var trHeadlines = new Array("Username", "Password", "WEB", "PMS", "M3U", "XML", "API")
|
||||
|
||||
for (var i = 0; i < trHeadlines.length; i++) {
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "TD";
|
||||
newTD["_text"] = trHeadlines[i];
|
||||
tr.appendChild(createElement(newTD));
|
||||
}
|
||||
|
||||
|
||||
// Sort users
|
||||
var userIds = getObjKeys(users);
|
||||
|
||||
var userObj = new Object();
|
||||
|
||||
for (var i = 0; i < userIds.length; i++) {
|
||||
var username = users[userIds[i]]["data"]["username"];
|
||||
userObj[username] = userIds[i];
|
||||
}
|
||||
|
||||
var allUsers = getObjKeys(userObj);
|
||||
allUsers.sort();
|
||||
// --
|
||||
|
||||
for (var i = 0; i < allUsers.length; i++) {
|
||||
var table = document.getElementById("id_mapping");
|
||||
var userID = userObj[allUsers[i]];
|
||||
var username = allUsers[i];
|
||||
var item = users[userID]["data"];
|
||||
|
||||
// Create TR
|
||||
var newTR = new Object();
|
||||
newTR["_element"] = "TR";
|
||||
newTR["class"] = "";
|
||||
newTR["id"] = userID;
|
||||
newTR["onclick"] = 'javascript: userDetail("' + userID + '");';
|
||||
table.appendChild(createElement(newTR));
|
||||
|
||||
var tr = table.lastChild;
|
||||
|
||||
// Create username TD
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = username;
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create password TD
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
newTD["_text"] = ".....";
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create web access
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
switch(item["authentication.web"]){
|
||||
case true: newTD["_text"] = "✓"; break;
|
||||
default: newTD["_text"] = "-"; break;
|
||||
}
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create PMS access
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
switch(item["authentication.pms"]){
|
||||
case true: newTD["_text"] = "✓"; break;
|
||||
default: newTD["_text"] = "-"; break;
|
||||
}
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create M3U access
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
switch(item["authentication.m3u"]){
|
||||
case true: newTD["_text"] = "✓"; break;
|
||||
default: newTD["_text"] = "-"; break;
|
||||
}
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create XMLTV access
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
switch(item["authentication.xml"]){
|
||||
case true: newTD["_text"] = "✓"; break;
|
||||
default: newTD["_text"] = "-"; break;
|
||||
}
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
// Create API access
|
||||
var newTD = new Object();
|
||||
newTD["_element"] = "P";
|
||||
|
||||
switch(item["authentication.api"]){
|
||||
case true: newTD["_text"] = "✓"; break;
|
||||
default: newTD["_text"] = "-"; break;
|
||||
}
|
||||
createNewTD(newTD, tr);
|
||||
|
||||
}
|
||||
|
||||
// usage Info
|
||||
var div = document.getElementById("settings");
|
||||
switch(menu[activeMenu.id].hasOwnProperty("_usage")) {
|
||||
case true:
|
||||
var usageItem = new Object();
|
||||
usageItem["_element"] = "PRE"
|
||||
usageItem["_text"] = menu[activeMenu.id]["_usage"];
|
||||
|
||||
var newHR = new Object();
|
||||
newHR["_element"] = "HR"
|
||||
div.appendChild(createElement(newHR));
|
||||
div.appendChild(createElement(usageItem));
|
||||
}
|
||||
|
||||
sortTable(0);
|
||||
}
|
||||
|
||||
function userDetail(userID) {
|
||||
showPopUpElement('user-detail');
|
||||
setTimeout(function(){
|
||||
showElement("popup", true);
|
||||
}, 10);
|
||||
var defaultUser;
|
||||
|
||||
document.getElementById("saveUserDetail").setAttribute("onclick", 'javascript: saveUserDetail("' + userID + '", false)');
|
||||
document.getElementById("deleteUserDetail").setAttribute("onclick", 'javascript: saveUserDetail("' + userID + '", true)');
|
||||
|
||||
var data = new Object();
|
||||
|
||||
switch(userID) {
|
||||
case 0: // New User
|
||||
data["username"] = "";
|
||||
data["authentication.web"] = false;
|
||||
data["authentication.pms"] = true;
|
||||
data["authentication.xml"] = true;
|
||||
data["authentication.m3u"] = false;
|
||||
data["authentication.api"] = false;
|
||||
data["defaultUser"] = false;
|
||||
setTimeout(function(){
|
||||
showElement("deleteUserDetail", false)
|
||||
}, 1);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
data = users[userID]["data"];
|
||||
showElement("deleteUserDetail", true)
|
||||
document.getElementById("deleteUserDetail").className = "delete";
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
var username = data["username"];
|
||||
data["password"] = "";
|
||||
data["confirm"] = "";
|
||||
|
||||
var keys = getObjKeys(data);
|
||||
defaultUser = data["defaultUser"];
|
||||
if (data.hasOwnProperty("defaultUser")) {
|
||||
defaultUser = JSON.parse(data["defaultUser"]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
|
||||
if(document.getElementById(keys[i])){
|
||||
var td = document.getElementById(keys[i])
|
||||
} else {
|
||||
var td = undefined;
|
||||
}
|
||||
|
||||
var newItem = new Object();
|
||||
|
||||
newItem["_element"] = "INPUT";
|
||||
|
||||
newItem["value"] = data[keys[i]];
|
||||
newItem["name"] = keys[i];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
switch(keys[i].indexOf("authentication")) {
|
||||
case -1:
|
||||
if (keys[i] == "password" || keys[i] == "confirm") {
|
||||
newItem["type"] = "password";
|
||||
} else {
|
||||
newItem["type"] = "text";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
newItem["type"] = "checkbox";
|
||||
|
||||
if (keys[i] == "authentication.web" && defaultUser == true) {
|
||||
newItem["onclick"] = "return false";
|
||||
}
|
||||
|
||||
if (data[keys[i]] == true) {
|
||||
newItem["checked"] = data[keys[i]];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
switch(keys[i]) {
|
||||
case "defaultUser":
|
||||
//if (data[keys[i]] == true) {
|
||||
newItem["type"] = "hidden";
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
if (td != undefined) {
|
||||
td.innerHTML = "";
|
||||
var element = createNewElement(newItem)
|
||||
//console.log(element);
|
||||
td.appendChild(element);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (defaultUser == true) {
|
||||
showElement("deleteUserDetail", false)
|
||||
} else {
|
||||
showElement("deleteUserDetail", true)
|
||||
document.getElementById("deleteUserDetail").className = "delete";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function saveUserDetail(userID, deleteUser) {
|
||||
|
||||
var inputs = document.getElementById("user-detail-table").getElementsByTagName("INPUT");
|
||||
|
||||
var newUserData = new Object();
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
switch(inputs[i].type) {
|
||||
case "checkbox": newUserData[inputs[i].name] = inputs[i].checked; break;
|
||||
default: newUserData[inputs[i].name] = inputs[i].value; break;
|
||||
}
|
||||
|
||||
if (inputs["username"].value.length == 0) {
|
||||
inputs["username"].style.border = "solid 1px red";
|
||||
return;
|
||||
}
|
||||
|
||||
switch(userID) {
|
||||
case "0":
|
||||
if (inputs["password"].value.length == 0) {
|
||||
console.log(inputs["password"].value.length);
|
||||
inputs["password"].style.border = "solid 1px red";
|
||||
return
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (inputs["password"].value.length > 0) {
|
||||
if (inputs["password"].value != inputs["confirm"].value) {
|
||||
inputs["password"].style.border = "solid 1px red";
|
||||
inputs["confirm"].style.border = "solid 1px red";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var data = new Object();
|
||||
|
||||
switch(userID) {
|
||||
case "0":
|
||||
//data = newUserData
|
||||
data["userData"] = newUserData
|
||||
data["cmd"] = "saveNewUser"; break;
|
||||
|
||||
default:
|
||||
var thisUser = new Object();
|
||||
|
||||
if (deleteUser == true) {
|
||||
if (confirm('Delete the selected user?')) {
|
||||
data["deleteUser"] = true;
|
||||
} else {
|
||||
showElement("popup", false);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
thisUser[userID] = newUserData;
|
||||
|
||||
data["userData"] = thisUser;
|
||||
data["cmd"] = "saveUserData"; break;
|
||||
}
|
||||
|
||||
xTeVe(data);
|
||||
//createUsersTable()
|
||||
showElement("popup", false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
{
|
||||
"mainMenu": {
|
||||
"item":{
|
||||
"playlist": "Playlist",
|
||||
"pmsID": "PMS ID",
|
||||
"filter": "Filter",
|
||||
"xmltv": "XMLTV",
|
||||
"mapping": "Mapping",
|
||||
"users": "Users",
|
||||
"settings": "Settings",
|
||||
"log": "Log",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"headline": {
|
||||
"playlist": "Local or remote playlists",
|
||||
"filter": "Filter playlist",
|
||||
"xmltv": "Local or remote XMLTV files",
|
||||
"mapping": "Map playlist channels to EPG channels",
|
||||
"users": "User management",
|
||||
"settings": "Settings",
|
||||
"log": "Log",
|
||||
"logout": "Logout"
|
||||
}
|
||||
},
|
||||
"confirm":{
|
||||
"restore": "All data will be replaced with those from the backup. Should the files be restored?"
|
||||
},
|
||||
"alert": {
|
||||
"fileLoadingError": "File couldn't be loaded",
|
||||
"invalidChannelNumber": "Invalid channel number",
|
||||
"missingInput": "Missing input"
|
||||
},
|
||||
"button":{
|
||||
"back": "Back",
|
||||
"backup": "Backup",
|
||||
"bulkEdit": "Bulk Edit",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"done": "Done",
|
||||
"login": "Login",
|
||||
"new": "New",
|
||||
"next": "Next",
|
||||
"restore": "Restore",
|
||||
"save": "Save",
|
||||
"search": "Search",
|
||||
"update": "Update",
|
||||
"craeteAccount": "Create Account",
|
||||
"resetLogs": "Reset Logs",
|
||||
"uploadLogo": "Upload Logo"
|
||||
},
|
||||
"filter": {
|
||||
"table": {
|
||||
"name": "Filter Name",
|
||||
"type": "Filter Type",
|
||||
"filter": "Filter"
|
||||
},
|
||||
"custom": "Custom",
|
||||
"group": "Group",
|
||||
"name": {
|
||||
"title": "Filter Name",
|
||||
"placeholder": "Filter name",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"placeholder": "Description",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"title": "Type",
|
||||
"groupTitle": "Group Title",
|
||||
"customFilter": "Custom Filter"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"title": "Case Sensitive",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"filterRule": {
|
||||
"title": "Filter Rule",
|
||||
"placeholder": "Sport {HD} !{ES,IT}",
|
||||
"description": ""
|
||||
},
|
||||
"filterGroup": {
|
||||
"title": "Group Title",
|
||||
"placeholder": "",
|
||||
"description": "Select a M3U group. (Counter)<br>Changing the group title in the M3U invalidates the filter."
|
||||
},
|
||||
"include": {
|
||||
"title": "Include",
|
||||
"placeholder": "FHD,UHD",
|
||||
"description": "Channel name must include.<br>(Comma separated) Comma means or"
|
||||
},
|
||||
"exclude": {
|
||||
"title": "Exclude",
|
||||
"placeholder": "ES,IT",
|
||||
"description": "Channel name must not contain.<br>(Comma separated) Comma means or"
|
||||
}
|
||||
|
||||
},
|
||||
"playlist": {
|
||||
"table": {
|
||||
"playlist": "Playlist",
|
||||
"tuner": "Tuner",
|
||||
"lastUpdate": "Last Update",
|
||||
"availability": "Availability",
|
||||
"type": "Type",
|
||||
"streams": "Streams",
|
||||
"groupTitle": "group-title",
|
||||
"tvgID": "tvg-id",
|
||||
"uniqueID": "Unique ID"
|
||||
},
|
||||
"playlistType": {
|
||||
"title": "Playlist type",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"title": "Type",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"placeholder": "Playlist name",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"placeholder": "Description",
|
||||
"description": ""
|
||||
},
|
||||
"fileM3U": {
|
||||
"title": "M3U File",
|
||||
"placeholder": "File path or URL of the M3U",
|
||||
"description": ""
|
||||
},
|
||||
"fileHDHR": {
|
||||
"title": "HDHomeRun IP",
|
||||
"placeholder": "IP address and port (192.168.1.10:5004)",
|
||||
"description": ""
|
||||
},
|
||||
"tuner": {
|
||||
"title": "Tuner / Streams",
|
||||
"placeholder": "",
|
||||
"description": "Number of parallel connections that can be established to the provider. <br>Only available with activated buffer.<br>New settings will only be applied after quitting all streams."
|
||||
}
|
||||
},
|
||||
"xmltv": {
|
||||
"table": {
|
||||
"guide": "Guide",
|
||||
"lastUpdate": "Last Update",
|
||||
"availability": "Availability",
|
||||
"channels": "Channels",
|
||||
"programs": "Programs"
|
||||
},
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"placeholder": "Guide name",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"placeholder": "Description",
|
||||
"description": ""
|
||||
},
|
||||
"fileXMLTV": {
|
||||
"title": "XMLTV File",
|
||||
"placeholder": "File path or URL of the XMLTV",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"mapping": {
|
||||
"table": {
|
||||
"chNo": "Ch. No.",
|
||||
"logo": "Logo",
|
||||
"channelName": "Channel Name",
|
||||
"playlist": "Playlist",
|
||||
"groupTitle": "Group Title",
|
||||
"xmltvFile": "XMLTV File",
|
||||
"xmltvID": "XMLTV ID"
|
||||
},
|
||||
"active": {
|
||||
"title": "Active",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"channelName": {
|
||||
"title": "Channel Name",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"updateChannelName": {
|
||||
"title": "Update Channel Name",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"channelLogo": {
|
||||
"title": "Logo URL",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"updateChannelLogo": {
|
||||
"title": "Update Channel Logo",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"epgCategory": {
|
||||
"title": "EPG Category",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"m3uGroupTitle": {
|
||||
"title": "Group Title (xteve.m3u)",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"xmltvFile": {
|
||||
"title": "XMLTV File",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"xmltvChannel": {
|
||||
"title": "XMLTV Channel",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"table": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"web": "WEB",
|
||||
"pms": "PMS",
|
||||
"m3u": "M3U",
|
||||
"xml": "XML",
|
||||
"api": "API"
|
||||
},
|
||||
"username": {
|
||||
"title": "Username",
|
||||
"placeholder": "Username",
|
||||
"description": ""
|
||||
},
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"placeholder": "Password",
|
||||
"description": ""
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Confirm",
|
||||
"placeholder": "Password confirm",
|
||||
"description": ""
|
||||
},
|
||||
"web": {
|
||||
"title": "Web Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"pms": {
|
||||
"title": "PMS Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"m3u": {
|
||||
"title": "M3U Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"xml": {
|
||||
"title": "XML Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"title": "API Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"category": {
|
||||
"general": "General",
|
||||
"files": "Files",
|
||||
"streaming": "Streaming",
|
||||
"backup": "Backup",
|
||||
"authentication": "Authentication"
|
||||
},
|
||||
"update": {
|
||||
"title": "Schedule for updating (Playlist, XMLTV, Backup)",
|
||||
"placeholder": "0000,1000,2000",
|
||||
"description": "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated."
|
||||
},
|
||||
"api": {
|
||||
"title": "API Interface",
|
||||
"description": "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"
|
||||
},
|
||||
"epgSource": {
|
||||
"title": "EPG Source",
|
||||
"description": "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
|
||||
},
|
||||
"tuner":{
|
||||
"title": "Number of Tuners",
|
||||
"description": "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
|
||||
},
|
||||
"filesUpdate": {
|
||||
"title": "Updates all files at startup",
|
||||
"description": "Updates all playlists, tuner and XMLTV files at startup."
|
||||
},
|
||||
"cacheImages": {
|
||||
"title": "Image caching",
|
||||
"description": "All images from the XMLTV file are cached, allowing faster rendering of the grid in the client.<br>Downloading the images may take a while and will be done in the background."
|
||||
},
|
||||
"replaceEmptyImages": {
|
||||
"title": "Replace missing program images",
|
||||
"description": "If the poster in the XMLTV program is missing, the channel logo will be used."
|
||||
},
|
||||
"xteveAutoUpdate": {
|
||||
"title": "Automatic update of xTeVe",
|
||||
"description": "If a new version of xTeVe is available, it will be automatically installed. The updates are downloaded from GitHub."
|
||||
},
|
||||
"streamBuffering": {
|
||||
"title": "Stream Buffer",
|
||||
"description": "- The stream is passed from xTeVe to Plex / Emby / M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support<br>- Re-streaming<br>- Separate tuner limit for each playlist"
|
||||
},
|
||||
"bufferSize": {
|
||||
"title": "Buffer Size",
|
||||
"description": "Buffer size in MB.<br>M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used."
|
||||
},
|
||||
"bufferTimeout": {
|
||||
"title": "Timeout for new client connections",
|
||||
"description": "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds.",
|
||||
"placeholder": "100"
|
||||
},
|
||||
"userAgent": {
|
||||
"title": "User agent",
|
||||
"description": "User Agent for HTTP requests",
|
||||
"placeholder": "xTeVe"
|
||||
},
|
||||
"backupPath": {
|
||||
"title": "Location for automatic backups",
|
||||
"placeholder": "/mnt/data/backup/xteve/",
|
||||
"description": "Before any update of the provider data by the schedule, xTeVe creates a backup. The path for the automatic backups can be changed. xTeVe requires write permission for this folder."
|
||||
},
|
||||
"tempPath": {
|
||||
"title": "Location for the temporary files",
|
||||
"placeholder": "/tmp/xteve/",
|
||||
"description": "Location for the buffer files."
|
||||
},
|
||||
"backupKeep": {
|
||||
"title": "Number of backups to keep",
|
||||
"description": "Number of backups to keep. Older backups are automatically deleted."
|
||||
},
|
||||
"authenticationWEB": {
|
||||
"title": "WEB Authentication",
|
||||
"description": "Access to the web interface only possible with credentials."
|
||||
},
|
||||
"authenticationPMS": {
|
||||
"title": "PMS Authentication",
|
||||
"description": "Plex requests are only possible with authentication. <br><b>Warning!!!</b> After activating this function xTeVe must be delete in the PMS DVR settings and set up again."
|
||||
},
|
||||
"authenticationM3U": {
|
||||
"title": "M3U Authentication",
|
||||
"description": "Downloading the xteve.m3u file via an HTTP request is only possible with authentication."
|
||||
},
|
||||
"authenticationXML": {
|
||||
"title": "XML Authentication",
|
||||
"description": "Downloading the xteve.xml file via an HTTP request is only possible with authentication"
|
||||
},
|
||||
"authenticationAPI": {
|
||||
"title": "API Authentication",
|
||||
"description": "Access to the API interface is only possible with authentication."
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
"epgSource": {
|
||||
"title": "EPG Source",
|
||||
"description": "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
|
||||
},
|
||||
"tuner":{
|
||||
"title": "Number of tuners",
|
||||
"description": "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
|
||||
},
|
||||
"m3u": {
|
||||
"title": "M3U Playlist",
|
||||
"placeholder": "File path or URL of the M3U",
|
||||
"description": "Local or remote playlists"
|
||||
},
|
||||
"xmltv": {
|
||||
"title": "XMLTV File",
|
||||
"placeholder": "File path or URL of the XMLTV",
|
||||
"description": "Local or remote XMLTV file"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"failed": "User authentication failed",
|
||||
"headline": "Login",
|
||||
"username": {
|
||||
"title": "Username",
|
||||
"placeholder": "Username"
|
||||
},
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"placeholder": "Password"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"failed": "Password does not match",
|
||||
"headline": "Create user account",
|
||||
"username": {
|
||||
"title": "Username",
|
||||
"placeholder": "Username"
|
||||
},
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"placeholder": "Password"
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Confirm",
|
||||
"placeholder": "Confirm"
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -1,45 +1,46 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>xTeVe</title>
|
||||
<link rel="stylesheet" href="css/screen.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
|
||||
<link rel="stylesheet" href="css/tokens.css">
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<script src="js/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="auth-screen">
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
|
||||
<div id="box">
|
||||
<main id="box" aria-labelledby="head-text">
|
||||
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">{{.login.headline}}</h1>
|
||||
<h1 id="head-text" class="center">Login</h1>
|
||||
</div>
|
||||
|
||||
<p id="err" class="errorMsg center">{{.authenticationErr}}</p>
|
||||
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true">{{.authenticationErr}}</p>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<form id="authentication" action="/web/" method="post">
|
||||
<form id="authentication" action="" method="post" aria-describedby="err" novalidate>
|
||||
|
||||
<h5>{{.login.username.title}}:</h5>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="">
|
||||
<h5>{{.login.password.title}}:</h5>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="">
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="current-password">
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="box-footer">
|
||||
<input id="submit" class="" type="button" value="{{.button.login}}" onclick="javascript: login();">
|
||||
<input id="submit" class="" type="button" value="Login" aria-label="Login" onclick="javascript: login();">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>xTeVe</title>
|
||||
<link rel="stylesheet" href="css/screen.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/tokens.css">
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="auth-screen">
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
|
||||
<div id="box">
|
||||
<main id="box" aria-labelledby="head-text">
|
||||
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">Maintenance</h1>
|
||||
@@ -24,7 +26,7 @@
|
||||
|
||||
<div id="box-footer"></div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Generated
+559
@@ -0,0 +1,559 @@
|
||||
{
|
||||
"name": "xteve-webui",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "xteve-webui",
|
||||
"devDependencies": {
|
||||
"jsdom": "30.1.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.0.1.tgz",
|
||||
"integrity": "sha512-C9duntabagkBZ1LebM7FKmphR4Q1pBclLxVbZETQV0akkFjV0ooFxo8FlvAQyKj9F6l8rEnmidgcTlpyxFYizg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.4.0",
|
||||
"@csstools/css-color-parser": "^4.2.3",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.1",
|
||||
"lru-cache": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.1.tgz",
|
||||
"integrity": "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.1.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz",
|
||||
"integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz",
|
||||
"integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.1",
|
||||
"@csstools/css-tokenizer": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.1.2",
|
||||
"@csstools/css-calc": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.1",
|
||||
"@csstools/css-tokenizer": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz",
|
||||
"integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz",
|
||||
"integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peerDependencies": {
|
||||
"css-tree": "^3.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-tree": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz",
|
||||
"integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz",
|
||||
"integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
|
||||
"integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.27.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
|
||||
"integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz",
|
||||
"integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.1.tgz",
|
||||
"integrity": "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^7.0.0",
|
||||
"@asamuzakjp/dom-selector": "^9.2.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.13",
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^7.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.10.2",
|
||||
"w3c-xmlserializer": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^17.1.1",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.2.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
||||
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.15",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.15.tgz",
|
||||
"integrity": "sha512-SJVBeHOxDbNoq14CvpAoA2mLEWdbldGK8nR+yumpjY5nrlZxOflcA7p1Qj1gbTGmbu9DY9qLj/bENSWhBzjxRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.15"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.15",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.15.tgz",
|
||||
"integrity": "sha512-ERuv0p98XgSzmlSLJr8vDNxX+uATGInlN97V3R+JpYWaSqn5IG3ewWgCwczk4bkOpgth/o8Nt5FOi4B4w0n/1A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz",
|
||||
"integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz",
|
||||
"integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "17.1.2",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz",
|
||||
"integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.14.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "xteve-webui",
|
||||
"private": true,
|
||||
"description": "Build tooling for the xTeVe web interface (TypeScript -> html/js/app.js)",
|
||||
"scripts": {
|
||||
"build": "tsc -p ts/tsconfig.json",
|
||||
"check": "tsc -p ts/tsconfig.json --noEmit",
|
||||
"test": "node --test \"tests/**/*.test.js\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "30.1.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
webui "xteve/html"
|
||||
)
|
||||
|
||||
// webFS serves the web interface. It is the embedded copy by default and the
|
||||
// local html/ directory when xTeVe runs with -dev.
|
||||
var webFS fs.FS = webui.FS
|
||||
|
||||
var webETags sync.Map // path -> ETag, only used when not in dev mode
|
||||
|
||||
// initWebFS : selects embedded or on-disk web files.
|
||||
func initWebFS(dev bool) {
|
||||
if dev {
|
||||
webFS = os.DirFS("html")
|
||||
return
|
||||
}
|
||||
webFS = webui.FS
|
||||
}
|
||||
|
||||
// readWebFile : reads a file from the web interface, name relative to html/.
|
||||
func readWebFile(name string) ([]byte, error) {
|
||||
name = strings.TrimPrefix(path.Clean("/"+name), "/")
|
||||
if name == "" || name == "." {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
return fs.ReadFile(webFS, name)
|
||||
}
|
||||
|
||||
// webFileETag : content hash used for HTTP caching of static web assets.
|
||||
func webFileETag(name string, content []byte) string {
|
||||
if !System.Dev {
|
||||
if v, ok := webETags.Load(name); ok {
|
||||
return v.(string)
|
||||
}
|
||||
}
|
||||
sum := sha256.Sum256(content)
|
||||
etag := `"` + hex.EncodeToString(sum[:16]) + `"`
|
||||
if !System.Dev {
|
||||
webETags.Store(name, etag)
|
||||
}
|
||||
return etag
|
||||
}
|
||||
+67
-63
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"../src/internal/authentication"
|
||||
"xteve/src/internal/authentication"
|
||||
)
|
||||
|
||||
func activatedSystemAuthentication() (err error) {
|
||||
@@ -16,7 +16,7 @@ func activatedSystemAuthentication() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var defaults = make(map[string]interface{})
|
||||
var defaults = make(map[string]any)
|
||||
defaults["authentication.web"] = false
|
||||
defaults["authentication.pms"] = false
|
||||
defaults["authentication.xml"] = false
|
||||
@@ -28,22 +28,15 @@ func activatedSystemAuthentication() (err error) {
|
||||
|
||||
func createFirstUserForAuthentication(username, password string) (token string, err error) {
|
||||
|
||||
var authenticationErr = func(err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = authentication.CreateDefaultUser(username, password); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = authentication.CreateDefaultUser(username, password)
|
||||
authenticationErr(err)
|
||||
if token, err = authentication.UserAuthentication(username, password); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
token, err = authentication.UserAuthentication(username, password)
|
||||
authenticationErr(err)
|
||||
|
||||
token, err = authentication.CheckTheValidityOfTheToken(token)
|
||||
authenticationErr(err)
|
||||
|
||||
var userData = make(map[string]interface{})
|
||||
var userData = make(map[string]any)
|
||||
userData["username"] = username
|
||||
userData["authentication.web"] = true
|
||||
userData["authentication.pms"] = true
|
||||
@@ -53,17 +46,18 @@ func createFirstUserForAuthentication(username, password string) (token string,
|
||||
userData["defaultUser"] = true
|
||||
|
||||
userID, err := authentication.GetUserID(token)
|
||||
authenticationErr(err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = authentication.WriteUserData(userID, userData)
|
||||
authenticationErr(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func tokenAuthentication(token string) (newToken string, err error) {
|
||||
|
||||
if System.ConfigurationWizard == true {
|
||||
if System.ConfigurationWizard {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,6 +66,8 @@ func tokenAuthentication(token string) (newToken string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// basicAuth : HTTP Basic authentication for the HDHomeRun endpoints. Verifies
|
||||
// the credentials on every request without creating a session token.
|
||||
func basicAuth(r *http.Request, level string) (username string, err error) {
|
||||
|
||||
err = errors.New("User authentication failed")
|
||||
@@ -82,88 +78,96 @@ func basicAuth(r *http.Request, level string) (username string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
payload, _ := base64.StdEncoding.DecodeString(auth[1])
|
||||
payload, decodeErr := base64.StdEncoding.DecodeString(auth[1])
|
||||
if decodeErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pair := strings.SplitN(string(payload), ":", 2)
|
||||
if len(pair) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
username = pair[0]
|
||||
var password = pair[1]
|
||||
|
||||
token, err := authentication.UserAuthentication(username, password)
|
||||
|
||||
userID, err := authentication.AuthenticateUser(username, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = checkAuthorizationLevel(token, level)
|
||||
err = checkAuthorizationLevelForUser(userID, level)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// urlAuth : username/password query parameters on the M3U and XMLTV URLs.
|
||||
// Verified per request, no session token is created.
|
||||
func urlAuth(r *http.Request, requestType string) (err error) {
|
||||
var level, token string
|
||||
|
||||
var username = r.URL.Query().Get("username")
|
||||
var password = r.URL.Query().Get("password")
|
||||
|
||||
var level string
|
||||
|
||||
switch requestType {
|
||||
|
||||
case "m3u":
|
||||
level = "authentication.m3u"
|
||||
if Settings.AuthenticationM3U == true {
|
||||
token, err = authentication.UserAuthentication(username, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkAuthorizationLevel(token, level)
|
||||
if !Settings.AuthenticationM3U {
|
||||
return
|
||||
}
|
||||
level = "authentication.m3u"
|
||||
|
||||
case "xml":
|
||||
level = "authentication.xml"
|
||||
if Settings.AuthenticationXML == true {
|
||||
token, err = authentication.UserAuthentication(username, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkAuthorizationLevel(token, level)
|
||||
if !Settings.AuthenticationXML {
|
||||
return
|
||||
}
|
||||
level = "authentication.xml"
|
||||
|
||||
default:
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
userID, err := authentication.AuthenticateUser(username, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = checkAuthorizationLevelForUser(userID, level)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func checkAuthorizationLevel(token, level string) (err error) {
|
||||
|
||||
var authenticationErr = func(err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userID, err := authentication.GetUserID(token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := authentication.GetUserID(token)
|
||||
authenticationErr(err)
|
||||
return checkAuthorizationLevelForUser(userID, level)
|
||||
}
|
||||
|
||||
func checkAuthorizationLevelForUser(userID, level string) (err error) {
|
||||
|
||||
userData, err := authentication.ReadUserData(userID)
|
||||
authenticationErr(err)
|
||||
|
||||
if len(userData) > 0 {
|
||||
|
||||
if v, ok := userData[level].(bool); ok {
|
||||
|
||||
if v == false {
|
||||
err = errors.New("No authorization")
|
||||
}
|
||||
|
||||
} else {
|
||||
userData[level] = false
|
||||
err = authentication.WriteUserData(userID, userData)
|
||||
err = errors.New("No authorization")
|
||||
}
|
||||
|
||||
} else {
|
||||
err = authentication.WriteUserData(userID, userData)
|
||||
err = errors.New("No authorization")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
if v, ok := userData[level].(bool); ok {
|
||||
if !v {
|
||||
err = errors.New("No authorization")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Level unknown for this user: record it as denied so it shows up in the UI.
|
||||
userData[level] = false
|
||||
if writeErr := authentication.WriteUserData(userID, userData); writeErr != nil {
|
||||
ShowError(writeErr, 0)
|
||||
}
|
||||
|
||||
return errors.New("No authorization")
|
||||
}
|
||||
+183
-180
@@ -1,274 +1,277 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
b64 "encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func xTeVeAutoBackup() (err error) {
|
||||
|
||||
var archiv = "xteve_auto_backup_" + time.Now().Format("20060102_1504") + ".zip"
|
||||
var target string
|
||||
var sourceFiles = make([]string, 0)
|
||||
var oldBackupFiles = make([]string, 0)
|
||||
var debug string
|
||||
var archiv = "xteve_auto_backup_" + time.Now().Format("20060102_1504") + ".zip"
|
||||
var target string
|
||||
var sourceFiles = make([]string, 0)
|
||||
var oldBackupFiles = make([]string, 0)
|
||||
var debug string
|
||||
|
||||
if len(Settings.BackupPath) > 0 {
|
||||
System.Folder.Backup = Settings.BackupPath
|
||||
}
|
||||
if len(Settings.BackupPath) > 0 {
|
||||
System.Folder.Backup = Settings.BackupPath
|
||||
}
|
||||
|
||||
showInfo("Backup Path:" + System.Folder.Backup)
|
||||
showInfo("Backup Path:" + System.Folder.Backup)
|
||||
|
||||
err = checkFolder(System.Folder.Backup)
|
||||
if err != nil {
|
||||
ShowError(err, 1070)
|
||||
return
|
||||
}
|
||||
err = checkFolder(System.Folder.Backup)
|
||||
if err != nil {
|
||||
ShowError(err, 1070)
|
||||
return
|
||||
}
|
||||
|
||||
// Alte Backups löschen
|
||||
files, err := ioutil.ReadDir(System.Folder.Backup)
|
||||
// Alte Backups löschen
|
||||
files, err := os.ReadDir(System.Folder.Backup)
|
||||
|
||||
if err == nil {
|
||||
if err == nil {
|
||||
|
||||
for _, file := range files {
|
||||
for _, file := range files {
|
||||
|
||||
if filepath.Ext(file.Name()) == ".zip" && strings.Contains(file.Name(), "xteve_auto_backup") {
|
||||
oldBackupFiles = append(oldBackupFiles, file.Name())
|
||||
}
|
||||
if filepath.Ext(file.Name()) == ".zip" && strings.Contains(file.Name(), "xteve_auto_backup") {
|
||||
oldBackupFiles = append(oldBackupFiles, file.Name())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Backups löschen
|
||||
var end int
|
||||
switch Settings.BackupKeep {
|
||||
case 0:
|
||||
end = 0
|
||||
default:
|
||||
end = Settings.BackupKeep - 1
|
||||
}
|
||||
// Alle Backups löschen
|
||||
var end int
|
||||
switch Settings.BackupKeep {
|
||||
case 0:
|
||||
end = 0
|
||||
default:
|
||||
end = Settings.BackupKeep - 1
|
||||
}
|
||||
|
||||
for i := 0; i < len(oldBackupFiles)-end; i++ {
|
||||
for i := 0; i < len(oldBackupFiles)-end; i++ {
|
||||
|
||||
os.RemoveAll(System.Folder.Backup + oldBackupFiles[i])
|
||||
debug = fmt.Sprintf("Delete backup file:%s", oldBackupFiles[i])
|
||||
showDebug(debug, 1)
|
||||
os.RemoveAll(System.Folder.Backup + oldBackupFiles[i])
|
||||
debug = fmt.Sprintf("Delete backup file:%s", oldBackupFiles[i])
|
||||
showDebug(debug, 1)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if Settings.BackupKeep == 0 {
|
||||
return
|
||||
}
|
||||
if Settings.BackupKeep == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
} else {
|
||||
|
||||
return
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Backup erstellen
|
||||
if err == nil {
|
||||
// Backup erstellen
|
||||
if err == nil {
|
||||
|
||||
target = System.Folder.Backup + archiv
|
||||
target = System.Folder.Backup + archiv
|
||||
|
||||
for _, i := range SystemFiles {
|
||||
sourceFiles = append(sourceFiles, System.Folder.Config+i)
|
||||
}
|
||||
for _, i := range SystemFiles {
|
||||
sourceFiles = append(sourceFiles, System.Folder.Config+i)
|
||||
}
|
||||
|
||||
sourceFiles = append(sourceFiles, System.Folder.ImagesUpload)
|
||||
sourceFiles = append(sourceFiles, System.Folder.ImagesUpload)
|
||||
|
||||
err = zipFiles(sourceFiles, target)
|
||||
err = zipFiles(sourceFiles, target)
|
||||
|
||||
if err == nil {
|
||||
if err == nil {
|
||||
|
||||
debug = fmt.Sprintf("Create backup file:%s", target)
|
||||
showDebug(debug, 1)
|
||||
debug = fmt.Sprintf("Create backup file:%s", target)
|
||||
showDebug(debug, 1)
|
||||
|
||||
showInfo("Backup file:" + target)
|
||||
showInfo("Backup file:" + target)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func xteveBackup() (archiv string, err error) {
|
||||
|
||||
err = checkFolder(System.Folder.Temp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkFolder(System.Folder.Temp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
archiv = "xteve_backup_" + time.Now().Format("20060102_1504") + ".zip"
|
||||
archiv = "xteve_backup_" + time.Now().Format("20060102_1504") + ".zip"
|
||||
|
||||
var target = System.Folder.Temp + archiv
|
||||
var sourceFiles = make([]string, 0)
|
||||
var target = System.Folder.Temp + archiv
|
||||
var sourceFiles = make([]string, 0)
|
||||
|
||||
for _, i := range SystemFiles {
|
||||
sourceFiles = append(sourceFiles, System.Folder.Config+i)
|
||||
}
|
||||
for _, i := range SystemFiles {
|
||||
sourceFiles = append(sourceFiles, System.Folder.Config+i)
|
||||
}
|
||||
|
||||
sourceFiles = append(sourceFiles, System.Folder.Data)
|
||||
sourceFiles = append(sourceFiles, System.Folder.Data)
|
||||
|
||||
err = zipFiles(sourceFiles, target)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
err = zipFiles(sourceFiles, target)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func xteveRestore(archive string) (newWebURL string, err error) {
|
||||
|
||||
var newPort, oldPort, backupVersion, tmpRestore string
|
||||
var newPort, oldPort, backupVersion, tmpRestore string
|
||||
|
||||
tmpRestore = System.Folder.Temp + "restore" + string(os.PathSeparator)
|
||||
tmpRestore = System.Folder.Temp + "restore" + string(os.PathSeparator)
|
||||
|
||||
err = checkFolder(tmpRestore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkFolder(tmpRestore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Zip Archiv in tmp entpacken
|
||||
err = extractZIP(archive, tmpRestore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Zip Archiv in tmp entpacken
|
||||
err = extractZIP(archive, tmpRestore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Neue Config laden um den Port und die Version zu überprüfen
|
||||
newConfig, err := loadJSONFileToMap(tmpRestore + "settings.json")
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
// Neue Config laden um den Port und die Version zu überprüfen
|
||||
newConfig, err := loadJSONFileToMap(tmpRestore + "settings.json")
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
backupVersion = newConfig["version"].(string)
|
||||
if backupVersion < System.Compatibility {
|
||||
err = errors.New(getErrMsg(1013))
|
||||
return
|
||||
}
|
||||
backupVersion, ok := newConfig["version"].(string)
|
||||
if !ok || backupVersion < System.Compatibility {
|
||||
err = errors.New(getErrMsg(1013))
|
||||
return
|
||||
}
|
||||
|
||||
// Zip Archiv in den Config Ordner entpacken
|
||||
err = extractZIP(archive, System.Folder.Config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Zip Archiv in den Config Ordner entpacken
|
||||
err = extractZIP(archive, System.Folder.Config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Neue Config laden um den Port und die Version zu überprüfen
|
||||
newConfig, err = loadJSONFileToMap(System.Folder.Config + "settings.json")
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
// Neue Config laden um den Port und die Version zu überprüfen
|
||||
newConfig, err = loadJSONFileToMap(System.Folder.Config + "settings.json")
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
newPort = newConfig["port"].(string)
|
||||
oldPort = Settings.Port
|
||||
newPort, ok = newConfig["port"].(string)
|
||||
if !ok {
|
||||
err = errors.New(getErrMsg(1030))
|
||||
return
|
||||
}
|
||||
oldPort = Settings.Port
|
||||
|
||||
if newPort == oldPort {
|
||||
if newPort == oldPort {
|
||||
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
loadSettings()
|
||||
loadSettings()
|
||||
|
||||
err := Init()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return "", err
|
||||
}
|
||||
err := Init()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = StartSystem(true)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return "", err
|
||||
}
|
||||
err = StartSystem(true)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var url = System.URLBase + "/web/"
|
||||
newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1)
|
||||
var url = System.URLBase + "/web/"
|
||||
newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1)
|
||||
|
||||
os.RemoveAll(tmpRestore)
|
||||
os.RemoveAll(tmpRestore)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func xteveRestoreFromWeb(input string) (newWebURL string, err error) {
|
||||
|
||||
// Base64 Json String in base64 umwandeln
|
||||
b64data := input[strings.IndexByte(input, ',')+1:]
|
||||
// Base64 Json String in base64 umwandeln
|
||||
b64data := input[strings.IndexByte(input, ',')+1:]
|
||||
|
||||
// Base64 in bytes umwandeln und speichern
|
||||
sDec, err := b64.StdEncoding.DecodeString(b64data)
|
||||
// Base64 in bytes umwandeln und speichern
|
||||
sDec, err := b64.StdEncoding.DecodeString(b64data)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var archive = System.Folder.Temp + "restore.zip"
|
||||
var archive = System.Folder.Temp + "restore.zip"
|
||||
|
||||
err = writeByteToFile(archive, sDec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = writeByteToFile(archive, sDec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newWebURL, err = xteveRestore(archive)
|
||||
newWebURL, err = xteveRestore(archive)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// XteveRestoreFromCLI : Wiederherstellung über die Kommandozeile
|
||||
func XteveRestoreFromCLI(archive string) (err error) {
|
||||
|
||||
var confirm string
|
||||
var confirm string
|
||||
|
||||
println()
|
||||
showInfo(fmt.Sprintf("Version:%s Build: %s", System.Version, System.Build))
|
||||
showInfo(fmt.Sprintf("Backup File:%s", archive))
|
||||
showInfo(fmt.Sprintf("System Folder:%s", getPlatformPath(System.Folder.Config)))
|
||||
println()
|
||||
println()
|
||||
showInfo(fmt.Sprintf("Version:%s Build: %s", System.Version, System.Build))
|
||||
showInfo(fmt.Sprintf("Backup File:%s", archive))
|
||||
showInfo(fmt.Sprintf("System Folder:%s", getPlatformPath(System.Folder.Config)))
|
||||
println()
|
||||
|
||||
fmt.Print("All data will be replaced with those from the backup. Should the files be restored? [yes|no]:")
|
||||
fmt.Print("All data will be replaced with those from the backup. Should the files be restored? [yes|no]:")
|
||||
|
||||
fmt.Scanln(&confirm)
|
||||
fmt.Scanln(&confirm)
|
||||
|
||||
switch strings.ToLower(confirm) {
|
||||
switch strings.ToLower(confirm) {
|
||||
|
||||
case "yes":
|
||||
break
|
||||
case "yes":
|
||||
break
|
||||
|
||||
case "no":
|
||||
return
|
||||
case "no":
|
||||
return
|
||||
|
||||
default:
|
||||
fmt.Println("Invalid input")
|
||||
return
|
||||
default:
|
||||
fmt.Println("Invalid input")
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if len(System.Folder.Config) > 0 {
|
||||
if len(System.Folder.Config) > 0 {
|
||||
|
||||
err = checkFilePermission(System.Folder.Config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkFilePermission(System.Folder.Config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = xteveRestore(archive)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = xteveRestore(archive)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
showHighlight(fmt.Sprintf("Restor:Backup was successfully restored. %s can now be started normally", System.Name))
|
||||
showHighlight(fmt.Sprintf("Restor:Backup was successfully restored. %s can now be started normally", System.Name))
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
+748
-798
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
package src
|
||||
|
||||
// bufferModeFor : buffer used for a playlist. A playlist can override the
|
||||
// global "buffer" setting with its own "buffer" parameter ("-", "xteve",
|
||||
// "ffmpeg" or "vlc"); anything else means "use the global setting".
|
||||
func bufferModeFor(playlistID string) string {
|
||||
|
||||
if len(playlistID) > 0 {
|
||||
|
||||
var playlistType string
|
||||
switch playlistID[0:1] {
|
||||
case "M":
|
||||
playlistType = "m3u"
|
||||
case "H":
|
||||
playlistType = "hdhr"
|
||||
}
|
||||
|
||||
switch v := getProviderParameter(playlistID, playlistType, "buffer"); v {
|
||||
case "-", "xteve", "ffmpeg", "vlc":
|
||||
return v
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Settings.Buffer
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Shared buffer state.
|
||||
//
|
||||
// One mutex guards the playlist table and every bufferStream in it. The
|
||||
// download goroutines keep their own private ThisStream (HLS sequence,
|
||||
// timing, bandwidth) and only publish status, errors and the cancel hook
|
||||
// through the helpers below. HTTP client goroutines only read snapshots.
|
||||
|
||||
var bufferMu sync.Mutex
|
||||
var bufferPlaylists = make(map[string]*Playlist)
|
||||
|
||||
// errTunerLimit : the playlist already serves as many streams as it has tuners.
|
||||
var errTunerLimit = errors.New("tuner limit reached")
|
||||
|
||||
// bufferStream : the shared view of one buffered stream.
|
||||
type bufferStream struct {
|
||||
URL string
|
||||
ChannelName string
|
||||
MD5 string
|
||||
Folder string
|
||||
PlaylistID string
|
||||
PlaylistName string
|
||||
|
||||
Status bool // first segment is on disk, clients may read
|
||||
Clients int // HTTP clients currently attached
|
||||
Err error // fatal downloader error; clients disconnect when set
|
||||
|
||||
cancel context.CancelFunc // stops an external process buffer
|
||||
}
|
||||
|
||||
// streamHTTPClient : used by the xTeVe buffer to fetch provider streams.
|
||||
// No overall timeout (bodies are endless), but dial and response-header
|
||||
// timeouts so a dead provider cannot hang a tuner forever. Redirects are
|
||||
// surfaced to the caller, which follows them itself.
|
||||
var streamHTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 20 * time.Second,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return errors.New("Redirect")
|
||||
},
|
||||
}
|
||||
|
||||
// bufferAcquireStream : registers a client for streamingURL on the playlist.
|
||||
// Returns the stream ID and whether a downloader has to be started. The
|
||||
// tuner check and the registration happen under one lock, so two clients
|
||||
// tuning at the same moment cannot both slip past the limit.
|
||||
func bufferAcquireStream(playlistID, streamingURL, channelName string) (streamID int, isNew bool, err error) {
|
||||
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
|
||||
playlist, ok := bufferPlaylists[playlistID]
|
||||
if !ok {
|
||||
|
||||
var playlistType string
|
||||
switch playlistID[0:1] {
|
||||
case "M":
|
||||
playlistType = "m3u"
|
||||
case "H":
|
||||
playlistType = "hdhr"
|
||||
}
|
||||
|
||||
playlist = &Playlist{
|
||||
Folder: System.Folder.Temp + playlistID + string(os.PathSeparator),
|
||||
PlaylistID: playlistID,
|
||||
Streams: make(map[int]*bufferStream),
|
||||
}
|
||||
|
||||
if err = checkFolder(playlist.Folder); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
playlist.Tuner = getTuner(playlistID, playlistType)
|
||||
playlist.PlaylistName = getProviderParameter(playlistID, playlistType, "name")
|
||||
|
||||
bufferPlaylists[playlistID] = playlist
|
||||
}
|
||||
|
||||
// Same URL already buffered: attach to it (restream).
|
||||
for id, s := range playlist.Streams {
|
||||
if s.URL == streamingURL {
|
||||
s.Clients++
|
||||
showDebug(fmt.Sprintf("Restream Status:Playlist: %s - Channel: %s - Connections: %d", playlist.PlaylistName, s.ChannelName, s.Clients), 1)
|
||||
showInfo(fmt.Sprintf("Streaming Status:Channel: %s (Clients: %d)", s.ChannelName, s.Clients))
|
||||
return id, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(playlist.Streams) >= playlist.Tuner {
|
||||
showInfo(fmt.Sprintf("Streaming Status:Playlist: %s - No new connections available. Tuner = %d", playlist.PlaylistName, playlist.Tuner))
|
||||
return 0, false, errTunerLimit
|
||||
}
|
||||
|
||||
streamID = createStreamID(playlist.Streams)
|
||||
|
||||
var md5 = getMD5(streamingURL)
|
||||
playlist.Streams[streamID] = &bufferStream{
|
||||
URL: streamingURL,
|
||||
ChannelName: channelName,
|
||||
MD5: md5,
|
||||
Folder: playlist.Folder + md5 + string(os.PathSeparator),
|
||||
PlaylistID: playlistID,
|
||||
PlaylistName: playlist.PlaylistName,
|
||||
Clients: 1,
|
||||
}
|
||||
|
||||
showInfo("Streaming Status:" + tunerStatusLocked(playlist))
|
||||
|
||||
return streamID, true, nil
|
||||
}
|
||||
|
||||
func tunerStatusLocked(playlist *Playlist) string {
|
||||
return fmt.Sprintf("Playlist: %s - Tuner: %d / %d", playlist.PlaylistName, len(playlist.Streams), playlist.Tuner)
|
||||
}
|
||||
|
||||
// bufferTunerStatus : "Playlist: x - Tuner: n / m" for log lines.
|
||||
func bufferTunerStatus(playlistID string) string {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
if playlist, ok := bufferPlaylists[playlistID]; ok {
|
||||
return tunerStatusLocked(playlist)
|
||||
}
|
||||
return "Playlist: " + playlistID + " - Tuner: 0 / 0"
|
||||
}
|
||||
|
||||
func lookupStreamLocked(playlistID string, streamID int) (*bufferStream, bool) {
|
||||
if playlist, ok := bufferPlaylists[playlistID]; ok {
|
||||
s, ok := playlist.Streams[streamID]
|
||||
return s, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// bufferStreamSnapshot : copy of the shared stream state.
|
||||
func bufferStreamSnapshot(playlistID string, streamID int) (bufferStream, bool) {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
if s, ok := lookupStreamLocked(playlistID, streamID); ok {
|
||||
var copy = *s
|
||||
copy.cancel = nil
|
||||
return copy, true
|
||||
}
|
||||
return bufferStream{}, false
|
||||
}
|
||||
|
||||
// bufferStreamActive : false once the last client left and the stream was removed.
|
||||
func bufferStreamActive(playlistID string, streamID int) bool {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
_, ok := lookupStreamLocked(playlistID, streamID)
|
||||
return ok
|
||||
}
|
||||
|
||||
// bufferSetStatus : downloader reports that segments are available.
|
||||
func bufferSetStatus(playlistID string, streamID int, status bool) {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
if s, ok := lookupStreamLocked(playlistID, streamID); ok {
|
||||
s.Status = status
|
||||
}
|
||||
}
|
||||
|
||||
// bufferSetError : downloader reports a fatal error; attached clients disconnect.
|
||||
func bufferSetError(playlistID string, streamID int, err error) {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
if s, ok := lookupStreamLocked(playlistID, streamID); ok && s.Err == nil {
|
||||
s.Err = err
|
||||
}
|
||||
}
|
||||
|
||||
// bufferSetCancel : registers the function that stops an external buffer process.
|
||||
func bufferSetCancel(playlistID string, streamID int, cancel context.CancelFunc) {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
if s, ok := lookupStreamLocked(playlistID, streamID); ok {
|
||||
s.cancel = cancel
|
||||
}
|
||||
}
|
||||
|
||||
// bufferReleaseClient : detaches one client. When the last one leaves the
|
||||
// stream is removed, its process (if any) cancelled and its files deleted.
|
||||
func bufferReleaseClient(playlistID string, streamID int) {
|
||||
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
|
||||
playlist, ok := bufferPlaylists[playlistID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s, ok := playlist.Streams[streamID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
s.Clients--
|
||||
showInfo("Streaming Status:Client has terminated the connection")
|
||||
showInfo(fmt.Sprintf("Streaming Status:Channel: %s (Clients: %d)", s.ChannelName, s.Clients))
|
||||
|
||||
if s.Clients > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
delete(playlist.Streams, streamID)
|
||||
|
||||
showDebug(fmt.Sprintf("Remove tmp folder:%s", s.Folder), 1)
|
||||
os.RemoveAll(s.Folder)
|
||||
|
||||
showInfo(fmt.Sprintf("Streaming Status:Channel: %s - No client is using this channel anymore. Streaming Server connection has ended", s.ChannelName))
|
||||
showInfo("Streaming Status:" + tunerStatusLocked(playlist))
|
||||
|
||||
if len(playlist.Streams) == 0 {
|
||||
delete(bufferPlaylists, playlistID)
|
||||
}
|
||||
}
|
||||
|
||||
// bufferActiveStreams : number of buffered streams for a playlist (0 when idle).
|
||||
func bufferActiveStreams(playlistID string) int {
|
||||
bufferMu.Lock()
|
||||
defer bufferMu.Unlock()
|
||||
if playlist, ok := bufferPlaylists[playlistID]; ok {
|
||||
return len(playlist.Streams)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakeProvider streams endless TS-looking bytes and counts concurrent connections.
|
||||
type fakeProvider struct {
|
||||
srv *httptest.Server
|
||||
active atomic.Int32
|
||||
max atomic.Int32
|
||||
}
|
||||
|
||||
func newFakeProvider(t *testing.T) *fakeProvider {
|
||||
t.Helper()
|
||||
p := &fakeProvider{}
|
||||
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n := p.active.Add(1)
|
||||
defer p.active.Add(-1)
|
||||
for {
|
||||
m := p.max.Load()
|
||||
if n <= m || p.max.CompareAndSwap(m, n) {
|
||||
break
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "video/mp2t")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher, _ := w.(http.Flusher)
|
||||
chunk := bytes.Repeat([]byte{0x47}, 2048)
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
if _, err := w.Write(chunk); err != nil {
|
||||
return
|
||||
}
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(p.srv.Close)
|
||||
return p
|
||||
}
|
||||
|
||||
func setupBuffer(t *testing.T, tuner string) (playlistID string, clientSrv *httptest.Server) {
|
||||
t.Helper()
|
||||
playlistID = "M-test"
|
||||
|
||||
System.Folder.Temp = t.TempDir() + string(os.PathSeparator)
|
||||
Settings.Buffer = "xteve"
|
||||
Settings.BufferSize = 1
|
||||
Settings.BufferTimeout = 0
|
||||
Settings.Files.M3U = map[string]any{playlistID: map[string]any{"name": "Test", "tuner": tuner}}
|
||||
|
||||
bufferMu.Lock()
|
||||
bufferPlaylists = make(map[string]*Playlist)
|
||||
bufferMu.Unlock()
|
||||
|
||||
clientSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
bufferingStream(playlistID, r.URL.Query().Get("u"), "chan", w, r)
|
||||
}))
|
||||
t.Cleanup(clientSrv.Close)
|
||||
return
|
||||
}
|
||||
|
||||
// tune requests a stream, reads at least want bytes (or gives up after 10 s)
|
||||
// and returns what it got.
|
||||
func tune(t *testing.T, clientSrv *httptest.Server, streamURL string, want int) []byte {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", clientSrv.URL+"/?u="+streamURL, nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("tune %s: %v", streamURL, err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var got []byte
|
||||
buf := make([]byte, 4096)
|
||||
for len(got) < want {
|
||||
n, err := resp.Body.Read(buf)
|
||||
got = append(got, buf[:n]...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
func waitIdle(t *testing.T, playlistID string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if bufferActiveStreams(playlistID) == 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("streams still active: %d", bufferActiveStreams(playlistID))
|
||||
}
|
||||
|
||||
func TestBufferRestreamSharesOneProviderConnection(t *testing.T) {
|
||||
provider := newFakeProvider(t)
|
||||
playlistID, clientSrv := setupBuffer(t, "1")
|
||||
streamURL := provider.srv.URL + "/live.ts"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([][]byte, 2)
|
||||
for i := range results {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
results[i] = tune(t, clientSrv, streamURL, 4096)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i, got := range results {
|
||||
if len(got) == 0 {
|
||||
t.Errorf("client %d received no data", i)
|
||||
} else if got[0] != 0x47 {
|
||||
t.Errorf("client %d received something other than the provider stream", i)
|
||||
}
|
||||
}
|
||||
if m := provider.max.Load(); m != 1 {
|
||||
t.Errorf("provider saw %d concurrent connections, want 1 (restream)", m)
|
||||
}
|
||||
|
||||
waitIdle(t, playlistID)
|
||||
|
||||
entries, _ := os.ReadDir(System.Folder.Temp + playlistID)
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("stream folders left behind: %v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferTunerLimitIsAtomic(t *testing.T) {
|
||||
provider := newFakeProvider(t)
|
||||
playlistID, clientSrv := setupBuffer(t, "2")
|
||||
|
||||
limitClip, err := readWebFile("video/stream-limit.ts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const clients = 6
|
||||
var wg sync.WaitGroup
|
||||
results := make([][]byte, clients)
|
||||
for i := 0; i < clients; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
results[i] = tune(t, clientSrv, provider.srv.URL+"/"+string(rune('a'+i))+".ts", 2048)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Both the provider stream and the tuner-limit clip are MPEG-TS, so both
|
||||
// start with 0x47. The fake provider sends nothing but 0x47 bytes; the clip
|
||||
// does not.
|
||||
providerPrefix := bytes.Repeat([]byte{0x47}, 188)
|
||||
var real, limited int
|
||||
for i, got := range results {
|
||||
switch {
|
||||
case len(got) < 188:
|
||||
t.Errorf("client %d received only %d bytes", i, len(got))
|
||||
case bytes.HasPrefix(limitClip, got[:188]):
|
||||
limited++
|
||||
case bytes.Equal(got[:188], providerPrefix):
|
||||
real++
|
||||
default:
|
||||
t.Errorf("client %d received unexpected bytes", i)
|
||||
}
|
||||
}
|
||||
|
||||
if real != 2 || limited != 4 {
|
||||
t.Errorf("real=%d limited=%d, want 2 real streams and 4 tuner-limit responses", real, limited)
|
||||
}
|
||||
if m := provider.max.Load(); m > 2 {
|
||||
t.Errorf("provider saw %d concurrent connections, tuner limit is 2", m)
|
||||
}
|
||||
|
||||
waitIdle(t, playlistID)
|
||||
}
|
||||
|
||||
func TestBufferAcquireReleaseUnderContention(t *testing.T) {
|
||||
playlistID, _ := setupBuffer(t, "3")
|
||||
Settings.Buffer = "-" // no downloader needed for this test
|
||||
Settings.Tuner = 3
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var over atomic.Int32
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
id, isNew, err := bufferAcquireStream(playlistID, "http://x/"+string(rune('A'+i%26))+string(rune('a'+i/26)), "c")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n := bufferActiveStreams(playlistID); n > 3 {
|
||||
over.Store(int32(n))
|
||||
}
|
||||
_ = isNew
|
||||
time.Sleep(time.Millisecond)
|
||||
bufferReleaseClient(playlistID, id)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if n := over.Load(); n != 0 {
|
||||
t.Errorf("tuner limit exceeded: saw %d streams", n)
|
||||
}
|
||||
if n := bufferActiveStreams(playlistID); n != 0 {
|
||||
t.Errorf("%d streams left after release", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferReleaseCancelsProcessAndRemovesFolder(t *testing.T) {
|
||||
playlistID, _ := setupBuffer(t, "1")
|
||||
Settings.Buffer = "-"
|
||||
|
||||
id, isNew, err := bufferAcquireStream(playlistID, "http://x/one", "c")
|
||||
if err != nil || !isNew {
|
||||
t.Fatalf("acquire: new=%v err=%v", isNew, err)
|
||||
}
|
||||
|
||||
snap, _ := bufferStreamSnapshot(playlistID, id)
|
||||
if err := os.MkdirAll(snap.Folder, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cancelled atomic.Bool
|
||||
bufferSetCancel(playlistID, id, func() { cancelled.Store(true) })
|
||||
|
||||
bufferReleaseClient(playlistID, id)
|
||||
|
||||
if !cancelled.Load() {
|
||||
t.Error("cancel hook not called when the last client left")
|
||||
}
|
||||
if _, err := os.Stat(snap.Folder); !os.IsNotExist(err) {
|
||||
t.Error("stream folder not removed")
|
||||
}
|
||||
if bufferStreamActive(playlistID, id) {
|
||||
t.Error("stream still active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFFmpegArgs(t *testing.T) {
|
||||
got := buildFFmpegArgs("-hide_banner -i [URL] -c copy -f mpegts pipe:1", "http://h/s.ts", "UA/1")
|
||||
want := []string{"-user_agent", "UA/1", "-hide_banner", "-i", "http://h/s.ts", "-c", "copy", "-f", "mpegts", "pipe:1"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v\nwant %v", got, want)
|
||||
}
|
||||
if got := buildFFmpegArgs("-i [URL]", "u", ""); !reflect.DeepEqual(got, []string{"-i", "u"}) {
|
||||
t.Errorf("no user agent: got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVLCArgs(t *testing.T) {
|
||||
got := buildVLCArgs("-I dummy [URL] --sout #std{mux=ts,access=file,dst=-}", "http://h/s.ts", "UA/1")
|
||||
want := []string{"-I", "dummy", "http://h/s.ts", ":http-user-agent=UA/1", "--sout", "#std{mux=ts,access=file,dst=-}"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v\nwant %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
var _ = io.EOF
|
||||
+36
-5
@@ -4,6 +4,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -91,12 +92,21 @@ func extractZIP(archive, target string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
var extractOne = func(file *zip.File) error {
|
||||
|
||||
path := filepath.Join(target, file.Name)
|
||||
|
||||
// Zip-slip guard: an entry like "../../etc/passwd" must not escape target.
|
||||
if !insideDir(target, path) {
|
||||
return fmt.Errorf("archive entry %q escapes the target directory", file.Name)
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
os.MkdirAll(path, file.Mode())
|
||||
continue
|
||||
return os.MkdirAll(path, file.Mode())
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileReader, err := file.Open()
|
||||
@@ -111,10 +121,14 @@ func extractZIP(archive, target string) (err error) {
|
||||
}
|
||||
defer targetFile.Close()
|
||||
|
||||
if _, err := io.Copy(targetFile, fileReader); err != nil {
|
||||
_, err = io.Copy(targetFile, fileReader)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
if err = extractOne(file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
@@ -146,3 +160,20 @@ func extractGZIP(gzipBody []byte, fileSource string) (body []byte, err error) {
|
||||
body = resB.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func compressGZIP(data *[]byte, file string) (err error) {
|
||||
|
||||
if len(file) != 0 {
|
||||
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := gzip.NewWriter(f)
|
||||
w.Write(*data)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+26
-36
@@ -5,7 +5,6 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// System : Beinhaltet alle Systeminformationen
|
||||
@@ -15,7 +14,7 @@ var System SystemStruct
|
||||
var WebScreenLog WebScreenLogStruct
|
||||
|
||||
// Settings : Inhalt der settings.json
|
||||
var Settings SettingsStrcut
|
||||
var Settings SettingsStruct
|
||||
|
||||
// Data : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
|
||||
var Data DataStruct
|
||||
@@ -23,12 +22,6 @@ var Data DataStruct
|
||||
// SystemFiles : Alle Systemdateien
|
||||
var SystemFiles = []string{"authentication.json", "pms.json", "settings.json", "xepg.json", "urls.json"}
|
||||
|
||||
// BufferInformation : Informationen über den Buffer (aktive Streams, maximale Streams)
|
||||
var BufferInformation sync.Map
|
||||
|
||||
// BufferClients : Anzahl der Clients die einen Stream über den Buffer abspielen
|
||||
var BufferClients sync.Map
|
||||
|
||||
// Init : Systeminitialisierung
|
||||
func Init() (err error) {
|
||||
|
||||
@@ -43,17 +36,17 @@ func Init() (err error) {
|
||||
System.ServerProtocol.M3U = "http"
|
||||
System.ServerProtocol.WEB = "http"
|
||||
System.ServerProtocol.XML = "http"
|
||||
System.DVRLimit = 480
|
||||
System.PlexChannelLimit = 480
|
||||
System.UnfilteredChannelLimit = 480
|
||||
System.Compatibility = "1.4.4"
|
||||
|
||||
// FFmpeg Default Einstellungen
|
||||
System.FFmpeg.DefaultOptions = "-hide_banner -loglevel error -i [URL] -c copy -f mpegts pipe:1"
|
||||
System.VLC.DefaultOptions = "-I dummy [URL] --sout #std{mux=ts,access=file,dst=-}"
|
||||
|
||||
// Default Logeinträge, wird später von denen aus der settings.json überschrieben. Muss gemacht werden, damit die ersten Einträge auch im Log (webUI aangezeigt werden)
|
||||
Settings.LogEntriesRAM = 500
|
||||
|
||||
// Variablen für den Update Prozess
|
||||
//System.Update.Git = "https://github.com/xteve-project/xTeVe-Downloads/blob"
|
||||
System.Update.Git = fmt.Sprintf("https://github.com/%s/%s/blob", System.GitHub.User, System.GitHub.Repo)
|
||||
System.Update.Name = "xteve_2"
|
||||
|
||||
// Ordnerpfade festlegen
|
||||
var tempFolder = os.TempDir() + string(os.PathSeparator) + System.AppName + string(os.PathSeparator)
|
||||
tempFolder = getPlatformPath(strings.Replace(tempFolder, "//", "/", -1))
|
||||
@@ -91,6 +84,8 @@ func Init() (err error) {
|
||||
System.File.XML = getPlatformFile(fmt.Sprintf("%s%s.xml", System.Folder.Data, System.AppName))
|
||||
System.File.M3U = getPlatformFile(fmt.Sprintf("%s%s.m3u", System.Folder.Data, System.AppName))
|
||||
|
||||
System.Compressed.GZxml = getPlatformFile(fmt.Sprintf("%s%s.xml.gz", System.Folder.Data, System.AppName))
|
||||
|
||||
err = activatedSystemAuthentication()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -109,7 +104,7 @@ func Init() (err error) {
|
||||
|
||||
// Überprüfen ob xTeVe als root läuft
|
||||
if os.Geteuid() == 0 {
|
||||
showWarning(2010)
|
||||
showWarning(2110)
|
||||
}
|
||||
|
||||
if System.Flag.Debug > 0 {
|
||||
@@ -133,7 +128,6 @@ func Init() (err error) {
|
||||
// Bedingte Update Änderungen durchführen
|
||||
err = conditionalUpdateChanges()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,8 +142,9 @@ func Init() (err error) {
|
||||
|
||||
// Berechtigung aller Ordner überprüfen
|
||||
err = checkFilePermission(System.Folder.Config)
|
||||
if err == nil {
|
||||
err = checkFilePermission(System.Folder.Temp)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
// Separaten tmp Ordner für jede Instanz
|
||||
@@ -161,6 +156,12 @@ func Init() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = checkFilePermission(System.Folder.Temp)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
err = removeChildItems(getPlatformPath(System.Folder.Temp))
|
||||
if err != nil {
|
||||
return
|
||||
@@ -169,7 +170,7 @@ func Init() (err error) {
|
||||
// Branch festlegen
|
||||
System.Branch = Settings.Branch
|
||||
|
||||
if System.Dev == true {
|
||||
if System.Dev {
|
||||
System.Branch = "Development"
|
||||
}
|
||||
|
||||
@@ -177,24 +178,15 @@ func Init() (err error) {
|
||||
System.Branch = "master"
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("GitHub:https://github.com/%s", System.GitHub.User))
|
||||
showInfo(fmt.Sprintf("Git Branch:%s [%s]", System.Branch, System.GitHub.User))
|
||||
showInfo(fmt.Sprintf("Branch:%s", System.Branch))
|
||||
|
||||
// Domainnamen setzten
|
||||
setGlobalDomain(fmt.Sprintf("%s:%s", System.IPAddress, Settings.Port))
|
||||
|
||||
System.URLBase = fmt.Sprintf("%s://%s:%s", System.ServerProtocol.WEB, System.IPAddress, Settings.Port)
|
||||
|
||||
// HTML Dateien erstellen, mit dev == true werden die lokalen HTML Dateien verwendet
|
||||
if System.Dev == true {
|
||||
|
||||
HTMLInit("webUI", "src", "html"+string(os.PathSeparator), "src"+string(os.PathSeparator)+"webUI.go")
|
||||
err = BuildGoFile()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
// Mit dev == true werden die lokalen HTML Dateien verwendet, sonst die eingebetteten
|
||||
initWebFS(System.Dev)
|
||||
|
||||
// DLNA Server starten
|
||||
err = SSDP()
|
||||
@@ -202,9 +194,6 @@ func Init() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// HTML Datein laden
|
||||
loadHTMLMap()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,10 +210,11 @@ func StartSystem(updateProviderFiles bool) (err error) {
|
||||
showInfo(fmt.Sprintf("UUID:%s", Settings.UUID))
|
||||
showInfo(fmt.Sprintf("Tuner (Plex / Emby):%d", Settings.Tuner))
|
||||
showInfo(fmt.Sprintf("EPG Source:%s", Settings.EpgSource))
|
||||
showInfo(fmt.Sprintf("Plex Channel Limit:%d", System.DVRLimit))
|
||||
showInfo(fmt.Sprintf("Plex Channel Limit:%d", System.PlexChannelLimit))
|
||||
showInfo(fmt.Sprintf("Unfiltered Chan. Limit:%d", System.UnfilteredChannelLimit))
|
||||
|
||||
// Providerdaten aktualisieren
|
||||
if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate == true || updateProviderFiles == true {
|
||||
if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate || updateProviderFiles {
|
||||
|
||||
err = xTeVeAutoBackup()
|
||||
if err != nil {
|
||||
|
||||
+307
-92
@@ -11,19 +11,23 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"../src/internal/authentication"
|
||||
"xteve/src/internal/authentication"
|
||||
"xteve/src/internal/imgcache"
|
||||
)
|
||||
|
||||
// Einstellungen ändern (WebUI)
|
||||
func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err error) {
|
||||
func updateServerSettings(request RequestStruct) (settings SettingsStruct, err error) {
|
||||
|
||||
var oldSettings = jsonToMap(mapToJSON(Settings))
|
||||
var newSettings = jsonToMap(mapToJSON(request.Settings))
|
||||
var reloadData = false
|
||||
var cacheImages = false
|
||||
var createXEPGFiles = false
|
||||
var triggerPlexGuideReload = false
|
||||
var debug string
|
||||
|
||||
// -vvv [URL] --sout '#transcode{vcodec=mp4v, acodec=mpga} :standard{access=http, mux=ogg}'
|
||||
|
||||
for key, value := range newSettings {
|
||||
|
||||
if _, ok := oldSettings[key]; ok {
|
||||
@@ -33,24 +37,57 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
case "tuner":
|
||||
showWarning(2105)
|
||||
|
||||
case "use_plexAPI":
|
||||
triggerPlexGuideReload = true
|
||||
|
||||
case "plex.url":
|
||||
if v, ok := value.(string); ok {
|
||||
value = strings.TrimRight(strings.TrimSpace(v), "/")
|
||||
}
|
||||
triggerPlexGuideReload = true
|
||||
|
||||
case "plex.token":
|
||||
if v, ok := value.(string); ok {
|
||||
value = strings.TrimSpace(v)
|
||||
// The UI only ever sees the mask; sending it back means "unchanged".
|
||||
if value == plexTokenMask {
|
||||
value = Settings.PlexToken
|
||||
}
|
||||
}
|
||||
triggerPlexGuideReload = true
|
||||
|
||||
case "epgSource":
|
||||
reloadData = true
|
||||
|
||||
case "update":
|
||||
// Leerzeichen aus den Werten entfernen und Formatierung der Uhrzeit überprüfen (0000 - 2359)
|
||||
var newUpdateTimes []string
|
||||
var newUpdateTimes = make([]string, 0)
|
||||
|
||||
for _, v := range value.([]interface{}) {
|
||||
updateTimes, ok := value.([]any)
|
||||
if !ok {
|
||||
err = errors.New(getErrMsg(1012))
|
||||
ShowError(err, 1012)
|
||||
return Settings, err
|
||||
}
|
||||
|
||||
v = strings.Replace(v.(string), " ", "", -1)
|
||||
for _, v := range updateTimes {
|
||||
|
||||
_, err := time.Parse("1504", v.(string))
|
||||
t, ok := v.(string)
|
||||
if !ok {
|
||||
err = errors.New(getErrMsg(1012))
|
||||
ShowError(err, 1012)
|
||||
return Settings, err
|
||||
}
|
||||
|
||||
t = strings.Replace(t, " ", "", -1)
|
||||
|
||||
_, err := time.Parse("1504", t)
|
||||
if err != nil {
|
||||
ShowError(err, 1012)
|
||||
return Settings, err
|
||||
}
|
||||
|
||||
newUpdateTimes = append(newUpdateTimes, v.(string))
|
||||
newUpdateTimes = append(newUpdateTimes, t)
|
||||
|
||||
}
|
||||
|
||||
@@ -62,6 +99,16 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
case "xepg.replace.missing.images":
|
||||
createXEPGFiles = true
|
||||
|
||||
case "xepg.missing.epg.mode":
|
||||
if v, ok := value.(string); ok {
|
||||
mode := strings.ToLower(strings.TrimSpace(v))
|
||||
if mode != "relaxed" {
|
||||
mode = "strict"
|
||||
}
|
||||
value = mode
|
||||
}
|
||||
reloadData = true
|
||||
|
||||
case "backup.path":
|
||||
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
|
||||
err = checkFolder(value.(string))
|
||||
@@ -94,26 +141,44 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
return
|
||||
}
|
||||
|
||||
case "ffmpeg.path", "vlc.path":
|
||||
var path = value.(string)
|
||||
if len(path) > 0 {
|
||||
|
||||
err = checkStreamingBinary(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case "scheme.m3u", "scheme.xml":
|
||||
createXEPGFiles = true
|
||||
|
||||
}
|
||||
|
||||
oldSettings[key] = value
|
||||
|
||||
switch fmt.Sprintf("%T", value) {
|
||||
if key == "plex.token" {
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: ******** (%T)", key, value)
|
||||
} else {
|
||||
switch fmt.Sprintf("%T", value) {
|
||||
|
||||
case "bool":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
|
||||
case "bool":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
|
||||
|
||||
case "string":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
|
||||
case "string":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
|
||||
|
||||
case "[]interface {}":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
|
||||
case "[]interface {}":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
|
||||
|
||||
case "float64":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
|
||||
case "float64":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
|
||||
|
||||
default:
|
||||
debug = fmt.Sprintf("%T", value)
|
||||
default:
|
||||
debug = fmt.Sprintf("%T", value)
|
||||
}
|
||||
}
|
||||
|
||||
showDebug(debug, 1)
|
||||
@@ -128,7 +193,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
return
|
||||
}
|
||||
|
||||
if Settings.AuthenticationWEB == false {
|
||||
if !Settings.AuthenticationWEB {
|
||||
|
||||
Settings.AuthenticationAPI = false
|
||||
Settings.AuthenticationM3U = false
|
||||
@@ -138,12 +203,39 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
}
|
||||
|
||||
// Buffer Einstellungen überprüfen
|
||||
if len(Settings.FFmpegOptions) == 0 {
|
||||
Settings.FFmpegOptions = System.FFmpeg.DefaultOptions
|
||||
}
|
||||
|
||||
if len(Settings.VLCOptions) == 0 {
|
||||
Settings.VLCOptions = System.VLC.DefaultOptions
|
||||
}
|
||||
|
||||
switch Settings.Buffer {
|
||||
|
||||
case "ffmpeg":
|
||||
|
||||
if len(Settings.FFmpegPath) == 0 {
|
||||
err = errors.New(getErrMsg(2020))
|
||||
return
|
||||
}
|
||||
|
||||
case "vlc":
|
||||
|
||||
if len(Settings.VLCPath) == 0 {
|
||||
err = errors.New(getErrMsg(2021))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = saveSettings(Settings)
|
||||
if err == nil {
|
||||
|
||||
settings = Settings
|
||||
|
||||
if reloadData == true {
|
||||
if reloadData {
|
||||
|
||||
err = buildDatabaseDVR()
|
||||
if err != nil {
|
||||
@@ -154,33 +246,46 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
}
|
||||
|
||||
if cacheImages == true {
|
||||
if cacheImages {
|
||||
|
||||
if Settings.EpgSource == "XEPG" {
|
||||
if Settings.EpgSource == "XEPG" && System.ImageCachingInProgress == 0 {
|
||||
|
||||
go func() {
|
||||
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
if Settings.CacheImages == true {
|
||||
switch Settings.CacheImages {
|
||||
|
||||
createXMLTVFile()
|
||||
cachingImages()
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
case false:
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
|
||||
} else {
|
||||
case true:
|
||||
go func() {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
|
||||
}
|
||||
System.ImageCachingInProgress = 1
|
||||
showInfo("Image Caching:Images are cached")
|
||||
|
||||
}()
|
||||
Data.Cache.Images.Image.Caching()
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
buildXEPG(false)
|
||||
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if createXEPGFiles == true {
|
||||
if createXEPGFiles {
|
||||
|
||||
go func() {
|
||||
createXMLTVFile()
|
||||
@@ -189,6 +294,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
}
|
||||
|
||||
if triggerPlexGuideReload {
|
||||
queuePlexGuideRefresh("settings change")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
@@ -197,8 +306,8 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
// Providerdaten speichern (WebUI)
|
||||
func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
var filesMap = make(map[string]interface{})
|
||||
var newData = make(map[string]interface{})
|
||||
var filesMap = make(map[string]any)
|
||||
var newData = make(map[string]any)
|
||||
var indicator string
|
||||
var reloadData = false
|
||||
|
||||
@@ -220,26 +329,35 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
}
|
||||
|
||||
if len(filesMap) == 0 {
|
||||
filesMap = make(map[string]interface{})
|
||||
filesMap = make(map[string]any)
|
||||
}
|
||||
|
||||
for dataID, data := range newData {
|
||||
|
||||
dataMap, ok := data.(map[string]any)
|
||||
if !ok {
|
||||
err = errors.New(getErrMsg(1020))
|
||||
return
|
||||
}
|
||||
|
||||
if dataID == "-" {
|
||||
|
||||
// Neue Providerdatei
|
||||
dataID = indicator + randomString(19)
|
||||
data.(map[string]interface{})["new"] = true
|
||||
filesMap[dataID] = data
|
||||
dataMap["new"] = true
|
||||
filesMap[dataID] = dataMap
|
||||
|
||||
} else {
|
||||
|
||||
// Bereits vorhandene Providerdatei
|
||||
for key, value := range data.(map[string]interface{}) {
|
||||
oldData, ok := filesMap[dataID].(map[string]any)
|
||||
if !ok {
|
||||
// Unknown file ID: nothing to update.
|
||||
continue
|
||||
}
|
||||
|
||||
var oldData = filesMap[dataID].(map[string]interface{})
|
||||
for key, value := range dataMap {
|
||||
oldData[key] = value
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -258,11 +376,11 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
}
|
||||
|
||||
// Neue Providerdatei
|
||||
if _, ok := data.(map[string]interface{})["new"]; ok {
|
||||
if _, ok := dataMap["new"]; ok {
|
||||
|
||||
reloadData = true
|
||||
err = getProviderData(fileType, dataID)
|
||||
delete(data.(map[string]interface{}), "new")
|
||||
delete(dataMap, "new")
|
||||
|
||||
if err != nil {
|
||||
delete(filesMap, dataID)
|
||||
@@ -271,7 +389,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
}
|
||||
|
||||
if _, ok := data.(map[string]interface{})["delete"]; ok {
|
||||
if _, ok := data.(map[string]any)["delete"]; ok {
|
||||
|
||||
deleteLocalProviderFiles(dataID, fileType)
|
||||
reloadData = true
|
||||
@@ -283,7 +401,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if reloadData == true {
|
||||
if reloadData {
|
||||
|
||||
err = buildDatabaseDVR()
|
||||
if err != nil {
|
||||
@@ -304,7 +422,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
// Providerdaten manuell aktualisieren (WebUI)
|
||||
func updateFile(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
var updateData = make(map[string]interface{})
|
||||
var updateData = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
|
||||
@@ -334,7 +452,7 @@ func updateFile(request RequestStruct, fileType string) (err error) {
|
||||
// Providerdaten löschen (WebUI)
|
||||
func deleteLocalProviderFiles(dataID, fileType string) {
|
||||
|
||||
var removeData = make(map[string]interface{})
|
||||
var removeData = make(map[string]any)
|
||||
var fileExtension string
|
||||
|
||||
switch fileType {
|
||||
@@ -357,15 +475,15 @@ func deleteLocalProviderFiles(dataID, fileType string) {
|
||||
os.RemoveAll(System.Folder.Data + dataID + fileExtension)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filtereinstellungen speichern (WebUI)
|
||||
func saveFilter(request RequestStruct) (settings SettingsStrcut, err error) {
|
||||
func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
|
||||
|
||||
var filterMap = make(map[int64]interface{})
|
||||
var newData = make(map[int64]interface{})
|
||||
var filterMap = make(map[int64]any)
|
||||
var newData = make(map[int64]any)
|
||||
var defaultFilter FilterStruct
|
||||
var newFilter = false
|
||||
|
||||
defaultFilter.Active = true
|
||||
defaultFilter.CaseSensitive = false
|
||||
@@ -389,23 +507,54 @@ func saveFilter(request RequestStruct) (settings SettingsStrcut, err error) {
|
||||
if dataID == -1 {
|
||||
|
||||
// Neuer Filter
|
||||
newFilter = true
|
||||
dataID = createNewID()
|
||||
filterMap[dataID] = jsonToMap(mapToJSON(defaultFilter))
|
||||
|
||||
}
|
||||
|
||||
// Filter aktualisieren / löschen
|
||||
for key, value := range data.(map[string]interface{}) {
|
||||
|
||||
var oldData = filterMap[dataID].(map[string]interface{})
|
||||
oldData[key] = value
|
||||
for key, value := range data.(map[string]any) {
|
||||
|
||||
// Filter löschen
|
||||
if _, ok := data.(map[string]interface{})["delete"]; ok {
|
||||
|
||||
if _, ok := data.(map[string]any)["delete"]; ok {
|
||||
delete(filterMap, dataID)
|
||||
break
|
||||
}
|
||||
|
||||
if filter, ok := data.(map[string]any)["filter"].(string); ok {
|
||||
|
||||
if len(filter) == 0 {
|
||||
|
||||
err = errors.New(getErrMsg(1014))
|
||||
if newFilter {
|
||||
delete(filterMap, dataID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Regular expressions are checked before anything is saved.
|
||||
var filterType, _ = data.(map[string]any)["type"].(string)
|
||||
if filterType == "" {
|
||||
if old, ok := filterMap[dataID].(map[string]any); ok {
|
||||
filterType, _ = old["type"].(string)
|
||||
}
|
||||
}
|
||||
if filterType == "regex-filter" {
|
||||
if reErr := validateRegexFilter(filter); reErr != nil {
|
||||
err = fmt.Errorf("invalid regular expression: %v", reErr)
|
||||
if newFilter {
|
||||
delete(filterMap, dataID)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if oldData, ok := filterMap[dataID].(map[string]any); ok {
|
||||
oldData[key] = value
|
||||
}
|
||||
|
||||
}
|
||||
@@ -434,6 +583,11 @@ func saveXEpgMapping(request RequestStruct) (err error) {
|
||||
|
||||
var tmp = Data.XEPG
|
||||
|
||||
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(mapToJSON(request.EpgMapping)), &tmp)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -446,8 +600,44 @@ func saveXEpgMapping(request RequestStruct) (err error) {
|
||||
|
||||
Data.XEPG.Channels = request.EpgMapping
|
||||
|
||||
cleanupXEPG()
|
||||
buildXEPG(true)
|
||||
if System.ScanInProgress == 0 {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
cleanupXEPG()
|
||||
System.ScanInProgress = 0
|
||||
buildXEPG(true)
|
||||
|
||||
} else {
|
||||
|
||||
// Wenn während des erstellen der Datanbank das Mapping erneut gespeichert wird, wird die Datenbank erst später erneut aktualisiert.
|
||||
go func() {
|
||||
|
||||
if System.BackgroundProcess {
|
||||
return
|
||||
}
|
||||
|
||||
System.BackgroundProcess = true
|
||||
|
||||
for {
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
if System.ScanInProgress == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.ScanInProgress = 1
|
||||
cleanupXEPG()
|
||||
System.ScanInProgress = 0
|
||||
buildXEPG(false)
|
||||
showInfo("XEPG:" + "Ready to use")
|
||||
|
||||
System.BackgroundProcess = false
|
||||
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -456,7 +646,7 @@ func saveUserData(request RequestStruct) (err error) {
|
||||
|
||||
var userData = request.UserData
|
||||
|
||||
var newCredentials = func(userID string, newUserData map[string]interface{}) (err error) {
|
||||
var newCredentials = func(userID string, newUserData map[string]any) (err error) {
|
||||
|
||||
var newUsername, newPassword string
|
||||
if username, ok := newUserData["username"].(string); ok {
|
||||
@@ -476,26 +666,26 @@ func saveUserData(request RequestStruct) (err error) {
|
||||
|
||||
for userID, newUserData := range userData {
|
||||
|
||||
err = newCredentials(userID, newUserData.(map[string]interface{}))
|
||||
err = newCredentials(userID, newUserData.(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if request.DeleteUser == true {
|
||||
if request.DeleteUser {
|
||||
err = authentication.RemoveUser(userID)
|
||||
return
|
||||
}
|
||||
|
||||
delete(newUserData.(map[string]interface{}), "password")
|
||||
delete(newUserData.(map[string]interface{}), "confirm")
|
||||
delete(newUserData.(map[string]any), "password")
|
||||
delete(newUserData.(map[string]any), "confirm")
|
||||
|
||||
if _, ok := newUserData.(map[string]interface{})["delete"]; ok {
|
||||
if _, ok := newUserData.(map[string]any)["delete"]; ok {
|
||||
|
||||
authentication.RemoveUser(userID)
|
||||
|
||||
} else {
|
||||
|
||||
err = authentication.WriteUserData(userID, newUserData.(map[string]interface{}))
|
||||
err = authentication.WriteUserData(userID, newUserData.(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -511,8 +701,16 @@ func saveUserData(request RequestStruct) (err error) {
|
||||
func saveNewUser(request RequestStruct) (err error) {
|
||||
|
||||
var data = request.UserData
|
||||
var username = data["username"].(string)
|
||||
var password = data["password"].(string)
|
||||
|
||||
username, ok := data["username"].(string)
|
||||
if !ok {
|
||||
return errors.New("Username is missing")
|
||||
}
|
||||
|
||||
password, ok := data["password"].(string)
|
||||
if !ok {
|
||||
return errors.New("Password is missing")
|
||||
}
|
||||
|
||||
delete(data, "password")
|
||||
delete(data, "confirm")
|
||||
@@ -545,11 +743,11 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
|
||||
case "m3u", "xmltv":
|
||||
|
||||
var filesMap = make(map[string]interface{})
|
||||
var data = make(map[string]interface{})
|
||||
var filesMap = make(map[string]any)
|
||||
var data = make(map[string]any)
|
||||
var indicator, dataID string
|
||||
|
||||
filesMap = make(map[string]interface{})
|
||||
filesMap = make(map[string]any)
|
||||
|
||||
data["type"] = key
|
||||
data["new"] = true
|
||||
@@ -612,6 +810,7 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
}
|
||||
|
||||
buildXEPG(false)
|
||||
System.ScanInProgress = 0
|
||||
|
||||
}
|
||||
|
||||
@@ -619,6 +818,10 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
|
||||
}
|
||||
|
||||
if nextStep == 10 {
|
||||
Settings.WizardCompleted = true
|
||||
}
|
||||
|
||||
err = saveSettings(Settings)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -653,6 +856,17 @@ func createFilterRules() (err error) {
|
||||
|
||||
Data.Filter = append(Data.Filter, dataFilter)
|
||||
|
||||
case "regex-filter":
|
||||
if err = validateRegexFilter(filter.Filter); err != nil {
|
||||
return fmt.Errorf("filter %q: invalid regular expression: %v", filter.Name, err)
|
||||
}
|
||||
|
||||
dataFilter.CaseSensitive = filter.CaseSensitive
|
||||
dataFilter.Rule = filter.Filter
|
||||
dataFilter.Type = filter.Type
|
||||
|
||||
Data.Filter = append(Data.Filter, dataFilter)
|
||||
|
||||
case "group-title":
|
||||
if len(filter.Include) > 0 {
|
||||
include = fmt.Sprintf(" {%s}", filter.Include)
|
||||
@@ -679,9 +893,9 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
|
||||
Data.Streams.All = make([]interface{}, 0)
|
||||
Data.Streams.Active = make([]interface{}, 0)
|
||||
Data.Streams.Inactive = make([]interface{}, 0)
|
||||
Data.Streams.All = make([]any, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.Active = make([]any, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.Inactive = make([]any, 0, System.UnfilteredChannelLimit)
|
||||
Data.Playlist.M3U.Groups.Text = []string{}
|
||||
Data.Playlist.M3U.Groups.Value = []string{}
|
||||
Data.StreamPreviewUI.Active = []string{}
|
||||
@@ -700,10 +914,10 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
var playlistFile = getLocalProviderFiles(fileType)
|
||||
|
||||
for n, i := range playlistFile {
|
||||
for _, i := range playlistFile {
|
||||
|
||||
var channels []interface{}
|
||||
var groupTitle, tvgID, uuid int = 0, 0, 0
|
||||
var channels []any
|
||||
var groupTitle, tvgID, uuid = 0, 0, 0
|
||||
var keys = []string{"group-title", "tvg-id", "uuid"}
|
||||
var compatibility = make(map[string]int)
|
||||
|
||||
@@ -723,7 +937,8 @@ func buildDatabaseDVR() (err error) {
|
||||
ShowError(err, 1005)
|
||||
err = errors.New(playlistName + ": Local copy of the file no longer exists")
|
||||
ShowError(err, 0)
|
||||
playlistFile = append(playlistFile[:n], playlistFile[n+1:]...)
|
||||
// playlistFile is not used after this loop; removing the entry
|
||||
// while ranging over it only skipped the following file.
|
||||
}
|
||||
|
||||
// Streams analysieren
|
||||
@@ -749,11 +964,7 @@ func buildDatabaseDVR() (err error) {
|
||||
if value, ok := s[key]; ok {
|
||||
if len(value) > 0 {
|
||||
|
||||
if _, ok := tmpGroupsM3U[value]; ok {
|
||||
tmpGroupsM3U[value]++
|
||||
} else {
|
||||
tmpGroupsM3U[value] = 1
|
||||
}
|
||||
tmpGroupsM3U[value]++
|
||||
|
||||
groupTitle++
|
||||
}
|
||||
@@ -829,7 +1040,7 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
for group, count := range tmpGroupsM3U {
|
||||
var text = fmt.Sprintf("%s (%d)", group, count)
|
||||
var value = fmt.Sprintf("%s", group)
|
||||
var value = group
|
||||
Data.Playlist.M3U.Groups.Text = append(Data.Playlist.M3U.Groups.Text, text)
|
||||
Data.Playlist.M3U.Groups.Value = append(Data.Playlist.M3U.Groups.Value, value)
|
||||
}
|
||||
@@ -837,20 +1048,20 @@ func buildDatabaseDVR() (err error) {
|
||||
sort.Strings(Data.Playlist.M3U.Groups.Text)
|
||||
sort.Strings(Data.Playlist.M3U.Groups.Value)
|
||||
|
||||
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.DVRLimit && len(Settings.Filter) == 0 {
|
||||
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.UnfilteredChannelLimit && len(Settings.Filter) == 0 {
|
||||
Data.Streams.Active = Data.Streams.All
|
||||
Data.Streams.Inactive = make([]interface{}, 0)
|
||||
Data.Streams.Inactive = make([]any, 0)
|
||||
|
||||
Data.StreamPreviewUI.Active = Data.StreamPreviewUI.Inactive
|
||||
Data.StreamPreviewUI.Inactive = []string{}
|
||||
|
||||
}
|
||||
|
||||
if len(Data.Streams.Active) > System.DVRLimit {
|
||||
if len(Data.Streams.Active) > System.PlexChannelLimit {
|
||||
showWarning(2000)
|
||||
}
|
||||
|
||||
if len(Settings.Filter) == 0 && len(Data.Streams.All) > System.DVRLimit {
|
||||
if len(Settings.Filter) == 0 && len(Data.Streams.All) > System.UnfilteredChannelLimit {
|
||||
showWarning(2001)
|
||||
}
|
||||
|
||||
@@ -862,6 +1073,10 @@ func buildDatabaseDVR() (err error) {
|
||||
sort.Strings(Data.StreamPreviewUI.Active)
|
||||
sort.Strings(Data.StreamPreviewUI.Inactive)
|
||||
|
||||
if Settings.EpgSource != "XEPG" {
|
||||
queuePlexGuideRefresh("lineup update")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -869,7 +1084,7 @@ func buildDatabaseDVR() (err error) {
|
||||
func getLocalProviderFiles(fileType string) (localFiles []string) {
|
||||
|
||||
var fileExtension string
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
|
||||
@@ -897,7 +1112,7 @@ func getLocalProviderFiles(fileType string) (localFiles []string) {
|
||||
// Providerparameter anhand von dem Key ausgeben
|
||||
func getProviderParameter(id, fileType, key string) (s string) {
|
||||
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
case "m3u":
|
||||
@@ -910,7 +1125,7 @@ func getProviderParameter(id, fileType, key string) (s string) {
|
||||
dataMap = Settings.Files.XMLTV
|
||||
}
|
||||
|
||||
if data, ok := dataMap[id].(map[string]interface{}); ok {
|
||||
if data, ok := dataMap[id].(map[string]any); ok {
|
||||
|
||||
if v, ok := data[key].(string); ok {
|
||||
s = v
|
||||
@@ -928,7 +1143,7 @@ func getProviderParameter(id, fileType, key string) (s string) {
|
||||
// Provider Statistiken Kompatibilität aktualisieren
|
||||
func setProviderCompatibility(id, fileType string, compatibility map[string]int) {
|
||||
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
case "m3u":
|
||||
@@ -941,7 +1156,7 @@ func setProviderCompatibility(id, fileType string, compatibility map[string]int)
|
||||
dataMap = Settings.Files.XMLTV
|
||||
}
|
||||
|
||||
if data, ok := dataMap[id].(map[string]interface{}); ok {
|
||||
if data, ok := dataMap[id].(map[string]any); ok {
|
||||
|
||||
data["compatibility"] = compatibility
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package src
|
||||
|
||||
import "testing"
|
||||
|
||||
func streamFor(name, group string) map[string]string {
|
||||
return map[string]string{
|
||||
"name": name,
|
||||
"group-title": group,
|
||||
"_values": "tvg-id=\"" + name + "\" group-title=\"" + group + "\"," + name,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexFilter(t *testing.T) {
|
||||
Data.Filter = []Filter{{Type: "regex-filter", Rule: `^(BBC|ITV) \d+$`, CaseSensitive: false}}
|
||||
regexFilterCache.Range(func(k, v any) bool { regexFilterCache.Delete(k); return true })
|
||||
|
||||
cases := map[string]bool{
|
||||
"BBC 1": true,
|
||||
"itv 2": true, // case-insensitive by default
|
||||
"BBC News": false,
|
||||
"Sky One": false,
|
||||
"BBC 1 HD": false,
|
||||
"Radio BBC": false,
|
||||
}
|
||||
// Group title is matched too.
|
||||
Data.Filter = append(Data.Filter, Filter{Type: "regex-filter", Rule: `^Sports$`})
|
||||
cases["Sports"] = false // name "Sports" is only in group below
|
||||
if !filterThisStream(streamFor("Golf", "Sports")) {
|
||||
t.Error("regex should match the group title")
|
||||
}
|
||||
delete(cases, "Sports")
|
||||
for name, want := range cases {
|
||||
if got := filterThisStream(streamFor(name, "UK")); got != want {
|
||||
t.Errorf("%q: got %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Data.Filter = []Filter{{Type: "regex-filter", Rule: `^bbc`, CaseSensitive: true}}
|
||||
if filterThisStream(streamFor("BBC 1", "UK")) {
|
||||
t.Error("case-sensitive rule must not match different case")
|
||||
}
|
||||
|
||||
Data.Filter = []Filter{{Type: "regex-filter", Rule: `(`, CaseSensitive: false}}
|
||||
if filterThisStream(streamFor("BBC 1", "UK")) {
|
||||
t.Error("invalid pattern must never match")
|
||||
}
|
||||
if validateRegexFilter(`(`) == nil {
|
||||
t.Error("validateRegexFilter accepted an invalid pattern")
|
||||
}
|
||||
|
||||
Data.Filter = nil
|
||||
}
|
||||
|
||||
func TestBufferModeFor(t *testing.T) {
|
||||
Settings.Buffer = "xteve"
|
||||
Settings.Files.M3U = map[string]any{
|
||||
"M-a": map[string]any{"name": "A", "buffer": "ffmpeg"},
|
||||
"M-b": map[string]any{"name": "B", "buffer": "default"},
|
||||
"M-c": map[string]any{"name": "C"},
|
||||
}
|
||||
Settings.Files.HDHR = map[string]any{
|
||||
"H-d": map[string]any{"name": "D", "buffer": "-"},
|
||||
}
|
||||
t.Cleanup(func() { Settings.Buffer = "-" })
|
||||
|
||||
cases := map[string]string{"M-a": "ffmpeg", "M-b": "xteve", "M-c": "xteve", "H-d": "-", "M-missing": "xteve", "": "xteve"}
|
||||
for id, want := range cases {
|
||||
if got := bufferModeFor(id); got != want {
|
||||
t.Errorf("%q: got %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -7,9 +7,9 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []interface{}, err error) {
|
||||
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []any, err error) {
|
||||
|
||||
var hdhrData []interface{}
|
||||
var hdhrData []any
|
||||
|
||||
err = json.Unmarshal(content, &hdhrData)
|
||||
if err == nil {
|
||||
@@ -17,7 +17,7 @@ func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []i
|
||||
for _, d := range hdhrData {
|
||||
|
||||
var channel = make(map[string]string)
|
||||
var data = d.(map[string]interface{})
|
||||
var data = d.(map[string]any)
|
||||
|
||||
channel["group-title"] = playlistName
|
||||
channel["name"] = data["GuideName"].(string)
|
||||
@@ -95,7 +95,7 @@ func getLineupStatus() (jsonContent []byte, err error) {
|
||||
lineupStatus.ScanInProgress = System.ScanInProgress
|
||||
lineupStatus.ScanPossible = 0
|
||||
lineupStatus.Source = "Cable"
|
||||
lineupStatus.SourceList = []string{"IPTV", "Cable"}
|
||||
lineupStatus.SourceList = []string{"Cable"}
|
||||
|
||||
jsonContent, err = json.MarshalIndent(lineupStatus, "", " ")
|
||||
|
||||
@@ -154,7 +154,7 @@ func getLineup() (jsonContent []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if xepgChannel.XActive == true {
|
||||
if xepgChannel.XActive {
|
||||
var stream LineupStream
|
||||
stream.GuideName = xepgChannel.XName
|
||||
stream.GuideNumber = xepgChannel.XChannelID
|
||||
@@ -176,7 +176,7 @@ func getLineup() (jsonContent []byte, err error) {
|
||||
|
||||
Data.Cache.PMS = nil
|
||||
|
||||
saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS)
|
||||
saveMapToJSONFile(System.File.URLS, streamingURLsSnapshot())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var htmlFolder string
|
||||
var goFile string
|
||||
var mapName string
|
||||
var packageName string
|
||||
|
||||
var blankMap = make(map[string]interface{})
|
||||
|
||||
// HTMLInit : Dateipfade festlegen
|
||||
// mapName = Name der zu erstellenden map
|
||||
// htmlFolder: Ordner der HTML Dateien
|
||||
// packageName: Name des package
|
||||
func HTMLInit(name, pkg, folder, file string) {
|
||||
|
||||
htmlFolder = folder
|
||||
goFile = file
|
||||
mapName = name
|
||||
packageName = pkg
|
||||
|
||||
}
|
||||
|
||||
// BuildGoFile : Erstellt das GO Dokument
|
||||
func BuildGoFile() error {
|
||||
|
||||
var err = checkHTMLFile(htmlFolder)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var content string
|
||||
content += `package ` + packageName + "\n\n"
|
||||
content += `var ` + mapName + ` = make(map[string]interface{})` + "\n\n"
|
||||
content += "func loadHTMLMap() {" + "\n\n"
|
||||
|
||||
content += createMapFromFiles(htmlFolder) + "\n"
|
||||
|
||||
content += "}" + "\n\n"
|
||||
writeStringToFile(goFile, content)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHTMLString : base64 -> string
|
||||
func GetHTMLString(base string) string {
|
||||
content, _ := base64.StdEncoding.DecodeString(base)
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func createMapFromFiles(folder string) string {
|
||||
|
||||
var path = getLocalPath(folder)
|
||||
|
||||
err := filepath.Walk(path, readFilesToMap)
|
||||
if err != nil {
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
var content string
|
||||
|
||||
for key := range blankMap {
|
||||
var newKey = key
|
||||
content += ` ` + mapName + `["` + newKey + `"` + `] = "` + blankMap[key].(string) + `"` + "\n"
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func readFilesToMap(path string, info os.FileInfo, err error) error {
|
||||
|
||||
if info.IsDir() == false {
|
||||
var base64Str = fileToBase64(getLocalPath(path))
|
||||
blankMap[path] = base64Str
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileToBase64(file string) string {
|
||||
|
||||
imgFile, _ := os.Open(file)
|
||||
defer imgFile.Close()
|
||||
|
||||
// create a new buffer base on file size
|
||||
fInfo, _ := imgFile.Stat()
|
||||
var size = fInfo.Size()
|
||||
buf := make([]byte, int64(size))
|
||||
|
||||
// read file content into buffer
|
||||
fReader := bufio.NewReader(imgFile)
|
||||
fReader.Read(buf)
|
||||
|
||||
imgBase64Str := base64.StdEncoding.EncodeToString(buf)
|
||||
|
||||
return imgBase64Str
|
||||
}
|
||||
|
||||
func getLocalPath(filename string) string {
|
||||
|
||||
path, file := filepath.Split(filename)
|
||||
var newPath = filepath.Dir(path)
|
||||
|
||||
var newFileName = newPath + "/" + file
|
||||
|
||||
return newFileName
|
||||
}
|
||||
|
||||
func writeStringToFile(filename, content string) error {
|
||||
|
||||
err := ioutil.WriteFile(getPlatformFile(filename), []byte(content), 0644)
|
||||
if err != nil {
|
||||
checkErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHTMLFile(filename string) error {
|
||||
|
||||
if _, err := os.Stat(getLocalPath(filename)); os.IsNotExist(err) {
|
||||
fmt.Println(filename)
|
||||
checkErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
log.Println("ERROR: [", err, "] in ", file, line)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// providerHTTPClient : Downloads of provider files (M3U, XMLTV, HDHR lineup).
|
||||
// Playlists and EPG files can be large, so the overall timeout is generous;
|
||||
// it only exists so a stalled provider cannot hang an update forever.
|
||||
var providerHTTPClient = &http.Client{Timeout: 5 * time.Minute}
|
||||
|
||||
// apiHTTPClient : Short request/response calls to other services (Plex API).
|
||||
var apiHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
+20
-155
@@ -1,170 +1,35 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getCacheImageURL(imageURL string) (cacheImageURL string) {
|
||||
|
||||
if Settings.CacheImages == false {
|
||||
return imageURL
|
||||
}
|
||||
|
||||
imageURL = strings.Trim(imageURL, "\r\n")
|
||||
|
||||
p, err := url.Parse(imageURL)
|
||||
if err != nil {
|
||||
// URL konnte nicht geparst werden, die ursprüngliche image url wird zurückgegeben
|
||||
showInfo(fmt.Sprintf("Image Caching:Image URL: %s", imageURL))
|
||||
showWarning(4101)
|
||||
return imageURL
|
||||
}
|
||||
var urlMD5 = getMD5(imageURL)
|
||||
var fileExtension = filepath.Ext(p.Path)
|
||||
|
||||
if len(fileExtension) == 0 {
|
||||
// Keine Dateierweiterung vorhanden, die ursprüngliche image url wird zurückgegeben
|
||||
return imageURL
|
||||
}
|
||||
|
||||
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesFiles) == -1 {
|
||||
Data.Cache.ImagesFiles = append(Data.Cache.ImagesFiles, urlMD5+fileExtension)
|
||||
}
|
||||
|
||||
if System.ImageCachingInProgress == 1 {
|
||||
return imageURL
|
||||
}
|
||||
|
||||
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesCache) != -1 {
|
||||
|
||||
cacheImageURL = fmt.Sprintf("%s://%s/images/%s%s", System.ServerProtocol.WEB, System.Domain, urlMD5, fileExtension)
|
||||
|
||||
} else {
|
||||
|
||||
if strings.Contains(imageURL, System.Domain+"/images/") == false {
|
||||
|
||||
if indexOfString(imageURL, Data.Cache.ImagesURLS) == -1 {
|
||||
Data.Cache.ImagesURLS = append(Data.Cache.ImagesURLS, imageURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
cacheImageURL = imageURL
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func cachingImages() {
|
||||
|
||||
if Settings.CacheImages == false || System.ImageCachingInProgress == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
System.ImageCachingInProgress = 1
|
||||
|
||||
showInfo("Image Caching:Images are cached")
|
||||
|
||||
for _, imageURL := range Data.Cache.ImagesURLS {
|
||||
|
||||
if len(imageURL) > 0 {
|
||||
cacheImage(imageURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
// Bilder die nicht mehr verwendet werden, werden gelöscht
|
||||
files, err := ioutil.ReadDir(System.Folder.ImagesCache)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
if indexOfString(file.Name(), Data.Cache.ImagesFiles) == -1 {
|
||||
|
||||
var debug = fmt.Sprintf("Image Caching:Remove file: %s %s %d", System.Folder.ImagesCache+file.Name(), file.Name(), len(file.Name()))
|
||||
showDebug(debug, 1)
|
||||
err := os.RemoveAll(System.Folder.ImagesCache + file.Name())
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func cacheImage(imageURL string) {
|
||||
|
||||
var debug string
|
||||
var urlMD5 = getMD5(imageURL)
|
||||
var fileExtension = filepath.Ext(imageURL)
|
||||
|
||||
debug = fmt.Sprintf("Image Caching:File: %s Download: %s", urlMD5+fileExtension, imageURL)
|
||||
showDebug(debug, 1)
|
||||
|
||||
resp, err := http.Get(imageURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
|
||||
var filePath = System.Folder.ImagesCache + urlMD5 + fileExtension
|
||||
|
||||
// Datei speichern
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func uploadLogo(input, filename string) (logoURL string, err error) {
|
||||
|
||||
b64data := input[strings.IndexByte(input, ',')+1:]
|
||||
filename, err = sanitizeUploadFilename(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// BAse64 in bytes umwandeln un speichern
|
||||
sDec, err := b64.StdEncoding.DecodeString(b64data)
|
||||
b64data := input[strings.IndexByte(input, ',')+1:]
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// BAse64 in bytes umwandeln un speichern
|
||||
sDec, err := b64.StdEncoding.DecodeString(b64data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var file = fmt.Sprintf("%s%s", System.Folder.ImagesUpload, filename)
|
||||
var file = fmt.Sprintf("%s%s", System.Folder.ImagesUpload, filename)
|
||||
|
||||
err = writeByteToFile(file, sDec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = writeByteToFile(file, sDec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.WEB, System.Domain, filename)
|
||||
logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.XML, System.Domain, filename)
|
||||
|
||||
return
|
||||
return
|
||||
|
||||
}
|
||||
+61
-65
@@ -1,99 +1,95 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ShowSystemInfo : Systeminformationen anzeigen
|
||||
func ShowSystemInfo() {
|
||||
|
||||
fmt.Print("Creating the information takes a moment...")
|
||||
err := buildDatabaseDVR()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
fmt.Print("Creating the information takes a moment...")
|
||||
err := buildDatabaseDVR()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
buildXEPG(false)
|
||||
buildXEPG(false)
|
||||
|
||||
fmt.Println("OK")
|
||||
println()
|
||||
fmt.Println("OK")
|
||||
println()
|
||||
|
||||
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
|
||||
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
|
||||
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
|
||||
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
|
||||
fmt.Printf("Version: %s %s.%s\n", System.Name, System.Version, System.Build)
|
||||
fmt.Printf("Branch: %s\n", System.Branch)
|
||||
fmt.Printf("Folder (config): %s\n", System.Folder.Config)
|
||||
|
||||
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
|
||||
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
|
||||
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
|
||||
fmt.Printf("Streams: %d / %d\n", len(Data.Streams.Active), len(Data.Streams.All))
|
||||
fmt.Printf("Filter: %d\n", len(Data.Filter))
|
||||
fmt.Printf("XEPG Chanels: %d\n", int(Data.XEPG.XEPGCount))
|
||||
|
||||
println()
|
||||
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
|
||||
println()
|
||||
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:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 100:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 10:
|
||||
fmt.Printf(" %d. %s\n", count, ipv4)
|
||||
case count < 100:
|
||||
fmt.Printf(" %d. %s\n", count, ipv4)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
println()
|
||||
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
|
||||
println()
|
||||
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:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 100:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 10:
|
||||
fmt.Printf(" %d. %s\n", count, ipv4)
|
||||
case count < 100:
|
||||
fmt.Printf(" %d. %s\n", count, ipv4)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [General]")
|
||||
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
|
||||
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
|
||||
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
|
||||
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
|
||||
fmt.Println("Settings [General]")
|
||||
fmt.Printf("UUID: %s\n", Settings.UUID)
|
||||
fmt.Printf("Tuner (Plex / Emby): %d\n", Settings.Tuner)
|
||||
fmt.Printf("EPG Source: %s\n", Settings.EpgSource)
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [Files]")
|
||||
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
|
||||
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
|
||||
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
|
||||
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
|
||||
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
|
||||
fmt.Println("Settings [Files]")
|
||||
fmt.Printf("Schedule: %s\n", strings.Join(Settings.Update, ","))
|
||||
fmt.Printf("Files Update: %t\n", Settings.FilesUpdate)
|
||||
fmt.Printf("Folder (tmp): %s\n", Settings.TempPath)
|
||||
fmt.Printf("Image Chaching: %t\n", Settings.CacheImages)
|
||||
fmt.Printf("Missing EPG Mode: %s\n", Settings.XepgMissingEPGMode)
|
||||
fmt.Printf("Replace EPG Image: %t\n", Settings.XepgReplaceMissingImages)
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [Streaming]")
|
||||
fmt.Println(fmt.Sprintf("Buffer: %t", Settings.Buffer))
|
||||
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
|
||||
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
|
||||
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
|
||||
fmt.Println("Settings [Streaming]")
|
||||
fmt.Printf("Buffer: %s\n", Settings.Buffer)
|
||||
fmt.Printf("UDPxy: %s\n", Settings.UDPxy)
|
||||
fmt.Printf("Buffer Size: %d KB\n", Settings.BufferSize)
|
||||
fmt.Printf("Timeout: %d ms\n", int(Settings.BufferTimeout))
|
||||
fmt.Printf("User Agent: %s\n", Settings.UserAgent)
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [Backup]")
|
||||
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
|
||||
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
|
||||
fmt.Println("Settings [Backup]")
|
||||
fmt.Printf("Folder (backup): %s\n", Settings.BackupPath)
|
||||
fmt.Printf("Backup Keep: %d\n", Settings.BackupKeep)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteFileAtomic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "authentication.json")
|
||||
|
||||
if err := writeFileAtomic(file, []byte("{\"a\":1}"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeFileAtomic(file, []byte("{\"a\":2}"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "{\"a\":2}" {
|
||||
t.Errorf("content = %q", got)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Mode().Perm() != 0600 {
|
||||
t.Errorf("mode = %o, want 0600", fi.Mode().Perm())
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Errorf("expected only the target file, found %d entries", len(entries))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,130 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := Init(t.TempDir()+"/authentication.json", 60); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUsersUseBcrypt(t *testing.T) {
|
||||
setup(t)
|
||||
id, err := CreateNewUser("alice", "pw1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := data["users"].(map[string]any)[id].(map[string]any)
|
||||
if !strings.HasPrefix(rec["_password"].(string), "$2") {
|
||||
t.Fatalf("password not bcrypt: %q", rec["_password"])
|
||||
}
|
||||
if _, err := AuthenticateUser("alice", "pw1"); err != nil {
|
||||
t.Errorf("valid login failed: %v", err)
|
||||
}
|
||||
if _, err := AuthenticateUser("alice", "wrong"); err == nil {
|
||||
t.Error("wrong password accepted")
|
||||
}
|
||||
if _, err := AuthenticateUser("bob", "pw1"); err == nil {
|
||||
t.Error("unknown user accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySHA256RecordIsUpgradedOnLogin(t *testing.T) {
|
||||
setup(t)
|
||||
id, err := CreateNewUser("legacy", "oldpw")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := data["users"].(map[string]any)[id].(map[string]any)
|
||||
salt := rec["_salt"].(string)
|
||||
rec["_password"] = SHA256("oldpw", salt) // what upstream stored
|
||||
|
||||
if _, err := AuthenticateUser("legacy", "nope"); err == nil {
|
||||
t.Fatal("wrong password accepted against legacy record")
|
||||
}
|
||||
if !strings.HasPrefix(rec["_password"].(string), "$2") == false {
|
||||
t.Fatal("record must not be upgraded on a failed login")
|
||||
}
|
||||
|
||||
if _, err := AuthenticateUser("legacy", "oldpw"); err != nil {
|
||||
t.Fatalf("legacy login failed: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(rec["_password"].(string), "$2") {
|
||||
t.Fatalf("record not upgraded to bcrypt: %q", rec["_password"])
|
||||
}
|
||||
if _, err := AuthenticateUser("legacy", "oldpw"); err != nil {
|
||||
t.Errorf("login after upgrade failed: %v", err)
|
||||
}
|
||||
|
||||
// The upgrade must have been persisted.
|
||||
if err := loadDatabase(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec = data["users"].(map[string]any)[id].(map[string]any)
|
||||
if !strings.HasPrefix(rec["_password"].(string), "$2") {
|
||||
t.Fatal("upgraded hash was not saved to disk")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeCredentialsUsesBcrypt(t *testing.T) {
|
||||
setup(t)
|
||||
id, _ := CreateNewUser("carol", "one")
|
||||
if err := ChangeCredentials(id, "", "two"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := AuthenticateUser("carol", "one"); err == nil {
|
||||
t.Error("old password still works")
|
||||
}
|
||||
if _, err := AuthenticateUser("carol", "two"); err != nil {
|
||||
t.Errorf("new password rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredTokensAreEvicted(t *testing.T) {
|
||||
setup(t)
|
||||
id, _ := CreateNewUser("dave", "pw")
|
||||
|
||||
tokensMu.Lock()
|
||||
tokens = make(map[string]any)
|
||||
tokens["stale"] = map[string]any{"id": id, "expires": time.Now().Local().Add(-time.Minute)}
|
||||
tokens["fresh"] = map[string]any{"id": id, "expires": time.Now().Local().Add(time.Hour)}
|
||||
tokensMu.Unlock()
|
||||
|
||||
tok := setToken(id, "-")
|
||||
|
||||
tokensMu.RLock()
|
||||
defer tokensMu.RUnlock()
|
||||
if _, ok := tokens["stale"]; ok {
|
||||
t.Error("expired token was not evicted")
|
||||
}
|
||||
if _, ok := tokens["fresh"]; !ok {
|
||||
t.Error("valid token was evicted")
|
||||
}
|
||||
if _, ok := tokens[tok]; !ok {
|
||||
t.Error("new token missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateUserDoesNotMintTokens(t *testing.T) {
|
||||
setup(t)
|
||||
CreateNewUser("erin", "pw")
|
||||
tokensMu.Lock()
|
||||
tokens = make(map[string]any)
|
||||
tokensMu.Unlock()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, err := AuthenticateUser("erin", "pw"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
tokensMu.RLock()
|
||||
defer tokensMu.RUnlock()
|
||||
if len(tokens) != 0 {
|
||||
t.Errorf("per-request authentication created %d tokens", len(tokens))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// httpClient : Client for logo downloads. Providers occasionally serve
|
||||
// images from hosts that never answer; without a timeout the caching
|
||||
// goroutine would hang forever.
|
||||
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
// Cache : Cache strcut
|
||||
type Cache struct {
|
||||
path string
|
||||
cacheURL string
|
||||
caching bool
|
||||
images map[string]string
|
||||
Queue []string
|
||||
Cache []string
|
||||
Image imageFunc
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type imageFunc struct {
|
||||
GetURL func(string) string
|
||||
Caching func()
|
||||
Remove func()
|
||||
}
|
||||
|
||||
// New : New cahce
|
||||
func New(path, chacheURL string, caching bool) (c *Cache, err error) {
|
||||
|
||||
c = &Cache{}
|
||||
|
||||
c.images = make(map[string]string)
|
||||
c.path = path
|
||||
c.cacheURL = chacheURL
|
||||
c.caching = caching
|
||||
c.Queue = []string{}
|
||||
c.Cache = []string{}
|
||||
|
||||
c.Image.GetURL = func(src string) (cacheURL string) {
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
src = strings.Trim(src, "\r\n")
|
||||
|
||||
if !c.caching {
|
||||
return src
|
||||
}
|
||||
|
||||
u, err := url.Parse(src)
|
||||
if err != nil || len(filepath.Ext(u.Path)) == 0 {
|
||||
return src
|
||||
}
|
||||
|
||||
var filename = fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))
|
||||
if cacheURL, ok := c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))]; ok {
|
||||
return cacheURL
|
||||
}
|
||||
|
||||
if indexOfString(filename, c.Cache) == -1 {
|
||||
|
||||
if indexOfString(src, c.Queue) == -1 {
|
||||
c.Queue = append(c.Queue, src)
|
||||
}
|
||||
|
||||
} else {
|
||||
c.images[filename] = c.cacheURL + filename
|
||||
src = c.cacheURL + filename
|
||||
}
|
||||
|
||||
/*
|
||||
if _, err := os.Stat(c.path + filename); err != nil {
|
||||
//c.images[filename] = c.cacheURL + filename
|
||||
if indexOfString(src, c.Queue) == -1 {
|
||||
c.Queue = append(c.Queue, src)
|
||||
}
|
||||
} else {
|
||||
c.images[filename] = c.cacheURL + filename
|
||||
}
|
||||
*/
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
// Caching downloads every queued image. The lock is only held while the
|
||||
// queue is copied and while the map / queue are updated per image; the
|
||||
// HTTP download and file write happen unlocked so GetURL callers (M3U /
|
||||
// XMLTV generation) are not blocked for the duration of the downloads.
|
||||
c.Image.Caching = func() {
|
||||
|
||||
c.Lock()
|
||||
var queue = make([]string, len(c.Queue))
|
||||
copy(queue, c.Queue)
|
||||
c.Unlock()
|
||||
|
||||
for _, src := range queue {
|
||||
|
||||
filename, err := c.download(src)
|
||||
if err != nil {
|
||||
// Stays in the queue and is retried on the next run.
|
||||
continue
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.images[filename] = c.cacheURL + filename
|
||||
c.Queue = removeStringFromSlice(src, c.Queue)
|
||||
c.Unlock()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
c.Image.Remove = func() {
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
files, err := os.ReadDir(c.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
switch c.caching {
|
||||
|
||||
case true:
|
||||
if _, ok := c.images[file.Name()]; !ok {
|
||||
os.RemoveAll(c.path + file.Name())
|
||||
}
|
||||
|
||||
case false:
|
||||
os.RemoveAll(c.path + file.Name())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(c.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
c.Cache = append(c.Cache, file.Name())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// download : Fetches src into the cache folder and returns the cached file
|
||||
// name (md5 of the URL plus the extension of the URL path). Must be called
|
||||
// without holding the lock.
|
||||
func (c *Cache) download(src string) (filename string, err error) {
|
||||
|
||||
u, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
filename = fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))
|
||||
|
||||
resp, err := httpClient.Get(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("%s: %s", src, resp.Status)
|
||||
return
|
||||
}
|
||||
|
||||
var path = filepath.Join(c.path, filename)
|
||||
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if closeErr := file.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
os.Remove(path)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func strToMD5(str string) string {
|
||||
md5Hasher := md5.New()
|
||||
md5Hasher.Write([]byte(str))
|
||||
return hex.EncodeToString(md5Hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func indexOfString(str string, slice []string) int {
|
||||
|
||||
for i, v := range slice {
|
||||
if str == v {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func removeStringFromSlice(str string, slice []string) []string {
|
||||
|
||||
var i = indexOfString(str, slice)
|
||||
|
||||
if i != -1 {
|
||||
slice = append(slice[:i], slice[i+1:]...)
|
||||
}
|
||||
|
||||
return slice
|
||||
}
|
||||
@@ -1,84 +1,84 @@
|
||||
package m3u
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type M3UStream struct {
|
||||
GroupTitle string `json:"group-title,required"`
|
||||
Name string `json:"name,required"`
|
||||
TvgID string `json:"tvg-id,required"`
|
||||
TvgLogo string `json:"tvg-logo,required"`
|
||||
TvgName string `json:"tvg-name,required"`
|
||||
URL string `json:"url,required"`
|
||||
UUIDKey string `json:"_uuid.key,omitempty"`
|
||||
UUIDValue string `json:"_uuid.value,omitempty"`
|
||||
GroupTitle string `json:"group-title"`
|
||||
Name string `json:"name"`
|
||||
TvgID string `json:"tvg-id"`
|
||||
TvgLogo string `json:"tvg-logo"`
|
||||
TvgName string `json:"tvg-name"`
|
||||
URL string `json:"url"`
|
||||
UUIDKey string `json:"_uuid.key,omitempty"`
|
||||
UUIDValue string `json:"_uuid.value,omitempty"`
|
||||
}
|
||||
|
||||
func TestStream1(t *testing.T) {
|
||||
|
||||
var file = "test_list_1.m3u"
|
||||
var content, err = ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
var file = "test_list_1.m3u"
|
||||
var content, err = os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
streams, err := MakeInterfaceFromM3U(content)
|
||||
streams, err := MakeInterfaceFromM3U(content)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = checkStream(streams)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkStream(streams)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Println("Streams:", len(streams))
|
||||
t.Log(streams)
|
||||
fmt.Println("Streams:", len(streams))
|
||||
t.Log(streams)
|
||||
|
||||
}
|
||||
|
||||
func checkStream(streamInterface []interface{}) (err error) {
|
||||
func checkStream(streamInterface []any) (err error) {
|
||||
|
||||
for i, s := range streamInterface {
|
||||
for i, s := range streamInterface {
|
||||
|
||||
var stream = s.(map[string]string)
|
||||
var m3uStream M3UStream
|
||||
var stream = s.(map[string]string)
|
||||
var m3uStream M3UStream
|
||||
|
||||
jsonString, err := json.MarshalIndent(stream, "", " ")
|
||||
jsonString, err := json.MarshalIndent(stream, "", " ")
|
||||
|
||||
if err == nil {
|
||||
if err == nil {
|
||||
|
||||
err = json.Unmarshal(jsonString, &m3uStream)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(jsonString, &m3uStream)
|
||||
if err == nil {
|
||||
|
||||
log.Print(fmt.Sprintf("Stream: %d", i))
|
||||
log.Print(fmt.Sprintf("Name*: %s", m3uStream.Name))
|
||||
log.Print(fmt.Sprintf("URL*: %s", m3uStream.URL))
|
||||
log.Print(fmt.Sprintf("tvg-name: %s", m3uStream.TvgName))
|
||||
log.Print(fmt.Sprintf("tvg-id**: %s", m3uStream.TvgID))
|
||||
log.Print(fmt.Sprintf("tvg-logo: %s", m3uStream.TvgLogo))
|
||||
log.Print(fmt.Sprintf("group-title**: %s", m3uStream.GroupTitle))
|
||||
log.Printf("Stream: %d", i)
|
||||
log.Printf("Name*: %s", m3uStream.Name)
|
||||
log.Printf("URL*: %s", m3uStream.URL)
|
||||
log.Printf("tvg-name: %s", m3uStream.TvgName)
|
||||
log.Printf("tvg-id**: %s", m3uStream.TvgID)
|
||||
log.Printf("tvg-logo: %s", m3uStream.TvgLogo)
|
||||
log.Printf("group-title**: %s", m3uStream.GroupTitle)
|
||||
|
||||
if len(m3uStream.UUIDKey) > 0 {
|
||||
log.Print(fmt.Sprintf("UUID key***: %s", m3uStream.UUIDKey))
|
||||
log.Print(fmt.Sprintf("UUID value: %s", m3uStream.UUIDValue))
|
||||
} else {
|
||||
log.Print(fmt.Sprintf("UUID key: false"))
|
||||
}
|
||||
if len(m3uStream.UUIDKey) > 0 {
|
||||
log.Printf("UUID key***: %s", m3uStream.UUIDKey)
|
||||
log.Printf("UUID value: %s", m3uStream.UUIDValue)
|
||||
} else {
|
||||
log.Print("UUID key: false")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(fmt.Sprintf("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -"))
|
||||
}
|
||||
log.Println("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -")
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
#EXTM3U url-tvg="http://example.com/file.xml" x-tvg-url="http://example.com/xteve.xml"
|
||||
#EXTINF:0 channelID="1" tvg-chno="1" tvg-name="Channel.1" tvg-id="tvg.id.1" tvg-logo="https://example/logo.png" group-title="Group 1",Channel 1
|
||||
#EXTINF:0 channelID="1" tvg-chno="1" tvg-name="Channel.1" tvg-id="tvg.id.1" tvg-logo="https://example/logo.png" group-title="Group 1", Channel 1
|
||||
http://example.com/stream/1
|
||||
|
||||
#EXTINF:0 channelID="2" tvg-chno="2" tvg-name="Channel.2" tvg-id="tvg.id.2" tvg-logo="https://example/logo.png" group-title="Group 2",Channel 2
|
||||
#123
|
||||
http://example.com/stream/2
|
||||
|
||||
#EXTINF:123, Sample artist - Sample title
|
||||
http://example.com/stream/3
|
||||
|
||||
#EXTINF:321,Example Artist - Example title
|
||||
http://example.com/stream/4
|
||||
@@ -1,162 +1,185 @@
|
||||
package m3u
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"errors"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MakeInterfaceFromM3U :
|
||||
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []interface{}, err error) {
|
||||
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []any, err error) {
|
||||
|
||||
var content = string(byteStream)
|
||||
var channelName string
|
||||
var content = string(byteStream)
|
||||
var channelName string
|
||||
var uuids []string
|
||||
|
||||
var parseMetaData = func(channel string) (stream map[string]string) {
|
||||
var parseMetaData = func(channel string) (stream map[string]string) {
|
||||
|
||||
stream = make(map[string]string)
|
||||
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
|
||||
var exceptForChannelName = `,([^\n]*|,[^\r]*)`
|
||||
stream = make(map[string]string)
|
||||
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
|
||||
var exceptForChannelName = `,([^\n]*|,[^\r]*)`
|
||||
|
||||
var lines = strings.Split(strings.Replace(channel, "\r\n", "\n", -1), "\n")
|
||||
var lines = strings.Split(strings.Replace(channel, "\r\n", "\n", -1), "\n")
|
||||
|
||||
// Zeilen mit # und leerer Zeilen entfernen
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
// Zeilen mit # und leerer Zeilen entfernen
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
|
||||
if len(lines[i]) == 0 || lines[i][0:1] == "#" {
|
||||
lines = append(lines[:i], lines[i+1:]...)
|
||||
}
|
||||
if len(lines[i]) == 0 || lines[i][0:1] == "#" {
|
||||
lines = append(lines[:i], lines[i+1:]...)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) >= 2 {
|
||||
if len(lines) >= 2 {
|
||||
|
||||
for _, line := range lines {
|
||||
for _, line := range lines {
|
||||
|
||||
_, err := url.ParseRequestURI(line)
|
||||
_, err := url.ParseRequestURI(line)
|
||||
|
||||
switch err {
|
||||
switch err {
|
||||
|
||||
case nil:
|
||||
stream["url"] = strings.Trim(line, "\r\n")
|
||||
case nil:
|
||||
stream["url"] = strings.Trim(line, "\r\n")
|
||||
|
||||
default:
|
||||
default:
|
||||
|
||||
var value string
|
||||
// Alle Parameter parsen
|
||||
var p = regexp.MustCompile(exceptForParameter)
|
||||
var streamParameter = p.FindAllString(line, -1)
|
||||
var value string
|
||||
// Alle Parameter parsen
|
||||
var p = regexp.MustCompile(exceptForParameter)
|
||||
var streamParameter = p.FindAllString(line, -1)
|
||||
|
||||
for _, p := range streamParameter {
|
||||
for _, p := range streamParameter {
|
||||
|
||||
line = strings.Replace(line, p, "", 1)
|
||||
line = strings.Replace(line, p, "", 1)
|
||||
|
||||
p = strings.Replace(p, `"`, "", -1)
|
||||
var parameter = strings.Split(p, "=")
|
||||
p = strings.Replace(p, `"`, "", -1)
|
||||
var parameter = strings.SplitN(p, "=", 2)
|
||||
|
||||
if len(parameter) == 2 {
|
||||
if len(parameter) == 2 {
|
||||
|
||||
// TVG Key als Kleinbuchstaben speichern
|
||||
switch strings.Contains(parameter[0], "tvg") {
|
||||
// TVG Key als Kleinbuchstaben speichern
|
||||
switch strings.Contains(parameter[0], "tvg") {
|
||||
|
||||
case true:
|
||||
stream[strings.ToLower(parameter[0])] = parameter[1]
|
||||
case false:
|
||||
stream[parameter[0]] = parameter[1]
|
||||
case true:
|
||||
stream[strings.ToLower(parameter[0])] = parameter[1]
|
||||
case false:
|
||||
stream[parameter[0]] = parameter[1]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// URL's nicht an die Filterfunktion übergeben
|
||||
if !strings.Contains(parameter[1], "://") && len(parameter[1]) > 0 {
|
||||
value = value + parameter[1] + " "
|
||||
}
|
||||
// URL's nicht an die Filterfunktion übergeben
|
||||
if !strings.Contains(parameter[1], "://") && len(parameter[1]) > 0 {
|
||||
value = value + parameter[1] + " "
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Kanalnamen parsen
|
||||
n := regexp.MustCompile(exceptForChannelName)
|
||||
var name = n.FindAllString(line, 1)
|
||||
// Kanalnamen parsen
|
||||
n := regexp.MustCompile(exceptForChannelName)
|
||||
var name = n.FindAllString(line, 1)
|
||||
|
||||
if len(name) > 0 {
|
||||
channelName = name[0]
|
||||
channelName = strings.Replace(channelName, `,`, "", 1)
|
||||
channelName = strings.TrimRight(channelName, "\r\n")
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
}
|
||||
if len(name) > 0 {
|
||||
channelName = name[0]
|
||||
channelName = strings.Replace(channelName, `,`, "", 1)
|
||||
channelName = strings.TrimRight(channelName, "\r\n")
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
}
|
||||
|
||||
if len(channelName) == 0 {
|
||||
if len(channelName) == 0 {
|
||||
|
||||
if v, ok := stream["tvg-name"]; ok {
|
||||
channelName = v
|
||||
}
|
||||
if v, ok := stream["tvg-name"]; ok {
|
||||
channelName = v
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
|
||||
// Kanäle ohne Namen werden augelassen
|
||||
if len(channelName) == 0 {
|
||||
return
|
||||
}
|
||||
// Kanäle ohne Namen werden augelassen
|
||||
if len(channelName) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
stream["name"] = channelName
|
||||
value = value + channelName
|
||||
stream["name"] = channelName
|
||||
value = value + channelName
|
||||
|
||||
stream["_values"] = value
|
||||
stream["_values"] = value
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Nach eindeutiger ID im Stream suchen
|
||||
for key, value := range stream {
|
||||
// Nach eindeutiger ID im Stream suchen
|
||||
for key, value := range stream {
|
||||
|
||||
if !strings.Contains(strings.ToLower(key), "tvg-id") {
|
||||
if !strings.Contains(strings.ToLower(key), "tvg-id") {
|
||||
|
||||
if strings.Contains(strings.ToLower(key), "id") {
|
||||
if strings.Contains(strings.ToLower(key), "id") {
|
||||
|
||||
stream["_uuid.key"] = key
|
||||
stream["_uuid.value"] = value
|
||||
//os.Exit(0)
|
||||
break
|
||||
if indexOfString(value, uuids) != -1 {
|
||||
log.Printf("Channel: %s - %s = %s ", stream["name"], key, value)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
uuids = append(uuids, value)
|
||||
|
||||
}
|
||||
stream["_uuid.key"] = key
|
||||
stream["_uuid.value"] = value
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Println(content)
|
||||
}
|
||||
|
||||
if strings.Contains(content, "#EXTM3U") {
|
||||
return
|
||||
}
|
||||
|
||||
var channels = strings.Split(content, "#EXTINF")
|
||||
//fmt.Println(content)
|
||||
if strings.Contains(content, "#EXT-X-TARGETDURATION") || strings.Contains(content, "#EXT-X-MEDIA-SEQUENCE") {
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
return
|
||||
}
|
||||
|
||||
channels = append(channels[:0], channels[1:]...)
|
||||
if strings.Contains(content, "#EXTM3U") {
|
||||
|
||||
for _, channel := range channels {
|
||||
var channels = strings.Split(content, "#EXTINF")
|
||||
|
||||
var stream = parseMetaData(channel)
|
||||
channels = append(channels[:0], channels[1:]...)
|
||||
|
||||
if len(stream) > 0 && stream != nil {
|
||||
allChannels = append(allChannels, stream)
|
||||
}
|
||||
for _, channel := range channels {
|
||||
|
||||
}
|
||||
var stream = parseMetaData(channel)
|
||||
|
||||
} else {
|
||||
if len(stream) > 0 && stream != nil {
|
||||
allChannels = append(allChannels, stream)
|
||||
}
|
||||
|
||||
err = errors.New("No valid m3u file")
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
|
||||
return
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func indexOfString(element string, data []string) int {
|
||||
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package up2date
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClientInfo : Information about the key (NAME OS, ARCH, UUID, KEY)
|
||||
type ClientInfo struct {
|
||||
Arch string `json:"arch,required"`
|
||||
Branch string `json:"branch,required"`
|
||||
CMD string `json:"cmd,omitempty"`
|
||||
Name string `json:"name,required"`
|
||||
OS string `json:"os,required"`
|
||||
URL string `json:"url,required"`
|
||||
|
||||
Response ServerResponse `json:"response,omitempty"`
|
||||
}
|
||||
|
||||
//ServerResponse : Response from server after client request
|
||||
type ServerResponse struct {
|
||||
Status bool `json:"status,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
UpdateBIN string `json:"update.url.bin,omitempty"`
|
||||
UpdateZIP string `json:"update.url.zip,omitempty"`
|
||||
Filename string `json:"filename.bin,omitempty"`
|
||||
}
|
||||
|
||||
// Updater : Client infos
|
||||
var Updater ClientInfo
|
||||
|
||||
// UpdateURL : URL for the new binary
|
||||
var UpdateURL string
|
||||
|
||||
// Init : Init
|
||||
func Init() {
|
||||
Updater.OS = runtime.GOOS
|
||||
Updater.Arch = runtime.GOARCH
|
||||
}
|
||||
|
||||
// GetVersion : Information about the latest version
|
||||
func GetVersion() (err error) {
|
||||
|
||||
Updater.CMD = "getVersion"
|
||||
err = serverRequest()
|
||||
return
|
||||
}
|
||||
|
||||
func serverRequest() (err error) {
|
||||
|
||||
var serverResponse ServerResponse
|
||||
jsonByte, err := json.MarshalIndent(Updater, "", " ")
|
||||
if err == nil {
|
||||
|
||||
// Serververbindung prüfen
|
||||
u, err := url.Parse(Updater.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var server = u.Host
|
||||
|
||||
timeout := time.Duration(1 * time.Second)
|
||||
_, err = net.DialTimeout("tcp", server, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check redirect 301 <---> 308
|
||||
redirect, err := http.NewRequest("POST", Updater.URL, nil)
|
||||
|
||||
client := &http.Client{}
|
||||
client.CheckRedirect = func(redirect *http.Request, via []*http.Request) error {
|
||||
return errors.New("Redirect")
|
||||
}
|
||||
|
||||
resp, err := client.Do(redirect)
|
||||
|
||||
if err != nil {
|
||||
// Redirect
|
||||
if resp.StatusCode >= 301 && resp.StatusCode <= 308 { //status code 301 <---> 308
|
||||
Updater.URL = resp.Header.Get("Location")
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// ---
|
||||
|
||||
req, err := http.NewRequest("POST", Updater.URL, bytes.NewBuffer(jsonByte))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client = &http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
//fmt.Println(resp.StatusCode, Updater.URL, Updater.CMD)
|
||||
err = fmt.Errorf(fmt.Sprintf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), Updater.URL))
|
||||
return err
|
||||
}
|
||||
|
||||
Updater.CMD = ""
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
err = json.Unmarshal(body, &serverResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Updater.Response = serverResponse
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
package up2date
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
)
|
||||
|
||||
// DoUpdate : Update binary
|
||||
func DoUpdate(fileType, filenameBIN string) (err error) {
|
||||
|
||||
var url string
|
||||
switch fileType {
|
||||
case "bin":
|
||||
url = Updater.Response.UpdateBIN
|
||||
case "zip":
|
||||
url = Updater.Response.UpdateZIP
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
filenameBIN = filenameBIN + ".exe"
|
||||
}
|
||||
|
||||
if len(url) > 0 {
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "New version ("+Updater.Name+"):", Updater.Response.Version)
|
||||
|
||||
// Download new binary
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Download new version...")
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Download new version...OK")
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Change binary filename to .filename
|
||||
binary, err := osext.Executable()
|
||||
var filename = getFilenameFromPath(binary)
|
||||
var path = getPlatformPath(binary)
|
||||
var oldBinary = path + "_old_" + filename
|
||||
var newBinary = binary
|
||||
|
||||
// ZIP
|
||||
var tmpFolder = path + "tmp"
|
||||
var tmpFile = tmpFolder + string(os.PathSeparator) + filenameBIN
|
||||
|
||||
//fmt.Println(binary, path+"."+filename)
|
||||
os.Rename(newBinary, oldBinary)
|
||||
|
||||
// Save the new binary with the old file name
|
||||
out, err := os.Create(binary)
|
||||
if err != nil {
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Write the body to file
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update as a ZIP file
|
||||
if fileType == "zip" {
|
||||
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Update file:", filenameBIN)
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...")
|
||||
err = extractZIP(binary, tmpFolder)
|
||||
|
||||
binary = newBinary
|
||||
|
||||
if err != nil {
|
||||
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...ERROR")
|
||||
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
|
||||
return err
|
||||
} else {
|
||||
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...OK")
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...")
|
||||
|
||||
err = copyFile(tmpFile, binary)
|
||||
if err == nil {
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...OK")
|
||||
} else {
|
||||
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...ERROR")
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
os.RemoveAll(tmpFolder)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Set the permission
|
||||
err = os.Chmod(binary, 0755)
|
||||
|
||||
// Close the new file !Windows
|
||||
out.Close()
|
||||
|
||||
log.Println("["+strings.ToUpper(fileType)+"]", "Update Successful")
|
||||
|
||||
// Restart binary (Windows)
|
||||
if runtime.GOOS == "windows" {
|
||||
|
||||
bin, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
return err
|
||||
}
|
||||
|
||||
var pid = os.Getpid()
|
||||
var process, _ = os.FindProcess(pid)
|
||||
|
||||
if proc, err := start(bin); err == nil {
|
||||
|
||||
os.RemoveAll(oldBinary)
|
||||
process.Kill()
|
||||
proc.Wait()
|
||||
|
||||
} else {
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Restart binary (Linux and UNIX)
|
||||
file, _ := osext.Executable()
|
||||
os.RemoveAll(oldBinary)
|
||||
err = syscall.Exec(file, os.Args, os.Environ())
|
||||
if err != nil {
|
||||
restorOldBinary(oldBinary, newBinary)
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func start(args ...string) (p *os.Process, err error) {
|
||||
|
||||
if args[0], err = exec.LookPath(args[0]); err == nil {
|
||||
//fmt.Println(args[0])
|
||||
var procAttr os.ProcAttr
|
||||
procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
|
||||
p, err := os.StartProcess(args[0], args, &procAttr)
|
||||
|
||||
if err == nil {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func restorOldBinary(oldBinary, newBinary string) {
|
||||
os.RemoveAll(newBinary)
|
||||
os.Rename(oldBinary, newBinary)
|
||||
}
|
||||
|
||||
func getPlatformFile(filename string) string {
|
||||
|
||||
path, file := filepath.Split(filename)
|
||||
var newPath = filepath.Dir(path)
|
||||
var newFileName = newPath + string(os.PathSeparator) + file
|
||||
|
||||
return newFileName
|
||||
}
|
||||
|
||||
func getFilenameFromPath(path string) string {
|
||||
|
||||
file := filepath.Base(path)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func getPlatformPath(path string) string {
|
||||
|
||||
var newPath = filepath.Dir(path) + string(os.PathSeparator)
|
||||
|
||||
return newPath
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func extractZIP(archive, target string) (err error) {
|
||||
|
||||
reader, err := zip.OpenReader(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
|
||||
path := filepath.Join(target, file.Name)
|
||||
if file.FileInfo().IsDir() {
|
||||
os.MkdirAll(path, file.Mode())
|
||||
continue
|
||||
}
|
||||
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer targetFile.Close()
|
||||
|
||||
if _, err := io.Copy(targetFile, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+65
-14
@@ -8,12 +8,13 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
m3u "../src/internal/m3u-parser"
|
||||
m3u "xteve/src/internal/m3u-parser"
|
||||
)
|
||||
|
||||
// Playlisten parsen
|
||||
func parsePlaylist(filename, fileType string) (channels []interface{}, err error) {
|
||||
func parsePlaylist(filename, fileType string) (channels []any, err error) {
|
||||
|
||||
content, err := readByteFromFile(filename)
|
||||
var id = strings.TrimSuffix(getFilenameFromPath(filename), path.Ext(getFilenameFromPath(filename)))
|
||||
@@ -34,21 +35,71 @@ func parsePlaylist(filename, fileType string) (channels []interface{}, err error
|
||||
}
|
||||
|
||||
// Streams filtern
|
||||
func filterThisStream(s interface{}) (status bool) {
|
||||
// Patterns for the {include} and !{exclude} parts of a filter rule.
|
||||
var filterIncludeRe = regexp.MustCompile(`[{]+[^.]+[}]`)
|
||||
var filterExcludeRe = regexp.MustCompile(`!+[{]+[^.]+[}]`)
|
||||
|
||||
// regexFilterCache : compiled "regex-filter" rules, keyed by rule and case mode.
|
||||
var regexFilterCache sync.Map
|
||||
|
||||
// compileRegexFilter : compiles (and caches) a regex-filter rule. Case
|
||||
// insensitive rules get the (?i) flag. Returns nil for an invalid pattern.
|
||||
func compileRegexFilter(rule string, caseSensitive bool) *regexp.Regexp {
|
||||
|
||||
var key = fmt.Sprintf("%t:%s", caseSensitive, rule)
|
||||
|
||||
if v, ok := regexFilterCache.Load(key); ok {
|
||||
return v.(*regexp.Regexp)
|
||||
}
|
||||
|
||||
var pattern = rule
|
||||
if !caseSensitive {
|
||||
pattern = "(?i)" + rule
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
regexFilterCache.Store(key, re)
|
||||
return re
|
||||
}
|
||||
|
||||
// validateRegexFilter : error for an invalid regex-filter rule.
|
||||
func validateRegexFilter(rule string) error {
|
||||
_, err := regexp.Compile(rule)
|
||||
return err
|
||||
}
|
||||
|
||||
func filterThisStream(s any) (status bool) {
|
||||
|
||||
status = false
|
||||
var stream = s.(map[string]string)
|
||||
var regexpYES = `[{]+[^.]+[}]`
|
||||
var regexpNO = `!+[{]+[^.]+[}]`
|
||||
|
||||
for _, filter := range Data.Filter {
|
||||
|
||||
if filter.Rule == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var group, name, search string
|
||||
var exclude, include string
|
||||
var match = false
|
||||
|
||||
var streamValues = strings.Replace(stream["_values"], "\r", "", -1)
|
||||
|
||||
// Regular expression, tried against the channel name, then the group
|
||||
// title, then the raw attribute line (so anchors work on the name).
|
||||
if filter.Type == "regex-filter" {
|
||||
if re := compileRegexFilter(filter.Rule, filter.CaseSensitive); re != nil {
|
||||
if re.MatchString(stream["name"]) || re.MatchString(stream["group-title"]) || re.MatchString(streamValues) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if v, ok := stream["group-title"]; ok {
|
||||
group = v
|
||||
}
|
||||
@@ -58,8 +109,7 @@ func filterThisStream(s interface{}) (status bool) {
|
||||
}
|
||||
|
||||
// Unerwünschte Streams !{DEU}
|
||||
r := regexp.MustCompile(regexpNO)
|
||||
val := r.FindStringSubmatch(filter.Rule)
|
||||
val := filterExcludeRe.FindStringSubmatch(filter.Rule)
|
||||
|
||||
if len(val) == 1 {
|
||||
|
||||
@@ -70,8 +120,7 @@ func filterThisStream(s interface{}) (status bool) {
|
||||
}
|
||||
|
||||
// Muss zusätzlich erfüllt sein {DEU}
|
||||
r = regexp.MustCompile(regexpYES)
|
||||
val = r.FindStringSubmatch(filter.Rule)
|
||||
val = filterIncludeRe.FindStringSubmatch(filter.Rule)
|
||||
|
||||
if len(val) == 1 {
|
||||
|
||||
@@ -110,18 +159,18 @@ func filterThisStream(s interface{}) (status bool) {
|
||||
}
|
||||
}
|
||||
|
||||
if match == true {
|
||||
if match {
|
||||
|
||||
if len(exclude) > 0 {
|
||||
var status = checkConditions(search, exclude, "exclude")
|
||||
if status == false {
|
||||
if !status {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(include) > 0 {
|
||||
var status = checkConditions(search, include, "include")
|
||||
if status == false {
|
||||
if !status {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -177,6 +226,7 @@ func checkConditions(streamValues, conditions, coType string) (status bool) {
|
||||
// xTeVe M3U Datei erstellen
|
||||
func buildM3U(groups []string) (m3u string, err error) {
|
||||
|
||||
var imgc = Data.Cache.Images
|
||||
var m3uChannels = make(map[float64]XEPGChannelStruct)
|
||||
var channelNumbers []float64
|
||||
|
||||
@@ -186,7 +236,7 @@ func buildM3U(groups []string) (m3u string, err error) {
|
||||
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
if err == nil {
|
||||
|
||||
if xepgChannel.XActive == true {
|
||||
if xepgChannel.XActive {
|
||||
|
||||
if len(groups) > 0 {
|
||||
|
||||
@@ -219,7 +269,8 @@ func buildM3U(groups []string) (m3u string, err error) {
|
||||
for _, channelNumber := range channelNumbers {
|
||||
|
||||
var channel = m3uChannels[channelNumber]
|
||||
var parameter = fmt.Sprintf(`#EXTINF:0 channelID="%s" tvg-chno="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s" group-title="%s",%s`+"\n", channel.XEPG, channel.XChannelID, channel.XName, channel.XChannelID, getCacheImageURL(channel.TvgLogo), channel.XGroupTitle, channel.XName)
|
||||
|
||||
var parameter = fmt.Sprintf(`#EXTINF:0 channelID="%s" tvg-chno="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s" group-title="%s",%s`+"\n", channel.XEPG, channel.XChannelID, channel.XName, channel.XChannelID, imgc.Image.GetURL(channel.TvgLogo), channel.XGroupTitle, channel.XName)
|
||||
var stream, err = createStreamingURL("M3U", channel.FileM3UID, channel.XChannelID, channel.XName, channel.URL)
|
||||
if err == nil {
|
||||
m3u = m3u + parameter + stream + "\n"
|
||||
|
||||
+1
-18
@@ -1,17 +1,12 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InitMaintenance : Wartungsprozess initialisieren
|
||||
func InitMaintenance() (err error) {
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
System.TimeForAutoUpdate = fmt.Sprintf("0%d%d", randomTime(0, 2), randomTime(10, 59))
|
||||
|
||||
go maintenance()
|
||||
|
||||
return
|
||||
@@ -52,7 +47,7 @@ func maintenance() {
|
||||
ShowError(err, 000)
|
||||
}
|
||||
|
||||
if Settings.CacheImages == false && System.ImageCachingInProgress == 0 {
|
||||
if !Settings.CacheImages && System.ImageCachingInProgress == 0 {
|
||||
removeChildItems(System.Folder.ImagesCache)
|
||||
}
|
||||
|
||||
@@ -64,21 +59,9 @@ func maintenance() {
|
||||
|
||||
}
|
||||
|
||||
// Update xTeVe (Binary)
|
||||
if System.TimeForAutoUpdate == t.Format("1504") {
|
||||
BinaryUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(60 * time.Second)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func randomTime(min, max int) int {
|
||||
rand.Seed(time.Now().Unix())
|
||||
return rand.Intn(max-min) + min
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// conditionalUpdateChanges : Settings-schema migrations applied at start-up.
|
||||
// (Binary self-update was removed; the Docker image is the only distribution channel.)
|
||||
func conditionalUpdateChanges() (err error) {
|
||||
|
||||
checkVersion:
|
||||
settingsMap, err := loadJSONFileToMap(System.File.Settings)
|
||||
if err != nil || len(settingsMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if settingsVersion, ok := settingsMap["version"].(string); ok {
|
||||
|
||||
if settingsVersion > System.DBVersion {
|
||||
showInfo("Settings DB Version:" + settingsVersion)
|
||||
showInfo("System DB Version:" + System.DBVersion)
|
||||
err = errors.New(getErrMsg(1031))
|
||||
return
|
||||
}
|
||||
|
||||
// Letzte Kompatible Version (1.4.4)
|
||||
if settingsVersion < System.Compatibility {
|
||||
err = errors.New(getErrMsg(1013))
|
||||
return
|
||||
}
|
||||
|
||||
switch settingsVersion {
|
||||
|
||||
case "1.4.4":
|
||||
// UUID Wert in xepg.json setzen
|
||||
err = setValueForUUID()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
|
||||
if oldFilter, ok := settingsMap["filter"].([]any); ok {
|
||||
var newFilterMap = convertToNewFilter(oldFilter)
|
||||
settingsMap["filter"] = newFilterMap
|
||||
|
||||
settingsMap["version"] = "2.0.0"
|
||||
|
||||
err = saveMapToJSONFile(System.File.Settings, settingsMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
goto checkVersion
|
||||
|
||||
} else {
|
||||
err = errors.New(getErrMsg(1030))
|
||||
return
|
||||
}
|
||||
|
||||
case "2.0.0":
|
||||
|
||||
if oldBuffer, ok := settingsMap["buffer"].(bool); ok {
|
||||
|
||||
var newBuffer string
|
||||
switch oldBuffer {
|
||||
case true:
|
||||
newBuffer = "xteve"
|
||||
case false:
|
||||
newBuffer = "-"
|
||||
}
|
||||
|
||||
settingsMap["buffer"] = newBuffer
|
||||
|
||||
settingsMap["version"] = "2.1.0"
|
||||
|
||||
err = saveMapToJSONFile(System.File.Settings, settingsMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
goto checkVersion
|
||||
|
||||
} else {
|
||||
err = errors.New(getErrMsg(1030))
|
||||
return
|
||||
}
|
||||
|
||||
case "2.1.0":
|
||||
// Falls es in einem späteren Update Änderungen an der Datenbank gibt, geht es hier weiter
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
} else {
|
||||
// settings.json ist zu alt (älter als Version 1.4.4)
|
||||
err = errors.New(getErrMsg(1013))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) {
|
||||
|
||||
newFilterMap = make(map[int]any)
|
||||
|
||||
switch reflect.TypeFor[[]any]().Kind() {
|
||||
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(oldFilter)
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
|
||||
var newFilter FilterStruct
|
||||
newFilter.Active = true
|
||||
newFilter.Name = fmt.Sprintf("Custom filter %d", i+1)
|
||||
newFilter.Filter = s.Index(i).Interface().(string)
|
||||
newFilter.Type = "custom-filter"
|
||||
newFilter.CaseSensitive = false
|
||||
|
||||
newFilterMap[i] = newFilter
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setValueForUUID() (err error) {
|
||||
|
||||
xepg, err := loadJSONFileToMap(System.File.XEPG)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range xepg {
|
||||
|
||||
var xepgChannel = c.(map[string]any)
|
||||
|
||||
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {
|
||||
|
||||
if value, ok := xepgChannel[uuidKey].(string); ok {
|
||||
|
||||
if len(value) > 0 {
|
||||
xepgChannel["_uuid.value"] = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = saveMapToJSONFile(System.File.XEPG, xepg)
|
||||
|
||||
return
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var plexRefreshState = struct {
|
||||
sync.Mutex
|
||||
lastRun time.Time
|
||||
scheduled bool
|
||||
inProgress bool
|
||||
queued bool
|
||||
reason string
|
||||
missingConfigLogged bool
|
||||
}{}
|
||||
|
||||
// queuePlexGuideRefresh schedules a debounced Plex DVR guide refresh.
|
||||
func queuePlexGuideRefresh(reason string) {
|
||||
|
||||
if !Settings.UsePlexAPI {
|
||||
return
|
||||
}
|
||||
|
||||
plexRefreshState.Lock()
|
||||
|
||||
if len(strings.TrimSpace(reason)) > 0 {
|
||||
plexRefreshState.reason = reason
|
||||
}
|
||||
|
||||
if plexRefreshState.scheduled || plexRefreshState.inProgress {
|
||||
plexRefreshState.queued = true
|
||||
plexRefreshState.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
plexRefreshState.scheduled = true
|
||||
delay := plexRefreshDelayLocked()
|
||||
plexRefreshState.Unlock()
|
||||
|
||||
go runPlexGuideRefresh(delay)
|
||||
}
|
||||
|
||||
func runPlexGuideRefresh(initialDelay time.Duration) {
|
||||
|
||||
if initialDelay > 0 {
|
||||
time.Sleep(initialDelay)
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
plexRefreshState.Lock()
|
||||
reason := strings.TrimSpace(plexRefreshState.reason)
|
||||
if len(reason) == 0 {
|
||||
reason = "update"
|
||||
}
|
||||
plexRefreshState.scheduled = false
|
||||
plexRefreshState.inProgress = true
|
||||
plexRefreshState.queued = false
|
||||
plexRefreshState.Unlock()
|
||||
|
||||
err := refreshPlexGuide(reason)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
plexRefreshState.Lock()
|
||||
plexRefreshState.lastRun = time.Now()
|
||||
runAgain := plexRefreshState.queued
|
||||
plexRefreshState.inProgress = false
|
||||
nextDelay := time.Duration(0)
|
||||
|
||||
if runAgain && Settings.UsePlexAPI {
|
||||
plexRefreshState.scheduled = true
|
||||
nextDelay = plexRefreshDelayLocked()
|
||||
}
|
||||
plexRefreshState.Unlock()
|
||||
|
||||
if !runAgain || !Settings.UsePlexAPI {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(nextDelay)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func plexRefreshDelayLocked() (delay time.Duration) {
|
||||
|
||||
const minDelay = 2 * time.Second
|
||||
const cooldown = 20 * time.Second
|
||||
|
||||
if plexRefreshState.lastRun.IsZero() {
|
||||
return minDelay
|
||||
}
|
||||
|
||||
since := time.Since(plexRefreshState.lastRun)
|
||||
if since >= cooldown {
|
||||
return minDelay
|
||||
}
|
||||
|
||||
return cooldown - since
|
||||
}
|
||||
|
||||
func refreshPlexGuide(reason string) (err error) {
|
||||
|
||||
baseURL, token, ready := getPlexConfig()
|
||||
if !ready {
|
||||
return nil
|
||||
}
|
||||
|
||||
dvrs, err := discoverPlexDVRs(baseURL, token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var refreshed int
|
||||
var failed int
|
||||
|
||||
for _, dvr := range dvrs {
|
||||
|
||||
endpoints := getPlexReloadEndpoints(dvr)
|
||||
if len(endpoints) == 0 {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
var endpointErr error
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
status, _, requestErr := doPlexRequest("POST", baseURL, endpoint, token)
|
||||
if requestErr != nil {
|
||||
endpointErr = requestErr
|
||||
continue
|
||||
}
|
||||
|
||||
if status >= 200 && status < 300 {
|
||||
refreshed++
|
||||
endpointErr = nil
|
||||
break
|
||||
}
|
||||
|
||||
endpointErr = fmt.Errorf("Plex API returned HTTP %d for %s", status, endpoint)
|
||||
}
|
||||
|
||||
if endpointErr != nil {
|
||||
failed++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if refreshed == 0 {
|
||||
if failed == 0 {
|
||||
return errors.New("Plex API guide refresh failed")
|
||||
}
|
||||
|
||||
return fmt.Errorf("Plex API guide refresh failed for %d DVR(s)", failed)
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("Plex API:Guide reload requested for %d DVR(s) (%s)", refreshed, reason))
|
||||
if failed > 0 {
|
||||
showInfo(fmt.Sprintf("Plex API:Guide reload failed for %d DVR(s)", failed))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPlexConfig() (baseURL, token string, ready bool) {
|
||||
|
||||
baseURL = strings.TrimSpace(Settings.PlexURL)
|
||||
token = strings.TrimSpace(Settings.PlexToken)
|
||||
|
||||
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") && len(baseURL) > 0 {
|
||||
baseURL = "http://" + baseURL
|
||||
}
|
||||
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
|
||||
plexRefreshState.Lock()
|
||||
defer plexRefreshState.Unlock()
|
||||
|
||||
if len(baseURL) == 0 || len(token) == 0 {
|
||||
|
||||
if !plexRefreshState.missingConfigLogged {
|
||||
showInfo("Plex API:Skipped refresh because plex.url or plex.token is empty")
|
||||
plexRefreshState.missingConfigLogged = true
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
plexRefreshState.missingConfigLogged = false
|
||||
|
||||
return baseURL, token, true
|
||||
}
|
||||
|
||||
func discoverPlexDVRs(baseURL, token string) (dvrs []map[string]any, err error) {
|
||||
|
||||
status, body, err := doPlexRequest("GET", baseURL, "/livetv/dvrs", token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if status < 200 || status >= 300 {
|
||||
err = fmt.Errorf("Plex API returned HTTP %d for /livetv/dvrs", status)
|
||||
return
|
||||
}
|
||||
|
||||
var payload = make(map[string]any)
|
||||
err = json.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mediaContainer, ok := payload["MediaContainer"].(map[string]any)
|
||||
if !ok {
|
||||
err = errors.New("Plex API response missing MediaContainer")
|
||||
return
|
||||
}
|
||||
|
||||
for _, key := range []string{"Dvr", "DVR", "Directory", "Metadata"} {
|
||||
if raw, found := mediaContainer[key]; found {
|
||||
if list, ok := raw.([]any); ok {
|
||||
dvrs = convertToPlexMapSlice(list)
|
||||
if len(dvrs) > 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = errors.New("Plex API returned no DVR entries")
|
||||
return
|
||||
}
|
||||
|
||||
func convertToPlexMapSlice(list []any) (dvrs []map[string]any) {
|
||||
|
||||
dvrs = make([]map[string]any, 0, len(list))
|
||||
|
||||
for _, item := range list {
|
||||
if dvr, ok := item.(map[string]any); ok {
|
||||
dvrs = append(dvrs, dvr)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) {
|
||||
|
||||
endpoints = make([]string, 0, 6)
|
||||
added := make(map[string]bool)
|
||||
|
||||
add := func(endpoint string) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if len(endpoint) == 0 {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(endpoint, "/") {
|
||||
endpoint = "/" + endpoint
|
||||
}
|
||||
if added[endpoint] {
|
||||
return
|
||||
}
|
||||
added[endpoint] = true
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
if key := getPlexMapString(dvr, "key"); len(key) > 0 {
|
||||
if strings.HasPrefix(key, "/") {
|
||||
add(strings.TrimRight(key, "/") + "/reloadGuide")
|
||||
} else {
|
||||
add("/livetv/dvrs/" + url.PathEscape(key) + "/reloadGuide")
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range []string{"uuid", "identifier", "machineIdentifier", "clientIdentifier", "id"} {
|
||||
if value := getPlexMapString(dvr, field); len(value) > 0 {
|
||||
add("/livetv/dvrs/" + url.PathEscape(value) + "/reloadGuide")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getPlexMapString(data map[string]any, field string) string {
|
||||
|
||||
for key, value := range data {
|
||||
if !strings.EqualFold(key, field) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func doPlexRequest(method, baseURL, endpoint, token string) (status int, body []byte, err error) {
|
||||
|
||||
requestURL := strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(endpoint, "/")
|
||||
|
||||
req, err := http.NewRequest(method, requestURL, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
if len(token) > 0 {
|
||||
query.Set("X-Plex-Token", token)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Plex-Token", token)
|
||||
|
||||
if len(System.DeviceID) > 0 {
|
||||
req.Header.Set("X-Plex-Client-Identifier", System.DeviceID)
|
||||
}
|
||||
|
||||
if len(System.Name) > 0 {
|
||||
req.Header.Set("X-Plex-Product", System.Name)
|
||||
req.Header.Set("X-Plex-Device-Name", System.Name)
|
||||
}
|
||||
|
||||
if len(System.Version) > 0 {
|
||||
req.Header.Set("X-Plex-Version", System.Version)
|
||||
}
|
||||
|
||||
resp, err := apiHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
status = resp.StatusCode
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+55
-30
@@ -2,13 +2,13 @@ package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
m3u "../src/internal/m3u-parser"
|
||||
m3u "xteve/src/internal/m3u-parser"
|
||||
)
|
||||
|
||||
// fileType: Welcher Dateityp soll aktualisiert werden (m3u, hdhr, xml) | fileID: Update einer bestimmten Datei (Provider ID)
|
||||
@@ -17,13 +17,13 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
var fileExtension, serverFileName string
|
||||
var body = make([]byte, 0)
|
||||
var newProvider = false
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
var saveDateFromProvider = func(fileSource, serverFileName, id string, body []byte) (err error) {
|
||||
|
||||
var data = make(map[string]interface{})
|
||||
var data = make(map[string]any)
|
||||
|
||||
if value, ok := dataMap[id].(map[string]interface{}); ok {
|
||||
if value, ok := dataMap[id].(map[string]any); ok {
|
||||
data = value
|
||||
} else {
|
||||
data["id.provider"] = id
|
||||
@@ -65,7 +65,7 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
}
|
||||
|
||||
case "compatibility":
|
||||
data[key] = make(map[string]interface{})
|
||||
data[key] = make(map[string]any)
|
||||
|
||||
case "counter.download":
|
||||
data[key] = 0.0
|
||||
@@ -142,8 +142,16 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
|
||||
for dataID, d := range dataMap {
|
||||
|
||||
var data = d.(map[string]interface{})
|
||||
var fileSource = data["file.source"].(string)
|
||||
data, ok := d.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
fileSource, ok := data["file.source"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
newProvider = false
|
||||
|
||||
if _, ok := data["new"]; ok {
|
||||
@@ -152,7 +160,7 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
}
|
||||
|
||||
// Wenn eine ID vorhanden ist und nicht mit der aus der Datanbank übereinstimmt, wird die Aktualisierung übersprungen (goto)
|
||||
if len(fileID) > 0 && newProvider == false {
|
||||
if len(fileID) > 0 && !newProvider {
|
||||
if dataID != fileID {
|
||||
goto Done
|
||||
}
|
||||
@@ -204,7 +212,7 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
ShowError(err, 000)
|
||||
var downloadErr = err
|
||||
|
||||
if newProvider == false {
|
||||
if !newProvider {
|
||||
|
||||
// Prüfen ob ältere Datei vorhanden ist
|
||||
var file = System.Folder.Data + dataID + fileExtension
|
||||
@@ -220,12 +228,12 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
}
|
||||
|
||||
// Fehler Counter um 1 erhöhen
|
||||
var data = make(map[string]interface{})
|
||||
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
|
||||
var data = make(map[string]any)
|
||||
if value, ok := dataMap[dataID].(map[string]any); ok {
|
||||
|
||||
data = value
|
||||
data["counter.error"] = data["counter.error"].(float64) + 1
|
||||
data["counter.download"] = data["counter.download"].(float64) + 1
|
||||
data["counter.error"] = toFloat64(data["counter.error"]) + 1
|
||||
data["counter.download"] = toFloat64(data["counter.download"]) + 1
|
||||
|
||||
}
|
||||
|
||||
@@ -236,17 +244,20 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
}
|
||||
|
||||
// Berechnen der Fehlerquote
|
||||
if newProvider == false {
|
||||
if !newProvider {
|
||||
|
||||
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
|
||||
if value, ok := dataMap[dataID].(map[string]any); ok {
|
||||
|
||||
var data = make(map[string]interface{})
|
||||
var data = make(map[string]any)
|
||||
data = value
|
||||
|
||||
if data["counter.error"].(float64) == 0 {
|
||||
var errCount = toFloat64(data["counter.error"])
|
||||
var dlCount = toFloat64(data["counter.download"])
|
||||
|
||||
if errCount == 0 || dlCount == 0 {
|
||||
data["provider.availability"] = 100
|
||||
} else {
|
||||
data["provider.availability"] = int(data["counter.error"].(float64)*100/data["counter.download"].(float64)*-1 + 100)
|
||||
data["provider.availability"] = int(errCount*100/dlCount*-1 + 100)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -282,15 +293,22 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(providerURL)
|
||||
req, err := http.NewRequest(http.MethodGet, providerURL, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Header.Set("User-Agent", Settings.UserAgent)
|
||||
req.Header.Set("User-Agent", getUserAgent())
|
||||
|
||||
resp, err := providerHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf(fmt.Sprintf("%d: %s "+http.StatusText(resp.StatusCode), resp.StatusCode, providerURL))
|
||||
err = fmt.Errorf("%d: %s %s", resp.StatusCode, providerURL, http.StatusText(resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -299,22 +317,29 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e
|
||||
|
||||
if index > -1 {
|
||||
|
||||
var headerFilename = resp.Header.Get("Content-Disposition")[index:len(resp.Header.Get("Content-Disposition"))]
|
||||
var value = strings.Split(headerFilename, `=`)
|
||||
var f = strings.Replace(value[1], `"`, "", -1)
|
||||
var headerFilename = resp.Header.Get("Content-Disposition")[index:]
|
||||
var value = strings.SplitN(headerFilename, `=`, 2)
|
||||
|
||||
f = strings.Replace(f, `;`, "", -1)
|
||||
filename = f
|
||||
showInfo("Header filename:" + filename)
|
||||
if len(value) == 2 {
|
||||
|
||||
} else {
|
||||
var f = strings.Replace(value[1], `"`, "", -1)
|
||||
|
||||
f = strings.Replace(f, `;`, "", -1)
|
||||
filename = f
|
||||
showInfo("Header filename:" + filename)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(filename) == 0 {
|
||||
|
||||
var cleanFilename = strings.SplitN(getFilenameFromPath(providerURL), "?", 2)
|
||||
filename = cleanFilename[0]
|
||||
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
+215
-103
@@ -10,13 +10,89 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// logMu guards WebScreenLog (its slice and counters) and System.Notification.
|
||||
// Log lines are appended from every goroutine (buffer, maintenance, web
|
||||
// handlers) and read by the websocket handler.
|
||||
var logMu sync.Mutex
|
||||
|
||||
// logAppend : Appends a timestamped line to the in-memory log and trims it
|
||||
// to Settings.LogEntriesRAM.
|
||||
func logAppend(logMsg string) {
|
||||
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
|
||||
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
|
||||
logCleanUpLocked()
|
||||
}
|
||||
|
||||
// logAppendCounted : Appends a warning / error line and bumps the matching
|
||||
// counter. Like the original code this does not trim the log; the next
|
||||
// info / debug line does.
|
||||
func logAppendCounted(logMsg string, isError bool) {
|
||||
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
|
||||
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
|
||||
|
||||
if isError {
|
||||
WebScreenLog.Errors++
|
||||
} else {
|
||||
WebScreenLog.Warnings++
|
||||
}
|
||||
}
|
||||
|
||||
// resetWebScreenLog : Clears the in-memory log and its counters.
|
||||
func resetWebScreenLog() {
|
||||
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
|
||||
WebScreenLog.Log = make([]string, 0)
|
||||
WebScreenLog.Errors = 0
|
||||
WebScreenLog.Warnings = 0
|
||||
}
|
||||
|
||||
// webScreenLogSnapshot : Copy of the log for the web client. The slice is
|
||||
// copied so the JSON encoder never races with a concurrent append.
|
||||
func webScreenLogSnapshot() (snapshot WebScreenLogStruct) {
|
||||
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
|
||||
snapshot = WebScreenLog
|
||||
snapshot.Log = make([]string, len(WebScreenLog.Log))
|
||||
copy(snapshot.Log, WebScreenLog.Log)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// notificationsSnapshot : Copy of the notifications map for the web client.
|
||||
func notificationsSnapshot() (snapshot map[string]Notification) {
|
||||
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
|
||||
if System.Notification == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
snapshot = make(map[string]Notification, len(System.Notification))
|
||||
for k, v := range System.Notification {
|
||||
snapshot[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func showInfo(str string) {
|
||||
|
||||
if System.Flag.Info == true {
|
||||
if System.Flag.Info {
|
||||
return
|
||||
}
|
||||
|
||||
var max = 22
|
||||
var max = 23
|
||||
var msg = strings.SplitN(str, ":", 2)
|
||||
var length = len(msg[0])
|
||||
var space string
|
||||
@@ -33,13 +109,10 @@ func showInfo(str string) {
|
||||
|
||||
printLogOnScreen(logMsg, "info")
|
||||
|
||||
logMsg = strings.Replace(logMsg, " ", " ", -1)
|
||||
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
|
||||
logCleanUp()
|
||||
logAppend(logMsg)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func showDebug(str string, level int) {
|
||||
@@ -48,11 +121,10 @@ func showDebug(str string, level int) {
|
||||
return
|
||||
}
|
||||
|
||||
var max = 22
|
||||
var max = 23
|
||||
var msg = strings.SplitN(str, ":", 2)
|
||||
var length = len(msg[0])
|
||||
var space string
|
||||
var mutex = sync.RWMutex{}
|
||||
|
||||
if len(msg) == 2 {
|
||||
|
||||
@@ -65,20 +137,15 @@ func showDebug(str string, level int) {
|
||||
|
||||
printLogOnScreen(logMsg, "debug")
|
||||
|
||||
mutex.Lock()
|
||||
logMsg = strings.Replace(logMsg, " ", " ", -1)
|
||||
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
|
||||
logCleanUp()
|
||||
mutex.Unlock()
|
||||
logAppend(logMsg)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func showHighlight(str string) {
|
||||
|
||||
var max = 22
|
||||
var max = 23
|
||||
var msg = strings.SplitN(str, ":", 2)
|
||||
var length = len(msg[0])
|
||||
var space string
|
||||
@@ -101,45 +168,34 @@ func showHighlight(str string) {
|
||||
}
|
||||
|
||||
notification.Type = "info"
|
||||
notification.Message = msg[1]
|
||||
// Messages without a "key:value" prefix are shown as they are.
|
||||
notification.Message = msg[len(msg)-1]
|
||||
|
||||
addNotification(notification)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func showWarning(errCode int) {
|
||||
|
||||
var errMsg = getErrMsg(errCode)
|
||||
var logMsg = fmt.Sprintf("[%s] [WARNING] %s", System.Name, errMsg)
|
||||
var mutex = sync.RWMutex{}
|
||||
|
||||
printLogOnScreen(logMsg, "warning")
|
||||
|
||||
mutex.Lock()
|
||||
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
|
||||
WebScreenLog.Warnings++
|
||||
mutex.Unlock()
|
||||
logAppendCounted(logMsg, false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ShowError : Zeigt die Fehlermeldungen in der Konsole
|
||||
func ShowError(err error, errCode int) {
|
||||
|
||||
var mutex = sync.RWMutex{}
|
||||
|
||||
var errMsg = getErrMsg(errCode)
|
||||
var logMsg = fmt.Sprintf("[%s] [ERROR] %s (%s) - EC: %d", System.Name, err, errMsg, errCode)
|
||||
|
||||
printLogOnScreen(logMsg, "error")
|
||||
|
||||
mutex.Lock()
|
||||
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
|
||||
WebScreenLog.Errors++
|
||||
mutex.Unlock()
|
||||
logAppendCounted(logMsg, true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func printLogOnScreen(logMsg string, logType string) {
|
||||
@@ -179,24 +235,17 @@ func printLogOnScreen(logMsg string, logType string) {
|
||||
|
||||
}
|
||||
|
||||
func logCleanUp() {
|
||||
// logCleanUpLocked : Trims the log to the newest Settings.LogEntriesRAM
|
||||
// lines and recounts warnings / errors. The caller must hold logMu.
|
||||
func logCleanUpLocked() {
|
||||
|
||||
WebScreenLog.Log = keepLastEntries(WebScreenLog.Log, Settings.LogEntriesRAM)
|
||||
|
||||
var logEntriesRAM = Settings.LogEntriesRAM
|
||||
var logs = WebScreenLog.Log
|
||||
|
||||
WebScreenLog.Warnings = 0
|
||||
WebScreenLog.Errors = 0
|
||||
|
||||
if len(logs) > logEntriesRAM {
|
||||
|
||||
var tmp = make([]string, 0)
|
||||
for i := len(logs) - logEntriesRAM; i < logEntriesRAM; i++ {
|
||||
tmp = append(tmp, logs[i])
|
||||
}
|
||||
|
||||
logs = tmp
|
||||
}
|
||||
|
||||
for _, log := range logs {
|
||||
|
||||
if strings.Contains(log, "WARNING") {
|
||||
@@ -209,9 +258,25 @@ func logCleanUp() {
|
||||
|
||||
}
|
||||
|
||||
WebScreenLog.Log = logs
|
||||
}
|
||||
|
||||
return
|
||||
// keepLastEntries : Returns the last max entries of logs (a copy, so the
|
||||
// old backing array does not keep growing). A max <= 0 keeps nothing, which
|
||||
// matches the original behaviour for an unset LogEntriesRAM.
|
||||
func keepLastEntries(logs []string, max int) []string {
|
||||
|
||||
if max < 0 {
|
||||
max = 0
|
||||
}
|
||||
|
||||
if len(logs) <= max {
|
||||
return logs
|
||||
}
|
||||
|
||||
var tmp = make([]string, max)
|
||||
copy(tmp, logs[len(logs)-max:])
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
// Fehlercodes
|
||||
@@ -224,153 +289,166 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
|
||||
// Errors
|
||||
case 1001:
|
||||
errMsg = fmt.Sprintf("Web server could not be started.")
|
||||
errMsg = "Web server could not be started."
|
||||
case 1002:
|
||||
errMsg = fmt.Sprintf("No local IP address found.")
|
||||
errMsg = "No local IP address found."
|
||||
case 1003:
|
||||
errMsg = fmt.Sprintf("Invalid xml")
|
||||
errMsg = "Invalid xml"
|
||||
case 1004:
|
||||
errMsg = fmt.Sprintf("File not found")
|
||||
errMsg = "File not found"
|
||||
case 1005:
|
||||
errMsg = fmt.Sprintf("Invalide m3u")
|
||||
errMsg = "Invalid M3U file, an extended M3U file is required."
|
||||
case 1006:
|
||||
errMsg = fmt.Sprintf("No playlist!")
|
||||
errMsg = "No playlist!"
|
||||
case 1007:
|
||||
errMsg = fmt.Sprintf("XEPG requires an XMLTV file.")
|
||||
errMsg = "XEPG requires an XMLTV file."
|
||||
case 1010:
|
||||
errMsg = fmt.Sprintf("Invalid file compression")
|
||||
errMsg = "Invalid file compression"
|
||||
case 1011:
|
||||
errMsg = fmt.Sprintf("Data is corrupt or unavailable, %s now uses an older version of this file", System.Name)
|
||||
case 1012:
|
||||
errMsg = fmt.Sprintf("Invalid formatting of the time")
|
||||
errMsg = "Invalid formatting of the time"
|
||||
case 1013:
|
||||
errMsg = fmt.Sprintf("Invalid settings file (settings.json), file must be at least version %s", System.Compatibility)
|
||||
case 1014:
|
||||
errMsg = "Invalid filter rule"
|
||||
|
||||
case 1020:
|
||||
errMsg = fmt.Sprintf("Data could not be saved, invalid keyword")
|
||||
errMsg = "Data could not be saved, invalid keyword"
|
||||
|
||||
// Datenbank Update
|
||||
case 1030:
|
||||
errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings)
|
||||
case 1031:
|
||||
errMsg = "Database error. The database version of your settings is not compatible with this version."
|
||||
|
||||
// M3U Parser
|
||||
case 1050:
|
||||
errMsg = fmt.Sprintf("Invalid duration specification in the M3U8 playlist.")
|
||||
errMsg = "Invalid duration specification in the M3U8 playlist."
|
||||
|
||||
// M3U Parser
|
||||
case 1060:
|
||||
errMsg = fmt.Sprintf("Invalid characters found in the tvg parameters, streams with invalid parameters were skipped.")
|
||||
errMsg = "Invalid characters found in the tvg parameters, streams with invalid parameters were skipped."
|
||||
|
||||
// Dateisystem
|
||||
case 1070:
|
||||
errMsg = fmt.Sprintf("Folder could not be created.")
|
||||
errMsg = "Folder could not be created."
|
||||
case 1071:
|
||||
errMsg = fmt.Sprintf("File could not be created")
|
||||
errMsg = "File could not be created"
|
||||
case 1072:
|
||||
errMsg = "File not found"
|
||||
|
||||
// Backup
|
||||
case 1090:
|
||||
errMsg = fmt.Sprintf("Automatic backup failed")
|
||||
errMsg = "Automatic backup failed"
|
||||
|
||||
// Websockets
|
||||
case 1100:
|
||||
errMsg = fmt.Sprintf("WebUI build error")
|
||||
errMsg = "WebUI build error"
|
||||
case 1101:
|
||||
errMsg = fmt.Sprintf("WebUI request error")
|
||||
errMsg = "WebUI request error"
|
||||
case 1102:
|
||||
errMsg = fmt.Sprintf("WebUI response error")
|
||||
errMsg = "WebUI response error"
|
||||
|
||||
// PMS Guide Numbers
|
||||
case 1200:
|
||||
errMsg = fmt.Sprintf("Could not create file")
|
||||
errMsg = "Could not create file"
|
||||
|
||||
// Stream URL Fehler
|
||||
case 1201:
|
||||
errMsg = fmt.Sprintf("Plex stream error")
|
||||
errMsg = "Plex stream error"
|
||||
case 1202:
|
||||
errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist")
|
||||
errMsg = "Steaming URL could not be found in any playlist"
|
||||
case 1203:
|
||||
errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist")
|
||||
errMsg = "Steaming URL could not be found in any playlist"
|
||||
case 1204:
|
||||
errMsg = "Streaming was stopped by third party transcoder (FFmpeg / VLC)"
|
||||
|
||||
// Warnings
|
||||
case 2000:
|
||||
errMsg = fmt.Sprintf("Plex can not handle more than %d streams. If you do not use Plex, you can ignore this warning.", System.DVRLimit)
|
||||
errMsg = fmt.Sprintf("Plex can not handle more than %d streams. If you do not use Plex, you can ignore this warning.", System.PlexChannelLimit)
|
||||
case 2001:
|
||||
errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.DVRLimit)
|
||||
errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.UnfilteredChannelLimit)
|
||||
case 2002:
|
||||
errMsg = fmt.Sprintf("PMS can not play m3u8 streams")
|
||||
errMsg = "PMS can not play m3u8 streams"
|
||||
case 2003:
|
||||
errMsg = fmt.Sprintf("PMS can not play streams over RTSP.")
|
||||
errMsg = "PMS can not play streams over RTSP."
|
||||
case 2004:
|
||||
errMsg = fmt.Sprintf("Buffer is disabled for this stream.")
|
||||
errMsg = "Buffer is disabled for this stream."
|
||||
case 2005:
|
||||
errMsg = fmt.Sprintf("There are no channels mapped, use the mapping menu to assign EPG data to the channels.")
|
||||
errMsg = "There are no channels mapped, use the mapping menu to assign EPG data to the channels."
|
||||
case 2010:
|
||||
errMsg = fmt.Sprintf("No valid streaming URL")
|
||||
errMsg = "No valid streaming URL"
|
||||
case 2020:
|
||||
errMsg = "FFmpeg binary was not found. Check the FFmpeg binary path in the xTeVe settings."
|
||||
case 2021:
|
||||
errMsg = "VLC binary was not found. Check the VLC path binary in the xTeVe settings."
|
||||
|
||||
case 2099:
|
||||
errMsg = fmt.Sprintf("Updates have been disabled by the developer")
|
||||
errMsg = "Updates have been disabled by the developer"
|
||||
|
||||
// Tuner
|
||||
case 2105:
|
||||
errMsg = fmt.Sprintf("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:
|
||||
errMsg = fmt.Sprintf("This function is only available with XEPG as EPG source")
|
||||
errMsg = "This function is only available with XEPG as EPG source"
|
||||
|
||||
case 2110:
|
||||
errMsg = fmt.Sprintf("Don't run this as Root!")
|
||||
errMsg = "Don't run this as Root!"
|
||||
|
||||
case 2300:
|
||||
errMsg = fmt.Sprintf("No channel logo found in the XMLTV or M3U file.")
|
||||
errMsg = "No channel logo found in the XMLTV or M3U file."
|
||||
case 2301:
|
||||
errMsg = fmt.Sprintf("XMLTV file no longer available, channel has been deactivated.")
|
||||
errMsg = "XMLTV file no longer available, channel has been deactivated."
|
||||
case 2302:
|
||||
errMsg = fmt.Sprintf("Channel ID in the XMLTV file has changed. Channel has been deactivated.")
|
||||
errMsg = "Channel ID in the XMLTV file has changed. Channel has been deactivated."
|
||||
|
||||
// Benutzerauthentifizierung
|
||||
case 3000:
|
||||
errMsg = fmt.Sprintf("Database for user authentication could not be initialized.")
|
||||
errMsg = "Database for user authentication could not be initialized."
|
||||
case 3001:
|
||||
errMsg = fmt.Sprintf("The user has no authorization to load the channels.")
|
||||
errMsg = "The user has no authorization to load the channels."
|
||||
|
||||
// Buffer
|
||||
case 4000:
|
||||
errMsg = fmt.Sprintf("Connection to streaming source was interrupted.")
|
||||
errMsg = "Connection to streaming source was interrupted."
|
||||
case 4001:
|
||||
errMsg = fmt.Sprintf("Too many errors connecting to the provider. Streaming is canceled.")
|
||||
errMsg = "Too many errors connecting to the provider. Streaming is canceled."
|
||||
case 4002:
|
||||
errMsg = fmt.Sprintf("New URL for the redirect to the streaming server is missing")
|
||||
errMsg = "New URL for the redirect to the streaming server is missing"
|
||||
case 4003:
|
||||
errMsg = fmt.Sprintf("Server sends an incompatible content-type")
|
||||
errMsg = "Server sends an incompatible content-type"
|
||||
case 4004:
|
||||
errMsg = fmt.Sprintf("This error message comes from the provider")
|
||||
errMsg = "This error message comes from the provider"
|
||||
case 4005:
|
||||
errMsg = fmt.Sprintf("Temporary buffer files could not be deleted")
|
||||
errMsg = "Temporary buffer files could not be deleted"
|
||||
case 4006:
|
||||
errMsg = "Server connection timeout"
|
||||
|
||||
// Buffer (M3U8)
|
||||
case 4050:
|
||||
errMsg = fmt.Sprintf("Invalid M3U8 file")
|
||||
errMsg = "Invalid M3U8 file"
|
||||
case 4051:
|
||||
errMsg = fmt.Sprintf("#EXTM3U header is missing")
|
||||
errMsg = "#EXTM3U header is missing"
|
||||
|
||||
// Caching
|
||||
case 4100:
|
||||
errMsg = fmt.Sprintf("Unknown content type for downloaded image")
|
||||
errMsg = "Unknown content type for downloaded image"
|
||||
case 4101:
|
||||
errMsg = fmt.Sprintf("Invalid URL, original URL is used for this image")
|
||||
errMsg = "Invalid URL, original URL is used for this image"
|
||||
|
||||
// API
|
||||
case 5000:
|
||||
errMsg = fmt.Sprintf("Invalid API command")
|
||||
errMsg = "Invalid API command"
|
||||
|
||||
// Update Server
|
||||
case 6001:
|
||||
errMsg = fmt.Sprintf("Ivalid key")
|
||||
errMsg = "Ivalid key"
|
||||
case 6002:
|
||||
errMsg = fmt.Sprintf("Update failed")
|
||||
errMsg = "Update failed"
|
||||
case 6003:
|
||||
errMsg = fmt.Sprintf("Server not available")
|
||||
errMsg = "Update server not available"
|
||||
case 6004:
|
||||
errMsg = fmt.Sprintf("xTeVe update available")
|
||||
errMsg = "xTeVe update available"
|
||||
|
||||
default:
|
||||
errMsg = fmt.Sprintf("Unknown error / warning (%d)", errCode)
|
||||
@@ -379,30 +457,64 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
return errMsg
|
||||
}
|
||||
|
||||
// maxNotifications : Number of notifications kept for the web interface.
|
||||
const maxNotifications = 10
|
||||
|
||||
func addNotification(notification Notification) (err error) {
|
||||
|
||||
var i int
|
||||
var t = time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
|
||||
notification.Time = strconv.FormatInt(t, 10)
|
||||
|
||||
insertNotification(notification)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// insertNotification : Stores the notification under its Time key and evicts
|
||||
// the oldest ones once more than maxNotifications are kept.
|
||||
func insertNotification(notification Notification) {
|
||||
|
||||
notification.New = true
|
||||
|
||||
if len(notification.Headline) == 0 {
|
||||
notification.Headline = strings.ToUpper(notification.Type)
|
||||
}
|
||||
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
|
||||
if len(System.Notification) == 0 {
|
||||
System.Notification = make(map[string]Notification)
|
||||
}
|
||||
|
||||
System.Notification[notification.Time] = notification
|
||||
|
||||
for key := range System.Notification {
|
||||
for len(System.Notification) > maxNotifications {
|
||||
delete(System.Notification, oldestNotificationKey(System.Notification))
|
||||
}
|
||||
|
||||
if i < len(System.Notification)-10 {
|
||||
delete(System.Notification, key)
|
||||
}
|
||||
|
||||
// oldestNotificationKey : Key of the notification with the smallest
|
||||
// timestamp. Keys are millisecond timestamps as decimal strings; anything
|
||||
// that does not parse sorts before everything that does.
|
||||
func oldestNotificationKey(notifications map[string]Notification) (oldest string) {
|
||||
|
||||
var oldestTime int64
|
||||
var found bool
|
||||
|
||||
for key, n := range notifications {
|
||||
|
||||
t, err := strconv.ParseInt(n.Time, 10, 64)
|
||||
if err != nil {
|
||||
return key
|
||||
}
|
||||
|
||||
i++
|
||||
if !found || t < oldestTime {
|
||||
oldest = key
|
||||
oldestTime = t
|
||||
found = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeepLastEntries(t *testing.T) {
|
||||
logs := []string{"1", "2", "3", "4", "5"}
|
||||
|
||||
got := keepLastEntries(logs, 3)
|
||||
if want := []string{"3", "4", "5"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("keepLastEntries(5 entries, 3) = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
if got := keepLastEntries(logs, 10); !reflect.DeepEqual(got, logs) {
|
||||
t.Errorf("keepLastEntries below the limit = %v, want %v", got, logs)
|
||||
}
|
||||
|
||||
if got := keepLastEntries(logs, 0); len(got) != 0 {
|
||||
t.Errorf("keepLastEntries(.., 0) = %v, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The ring buffer must keep the newest lines once the log is full; the old
|
||||
// loop bound dropped them.
|
||||
func TestLogCleanUpKeepsNewest(t *testing.T) {
|
||||
oldLimit := Settings.LogEntriesRAM
|
||||
resetWebScreenLog()
|
||||
t.Cleanup(func() {
|
||||
Settings.LogEntriesRAM = oldLimit
|
||||
resetWebScreenLog()
|
||||
})
|
||||
|
||||
Settings.LogEntriesRAM = 3
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
logAppend(fmt.Sprintf("line-%d", i))
|
||||
}
|
||||
|
||||
snapshot := webScreenLogSnapshot()
|
||||
if len(snapshot.Log) != 3 {
|
||||
t.Fatalf("log has %d entries, want 3: %v", len(snapshot.Log), snapshot.Log)
|
||||
}
|
||||
for i, want := range []string{"line-3", "line-4", "line-5"} {
|
||||
if !strings.HasSuffix(snapshot.Log[i], want) {
|
||||
t.Errorf("entry %d = %q, want suffix %q", i, snapshot.Log[i], want)
|
||||
}
|
||||
}
|
||||
|
||||
// Counters are recomputed from the retained lines.
|
||||
logAppendCounted("[ERROR] boom", true)
|
||||
logAppend("[WARNING] careful")
|
||||
snapshot = webScreenLogSnapshot()
|
||||
if snapshot.Errors != 1 || snapshot.Warnings != 1 {
|
||||
t.Errorf("errors/warnings = %d/%d, want 1/1 (%v)", snapshot.Errors, snapshot.Warnings, snapshot.Log)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNotificationEvictsOldest(t *testing.T) {
|
||||
old := System.Notification
|
||||
System.Notification = nil
|
||||
t.Cleanup(func() { System.Notification = old })
|
||||
|
||||
// Insert out of order so eviction cannot rely on insertion order.
|
||||
order := []int64{5, 1, 12, 3, 9, 2, 11, 7, 4, 10, 8, 6}
|
||||
for _, ts := range order {
|
||||
insertNotification(Notification{Type: "info", Message: "n", Time: strconv.FormatInt(1000+ts, 10)})
|
||||
}
|
||||
|
||||
got := notificationsSnapshot()
|
||||
if len(got) != maxNotifications {
|
||||
t.Fatalf("kept %d notifications, want %d", len(got), maxNotifications)
|
||||
}
|
||||
for _, ts := range []int64{1, 2} {
|
||||
if _, ok := got[strconv.FormatInt(1000+ts, 10)]; ok {
|
||||
t.Errorf("oldest notification %d should have been evicted", ts)
|
||||
}
|
||||
}
|
||||
for ts := int64(3); ts <= 12; ts++ {
|
||||
if _, ok := got[strconv.FormatInt(1000+ts, 10)]; !ok {
|
||||
t.Errorf("notification %d missing", ts)
|
||||
}
|
||||
}
|
||||
|
||||
// The web-client copy must be independent of the live map.
|
||||
delete(got, "1012")
|
||||
if _, ok := notificationsSnapshot()["1012"]; !ok {
|
||||
t.Error("snapshot is not a copy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowHighlightWithoutColon(t *testing.T) {
|
||||
old := System.Notification
|
||||
System.Notification = nil
|
||||
t.Cleanup(func() { System.Notification = old })
|
||||
|
||||
showHighlight("no separator here") // used to index msg[1] and panic
|
||||
|
||||
for _, n := range notificationsSnapshot() {
|
||||
if n.Message != "no separator here" {
|
||||
t.Errorf("message = %q", n.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The web UI renders log lines as text inside <pre>, so the server must store
|
||||
// plain spaces, not HTML entities.
|
||||
func TestLogLinesContainNoHTMLEntities(t *testing.T) {
|
||||
var oldLimit = Settings.LogEntriesRAM
|
||||
Settings.LogEntriesRAM = 500
|
||||
t.Cleanup(func() { Settings.LogEntriesRAM = oldLimit })
|
||||
|
||||
resetWebScreenLog()
|
||||
showInfo("Version:2.2.0 Build: 0201")
|
||||
showDebug("Buffer Status:some debug text", 0)
|
||||
|
||||
snap := webScreenLogSnapshot()
|
||||
if len(snap.Log) == 0 {
|
||||
t.Fatal("no log lines recorded")
|
||||
}
|
||||
if !strings.Contains(snap.Log[0], "Version: 2.2.0 Build: 0201") {
|
||||
t.Errorf("padding should be plain spaces, got %q", snap.Log[0])
|
||||
}
|
||||
for _, line := range snap.Log {
|
||||
if strings.Contains(line, " ") {
|
||||
t.Errorf("log line contains an HTML entity: %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Input validation for values that reach the file system or an external
|
||||
// process. Everything here is deliberately strict: xTeVe runs on a trusted
|
||||
// LAN, but playlists, EPG feeds and any browser on that LAN are not trusted.
|
||||
|
||||
var uploadExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true, ".ico": true}
|
||||
|
||||
var unsafeFilenameChars = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
|
||||
|
||||
// sanitizeUploadFilename : reduces a client-supplied logo file name to a safe
|
||||
// base name with an image extension.
|
||||
func sanitizeUploadFilename(name string) (string, error) {
|
||||
|
||||
name = strings.TrimSpace(name)
|
||||
// Browsers on Windows may send backslash paths; keep only the last part.
|
||||
if i := strings.LastIndexAny(name, `\/`); i >= 0 {
|
||||
name = name[i+1:]
|
||||
}
|
||||
name = filepath.Base(name)
|
||||
if name == "" || name == "." || name == ".." || name == string(filepath.Separator) {
|
||||
return "", errors.New("invalid file name")
|
||||
}
|
||||
|
||||
var ext = strings.ToLower(filepath.Ext(name))
|
||||
if !uploadExtensions[ext] {
|
||||
return "", fmt.Errorf("unsupported image type %q", ext)
|
||||
}
|
||||
|
||||
var base = unsafeFilenameChars.ReplaceAllString(strings.TrimSuffix(name, filepath.Ext(name)), "_")
|
||||
base = strings.Trim(base, "._")
|
||||
if base == "" {
|
||||
return "", errors.New("invalid file name")
|
||||
}
|
||||
|
||||
return base + ext, nil
|
||||
}
|
||||
|
||||
var streamingBinaries = map[string]bool{"ffmpeg": true, "ffmpeg.exe": true, "vlc": true, "vlc.exe": true, "cvlc": true, "cvlc.exe": true}
|
||||
|
||||
// checkStreamingBinary : the configured ffmpeg / VLC path must be an existing
|
||||
// regular file whose name is one of the known players. This is what stops a
|
||||
// settings change from turning the buffer into "run any program".
|
||||
func checkStreamingBinary(path string) error {
|
||||
|
||||
if err := checkFile(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(getPlatformFile(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s is not a regular file", path)
|
||||
}
|
||||
|
||||
if !streamingBinaries[strings.ToLower(filepath.Base(path))] {
|
||||
return fmt.Errorf("%s is not a supported streaming binary (ffmpeg, vlc, cvlc)", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var streamSchemes = map[string]bool{"http": true, "https": true, "rtsp": true, "rtsps": true, "rtmp": true, "rtmps": true, "rtp": true, "udp": true, "mms": true, "mmsh": true}
|
||||
|
||||
// checkStreamURL : provider stream URLs are handed to ffmpeg / VLC verbatim.
|
||||
// Only network schemes are allowed, so a playlist cannot point the buffer at
|
||||
// file:, concat:, pipe: or similar local sources.
|
||||
func checkStreamURL(raw string) error {
|
||||
|
||||
u, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !streamSchemes[strings.ToLower(u.Scheme)] {
|
||||
return fmt.Errorf("stream URL scheme %q is not allowed", u.Scheme)
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
return errors.New("stream URL has no host")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// insideDir : true when path (already joined) stays inside dir.
|
||||
func insideDir(dir, path string) bool {
|
||||
|
||||
dir = filepath.Clean(dir)
|
||||
path = filepath.Clean(path)
|
||||
|
||||
if path == dir {
|
||||
return true
|
||||
}
|
||||
|
||||
return strings.HasPrefix(path, dir+string(filepath.Separator))
|
||||
}
|
||||
|
||||
// plexTokenMask : what the UI sees instead of the real Plex token.
|
||||
const plexTokenMask = "********"
|
||||
|
||||
// maskSettings : copy of Settings with secrets replaced for the web client.
|
||||
func maskSettings(s SettingsStruct) SettingsStruct {
|
||||
|
||||
if len(s.PlexToken) > 0 {
|
||||
s.PlexToken = plexTokenMask
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// writePrivateFile : like writeByteToFile but readable only by the owner.
|
||||
func writePrivateFile(file string, data []byte) error {
|
||||
|
||||
// The temp file is chmod'ed before the rename, so an existing
|
||||
// world-readable file is replaced by a 0600 one.
|
||||
return writeFileAtomic(getPlatformFile(file), data, 0600)
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"xteve/src/internal/authentication"
|
||||
)
|
||||
|
||||
func TestSanitizeUploadFilename(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"logo.png": "logo.png",
|
||||
"../../etc/passwd.png": "passwd.png",
|
||||
"my logo (1).JPG": "my_logo_1.jpg",
|
||||
"..\\..\\evil.gif": "evil.gif",
|
||||
"weird/../name.webp": "name.webp",
|
||||
"C:\\Users\\x\\pic.jpeg": "pic.jpeg",
|
||||
}
|
||||
for in, want := range cases {
|
||||
got, err := sanitizeUploadFilename(in)
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error %v", in, err)
|
||||
continue
|
||||
}
|
||||
if filepath.Base(got) != got || got != want {
|
||||
t.Errorf("%q: got %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, bad := range []string{"", ".", "..", "/", "shell.sh", "logo", "image.svg", ".png", "....png"} {
|
||||
if got, err := sanitizeUploadFilename(bad); err == nil {
|
||||
t.Errorf("%q: expected rejection, got %q", bad, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStreamingBinary(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ok := filepath.Join(dir, "ffmpeg")
|
||||
if err := os.WriteFile(ok, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bad := filepath.Join(dir, "sh")
|
||||
if err := os.WriteFile(bad, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := checkStreamingBinary(ok); err != nil {
|
||||
t.Errorf("ffmpeg path rejected: %v", err)
|
||||
}
|
||||
if err := checkStreamingBinary(bad); err == nil {
|
||||
t.Error("arbitrary binary accepted")
|
||||
}
|
||||
if err := checkStreamingBinary(dir); err == nil {
|
||||
t.Error("directory accepted")
|
||||
}
|
||||
if err := checkStreamingBinary(filepath.Join(dir, "missing", "ffmpeg")); err == nil {
|
||||
t.Error("missing file accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStreamURL(t *testing.T) {
|
||||
for _, good := range []string{"http://host/live.ts", "https://h:8080/x.m3u8", "rtsp://cam/1", "udp://239.0.0.1:1234", "rtmp://srv/app/key"} {
|
||||
if err := checkStreamURL(good); err != nil {
|
||||
t.Errorf("%q rejected: %v", good, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"file:///etc/passwd", "concat:a|b", "pipe:0", "data:text/plain,x", "/etc/passwd", "", "http://"} {
|
||||
if err := checkStreamURL(bad); err == nil {
|
||||
t.Errorf("%q accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractZIPRejectsZipSlip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
archive := filepath.Join(dir, "evil.zip")
|
||||
|
||||
f, err := os.Create(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zw := zip.NewWriter(f)
|
||||
for name, body := range map[string]string{"ok.txt": "fine", "../escape.txt": "nope"} {
|
||||
w, err := zw.Create(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
}
|
||||
zw.Close()
|
||||
f.Close()
|
||||
|
||||
target := filepath.Join(dir, "out")
|
||||
if err := extractZIP(archive, target); err == nil {
|
||||
t.Fatal("expected zip-slip entry to be rejected")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "escape.txt")); err == nil {
|
||||
t.Fatal("escaped file was written outside the target directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractZIPNormalArchive(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
archive := filepath.Join(dir, "ok.zip")
|
||||
|
||||
f, _ := os.Create(archive)
|
||||
zw := zip.NewWriter(f)
|
||||
w, _ := zw.Create("sub/file.txt")
|
||||
w.Write([]byte("hello"))
|
||||
zw.Close()
|
||||
f.Close()
|
||||
|
||||
target := filepath.Join(dir, "out")
|
||||
if err := extractZIP(archive, target); err != nil {
|
||||
t.Fatalf("extract failed: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(target, "sub", "file.txt"))
|
||||
if err != nil || string(got) != "hello" {
|
||||
t.Fatalf("extracted content wrong: %q %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskSettings(t *testing.T) {
|
||||
var s SettingsStruct
|
||||
s.PlexToken = "real-token"
|
||||
s.PlexURL = "http://plex:32400"
|
||||
|
||||
m := maskSettings(s)
|
||||
if m.PlexToken != plexTokenMask {
|
||||
t.Errorf("token not masked: %q", m.PlexToken)
|
||||
}
|
||||
if m.PlexURL != s.PlexURL {
|
||||
t.Error("unrelated field changed")
|
||||
}
|
||||
if s.PlexToken != "real-token" {
|
||||
t.Error("original settings mutated")
|
||||
}
|
||||
|
||||
var empty SettingsStruct
|
||||
if maskSettings(empty).PlexToken != "" {
|
||||
t.Error("empty token must stay empty so the UI shows an empty field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePrivateFile(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "settings.json")
|
||||
if err := os.WriteFile(file, []byte("old"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writePrivateFile(file, []byte("new")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fi, _ := os.Stat(file)
|
||||
if fi.Mode().Perm() != 0600 {
|
||||
t.Errorf("mode = %o, want 0600", fi.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadRequiresWebSession(t *testing.T) {
|
||||
if err := authentication.Init(t.TempDir()+"/authentication.json", 60); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := authentication.CreateNewUser("admin", "secret"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token, err := authentication.UserAuthentication("admin", "secret")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userID, _ := authentication.GetUserID(token)
|
||||
if err := authentication.WriteUserData(userID, map[string]any{"authentication.web": true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
System.Folder.Temp = t.TempDir() + string(os.PathSeparator)
|
||||
if err := os.WriteFile(System.Folder.Temp+"backup.zip", []byte("zip"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
Settings.AuthenticationWEB = true
|
||||
System.ConfigurationWizard = false
|
||||
t.Cleanup(func() { Settings.AuthenticationWEB = false })
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(Download))
|
||||
defer srv.Close()
|
||||
|
||||
resp, err := http.Get(srv.URL + "/download/backup.zip")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("anonymous download: got %d, want 403", resp.StatusCode)
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", srv.URL+"/download/backup.zip", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "Token", Value: token})
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("authenticated download: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
+78
-47
@@ -1,72 +1,103 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/koron/go-ssdp"
|
||||
"github.com/koron/go-ssdp"
|
||||
)
|
||||
|
||||
// ssdpState : The running advertiser, so Shutdown can say goodbye to the
|
||||
// network before the process exits.
|
||||
var ssdpState struct {
|
||||
sync.Mutex
|
||||
adv *ssdp.Advertiser
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Shutdown : Stops background services (currently the SSDP advertiser).
|
||||
// Called from main on SIGINT / SIGTERM.
|
||||
func Shutdown() {
|
||||
|
||||
ssdpState.Lock()
|
||||
defer ssdpState.Unlock()
|
||||
|
||||
if ssdpState.adv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(ssdpState.done)
|
||||
ssdpState.adv.Bye()
|
||||
ssdpState.adv.Close()
|
||||
ssdpState.adv = nil
|
||||
|
||||
}
|
||||
|
||||
// SSDP : SSPD / DLNA Server
|
||||
func SSDP() (err error) {
|
||||
|
||||
if Settings.SSDP == false || System.Flag.Info == true {
|
||||
return
|
||||
}
|
||||
if !Settings.SSDP || System.Flag.Info {
|
||||
return
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP))
|
||||
showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP))
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
ad, err := ssdp.Advertise(
|
||||
"upnp:rootdevice", // send as "ST"
|
||||
fmt.Sprintf("uuid:%s::upnp:rootdevice", System.DeviceID), // send as "USN"
|
||||
fmt.Sprintf("%s/device.xml", System.URLBase), // send as "LOCATION"
|
||||
System.AppName, // send as "SERVER"
|
||||
1800) // send as "maxAge" in "CACHE-CONTROL"
|
||||
|
||||
ad, err := ssdp.Advertise(
|
||||
fmt.Sprintf("upnp:rootdevice"), // send as "ST"
|
||||
fmt.Sprintf("uuid:%s::upnp:rootdevice", System.DeviceID), // send as "USN"
|
||||
fmt.Sprintf("%s/device.xml", System.URLBase), // send as "LOCATION"
|
||||
System.AppName, // send as "SERVER"
|
||||
1800) // send as "maxAge" in "CACHE-CONTROL"
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Debug SSDP
|
||||
if System.Flag.Debug == 3 {
|
||||
ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags)
|
||||
}
|
||||
|
||||
// Debug SSDP
|
||||
if System.Flag.Debug == 3 {
|
||||
ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags)
|
||||
}
|
||||
var done = make(chan struct{})
|
||||
|
||||
go func(adv *ssdp.Advertiser) {
|
||||
ssdpState.Lock()
|
||||
ssdpState.adv = ad
|
||||
ssdpState.done = done
|
||||
ssdpState.Unlock()
|
||||
|
||||
aliveTick := time.Tick(300 * time.Second)
|
||||
go func(adv *ssdp.Advertiser, done chan struct{}) {
|
||||
|
||||
loop:
|
||||
for {
|
||||
aliveTick := time.NewTicker(300 * time.Second)
|
||||
defer aliveTick.Stop()
|
||||
|
||||
select {
|
||||
for {
|
||||
|
||||
case <-aliveTick:
|
||||
err = adv.Alive()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
adv.Bye()
|
||||
adv.Close()
|
||||
break loop
|
||||
}
|
||||
select {
|
||||
|
||||
case <-quit:
|
||||
adv.Bye()
|
||||
adv.Close()
|
||||
os.Exit(0)
|
||||
break loop
|
||||
case <-aliveTick.C:
|
||||
if aliveErr := adv.Alive(); aliveErr != nil {
|
||||
ShowError(aliveErr, 0)
|
||||
ssdpState.Lock()
|
||||
if ssdpState.adv == adv {
|
||||
ssdpState.adv = nil
|
||||
}
|
||||
ssdpState.Unlock()
|
||||
adv.Bye()
|
||||
adv.Close()
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}(ad)
|
||||
}
|
||||
|
||||
return
|
||||
}(ad, done)
|
||||
|
||||
return
|
||||
}
|
||||
+2
-14
@@ -2,20 +2,14 @@ package src
|
||||
|
||||
import "time"
|
||||
|
||||
// Playlist : Enthält allen Playlistinformationen, die der Buffer benötigr
|
||||
// Playlist : per-provider buffer state (guarded by bufferMu in buffer_state.go)
|
||||
type Playlist struct {
|
||||
Folder string
|
||||
PlaylistID string
|
||||
PlaylistName string
|
||||
Tuner int
|
||||
|
||||
Clients map[int]ThisClient
|
||||
Streams map[int]ThisStream
|
||||
}
|
||||
|
||||
// ThisClient : Clientinfos
|
||||
type ThisClient struct {
|
||||
Connection int
|
||||
Streams map[int]*bufferStream
|
||||
}
|
||||
|
||||
// ThisStream : Enthält Informationen zu dem abzuspielenden Stream einer Playlist
|
||||
@@ -94,12 +88,6 @@ type DynamicStream struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
// ClientConnection : Client Verbindungen
|
||||
type ClientConnection struct {
|
||||
Connection int
|
||||
Error error
|
||||
}
|
||||
|
||||
// BandwidthCalculation : Bandbreitenberechnung für den Stream
|
||||
type BandwidthCalculation struct {
|
||||
NetworkBandwidth int
|
||||
|
||||
+1
-5
@@ -47,11 +47,7 @@ type LineupStatus struct {
|
||||
}
|
||||
|
||||
// Lineup : HDHR Lineup /lineup.json
|
||||
type Lineup []interface {
|
||||
//GuideName string `json:"GuideName"`
|
||||
//GuideNumber string `json:"GuideNumber"`
|
||||
//URL string `json:"URL"`
|
||||
}
|
||||
type Lineup []any
|
||||
|
||||
// LineupStream : HDHR einzelner Stream im Lineup
|
||||
type LineupStream struct {
|
||||
|
||||
+121
-119
@@ -1,5 +1,7 @@
|
||||
package src
|
||||
|
||||
import "xteve/src/internal/imgcache"
|
||||
|
||||
// SystemStruct : Beinhaltet alle Systeminformationen
|
||||
type SystemStruct struct {
|
||||
Addresses struct {
|
||||
@@ -8,18 +10,30 @@ type SystemStruct struct {
|
||||
XML string
|
||||
}
|
||||
|
||||
APIVersion string
|
||||
AppName string
|
||||
ARCH string
|
||||
Branch string
|
||||
Build string
|
||||
Compatibility string
|
||||
ConfigurationWizard bool
|
||||
DBVersion string
|
||||
Dev bool
|
||||
DeviceID string
|
||||
Domain string
|
||||
DVRLimit int
|
||||
APIVersion string
|
||||
AppName string
|
||||
ARCH string
|
||||
BackgroundProcess bool
|
||||
Branch string
|
||||
Build string
|
||||
Compatibility string
|
||||
ConfigurationWizard bool
|
||||
DBVersion string
|
||||
Dev bool
|
||||
DeviceID string
|
||||
Domain string
|
||||
PlexChannelLimit int
|
||||
UnfilteredChannelLimit int
|
||||
|
||||
FFmpeg struct {
|
||||
DefaultOptions string
|
||||
Path string
|
||||
}
|
||||
|
||||
VLC struct {
|
||||
DefaultOptions string
|
||||
Path string
|
||||
}
|
||||
|
||||
File struct {
|
||||
Authentication string
|
||||
@@ -31,6 +45,10 @@ type SystemStruct struct {
|
||||
XML string
|
||||
}
|
||||
|
||||
Compressed struct {
|
||||
GZxml string
|
||||
}
|
||||
|
||||
Flag struct {
|
||||
Branch string
|
||||
Debug int
|
||||
@@ -59,7 +77,6 @@ type SystemStruct struct {
|
||||
Name string
|
||||
OS string
|
||||
ScanInProgress int
|
||||
TimeForAutoUpdate string
|
||||
|
||||
Notification map[string]Notification
|
||||
|
||||
@@ -71,34 +88,18 @@ type SystemStruct struct {
|
||||
XML string
|
||||
}
|
||||
|
||||
GitHub struct {
|
||||
Branch string
|
||||
Repo string
|
||||
Update bool
|
||||
User string
|
||||
}
|
||||
|
||||
Update struct {
|
||||
Git string
|
||||
Name string
|
||||
}
|
||||
|
||||
URLBase string
|
||||
UDPxy string
|
||||
Version string
|
||||
WEB struct {
|
||||
Menu []string
|
||||
}
|
||||
}
|
||||
|
||||
// GitStruct : Updateinformationen von GitHub
|
||||
type GitStruct struct {
|
||||
Filename string `json:"filename"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// DataStruct : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
|
||||
type DataStruct struct {
|
||||
Cache struct {
|
||||
Images *imgcache.Cache
|
||||
ImagesCache []string
|
||||
ImagesFiles []string
|
||||
ImagesURLS []string
|
||||
@@ -129,18 +130,18 @@ type DataStruct struct {
|
||||
}
|
||||
|
||||
Streams struct {
|
||||
Active []interface{}
|
||||
All []interface{}
|
||||
Inactive []interface{}
|
||||
Active []any
|
||||
All []any
|
||||
Inactive []any
|
||||
}
|
||||
|
||||
XMLTV struct {
|
||||
Files []string
|
||||
Mapping map[string]interface{}
|
||||
Mapping map[string]any
|
||||
}
|
||||
|
||||
XEPG struct {
|
||||
Channels map[string]interface{}
|
||||
Channels map[string]any
|
||||
XEPGCount int64
|
||||
}
|
||||
}
|
||||
@@ -154,85 +155,89 @@ type Filter struct {
|
||||
|
||||
// XEPGChannelStruct : XEPG Struktur
|
||||
type XEPGChannelStruct struct {
|
||||
FileM3UID string `json:"_file.m3u.id,required"`
|
||||
FileM3UName string `json:"_file.m3u.name,required"`
|
||||
FileM3UPath string `json:"_file.m3u.path,required"`
|
||||
GroupTitle string `json:"group-title,required"`
|
||||
Name string `json:"name,required"`
|
||||
TvgID string `json:"tvg-id,required"`
|
||||
TvgLogo string `json:"tvg-logo,required"`
|
||||
TvgName string `json:"tvg-name,required"`
|
||||
URL string `json:"url,required"`
|
||||
UUIDKey string `json:"_uuid.key,required"`
|
||||
FileM3UID string `json:"_file.m3u.id"`
|
||||
FileM3UName string `json:"_file.m3u.name"`
|
||||
FileM3UPath string `json:"_file.m3u.path"`
|
||||
GroupTitle string `json:"group-title"`
|
||||
Name string `json:"name"`
|
||||
TvgID string `json:"tvg-id"`
|
||||
TvgLogo string `json:"tvg-logo"`
|
||||
TvgName string `json:"tvg-name"`
|
||||
URL string `json:"url"`
|
||||
UUIDKey string `json:"_uuid.key"`
|
||||
UUIDValue string `json:"_uuid.value,omitempty"`
|
||||
Values string `json:"_values,required"`
|
||||
XActive bool `json:"x-active,required"`
|
||||
XCategory string `json:"x-category,required"`
|
||||
XChannelID string `json:"x-channelID,required"`
|
||||
XEPG string `json:"x-epg,required"`
|
||||
XGroupTitle string `json:"x-group-title,required"`
|
||||
XMapping string `json:"x-mapping,required"`
|
||||
XmltvFile string `json:"x-xmltv-file,required"`
|
||||
XName string `json:"x-name,required"`
|
||||
XUpdateChannelIcon bool `json:"x-update-channel-icon,required"`
|
||||
XUpdateChannelName bool `json:"x-update-channel-name,required"`
|
||||
Values string `json:"_values"`
|
||||
XActive bool `json:"x-active"`
|
||||
XCategory string `json:"x-category"`
|
||||
XChannelID string `json:"x-channelID"`
|
||||
XEPG string `json:"x-epg"`
|
||||
XGroupTitle string `json:"x-group-title"`
|
||||
XMapping string `json:"x-mapping"`
|
||||
XmltvFile string `json:"x-xmltv-file"`
|
||||
XName string `json:"x-name"`
|
||||
XUpdateChannelIcon bool `json:"x-update-channel-icon"`
|
||||
XUpdateChannelName bool `json:"x-update-channel-name"`
|
||||
XDescription string `json:"x-description"`
|
||||
}
|
||||
|
||||
// M3UChannelStructXEPG : M3U Struktur für XEPG
|
||||
type M3UChannelStructXEPG struct {
|
||||
FileM3UID string `json:"_file.m3u.id,required"`
|
||||
FileM3UName string `json:"_file.m3u.name,required"`
|
||||
FileM3UPath string `json:"_file.m3u.path,required"`
|
||||
GroupTitle string `json:"group-title,required"`
|
||||
Name string `json:"name,required"`
|
||||
TvgID string `json:"tvg-id,required"`
|
||||
TvgLogo string `json:"tvg-logo,required"`
|
||||
TvgName string `json:"tvg-name,required"`
|
||||
URL string `json:"url,required"`
|
||||
UUIDKey string `json:"_uuid.key,required"`
|
||||
UUIDValue string `json:"_uuid.value,required"`
|
||||
Values string `json:"_values,required"`
|
||||
FileM3UID string `json:"_file.m3u.id"`
|
||||
FileM3UName string `json:"_file.m3u.name"`
|
||||
FileM3UPath string `json:"_file.m3u.path"`
|
||||
GroupTitle string `json:"group-title"`
|
||||
Name string `json:"name"`
|
||||
TvgID string `json:"tvg-id"`
|
||||
TvgLogo string `json:"tvg-logo"`
|
||||
TvgName string `json:"tvg-name"`
|
||||
URL string `json:"url"`
|
||||
UUIDKey string `json:"_uuid.key"`
|
||||
UUIDValue string `json:"_uuid.value"`
|
||||
Values string `json:"_values"`
|
||||
}
|
||||
|
||||
// FilterStruct : Filter Struktur
|
||||
type FilterStruct struct {
|
||||
Active bool `json:"active,required"`
|
||||
CaseSensitive bool `json:"caseSensitive,required"`
|
||||
Description string `json:"description,required"`
|
||||
Exclude string `json:"exclude,required"`
|
||||
Filter string `json:"filter,required"`
|
||||
Include string `json:"include,required"`
|
||||
Name string `json:"name,required"`
|
||||
Active bool `json:"active"`
|
||||
CaseSensitive bool `json:"caseSensitive"`
|
||||
Description string `json:"description"`
|
||||
Exclude string `json:"exclude"`
|
||||
Filter string `json:"filter"`
|
||||
Include string `json:"include"`
|
||||
Name string `json:"name"`
|
||||
Rule string `json:"rule,omitempty"`
|
||||
Type string `json:"type,required"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// StreamingURLS : Informationen zu allen streaming URL's
|
||||
type StreamingURLS struct {
|
||||
Streams map[string]StreamInfo `json:"channels,required"`
|
||||
Streams map[string]StreamInfo `json:"channels"`
|
||||
}
|
||||
|
||||
// StreamInfo : Informationen zum Kanal für die streaming URL
|
||||
type StreamInfo struct {
|
||||
ChannelNumber string `json:"channelNumber,required"`
|
||||
Name string `json:"name,required"`
|
||||
PlaylistID string `json:"playlistID,required"`
|
||||
URL string `json:"url,required"`
|
||||
URLid string `json:"urlID,required"`
|
||||
ChannelNumber string `json:"channelNumber"`
|
||||
Name string `json:"name"`
|
||||
PlaylistID string `json:"playlistID"`
|
||||
URL string `json:"url"`
|
||||
URLid string `json:"urlID"`
|
||||
}
|
||||
|
||||
// Notification : Notifikationen im Webinterface
|
||||
type Notification struct {
|
||||
Headline string `json:"headline,required"`
|
||||
Message string `json:"message,required"`
|
||||
New bool `json:"new,required"`
|
||||
Time string `json:"time,required"`
|
||||
Type string `json:"type,required"`
|
||||
Headline string `json:"headline"`
|
||||
Message string `json:"message"`
|
||||
New bool `json:"new"`
|
||||
Time string `json:"time"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// SettingsStrcut : Inhalt der settings.json
|
||||
type SettingsStrcut struct {
|
||||
// SettingsStruct : Inhalt der settings.json
|
||||
type SettingsStruct struct {
|
||||
API bool `json:"api"`
|
||||
UsePlexAPI bool `json:"use_plexAPI"`
|
||||
PlexURL string `json:"plex.url"`
|
||||
PlexToken string `json:"plex.token"`
|
||||
AuthenticationAPI bool `json:"authentication.api"`
|
||||
AuthenticationM3U bool `json:"authentication.m3u"`
|
||||
AuthenticationPMS bool `json:"authentication.pms"`
|
||||
@@ -241,43 +246,40 @@ type SettingsStrcut struct {
|
||||
BackupKeep int `json:"backup.keep"`
|
||||
BackupPath string `json:"backup.path"`
|
||||
Branch string `json:"git.branch,omitempty"`
|
||||
Buffer bool `json:"buffer"`
|
||||
Buffer string `json:"buffer"`
|
||||
BufferSize int `json:"buffer.size.kb"`
|
||||
BufferTimeout float64 `json:"buffer.timeout"`
|
||||
CacheImages bool `json:"cache.images"`
|
||||
EpgSource string `json:"epgSource"`
|
||||
FFmpegOptions string `json:"ffmpeg.options"`
|
||||
FFmpegPath string `json:"ffmpeg.path"`
|
||||
VLCOptions string `json:"vlc.options"`
|
||||
VLCPath string `json:"vlc.path"`
|
||||
FileM3U []string `json:"file,omitempty"` // Beim Wizard wird die M3U in ein Slice gespeichert
|
||||
FileXMLTV []string `json:"xmltv,omitempty"` // Altes Speichersystem der Provider XML Datei Slice (Wird für die Umwandlung auf das neue benötigt)
|
||||
|
||||
Files struct {
|
||||
HDHR map[string]interface{} `json:"hdhr"`
|
||||
M3U map[string]interface{} `json:"m3u"`
|
||||
XMLTV map[string]interface{} `json:"xmltv"`
|
||||
HDHR map[string]any `json:"hdhr"`
|
||||
M3U map[string]any `json:"m3u"`
|
||||
XMLTV map[string]any `json:"xmltv"`
|
||||
} `json:"files"`
|
||||
|
||||
FilesUpdate bool `json:"files.update"`
|
||||
Filter map[int64]interface{} `json:"filter"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Language string `json:"language"`
|
||||
LogEntriesRAM int `json:"log.entries.ram"`
|
||||
M3U8AdaptiveBandwidthMBPS int `json:"m3u8.adaptive.bandwidth.mbps"`
|
||||
MappingFirstChannel float64 `json:"mapping.first.channel"`
|
||||
Port string `json:"port"`
|
||||
SSDP bool `json:"ssdp"`
|
||||
TempPath string `json:"temp.path"`
|
||||
Tuner int `json:"tuner"`
|
||||
Update []string `json:"update"`
|
||||
UpdateURL string `json:"update.url,omitempty"`
|
||||
UserAgent string `json:"user.agent"`
|
||||
UUID string `json:"uuid"`
|
||||
Version string `json:"version"`
|
||||
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
|
||||
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
|
||||
}
|
||||
|
||||
// LanguageUI : Sprache für das WebUI
|
||||
type LanguageUI struct {
|
||||
Login struct {
|
||||
Failed string
|
||||
}
|
||||
FilesUpdate bool `json:"files.update"`
|
||||
Filter map[int64]any `json:"filter"`
|
||||
Key string `json:"key,omitempty"`
|
||||
LogEntriesRAM int `json:"log.entries.ram"`
|
||||
M3U8AdaptiveBandwidthMBPS int `json:"m3u8.adaptive.bandwidth.mbps"`
|
||||
MappingFirstChannel float64 `json:"mapping.first.channel"`
|
||||
Port string `json:"port"`
|
||||
SSDP bool `json:"ssdp"`
|
||||
TempPath string `json:"temp.path"`
|
||||
Tuner int `json:"tuner"`
|
||||
Update []string `json:"update"`
|
||||
UserAgent string `json:"user.agent"`
|
||||
UUID string `json:"uuid"`
|
||||
UDPxy string `json:"udpxy"`
|
||||
Version string `json:"version"`
|
||||
XepgMissingEPGMode string `json:"xepg.missing.epg.mode"`
|
||||
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
|
||||
WizardCompleted bool `json:"wizard.completed"`
|
||||
}
|
||||
+59
-43
@@ -3,14 +3,18 @@ package src
|
||||
// RequestStruct : Anfragen über die Websocket Schnittstelle
|
||||
type RequestStruct struct {
|
||||
// Befehle an xTeVe
|
||||
Cmd string `json:"cmd,required"`
|
||||
Cmd string `json:"cmd"`
|
||||
|
||||
// Client-chosen request id, echoed in the response so the web UI can
|
||||
// match answers on a long-lived connection.
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
// Benutzer
|
||||
DeleteUser bool `json:"deleteUser,omitempty"`
|
||||
UserData map[string]interface{} `json:"userData,omitempty"`
|
||||
DeleteUser bool `json:"deleteUser,omitempty"`
|
||||
UserData map[string]any `json:"userData,omitempty"`
|
||||
|
||||
// Mapping
|
||||
EpgMapping map[string]interface{} `json:"epgMapping,omitempty"`
|
||||
EpgMapping map[string]any `json:"epgMapping,omitempty"`
|
||||
|
||||
// Restore
|
||||
Base64 string `json:"base64,omitempty"`
|
||||
@@ -18,6 +22,9 @@ type RequestStruct struct {
|
||||
// Neue Werte für die Einstellungen (settings.json)
|
||||
Settings struct {
|
||||
API *bool `json:"api,omitempty"`
|
||||
UsePlexAPI *bool `json:"use_plexAPI,omitempty"`
|
||||
PlexURL *string `json:"plex.url,omitempty"`
|
||||
PlexToken *string `json:"plex.token,omitempty"`
|
||||
AuthenticationAPI *bool `json:"authentication.api,omitempty"`
|
||||
AuthenticationM3U *bool `json:"authentication.m3u,omitempty"`
|
||||
AuthenticationPMS *bool `json:"authentication.pms,omitempty"`
|
||||
@@ -25,32 +32,39 @@ type RequestStruct struct {
|
||||
AuthenticationXML *bool `json:"authentication.xml,omitempty"`
|
||||
BackupKeep *int `json:"backup.keep,omitempty"`
|
||||
BackupPath *string `json:"backup.path,omitempty"`
|
||||
Buffer *bool `json:"buffer,omitempty"`
|
||||
BufferSize *int `json:"buffer.size.kb, omitempty"`
|
||||
Buffer *string `json:"buffer,omitempty"`
|
||||
BufferSize *int `json:"buffer.size.kb,omitempty"`
|
||||
BufferTimeout *float64 `json:"buffer.timeout,omitempty"`
|
||||
CacheImages *bool `json:"cache.images,omitempty"`
|
||||
EpgSource *string `json:"epgSource,omitempty"`
|
||||
FFmpegOptions *string `json:"ffmpeg.options,omitempty"`
|
||||
FFmpegPath *string `json:"ffmpeg.path,omitempty"`
|
||||
VLCOptions *string `json:"vlc.options,omitempty"`
|
||||
VLCPath *string `json:"vlc.path,omitempty"`
|
||||
FilesUpdate *bool `json:"files.update,omitempty"`
|
||||
TempPath *string `json:"temp.path,omitempty"`
|
||||
Tuner *int `json:"tuner,omitempty"`
|
||||
UDPxy *string `json:"udpxy,omitempty"`
|
||||
Update *[]string `json:"update,omitempty"`
|
||||
UserAgent *string `json:"user.agent,omitempty"`
|
||||
XepgMissingEPGMode *string `json:"xepg.missing.epg.mode,omitempty"`
|
||||
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
|
||||
XteveAutoUpdate *bool `json:"xteveAutoUpdate,omitempty"`
|
||||
} `json:"settings,omitempty"`
|
||||
SchemeM3U *string `json:"scheme.m3u,omitempty"`
|
||||
SchemeXML *string `json:"scheme.xml,omitempty"`
|
||||
} `json:"settings"`
|
||||
|
||||
// Upload Logo
|
||||
Filename string `json:"filename,omitempty"`
|
||||
|
||||
// Filter
|
||||
Filter map[int64]interface{} `json:"filter,omitempty"`
|
||||
Filter map[int64]any `json:"filter,omitempty"`
|
||||
|
||||
// Dateien (M3U, HDHR, XMLTV)
|
||||
Files struct {
|
||||
HDHR map[string]interface{} `json:"hdhr,omitempty"`
|
||||
M3U map[string]interface{} `json:"m3u,omitempty"`
|
||||
XMLTV map[string]interface{} `json:"xmltv,omitempty"`
|
||||
} `json:"files,omitempty"`
|
||||
HDHR map[string]any `json:"hdhr,omitempty"`
|
||||
M3U map[string]any `json:"m3u,omitempty"`
|
||||
XMLTV map[string]any `json:"xmltv,omitempty"`
|
||||
} `json:"files"`
|
||||
|
||||
// Wizard
|
||||
Wizard struct {
|
||||
@@ -58,57 +72,59 @@ type RequestStruct struct {
|
||||
M3U *string `json:"m3u,omitempty"`
|
||||
Tuner *int `json:"tuner,omitempty"`
|
||||
XMLTV *string `json:"xmltv,omitempty"`
|
||||
} `json:"wizard,omitempty"`
|
||||
} `json:"wizard"`
|
||||
}
|
||||
|
||||
// ResponseStruct : Antworten an den Client (WEB)
|
||||
type ResponseStruct struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
ClientInfo struct {
|
||||
ARCH string `json:"arch"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
DVR string `json:"DVR"`
|
||||
EpgSource string `json:"epgSource"`
|
||||
Errors int `json:"errors"`
|
||||
M3U string `json:"m3u-url,required"`
|
||||
M3U string `json:"m3u-url"`
|
||||
OS string `json:"os"`
|
||||
Streams string `json:"streams"`
|
||||
UUID string `json:"uuid"`
|
||||
Version string `json:"version"`
|
||||
Warnings int `json:"warnings"`
|
||||
XEPGCount int64 `json:"xepg"`
|
||||
XML string `json:"xepg-url,required"`
|
||||
} `json:"clientInfo,omitempty"`
|
||||
XML string `json:"xepg-url"`
|
||||
} `json:"clientInfo"`
|
||||
|
||||
Data struct {
|
||||
Playlist struct {
|
||||
M3U struct {
|
||||
Groups struct {
|
||||
Text []string `json:"text,required"`
|
||||
Value []string `json:"value,required"`
|
||||
} `json:"groups,required"`
|
||||
} `json:"m3u,required"`
|
||||
} `json:"playlist,required"`
|
||||
Text []string `json:"text"`
|
||||
Value []string `json:"value"`
|
||||
} `json:"groups"`
|
||||
} `json:"m3u"`
|
||||
} `json:"playlist"`
|
||||
|
||||
StreamPreviewUI struct {
|
||||
Active []string `json:"activeStreams,required"`
|
||||
Inactive []string `json:"inactiveStreams,required"`
|
||||
Active []string `json:"activeStreams"`
|
||||
Inactive []string `json:"inactiveStreams"`
|
||||
}
|
||||
} `json:"data,required"`
|
||||
} `json:"data"`
|
||||
|
||||
Alert string `json:"alert,omitempty"`
|
||||
ConfigurationWizard bool `json:"configurationWizard,required"`
|
||||
Error string `json:"err,omitempty"`
|
||||
Log WebScreenLogStruct `json:"log,required"`
|
||||
LogoURL string `json:"logoURL,omitempty"`
|
||||
OpenLink string `json:"openLink,omitempty"`
|
||||
OpenMenu string `json:"openMenu,omitempty"`
|
||||
Reload bool `json:"reload,omitempty"`
|
||||
Settings SettingsStrcut `json:"settings,required"`
|
||||
Status bool `json:"status,required"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Users map[string]interface{} `json:"users,omitempty"`
|
||||
Wizard int `json:"wizard,omitempty"`
|
||||
XEPG map[string]interface{} `json:"xepg,required"`
|
||||
Alert string `json:"alert,omitempty"`
|
||||
ConfigurationWizard bool `json:"configurationWizard"`
|
||||
Error string `json:"err,omitempty"`
|
||||
Log WebScreenLogStruct `json:"log"`
|
||||
LogoURL string `json:"logoURL,omitempty"`
|
||||
OpenLink string `json:"openLink,omitempty"`
|
||||
OpenMenu string `json:"openMenu,omitempty"`
|
||||
Reload bool `json:"reload,omitempty"`
|
||||
Settings SettingsStruct `json:"settings"`
|
||||
Status bool `json:"status"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Users map[string]any `json:"users,omitempty"`
|
||||
Wizard int `json:"wizard,omitempty"`
|
||||
XEPG map[string]any `json:"xepg"`
|
||||
|
||||
Notification map[string]Notification `json:"notification,omitempty"`
|
||||
}
|
||||
@@ -125,7 +141,7 @@ type APIRequestStruct struct {
|
||||
type APIResponseStruct struct {
|
||||
EpgSource string `json:"epg.source,omitempty"`
|
||||
Error string `json:"err,omitempty"`
|
||||
Status bool `json:"status,required"`
|
||||
Status bool `json:"status"`
|
||||
StreamsActive int64 `json:"streams.active,omitempty"`
|
||||
StreamsAll int64 `json:"streams.all,omitempty"`
|
||||
StreamsXepg int64 `json:"streams.xepg,omitempty"`
|
||||
@@ -139,7 +155,7 @@ type APIResponseStruct struct {
|
||||
|
||||
// WebScreenLogStruct : Logs werden im RAM gespeichert und für das Webinterface bereitgestellt
|
||||
type WebScreenLogStruct struct {
|
||||
Errors int `json:"errors,required"`
|
||||
Log []string `json:"log,required"`
|
||||
Warnings int `json:"warnings,required"`
|
||||
Errors int `json:"errors"`
|
||||
Log []string `json:"log"`
|
||||
Warnings int `json:"warnings"`
|
||||
}
|
||||
+53
-1
@@ -42,12 +42,16 @@ type Program struct {
|
||||
Country []*Country `xml:"country"`
|
||||
EpisodeNum []*EpisodeNum `xml:"episode-num"`
|
||||
Poster []Poster `xml:"icon"`
|
||||
Credits Credits `xml:"credits,omitempty"` //`xml:",innerxml,omitempty"`
|
||||
Rating []Rating `xml:"rating"`
|
||||
StarRating []StarRating `xml:"star-rating"`
|
||||
Language []*Language `xml:"language"`
|
||||
Video Video `xml:"video"`
|
||||
Date string `xml:"date"`
|
||||
PreviouslyShown *PreviouslyShown `xml:"previously-shown"`
|
||||
New *New `xml:"new"`
|
||||
Live *Live `xml:"live"`
|
||||
Premiere *Live `xml:"premiere"`
|
||||
}
|
||||
|
||||
// Title : Programmtitel
|
||||
@@ -62,7 +66,7 @@ type SubTitle struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
//Desc : Programmbeschreibung
|
||||
// Desc : Programmbeschreibung
|
||||
type Desc struct {
|
||||
Lang string `xml:"lang,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
@@ -74,6 +78,19 @@ type Category struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Rating : Bewertung
|
||||
type Rating struct {
|
||||
System string `xml:"system,attr"`
|
||||
Value string `xml:"value"`
|
||||
Icon []Icon `xml:"icon"`
|
||||
}
|
||||
|
||||
// StarRating : Bewertung / Kritiken
|
||||
type StarRating struct {
|
||||
Value string `xml:"value"`
|
||||
System string `xml:"system,attr"`
|
||||
}
|
||||
|
||||
// Language : Sprachen
|
||||
type Language struct {
|
||||
Value string `xml:",chardata"`
|
||||
@@ -99,6 +116,41 @@ type Poster struct {
|
||||
Width string `xml:"width,attr"`
|
||||
}
|
||||
|
||||
// Credits : Credits
|
||||
type Credits struct {
|
||||
Director []Director `xml:"director,omitempty"`
|
||||
Actor []Actor `xml:"actor,omitempty"`
|
||||
Writer []Writer `xml:"writer,omitempty"`
|
||||
Presenter []Presenter `xml:"presenter,omitempty"`
|
||||
Producer []Producer `xml:"producer,omitempty"`
|
||||
}
|
||||
|
||||
// Director : Director
|
||||
type Director struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Actor : Actor
|
||||
type Actor struct {
|
||||
Value string `xml:",chardata"`
|
||||
Role string `xml:"role,attr,omitempty"`
|
||||
}
|
||||
|
||||
// Writer : Writer
|
||||
type Writer struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Presenter : Presenter
|
||||
type Presenter struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Producer : Producer
|
||||
type Producer struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Video : Video Metadaten
|
||||
type Video struct {
|
||||
Aspect string `xml:"aspect,omitempty"`
|
||||
|
||||
+111
-21
@@ -7,13 +7,14 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entwicklerinfos anzeigen
|
||||
func showDevInfo() {
|
||||
|
||||
if System.Dev == true {
|
||||
if System.Dev {
|
||||
|
||||
fmt.Print("\033[31m")
|
||||
fmt.Println("* * * * * D E V M O D E * * * * *")
|
||||
@@ -25,7 +26,6 @@ func showDevInfo() {
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alle Systemordner erstellen
|
||||
@@ -59,7 +59,7 @@ func createSystemFiles() (err error) {
|
||||
err = checkFile(filename)
|
||||
if err != nil {
|
||||
// Datei existiert nicht, wird jetzt erstellt
|
||||
err = saveMapToJSONFile(filename, make(map[string]interface{}))
|
||||
err = saveMapToJSONFile(filename, make(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -90,22 +90,35 @@ func createSystemFiles() (err error) {
|
||||
}
|
||||
|
||||
// Einstellungen laden und default Werte setzen (xTeVe)
|
||||
func loadSettings() (settings SettingsStrcut, err error) {
|
||||
func loadSettings() (settings SettingsStruct, err error) {
|
||||
|
||||
settingsMap, err := loadJSONFileToMap(System.File.Settings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deafult Werte setzten
|
||||
var defaults = make(map[string]interface{})
|
||||
var dataMap = make(map[string]interface{})
|
||||
var freshInstall = len(settingsMap) == 0
|
||||
|
||||
dataMap["xmltv"] = make(map[string]interface{})
|
||||
dataMap["m3u"] = make(map[string]interface{})
|
||||
dataMap["hdhr"] = make(map[string]interface{})
|
||||
// Deafult Werte setzten
|
||||
var defaults = make(map[string]any)
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
dataMap["xmltv"] = make(map[string]any)
|
||||
dataMap["m3u"] = make(map[string]any)
|
||||
dataMap["hdhr"] = make(map[string]any)
|
||||
|
||||
defaultFFmpegPath := ""
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
|
||||
containerFFmpegPath := "/usr/local/bin/ffmpeg"
|
||||
if checkFile(containerFFmpegPath) == nil {
|
||||
defaultFFmpegPath = containerFFmpegPath
|
||||
}
|
||||
}
|
||||
|
||||
defaults["api"] = false
|
||||
defaults["use_plexAPI"] = false
|
||||
defaults["plex.url"] = ""
|
||||
defaults["plex.token"] = ""
|
||||
defaults["authentication.api"] = false
|
||||
defaults["authentication.m3u"] = false
|
||||
defaults["authentication.pms"] = false
|
||||
@@ -113,29 +126,37 @@ func loadSettings() (settings SettingsStrcut, err error) {
|
||||
defaults["authentication.xml"] = false
|
||||
defaults["backup.keep"] = 10
|
||||
defaults["backup.path"] = System.Folder.Backup
|
||||
defaults["buffer"] = false
|
||||
defaults["buffer"] = "-"
|
||||
defaults["buffer.size.kb"] = 1024
|
||||
defaults["buffer.timeout"] = 500
|
||||
defaults["cache.images"] = false
|
||||
defaults["epgSource"] = "PMS"
|
||||
defaults["ffmpeg.path"] = defaultFFmpegPath
|
||||
defaults["ffmpeg.options"] = System.FFmpeg.DefaultOptions
|
||||
defaults["vlc.options"] = System.VLC.DefaultOptions
|
||||
defaults["files"] = dataMap
|
||||
defaults["files.update"] = true
|
||||
defaults["filter"] = make(map[string]interface{})
|
||||
defaults["filter"] = make(map[string]any)
|
||||
defaults["git.branch"] = System.Branch
|
||||
defaults["language"] = "en"
|
||||
defaults["log.entries.ram"] = 500
|
||||
defaults["mapping.first.channel"] = 1000
|
||||
defaults["xepg.missing.epg.mode"] = "strict"
|
||||
defaults["xepg.replace.missing.images"] = true
|
||||
defaults["m3u8.adaptive.bandwidth.mbps"] = 10
|
||||
defaults["port"] = "34400"
|
||||
defaults["ssdp"] = true
|
||||
defaults["tuner"] = 1
|
||||
defaults["update"] = []string{"0000"}
|
||||
defaults["user.agent"] = System.Name
|
||||
defaults["user.agent"] = defaultUserAgent
|
||||
defaults["uuid"] = createUUID()
|
||||
defaults["udpxy"] = ""
|
||||
defaults["version"] = System.DBVersion
|
||||
defaults["xteveAutoUpdate"] = true
|
||||
defaults["temp.path"] = System.Folder.Temp
|
||||
defaults["wizard.completed"] = !freshInstall
|
||||
var defaultTempPath = System.Folder.Temp
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
|
||||
defaultTempPath = System.Folder.Config + "tmp" + string(os.PathSeparator)
|
||||
}
|
||||
defaults["temp.path"] = defaultTempPath
|
||||
|
||||
// Default Werte setzen
|
||||
for key, value := range defaults {
|
||||
@@ -159,15 +180,37 @@ func loadSettings() (settings SettingsStrcut, err error) {
|
||||
showInfo(fmt.Sprintf("Git Branch:Switching Git Branch to -> %s", settings.Branch))
|
||||
}
|
||||
|
||||
if len(settings.FFmpegPath) == 0 {
|
||||
containerFFmpegPath := "/usr/local/bin/ffmpeg"
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 && checkFile(containerFFmpegPath) == nil {
|
||||
settings.FFmpegPath = containerFFmpegPath
|
||||
} else {
|
||||
settings.FFmpegPath = searchFileInOS("ffmpeg")
|
||||
}
|
||||
}
|
||||
|
||||
if len(settings.VLCPath) == 0 {
|
||||
settings.VLCPath = searchFileInOS("cvlc")
|
||||
}
|
||||
|
||||
settings.Version = System.DBVersion
|
||||
|
||||
err = saveSettings(settings)
|
||||
|
||||
// Warung wenn FFmpeg nicht gefunden wurde
|
||||
if len(Settings.FFmpegPath) == 0 && Settings.Buffer == "ffmpeg" {
|
||||
showWarning(2020)
|
||||
}
|
||||
|
||||
if len(Settings.VLCPath) == 0 && Settings.Buffer == "vlc" {
|
||||
showWarning(2021)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Einstellungen speichern (xTeVe)
|
||||
func saveSettings(settings SettingsStrcut) (err error) {
|
||||
func saveSettings(settings SettingsStruct) (err error) {
|
||||
|
||||
if settings.BackupKeep == 0 {
|
||||
settings.BackupKeep = 10
|
||||
@@ -181,16 +224,44 @@ func saveSettings(settings SettingsStrcut) (err error) {
|
||||
settings.BufferTimeout = 0
|
||||
}
|
||||
|
||||
var userAgent = strings.TrimSpace(settings.UserAgent)
|
||||
if len(userAgent) == 0 || userAgent == System.Name {
|
||||
settings.UserAgent = defaultUserAgent
|
||||
}
|
||||
|
||||
settings.XepgMissingEPGMode = strings.ToLower(strings.TrimSpace(settings.XepgMissingEPGMode))
|
||||
if settings.XepgMissingEPGMode != "relaxed" {
|
||||
settings.XepgMissingEPGMode = "strict"
|
||||
}
|
||||
|
||||
settings.TempPath = strings.TrimRight(settings.TempPath, string(os.PathSeparator)) + string(os.PathSeparator)
|
||||
System.Folder.Temp = settings.TempPath + settings.UUID + string(os.PathSeparator)
|
||||
|
||||
err = writeByteToFile(System.File.Settings, []byte(mapToJSON(settings)))
|
||||
err = checkFolder(System.Folder.Temp)
|
||||
if err != nil {
|
||||
|
||||
fallbackTempPath := System.Folder.Config + "tmp" + string(os.PathSeparator)
|
||||
fallbackTempFolder := fallbackTempPath + settings.UUID + string(os.PathSeparator)
|
||||
fallbackErr := checkFolder(fallbackTempFolder)
|
||||
|
||||
if fallbackErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.TempPath = fallbackTempPath
|
||||
System.Folder.Temp = fallbackTempFolder
|
||||
showInfo(fmt.Sprintf("Temporary Folder:Fallback to %s", getPlatformPath(System.Folder.Temp)))
|
||||
|
||||
}
|
||||
|
||||
err = writePrivateFile(System.File.Settings, []byte(mapToJSON(settings)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Settings = settings
|
||||
|
||||
if System.Dev == true {
|
||||
if System.Dev {
|
||||
Settings.UUID = "2019-01-DEV-xTeVe!"
|
||||
}
|
||||
|
||||
@@ -200,6 +271,21 @@ func saveSettings(settings SettingsStrcut) (err error) {
|
||||
}
|
||||
|
||||
// Zugriff über die Domain ermöglichen
|
||||
// streamingURLsMu guards Data.Cache.StreamingURLS, which is written by
|
||||
// /lineup.json and /m3u/ requests while /stream/ requests read it.
|
||||
var streamingURLsMu sync.Mutex
|
||||
|
||||
// streamingURLsSnapshot : copy of the URL cache for persisting to disk.
|
||||
func streamingURLsSnapshot() map[string]StreamInfo {
|
||||
streamingURLsMu.Lock()
|
||||
defer streamingURLsMu.Unlock()
|
||||
var out = make(map[string]StreamInfo, len(Data.Cache.StreamingURLS))
|
||||
for k, v := range Data.Cache.StreamingURLS {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func setGlobalDomain(domain string) {
|
||||
|
||||
System.Domain = domain
|
||||
@@ -230,7 +316,6 @@ func setGlobalDomain(domain string) {
|
||||
System.Addresses.XML = getErrMsg(2106)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UUID generieren
|
||||
@@ -252,7 +337,6 @@ func setDeviceID() {
|
||||
System.DeviceID = fmt.Sprintf("%s:%d", id, Settings.Tuner)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Provider Streaming-URL zu xTeVe Streaming-URL konvertieren
|
||||
@@ -261,6 +345,9 @@ func createStreamingURL(streamingType, playlistID, channelNumber, channelName, u
|
||||
var streamInfo StreamInfo
|
||||
var serverProtocol string
|
||||
|
||||
streamingURLsMu.Lock()
|
||||
defer streamingURLsMu.Unlock()
|
||||
|
||||
if len(Data.Cache.StreamingURLS) == 0 {
|
||||
Data.Cache.StreamingURLS = make(map[string]StreamInfo)
|
||||
}
|
||||
@@ -300,6 +387,9 @@ func createStreamingURL(streamingType, playlistID, channelNumber, channelName, u
|
||||
|
||||
func getStreamInfo(urlID string) (streamInfo StreamInfo, err error) {
|
||||
|
||||
streamingURLsMu.Lock()
|
||||
defer streamingURLsMu.Unlock()
|
||||
|
||||
if len(Data.Cache.StreamingURLS) == 0 {
|
||||
|
||||
tmp, err := loadJSONFileToMap(System.File.URLS)
|
||||
|
||||
+149
-48
@@ -7,11 +7,14 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
@@ -42,13 +45,22 @@ func checkFolder(path string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prüft ob die datei im Dateisystem existiert
|
||||
// Prüft ob die Datei im Dateisystem existiert
|
||||
func checkFile(filename string) (err error) {
|
||||
|
||||
var file = getPlatformFile(filename)
|
||||
|
||||
if _, err = os.Stat(file); os.IsNotExist(err) {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.Mode().IsDir() {
|
||||
err = fmt.Errorf("%s: %s", file, getErrMsg(1072))
|
||||
}
|
||||
|
||||
return
|
||||
@@ -77,11 +89,12 @@ func GetUserHomeDirectory() (userHomeDirectory string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prüft Dateiberechtigung
|
||||
func checkFilePermission(dir string) (err error) {
|
||||
|
||||
var filename = dir + "permission.test"
|
||||
|
||||
err = ioutil.WriteFile(filename, []byte(""), 0644)
|
||||
err = os.WriteFile(filename, []byte(""), 0644)
|
||||
if err == nil {
|
||||
err = os.RemoveAll(filename)
|
||||
}
|
||||
@@ -109,13 +122,34 @@ func getFilenameFromPath(path string) (file string) {
|
||||
return filepath.Base(path)
|
||||
}
|
||||
|
||||
// Nicht mehr verwendete Systemdaten löschen
|
||||
func removeOldSystemData() {
|
||||
// Temporären Ordner löschen
|
||||
os.RemoveAll(System.Folder.Temp)
|
||||
// Sucht eine Datei im OS
|
||||
func searchFileInOS(file string) (path string) {
|
||||
|
||||
switch runtime.GOOS {
|
||||
|
||||
case "linux", "darwin", "freebsd":
|
||||
var args = file
|
||||
var cmd = exec.Command("which", strings.Split(args, " ")...)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
|
||||
var slice = strings.Split(strings.Replace(string(out), "\r\n", "\n", -1), "\n")
|
||||
|
||||
if len(slice) > 0 {
|
||||
path = strings.Trim(slice[0], "\r\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
func removeChildItems(dir string) error {
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*"))
|
||||
@@ -136,7 +170,7 @@ func removeChildItems(dir string) error {
|
||||
}
|
||||
|
||||
// JSON
|
||||
func mapToJSON(tmpMap interface{}) string {
|
||||
func mapToJSON(tmpMap any) string {
|
||||
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
if err != nil {
|
||||
@@ -146,30 +180,22 @@ func mapToJSON(tmpMap interface{}) string {
|
||||
return string(jsonString)
|
||||
}
|
||||
|
||||
func jsonToMap(content string) map[string]interface{} {
|
||||
func jsonToMap(content string) map[string]any {
|
||||
|
||||
var tmpMap = make(map[string]interface{})
|
||||
var tmpMap = make(map[string]any)
|
||||
json.Unmarshal([]byte(content), &tmpMap)
|
||||
|
||||
return (tmpMap)
|
||||
}
|
||||
|
||||
func jsonToMapInt64(content string) map[int64]interface{} {
|
||||
|
||||
var tmpMap = make(map[int64]interface{})
|
||||
json.Unmarshal([]byte(content), &tmpMap)
|
||||
|
||||
return (tmpMap)
|
||||
}
|
||||
|
||||
func jsonToInterface(content string) (tmpMap interface{}, err error) {
|
||||
func jsonToInterface(content string) (tmpMap any, err error) {
|
||||
|
||||
err = json.Unmarshal([]byte(content), &tmpMap)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func saveMapToJSONFile(file string, tmpMap interface{}) error {
|
||||
func saveMapToJSONFile(file string, tmpMap any) error {
|
||||
|
||||
var filename = getPlatformFile(file)
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
@@ -178,38 +204,67 @@ func saveMapToJSONFile(file string, tmpMap interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filename, []byte(jsonString), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return writeFileAtomic(filename, []byte(jsonString), 0644)
|
||||
}
|
||||
|
||||
func loadJSONFileToMap(file string) (tmpMap map[string]interface{}, err error) {
|
||||
func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) {
|
||||
|
||||
f, err := os.Open(getPlatformFile(file))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content, err := ioutil.ReadAll(f)
|
||||
content, err := io.ReadAll(f)
|
||||
|
||||
if err == nil {
|
||||
err = json.Unmarshal([]byte(content), &tmpMap)
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// toFloat64 : Numeric value of a JSON decoded field; 0 for anything that
|
||||
// is not a number (missing key, wrong type).
|
||||
func toFloat64(v any) float64 {
|
||||
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n
|
||||
case int:
|
||||
return float64(n)
|
||||
case int64:
|
||||
return float64(n)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// removeStrings : Copy of list without the entries present in remove.
|
||||
// Order of the remaining entries is kept.
|
||||
func removeStrings(list []string, remove map[string]bool) []string {
|
||||
|
||||
var result = make([]string, 0, len(list))
|
||||
|
||||
for _, s := range list {
|
||||
if !remove[s] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Binary
|
||||
func readByteFromFile(file string) (content []byte, err error) {
|
||||
|
||||
f, err := os.Open(getPlatformFile(file))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content, err = ioutil.ReadAll(f)
|
||||
f.Close()
|
||||
content, err = io.ReadAll(f)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -217,7 +272,48 @@ func readByteFromFile(file string) (content []byte, err error) {
|
||||
func writeByteToFile(file string, data []byte) (err error) {
|
||||
|
||||
var filename = getPlatformFile(file)
|
||||
err = ioutil.WriteFile(filename, data, 0644)
|
||||
err = writeFileAtomic(filename, data, 0644)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// writeFileAtomic : Writes data to a temporary file in the target directory,
|
||||
// fsyncs it and renames it over name, so readers (and a crash mid-write)
|
||||
// never see a truncated or half-written file. The temp file is removed on
|
||||
// any error.
|
||||
func writeFileAtomic(name string, data []byte, perm os.FileMode) (err error) {
|
||||
|
||||
tmp, err := os.CreateTemp(filepath.Dir(name), ".tmp-*")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var tmpName = tmp.Name()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = tmp.Write(data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = tmp.Sync(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = tmp.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Chmod(tmpName, perm); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Rename(tmpName, name)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -232,7 +328,7 @@ func readStringFromFile(file string) (str string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
content, err = ioutil.ReadFile(filename)
|
||||
content, err = os.ReadFile(filename)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
@@ -276,6 +372,22 @@ func resolveHostIP() (err error) {
|
||||
|
||||
}
|
||||
|
||||
if len(System.IPAddress) == 0 {
|
||||
|
||||
switch len(System.IPAddressesV4) {
|
||||
|
||||
case 0:
|
||||
if len(System.IPAddressesV6) > 0 {
|
||||
System.IPAddress = System.IPAddressesV6[0]
|
||||
}
|
||||
|
||||
default:
|
||||
System.IPAddress = System.IPAddressesV4[0]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.Hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -300,7 +412,7 @@ func randomString(n int) string {
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func parseTemplate(content string, tmpMap map[string]interface{}) (result string) {
|
||||
func parseTemplate(content string, tmpMap map[string]any) (result string) {
|
||||
|
||||
t := template.Must(template.New("template").Parse(content))
|
||||
|
||||
@@ -336,17 +448,6 @@ func indexOfFloat64(element float64, data []float64) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func indexOfInt(element int, data []int) int {
|
||||
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return (k)
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func getMD5(str string) string {
|
||||
|
||||
md5Hasher := md5.New()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteFileAtomic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "settings.json")
|
||||
|
||||
if err := writeFileAtomic(file, []byte("first"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeFileAtomic(file, []byte("second"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "second" {
|
||||
t.Errorf("content = %q, want %q", got, "second")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Mode().Perm() != 0600 {
|
||||
t.Errorf("mode = %o, want 0600", fi.Mode().Perm())
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Name() != "settings.json" {
|
||||
t.Errorf("unexpected leftover file %q", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFileAtomicMissingDir(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "missing", "settings.json")
|
||||
if err := writeFileAtomic(file, []byte("x"), 0644); err == nil {
|
||||
t.Fatal("expected an error for a missing directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveStrings(t *testing.T) {
|
||||
list := []string{"a.xml", "b.xml", "c.xml", "d.xml"}
|
||||
got := removeStrings(list, map[string]bool{"b.xml": true, "d.xml": true})
|
||||
want := []string{"a.xml", "c.xml"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("removeStrings = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
// Nothing to remove: same content, original untouched.
|
||||
got = removeStrings(list, nil)
|
||||
if !reflect.DeepEqual(got, list) {
|
||||
t.Errorf("removeStrings(nil) = %v, want %v", got, list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToFloat64(t *testing.T) {
|
||||
cases := []struct {
|
||||
in any
|
||||
want float64
|
||||
}{
|
||||
{float64(3), 3},
|
||||
{int(2), 2},
|
||||
{int64(1), 1},
|
||||
{"x", 0},
|
||||
{nil, 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := toFloat64(c.in); got != c.want {
|
||||
t.Errorf("toFloat64(%v) = %v, want %v", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
-273
@@ -1,273 +0,0 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
up2date "../src/internal/up2date/client"
|
||||
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// BinaryUpdate : Binary Update Prozess. Git Branch master und beta wird von GitHub geladen.
|
||||
func BinaryUpdate() (err error) {
|
||||
|
||||
if System.GitHub.Update == false {
|
||||
showWarning(2099)
|
||||
return
|
||||
}
|
||||
|
||||
var debug string
|
||||
|
||||
var updater = &up2date.Updater
|
||||
updater.Name = System.Update.Name
|
||||
updater.Branch = System.Branch
|
||||
|
||||
up2date.Init()
|
||||
|
||||
switch System.Branch {
|
||||
|
||||
// Update von GitHub
|
||||
case "master", "beta":
|
||||
|
||||
var gitInfo = fmt.Sprintf("%s/%s/info.json?raw=true", System.Update.Git, System.Branch)
|
||||
var zipFile = fmt.Sprintf("%s/%s/%s_%s_%s.zip?raw=true", System.Update.Git, System.Branch, System.AppName, System.OS, System.ARCH)
|
||||
var body []byte
|
||||
|
||||
var git GitStruct
|
||||
|
||||
resp, err := http.Get(gitInfo)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
err = fmt.Errorf(fmt.Sprintf("Update Server: %s (%s)", http.StatusText(resp.StatusCode), gitInfo))
|
||||
ShowError(err, 6003)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = fmt.Errorf(fmt.Sprintf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), gitInfo))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
|
||||
err = json.Unmarshal(body, &git)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Response.Status = true
|
||||
updater.Response.UpdateZIP = zipFile
|
||||
updater.Response.Version = git.Version
|
||||
updater.Response.Filename = git.Filename
|
||||
|
||||
// Update vom eigenen Server
|
||||
default:
|
||||
|
||||
updater.URL = Settings.UpdateURL
|
||||
|
||||
if len(updater.URL) == 0 {
|
||||
showInfo(fmt.Sprintf("Update URL:No server URL specified, update will not be performed. Branch: %s", System.Branch))
|
||||
return
|
||||
}
|
||||
|
||||
showInfo("Update URL:" + updater.URL)
|
||||
fmt.Println("-----------------")
|
||||
|
||||
// Versionsinformationen vom Server laden
|
||||
err = up2date.GetVersion()
|
||||
if err != nil {
|
||||
|
||||
debug = fmt.Sprintf(err.Error())
|
||||
showDebug(debug, 1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(updater.Response.Reason) > 0 {
|
||||
|
||||
err = fmt.Errorf(fmt.Sprintf("Update Server: %s", updater.Response.Reason))
|
||||
ShowError(err, 6002)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var currentVersion = System.Version + "." + System.Build
|
||||
|
||||
// Versionsnummer überprüfen
|
||||
if updater.Response.Version > currentVersion && updater.Response.Status == true {
|
||||
|
||||
if Settings.XteveAutoUpdate == true {
|
||||
// Update durchführen
|
||||
var fileType, url string
|
||||
|
||||
showInfo(fmt.Sprintf("Update Available:Version: %s", updater.Response.Version))
|
||||
|
||||
switch System.Branch {
|
||||
|
||||
// Update von GitHub
|
||||
case "master", "beta":
|
||||
showInfo(fmt.Sprintf("Update Server:GitHub"))
|
||||
|
||||
// Update vom eigenen Server
|
||||
default:
|
||||
showInfo(fmt.Sprintf("Update Server:%s", Settings.UpdateURL))
|
||||
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("Start Update:Branch: %s", updater.Branch))
|
||||
|
||||
// Neue Version als BIN Datei herunterladen
|
||||
if len(updater.Response.UpdateBIN) > 0 {
|
||||
url = updater.Response.UpdateBIN
|
||||
fileType = "bin"
|
||||
}
|
||||
|
||||
// Neue Version als ZIP Datei herunterladen
|
||||
if len(updater.Response.UpdateZIP) > 0 {
|
||||
url = updater.Response.UpdateZIP
|
||||
fileType = "zip"
|
||||
}
|
||||
|
||||
if len(url) > 0 {
|
||||
|
||||
err = up2date.DoUpdate(fileType, updater.Response.Filename)
|
||||
if err != nil {
|
||||
ShowError(err, 6002)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// Hinweis ausgeben
|
||||
showWarning(6004)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func conditionalUpdateChanges() (err error) {
|
||||
|
||||
checkVersion:
|
||||
settingsMap, err := loadJSONFileToMap(System.File.Settings)
|
||||
if err != nil || len(settingsMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if settingsVersion, ok := settingsMap["version"].(string); ok {
|
||||
|
||||
// Letzte Kompatible Version (1.4.4)
|
||||
if settingsVersion < System.Compatibility {
|
||||
err = errors.New(getErrMsg(1013))
|
||||
return
|
||||
}
|
||||
|
||||
switch settingsVersion {
|
||||
|
||||
case "1.4.4":
|
||||
// UUID Wert in xepg.json setzen
|
||||
err = setValueForUUID()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
|
||||
if oldFilter, ok := settingsMap["filter"].([]interface{}); ok {
|
||||
var newFilterMap = convertToNewFilter(oldFilter)
|
||||
settingsMap["filter"] = newFilterMap
|
||||
|
||||
settingsMap["version"] = "2.0.0"
|
||||
|
||||
err = saveMapToJSONFile(System.File.Settings, settingsMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
goto checkVersion
|
||||
|
||||
} else {
|
||||
err = errors.New(getErrMsg(1030))
|
||||
return
|
||||
}
|
||||
|
||||
case "2.0.0":
|
||||
// Falls es in einem späteren Update Änderungen an der Datenbank gibt, geht es hier weiter
|
||||
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// settings.json ist zu alt (älter als Version 1.4.4)
|
||||
err = errors.New(getErrMsg(1013))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func convertToNewFilter(oldFilter []interface{}) (newFilterMap map[int]interface{}) {
|
||||
|
||||
newFilterMap = make(map[int]interface{})
|
||||
|
||||
switch reflect.TypeOf(oldFilter).Kind() {
|
||||
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(oldFilter)
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
|
||||
var newFilter FilterStruct
|
||||
newFilter.Active = true
|
||||
newFilter.Name = fmt.Sprintf("Custom filter %d", i+1)
|
||||
newFilter.Filter = s.Index(i).Interface().(string)
|
||||
newFilter.Type = "custom-filter"
|
||||
newFilter.CaseSensitive = false
|
||||
|
||||
newFilterMap[i] = newFilter
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setValueForUUID() (err error) {
|
||||
|
||||
xepg, err := loadJSONFileToMap(System.File.XEPG)
|
||||
|
||||
for _, c := range xepg {
|
||||
|
||||
var xepgChannel = c.(map[string]interface{})
|
||||
|
||||
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {
|
||||
|
||||
if value, ok := xepgChannel[uuidKey].(string); ok {
|
||||
|
||||
if len(value) > 0 {
|
||||
xepgChannel["_uuid.value"] = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = saveMapToJSONFile(System.File.XEPG, xepg)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package src
|
||||
|
||||
import "strings"
|
||||
|
||||
const defaultUserAgent = "otg/1.5.1 (AppleTv Apple TV 4; tvOS16.0; appletv.client) libcurl/7.58.0 OpenSSL/1.0.2o zlib/1.2.11 clib/1.8.56"
|
||||
|
||||
func getUserAgent() string {
|
||||
|
||||
var userAgent = strings.TrimSpace(Settings.UserAgent)
|
||||
if len(userAgent) == 0 {
|
||||
return defaultUserAgent
|
||||
}
|
||||
|
||||
return userAgent
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package src
|
||||
|
||||
var webUI = make(map[string]interface{})
|
||||
|
||||
func loadHTMLMap() {
|
||||
|
||||
webUI["html/configuration.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvY29uZmlndXJhdGlvbl90cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvbmV0d29ya190cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvbWVudV90cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvc2V0dGluZ3NfdHMuanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL2Jhc2VfdHMuanMiPjwvc2NyaXB0PgogIDwvaGVhZD4KCiAgICA8Ym9keSBvbmxvYWQ9ImphdmFzY3JpcHQ6IHJlYWR5Rm9yQ29uZmlndXJhdGlvbigwKTsiPgogICAgICAgICAgCiAgICAgIDxkaXYgaWQ9ImxvYWRpbmciIGNsYXNzPSJibG9jayI+CiAgICAgICAgPGRpdiBjbGFzcz0ibG9hZGVyIj48L2Rpdj4KICAgICAgPC9kaXY+CgogICAgICA8ZGl2IGlkPSJoZWFkZXIiIGNsYXNzPSJpbWdDZW50ZXIiPjwvZGl2PgogICAgICA8ZGl2IGlkPSJib3giPgogICAgICAgIAogICAgICAgIDx0YWJsZSBpZD0iY2xpZW50SW5mbyIgY2xhc3M9InZpc2libGUiPgogICAgICAgICAgPHRyPgogICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5WZXJzaW9uOjwvdGQ+CiAgICAgICAgICAgIDx0ZCBpZD0idmVyc2lvbiIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5PUzo8L3RkPgogICAgICAgICAgICA8dGQgaWQ9Im9zIiBjbGFzcz0idGRWYWwiPiZuYnNwOzwvdGQ+CiAgICAgICAgICA8L3RyPgogICAgICAgICAgPHRyPgogICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5VVUlEOjwvdGQ+CiAgICAgICAgICAgIDx0ZCBpZD0idXVpZCIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5BcmNoOjwvdGQ+CiAgICAgICAgICAgIDx0ZCBpZD0iYXJjaCIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgPC90cj4KICAgICAgICAgIDx0cj4KICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+U3RyZWFtczo8L3RkPgogICAgICAgICAgICA8dGQgaWQ9InN0cmVhbXMiIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+RFZSOjwvdGQ+CiAgICAgICAgICAgIDx0ZCBpZD0iRFZSIiBjbGFzcz0idGRWYWwiPiZuYnNwOzwvdGQ+CiAgICAgICAgICA8L3RyPgogICAgICAgIDwvdGFibGU+CiAgICAgICAgCiAgICAgICAgPGRpdiBpZD0iaGVhZGxpbmUiPgogICAgICAgICAgPGgxIGlkPSJoZWFkLXRleHQiIGNsYXNzPSJjZW50ZXIiPkNvbmZpZ3VyYXRpb248L2gxPiAgICAgIAogICAgICAgIDwvZGl2PgogICAgICAgIDxwIGlkPSJlcnIiIGNsYXNzPSJlcnJvck1zZyBjZW50ZXIiPjwvcD4gICAKICAgICAgICA8ZGl2IGlkPSJjb250ZW50Ij4KICAgICAgICAgICAgCiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBpZD0iYm94LWZvb3RlciI+CiAgICAgICAgICA8aW5wdXQgaWQ9Im5leHQiIGNsYXNzPSIiIHR5cGU9ImJ1dHRvbiIgbmFtZT0ibmV4dCIgdmFsdWU9Ik5leHQiIG9uY2xpY2s9ImphdmFzY3JpcHQ6IHNhdmVXaXphcmQoKTsiPgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIDwvYm9keT4KPC9odG1sPg=="
|
||||
webUI["html/img/x_ transparent.png"] = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAA6ppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0xMS0xNVQxNzoxMTo3NjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MTQ0PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj4xNDQ8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI1NjwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoeXQb1AAAaJElEQVR4Ae2dCZhcVZXHz+1KSFiGkISkqyIoOgI68EkgOBNkEcQRlIFRZDGABAQEhInGbxw+RUZRNhWRLawDYR0GHAaHUVkGvwGURUcgwIiAOGxjV3UHAslHQrbuM/9TlQ6d7qruWt527/u/7+uuqvvucu7vvnveuffde54IDxIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARLwiIDzQtayHi0q23shaydCOnlISu7nnWThRdrFOkPWyCleyNqJkF1yjhTd8k6yiDvtuLgLiCR/lVnIZ14keWU7k2Xyuu4oU92r2RazQ+nWypXI4W86zCXbyZ28KBvJ97MtpEhX1gWsyleS08TJU17I2omQKpvLarmqkywyn7ZHPw9rLvTOvwbX6xEyxS3Nenv4oQCcWwmQc/D3dtaBdiyfyv5S1mM7zieLGSzWEsS6KIuiRSzTGTD9H404z1iy80MBWNVL7hn8nx8LhaxlqnKBvKbvyppYHctTM/0nd5xPljNwcrcUs2/6DyL0RwGYxDOcjR3vGBQ+4M8tMElmdQ3n6NGjYPofGE6F6tTESRmm/1xxTuuczWSQH08BhqJbqlNkuTyJoK2GBgf53S6mkrvB+7qZ6b9Gfod6hHv3dzKAGbX9pNvd51N7+WUBGNlJbgm07FH4G/AJdFuyqlwotXFzW8kzk2itXAFZwu38BlrlPN86v4ntnwIwqUvuASiAc+1r4MdkqXUef6tZM/0P8rcCTUmO9RvyraZiZiySf0OAQYCq46QiD0Lz7jYYFOxnARZPt7vZu/r1aREKzEz/Kd7J3rzAS2Si7IxHfq80nyQ7Mf20AIyfc2thBRyJv2XZwRmTJP14dNar3THlHl+2Nesl5M4vUpDjfe381vD+KgCTvuhehAI4yb4GfkzFjMdlXtWxV4+EvH/rlcytCutkASwzr59K+TsEGNpYZb0OQ4G5Q4OC/N4ln4PSuzXzdcuH6b8I4/7ZsERXZb49RhHQbwtgsGIFORWWwAuDP4P9HJBLsUpwWubrt1Yuh4zhmv5O3pLxUMaed367jsJQANPdW7AA5kAJrMl85+hMwC2RfEFnWcScuqJHoIRPx1xK2tmfKtPcc2kLEUX5YSgAIzHD/Rb/T48CSqbzUDkUVsAhmZTRTP8BuTiTskUllJMb8Bj6+qiySzufMOYABimq2mLMe/DzrweDgvx00iebyg6yuXstU/XrUZsQC/nu/5yMk13FLM5AjnAsAGsQW4M93tZiy+JA2qd+NVSmYzn0JfVPphRa0TkoOeTOvwqdf05Ind+ulLAUgNVomivj/7H2NehDMQnVq5/JRB1tjcJAxhRS1GC65Gvo/E9EnW3a+YWnAIxoyf0M/8Mei1o9bW2AbY5K++ivzvpPTVuMGMv/CR6/ZsviiqiyYSoAg1OSf8BQ4KmIOGUzG8XO8+UpK7qKfg5wsmGJxNNKr2C+5bh4sk4/17AmAYfzLOtf4PGgPR3YePipoH53YcVd0d2ZeJ3M9O+vrvUP8+7vsJOhIPvA9P9V4mwTKjBcC8AA5sWLkGK77Zua/Hbb2vLkMDu/XT8qZ4bc+a2KYSsAq6F5EXLyb/Y12EMx4HkbvgOSPCp6ODrIwUkWmWhZTn4BquckWmYKhYU9BBgEmhcvQk4OgNUT/3sFKjodE5C2zddWJoZ32DqLcTJz3ROl8Oo3pEbhWwBW2bx4EVK4FF+ik4a0bzxftbozMdTOr7Bs5uah89vFkQ8FYDWteREK3aR7l6yCR+E4j7Iehg7y2TiLSDnv8zFsvDtlGRIrPh9DgEGcefEiVMC7BbqdLYmO9gjf9H8UD1b3worS0DeVrb8u8mMBWJXNi5DgjS1Olq4nEOKXfrkarxjbPPKqaXUnYpimv8ibuC5wbeSn89v1kS8FYDUuuZfQ0KF7EdoaQ4HzrbqRHWU9FKb/IZHll7WMnHyx6mEqa3LFLE++hgBDYZZ1IS7oY4YGBfe9gF2RUfiprzkh+R14Zd8ZSXuNeBXG/Se2l9TvVPlVAH26GdZ5PY7m29bvJhxFeicvYyXbjh3vYCvrbej8h45Skr+nnDyNcf9fwfQP/72TdVopf0OAQQi1Pd1hexFSeQ+W6v5gsMptfdZM/zA7v8gKMDHXXrns/HY95FcBWO1nuMfw/xv2NeDjRHgQ2qet+tVM/wVtpfUhUZfMW7dc3AdpY5Ex3wrAkBblh/j/n7HQzUKmiilPkWukopu2Ic6lAY/7b8Gk3zVtMAkqCRWAeREaJ0ejm4TrRUjlvejI57V05ZrfQZXDWkrjS2Qnf5QJwT8Jaqo18jsJOBxPWQ/ABf/T4cHB/HaonWBrq62IHOtYplvCz4DN+k8fK6qH51dD5t0x/LNt4rk/aAEMXgKhexEaHAqobjJY5Yafy/H+gTA7v1X56+z877Q8LYB3WGD/t06AV+FfI2inocGBfb8IHeArDevUo7bO/18bnvf5hIOFV5SDMOtv1hAPEKACGH4Z9OgHEWTm4dh3yuFpffjtsJG3IB+t6+gibNP/T7IZtvhmzZV6ytcMhwDDG2CG+z2C5g8PDua34tHvWjwVUB3pJi1U099hNYTDK9bZ+UdcxlQAI5AgYIa7ChdMyF6EtpOKnLVB1Xv0YIz7D98gLJwfZ2Hy8/5wqhNdTTgEaMTSfOytkCdxeutGUbwOt6FAl+yBvQKPSMimv5MHMO7fF+P+fq/bKybhaQE0AruFewOnjoIlMNAoitfhNhTol2sxFJhYfctQmLP+r+FNUUey8ze+UmkBNGZTO1PW78A0PmOsaN6et0UxKn/urfyjCd4lB2K1X7hrO0are5PnqADGAqVawKPBBxHtI2NF5flMEbgQcznhTuZGhJoKoBmQZd0G0RbhThm/w81m5GGc0Qk4PMYtYrWfc7bqj8coBDgHMAqc9adqXoRy6TBiPQNfvjhZhnUO2ObNzt9Mk1EBNEPJ4hTdrZgQXNhsdMZLiYCTk7HI6YWUSveuWA4BWmky21I7UPUitF0ryRg3IQIOTzVKLtgXecZBkQqgVao9OgtJHsbfRq0mZfwYCTh5BuP+D8P0Ny8/PJokwCFAk6DWRzMvQi54L0Lrq+vJl7fRJubai52/xQajAmgRWDV6sfr2nXvbSco0sRD4KuZono4l58Az5RCg3Qbu0yJW0j2JR4MhOs1ol0ry6Zz8GOP+w5IvOIwSaQG0247TXQVJj4Xpyb3l7TLsNJ2Tl2QiXujBo20CVABto0NCexW3ysWdZMG0bRJwsgbKdLine truncated
|
||||
webUI["html/js/configuration_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBXaXphcmRDYXRlZ29yeSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIFdpemFyZENhdGVnb3J5KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJjb250ZW50IjsKICAgIH0KICAgIFdpemFyZENhdGVnb3J5LnByb3RvdHlwZS5jcmVhdGVDYXRlZ29yeUhlYWRsaW5lID0gZnVuY3Rpb24gKHZhbHVlKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJINCIpOwogICAgICAgIGVsZW1lbnQuaW5uZXJIVE1MID0gdmFsdWU7CiAgICAgICAgcmV0dXJuIGVsZW1lbnQ7CiAgICB9OwogICAgcmV0dXJuIFdpemFyZENhdGVnb3J5Owp9KCkpOwp2YXIgV2l6YXJkSXRlbSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uIChfc3VwZXIpIHsKICAgIF9fZXh0ZW5kcyhXaXphcmRJdGVtLCBfc3VwZXIpOwogICAgZnVuY3Rpb24gV2l6YXJkSXRlbShrZXksIGhlYWRsaW5lKSB7CiAgICAgICAgdmFyIF90aGlzID0gX3N1cGVyLmNhbGwodGhpcykgfHwgdGhpczsKICAgICAgICBfdGhpcy5oZWFkbGluZSA9IGhlYWRsaW5lOwogICAgICAgIF90aGlzLmtleSA9IGtleTsKICAgICAgICByZXR1cm4gX3RoaXM7CiAgICB9CiAgICBXaXphcmRJdGVtLnByb3RvdHlwZS5jcmVhdGVXaXphcmQgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgdmFyIGhlYWRsaW5lID0gdGhpcy5jcmVhdGVDYXRlZ29yeUhlYWRsaW5lKHRoaXMuaGVhZGxpbmUpOwogICAgICAgIHZhciBrZXkgPSB0aGlzLmtleTsKICAgICAgICB2YXIgY29udGVudCA9IG5ldyBQb3B1cENvbnRlbnQoKTsKICAgICAgICB2YXIgZGVzY3JpcHRpb247CiAgICAgICAgdmFyIGRvYyA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKHRoaXMuRG9jdW1lbnRJRCk7CiAgICAgICAgZG9jLmlubmVySFRNTCA9ICIiOwogICAgICAgIGRvYy5hcHBlbmRDaGlsZChoZWFkbGluZSk7CiAgICAgICAgc3dpdGNoIChrZXkpIHsKICAgICAgICAgICAgY2FzZSAidHVuZXIiOgogICAgICAgICAgICAgICAgdmFyIHRleHQgPSBuZXcgQXJyYXkoKTsKICAgICAgICAgICAgICAgIHZhciB2YWx1ZXMgPSBuZXcgQXJyYXkoKTsKICAgICAgICAgICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDw9IDEwMDsgaSsrKSB7CiAgICAgICAgICAgICAgICAgICAgdGV4dC5wdXNoKGkpOwogICAgICAgICAgICAgICAgICAgIHZhbHVlcy5wdXNoKGkpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgdmFyIHNlbGVjdCA9IGNvbnRlbnQuY3JlYXRlU2VsZWN0KHRleHQsIHZhbHVlcywgIjEiLCBrZXkpOwogICAgICAgICAgICAgICAgc2VsZWN0LnNldEF0dHJpYnV0ZSgiY2xhc3MiLCAid2l6YXJkIik7CiAgICAgICAgICAgICAgICBzZWxlY3QuaWQgPSBrZXk7CiAgICAgICAgICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoc2VsZWN0KTsKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uID0gInt7LndpemFyZC50dW5lci5kZXNjcmlwdGlvbn19IjsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJlcGdTb3VyY2UiOgogICAgICAgICAgICAgICAgdmFyIHRleHQgPSBbIlBNUyIsICJYRVBHIl07CiAgICAgICAgICAgICAgICB2YXIgdmFsdWVzID0gWyJQTVMiLCAiWEVQRyJdOwogICAgICAgICAgICAgICAgdmFyIHNlbGVjdCA9IGNvbnRlbnQuY3JlYXRlU2VsZWN0KHRleHQsIHZhbHVlcywgIlhFUEciLCBrZXkpOwogICAgICAgICAgICAgICAgc2VsZWN0LnNldEF0dHJpYnV0ZSgiY2xhc3MiLCAid2l6YXJkIik7CiAgICAgICAgICAgICAgICBzZWxlY3QuaWQgPSBrZXk7CiAgICAgICAgICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoc2VsZWN0KTsKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uID0gInt7LndpemFyZC5lcGdTb3VyY2UuZGVzY3JpcHRpb259fSI7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSAibTN1IjoKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCBrZXksICIiKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgicGxhY2Vob2xkZXIiLCAie3sud2l6YXJkLm0zdS5wbGFjZWhvbGRlcn19Iik7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoImNsYXNzIiwgIndpemFyZCIpOwogICAgICAgICAgICAgICAgaW5wdXQuaWQgPSBrZXk7CiAgICAgICAgICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoaW5wdXQpOwogICAgICAgICAgICAgICAgZGVzY3JpcHRpb24gPSAie3sud2l6YXJkLm0zdS5kZXNjcmlwdGlvbn19IjsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJ4bWx0diI6CiAgICAgICAgICAgICAgICB2YXIgaW5wdXQgPSBjb250ZW50LmNyZWF0ZUlucHV0KCJ0ZXh0Iiwga2V5LCAiIik7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoInBsYWNlaG9sZGVyIiwgInt7LndpemFyZC54bWx0di5wbGFjZWhvbGRlcn19Iik7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoImNsYXNzIiwgIndpemFyZCIpOwogICAgICAgICAgICAgICAgaW5wdXQuaWQgPSBrZXk7CiAgICAgICAgICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoaW5wdXQpOwogICAgICAgICAgICAgICAgZGVzY3JpcHRpb24gPSAie3sud2l6YXJkLnhtbHR2LmRlc2NyaXB0aW9ufX0iOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGRlZmF1bHQ6CiAgICAgICAgICAgICAgICBjb25zb2xlLmxvZyhrZXkpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgfQogICAgICAgIHZhciBwcmUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJQUkUiKTsKICAgICAgICBwcmUuaW5uZXJIVE1MID0gZGVzY3JpcHRpb247CiAgICAgICAgZG9jLmFwcGVuZENoaWxkKHByZSk7CiAgICAgICAgY29uc29sZS5sb2coaGVhZGxpbmUsIGtleSk7CiAgICB9OwogICAgcmV0dXJuIFdpemFyZEl0ZW07Cn0oV2l6YXJkQ2F0ZWdvcnkpKTsKZnVuY3Rpb24gcmVhZHlGb3JDb25maWd1cmF0aW9uKHdpemFyZCkgewogICAgdmFyIHNlcnZlciA9IG5ldyBTZXJ2ZXIoImdldFNlcnZlckNvbmZpZyIpOwogICAgc2VydmVyLnJlcXVlc3QobmV3IE9iamVjdCgpKTsKICAgIHNob3dFbGVtZW50KCJsbLine truncated
|
||||
webUI["html/js/files.js"] = "ZnVuY3Rpb24gb3BlbkZpbGVzKGVsbSwgZmlsZVR5cGUpIHsKICAvL2RvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJzZXR0aW5ncyIpLmlubmVySFRNTCA9ICJUZXN0IjsKICAKICBjb2x1bW5Ub1NvcnQgPSAwOyAKICB2YXIgbmV3RGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CiAgCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJJTlBVVCI7CiAgbmV3RW50cnlbInR5cGUiXSA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gPSAiTmV3IjsKICBuZXdFbnRyeVsib25jbGljayJdID0gJ2ZpbGVEZXRhaWwoIi0iLCAiJyArIGZpbGVUeXBlICsgJyIpJzsKICBuZXdEaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbImNsYXNzIl0gPSAiYnV0dG9uIjsKICBuZXdFbnRyeVsidmFsdWUiXSA9ICJVcGRhdGUiOwogIG5ld0VudHJ5WyJvbmNsaWNrIl0gPSAiZmlsZURldGFpbCgwKSI7CiAgLy9uZXdEaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CgogICAvLyBCdWlsZCB0YWJsZQogIHZhciBuZXdUYWJsZSA9IG5ldyBPYmplY3QoKTsKICBuZXdUYWJsZVsiX2VsZW1lbnQiXSAgPSAiVEFCTEUiOwogIG5ld1RhYmxlWyJpZCJdICAgICAgICA9ICJpZF9tYXBwaW5nIjsKICBuZXdUYWJsZVsiY2xhc3MiXSAgICAgPSAidGFibGUtbWFwcGluZyI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VGFibGUpKTsKCiAgc2V0VGltZW91dChmdW5jdGlvbigpeyAKICAgIGNyZWF0ZUZpbGVzVGFibGUoZmlsZVR5cGUpOyAKICB9LCAxMCk7Cgp9CgpmdW5jdGlvbiBjcmVhdGVGaWxlc1RhYmxlKGZpbGVUeXBlKSB7CiAgdmFyIHRhYmxlID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlkX21hcHBpbmciKTsKICB2YXIgYXZhaWxhYmxlRmlsZVR5cGVzID0gbmV3IEFycmF5KCk7CiAgCiAgdGFibGUuaW5uZXJIVE1MID0gIiI7CiAgdmFyIG5ld1RSID0gbmV3IE9iamVjdCgpOwogIG5ld1RSWyJfZWxlbWVudCJdID0gIlRSIjsKICBuZXdUUlsiY2xhc3MiXSAgICA9ICJ0YWJsZS1tYXBwaW5nLWhlYWRlciI7CiAgdGFibGUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdUUikpOwoKICB2YXIgdHIgPSB0YWJsZS5sYXN0Q2hpbGQ7CgogIHN3aXRjaChmaWxlVHlwZSkgewogICAgY2FzZSAieG1sdHYiOiAKICAgICAgYXZhaWxhYmxlRmlsZVR5cGVzID0gbmV3IEFycmF5KCJ4bWx0diIpOyAKICAgICAgdmFyIHRySGVhZGxpbmVzID0gbmV3IEFycmF5KCJHdWlkZSIsICJMYXN0IFVwZGF0ZSIsICJBdmFpbGFiaWxpdHkgJSIsICJDaGFubmVscyIsICJQcm9ncmFtcyIpCiAgICAgIHZhciBjb21wYXRpYmlsaXR5S2V5cyA9IG5ldyBBcnJheSgieG1sdHYuY2hhbm5lbHMiLCAieG1sdHYucHJvZ3JhbXMiKQogICAgICBicmVhazsKCiAgICBjYXNlICJtM3UiOgogICAgICBhdmFpbGFibGVGaWxlVHlwZXMgPSBuZXcgQXJyYXkoIm0zdSIsICJoZGhyIik7IAogICAgICB2YXIgdHJIZWFkbGluZXMgPSBuZXcgQXJyYXkoIlBsYXlsaXN0IiwgIkxhc3QgVXBkYXRlIiwgIkF2YWlsYWJpbGl0eSAlIiwgIlR5cGUiLCAiU3RyZWFtcyIsICJncm91cC10aXRsZSAlIiwgInR2Zy1pZCAlIiwgIlVuaXF1ZSBJRCAlIik7CiAgICAgIHZhciBjb21wYXRpYmlsaXR5S2V5cyA9IG5ldyBBcnJheSgic3RyZWFtcyIsICJncm91cC50aXRsZSIsICJ0dmcuaWQiLCAic3RyZWFtLmlkIik7CiAgICAgIGJyZWFrOwogIH0KCiAgZm9yICh2YXIgaSA9IDA7IGkgPCB0ckhlYWRsaW5lcy5sZW5ndGg7IGkrKykgewogICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgbmV3VERbIl9lbGVtZW50Il0gPSAiVEQiOwogICAgbmV3VERbIl90ZXh0Il0gICAgPSB0ckhlYWRsaW5lc1tpXTsKICAgIHRyLmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VEQpKTsKICB9CiAgCiAgZm9yICh2YXIgaSA9IDA7IGkgPCBhdmFpbGFibGVGaWxlVHlwZXMubGVuZ3RoOyBpKyspIHsKICAgIAogICAgdmFyIGZpbGVUeXBlID0gYXZhaWxhYmxlRmlsZVR5cGVzW2ldCgogICAgdmFyIGRhdGEgPSBjb25maWdbImZpbGVzIl1bZmlsZVR5cGVdOwogICAgCiAgICB2YXIgYWxsRmlsZXMgPSBnZXRPYmpLZXlzKGRhdGEpCiAgCiAgICBmb3IgKHZhciBmID0gMDsgZiA8IGFsbEZpbGVzLmxlbmd0aDsgZisrKSB7CiAgICAgIHZhciBlbG0gICAgICAgICAgID0gZGF0YVthbGxGaWxlc1tmXV07CiAgICAgIHZhciB0YWJsZSAgICAgICAgID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlkX21hcHBpbmciKTsKICAgICAgdmFyIGZpbGVJRCAgICAgICAgPSBlbG1bImlkLnByb3ZpZGVyIl07CiAgICAgIHZhciBuYW1lICAgICAgICAgID0gZWxtWyJuYW1lIl07CiAgICAgIHZhciBsYXN0VXBkYXRlICAgID0gZWxtWyJsYXN0LnVwZGF0ZSJdOwogICAgICB2YXIgYXZhaWxhYmlsaXR5ICA9IGVsbVsicHJvdmlkZXIuYXZhaWxhYmlsaXR5Il07CiAgICAgIHZhciB0eXBlICAgICAgICAgID0gZWxtWyJ0eXBlIl0udG9VcHBlckNhc2UoKTsKICAgICAgdmFyIGNvbXBhdGliaWxpdHkgPSBlbG1bImNvbXBhdGliaWxpdHkiXTsKCiAgICAgIC8vIENyZWF0ZSBUUgogICAgICB2YXIgbmV3VFIgPSBuZXcgT2JqZWN0KCk7CiAgICAgIG5ld1RSWyJfZWxlbWVudCJdICAgICAgID0gIlRSIjsKICAgICAgbmV3VFJbImNsYXNzIl0gICAgICAgICAgPSAiIjsKICAgICAgbmV3VFJbImlkIl0gICAgICAgICAgICAgPSBmaWxlSUQ7CiAgICAgIG5ld1RSWyJvbmNsaWNrIl0gICAgICAgID0gJ2phdmFzY3JpcHQ6IGZpbGVEZXRhaWwoIicgKyBmaWxlSUQgKyAnIiwiJyArIGZpbGVUeXBlICsgJyIpOyc7CiAgICAgIHRhYmxlLmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VFIpKTsKCiAgICAgIHZhciB0ciA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICAgIC8vIENyZWF0ZSBmaWxlIG5hbWUgVEQKICAgICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgICAgbmV3VERbIl90ZXh0Il0gICAgPSBuYW1lOwogICAgICBjcmVhdGVOZXdURChuZXdURCwgdHIpOwoKICAgICAgLy8gQ3JlYXRlIGxhc3QgdXBkYXRlIFRECiAgICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgICAgbmV3VERbIl9lbGVtZW50Il0gPSAiUCI7CiAgICAgIG5ld1REWyJfdGV4dCJdICAgID0gbGFzdFVwZGF0ZTsKICAgICAgY3JlYXRlTmV3VEQobmV3VEQsIHRyKTsKCiAgICAgIC8vIENyZWF0ZSBhdmFpbGFiaWxpdHkgVEQKICAgICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgICAgbmV3VERbIl90ZXh0Il0gICAgPSBhdmFpbGFiaWxpdHk7CiAgICAgIGNyZWF0ZU5ld1REKG5ld1RELine truncated
|
||||
webUI["html/js/logs_ts.js"] = "dmFyIExvZyA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIExvZygpIHsKICAgIH0KICAgIExvZy5wcm90b3R5cGUuY3JlYXRlTG9nID0gZnVuY3Rpb24gKGVudHJ5KSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJQUkUiKTsKICAgICAgICBpZiAoZW50cnkuaW5kZXhPZigiV0FSTklORyIpICE9IC0xKSB7CiAgICAgICAgICAgIGVsZW1lbnQuY2xhc3NOYW1lID0gIndhcm5pbmdNc2ciOwogICAgICAgIH0KICAgICAgICBpZiAoZW50cnkuaW5kZXhPZigiRVJST1IiKSAhPSAtMSkgewogICAgICAgICAgICBlbGVtZW50LmNsYXNzTmFtZSA9ICJlcnJvck1zZyI7CiAgICAgICAgfQogICAgICAgIGlmIChlbnRyeS5pbmRleE9mKCJERUJVRyIpICE9IC0xKSB7CiAgICAgICAgICAgIGVsZW1lbnQuY2xhc3NOYW1lID0gImRlYnVnTXNnIjsKICAgICAgICB9CiAgICAgICAgZWxlbWVudC5pbm5lckhUTUwgPSBlbnRyeTsKICAgICAgICByZXR1cm4gZWxlbWVudDsKICAgIH07CiAgICByZXR1cm4gTG9nOwp9KCkpOwpmdW5jdGlvbiBzaG93TG9ncyhib3R0b20pIHsKICAgIHZhciBsb2cgPSBuZXcgTG9nKCk7CiAgICB2YXIgbG9ncyA9IFNFUlZFUlsibG9nIl1bImxvZyJdOwogICAgdmFyIGRpdiA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJjb250ZW50X2xvZyIpOwogICAgZGl2LmlubmVySFRNTCA9ICIiOwogICAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKGxvZ3MpOwogICAga2V5cy5mb3JFYWNoKGZ1bmN0aW9uIChsb2dJRCkgewogICAgICAgIHZhciBlbnRyeSA9IGxvZy5jcmVhdGVMb2cobG9nc1tsb2dJRF0pOwogICAgICAgIGRpdi5hcHBlbmQoZW50cnkpOwogICAgfSk7CiAgICBzZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHsKICAgICAgICBpZiAoYm90dG9tID09IHRydWUpIHsKICAgICAgICAgICAgdmFyIHdyYXBwZXIgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYm94LXdyYXBwZXIiKTsKICAgICAgICAgICAgd3JhcHBlci5zY3JvbGxUb3AgPSB3cmFwcGVyLnNjcm9sbEhlaWdodDsKICAgICAgICB9CiAgICB9LCAxMCk7Cn0KZnVuY3Rpb24gcmVzZXRMb2dzKCkgewogICAgdmFyIGNtZCA9ICJyZXNldExvZ3MiOwogICAgdmFyIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgICB2YXIgc2VydmVyID0gbmV3IFNlcnZlcihjbWQpOwogICAgc2VydmVyLnJlcXVlc3QoZGF0YSk7Cn0K"
|
||||
webUI["html/js/menu.js"] = "CmZ1bmN0aW9uIHNldE1lbnVJdGVtKCkgewoKICBtZW51ID0gbmV3IE9iamVjdCgpOwogIHN1Yk1lbnUgPSBuZXcgT2JqZWN0KCk7CgogIHZhciBtZW51X20zdSA9IG5ldyBPYmplY3QoKTsKICBtZW51X20zdVsiX21lbnVUeXBlIl0gICAgICAgPSAiaW5wdXRBcnJheSI7CiAgbWVudV9tM3VbIl9lbGVtZW50Il0gICAgICAgID0gIkxJIjsKICBtZW51X20zdVsiX2NvbmZpZ0tleSJdICAgICAgPSAiZmlsZXMubTN1IjsKICBtZW51X20zdVsiX3RleHQiXSAgICAgICAgICAgPSAiUGxheWxpc3QiOwogIG1lbnVfbTN1WyJfaWNvbiJdICAgICAgICAgICA9ICJpbWcvbTN1LnBuZyI7CiAgbWVudV9tM3VbIl9oZWFkbGluZSJdICAgICAgID0gIlBsYXlsaXN0czogTG9jYWwgb3IgcmVtb3RlIjsKICBtZW51X20zdVsiX3VzYWdlIl0gICAgICAgICAgPSAiPGI+SW5mbzwvYj48YnI+QXZhaWxhYmlsaXR5OiBGaWxlIGF2YWlsYWJpbGl0eSBpbiBwZXJjZW50PGJyPlN0cmVhbXM6ICAgICAgTnVtYmVyIG9mIHN0cmVhbXMgaW4gdGhlIGZpbGUuPGJyPmdyb3VwLXRpdGxlOiAgU3RyZWFtcyB0aGF0IGFyZSBhc3NpZ25lZCB0byBhIGdyb3VwLiBTaW1wbGlmaWVzIGZpbHRlcmluZyBzdHJlYW1zPGJyPnR2Zy1pZDogICAgICAgVGhpcyBJRCBpcyB1c2VkIGZvciBhdXRvbWF0aWMgbWFwcGluZywgbXVzdCBtYXRjaCB3aXRoIHRoZSBjaGFubmVsIElEIGluIHRoZSBYTUxUViBmaWxlLjxicj5VbmlxdWUgSUQ6ICAgIFN0cmVhbXMgd2l0aCBhIHVuaXF1ZSBJRCB0byBpZGVudGlmeSB0aGVtLiBBbGxvd3MgY2hhbm5lbCBuYW1lIGNoYW5nZXMgaW4gdGhlIE0zVSB3aXRob3V0IGxvc2luZyB0aGUgWE1MVFYgbWFwcGluZyAoUFBWIC8gbGl2ZSBldmVudHMpLjxicj48YnI+PGI+VXNhZ2UgTTNVOjwvYj48YnI+UmVtb3RlIHBsYXlsaXN0OiBodHRwOi8veW91ci5pcHR2LnByb3ZpZGVyLmNvbS9maWxlLm0zdTxicj5Mb2NhbCAgcGxheWxpc3Q6IC9wYXRoL3RvL2ZpbGUubTN1PGJyPjxicj48Yj5Vc2FnZSBIREhvbWVSdW46PC9iPjxicj5JUDogMTkyLjE2OC4xLjEwOjUwMDQ8YnI+IgogIG1lbnVfbTN1WyJuYW1lIl0gICAgICAgICAgICA9ICJmaWxlIjsKICBtZW51X20zdVsiaWQiXSAgICAgICAgICAgICAgPSAiZmlsZSI7CiAgbWVudV9tM3VbInZhbHVlIl0gICAgICAgICAgID0gbWVudV9tM3VbIm5hbWUiXTsKICBtZW51X20zdVsicGxhY2Vob2xkZXIiXSAgICAgPSAiUGxheWxpc3Q6IGxvY2FsIG9yIHJlbW90ZSI7CiAgbWVudV9tM3VbIm9uY2xpY2siXSAgICAgICAgID0gImphdmFzY3JpcHQ6IHRvZ2dsZU1lbnUodGhpcyk7IjsKICBtZW51X20zdVsiY2xhc3MiXSAgICAgICAgICAgPSAibWVudS1ub3RBY3RpdmUiOwoKCiAgdmFyIG1lbnVfZmlsdGVyID0gbmV3IE9iamVjdCgpOwogIG1lbnVfZmlsdGVyWyJfbWVudVR5cGUiXSAgICA9ICJpbnB1dEFycmF5IjsKICBtZW51X2ZpbHRlclsiX2VsZW1lbnQiXSAgICAgPSAiTEkiOwogIG1lbnVfZmlsdGVyWyJfY29uZmlnS2V5Il0gICA9ICJmaWx0ZXIiOwogIG1lbnVfZmlsdGVyWyJfdGV4dCJdICAgICAgICA9ICJGaWx0ZXIiOwogIG1lbnVfZmlsdGVyWyJfaWNvbiJdICAgICAgICA9ICJpbWcvZmlsdGVyLnBuZyI7CiAgbWVudV9maWx0ZXJbIl9oZWFkbGluZSJdICAgID0gIkZpbHRlciBieSBNM1UgcGFyYW1ldGVycywgZS5nLiBncm91cC10aXRsZSI7CiAgbWVudV9maWx0ZXJbIl91c2FnZSJdICAgICAgID0gIjxiPlVzYWdlOjwvYj48YnI+U3BvcnQgLSBBbGwgc3BvcnRzIGNoYW5uZWxzPGJyPlNwb3J0IHtIRH0gLSBBbGwgSEQgc3BvcnRzIGNoYW5uZWxzPGJyPlNwb3J0IHtIRH0gIXtFUyxERX0gLSBBbGwgSEQgc3BvcnRzIGNoYW5uZWxzLCBidXQgbm8gU3BhbmlzaCBhbmQgR2VybWFuPGJyPjxicj5UbyBmaWx0ZXIgdGhlIHN0cmVhbXMgb2YgYSBIREhvbWVSdW4sIHRoZSBwbGF5bGlzdCBuYW1lIGNhbiBiZSBlbnRlcmVkOjxicj5NeSB0dW5lciB7SER9IgogIC8vbWVudV9maWx0ZXJbIl91c2FnZSJdICAgICAgID0gIjxiPlVzYWdlOjwvYj48YnI+QWxsIHNwb3J0cyBjaGFubmVsczogU3BvcnQ8YnI+QWxsIEhEIHNwb3J0cyBjaGFubmVsczogU3BvcnQge0hEfTxicj5BbGwgSEQgc3BvcnRzIGNoYW5uZWxzLCBidXQgbm8gU3BhbmlzaCBhbmQgR2VybWFuOiBTcG9ydCB7SER9ICF7RVMsREV9IgogIG1lbnVfZmlsdGVyWyJuYW1lIl0gICAgICAgICA9ICJmaWx0ZXIiOwogIG1lbnVfZmlsdGVyWyJpZCJdICAgICAgICAgICA9ICJNM1UiOwogIG1lbnVfZmlsdGVyWyJ2YWx1ZSJdICAgICAgICA9IG1lbnVfZmlsdGVyWyJuYW1lIl07CiAgbWVudV9maWx0ZXJbInBsYWNlaG9sZGVyIl0gID0gIkZpbHRlciBzdHJlYW1zOiBTcG9ydCI7CiAgbWVudV9maWx0ZXJbIm9uY2xpY2siXSAgICAgID0gImphdmFzY3JpcHQ6IHRvZ2dsZU1lbnUodGhpcyk7IjsKICBtZW51X2ZpbHRlclsiY2xhc3MiXSAgICAgICAgPSAibWVudS1ub3RBY3RpdmUiOwoKICB2YXIgbWVudV9pZCA9IG5ldyBPYmplY3QoKTsKICBtZW51X2lkWyJfbWVudVR5cGUiXSAgICAgICAgPSAiaW5wdXRBcnJheSI7CiAgbWVudV9pZFsiX2VsZW1lbnQiXSAgICAgICAgID0gIkxJIjsKICBtZW51X2lkWyJfY29uZmlnS2V5Il0gICAgICAgPSAiaWQiOwogIG1lbnVfaWRbIl90ZXh0Il0gICAgICAgICAgICA9ICJQTVMgSUQiOwogIG1lbnVfaWRbIl9pY29uIl0gICAgICAgICAgICA9ICJpbWcvbnVtYmVyLnBuZyI7CiAgbWVudV9pZFsiX2hlYWRsaW5lIl0gICAgICAgID0gIlNldHVwIFBNUyBndWlkZSBudW1iZXIiOwogIG1lbnVfaWRbIl91c2FnZSJdICAgICAgICAgICA9ICdTb21lIHBsYXlsaXN0cyBoYXZlIHVuaXF1ZSBjaGFubmVsIElEcy48YnI+RW50ZXIgdGhlIGtleXdvcmQgb2YgdGhlIElELiBUaGUgY2hhbm5lbCBhc3NpZ25tZW50IGluIFBNUyB3aWxsIGNoYW5nZSBhcyBhIHJlc3VsdC48YnI+PGJyPmUuZy4gY2hhbm5lbElEPGJyPiNFWFRJTkY6MCB0eXBlPSJzdHJlYW0iIDxiPmNoYW5uZWxJZDwvYj49IjgxIiwgTXkgU3RyZWFtaW5nIENoYW5uZWwgSEQ8YnI+PGJyPk9ubHkgZW50ZXIgaGVyZSBpZiB5b3Uga25vdyB3aGF0IHlvdSBhcmUgZG9pbmchJwogIG1lbnVfaWRbIm5hbWUiXSAgICAgICAgICAgICA9ICJpZCI7CiAgbWVudV9pZFsiaWQiXSAgICAgICAgICAgICAgID0gImlkIjsKICBtZW51X2lkWyJ2YWx1ZSJdICAgICAgICAgICAgPSBtZW51X2lkWyJuYW1lIl07CiAgbWVudV9pZFsicGxhY2Vob2xkZXIiXSAgICAgID0gIlVuaXF1ZSBJRCBmcm9tIHRoZSBNM1UgZmlsZSI7CiAgbWVudV9pZFsib25jbGljayJdICAgICAgICAgID0gImphdmFzY3JpcHQ6IHRvZ2dsZU1lbnUodGhpcyk7IjsKICBtZW51X2lkWyJjbGFzcyJdICAgICAgICAgICAgPSAibWVudS1ub3RBY3RpdmUiOwoKCiAgdmFyIG1lbnVfeG1sdHYgPSBuZXcgT2JqZWN0KCk7CiAgbWVudV94bWx0dlsiX21lbnVUeXBlIl0gICAgID0gImlucHV0QXJyYXkiOwogIG1lbnVfeG1sdHZbIl9lbGVtZW50Il0gICAgICA9ICJMSSI7CiAgbWVudV94bWx0dlsiX2NvbmZpZ0tleSJdICAgID0gImZpbGVzLnhtbHR2IjsKICBtZW51X3htbHR2WyJfdGV4dCJdICAgICAgICAgPSAiWE1MVFYiOwogIG1lbnVfeG1sdHZbIl9pY29uIl0gICAgICAgICA9ICJpbWcveG1sdHYucG5nIjsKICBtZW51X3htbHR2WyJfaGVhZGxpbmUiXSAgICAgPSAiWE1MVFYgZmlsZXM6IExvY2FsIG9yIHJlbW90ZSI7CiAgbWVudV94bLine truncated
|
||||
webUI["html/js/settings_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBTZXR0aW5nc0NhdGVnb3J5ID0gLyoqIEBjbGFzcyAqLyAoZnVuY3Rpb24gKCkgewogICAgZnVuY3Rpb24gU2V0dGluZ3NDYXRlZ29yeSgpIHsKICAgICAgICB0aGlzLkRvY3VtZW50SUQgPSAiY29udGVudF9zZXR0aW5ncyI7CiAgICB9CiAgICBTZXR0aW5nc0NhdGVnb3J5LnByb3RvdHlwZS5jcmVhdGVDYXRlZ29yeUhlYWRsaW5lID0gZnVuY3Rpb24gKHZhbHVlKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJINCIpOwogICAgICAgIGVsZW1lbnQuaW5uZXJIVE1MID0gdmFsdWU7CiAgICAgICAgcmV0dXJuIGVsZW1lbnQ7CiAgICB9OwogICAgU2V0dGluZ3NDYXRlZ29yeS5wcm90b3R5cGUuY3JlYXRlSFIgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJIUiIpOwogICAgICAgIHJldHVybiBlbGVtZW50OwogICAgfTsKICAgIFNldHRpbmdzQ2F0ZWdvcnkucHJvdG90eXBlLmNyZWF0ZVNldHRpbmdzID0gZnVuY3Rpb24gKHNldHRpbmdzS2V5KSB7CiAgICAgICAgdmFyIHNldHRpbmcgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJUUiIpOwogICAgICAgIHZhciBjb250ZW50ID0gbmV3IFBvcHVwQ29udGVudCgpOwogICAgICAgIHZhciBkYXRhID0gU0VSVkVSWyJzZXR0aW5ncyJdW3NldHRpbmdzS2V5XTsKICAgICAgICBzd2l0Y2ggKHNldHRpbmdzS2V5KSB7CiAgICAgICAgICAgIC8vIFRleHRlaW5nYWJlbgogICAgICAgICAgICBjYXNlICJ1cGRhdGUiOgogICAgICAgICAgICAgICAgdmFyIHRkTGVmdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlREIik7CiAgICAgICAgICAgICAgICB0ZExlZnQuaW5uZXJIVE1MID0gInt7LnNldHRpbmdzLnVwZGF0ZS50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAidXBkYXRlIiwgZGF0YS50b1N0cmluZygpKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgicGxhY2Vob2xkZXIiLCAie3suc2V0dGluZ3MudXBkYXRlLnBsYWNlaG9sZGVyfX0iKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgib25jaGFuZ2UiLCAiamF2YXNjcmlwdDogdGhpcy5jbGFzc05hbWUgPSAnY2hhbmdlZCciKTsKICAgICAgICAgICAgICAgIHRkUmlnaHQuYXBwZW5kQ2hpbGQoaW5wdXQpOwogICAgICAgICAgICAgICAgc2V0dGluZy5hcHBlbmRDaGlsZCh0ZExlZnQpOwogICAgICAgICAgICAgICAgc2V0dGluZy5hcHBlbmRDaGlsZCh0ZFJpZ2h0KTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJiYWNrdXAucGF0aCI6CiAgICAgICAgICAgICAgICB2YXIgdGRMZWZ0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHRkTGVmdC5pbm5lckhUTUwgPSAie3suc2V0dGluZ3MuYmFja3VwUGF0aC50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAiYmFja3VwLnBhdGgiLCBkYXRhKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgicGxhY2Vob2xkZXIiLCAie3suc2V0dGluZ3MuYmFja3VwUGF0aC5wbGFjZWhvbGRlcn19Iik7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoIm9uY2hhbmdlIiwgImphdmFzY3JpcHQ6IHRoaXMuY2xhc3NOYW1lID0gJ2NoYW5nZWQnIik7CiAgICAgICAgICAgICAgICB0ZFJpZ2h0LmFwcGVuZENoaWxkKGlucHV0KTsKICAgICAgICAgICAgICAgIHNldHRpbmcuYXBwZW5kQ2hpbGQodGRMZWZ0KTsKICAgICAgICAgICAgICAgIHNldHRpbmcuYXBwZW5kQ2hpbGQodGRSaWdodCk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSAidGVtcC5wYXRoIjoKICAgICAgICAgICAgICAgIHZhciB0ZExlZnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJURCIpOwogICAgICAgICAgICAgICAgdGRMZWZ0LmlubmVySFRNTCA9ICJ7ey5zZXR0aW5ncy50ZW1wUGF0aC50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAidGVtcC5wYXRoIiwgZGF0YSk7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoInBsYWNlaG9sZGVyIiwgInt7LnNldHRpbmdzLnRtcFBhdGgucGxhY2Vob2xkZXJ9fSIpOwogICAgICAgICAgICAgICAgaW5wdXQuc2V0QXR0cmlidXRlKCJvbmNoYW5nZSIsICJqYXZhc2NyaXB0OiB0aGlzLmNsYXNzTmFtZSA9ICdjaGFuZ2VkJyIpOwogICAgICAgICAgICAgICAgdGRSaWdodC5hcHBlbmRDaGlsZChpbnB1dCk7CiAgICAgICAgICAgICAgICBzZXR0aW5nLmFwcGVuZENoaWxkKHRkTGVmdCk7CiAgICAgICAgICAgICAgICBzZXR0aW5nLmFwcGVuZENoaWxkKHRkUmlnaHQpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgInVzZXIuYWdlbnQiOgogICAgICAgICAgICAgICAgdmFyIHRkTGVmdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlREIik7CiAgICAgICAgICAgICAgICB0ZExlZnQuaW5uZXJIVE1MID0gInt7LnNldHRpbmdzLnVzZXJBZ2VudC50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAidXNlci5hZ2VudCIsIGRhdGEpOwogICAgICAgICAgICAgICAgaW5wdXQuc2V0QXR0cmlidXRlKCJwbGFjZWhvbGRlciIsICJ7ey5zZXR0aW5ncy51c2VyQWdlbnQucGxhY2Vob2xkZXJ9fSIpOwogICAgICAgICAgICAgICAgaW5wdXQuc2V0QXR0cmlidXRlKCJvbmNoYW5nZSIsICJqYXZhc2NyaXB0OiB0aGlzLmNsYXNzTmFtZSA9ICdjaGFuZ2Line truncated
|
||||
webUI["html/img/settings.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0wOC0xMFQxODowODo4OTwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Ckxt87EAAAS7SURBVGgFzZpdiFVVGIbn5N+IMgVhaRLqWI7mQIJoIVRgOhQZNDAhWcbcVuPV3BRE6I14440iQQSaN0p6kz8zI0k5inUhZSSaYOVYODMo5eiMOOPf+LzH2cPMPmvttdbe53jOBy9rr2+938/ae51vr733qaoqgQwPD68B/wDJfXAP3AE94JMShCy+SxJ9CvSDJFlU7MiPFdsh/hrBdIffBsd48HApJjLPI4tnPThBlFJMZI5HBuWZCIt9AmgEzeAJR6JzHeMadnKI8wbYCz4Fkz18JlNwMgf8BCL5j4O3bVaMdUfEhPZqgv1U7HbEbDvoT7HZOPUYNwAlHheV09a4A3RTgMqtj9QY7JdgeM5i3I4+fDIYrQeq/UnyNYOTlBDtS+BwEjk2doz+KyO2OY5bwVCME+/qylTHT4C1D3k+uB33Yul3ov/eMuajlu1RH+II5wtT4hNNSnQ6U/kzbRkfq351bCfF8apAm7kmvq38njWRK0S305SHcSK5XO4U5O9MBmXWbSe3E6YccialdKzH2TTnQEF10XgZ5DdivsxEhkyxjVdERAwu03xmMiqD7hYx19km4cyHq6KyeHKkWpSz+ciVrHVpRYZk/wLHp0H2bULkNKzt4Eq86TKxLq3IECf6nXwV9QPbAfjHQSfoD7SN6L63gYhvbrkiTwKf/dPYpbdPVxKMnigdg0VAG8FQye8AzBl6aIk2A+wPiNoHd63LNZx3wbUAvz/CfTzJb/43AmkGpLfAi0DPE/NALQgtve+xFPdi5xRiNkHa5ySOJ/TR7RrBJdqL4AAxL+p+8ToYAFklNCnF3pM1KPba2L6v8vons5oPsko9ZyZoa0PsOoKezxoY+y5N5DYHWSvDTXzUMJH7IUkRW0tbyyV0CcfDDKmqDMe1KfqnQyehGNgo9q8p4hWYFGsiBY4fsSK/BRkkaPgj5PhMy720BnVFesfnlKo3Das0bw8XYJf196GEuzWRLSDoRypLg2w06FyqTS6Cx7iK1eeqGqrnugmuBgvB8+A5UAuqQYiU+oZ4h2T+BV3gAlDp/pai0U1rFpVGMAv8AHzFd4vShMOQLcoB+DNB/sSbM3ZoMV4BQsW0adSJSbNp1Hu1mY40/YZxpPdJaUSfFzrBMXAjjQNsdvll6cHCWVvKJIphdhcny11pqmolCk5aIDif0BKdZBucgPlO8kh/r8O4HtwClSCbk86HtQqQuUrvKVCf5OARjt0lll4H/WKKmbS0tmJQKZNQ7nq9+w0n2LhTN04E8jKMPpZ1hcli8tGTZYEYJwJrZQGzchTGz3a2ifwdkHc73LYAfpx6DcWXQK1LeiDsdpFGx7UOwUGQJKpmGyIjjvWh50iSQWzsZ/ofgvx+jnYp+B/YpJcB7QXDBKPJ4JDF6+/oCwoBuhoL36R+Jp4RJH16u2IgaxJpHhMehsBY3wTjV2YbOuuumLHrwCW6msbSj74O/DHGgV4Ohl8Jw1nSMmsB+tL6Wnw83odzBrhEr2GtgvFE8A74ADxtJZZygMC25cjQqBwqdg62qpUlziUP45Cq6OGuqqoUE/FJ8i+v7MpJYvEsHl1A5gNty2vLmaN3bBLdAvqB3svqXxLRvyEuc9zs7SiA+ACpw05pJx8SoAAAAABJRU5ErkJggg=="
|
||||
webUI["html/img/x_white.png"] = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAA6ppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0xMS0xNVQxNzoxMToyNTwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MTQ0PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj4xNDQ8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI1NjwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgrJkQrHAAAWO0lEQVR4Ae1da5icRZV+537pyfRcEdbltl5AFEkgywaDgI8sAWRZszyyYBACbAgQQgARuSmykfVBXJEIAgqEIGC4emGjQUVWMJJduUSeIIquoiJgpjPTPcncb/t+3ZlmMtPd0z3p/r5zvjr1I+n+uqerznvOW3Wq6pyqsrGxMVgxBFxFoNxVwU1uQ8BDwAhgduA0AkYAp9VvwhsBzAacRsAI4LT6TXgjgNmA0wgYAZxWvwlvBDAbcBoBI4DT6jfhjQBmA04jYARwWv0mvBHAbMBpBIwATqvfhDcCmA04jYARwGn1m/BGALMBpxEwAjitfhPeCGA24DQCRgCn1W/CGwHMBpxGwAjgtPpNeCOA2YDTCBgBnFa/CW8EMBtwGgEjgNPqN+GNAGYDTiNgBHBa/Sa8EcBswGkEjABOq9+ENwKYDTiNgBHAafWb8EYAswGnETACOK1+E94IYDbgNAJGAKfVb8IbAcwGnEbACOC0+k14I4DZgNMIGAGcVr8JbwQwG3AaASOA0+o34Y0AZgNOI2AEcFr9JrwRwGzAaQSMAE6r34Q3ApgNOI2AEcBp9ZvwRgCzAacRqAxQ+nv68JvhAOsvrOr51Ti+prA/CfDbr4/ilp4A6y+s6isbECkr7E+K9e0gCfDcEFbpUVJjOTa3Y08lQ+bSBP6rv1hGUtrf2bcSlzWUtoocvx6kPq9vxPurcrRN1kfdozgnIatJ2VrzzT411l9VhvubEA2o+yeAQRKgFvhWE+qCEz6bAWV7vr4fq/uyfSjl+RujWNEtpTHTtmPlLMwLtBMMkgBE54BK3Ng4LUqCvnBJN/4yKqg9U5tC56dLdgvTbT62FpdF0u+CeREwASj00nos5FigpMRHQQsTW+7tw2NKXP89KrAmisCH/7KxsbHA1dk5hoM68NpI4A3JtwFrmnB6Xb5f9u17dH7e26Gj+y8vw+MtOLraN2yyVhT8CMCmtZTh3iYQFC3lom7Q2qSVc/U4P5dHRFg/NSiCAGzHkdW4Iri1sEJNmU42rU1UofPzPSXOD3dUrp0lBTwRLlAKDO6JHbEVzwxKgWbadtzbjEUyZi9vJp2fTnmD0lQMW8rxQjv2ktLxihkBiBT35O5rAvebtJQVCfxVhs1xOFJh/dTsHU2CrJ/tkWVu+1bgtqgW+8fWUZwvYMX9vn58V4nzsyyChcLCSQS5QGnDX5zAmt70O+kv1jbjX4NzhBQ5P7OrsLENwuwfEgmwfQxzYvidkji5tnL8qh3tAQ2lC7vwHQ3df0M5nm3DfhXiurOA9JYTh4YyL0SCUSIqSmwUywJyhO7v12H91OPNjRKtnw2TSAA2a24VrhOzUjYtDx/qw8O+d8N0fi4UthSbDajT63GGvH3DVGslukCplnGDekEnfjSQDVVZz3erwEvtaPNx1NLi/OxX6Tk/HNVlFqEjAMEiYow4aJfnNWZU5JYRLPexP/6WEuenht5ss1zrpyrlEoCN26Mcq/Wsiq7tw7d9Ga+4+eAn2TISPs+HNzRiTpA5V9M3UzQB2PyP1ODCoCNmp0dx/Bvnc0Oq9LGF5yW8LQj55aO1WF4vvZly5wBp5NirHhrDi0PpB6JfLKrzAvtKV9b249Su0v180X55rwov5IFhjsKLAgIQwV8NY24MfaXvXIuire+24MTS7PfQ+WHAs/zuv7IMT7bi8EBTvfJUpXQXKCWGrsQxLyy5NFxl5IV866fKrmnQYf1sqg4CsKFMHPsXqWvJKZam/31jBEwYKHp5oB+Pis9IptQfrgGPOdFSdLhAKTR1JY6taynmOUJbks4Pd52FF+6HbGrzlu+0FD0t1ZY4xjNUEsVzhOj8yLf+sjIvzVeR9ZOlmgjA5jJxTMvw+pcR8AiJopQH+/GIBufn0giOLc3svygwZvwRTS5QSgBdiWPrW7Fg11K/tTg/86rxVCs0LPzsRARlIwDbzo1F7ywxJQ1fEkf3rjlCDDWV7/w0lXtKUWf9NCcldrQTabGPnsSxP4/g0l1whB7qx8ManJ+vR8FsPo1FnwuURvnMBO5Wkjj2o9aZnALSwZWfGDrEH5d0Tj1u1xOylbaf1AvFBGDi2MEx/FZD4tjeldhceEjwyXEw00B4ObAK/9MGJTs0GbBU6QKl5FCUOPbHYXxqWwb0czyi8yPf+uvLsJbHG+cQQ/xHiglAbA+pwn8oSRy7vRdP5n3kEZ2foNIsC7LYVVHveGPVRTcBCP0nI/hHDWvPPIL17AR68lsRuqBbget/ah3OVt35J4mrngCMt71HSeLYH4ZxeR6OENOLHxTv+r+jUtMJTjnGKMWT4IlSrRvACZ0THwh9zWCBJ1u8/exsJTbmBTwzwVJyqS7Dhlbv4IIQFPUjQEoHWhLHUo5QjsXbCxLSrZ+Af2FWSKyfsoSEAJTki404SEOf9H/DuDLL1tgj/XhAvPNzQi0u1pOkmuofc/wbEhcoJeHLycSx3vwmmjlAKfVHvAnhp1MSplQ4P29ntLO/p7+UXBelrsDP33+PkhvHRrkiFMekvl6+81ORvMfEz7OPfDCe8LhAKbC4La8iceyVYVw9YUXoUQ3Oz9UNOCr79N0HYy1FFaFygVIAMR+XN44xCk14oSP0s1YcVgUVzs+RNXiiBToD3nIZQthGAMrarOTGMTpCZ8XBM0V5ypXwdU+egM27S8Jn/bSWEI4AKb5/djtWTvAxcnUCgX7GHSWuCwkvj7XgBA3b7TOAMbQEoAfEG8d+nnf4zQywc+RPLooou8y8IL2ElgBE4dURzI4hIf4khYIU5vOX51Z7m76hm/q+hWII5wBp4Zg4pjdRIy1FgC94YSFvKgmx9RPbMBOA4vH2rjPFn88aoInnrvrWKN4ZypnvBLHD7AKlxGQEMhPHuO5upSAEzqrHnWoTHfOXNPwEIBbPDeEDWzEoPkQif7WV+psHVOEXbXBh7Ay5C5QyFEWJY6W27Hx+vy6Z6OiC9RMNJwhAOS+J4JiQrmTnY9MFfefLjThQeaJj/vI64QKl4OC1igfFpO+55q+5En3zY3V4sJQXfJSo2TP+WVdGAAK0e/LGMeZkWcmGwD6V4BFXThWHCEC9Hs8bxxzxbQu3Yt5MzlX/Jsc6CLcIQKu4vhGzNSSOFW7Au/oXK2dhnnvIODQHSBvIr0dwSAfkJ46lG+zDiwU1+EGLdzeza8W5EYAK3r8CX3HM081t1rtXeEfLOGj9hMVFAlDsJXU4Sf+hTrnNOs9PmZfzzSbs5qghuEoAGsc3otgz7IEu+XDg8shMTq7O55dVfMfFOUBaMU8P4UNbMeJwiMT8avx3q3fniLPF1ZEvqfAPVuEqPRd6Ft1GW3itS7PT1k9InR4BKD8Tx47cig1OJo492oKFzoeHOD0CkACcBTDdW8uNY0UcBJZFzPo9OF0nACHYu8K5/X9uBf5nYxHZpPinjACe8k6uBfM/HCkN5VjbDOd9nx3aNgLsAGJVI97txmrIzY3Yz9Z/x3s7I8AOJCIMBWsGT74Pdzm9HmfYDuAEHRsB3gLj4Eo1N4691ehCXnGIu8Vc/50Rc30ZdGc0wD2x4zrx+MCkx2F4W1OGZ9owxw03L3+F2QiwE1b0gNYwMCaMLvINjWb9O+k69cYILine truncated
|
||||
webUI["html/maintenance.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgPC9oZWFkPgoKICAgIDxib2R5PgogICAgICAgICAgCiAgICAgIDxkaXYgaWQ9ImhlYWRlciIgY2xhc3M9ImltZ0NlbnRlciI+PC9kaXY+CgogICAgICA8ZGl2IGlkPSJib3giPgoKICAgICAgICA8ZGl2IGlkPSJoZWFkbGluZSI+CiAgICAgICAgICA8aDEgaWQ9ImhlYWQtdGV4dCIgY2xhc3M9ImNlbnRlciI+TWFpbnRlbmFuY2U8L2gxPiAgICAgIAogICAgICAgIDwvZGl2PiAgCgogICAgICAgIDxkaXYgaWQ9ImNvbnRlbnQiPgogICAgICAgICAgeFRlVmUgaXMgdXBkYXRpbmcgdGhlIGRhdGFiYXNlLCBwbGVhc2UgdHJ5IGFnYWluIGxhdGVyLgogICAgICAgIDwvZGl2PgoKICAgICAgICA8ZGl2IGlkPSJib3gtZm9vdGVyIj48L2Rpdj4KICAgICAgICAKICAgICAgPC9kaXY+CgogICAgPC9ib2R5Pgo8L2h0bWw+"
|
||||
webUI["html/img/filter.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0wNy0yOFQxOTowNzo2OTwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cs038OQAAAOISURBVGgF5ZpLSBVRGMfvvfmqCKKiKCqKtE3SQ8haJJm1ctPCZYHrFhXtohcUBLVsU1BRBJKLXpsSKs1aCL1oUUhvMgIloodZKmLZ72+OzNVx7jzOXGfqg/89Z875zv/7f35n7px7vanUP2JptzyGhoYKmK8GW0EZmA/mgW7wHrwDN0BTOp0epPVtxBBnLdgIVoE5YBboBR2gDTTA/5DWn0FeDPaBz8CLdeF0EBR5jYRvKbgABoAXa8Opwit/CudK8NoLs4PPM8ZyBsNnB/jpsD7X0C8cjgDXnaQkqkEPCGMSuGWivxxzx8OQj6xtpHWuPhPloHfEMWzTD0HN2GQY2xuW2Lb+/Fh+VaIQPLE5meh+gmSxFYz+BjBogtjGscfiH26Z2GmbNNltVQAIpwDdP6ZNO2iRYqTpZGhfgWUaiMC2w1kCzkbALcqLvDVvUyK6MW9HFES0b8BvsFwXEZi4F+iBtykCcjtlqf0igr52VJ1eqiIgzzdlrRIZfWfJd3SD8ZbqHvkB4XSDpJNB9V0VcX/cT4Ys/zEzSuSj/3WxW9GpRD7ETpZ/QcOJ6LyfdHugitxNehbob9G7lo4PXWBmQhPqQ/fsDOeUfjqNCU1Csi+TQ5+2luzc3yaRr6elevQZwhZr4bomYam0U41yabYqov5hvSTMjlp6RyuiAarSTLPZmox5246+lVREx/isiuh6NxhUJwF2yEpCWu1bK8WEsjyRgCSa0XrVrjNra2mC7TWD5ilYAuJoA4jSlnppF5dVEU3g0ENTD4b3nsZiZsfGJuGqj8qY+CINGqP2GLZCJ+HjtpblxAJ9k3cPrLfGJrnVUaSCarxw0jFua1lOLNBerANx+byya6IkLM2uLZWpAl6/Mcc1EjvjKtLrJNLqI5HnjfQ+bsVeteb0g2y/t7hGvd7CNjenOL8OkJ40KtOdTF+Cl/nV6Mkf4gxocI9vZPYLLKs9iQrqRIACcM2IXGeSboYrg+rztY5ARaDJWUeo0W+sXudLTFhnApaAW6FkZy/+yuXasLoCrSfwVHAnW0+gK93YawKJMLUIAdNAKwhqnSxcYUpPKB6EBE2mg7VR///EX24jybTQerXnOC70FyVP3gjTPXPTQyaP8NFPNeJrCNTPP667JKOq6VNo/A2hes5ccUjmEmPmDoD5+FMgWCcA+3FG57QJP//kQ1PgGBIOToEDgUn+t4V/AJeGknwARIKLAAAAAElFTkSuQmCC"
|
||||
webUI["html/img/x_black.png"] = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAA6ppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0xMS0xNVQxNzoxMTozNzwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MTQ0PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj4xNDQ8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI1NjwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZIUJ+AAAXy0lEQVR4Ae2dCbhO1RrH/+Z5noUolDFjSBkuTsbUEXUoVG5JXZVLw71pvLqSBokGTzqkSAMhHZwyJFTITB2ZT6LIPA/3Xb6cew7f8U17WGvv/3o8j+/b315rve9vvevsd++93ncBLCRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQjkCWdJ8d/9i1J668yvFeo+3wh2/x1cxoKzter3RZ9H7A8V6j7fD1F3DkcLSVY6qXPabaMVauXR/39I+xDeeqHzqAljWRut25HmPpadjbaN0xlgacq7t9M0YPc667jD1lzfjV2W9DHsP6Vc52GUNv+QvipXdiqO9g1VvvNMb6T51Ev+44sN9BOhm6cnUCHDuG+xNw7GgGiXT+0qItbr9LZwGVbKXK4PkRuguZJt+wwVi2JO2b8x+yOd9lhh73/I69f6CNIRdrEb1Jc3w6AYcOZtBCqy+jP0T1a7SSKFNh5iXh8X6Z/urID25PAFFy1TI1YFWqOaJvzJ3kyo3KV2HKhzE3ZE8DXe7Ag4/b07TVre7eiYS2bt37pimjwQQQWb6Zg/geKFAwTSytP1xRFds2Y91K7YQU52f8DOTOo51gFwt09gz6dMH61Rf/4vARV+8B0nT9cy8evAMCxZTy3GvK1datvPgWChXRTajg8owcigXJwX9y9qgeVwDRecdW5MyJRs2cVT/a3uSv7BVVMHVStPVtqCfOzz+esKFdG5qUNyoP98YZLf7eaTMBhPOSBWjWBmXL24DchiblFd6WjTpcxJVuJUtj/HQznJ99e5EQh31/2jAk0TSphwsUkPzUKTzQA/K+yZTy3AiUKKWFsOL8FC6qhSQhhRjYBzu2hTzLsRN0mgCitNxcPtbXMeVj7ahIMQwdHWsjsdfv0gM3do69GSdaSByFmVOc6CjsPnRygQJCb1iDCpVQo07YKrh6ojy93bgBP611TQjl/Bjy5GftCtzXDadPu8YqWMeuLoYLJpA6li8/5vyIipUz+12v4/Iir3l1yBs9V8rYKWh7sys9R9bpkUNo2wAbf4qslv1na+YCBRQ+fEgtkZBVIkaUosXx31HuSBrf3QzrFzr/elBD6xe59HOBAqa061ccP6YeChlRqtZQXtDP6xwVVpyfcdORJ6+jnUbX2SfjMfyZ6KraXUtLFyigdJYsmDjLmDmwZzda1MCeP+wesP+3b4rz88tPyvmRq7qWRUsXKEDq7Fk81At7XfKtIx2tYiXxn5GRVor+/FsSzHB+ThxHvwRtrV/46+oCBUxD/myIX3FL9+gNxcmaV9dU4Q0pG2zvU14+yJMfI5yfZwYg6XPbgcTQgd4TQBTblKJe8dRrFIOODlaVxdIfvWd7hMPI91GrnoNaRdtV0lTIBNC7aHwPkAYuVy7M/B7Vaqcd0PrDZx+ohX32lZtvx+iJ9jVvWcup2xBXF7LMUe9iwgQQglWrI2mpGWtdRNq7OmPWNFvGXZyfeWshb6A1L6dP4daW+G6h5mKKeBrfBKeHJ3cCTz+S/oDWn9XKHHuWJcvKC/2tX8bm5WeNsH6RVPt7gDRLV4Fjtc0IHMtXQEULfDk1TXZrPnS+DQ8PtqYpW1tZ+BUG3Qt5iGdCMcQFCqAsUhTJK1GmnAlggTs7WJlHqHhJ5fzIW2fNi7wPaV0Hu3ZqLmaaeIa4QAF5zQockxwqBQulgY71gzg/+ls/zqJ/L4OsXwbFHBcoYEESOJYjBxqbEDgmeYTkz7Yld8M3dcMjT8U6hRyo/+ZwjHvTgX4s7MIoFyigd/bsmLIA9ZtYSMHGpnq0xdxZMbVvivOzfAluaYaThixhPD8kBk4AEb18RSSvQAHrHIzzOKz//9ftKqHiwQPRtzzmY3S4NfrqztQ8sA9x9VQ8k2nFNBcowFdw79higFmItDJLxXefMz1Kw+jUFQOejrKuk9X694SEuhtYzJwAAloCx+Q6YETgmOQAFuPYuili8yhWAhO+QJ58EVd0uMKEdzDqRYf7tKo7M12ggPYSODZ7OSpVsYqFje2kbkWLmhEvinxnMjp2tVEqS5resBodGuGoOQleM2pt1GPQjKIrezIlcOyyy/HUSxeIH+KrOD/6W//RI+h7u7nWL0NgrAsUMB954SLJpZvHhTAmHX6+pr5KfLR9S1iymOL8PHE/5s0OSyNdTzLZBQowNShwTHaCaFkrrHSwb3+ETt10tZnzck2dqFL7G15MdoEC6GXNiTyCMCJwrHwl/HtoaIPpeKsB1r/1F5MyOGUO3fwrQEC31h1UkJQB5Sy6tMTi+ZlKWqy4WvMjAZY6l5MncFNTrFyqs4xhymb+FSCgaPIXePf1MHV29bQseOVd5M08lcOQN3S3fsH3whPesH5RxfCb4PS2vGgu4jqhROn0x3T8LBGeefNjblIQ2Tp0waDnghzX6lDyDAx+SCuJYhHGKy5QgIEkKpTAMf2jxWUnhPjmF4aMGOH8/JaKNnUczf4Si3WHUdcrLlBA1ZT1ZgSOZcmKl99Fnoxbuejv/Jw5rcKdncx9FIYFx3iKh1ygAAlTAsckslEmQNpD9PbxBjg/rz6PjxJjNDjdqnvLBQrQlXhcCRzTf6MNcYQ6X4+li2GE87NkPrq20i23c+zTyVsuUICH7D5ixI5j4gi9Mha5c6uUcpo/95QM2LJ3iWaZzWO3fmnBcy5QgIoEjkncTOPmljCysRFZKX1zggE7o/W9DSuX2cjBvaa96AIFaGbLpgLHGlznHluv9DzmNTMeLUTF27sTQHAYFDgW1eA5UWnVUvXS98QJJ/pyow+PukABlBI4JuvPZGkNS3QEZMPC2+Nc2/wmOpkjrOXpCSAsZN+KcpejZt0IsfD0cwQG3H2pZUuegORpFygwQnnzqcCxK6p6YrwcVGLSWAy4x8H+3OnKBxNAwEpU7vRFyJHTHcYm9pqyDu0a4sgRE2WPSGavu0ABGAYFjkU0ejadLEF23dtiZ6pNzWvVrD8mgCBftgQNmqDilVrR11SYJ/tbmdVUUyX/EssfLlBAWdlW8auVur9zdd1cZnyMe7WPxrSOkp8mgFBr1R7vS+CYz7QO31x2bEGbuti/L/wapp/pGxcoMFCbU1CoCOo1Nn3YbJFfdia/syM2b7SlcV0b9eJiuEuzHvIY1q649Ck+/XXYYHWn5LPiS2egytVIWmZA4JiTtjhvFnq0M2VbFwvB+MwFCpCTxb2SRqVNJws5mt3U778h4cawEhaZrWcQ6X05AYTDquWoVgtVqgdB4rdDEpdzTzzWr/ab3gF9/XcPkDbOA/8OSd7PMnIoFiT7FoMv7wHSRrvxDfhkLrL69TIoHCRve5cWOHUqDYnfPvh47GWod2yDxM000T5wzCar3LcXCXGQLine truncated
|
||||
webUI["html/js/mapping-editor.js"] = "dmFyIG1hcHBpbmdFcnJvciAgPSBmYWxzZTsKdmFyIGJ1bGsgICAgICAgICAgPSBmYWxzZTsKdmFyIGJ1bGtFZGl0QWxsICAgPSBmYWxzZTsgCnZhciBzZWxlY3RPYmogICAgID0gbmV3IE9iamVjdCgpOwp2YXIgc2VhcmNoT2JqICAgICA9IG5ldyBPYmplY3QoKTsKCnZhciBidWxrSURzICAgICAgID0gbmV3IEFycmF5KCk7CnZhciBidWxrQ2hhbmdlT2JqID0gbmV3IE9iamVjdCgpOwoKZnVuY3Rpb24gY2hlY2tVbmRvKGtleSwgZWxtKSB7CiAgdmFyIHRtcCA9IG5ldyBPYmplY3QoKTsKICB0bXAgPSBlbG0KICBjb25zb2xlLmxvZygiLS0iKTsKICBpZiAodW5kby5oYXNPd25Qcm9wZXJ0eSgiZXBnTWFwcGluZyIpKSB7CiAgICB4RVBHWyJlcGdNYXBwaW5nIl0gPSBKU09OLnBhcnNlKEpTT04uc3RyaW5naWZ5KHVuZG9bImVwZ01hcHBpbmciXSkpOzsKICB9IGVsc2UgewogICAgdW5kb1siZXBnTWFwcGluZyJdID0gSlNPTi5wYXJzZShKU09OLnN0cmluZ2lmeShlbG0pKTsKICB9Cn0KCi8vdmFyIHBsZXhDYXRlZ29yaWVzID0gbmV3IEFycmF5KCItIiwgIkFjdGlvbiBzcG9ydHMiLCAiQWN0aW9uIiwgIkFkdWx0cyBvbmx5IiwgIkFkdmVudHVyZSIsICJBZXJvYmljcyIsICJBbmltYWxzIiwgIkFuaW1hdGVkIiwgIkFuaW1lIiwgIkFudGhvbG9neSIsICJBcmNoZXJ5IiwgIkFydCIsICJBcnRzL2NyYWZ0cyIsICJBdWN0aW9uIiwgIkF1dG8gcmFjaW5nIiwgIkF1dG8iLCAiQXZpYXRpb24iLCAiQXdhcmRzIiwgIkJhbGxldCIsICJCYXNlYmFsbCIsICJCYXNrZXRiYWxsIiwgIkJpY3ljbGUgcmFjaW5nIiwgIkJpY3ljbGUiLCAiQmlsbGlhcmRzIiwgIkJpb2dyYXBoeSIsICJCb2F0IHJhY2luZyIsICJCb2F0IiwgIkJvd2xpbmciLCAiQm94aW5nIiwgIkJ1cy4vZmluYW5jaWFsIiwgIkNoaWxkcmVuIiwgIkNvbGxlY3RpYmxlcyIsICJDb21lZHkgZHJhbWEiLCAiQ29tZWR5IiwgIkNvbW11bml0eSIsICJDb21wdXRlcnMiLCAiQ29uc3VtZXIiLCAiQ29va2luZyIsICJDcmltZSBkcmFtYSIsICJDcmltZSIsICJEYW5jZSIsICJEYXJrIGNvbWVkeSIsICJEZWJhdGUiLCAiRGl2aW5nIiwgIkRvY3VkcmFtYSIsICJEb2N1bWVudGFyeSIsICJEcmFtYSIsICJFZHVjYXRpb25hbCIsICJFbnRlcnRhaW5tZW50IiwgIkVudmlyb25tZW50IiwgIkVxdWVzdHJpYW4iLCAiRXJvdGljIiwgIkV2ZW50IiwgIkZhbnRhc3kiLCAiRmFzaGlvbiIsICJGZWF0dXJlIEZpbG0iLCAiRmlzaGluZyIsICJGb290YmFsbCIsICJHYW1lIHNob3ciLCAiR2FtaW5nIiwgIkdheS9sZXNiaWFuIiwgIkdvbGYiLCAiSGFuZGJhbGwiLCAiSGVhbHRoIiwgIkhpc3RvcmljYWwgZHJhbWEiLCAiSGlzdG9yeSIsICJIb2NrZXkiLCAiSG9saWRheSIsICJIb21lIGltcHJvdmVtZW50IiwgIkhvcnJvciIsICJIb3JzZSIsICJIb3VzZS9nYXJkZW4iLCAiSG93LXRvIiwgIkludGVydmlldyIsICJJbnRsIHNvY2NlciIsICJMYXciLCAiTWFydGlhbCBhcnRzIiwgIk1lZGljYWwiLCAiTWlsaXRhcnkiLCAiTWluaXNlcmllcyIsICJNaXhlZCBtYXJ0aWFsIGFydHMiLCAiTW90b3JjeWNsZSByYWNpbmciLCAiTW90b3JjeWNsZSIsICJNb3RvcnNwb3J0cyIsICJNb3VudGFpbiBiaWtpbmciLCAiTXVzaWMiLCAiTXVzaWNhbCBjb21lZHkiLCAiTXVzaWNhbCIsICJNeXN0ZXJ5IiwgIk5hdHVyZSIsICJOZXdzIiwgIk5ld3NtYWdhemluZSIsICJPbHltcGljcyIsICJPcGVyYSIsICJPdXRkb29ycyIsICJQYXJhZGUiLCAiUGFyYW5vcm1hbCIsICJQYXJlbnRpbmciLCAiUGVyZm9ybWluZyBhcnRzIiwgIlBsYXlvZmYgc3BvcnRzIiwgIlBva2VyIiwgIlBvbGl0aWNzIiwgIlBybyB3cmVzdGxpbmciLCAiUHVibGljIGFmZmFpcnMiLCAiUmVhbGl0eSIsICJSZWxpZ2lvdXMiLCAiUm9kZW8iLCAiUm9sbGVyIGRlcmJ5IiwgIlJvbWFuY2UiLCAiUm9tYW50aWMgY29tZWR5IiwgIlJ1Z2J5IiwgIlJ1bm5pbmciLCAiU2FpbGluZyIsICJTY2llbmNlIGZpY3Rpb24iLCAiU2NpZW5jZSIsICJTZWxmIGltcHJvdmVtZW50IiwgIlNlcmllcyIsICJTaG9vdGluZyIsICJTaG9wcGluZyIsICJTaG9ydCBGaWxtIiwgIlNpdGNvbSIsICJTa2lpbmciLCAiU25vb2tlciIsICJTb2FwIiwgIlNvY2NlciIsICJTcGVjaWFsIiwgIlNwb3J0cyIsICJzcG9ydHMiLCAiU3BvcnRzIGV2ZW50IiwgIlNwb3J0cyBub24tZXZlbnQiLCAiU3BvcnRzIHRhbGsiLCAiU3RhbmR1cCIsICJTdXJmaW5nIiwgIlN1c3BlbnNlIiwgIlRWIE1vdmllIiwgIlRhbGsiLCAiVGVjaG5vbG9neSIsICJUZW5uaXMiLCAiVGhlYXRlciIsICJUaHJpbGxlciIsICJUcmFjay9maWVsZCIsICJUcmF2ZWwiLCAiVHJpYXRobG9uIiwgIlZhcmlldHkiLCAiVm9sbGV5YmFsbCIsICJXYXIiLCAiV2F0ZXJzcG9ydHMiLCAiV2VhdGhlciIsICJXZXN0ZXJuIiwgIldyZXN0bGluZyIsICJZYWNodCByYWNpbmciLCAibW92aWUiLCAic2VyaWVzIiwgInNwb3J0cyIsICJ0dnNob3ciKTsKdmFyIHBsZXhDYXRlZ29yaWVzVmFsdWVzID0gbmV3IEFycmF5KCItIiwgIktpZHMiLCAiTmV3cyIsICJNb3ZpZSIsICJTZXJpZXMiLCAiU3BvcnRzIikKdmFyIHBsZXhDYXRlZ29yaWVzT3B0aW9uID0gbmV3IEFycmF5KCItIiwgIktpZHMgKEVtYnkgb25seSkiLCAiTmV3cyIsICJNb3ZpZSIsICJTZXJpZXMiLCAiU3BvcnRzIikKCgpmdW5jdGlvbiBvcGVuTWFwcGluZ0VkaXRvcihlbG0pIHsKICB2YXIgY29sdW1uVG9Tb3J0ICA9IDEKCiAgY2hlY2tVbmRvKCJlcGdNYXBwaW5nIiwgeEVQR1siZXBnTWFwcGluZyJdKQoKICB2YXIgbmV3RGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CiAgCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gICAgICAgPSAiU2F2ZSI7CiAgbmV3RW50cnlbIm9uY2xpY2siXSAgICAgPSAic2F2ZVhFUEcoKSI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gICAgICAgPSAiQnVsayBFZGl0IjsKICBuZXdFbnRyeVsib25jbGljayJdICAgICA9ICJidWxrRWRpdCgpIjsKICBuZXdEaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gICAgPSAiSU5QVVQiOwogIG5ld0VudHJ5WyJ0eXBlIl0gICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbImNsYXNzIl0gICAgICAgPSAiYnV0dG9uIjsKICBuZXdFbnRyeVsidmFsdWUiXSAgICAgICA9ICJTaG93IFhFUEciOwogIG5ld0VudHJ5WyJvbmNsaWNrIl0gICAgID0gInNob3dYRVBHKCkiOwogIG5ld0Rpdi5hcHBlbmRDaGlLine truncated
|
||||
webUI["html/js/menu_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBNYWluTWVudSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIE1haW5NZW51KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJtYWluLW1lbnUiOwogICAgICAgIHRoaXMuSFRNTFRhZyA9ICJMSSI7CiAgICAgICAgdGhpcy5JbWFnZVBhdGggPSAiaW1nLyI7CiAgICB9CiAgICBNYWluTWVudS5wcm90b3R5cGUuY3JlYXRlSU1HID0gZnVuY3Rpb24gKHNyYykgewogICAgICAgIHZhciBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiSU1HIik7CiAgICAgICAgZWxlbWVudC5zZXRBdHRyaWJ1dGUoInNyYyIsIHRoaXMuSW1hZ2VQYXRoICsgc3JjKTsKICAgICAgICByZXR1cm4gZWxlbWVudDsKICAgIH07CiAgICBNYWluTWVudS5wcm90b3R5cGUuY3JlYXRlVmFsdWUgPSBmdW5jdGlvbiAodmFsdWUpIHsKICAgICAgICB2YXIgZWxlbWVudCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlAiKTsKICAgICAgICBlbGVtZW50LmlubmVySFRNTCA9IHZhbHVlOwogICAgICAgIHJldHVybiBlbGVtZW50OwogICAgfTsKICAgIHJldHVybiBNYWluTWVudTsKfSgpKTsKdmFyIE1haW5NZW51SXRlbSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uIChfc3VwZXIpIHsKICAgIF9fZXh0ZW5kcyhNYWluTWVudUl0ZW0sIF9zdXBlcik7CiAgICBmdW5jdGlvbiBNYWluTWVudUl0ZW0obWVudUtleSwgdmFsdWUsIGltYWdlLCBoZWFkbGluZSkgewogICAgICAgIHZhciBfdGhpcyA9IF9zdXBlci5jYWxsKHRoaXMpIHx8IHRoaXM7CiAgICAgICAgX3RoaXMubWVudUtleSA9IG1lbnVLZXk7CiAgICAgICAgX3RoaXMudmFsdWUgPSB2YWx1ZTsKICAgICAgICBfdGhpcy5pbWdTcmMgPSBpbWFnZTsKICAgICAgICBfdGhpcy5oZWFkbGluZSA9IGhlYWRsaW5lOwogICAgICAgIHJldHVybiBfdGhpczsKICAgIH0KICAgIE1haW5NZW51SXRlbS5wcm90b3R5cGUuY3JlYXRlSXRlbSA9IGZ1bmN0aW9uICgpIHsKICAgICAgICB2YXIgaXRlbSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIkxJIik7CiAgICAgICAgaXRlbS5zZXRBdHRyaWJ1dGUoIm9uY2xpY2siLCAiamF2YXNjcmlwdDogb3BlblRoaXNNZW51KHRoaXMpIik7CiAgICAgICAgaXRlbS5zZXRBdHRyaWJ1dGUoImlkIiwgdGhpcy5pZCk7CiAgICAgICAgdmFyIGltZyA9IHRoaXMuY3JlYXRlSU1HKHRoaXMuaW1nU3JjKTsKICAgICAgICB2YXIgdmFsdWUgPSB0aGlzLmNyZWF0ZVZhbHVlKHRoaXMudmFsdWUpOwogICAgICAgIGl0ZW0uYXBwZW5kQ2hpbGQoaW1nKTsKICAgICAgICBpdGVtLmFwcGVuZENoaWxkKHZhbHVlKTsKICAgICAgICB2YXIgZG9jID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQodGhpcy5Eb2N1bWVudElEKTsKICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoaXRlbSk7CiAgICAgICAgc3dpdGNoICh0aGlzLm1lbnVLZXkpIHsKICAgICAgICAgICAgY2FzZSAicGxheWxpc3QiOgogICAgICAgICAgICAgICAgdGhpcy50YWJsZUhlYWRlciA9IFsie3sucGxheWxpc3QudGFibGUucGxheWxpc3R9fSIsICJ7ey5wbGF5bGlzdC50YWJsZS50dW5lcn19IiwgInt7LnBsYXlsaXN0LnRhYmxlLmxhc3RVcGRhdGV9fSIsICJ7ey5wbGF5bGlzdC50YWJsZS5hdmFpbGFiaWxpdHl9fSAlIiwgInt7LnBsYXlsaXN0LnRhYmxlLnR5cGV9fSIsICJ7ey5wbGF5bGlzdC50YWJsZS5zdHJlYW1zfX0iLCAie3sucGxheWxpc3QudGFibGUuZ3JvdXBUaXRsZX19ICUiLCAie3sucGxheWxpc3QudGFibGUudHZnSUR9fSAlIiwgInt7LnBsYXlsaXN0LnRhYmxlLnVuaXF1ZUlEfX0gJSJdOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgInhtbHR2IjoKICAgICAgICAgICAgICAgIHRoaXMudGFibGVIZWFkZXIgPSBbInt7LnhtbHR2LnRhYmxlLmd1aWRlfX0iLCAie3sueG1sdHYudGFibGUubGFzdFVwZGF0ZX19IiwgInt7LnhtbHR2LnRhYmxlLmF2YWlsYWJpbGl0eX19ICUiLCAie3sueG1sdHYudGFibGUuY2hhbm5lbHN9fSIsICJ7ey54bWx0di50YWJsZS5wcm9ncmFtc319Il07CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSAiZmlsdGVyIjoKICAgICAgICAgICAgICAgIHRoaXMudGFibGVIZWFkZXIgPSBbInt7LmZpbHRlci50YWJsZS5uYW1lfX0iLCAie3suZmlsdGVyLnRhYmxlLnR5cGV9fSIsICJ7ey5maWx0ZXIudGFibGUuZmlsdGVyfX0iXTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJ1c2VycyI6CiAgICAgICAgICAgICAgICB0aGlzLnRhYmxlSGVhZGVyID0gWyJ7ey51c2Vycy50YWJsZS51c2VybmFtZX19IiwgInt7LnVzZXJzLnRhYmxlLnBhc3N3b3JkfX0iLCAie3sudXNlcnMudGFibGUud2VifX0iLCAie3sudXNlcnMudGFibGUucG1zfX0iLCAie3sudXNlcnMudGFibGUubTN1fX0iLCAie3sudXNlcnMudGFibGUueG1sfX0iLCAie3sudXNlcnMudGFibGUuYXBpfX0iXTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJtYXBwaW5nIjoKICAgICAgICAgICAgICAgIHRoaXMudGFibGVIZWFkZXIgPSBbIkJVTEsiLCAie3subWFwcGluZy50YWJsZS5jaE5vfX0iLCAie3subWFwcGluZy50YWJsZS5sb2dvfX0iLCAie3subWFwcGluZy50YWJsZS5jaGFubmVsTmFtZX19IiwgInt7Lm1hcHBpbmcudGFibGUucGxheWxpc3R9fSIsICJ7ey5tYXBwaW5nLnRhYmxlLmdyb3VwVGl0bGV9fSIsICJ7ey5tYXBwaW5nLnRhYmxlLnhtbHR2RmlsZX19IiwgInt7Lm1hcHBpbmcudGFibGUueG1sdHZJRH19Il07CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICB9CiAgICAgICAgLy9jb25zb2xlLmxvZyh0aGlzLm1lbnVLZXksIHRoaXMudGFibGVIZWFkZXIpOwogICAgfTsKICAgIHJldHVybiBNYWluTWVudUl0ZW07Cn0oTWFpbk1lbnUpKTsKdmFyIENvbnRlbnQgPSAvKiogQGNsYXNzICovIChmdW5jdGlvbiAoKSB7CiAgICBmdW5jdGlvbiBDb250ZW50KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJjb250ZW50IjsKICAgICAgICB0aGlzLlRhYmxlSUQgPSAiY29udGVudF90YWJsZSI7CiAgICAgICAgdGhpcy5oZWFkZXJDbGFzcyA9ICJjb250ZW50X3RhYmxlX2hlYWRlciI7CiAgICAgICAgdGhpcy5pbnRlcmFjdGlvbklEID0gImNvbnRlbnQtaW50ZXJhY3Line truncated
|
||||
webUI["html/lang/en.json"] = "ewogICJtYWluTWVudSI6IHsKICAgICJpdGVtIjp7CiAgICAgICJwbGF5bGlzdCI6ICJQbGF5bGlzdCIsCiAgICAgICJwbXNJRCI6ICJQTVMgSUQiLAogICAgICAiZmlsdGVyIjogIkZpbHRlciIsCiAgICAgICJ4bWx0diI6ICJYTUxUViIsCiAgICAgICJtYXBwaW5nIjogIk1hcHBpbmciLAogICAgICAidXNlcnMiOiAiVXNlcnMiLAogICAgICAic2V0dGluZ3MiOiAiU2V0dGluZ3MiLAogICAgICAibG9nIjogIkxvZyIsCiAgICAgICJsb2dvdXQiOiAiTG9nb3V0IgogICAgfSwKICAgICJoZWFkbGluZSI6IHsKICAgICAgInBsYXlsaXN0IjogIkxvY2FsIG9yIHJlbW90ZSBwbGF5bGlzdHMiLAogICAgICAiZmlsdGVyIjogIkZpbHRlciBwbGF5bGlzdCIsCiAgICAgICJ4bWx0diI6ICJMb2NhbCBvciByZW1vdGUgWE1MVFYgZmlsZXMiLAogICAgICAibWFwcGluZyI6ICJNYXAgcGxheWxpc3QgY2hhbm5lbHMgdG8gRVBHIGNoYW5uZWxzIiwKICAgICAgInVzZXJzIjogIlVzZXIgbWFuYWdlbWVudCIsCiAgICAgICJzZXR0aW5ncyI6ICJTZXR0aW5ncyIsCiAgICAgICJsb2ciOiAiTG9nIiwKICAgICAgImxvZ291dCI6ICJMb2dvdXQiCiAgICB9CiAgfSwKICAiY29uZmlybSI6ewogICAgInJlc3RvcmUiOiAiQWxsIGRhdGEgd2lsbCBiZSByZXBsYWNlZCB3aXRoIHRob3NlIGZyb20gdGhlIGJhY2t1cC4gU2hvdWxkIHRoZSBmaWxlcyBiZSByZXN0b3JlZD8iCiAgfSwKICAiYWxlcnQiOiB7CiAgICAiZmlsZUxvYWRpbmdFcnJvciI6ICJGaWxlIGNvdWxkbid0IGJlIGxvYWRlZCIsCiAgICAiaW52YWxpZENoYW5uZWxOdW1iZXIiOiAiSW52YWxpZCBjaGFubmVsIG51bWJlciIsCiAgICAibWlzc2luZ0lucHV0IjogIk1pc3NpbmcgaW5wdXQiCiAgfSwKICAiYnV0dG9uIjp7CiAgICAiYmFjayI6ICJCYWNrIiwKICAgICJiYWNrdXAiOiAiQmFja3VwIiwKICAgICJidWxrRWRpdCI6ICJCdWxrIEVkaXQiLAogICAgImNhbmNlbCI6ICJDYW5jZWwiLAogICAgImRlbGV0ZSI6ICJEZWxldGUiLAogICAgImRvbmUiOiAiRG9uZSIsCiAgICAibG9naW4iOiAiTG9naW4iLAogICAgIm5ldyI6ICJOZXciLAogICAgIm5leHQiOiAiTmV4dCIsCiAgICAicmVzdG9yZSI6ICJSZXN0b3JlIiwKICAgICJzYXZlIjogIlNhdmUiLAogICAgInNlYXJjaCI6ICJTZWFyY2giLAogICAgInVwZGF0ZSI6ICJVcGRhdGUiLAogICAgImNyYWV0ZUFjY291bnQiOiAiQ3JlYXRlIEFjY291bnQiLAogICAgInJlc2V0TG9ncyI6ICJSZXNldCBMb2dzIiwKICAgICJ1cGxvYWRMb2dvIjogIlVwbG9hZCBMb2dvIgogIH0sCiAgImZpbHRlciI6IHsKICAgICJ0YWJsZSI6IHsKICAgICAgIm5hbWUiOiAiRmlsdGVyIE5hbWUiLAogICAgICAidHlwZSI6ICJGaWx0ZXIgVHlwZSIsCiAgICAgICJmaWx0ZXIiOiAiRmlsdGVyIgogICAgfSwKICAgICJjdXN0b20iOiAiQ3VzdG9tIiwKICAgICJncm91cCI6ICJHcm91cCIsCiAgICAibmFtZSI6IHsKICAgICAgInRpdGxlIjogIkZpbHRlciBOYW1lIiwKICAgICAgInBsYWNlaG9sZGVyIjogIkZpbHRlciBuYW1lIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAiZGVzY3JpcHRpb24iOiB7CiAgICAgICJ0aXRsZSI6ICJEZXNjcmlwdGlvbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJEZXNjcmlwdGlvbiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgInR5cGUiOiB7CiAgICAgICJ0aXRsZSI6ICJUeXBlIiwKICAgICAgImdyb3VwVGl0bGUiOiAiR3JvdXAgVGl0bGUiLAogICAgICAiY3VzdG9tRmlsdGVyIjogIkN1c3RvbSBGaWx0ZXIiCiAgICB9LAogICAgImNhc2VTZW5zaXRpdmUiOiB7CiAgICAgICJ0aXRsZSI6ICJDYXNlIFNlbnNpdGl2ZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWx0ZXJSdWxlIjogewogICAgICAidGl0bGUiOiAiRmlsdGVyIFJ1bGUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiU3BvcnQge0hEfSAhe0VTLElUfSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgImZpbHRlckdyb3VwIjogewogICAgICAidGl0bGUiOiAiR3JvdXAgVGl0bGUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlNlbGVjdCBhIE0zVSBncm91cC4gKENvdW50ZXIpPGJyPkNoYW5naW5nIHRoZSBncm91cCB0aXRsZSBpbiB0aGUgTTNVIGludmFsaWRhdGVzIHRoZSBmaWx0ZXIuIgogICAgfSwKICAgICJpbmNsdWRlIjogewogICAgICAidGl0bGUiOiAiSW5jbHVkZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJGSEQsVUhEIiwKICAgICAgImRlc2NyaXB0aW9uIjogIkNoYW5uZWwgbmFtZSBtdXN0IGluY2x1ZGUuPGJyPihDb21tYSBzZXBhcmF0ZWQpIENvbW1hIG1lYW5zIG9yIgogICAgfSwKICAgICJleGNsdWRlIjogewogICAgICAidGl0bGUiOiAiRXhjbHVkZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJFUyxJVCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJDaGFubmVsIG5hbWUgbXVzdCBub3QgY29udGFpbi48YnI+KENvbW1hIHNlcGFyYXRlZCkgQ29tbWEgbWVhbnMgb3IiCiAgICB9CgogIH0sCiAgInBsYXlsaXN0IjogewogICAgInRhYmxlIjogewogICAgICAicGxheWxpc3QiOiAiUGxheWxpc3QiLAogICAgICAidHVuZXIiOiAiVHVuZXIiLAogICAgICAibGFzdFVwZGF0ZSI6ICJMYXN0IFVwZGF0ZSIsCiAgICAgICJhdmFpbGFiaWxpdHkiOiAiQXZhaWxhYmlsaXR5IiwKICAgICAgInR5cGUiOiAiVHlwZSIsCiAgICAgICJzdHJlYW1zIjogIlN0cmVhbXMiLAogICAgICAiZ3JvdXBUaXRsZSI6ICJncm91cC10aXRsZSIsCiAgICAgICJ0dmdJRCI6ICJ0dmctaWQiLAogICAgICAidW5pcXVlSUQiOiAiVW5pcXVlIElEIgogICAgfSwKICAgICJwbGF5bGlzdFR5cGUiOiB7CiAgICAgICJ0aXRsZSI6ICJQbGF5bGlzdCB0eXBlIiwKICAgICAgInBsYWNlaG9sZGVyIjogIiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgInR5cGUiOiB7CiAgICAgICJ0aXRsZSI6ICJUeXBlIiwKICAgICAgInBsYWNlaG9sZGVyIjogIiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgIm5hbWUiOiB7CiAgICAgICJ0aXRsZSI6ICJOYW1lIiwKICAgICAgInBsYWNlaG9sZGVyIjogIlBsYXlsaXN0IG5hbWUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJkZXNjcmlwdGlvbiI6IHsKICAgICAgInRpdGxlIjogIkRlc2NyaXB0aW9uIiwKICAgICAgInBsYWNlaG9sZGVyIjogIkRlc2NyaXB0aW9uIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAiZmlsZU0zVSI6IHsKICAgICAgInRpdGxlIjogIk0zVSBGaWxlIiwKICAgICAgInBsYWNlaG9sZGVyIjogIkZpbGUgcGF0aCBvciBVUkwgb2YgdGhlIE0zVSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgImZpbGVIREhSIjogewogICAgICAidGl0bGUiOiAiSERIb21lUnVuIElQIiwKICAgICAgInBsYWNlaG9sZGVyIjogIklQIGFkZHJlc3MgYW5kIHBvcnQgKDE5Mi4xNjguMS4xMDo1MDA0KSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgInR1bmVyIjogewogICAgICAidGl0bGUiOiAiVHVuZXIgLyBTdHJlYW1zIiwKICAgICAgInBsYWNlaG9sZGVyIjogIiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJOdW1iZXIgb2YgcGFyYWxsZWwgY29ubmVjdGlvbnMgdGhhdCBjYW4gYmUgZXN0YWJLine truncated
|
||||
webUI["html/img/log.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0xMC0xNFQxMToxMDo0MjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CkP32mEAAANASURBVGgF7ZlPiE1RHMffw1AYhiL5k1HEijJiYaFZiHoyFspW2RFRkkSZGgtFWJIwi9FYmo0VOywkKTbKgjSlWcifRCPX51dv6s2d33md77vvXjfmV9859/7u93x/v98959x73p1KRbAkSargAhgDH0E/qIYkuHYEvAPGvwbmNOEerXM/0Q6BRSFuZj/ix0DaDnvCkGppIucDAe4OhzvkcUO+GaELAf8ux7/b8Zlrr+Pvc3zm6nX8NccXdKmFLHaUPJ/Ruhyu5zNap8Nd6PiCLrUQTyi0Rjy/5/M0Zd+smB7M32XwtgBr07aC6yfSTs7XO77OALfH4VbgHsD/HLytVquJx4nyIdQFBsHftscksK5Z0sGhpuN8Or4Aa5sJFHjtO7G2MjKvvZjN1sglOpSlCMt9LrDZ4S4Hd0Qg24vrK+gAZbPtjMqTdFKhEdkIsYxFWP720JlioUK6pzDL41jtpRIqxOOWxecuB3fhRGT8E85YBK8VygI6GSRTCxlH3TaJgyw4O2678aCxO94LbNPovYDdmOrUukIBN/MqwjJEOwGPOHR31W4VONVCLEBR9lAJpBbSrYhn5K5R+quFnGUOr1ICtMIlxjz6XVb6qot9JeKvCPSAdlQJJHDtd8hOIN0wtRDLxx6Ntr0ulalTq1TJNyYzXUjj3SjD8X87Iva7uR8s5+2bi6FtX1oOgi8g2tSn1g2yPx+t3gIR/c90s1+C1vuO/YkxdWrdjxFtE2dE0VELWaqIZ+QuUfqrhZxmyKUvgEoyE1xizOR4YOI8plXXyAZEbYsyTJvXFsUW+x6wGUSbWogJ237rZHSEgojq1CooLT3MdCH6Pcu3xz8zIq0s9lvc2+sgz6dWH/rnQPB/jlybZGoh99hCHJqk0P6TD0jaI/4b7dVYeXVq2bemokyKpRZiHwWKMimWWshxhryjoEpOKXHUNbIN8WcUc5s2r8Vue7l9oAaiTS3EhDeB6EUYnUlGojq1MobLr3uokB/5hcys7OYWKuRl5nD5Cbi5uYXw0ntPHoay2S8Seuol5RZSJ0r/n/DEc/BdrN9kTZrH7BkwDspgwyQxW6uggU3nHnAXvAG/QZE2SrARsL8hJffwDxM0mNDPvT8IAAAAAElFTkSuQmCC"
|
||||
webUI["html/img/stream-limit.jpg"] = "/9j/4AAQSkZJRgABAQAAAQABAAD/4gJoSUNDX1BST0ZJTEUAAQEAAAJYbGNtcwQwAABtbnRyUkdCIFhZWiAH4wAFABcAFgAuAAphY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAAAEBjcHJ0AAABSAAAAE53dHB0AAABmAAAABRjaGFkAAABrAAAACxyWFlaAAAB2AAAABRiWFlaAAAB7AAAABRnWFlaAAACAAAAABRyVFJDAAACFAAAACBnVFJDAAACFAAAACBiVFJDAAACFAAAACBjaHJtAAACNAAAACRtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACQAAAAcAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAbWx1YwAAAAAAAAABAAAADGVuVVMAAAAyAAAAHABOAG8AIABjAG8AcAB5AHIAaQBnAGgAdAAsACAAdQBzAGUAIABmAHIAZQBlAGwAeQAAAABYWVogAAAAAAAA9tYAAQAAAADTLXNmMzIAAAAAAAEMSgAABeP///MqAAAHmwAA/Yf///ui///9owAAA9gAAMCUWFlaIAAAAAAAAG+UAAA47gAAA5BYWVogAAAAAAAAJJ0AAA+DAAC2vlhZWiAAAAAAAABipQAAt5AAABjecGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACltjaHJtAAAAAAADAAAAAKPXAABUewAATM0AAJmaAAAmZgAAD1z/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAQ4B4ADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKLine truncated
|
||||
webUI["html/img/xmltv.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0wNy0yOFQyMDowNzozMzwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Co6j9bsAAAGgSURBVGgF7VqxTsNADL0gYGEon8DOwsbAAH8BYmJn6cYIn8DC3h0+oixISFRiYYCJL0AwMMBAeK5w2rpV6uikxHfYknW98+vds18TK21DWGBlWR7CH+BfcEv2AzLP8HP42gLqkyUATuEp2PWEdQjF9ATs1zF/g29Mrxt+vVcUxR3xWxEktzFPJQmivsv8ZSI9DiQyVnxlIonwn6fpiczXpNuVVXH8O+a3Ys3y9NUyuf/NTTbEHZTjUlGSRzSiPhroUIENwB4AS/vS/susDwDhTpYBER9g7wHh5DWyibV9CiitCZbIafDEYUuJHQI3Nr/9ciWsjK6IFSWYhyvClbAyZqOIlYJG84jq7E1Ot97Zm+TinV1TrWwudk9EI3ebGFekzWprzspGEU2ySWCiOrs/s9dr7M/s9fVJJJrNXcsTsfaJc0WsKZINH+/sf1Jqvl1n1f2ZnStRN/pdq646XcSkIh9dkIg4s+IrE3nCpp8RG7f91ns+cCYR/LD4jcAZB42PN+A7/mcQ8ZxJhBYQvMJwBB/BKTFLVoLMC/wCfgyv7BesTKUC2LKM3wAAAABJRU5ErkJggg=="
|
||||
webUI["html/js/network_ts.js"] = "dmFyIFNlcnZlciA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIFNlcnZlcihjbWQpIHsKICAgICAgICB0aGlzLmNtZCA9IGNtZDsKICAgIH0KICAgIFNlcnZlci5wcm90b3R5cGUucmVxdWVzdCA9IGZ1bmN0aW9uIChkYXRhKSB7CiAgICAgICAgaWYgKFNFUlZFUl9DT05ORUNUSU9OID09IHRydWUpIHsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICBTRVJWRVJfQ09OTkVDVElPTiA9IHRydWU7CiAgICAgICAgY29uc29sZS5sb2coZGF0YSk7CiAgICAgICAgaWYgKHRoaXMuY21kICE9ICJ1cGRhdGVMb2ciKSB7CiAgICAgICAgICAgIHNob3dFbGVtZW50KCJsb2FkaW5nIiwgdHJ1ZSk7CiAgICAgICAgICAgIFVORE8gPSBuZXcgT2JqZWN0KCk7CiAgICAgICAgfQogICAgICAgIHN3aXRjaCAod2luZG93LmxvY2F0aW9uLnByb3RvY29sKSB7CiAgICAgICAgICAgIGNhc2UgImh0dHA6IjoKICAgICAgICAgICAgICAgIHRoaXMucHJvdG9jb2wgPSAid3M6Ly8iOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgImh0dHBzOiI6CiAgICAgICAgICAgICAgICB0aGlzLnByb3RvY29sID0gIndzczovLyI7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICB9CiAgICAgICAgdmFyIHVybCA9IHRoaXMucHJvdG9jb2wgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAiOiIgKyB3aW5kb3cubG9jYXRpb24ucG9ydCArICIvZGF0YS8iICsgIj9Ub2tlbj0iICsgZ2V0Q29va2llKCJUb2tlbiIpOwogICAgICAgIGRhdGFbImNtZCJdID0gdGhpcy5jbWQ7CiAgICAgICAgdmFyIHdzID0gbmV3IFdlYlNvY2tldCh1cmwpOwogICAgICAgIHdzLm9ub3BlbiA9IGZ1bmN0aW9uICgpIHsKICAgICAgICAgICAgV1NfQVZBSUxBQkxFID0gdHJ1ZTsKICAgICAgICAgICAgY29uc29sZS5sb2coIlJFUVVFU1QgKEpTKToiKTsKICAgICAgICAgICAgY29uc29sZS5sb2coZGF0YSk7CiAgICAgICAgICAgIGNvbnNvbGUubG9nKCJSRVFVRVNUOiAoSlNPTikiKTsKICAgICAgICAgICAgY29uc29sZS5sb2coSlNPTi5zdHJpbmdpZnkoZGF0YSkpOwogICAgICAgICAgICB0aGlzLnNlbmQoSlNPTi5zdHJpbmdpZnkoZGF0YSkpOwogICAgICAgIH07CiAgICAgICAgd3Mub25lcnJvciA9IGZ1bmN0aW9uIChlKSB7CiAgICAgICAgICAgIGNvbnNvbGUubG9nKCJObyB3ZWJzb2NrZXQgY29ubmVjdGlvbiB0byB4VGVWZSBjb3VsZCBiZSBlc3RhYmxpc2hlZC4gQ2hlY2sgeW91ciBuZXR3b3JrIGNvbmZpZ3VyYXRpb24uIik7CiAgICAgICAgICAgIFNFUlZFUl9DT05ORUNUSU9OID0gZmFsc2U7CiAgICAgICAgICAgIGlmIChXU19BVkFJTEFCTEUgPT0gZmFsc2UpIHsKICAgICAgICAgICAgICAgIGFsZXJ0KCJObyB3ZWJzb2NrZXQgY29ubmVjdGlvbiB0byB4VGVWZSBjb3VsZCBiZSBlc3RhYmxpc2hlZC4gQ2hlY2sgeW91ciBuZXR3b3JrIGNvbmZpZ3VyYXRpb24uIik7CiAgICAgICAgICAgIH0KICAgICAgICB9OwogICAgICAgIHdzLm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKSB7CiAgICAgICAgICAgIFNFUlZFUl9DT05ORUNUSU9OID0gZmFsc2U7CiAgICAgICAgICAgIHNob3dFbGVtZW50KCJsb2FkaW5nIiwgZmFsc2UpOwogICAgICAgICAgICBjb25zb2xlLmxvZygiUkVTUE9OU0U6Iik7CiAgICAgICAgICAgIHZhciByZXNwb25zZSA9IEpTT04ucGFyc2UoZS5kYXRhKTsKICAgICAgICAgICAgY29uc29sZS5sb2cocmVzcG9uc2UpOwogICAgICAgICAgICBpZiAocmVzcG9uc2UuaGFzT3duUHJvcGVydHkoInRva2VuIikpIHsKICAgICAgICAgICAgICAgIGRvY3VtZW50LmNvb2tpZSA9ICJUb2tlbj0iICsgcmVzcG9uc2VbInRva2VuIl07CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKHJlc3BvbnNlWyJzdGF0dXMiXSA9PSBmYWxzZSkgewogICAgICAgICAgICAgICAgYWxlcnQocmVzcG9uc2VbImVyciJdKTsKICAgICAgICAgICAgICAgIGlmIChyZXNwb25zZS5oYXNPd25Qcm9wZXJ0eSgicmVsb2FkIikpIHsKICAgICAgICAgICAgICAgICAgICBsb2NhdGlvbi5yZWxvYWQoKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAocmVzcG9uc2UuaGFzT3duUHJvcGVydHkoImxvZ29VUkwiKSkgewogICAgICAgICAgICAgICAgdmFyIGRpdiA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJjaGFubmVsLWljb24iKTsKICAgICAgICAgICAgICAgIGRpdi52YWx1ZSA9IHJlc3BvbnNlWyJsb2dvVVJMIl07CiAgICAgICAgICAgICAgICBkaXYuY2xhc3NOYW1lID0gImNoYW5nZWQiOwogICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICB9CiAgICAgICAgICAgIHN3aXRjaCAoZGF0YVsiY21kIl0pIHsKICAgICAgICAgICAgICAgIGNhc2UgInVwZGF0ZUxvZyI6CiAgICAgICAgICAgICAgICAgICAgU0VSVkVSWyJsb2ciXSA9IHJlc3BvbnNlWyJsb2ciXTsKICAgICAgICAgICAgICAgICAgICBpZiAoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImNvbnRlbnRfbG9nIikpIHsKICAgICAgICAgICAgICAgICAgICAgICAgc2hvd0xvZ3MoZmFsc2UpOwogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgICAgICBkZWZhdWx0OgogICAgICAgICAgICAgICAgICAgIFNFUlZFUiA9IG5ldyBPYmplY3QoKTsKICAgICAgICAgICAgICAgICAgICBTRVJWRVIgPSByZXNwb25zZTsKICAgICAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAocmVzcG9uc2UuaGFzT3duUHJvcGVydHkoIm9wZW5NZW51IikpIHsKICAgICAgICAgICAgICAgIHZhciBtZW51ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQocmVzcG9uc2VbIm9wZW5NZW51Il0pOwogICAgICAgICAgICAgICAgbWVudS5jbGljaygpOwogICAgICAgICAgICAgICAgc2hvd0VsZW1lbnQoInBvcHVwIiwgZmFsc2UpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChyZXNwb25zZS5oYXNPd25Qcm9wZXJ0eSgib3BlbkxpbmsiKSkgewogICAgICAgICAgICAgICAgd2luZG93LmxvY2F0aW9uID0gcmVzcG9uc2VbIm9wZW5MaW5rIl07CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKHJlc3BvbnNlLmhhc093blByb3BlcnR5KCJhbGVydCIpKSB7CiAgICAgICAgICAgICAgICBhbGVydChyZXNwb25zZVsiYWxlcnQiXSk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKHJlc3BvbnNlLmhhc093blByb3BlcnR5KCJyZWxvYWQiKSkgewogICAgICAgICAgICAgICAgbG9jYXRpb24ucmVsb2FkKCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKHJlc3BvbnNlLmhhc093blByb3BlcnR5KCJ3aXphcmQiKSkgewogICAgICAgICAgICAgICAgY3JlYXRlTGF5b3V0KCk7CiAgICAgICAgICAgICAgICBjb25maWd1cmF0aW9uV2l6YXJkW3Jlc3BvbnNlWyJ3aXphcmQiXV0uY3JlYXRlV2l6YXJkKCk7CiAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgIH0KICAgICAgICAgICAgY3JlYXRlTGF5b3V0KCk7CiAgICAgICAgfTsKICAgIH07CiAgICByZXR1cm4gU2VydmVyOwp9KCkpOwpmdW5jdGlvbiBnZXRDb29raWUobmFtZSkgewogICAgdmFyIHZhbHVlID0gIjsgIiArIGRvY3VtZW50LmNvb2tpZTsKICAgIHZhciBwYXJ0cyA9IHZhbHVLine truncated
|
||||
webUI["html/lang/de.json"] = "ewogICJtYWluTWVudSI6IHsKICAgICJpdGVtIjp7CiAgICAgICJwbGF5bGlzdCI6ICJXaWVkZXJnYWJlbGlzdGUiLAogICAgICAicG1zSUQiOiAiUE1TIElEIiwKICAgICAgImZpbHRlciI6ICJGaWx0ZXIiLAogICAgICAieG1sdHYiOiAiWE1MVFYiLAogICAgICAibWFwcGluZyI6ICJNYXBwaW5nIiwKICAgICAgInVzZXJzIjogIkJlbnV0emVyIiwKICAgICAgInNldHRpbmdzIjogIkVpbnN0ZWxsdW5nZW4iLAogICAgICAibG9nIjogIkxvZyIsCiAgICAgICJsb2dvdXQiOiAiQWJtZWxkZW4iCiAgICB9LAogICAgImhlYWRsaW5lIjogewogICAgICAicGxheWxpc3QiOiAiTG9rYWxlIG9kZXIgZW50ZmVybnRlIFdpZWRlcmdhYmVsaXN0ZW4iLAogICAgICAiZmlsdGVyIjogIkZpbHRlcm4gdm9uIEthbsOkbGVuIiwKICAgICAgInhtbHR2IjogIkxva2FsZSBvZGVyIGVudGZlcm50ZSBYTUxUViBEYXRlaWVuIiwKICAgICAgIm1hcHBpbmciOiAiS2Fuw6RsZSBkZXIgV2llZGVyZ2FiZWxpc3RlbiBFUEcgS2Fuw6RsZW4genVvcmRuZW4iLAogICAgICAidXNlcnMiOiAiQmVudXR6ZXJ2ZXJ3YWx0dW5nIiwKICAgICAgInNldHRpbmdzIjogIkVpbnN0ZWxsdW5nZW4iLAogICAgICAibG9nIjogIkxvZyIsCiAgICAgICJsb2dvdXQiOiAiQWJtZWxkZW4iCiAgICB9CiAgfSwKICAiY29uZmlybSI6ewogICAgInJlc3RvcmUiOiAiQWxsZSBEYXRlbiB3ZXJkZW4gbWl0IGRlbmVuIGF1cyBkZW0gQmFja3VwIGVyc2V0enQuIFdpZWRlcmhlc3RlbGx1bmcgZHVyY2hmw7xocmVuPyIKICB9LAogICJhbGVydCI6IHsKICAgICJmaWxlTG9hZGluZ0Vycm9yIjogIkRhdGVpIGtvbm50ZSBuaWNodCBnZWxhZGVuIHdlcmRlbiIsCiAgICAiaW52YWxpZENoYW5uZWxOdW1iZXIiOiAiVW5nw7xsdGlnZSBLYW5hbG51bW1lciIKICB9LAogICJidXR0b24iOnsKICAgICJiYWNrIjogIlp1csO8Y2siLAogICAgImJhY2t1cCI6ICJCYWNrdXAiLAogICAgImJ1bGtFZGl0IjogIk1hc3NlbmJlYXJiZWl0dW5nIiwKICAgICJjYW5jZWwiOiAiQWJicmVjaGVuIiwKICAgICJkZWxldGUiOiAiTMO2c2NoZW4iLAogICAgImRvbmUiOiAiRmVydGlnIiwKICAgICJsb2dpbiI6ICJBbm1lbGRlbiIsCiAgICAibmV3IjogIk5ldSIsCiAgICAibmV4dCI6ICJXZWl0ZXIiLAogICAgInJlc3RvcmUiOiAiV2llZGVyaGVyc3RlbGxlbiIsCiAgICAic2F2ZSI6ICJTcGVjaWhlcm4iLAogICAgInNlYXJjaCI6ICJTdWNoZW4iLAogICAgInVwZGF0ZSI6ICJVcGRhdGUiLAogICAgImNyYWV0ZUFjY291bnQiOiAiQWNjb3VudCBlcnN0ZWxsZW4iLAogICAgInJlc2V0bG9ncyI6ICJMb2dzIGzDtnNjaGVuIiwKICAgICJ1cGxvYWRMb2dvIjogIkxvZ28gaG9jaGxhZGVuIgogIH0sCiAgImZpbHRlciI6IHsKICAgICJ0YWJsZSI6IHsKICAgICAgIm5hbWUiOiAiRmlsdGVyIE5hbWUiLAogICAgICAidHlwZSI6ICJGaWx0ZXIgVHlwIiwKICAgICAgImZpbHRlciI6ICJGaWx0ZXIiCiAgICB9LAogICAgImN1c3RvbSI6ICJCZW51dHplcmRlZmluaWVydCIsCiAgICAiZ3JvdXAiOiAiR3J1cHBlIiwKICAgICJuYW1lIjogewogICAgICAidGl0bGUiOiAiRmlsdGVyIE5hbWUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiRmlsdGVyIE5hbWUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJkZXNjcmlwdGlvbiI6IHsKICAgICAgInRpdGxlIjogIkJlc2NocmVpYnVuZyIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJCZXNjaHJlaWJ1bmciLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJ0eXBlIjogewogICAgICAidGl0bGUiOiAiVHlwIiwKICAgICAgImdyb3VwVGl0bGUiOiAiR3JvdXBlbnRpdGVsIiwKICAgICAgImN1c3RvbUZpbHRlciI6ICJCZW51dHplcmRlZmluaWVydGVyIEZpbHRlciIKICAgIH0sCiAgICAiY2FzZVNlbnNpdGl2ZSI6IHsKICAgICAgInRpdGxlIjogIkdyb8OfLSAvIEtsZWluc2NocmVpYnVuZyBiZWFjaHRlbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWx0ZXJSdWxlIjogewogICAgICAidGl0bGUiOiAiRmlsdGVycmVnZWwiLAogICAgICAicGxhY2Vob2xkZXIiOiAiU3BvcnQge0hEfSAhe0VTLElUfSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgImZpbHRlckdyb3VwIjogewogICAgICAidGl0bGUiOiAiR3JvdXBlbnRpdGVsIiwKICAgICAgInBsYWNlaG9sZGVyIjogIiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJHcnVwcGUgYXVzIGRlciBNM1UgYXVzd8OkaGxlbi4gKFrDpGhsZXIpPGJyPsOkbmRlcnVuZyBkZXMgR3J1cHBlbnRpdGVscyBpbiBkZXIgTTNVIG1hY2h0IGRlbiBGaWx0ZXIgdW5nw7xsdGlnLiIKICAgIH0sCiAgICAiaW5jbHVkZSI6IHsKICAgICAgInRpdGxlIjogIk11c3MgYmVpbmhhbHRlbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJGSEQsVUhEIiwKICAgICAgImRlc2NyaXB0aW9uIjogIkthbmFsbmFtZSBtdXNzIGRpZXNlIFfDtnJ0ZXIgZW50aGFsdGVuLjxicj4oS29tbWFnZXRyZW5udCkgS29tbWEgYmVkZXV0ZXQgb2RlciIKICAgIH0sCiAgICAiZXhjbHVkZSI6IHsKICAgICAgInRpdGxlIjogIkRhcmYgbmljaHQgYmVpbmhhbHRlbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJFUyxJVCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJLYW5hbG5hbWUgZGFyZiBkaWVzZSBXw7ZydGVyIG5pY2h0IGVudGhhbHRlbi48YnI+KEtvbW1hZ2V0cmVubnQpS29tbWEgYmVkZXV0ZXQgb2RlciIKICAgIH0KCiAgfSwKICAicGxheWxpc3QiOiB7CiAgICAidGFibGUiOiB7CiAgICAgICJwbGF5bGlzdCI6ICJXaWVkZXJnYWJlbGlzdGUiLAogICAgICAidHVuZXIiOiAiVHVuZXIiLAogICAgICAibGFzdFVwZGF0ZSI6ICJMZXR6dGUgQWt0dWFsaXNpZXJ1bmciLAogICAgICAiYXZhaWxhYmlsaXR5IjogIlZlcmbDvGdiYXJrZWl0IiwKICAgICAgInR5cGUiOiAiVHlwIiwKICAgICAgInN0cmVhbXMiOiAiU3RyZWFtcyIsCiAgICAgICJncm91cFRpdGxlIjogIkdyb3VwZW50aXRlbCIsCiAgICAgICJ0dmdJRCI6ICJ0dmctaWQiLAogICAgICAidW5pcXVlSUQiOiAiRWluZGV1dGlnZSBJRCIKICAgIH0sCiAgICAicGxheWxpc3RUeXBlIjogewogICAgICAidGl0bGUiOiAiV2llZGVyZ2FiZWxpc3RlbiBUeXAiLAogICAgICAicGxhY2Vob2xkZXIiOiAiIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAidHlwZSI6IHsKICAgICAgInRpdGxlIjogIlR5cCIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJuYW1lIjogewogICAgICAidGl0bGUiOiAiTmFtZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJOYW1lIGRlciBXaWVkZXJnYWJlbGlzdGUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJkZXNjcmlwdGlvbiI6IHsKICAgICAgInRpdGxlIjogIkJlc2NocmVpYnVuZyIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJCZXNjaHJlaWJ1bmciLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWxlTTNVIjogewogICAgICAidGl0bGUiOiAiTTNVIERhdGVpIiwKICAgICAgInBsYWNlaG9sZGVyIjogIkRhdGVpcGZhZCBvZGVyIFVSTCBkZXIgTTNVIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAiZmlsZUhESFIiOiB7CiAgICAgICJ0aXRsZSI6ICJIREhvbWVSdW4Line truncated
|
||||
webUI["html/video/stream-limit.ts"] = "R0AREABC8CUAAcEAAP8B/wAB/IAUSBIBBkZGbXBlZwlTZXJ2aWNlMDF3fEPK//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9HQAAQAACwDQABwQAAAAHwACqxBLL//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////0dQABAAArASAAHBAADhAPAAG+EA8AAVvU1W////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////R0EAMAdQAAB7DH4AAAAB4AAAgMAKMQAJEKERAAfYYQAAAAEJ8AAAAAFnZAAorNlAeAIn5cBEAAADAAQAAAMAyDxgxlgAAAABaOvjyyLAAAABBgX//6rcRem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTUyIHIyODU0IGU5YTU5MDMgLSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDE3IC0gaHR0cDovL3dHAQARd3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZUcBABJhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MyBiX3B5cmFtaWQ9MiBiX2FkRwEAE2FwdD0xIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MSBvcGVuX2dvcD0wIHdlaWdodHA9MiBrZXlpbnQ9MjUwIGtleWludF9taW49MjUgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD00MCByYz1jcmYgbWJ0cmVlPTEgY3JmPTIzLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD1HAQAUNCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAABZYiEADv//vdOvwKbVMIqA5JXCvbKpCZZuVJrAfKmAAADAAADAAADAAADAAADAq7aiiFafTJvTQAAAwAAAwAAF1AAAAVUAAADAiYAAAMBNwAAAwDUAAADAMkAAAMA4gAAAwD+AAADATIAAAMCGgAABAQAAAYoAAAOwAAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAA0cBABUAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAA2F55UwH3zADgCn7Xsg5urkZA62Mq62dWKwprJak9U0cjT1Yh9J7WRISuxmagZEf3SG5hJ5glLK+m1DQRdCFD2mN6YZGjkCTKwYyDxKlNHkaW8fuatJR0sAtCoVfOjRr2/NS+bw85zMQI7VFu7+JhD/Go1Q5RgMv5bIq0gpxJKRnF31ojI62CwCRiJ1dHa6nIwcU00zURwEAFhyu6/UdOQ6gXR/Fn3SkmUwkQsjmQF0XEAAAAwAAQkN8C3guxx7g23xd3I1uQvL9Dh+C+fpNfOmVma2q7DiUF3VD8o51I57iYniPq7fe7f2YnPwRwynt2WAsZSjTMgHnGbk5zsyy+rXIMy1p3notBIJkpSBHSuogn9FDL99PItYMLSYCXzbB1ngJo9jnaTgI0b904RmkSjuZtpKyt0ofkYfxNRrztwDGURi3JRPkpuouCWX8KWcxn8NHAQAXWrAsVbXQ0f+vby6QN9oHh0jRsBw7+79AAAsCUlIAAAMAAIGTVQt7SJOBNtpTiAsB1/AzhYzJ8lMVgRENWBT0VBkiPo9+OyjWfg6dlrISORs+GKnEy5aUKA1M9uXDtedogUi7S7uVd54Gj1Qb5NTw4Jix5w1Ejl/de0xvxERyv4b3rmZdsU72FOcfnbK1K8lG4wnxVzYPGiElBGSrNsWNXskCT++8dTbAQv4ejltKQDN2z2xpw5g1CkcBABjFGRM3p9Rp9BGLQ/gaCYSE37IZAAADAFsvgUK7rSLf56T0ZOVulX2oYo74ASEKbonCVfRcGAke05L38QhPtmJ8xRmebFR5/801CbQAWvnAOc2AWgY1coPTdBTZIfsBwU94Gu+qjaoR4dfBX9u8RZXmrbLBCqJ8wTgvf9kJu7/27gcTx8SsJfq40cjhNaRNiof2e8CF4imWObVdvBDpPRxSiR1tsORfA0r+gqjURLyvIwDsE+O9NkeiRwEAGZMG5kW8doFnKjj4QW/OIpFfzF+53Eb4LrnRmrkHqYA/RnqVvBWNpUSFdATxWKzy0MBYRpY4IoUCpCeWwgQ/9Ny0Yi1iQ++wmh1ay/SnYR5B8k+FpDYbWDODuSuCMmrTDKXjicAStFseuB4peB1Ufu7fitj0L0uZzCgGY5zkzbBp3bfXJQEGCnSwEtW+TY50gbhenVCesUfJKjvG6PshP5ScKPdVkZltrS4wx1CntFR+srvN3MjP6p1HAQAaM8+T4dTA6o28g/ki3+J9b5WD8oY+fVgTsb1jfV2GWWd2Hh8JK00uloyX94k2QHGzhbuiS1YKiEdCLz9Pt3RuxuTNm9XSlLwQvyRvfY6sKD8KIinvSq3XP//Kx5Ni82QOXvrn5eEmFlTGuT36Yax5OR4BI3UhdWSGGa0YoqeD7zcvWv64AAANbhz0PNupTjTGu6vxaQq6PcgarV8F7NT0zHSQCZ8KuEFdideJmx4581/BCMYXuJt5cEcBABufpZ3Gzdy5JLw0J2eg1hz70uf/nD82aqNxy/HUvEd6+w5woyIYb7nzl1eo/+ACzPidfiSKydCNxKh9e7tKjNNTJYRX+1f5A398n+2wKJPX7dRmPk4C0j/auaYtAMF+Ft5g6ZqxQuA071fEmj4uiYMR7F+JbZ7CjA98DObmNy/ZWoJQvMrvR3ep5XAmNFkqs1Ma20JWsiikVsr/jblTK0PBVnLul36knafsXUW38/19lMQo0HUcMvrmRwEAHI2OaNsvRTodm5RGcpDEKfXIqXsLfH53r0F3K+sfRvbk3u2ilZd/c5zH95TBbceTvFEt5mD6oPEhxHX3gId6ZBsNNUKUnWDxoX7MaYE38/ELMyIVzRxDCIEkf+q7aY6tcZEMrPBa352hxE6SJ+2y/zlDc8X4jSYerSPRgLBiUm/TSHdm/sHP7d/kl2fmIUNKBbeaghigfwmfDnaZ3wPkRi3oXIp3YypbLYpeup62X805+oYMAgynQd1HAQAdHLgV5nC45givqz8G4AAB0pcgXQpZhwRtJ0wE/aFEPMXugXV9NeZnpzJeXaijTBtaFonT4e+hZZXjLHhxTugKb085RNFOoT0GAB6b2twqc++PKcb796IKMKtNpGaUWjoFvBDGh4HrRguoZWCbLy2RFxUX8SmNUb7qfDXwIi4nY4i89/TcrSc0q64NPWA3SPmQ5D5/ZFbRzBtFuMVVmbEF0cxv9RoqLyW/VGkp+bBAmoMhAw82eI9VskcBAB7bidwIeUDbJvHb2Z8Ep/avJccAh157ZUytHDDW9DhmdbdBzT1TpsJFsFWe8FOQfaNpJVfj2nGqShY4+DhxRYxOpzXw2VF7hQpp6PqFUtyi4ib3VHETkSuHz+2uom/bMRbhw+hQ05xdXOHcFVpih+k58QKrhN+txmOmV3ULdeiVLUplf35jQJTUIZltXjTCZhljRc0sdqOZHDZjy58Sc6hmq8D6lztRR5jN6ifzzKdZbHOpMvK28A4ZRwEAHwyCdGDG4LksEyABPYPnzVuaVJHpUiKgtb3006VleBPo2EZenL5/vyGrjLMas0C6R0iyXZjr0xqvCfaFfxVO/+DQUMKaj0XLNNIrLqXxwzxzy2ENSUjNNySzfvDkpjuJVywa0uwsg60r6EURKgU15gAtbm3Kx4tjb3pRMM8SZHnsN075ztW98+aTrdbRryY9XR74p76dkImiqWIVRQAAAwAAerbiggDgwfd5iHYwqlkEXOqApTuXEhFHAQAQ1PUugOrA4HsFQsLzVf2RjOkhWvlC/kwNqqDXkwlUM6egLoIdgXAyBFgSXuM59iSh6DSi4LwFa/FhyGMrxPV0UF8QD9dGgSeXBUvExQHAGOHz98Mbwm3lBdIzxkzeGLDhi0wZ5SurQPpWomKoDW7t0a+GpQkmCEgVO7iBc/JrOFIt8BBRW1E3EOMv6Wpc5BLine truncated
|
||||
webUI["html/create-first-user.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvbmV0d29ya190cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvYXV0aGVudGljYXRpb25fdHMuanMiPjwvc2NyaXB0PgogIDwvaGVhZD4KCiAgICA8Ym9keT4KICAgICAgICAgIAogICAgICA8ZGl2IGlkPSJoZWFkZXIiIGNsYXNzPSJpbWdDZW50ZXIiPjwvZGl2PgoKICAgICAgPGRpdiBpZD0iYm94Ij4KCiAgICAgICAgPGRpdiBpZD0iaGVhZGxpbmUiPgogICAgICAgICAgPGgxIGlkPSJoZWFkLXRleHQiIGNsYXNzPSJjZW50ZXIiPnt7LmFjY291bnQuaGVhZGxpbmV9fTwvaDE+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxwIGlkPSJlcnIiIGNsYXNzPSJlcnJvck1zZyBjZW50ZXIiPjwvcD4KCiAgICAgICAgPGRpdiBpZD0iY29udGVudCI+CgogICAgICAgICAgICA8Zm9ybSBpZD0iYXV0aGVudGljYXRpb24iIGFjdGlvbj0iL3dlYi8iIG1ldGhvZD0icG9zdCI+CgogICAgICAgICAgICAgIDxoNT57ey5hY2NvdW50LnVzZXJuYW1lLnRpdGxlfX06PC9oNT4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9InVzZXJuYW1lIiB0eXBlPSJ0ZXh0IiBuYW1lPSJ1c2VybmFtZSIgcGxhY2Vob2xkZXI9IlVzZXJuYW1lIiB2YWx1ZT0iIj4KICAgICAgICAgICAgICA8aDU+e3suYWNjb3VudC5wYXNzd29yZC50aXRsZX19OjwvaDU+CiAgICAgICAgICAgICAgPGlucHV0IGlkPSJwYXNzd29yZCIgdHlwZT0icGFzc3dvcmQiIG5hbWU9InBhc3N3b3JkIiBwbGFjZWhvbGRlcj0iUGFzc3dvcmQiIHZhbHVlPSIiPgogICAgICAgICAgICAgIDxoNT57ey5hY2NvdW50LmNvbmZpcm0udGl0bGV9fTo8L2g1PgogICAgICAgICAgICAgIDxpbnB1dCBpZD0iY29uZmlybSIgIHR5cGU9InBhc3N3b3JkIiBuYW1lPSJjb25maXJtIiAgcGxhY2Vob2xkZXI9IkNvbmZpcm0iIHZhbHVlPSIiPgogICAgICAgICAgICAgIAogICAgICAgICAgICA8L2Zvcm0+CgogICAgICAgIDwvZGl2PgogICAgICAgIAogICAgICAgIDxkaXYgaWQ9ImJveC1mb290ZXIiPgogICAgICAgICAgPGlucHV0IGlkPSJzdWJtaXQiIGNsYXNzPSIiIHR5cGU9ImJ1dHRvbiIgdmFsdWU9Int7LmJ1dHRvbi5jcmFldGVBY2NvdW50fX0iIG9uY2xpY2s9ImphdmFzY3JpcHQ6IGxvZ2luKCk7Ij4KICAgICAgICA8L2Rpdj4KICAgICAgCiAgICAgICAgCiAgICAgIDwvZGl2PgogICAgPC9ib2R5Pgo8L2h0bWw+"
|
||||
webUI["html/css/screen.css"] = "bmF2IGltZyB7CiAgZGlzcGxheTogYmxvY2s7CiAgbWF4LWhlaWdodDogMjBweDsKICBtYXgtd2lkdGg6IDIwcHg7CiAgZmxvYXQ6IGxlZnQ7Cn0KCm5hdiBwIHsKICB0ZXh0LWFsaWduOiBsZWZ0OwogIHBhZGRpbmc6IDBweCAzMHB4Owp9CgojbGF5b3V0IHsKICBkaXNwbGF5OiBibG9jazsKICBoZWlnaHQ6IDEwMCU7Cn0KCgoubGF5b3V0LWxlZnQgewogIGRpc3BsYXk6IGJsb2NrOwogIG1pbi13aWR0aDogMTUwcHg7CiAgbWF4LXdpZHRoOiAyMCU7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICBoZWlnaHQ6IGluaGVyaXQ7CiAgZmxvYXQ6IGxlZnQ7Cn0KCi5sYXlvdXQtcmlnaHQgewogIGRpc3BsYXk6IGJsb2NrOwogIGJhY2tncm91bmQtY29sb3I6ICM0NDQ7Cn0KCiNtZW51LXdyYXBwZXIgewogIGhlaWdodDogMTAwJTsKfQoKCiNsb2dvIHsKICBkaXNwbGF5OiBibG9jazsKICBtaW4td2lkdGg6IDE4MHB4OwogIHdpZHRoOiAxMDAlOwogIGhlaWdodDogMTAwcHg7CiAgYmFja2dyb3VuZDogdXJsKCIuLi9pbWcvbG9nb193XzYwMHgyMDAucG5nIik7CiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDsKICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7IAogIGJhY2tncm91bmQtc2l6ZTogMTAwJTsKfQoKCiNwYWdlIHsKICBtYXgtd2lkdGg6IDk1MHB4OwogIG1hcmdpbjogYXV0bzsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjNDQ0OwoKICAvKgogIGhlaWdodDogLXdlYmtpdC1jYWxjKDEwMCUgLSAxMzBweCk7CiAgaGVpZ2h0OiAtbW96LWNhbGMoMTAwJSAtIDEzMHB4KTsKICBoZWlnaHQ6IGNhbGMoMTAwJSAtIDEzMHB4KTsKICAqLwogIAogIG1pbi1oZWlnaHQ6IC13ZWJraXQtY2FsYygxMDAlIC0gMTIwcHgpOwogIG1pbi1oZWlnaHQ6IC1tb3otY2FsYygxMDAlIC0gMTIwcHgpOwogIG1pbi1oZWlnaHQ6IGNhbGMoMTAwJSAtIDEyMHB4KTsKICAKICAKICBib3gtc2hhZG93OiAwcHggNXB4IDVweCAjMjIyOwogIAp9CgojdWlTZXR0aW5nIHsKICBmbG9hdDogcmlnaHQ7CiAgbWFyZ2luLXJpZ2h0OiAyNXB4Owp9CgojYm94IGlucHV0W3R5cGU9dGV4dF0sICNib3ggaW5wdXRbdHlwZT1wYXNzd29yZF0gewogIHdpZHRoOiAtd2Via2l0LWNhbGMoMTAwJSAtIDIwcHgpOwogIHdpZHRoOiAtbW96LWNhbGMoMTAwJSAtIDIwcHgpOwogIHdpZHRoOiBjYWxjKDEwMCUgLSAyMHB4KTsKfQoKI2JveCBpbnB1dFt0eXBlPXN1Ym1pdF17CiAgbWFyZ2luOiA1MHB4IGF1dG87Cn0KCiNzZXR0aW5ncyB7CiAgZGlzcGxheTogYmxvY2s7CiAgcGFkZGluZzogMTBweCAxMHB4Owp9Cgojc2V0dGluZ3MgaDUgewogIG1hcmdpbjogNTBweCAwcHggMTBweCAwcHg7Cn0KCiNjb250ZW50LWludGVyYWN0aW9uIC5zZWFyY2ggewogIHdpZHRoOiAyMDBweDsKICBib3JkZXI6IDFweCBzb2xpZCAjMDAwOwogIHBhZGRpbmc6IDlweDsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzOwogIG1hcmdpbjogMTBweDsKICBmbG9hdDogcmlnaHQ7CiAgYm9yZGVyLXJhZGl1czogM3B4OwoKfQoKI215U3RyZWFtcyB7CiAgcG9zaXRpb246IGZpeGVkOwogIGJvdHRvbTogMHB4OwogIGJhY2tncm91bmQtY29sb3I6ICMxMTE7CiAgd2lkdGg6IDEwMCU7CiAgbWF4LXdpZHRoOiA5NTBweDsKCiAgLyoKICBtYXgtaGVpZ2h0OiAxMDBweDsKICAqLwogIG1hcmdpbi1ib3R0b206IDBweDsKfQoKI215U3RyZWFtcyBpbWcgewogIHdpZHRoOiA0JTsKICBwYWRkaW5nOiAycHggNXB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBmbG9hdDogcmlnaHQ7Cn0KCiNzZXR0aW5ncy1mb290ZXIgewoKfQoKCi8qIFdpemFyZCovCiNib3ggewogIGJhY2tncm91bmQtY29sb3I6ICM0NDQ7CiAgbWluLWhlaWdodDogNDAwcHg7CiAgCiAgZGlzcGxheTogZmxleDsKICBmbGV4LWRpcmVjdGlvbjogY29sdW1uOwogIGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjsKfQoKI2JveCBwewogIHBhZGRpbmc6IDEwcHggMHB4Owp9CgojYm94LWZvb3RlciB7CiAgbWFyZ2luLXRvcDogYXV0bzsKfQoKI2JveC1mb290ZXIgewogIG1hcmdpbjogYXV0bzsKICBwYWRkaW5nOiAxMHB4Owp9CgojaGVhZGxpbmUgewogIGJhY2tncm91bmQtY29sb3I6ICMyMjI7CiAgYm9yZGVyLWJvdHRvbTogc29saWQgMnB4ICMyMjI7CiAgdHJhbnNpdGlvbjogYWxsIDAuNXM7CiAgcGFkZGluZzogMTBweCAwcHg7CiAgZGlzcGxheTogYmxvY2s7Cn0KCiNjb250ZW50IHsKICBkaXNwbGF5OiBibG9jazsKICBvdmVyZmxvdzogYXV0bzsKICBwYWRkaW5nOiAxMHB4Owp9CgovKiAtLS0gKi8KCgojY2xpZW50SW5mbywgI2FjdGl2ZVN0cmVhbXMsICNpbmFjdGl2ZVN0cmVhbXMgewogIGZvbnQtZmFtaWx5OiBtb25vc3BhY2U7CiAgZGlzcGxheTogYmxvY2s7CiAgZm9udC1zaXplOiA5cHg7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICBjb2xvcjogIzAwRTZGRjsKICBib3JkZXItYm90dG9tOiBzb2xpZCAwcHg7OwogIHBhZGRpbmc6IDBweDsKICBsZXR0ZXItc3BhY2luZzogMXB4OwogIG92ZXJmbG93LXg6IGhpZGRlbjsKICBib3JkZXItc3BhY2luZzogNHB4IDRweDsKICBib3JkZXItYm90dG9tOiBzb2xpZCAxcHggIzQ0NDsKfQoKI215U3RyZWFtc0JveCB7CiAgcG9zaXRpb246IHJlbGF0aXZlOwogIHBhZGRpbmc6IDBweDsKICAvKmhlaWdodDogMTAwcHg7Ki8KICBtYXgtaGVpZ2h0OiAxNTBweDsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTExOwogIGNvbG9yOiB3aGl0ZTsKICBkaXNwbGF5OmZsZXg7CiAganVzdGlmeS1jb250ZW50OmNlbnRlcjsKICBhbGlnbi1pdGVtczpjZW50ZXI7Cn0KCiNvcGVuU3RyZWFtcyB7CiAgd2lkdGg6IDIwcHg7CiAgaGVpZ2h0OiAyMHB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBmbG9hdDogcmlnaHQ7CiAgcG9zaXRpb246IGFic29sdXRlOwogIHJpZ2h0OiAwcHg7CiAgYm90dG9tOiAwcHg7CiAgYmFja2dyb3VuZDogdXJsKCIuLi9pbWcvdG91Y2gucG5nIik7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICAKICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBib3R0b20gcmlnaHQ7ICAKfQoKI2FsbFN0cmVhbXMgewogIHdpZHRoOiAxMDAlOwogIGhlaWdodDogMTAwJTsKICBwYWRkaW5nOiAycHg7ICAgICAKfQoKI2FjdGl2ZVN0cmVhbXMsICNpbmFjdGl2ZVN0cmVhbXMgewogIG92ZXJmbG93LXk6IHNjcm9sbDsKICB3aWR0aDogNTAlOwogIG1heC1oZWlnaHQ6IDEwMHB4OwogIGZsb2F0OiBsZWZ0Owp9CgojYWN0aXZlU3RyZWFtcyAudGRLZXksICNpbmFjdGl2ZVN0cmVhbXMgLnRkS2V5IHsKICB3aWR0aDogNzVweDsKfQoKCgoKI2luYWN0aXZlU3RyZWFtcyAudGRLZXkgewogIGNvbG9yOiByZWQ7Cn0KCiNjbGllbnRJbmZvIC50ZFZhbCwgI2xvZ0luZm8gLnRkVmFsLCAjYWN0aXZlU3RyZWFtcyAudGRWYWwsICNpbmFjdGl2ZVN0cmVhbXMgLnRkVmFsLCAjbWFwcGluZ0luZm8gLnRkVmFsewogIGNvbG9yOiAjYWFhOwogIHdoaXRlLXNwYWNlOiBpbmhlcml0Owp9CgojYm94LXdyYXBwZXIgewogIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICB3aWR0aDogMTAwJTsKICAKICBvdmVyZmxvdy15OiBzY3JvbGw7Cn0KCiNjb250ZW50X3RhYmxlLCAjbWFwcGluZy1kZXRhaWwtdGFibGUsICNjb250ZW50X3RhYmxlIHsKICBkaXNwbGF5OiB0YWJsZTsKICAKICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOwogIG92ZXJmbG93LXk6IHNjcm9sbDsKICB3aWR0aDogMTAwJTsKfQoKCiNjb250ZW50X3RhYmxlIC5jb250ZW50X3RhYLine truncated
|
||||
webUI["html/img/logout.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0xMC0xM1QxMToxMDoxODwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cg27QeEAAANQSURBVGgF7Zk9aBRBGIZzSRSN8YdoCpEkghCCqK1o4g9IsBCCNmIniCAiWAhqI8G02qiIoqCCWggSEcFKEFPYpVAU8QdFUyiKRpRokBjP55OdZDLZvZnZ7N3Ngh887M7sN/O9797t7c5eTc3/8D8DxWKxGS5Ai//oAEYgvB4OwleQaAtAlp8ERG+Bp6Jei/wYQfRy6NfE67vhG0FtA/TBqK7c2A/bCGJ3wpAhOq4ZphGUroEHcYoT+sIygsgmOAu/EwQndYdhBHV1sB8+Jym19Lf6/faVIRuBG+GRRajtcPWMoKwFbtgUOh6vvBGEzYFj8MNRpEtaZY2gaAe8cVHmmXOc/K3QXIZv/+SUFFgJ96AS8ZwiZ6Ab6iZVzGCPiRbCKRiDasQHip6EdD/PDKyFvfAJQgg5kVfB/VoieT0MQojxE1G9UG/7otWS0A7pPkrb7DM/Ppcp+mAAM/bFGEmL4RL8gVDjI8JWOZ0bEjvhSahO0CXXsLMZWZYegREIMV4jaoHTJyNJJLfC7RCdoOm6sxGVyKAeeBegoU6l0XmLiXlwAqp1o4w7j3d1AwW9YdtnNrnQzkOXLddyfAPHv8Ey6ABpd8N88In2QqHwymfARC5mCrAH0i6qGDr9jk3fbNgFz8A1DkwIS7tDpSVwGdLcexIfPZhPHpf2wS+wRX9a/dPGUakLzBdwNgGJRlQBJtgMw5aJ3qr8TLYUmwVHwXXxZTUiwphvHYxDUnzPxIA5CdXa4E5SVa3fyUhkRtYqpSKbNYxpJiq+ncpDJar7GGlknvcl5mqM05BZH4Xl3iOLpLh7j7OR6MScSzAykplg20QIWA0PDSG+RuTdQVy8sNXP9DgK5N4jK9AvkRpfI4sYF/c2836mQl0nQ4zce66AfZFkTMqYl2DGaSMt/CYOHpsuaPco5bLUzUuMGkLHaQ+ovjwZUZrVdpAHRnnw/Bd5MrJCiY62t4x2+E2uBfkfRg95TdSkK8/LJ7JWF83+Nb5Ww0Zf+E3OviwXVMgT8dLwVRsKEd0A+uP8biMlH01MHAYVF/Oh2lCJenkDqpbUN9nPyzU91QnCRbyEPEXn1sQhxMv7tG1T7cW3rK/r44eVtxfxm6gwBh38zJqPJuUtXu3Z/wLwuBaBLgMkKwAAAABJRU5ErkJggg=="
|
||||
webUI["html/img/mapping.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0wOC0wMlQxMjowODo5NzwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CpRxQsEAAAJLSURBVGgF7VoxTgMxEMwBHWlAaeABVCEtBcoD+EBewCdCyQsQtLyAPCBvCBSRIBUPCF1ogqBJjlkrezGr6HTrO98lhy1Z9trr2VmvnbMNjUadUhzHLeQB8hw5LS3QOUbubvIf7X3kKXIZibgS51bCZdWgMT6D8nECgArkngagQN0B8dhbkblalVmLIyheCGUthhjuLBq77MihA0xTjHHBEBBOorHLjjghbNOg4Mg2RYO4hIiEiHiaAV5aSwf8hRgjZdHtTTTc2ZFXpZkY+hMx5k3IZYlr7jgudJHp2JElLaF0K1mirYn8nAWgQB3ibM59ERNCA52d6Nghv9isQiUtn0kURe92I9eBcYA6YZxym8dyDuwRuMw82gjQYQbsPdLDdNCROO0US3uEfp3usTZpjf5J2CNtNFwjl7FHvmBnCB5PCQkQoJudJtGvE23sJEFuI39rQArS7dskXK6nlwkAKiB1VxAxLcyUePAH8cQmlbEul4+UM8LkVjPc2ZHcaFUDBEeqjoC0HyIiZ6RqOUSk6ghI+xyRD9mRQTYfIktPylaX16rhzo48KE29QH8kxjxC/hFtZYiGe70OjWVMW7Dx32bA3iO7//iAC8DOPweZFQhH6O+CmkRvW2f28oV8owEoUHdMPPg70rFJZajTkqT7uZ3ObaHEuuHOjnCpsb8vlKUsur2JhruLA94Y5QEOjuSZPR9jQ0R8zGoezNpFhN5RtUm+/bpgaG1u0jd2OSLDTRopbZ/okxcrLUYKvKprbRfHhXr8m5PK/y1V/gWRKLfiNSmxEAAAAABJRU5ErkJggg=="
|
||||
webUI["html/index.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPCEtLS0KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wIiAvPiAKICAgIC0tPgogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL25ldHdvcmtfdHMuanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL21lbnVfdHMuanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL3NldHRpbmdzX3RzLmpzIj48L3NjcmlwdD4KICAgIDxzY3JpcHQgbGFuZ3VhZ2U9ImphdmFzY3JpcHQiIHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSJqcy9sb2dzX3RzLmpzIj48L3NjcmlwdD4KICAgIDxzY3JpcHQgbGFuZ3VhZ2U9ImphdmFzY3JpcHQiIHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSJqcy9iYXNlX3RzLmpzIj48L3NjcmlwdD4KCiAgPC9oZWFkPgoKICAgIDxib2R5IG9ubG9hZD0iamF2YXNjcmlwdDogUGFnZVJlYWR5KCk7Ij4KCiAgICAgIDxkaXYgaWQ9ImxvYWRpbmciIGNsYXNzPSJub25lIj4KICAgICAgICA8ZGl2IGNsYXNzPSJsb2FkZXIiPjwvZGl2PgogICAgICA8L2Rpdj4KCiAgICAgIDxkaXYgaWQ9InBvcHVwIiBjbGFzcz0ibm9uZSI+CiAgICAgICAgPGRpdiBpZD0icG9wdXAtY3VzdG9tIj48L2Rpdj4KICAgICAgPC9kaXY+CgogICAgICA8ZGl2IGlkPSJsYXlvdXQiPgoKICAgICAgICA8IS0tCiAgICAgICAgPGRpdiBpZD0ibm90aWZpY2F0aW9uIj4KICAgICAgICAgIDxkaXYgY2xhc3M9ImVsZW1lbnQiPgogICAgICAgICAgICA8aDU+WEVQRzwvaDU+CiAgICAgICAgICAgIDxwcmU+MTEuMDUuMjAxOSAtIDIwOjIxPC9wcmU+CiAgICAgICAgICAgIDxocj4KICAgICAgICAgICAgPHA+SGFsbG8gZGFzIGlzdCBlaW4gVGVzdC4gVW5kIG5vY2ggbWVociBUZXh0LjwvcD4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICAtLT4KCiAgICAgICAgPGRpdiBpZD0ibWVudS13cmFwcGVyIiBjbGFzcz0ibGF5b3V0LWxlZnQiPgogICAgICAgICAgPGRpdiBpZD0gImJyYW5jaCI+PC9kaXY+CiAgICAgICAgICA8ZGl2IGlkPSJsb2dvIj48L2Rpdj4KICAgICAgICAgIDxuYXYgaWQ9Im1haW4tbWVudSI+PC9uYXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9ImxheW91dC1yaWdodCI+CgogICAgICAgICAgPHRhYmxlIGlkPSJjbGllbnRJbmZvIiBjbGFzcz0iIj4KCiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij54VGVWZTo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0idmVyc2lvbiIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkiPk9TOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJvcyIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkgcGhvbmUiPkRWUiBJUDo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0iRFZSIiBjbGFzcz0idGRWYWwgcGhvbmUiPiZuYnNwOzwvdGQ+CiAgICAgICAgICAgIDwvdHI+CgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+VVVJRDo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0idXVpZCIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkiPkFyY2g6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9ImFyY2giIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5IHBob25lIj5NM1UgVVJMOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJtM3UtdXJsIiBjbGFzcz0idGRWYWwgcGhvbmUiPiZuYnNwOzwvdGQ+CiAgICAgICAgICAgIDwvdHI+CgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+QXZhaWxhYmxlIFN0cmVhbXM6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9InN0cmVhbXMiIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5FUEcgU291cmNlOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJlcGdTb3VyY2UiIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5IHBob25lIj5YRVBHIFVSTDo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0ieGVwZy11cmwiIGNsYXNzPSJ0ZFZhbCBwaG9uZSI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5YRVBHIENoYW5uZWxzOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJ4ZXBnIiBjbGFzcz0idGRWYWwiPiZuYnNwOzwvdGQ+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+RXJyb3JzOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJlcnJvcnMiIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5XYXJuaW5nczo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0id2FybmluZ3MiIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAKICAgICAgICAgIDxkaXYgaWQ9Im15U3RyZWFtc0JveCIgY2xhc3M9Im5vdFZpc2libGUiPgoKICAgICAgICAgICAgPGRpdiBpZD0iYWxsU3RyZWFtcyI+CiAgICAgICAgICAgICAgPHRhYmxlIGlkPSJhY3RpdmVTdHJlYW1zIj48L3RhYmxlPgogICAgICAgICAgICAgIDx0YWJsZSBpZD0iaW5hY3RpdmVTdHJlYW1zIj48L3RhYmxlPgogICAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICA8L2Rpdj4KICAgICAgICAgIAogICAgICAgICAgPGRpdiBpZD0iY29udGVudCIgY2xhc3M9IiI+PC9kaXY+CiAgICAgICAgICAgIAogICAgICAgIDwvZGl2PgoKICAgICAgPC9kaXY+CiAgICAgIAogICAgPC9ib2R5PgogICAgCjwvaHRtbD4="
|
||||
webUI["html/js/classes_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBNYWluTWVudSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIE1haW5NZW51KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJtYWluLW1lbnUiOwogICAgICAgIHRoaXMuSFRNTFRhZyA9ICJMSSI7CiAgICB9CiAgICBNYWluTWVudS5wcm90b3R5cGUuY3JlYXRlID0gZnVuY3Rpb24gKCkgewogICAgICAgIGNvbnNvbGUubG9nKHRoaXMuRG9jdW1lbnRJRCk7CiAgICB9OwogICAgcmV0dXJuIE1haW5NZW51Owp9KCkpOwp2YXIgTWFpbk1lbnVJdGVtID0gLyoqIEBjbGFzcyAqLyAoZnVuY3Rpb24gKF9zdXBlcikgewogICAgX19leHRlbmRzKE1haW5NZW51SXRlbSwgX3N1cGVyKTsKICAgIGZ1bmN0aW9uIE1haW5NZW51SXRlbSgpIHsKICAgICAgICByZXR1cm4gX3N1cGVyICE9PSBudWxsICYmIF9zdXBlci5hcHBseSh0aGlzLCBhcmd1bWVudHMpIHx8IHRoaXM7CiAgICB9CiAgICBNYWluTWVudUl0ZW0ucHJvdG90eXBlLmNyZWF0ZTIgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KHRoaXMuSFRNTFRhZyk7CiAgICAgICAgZWxlbWVudC5pbm5lclRleHQgPSB0aGlzLlZhbHVlOwogICAgICAgIGNvbnNvbGUubG9nKGVsZW1lbnQpOwogICAgfTsKICAgIHJldHVybiBNYWluTWVudUl0ZW07Cn0oTWFpbk1lbnUpKTsKZnVuY3Rpb24gcGFnZVJlYWR5KCkgewogICAgdmFyIGl0ZW0gPSBuZXcgTWFpbk1lbnVJdGVtKCk7CiAgICBpdGVtLlZhbHVlID0gIlRlc3QiOwogICAgaXRlbS5jcmVhdGUyKCk7Cn0K"
|
||||
webUI["html/login.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvbmV0d29ya190cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvYXV0aGVudGljYXRpb25fdHMuanMiPjwvc2NyaXB0PgogIDwvaGVhZD4KCiAgICA8Ym9keT4KICAgICAgICAgIAogICAgICA8ZGl2IGlkPSJoZWFkZXIiIGNsYXNzPSJpbWdDZW50ZXIiPjwvZGl2PgoKICAgICAgPGRpdiBpZD0iYm94Ij4KCiAgICAgICAgPGRpdiBpZD0iaGVhZGxpbmUiPgogICAgICAgICAgPGgxIGlkPSJoZWFkLXRleHQiIGNsYXNzPSJjZW50ZXIiPnt7LmxvZ2luLmhlYWRsaW5lfX08L2gxPgogICAgICAgIDwvZGl2PgoKICAgICAgICA8cCBpZD0iZXJyIiBjbGFzcz0iZXJyb3JNc2cgY2VudGVyIj57ey5hdXRoZW50aWNhdGlvbkVycn19PC9wPgoKICAgICAgICA8ZGl2IGlkPSJjb250ZW50Ij4KCiAgICAgICAgICAgIDxmb3JtIGlkPSJhdXRoZW50aWNhdGlvbiIgYWN0aW9uPSIvd2ViLyIgbWV0aG9kPSJwb3N0Ij4KCiAgICAgICAgICAgICAgPGg1Pnt7LmxvZ2luLnVzZXJuYW1lLnRpdGxlfX06PC9oNT4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9InVzZXJuYW1lIiB0eXBlPSJ0ZXh0IiBuYW1lPSJ1c2VybmFtZSIgcGxhY2Vob2xkZXI9IlVzZXJuYW1lIiB2YWx1ZT0iIj4KICAgICAgICAgICAgICA8aDU+e3subG9naW4ucGFzc3dvcmQudGl0bGV9fTo8L2g1PgogICAgICAgICAgICAgIDxpbnB1dCBpZD0icGFzc3dvcmQiIHR5cGU9InBhc3N3b3JkIiBuYW1lPSJwYXNzd29yZCIgcGxhY2Vob2xkZXI9IlBhc3N3b3JkIiB2YWx1ZT0iIj4KCiAgICAgICAgICAgIDwvZm9ybT4KCiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgaWQ9ImJveC1mb290ZXIiPgogICAgICAgICAgPGlucHV0IGlkPSJzdWJtaXQiIGNsYXNzPSIiIHR5cGU9ImJ1dHRvbiIgdmFsdWU9Int7LmJ1dHRvbi5sb2dpbn19IiBvbmNsaWNrPSJqYXZhc2NyaXB0OiBsb2dpbigpOyI+CiAgICAgICAgPC9kaXY+CiAgICAgICAgCiAgICAgIDwvZGl2PgoKICAgIDwvYm9keT4KICAgIAo8L2h0bWw+"
|
||||
webUI["html/img/logo_b_880x200.jpg"] = "/9j/4AAQSkZJRgABAQABLAEsAAD/4QDIRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAPAAAAcgEyAAIAAAAUAAAAgodpAAQAAAABAAAAlgAAAAAAAAEsAAAAAQAAASwAAAABUGl4ZWxtYXRvciAzLjMAADIwMTk6MDg6MDIgMjA6MDg6MTQAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAA3CgAwAEAAAAAQAAAMgAAAAA/+EJkGh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcDpDcmVhdG9yVG9vbD0iUGl4ZWxtYXRvciAzLjMiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTA4LTAyVDIwOjA4OjE0Ii8+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAyANwAwERAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/bAEMBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/dAAQAbv/aAAwDAQACEQMRAD8A/iHoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAP/9D+IegAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgLine truncated
|
||||
webUI["html/img/logo_w_600x200.png"] = "iVBORw0KGgoAAAANSUhEUgAAAlgAAADICAYAAAA0n5+2AAAAAXNSR0IArs4c6QAAAAlwSFlzAAAuIwAALiMBeKU/dgAABCZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjMwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MzAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NjAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4yMDA8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZGM6c3ViamVjdD4KICAgICAgICAgICAgPHJkZjpCYWcvPgogICAgICAgICA8L2RjOnN1YmplY3Q+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE5LTA4LTAxVDEyOjA4OjQzPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5QaXhlbG1hdG9yIDMuMzwveG1wOkNyZWF0b3JUb29sPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KaDoJhwAAJyJJREFUeAHtnQm8XUV9x+ckIQHZEkKWF4UCAlZZZSuyuICRRRCQtmxWwYoohYIial0qrljcgbYqCmhRjEEoAaIGFShirLJ8gCI0CBqweS8JYYkQlkBOf/+X98J9793lnHtm5px773c+n8m7Z87M//+f70zu+d+ZOTPOESAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAgV4ikPRSZakrBNI03UwUjoKEdwKrkiS53LvUHhMYuX8uV5vN6xTEYvM62bptBHsfFJcbIuhBRZcTwMHq8gameiMJ6Ev6MKVcMzKVK08EJurBtNqTrJ4Uo/45WxVfEKnyz0jPNLXZk5H0FVIjNr+XgJcXEpKt8A1ickC2rOSCQGMC4xrf4g4EIAABCEQmcKP0rYikc33pOTSSrkJq5FztLAExnCuz80eFjKUwBIYI4GDRFSAAAQhUhMDQCOCVEc15a0RdRVTFsvMFGTm3iKGUhcAwARysYRL8hQAEIFANAj+IaMahGh2aFFFfu6piOVg2PbisXSMpB4FaAjhYtTT4DAEIQKB8AjfJhIFIZmwsPW+KpKstNXIAt1PBndoqnL9QTOc2v3WU6CgCOFgd1VwYCwEIdDsBjaDEnqaKNTrUbtPFsu85GRhzerZdHpTrEAI4WB3SUJgJAQj0FIGYIymHa5RoQoXpxnKwFsi5fazCHDCtwwjgYHVYg2EuBCDQEwQWqpYPRarpVOmxPaYqF+T4bSGj9oxkWEynNlKVUFMmARysMumjGwIQgEAdAhpJSZU8p86tUEmxRony2n+kCsTYr3GV9Fyd1zjyQ6AZARysZnS4BwEIQKA8AjFHVI7UaFEMRyYvzaPzFmgz/3w5tR2x4Wqb9aNYCQRwsEqAjkoIQAACrQjogX+78ixqlc/T/VmS8xpPsryIkcM3TYL28yKstZCYzmxra8jRFQRwsLqiGakEBCDQpQR6eZrwCLXp+AjtulI6rougBxU9RgAHq8canOpCAAIdRSCmg1W1Q9BjrQubp9FCO5eRAAGvBHCwvOJEGAQgAAF/BPTgv0fS7vYnsamkbTQtt2vTHJFuyo5NperASOqYHowEutfU4GD1WotTXwhAoNMIxHQAYo0atWqDw5RhYqtMHu7bwdoLPMhBBATGEMDBGoOEBAhAAAKVIhBzmjDWW3utAMdy9K7SKOHqVsZwHwLtEMDBaocaZSAAAQhEIiAH4AGp+m0kda/S9NwrIumqq0b6X6IbB9e96T8x5uigf+uRWGkCOFiVbh6MgwAEIDBIIKYjEGv0qFHTHqQb5mSFDgNScGNoJcjvXQI4WL3b9tQcAhDoHAJzZart7h4jlO1gxdJ/hUYH7WBtAgSCEKjyAZ9BKozQnidwnwh8pwQKW0lnrPPeyqifHTXCwypQx5Ij8LCmzn4p8fsHUlErdg/p2lI6H6pNjPFZem1h++ExdElHzFHBSFVCDQQgAIEeI6AHx96KMcITPYa2Z6qrznNqjA40pOPMMsBK98GR6vhH6ani0UBlYEdnIAJMEQYCi1gIQAACnglcIXmxRgljTdONRhRL71yN0MWach1dR657hABThD3S0FQTAr1EQKMT01XfzYfi1FF/Ld3SNlG0qU3bC+nRmr+1nx/Ug/gR3Ss9yI5lqtcvZMjsCMbsK10zpHNpBF2DKqTPjsWx43FihMpPD3ZjH47RsFXSgYNVpdbAFghAoC0Cehj9hQrazt8HDMW+tgSNLSTR6e+UfKPiTRbN0dHfsoI5BjEcLJvdMGfnmxEruq90mWMcOixSG94WWkle+T3Uh/Oi6dj8vT0HvSQ92iVuw2itN979wU1Pbo6mb3m6vXve7R1NX+z6RatYcUX68rR2WFhcUksJK/XwsGNGujqIpz2Ih50pc6y2iVjhe6XrRsVrxPrHEfU61XuK9Nn2ArYYPHT4qep3cGglw/JVt6/q8xnD1wH/fkb1+nhA+ZlE92ofzgSnSzL1roNlw9H9zr4ot4vYlk+7CW4XOVn3B9eZppP0NXyHXux+ZXBdwwoSvZ3Xl5w4fMnfFwnoyxQH60UcbX8Sxz1U+BzFN7ctxG/BOyTuM4q2I3iUNT1iME/6YrxpZzucT1e9HtffoEF1smfRYsUtgipaK3xH1cnOeCwl0IdLwV6KUhsG7s2wdv+T4zSCFfOYhA00onSxfoaG5z7gPhnZuXpQv6n/sTc7E7UOTUAPpT0Vr5Ue29G8Ks6VVfvVij9SvEv2HasY/v92vO0F1lO9YjhyUuPMcY7hXN1VlnNFH7Zm7q0Q48ugukRnDc7DfySygfu5pe60oDr7070k/wNBddQKX+ukHuumJitrk/kMgaIEah5Kv5GsKjlWo6u2oxIuV/ydbLa1SyGDjWA9HVJBjexYb/XF0vPDmrpF+UgfjoK5kkp628GyJpnpvqR/r4/aOmvcuW5p+vIgOm1q0LlLNHo1Poj8+kI/oqlBG1kgQMALAT2UtlK0EauqO1aj6/sKJfynbD9HMcgSDI3APCkdxiZGOEj12DCColgOVrS3B8WNPhyh41RZBQ6WrZuY4N6uqcLlERvqJW6N+7amCv1/AfdrfUrqXhWtLombP+SkRlOJoLine truncated
|
||||
webUI["html/img/users.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0wOC0zMFQxNzowODozODwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CjU01MMAAANJSURBVGgF1ZnLaxNRFMYz2rpoFbW+SCsihaJQRMT6rPhAXAq6c+HChSguRFd1oRtd+A/oWhQ3oiiWFB91UbqRqlXXgoIIQhFbQTTQao2/GzLDZEjIPefO7SQHPjJz55zzfd/MZCa5N8h5ilKptJvWp8FekAed4Bv4Cj6Dh2A4CIJZPpszMHEV2MQ0SZdB0DROENMHjoEzQBoFCpZnbgYR58E/qfpE/kv2F2dmBvJ94G9ClHb3SiZGUBuASa3qGnVzjG3RmFmkKYrVHGB7e2zfdbOdBhc1TVyNnNSQNqg5wVURf/FdjRxpIEpzuIOio9JCtRHO2hrINkgJLfPFt6vaCII2WYrSpPVJi1RGuBrmTbxeSibIX5j3CUauAfOo9BXzNH4nMJ5TXREI1gHzqPQVRtc2zCyxJdAaGbclcMj7zi/jOdt6rZFhCH7akijznknqVEY4U78heSAhUuTekNSojFQIChIiYe57TtZrSY2LkecQzUjIBLljglz3VJ4qg+AXSDNGabbWXZ2wA6Qjabqg136hhHK6y60V8o2EGyl8/qHHpKZPGkYeQzyvIa9RM86XvFhjvOGQsxGIp2B50ZDJLuG2XZqnLO5rM3viGh9o0OZJon1bRLxxdHLcns1jJiYOAu2U0D2P0uStMXJdcVU+UdMlZ/NYgaA28ERg5ge5/R4l6VsjrANMWJiZJWeXnqm60vnxW90ul6u8ByaS4zX2i+S+qjGuGkrdSEWFzT1vlhlSC19Gui0UtnNriSfi6vX1ZWRjPcLEeGrzYqkb4SyvQGxvQnC93cF6BzIfx8gpYBsfSdycuei4AAR1ggugCCRh1lbugIF4vwXbhtisjfSDc+ARSOOf4lv6mNWv1VIj1ouQNF9K851gDzArtWbV1uYxS5o4zB+sp+AuKPC+cVv5RXwPuATMmUpreY1Wopgh+ybYKj4dFHWB+8DMwTZTjCHmsJUhEgfAF9DMcQtx9V8dHMyDqWZ2ENM2FL8yVV92kkY56GM5Lc6Z1vY0jbp5EJQnuqPLg4kdLWTCnIxVIJoDi4wweNYcbbGIfuLEjRxqMRNGbvTvsmyE22oZg70taKQn1BxekXw40GKfK0O9oZHsJ8ZCRbLPaI3xP7YzeQoHxWckAAAAAElFTkSuQmCC"
|
||||
webUI["html/js/log.js"] = "dmFyIGxvZ0ludGVydmFsCgoKZnVuY3Rpb24gdXBkYXRlTG9nKCkgewogIHZhciBkYXRhID0gbmV3IE9iamVjdCgpOwogIGRhdGFbImNtZCJdID0gImdldExvZyI7CiAgeFRlVmUoZGF0YSk7CiAgd3JpdGVMb2dJbkRpdigpOwogIHJldHVybgp9CgpmdW5jdGlvbiB3cml0ZUxvZ0luRGl2KCkgewogIHZhciBsb2dzID0gbG9nWyJsb2ciXTsKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIikubGFzdENoaWxkLmxhc3RDaGlsZDsKICBkaXYuaW5uZXJIVE1MID0gIiI7CgogIHZhciBtYXggPSA1MDsKICAKICAKICBmb3IgKHZhciBpID0gMDsgaSA8IGxvZ3MubGVuZ3RoOyBpKyspIHsKICAgIHZhciBuZXdFbnRyeSA9IG5ldyBPYmplY3QoKTsKICAgIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJQIjsKCiAgICBpZiAobG9nc1tpXS5pbmNsdWRlcygiRVJST1IiKSkgewovLyAgICAgIGNhc2UgIndhcm5pbmdzIjogIG1zZ1R5cGUgPSAid2FybmluZ01zZyI7IGJyZWFrOwogICAgICBuZXdFbnRyeVsiY2xhc3MiXSAgID0gImVycm9yTXNnIjsKICAgIH0KCiAgICBpZiAobG9nc1tpXS5pbmNsdWRlcygiV0FSTklORyIpKSB7Ci8vICAgICAgY2FzZSAid2FybmluZ3MiOiAgbXNnVHlwZSA9ICJ3YXJuaW5nTXNnIjsgYnJlYWs7CiAgICAgIG5ld0VudHJ5WyJjbGFzcyJdICAgPSAid2FybmluZ01zZyI7CiAgICB9CgogICAgbmV3RW50cnlbIl90ZXh0Il0gICAgID0gbG9nc1tpXTsKICAgIAogICAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKICB9CgogIGNhbGN1bGF0ZVdyYXBwZXJIZWlnaHQoKTsKICB2YXIgc2Nyb2xsRGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJveC13cmFwcGVyIik7CiAgc2Nyb2xsRGl2LnNjcm9sbFRvcCA9IHNjcm9sbERpdi5zY3JvbGxIZWlnaHQ7Cn0KCmZ1bmN0aW9uIHNob3dMb2cob2JqKSB7CiAgLy9sb2dJbnRlcnZhbCA9IHNldEludGVydmFsKHVwZGF0ZUxvZywgNTAwMCk7CgogIHZhciBsb2dzID0gbG9nWyJsb2ciXTsKCiAgdmFyIGRpdiA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJzZXR0aW5ncyIpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gID0gIkhSIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwogIC8vZGl2ID0gZGl2Lmxhc3RDaGlsZDsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gICAgICAgPSAiRW1wdHkgTG9nIjsKICBuZXdFbnRyeVsib25jbGljayJdICAgICA9ICJlbXB0eUxvZygpIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gICAgPSAiY29kZSI7CiAgbmV3RW50cnlbIl90ZXh0Il0gICAgICAgID0gIlVwZGF0ZSBMb2c6ICI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJjaGVja2JveCI7CiAgLy9uZXdFbnRyeVsiY2hlY2tlZCJdICAgICA9ICJjaGVja2JveCI7CiAgbmV3RW50cnlbIm9uY2xpY2siXSAgICAgPSAibG9nVXBkYXRlcyh0aGlzKSI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgCgoKICAKICB2YXIgbmV3V3JhcHBlciA9IG5ldyBPYmplY3QoKTsKICBuZXdXcmFwcGVyWyJfZWxlbWVudCJdICA9ICJESVYiOwogIG5ld1dyYXBwZXJbImlkIl0gICAgICAgID0gImJveC13cmFwcGVyIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdXcmFwcGVyKSk7CiAgZGl2ID0gZGl2Lmxhc3RDaGlsZDsKCiAgdmFyIG5ld1ByZSA9IG5ldyBPYmplY3QoKTsKICBuZXdQcmVbIl9lbGVtZW50Il0gID0gIlBSRSI7CiAgbmV3UHJlWyJpZCJdICAgICAgICA9ICJsb2dTY3JlZW4iOwogIGRpdi5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld1ByZSkpOwoKICBkaXYgPSBkaXYubGFzdENoaWxkOwoKICB3cml0ZUxvZ0luRGl2KCkKICByZXR1cm4KfQoKZnVuY3Rpb24gZW1wdHlMb2coKSB7CiAgdmFyIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgZGF0YVsiY21kIl0gPSAiZW1wdHlMb2ciOwogIHhUZVZlKGRhdGEpOwogIHJldHVybgp9CgpmdW5jdGlvbiBsb2dVcGRhdGVzKGVsbSkgewogIHN3aXRjaChlbG0uY2hlY2tlZCkgewogICAgY2FzZSBmYWxzZTogY2xlYXJJbnRlcnZhbChsb2dJbnRlcnZhbCk7IGJyZWFrOwogICAgY2FzZSB0cnVlOiBsb2dJbnRlcnZhbCA9IHNldEludGVydmFsKHVwZGF0ZUxvZywgNTAwMCk7IGJyZWFrOwogICAgCiAgfQp9"
|
||||
webUI["html/js/configuaration.js"] = "dmFyIGNvbmZpZ01lbnUgPSBuZXcgT2JqZWN0KCk7CnZhciB3aXphcmQgPSBuZXcgQXJyYXkoImtleSIsICJ0dW5lciIsICJlcGdTb3VyY2UiLCAibTN1IiwgImNvbXBsZXRlIik7CnZhciBhY3RpdmVXaXphcmQ7CnZhciBkdnJJUAoKdmFyIGNvbmZpZ01lbnVfdHVuZXIgPSBuZXcgT2JqZWN0KCk7CmNvbmZpZ01lbnVfdHVuZXJbIl9lbGVtZW50Il0gICAgICA9ICJTRUxFQ1QiOwpjb25maWdNZW51X3R1bmVyWyJfbWVudVR5cGUiXSAgICAgPSAic2luZ2xlSW5wdXQiOwpjb25maWdNZW51X3R1bmVyWyJfY29uZmlnS2V5Il0gICAgPSAidHVuZXIiOwpjb25maWdNZW51X3R1bmVyWyJfbGFiZWwiXSAgICAgICAgPSAiQXZhaWxhYmxlIHR1bmVycyI7CmNvbmZpZ01lbnVfdHVuZXJbIm5hbWUiXSAgICAgICAgICA9ICJ0dW5lciI7CmNvbmZpZ01lbnVfdHVuZXJbImlkIl0gICAgICAgICAgICA9ICJUdW5lciI7CmNvbmZpZ01lbnVfdHVuZXJbInBsYWNlaG9sZGVyIl0gICA9ICJUdW5lciI7CmNvbmZpZ01lbnVfdHVuZXJbIl91c2FnZSJdICAgICAgICA9ICJUaGlzIHNldHRpbmcgaXMgb25seSB1c2VkIGJ5IFBsZXggYW5kIEVtYnkuPGJyPlRoZSBudW1iZXIgb2YgY29uY3VycmVudCBzdHJlYW1zIGFsbG93ZWQgYnkgdGhlIElQVFYgcHJvdmlkZXIuIgoKCnZhciBvcHRpb25WYWx1ZXMgPSBuZXcgQXJyYXkoKTsKZm9yICh2YXIgaSA9IDE7IGkgPD0gMTAwOyBpKyspIHsKICBvcHRpb25WYWx1ZXMucHVzaChpKQp9CmNvbmZpZ01lbnVfdHVuZXJbIl9vcHRpb25WYWx1ZXMiXSA9IG9wdGlvblZhbHVlczsKCnZhciBjb25maWdNZW51X2VwZyA9IG5ldyBPYmplY3QoKTsKY29uZmlnTWVudV9lcGdbIl9lbGVtZW50Il0gICAgICA9ICJTRUxFQ1QiOwpjb25maWdNZW51X2VwZ1siX21lbnVUeXBlIl0gICAgID0gInNpbmdsZUlucHV0IjsKY29uZmlnTWVudV9lcGdbIl9jb25maWdLZXkiXSAgICA9ICJlcGdTb3VyY2UiOwpjb25maWdNZW51X2VwZ1siX2xhYmVsIl0gICAgICAgID0gIlNlbGVjdGlvbiBvZiB0aGUgRVBHIHNvdXJjZSI7CmNvbmZpZ01lbnVfZXBnWyJuYW1lIl0gICAgICAgICAgPSAiZXBnU291cmNlIjsKY29uZmlnTWVudV9lcGdbImlkIl0gICAgICAgICAgICA9ICJFUEcgc291cmNlIjsKY29uZmlnTWVudV9lcGdbInBsYWNlaG9sZGVyIl0gICA9ICJFUEcgc291cmNlIjsKY29uZmlnTWVudV9lcGdbIl9vcHRpb25WYWx1ZXMiXSA9IG5ldyBBcnJheSgiUE1TIiwgIlhFUEciKTsKY29uZmlnTWVudV9lcGdbIl91c2FnZSJdICAgICAgICA9ICJQTVM6ICAgVXNlIEVQRyBkYXRhIGZyb20gUGxleCBvciBFbWJ5PGJyPlhFUEc6ICBVc2Ugb2YgZXh0ZXJuYWwgRVBHIGRhdGEgKFhNTFRWKTxicj4gICAgICAgU2V2ZXJhbCBYTUxUViBzb3VyY2VzIHBvc3NpYmxlPGJyPiAgICAgICBBbGxvd3MgZWRpdGluZyBhbmQgb3JkZXIgY2hhbm5lbHM8YnI+ICAgICAgIE0zVSAvIFhNTFRWIGV4cG9ydCAoSFRUUCBsaW5rIGZvciBJUFRWIGFwcHMpIgoKdmFyIGNvbmZpZ01lbnVfbTN1ID0gbmV3IE9iamVjdCgpOwpjb25maWdNZW51X20zdVsiX2VsZW1lbnQiXSAgICAgICAgPSAiSU5QVVQiOwpjb25maWdNZW51X20zdVsiX21lbnVUeXBlIl0gICAgICAgPSAiaW5wdXRBcnJheSI7CmNvbmZpZ01lbnVfbTN1WyJfY29uZmlnS2V5Il0gICAgICA9ICJmaWxlIjsKY29uZmlnTWVudV9tM3VbIl9sYWJlbCJdICAgICAgICAgID0gIk0zVSBGaWxlOiBsb2NhbCBvciByZW1vdGUiOwpjb25maWdNZW51X20zdVsibmFtZSJdICAgICAgICAgICAgPSAiZmlsZSI7CmNvbmZpZ01lbnVfbTN1WyJpZCJdICAgICAgICAgICAgICA9ICJtM3UiOwpjb25maWdNZW51X20zdVsidHlwZSJdICAgICAgICAgICAgPSAidGV4dCI7CmNvbmZpZ01lbnVfbTN1WyJwbGFjZWhvbGRlciJdICAgICA9ICJNM1UgRmlsZSI7CmNvbmZpZ01lbnVfbTN1WyJfdXNhZ2UiXSAgICAgICAgICA9ICJSZW1vdGUgcGxheWxpc3Q6IGh0dHA6Ly95b3VyLnByb3ZpZGVyLmNvbS9maWxlLm0zdTxicj5Mb2NhbCAgcGxheWxpc3Q6IC9wYXRoL3RvL2ZpbGUubTN1IgoKCmNvbmZpZ01lbnVfbTN1WyJ2YWx1ZSJdICAgICAgICAgICA9ICJodHRwOi8vd2Vic3J2LmxvY2FsOjgwODAva2FiZWwubTN1IjsKCnZhciBjb25maWdNZW51X2NvbXBsZXRlID0gbmV3IE9iamVjdCgpOwpjb25maWdNZW51X2NvbXBsZXRlWyJfZWxlbWVudCJdICAgICAgICA9ICJIMiI7CmNvbmZpZ01lbnVfY29tcGxldGVbIl9tZW51VHlwZSJdICAgICAgID0gImlucHV0QXJyYXkiOwpjb25maWdNZW51X2NvbXBsZXRlWyJfY29uZmlnS2V5Il0gICAgICA9ICJmaWxlIjsKY29uZmlnTWVudV9jb21wbGV0ZVsiX3RleHQiXSAgICAgICAgICAgPSAieFRlVmUgd2FzIHN1Y2Nlc3NmdWxseSBzZXQgdXAiOwpjb25maWdNZW51X2NvbXBsZXRlWyJuYW1lIl0gICAgICAgICAgICA9ICJjb21wbGV0ZSI7CmNvbmZpZ01lbnVfY29tcGxldGVbImlkIl0gICAgICAgICAgICAgID0gImNvbXBsZXRlIjsKY29uZmlnTWVudV9jb21wbGV0ZVsidHlwZSJdICAgICAgICAgICAgPSAidGV4dCI7CmNvbmZpZ01lbnVfY29tcGxldGVbImNsYXNzIl0gICAgICAgICAgID0gImNlbnRlciI7Cgpjb25maWdNZW51WyJ0dW5lciJdICAgICA9IGNvbmZpZ01lbnVfdHVuZXI7CmNvbmZpZ01lbnVbImVwZ1NvdXJjZSJdID0gY29uZmlnTWVudV9lcGc7CmNvbmZpZ01lbnVbIm0zdSJdICAgICAgID0gY29uZmlnTWVudV9tM3U7CmNvbmZpZ01lbnVbImNvbXBsZXRlIl0gID0gY29uZmlnTWVudV9jb21wbGV0ZTsKCmZ1bmN0aW9uIHJlYWR5Rm9yQ29uZmlndXJhdGlvbigpIHsKICB2YXIgZGF0YSA9IG5ldyBPYmplY3QoKTsKICBkYXRhWyJjbWQiXSA9ICJnZXRTZXJ2ZXJDb25maWciOwogIHhUZVZlKGRhdGEpOwogIHNob3dMb2FkaW5nU2NyZWVuKGZhbHNlKTsKfQoKZnVuY3Rpb24gY3JlYXRlQ29uZmlndXJhdGlvbihlbG0pIHsKCiAgYWN0aXZlV2l6YXJkID0gZWxtOwogIHZhciBpdGVtICA9IGNvbmZpZ01lbnVbZWxtXTsKCiAgdmFyIGRpdiAgID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImNvbnRlbnQiKTsKICBkaXYuaW5uZXJIVE1MID0gIiI7CiAgZGl2LnNldEF0dHJpYnV0ZSgiZGF0YS1jb25maWdLZXkiLCBpdGVtWyJfY29uZmlnS2V5Il0pOwogIGRpdi5zZXRBdHRyaWJ1dGUoImRhdGEtbWVudVR5cGUiLCBpdGVtWyJfbWVudVR5cGUiXSk7CiAgCiAgc3dpdGNoKGl0ZW0uaGFzT3duUHJvcGVydHkoIl9sYWJlbCIpKSB7CiAgICBjYXNlIHRydWU6CiAgICAgIHZhciBuZXdJdGVtID0gbmV3IE9iamVjdCgpOwogICAgICBuZXdJdGVtWyJfZWxlbWVudCJdID0gIkxBQkVMIjsKICAgICAgbmV3SXRlbVsiX3RleHQiXSAgICA9IGl0ZW1bIl9sYWJlbCJdOyAKICAgICAgbmV3SXRlbVsiZm9yIl0gICAgICA9IGl0ZW1bImlkIl07CiAgICAgIGRpdi5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0l0ZW0pKTsKICAgICAgYnJlYWsKICB9CgogIHN3aXRjaChpdGVtWyJfZWxlbWVudCJdKSB7CiAgICBjYXNlICJTRUxFQ1QiOgogICAgICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChpdGVtKSk7CiAgICAgIHZhciBzZWxlY3RFbGVtZW50ID0gZGl2LmdldEVsZW1lbnRzQnlUYWdOYW1lKCJTRUxFQ1QiKVswXTsKICAgICAgdmFyIHZhbHVlcyA9IGl0ZW1bIl9vcHRpb25WYWx1ZXMiXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCB2YWx1ZXMubGVuZ3RoOyBpKyspIHsKICAgICAgICBLine truncated
|
||||
webUI["html/css/base.css"] = "KiB7CiAgLXdlYmtpdC1hcHBlYXJhbmNlOiBub25lOwogIC1tb3otYXBwZWFyYW5jZTogbm9uZTsKICAtbXMtYXBwZWFyYW5jZTogbm9uZTsKICBmb250LWZhbWlseTogIkFyaWFsIiwgc2Fucy1zZXJpZjsKICBsZXR0ZXItc3BhY2luZzogMnB4OyAKfQoKLyoKOjotd2Via2l0LXNjcm9sbGJhciB7IAogICAgZGlzcGxheTogbm9uZTsgCn0KKi8KCjo6LXdlYmtpdC1zY3JvbGxiYXIgewogIHdpZHRoOiAxMnB4OwogIGhlaWdodDogMTJweDsKfQoKIAo6Oi13ZWJraXQtc2Nyb2xsYmFyLXRyYWNrIHsKICAtd2Via2l0LWJveC1zaGFkb3c6IGluc2V0IDAgMCA2cHggcmdiYSgwLDAsMCwwLjMpOyAKICBib3JkZXItcmFkaXVzOiA1cHg7CiAgICAKfQogCjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWIgewogIGJvcmRlci1yYWRpdXM6IDVweDsKICAtd2Via2l0LWJveC1zaGFkb3c6IGluc2V0IDAgMCA2cHggcmdiYSgwLCAwLCAwLDAuNik7IAogIGJhY2tncm91bmQtY29sb3I6ICM0NDQ7Cn0KCjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWI6aG92ZXIgewogIGJhY2tncm91bmQ6ICMzMzM7IAp9Cgo6Oi13ZWJraXQtc2Nyb2xsYmFyLWNvcm5lciB7IAogIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50OyAKfQoKYSB7CiAgY29sb3I6ICMwMEU2RkY7Cn0KCmh0bWwsIGJvZHkgewogIGNvbG9yOiAjZmZmOwogIG1hcmdpbjogMHB4IGF1dG87CiAgaGVpZ2h0OiAxMDAlOwogIGZvbnQtc2l6ZTogMTRweDsKfQoKaDIgewogIGZvbnQtc2l6ZTogMjRweDsKICBsZXR0ZXItc3BhY2luZzogMnB4Owp9CgpoMyB7CiAgZm9udC1zaXplOiAyMnB4OwogIGxldHRlci1zcGFjaW5nOiAxcHg7Cn0KCmg0IHsKICBmb250LXNpemU6IDIwcHg7CiAgbGV0dGVyLXNwYWNpbmc6IDFweDsKICBsaW5lLWhlaWdodDogMS41ZW07Cgp9CgpoNSB7CiAgZm9udC1zaXplOiAxNnB4OwogIGxldHRlci1zcGFjaW5nOiAxcHg7CiAgbGluZS1oZWlnaHQ6IDEuMmVtOwogIG1hcmdpbjogMjVweCAwcHggMTBweCAwcHg7Cn0KCmhyIHsKICBib3JkZXI6IDA7CiAgaGVpZ2h0OiAxcHg7CiAgYmFja2dyb3VuZDogIzMzMzsKICBtYXJnaW46IDEwcHggMHB4Owp9CgpwIHsKICBtYXJnaW46IDJweDsKICBwYWRkaW5nOiAycHggNXB4Owp9CgpwcmUgewogIG1hcmdpbjogMHB4IDBweCA1cHggMHB4OwogIGZvbnQtc2l6ZTogMTJweDsKICBjb2xvcjogI2RkZDsKICBsZXR0ZXItc3BhY2luZzogMXB4OwogIHdoaXRlLXNwYWNlOiBwcmUtd3JhcDsKICBmb250LWZhbWlseTogbW9ub3NwYWNlOyAKICBmb250LXNpemU6IDEycHg7IAogIGZvbnQtc3R5bGU6IG5vcm1hbDsgCiAgZm9udC12YXJpYW50OiBub3JtYWw7IAogIGxpbmUtaGVpZ2h0OiAxLjZlbTsgCn0KCmxhYmVsIHsKICBtYXJnaW4tYm90dG9tOiAyMHB4OwogIGRpc3BsYXk6IGJsb2NrOwp9CgpsaSB7CiAgbGlzdC1zdHlsZS10eXBlOiBub25lOwogIGJhY2tncm91bmQtY29sb3I6ICMxMTE7CiAgcGFkZGluZzogMTBweCAyMHB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBib3JkZXItbGVmdDogc29saWQgMnB4ICMxMTE7CiAgdHJhbnNpdGlvbjogYWxsIDAuMzsKfQoKbGk6aG92ZXIgewogIGJvcmRlci1jb2xvcjogIzAwRTZGRgp9CgpzZWxlY3QgewogIGN1cnNvcjogcG9pbnRlcjsKICB3aWR0aDogY2FsYygxMDAlICsgMnB4KTsKICBib3JkZXI6IHNvbGlkIDBweCAjMDBFNkZGOwogIGJvcmRlci1yYWRpdXM6IDBweDsKICBvdXRsaW5lOiBub25lOwogIGNvbG9yOiAjZmZmOwogIHBhZGRpbmc6IDlweCAxMHB4OwogIGRpc3BsYXk6YmxvY2s7CiAgYmFja2dyb3VuZC1jb2xvcjogIzMzMzsKICBmb250LXNpemU6IDE0cHg7CiAgbWFyZ2luOiA1cHggMHB4IDVweCAwcHg7Cn0KCnNlbGVjdDpmb2N1cyB7CiAgb3V0bGluZTogbm9uZTsKfQoKaW5wdXQgewogIC13ZWJraXQtYXBwZWFyYW5jZTogbm9uZTsKICBtYXJnaW46IDVweCAwcHg7CiAgcGFkZGluZzogMi41cHggMTBweDsKICBvdXRsaW5lOiBub25lOwogIGZvbnQtc2l6ZTogMTRweDsKfQoKaW5wdXRbdHlwZT1idXR0b25dLCBpbnB1dFt0eXBlPXN1Ym1pdF0gewogIGN1cnNvcjogcG9pbnRlcjsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogIG1hcmdpbjogMTBweCAxMHB4OwogIHBhZGRpbmc6IDEwcHggMjVweDsKICBib3JkZXI6IHNvbGlkIDBweDsKICBib3JkZXItY29sb3I6ICMwMDA7CiAgYm9yZGVyLXJhZGl1czogM3B4OwogIG91dGxpbmU6IG5vbmU7CiAgY29sb3I6ICNmZmY7Cn0KCmlucHV0W3R5cGU9YnV0dG9uXTpmb2N1cyB7IAogICBvdXRsaW5lOiBub25lOwp9CgppbnB1dFt0eXBlPWJ1dHRvbl06aG92ZXIgewogIGJhY2tncm91bmQtY29sb3I6ICMwMEU2RkY7CiAgY29sb3I6ICMwMDA7Cn0KCmlucHV0W3R5cGU9YnV0dG9uXTpob3Zlci5kZWxldGUgIHsKICBiYWNrZ3JvdW5kLWNvbG9yOiByZWQ7CiAgY29sb3I6ICNmZmY7Cn0KCmlucHV0W3R5cGU9dGV4dF0sIGlucHV0W3R5cGU9c2VhcmNoXSwgaW5wdXRbdHlwZT1wYXNzd29yZF0gewogIGNvbG9yOiAjZmZmOwogIHdpZHRoOiAtd2Via2l0LWNhbGMoMTAwJSAtIDBweCk7CiAgd2lkdGg6IC1tb3otY2FsYygxMDAlIC0gMHB4KTsKICB3aWR0aDogY2FsYygxMDAlIC0gMHB4KTsKICBvdXRsaW5lOiBub25lOwogIGJvcmRlcjogc29saWQgMXB4IHRyYW5zcGFyZW50OwogIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50OwogIGJvcmRlci1ib3R0b20tY29sb3I6ICM1NTU7CiAgYm9yZGVyLXJhZGl1czogMHB4OwogIHBhZGRpbmc6IDhweCAxMHB4Owp9CgppbnB1dFt0eXBlPSJjaGVja2JveCJdIHsKICBib3JkZXI6IHNvbGlkIDFweCAjMDBFNkZGOwogIGJhY2tncm91bmQtY29sb3I6ICMzMzM7CiAgaGVpZ2h0OiAyNXB4OwogIHdpZHRoOiAyNXB4OwogIGN1cnNvcjogcG9pbnRlcjsKICAvKgogIC13ZWJraXQtYXBwZWFyYW5jZTogY2hlY2tib3g7CiAgKi8KfQoKaW5wdXRbdHlwZT0iY2hlY2tib3giXTpjaGVja2VkIHsKICBjb2xvcjogI2ZmZjsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDBFNkZGOwogIC8qZGlzcGxheTogaW5saW5lLWJsb2NrOyovCn0KCmlucHV0W3R5cGU9ImNoZWNrYm94Il06YmVmb3JlIHsKICBwb3NpdGlvbjogaW5pdGlhbDsKICBsZWZ0OiAwcHg7CiAgbWFyZ2luLWxlZnQ6IC00cHg7CiAgY29udGVudDogIiAiOwp9CgppbnB1dFt0eXBlPSJjaGVja2JveCJdOmNoZWNrZWQ6YmVmb3JlIHsKICBwb3NpdGlvbjogaW5pdGlhbDsKICBsZWZ0OiAwcHg7CiAgbWFyZ2luLWxlZnQ6IC0zcHg7CiAgY29udGVudDogIuKckyI7CiAgY29sb3I6ICMwMDA7Cn0KCgppbnB1dFt0eXBlPWJ1dHRvbl0uY2FuY2VsIHsKICAKICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDsKICBib3JkZXItY29sb3I6IHJlZDsKfQoKaW5wdXRbdHlwZT1idXR0b25dLnNhdmV7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICBmbG9hdDogcmlnaHQ7Cn0KCgppbnB1dFt0eXBlPWJ1dHRvbl0uYmxhY2ssIGlucHV0W3R5cGU9c3VibWl0XS5ibGFja3sKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogIGJvcmRlci1jb2xvcjogIzAwMDsKfQoKaW5wdXRbdHlwZT1idXR0b25dLmNlbnRlcnsKICBtYXJnaW4tcmlnaHQ6IGF1dG87CiAgbWFyZ2luLWxlZnQ6IGF1dG87CiAgYmFja2dyb3VuZC1jb2xvcjogIzAwMDsKICBib3JkZXItY29sb3I6ICMwMDA7Cn0KCi5wb2ludGVyIHsKICBjdXJzb3I6IHBvaW50ZXI7Cn0KCi5wb2ludGVyOmhvdmVyIHsKICBjb2xLine truncated
|
||||
webUI["html/img/BC-QR.jpg"] = "/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuqADAAQAAAABAAAAugAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAugC6AwERAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/bAEMBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/dAAQAGP/aAAwDAQACEQMRAD8A/v4oAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/0P7+KAP5/wD/AILmf8FzP+HL/wDwy8B+y9/w0mP2k/8AhdmB/wALsPwe/wCEL/4U8fhLn5j8Jfih/b//AAkH/C0fTRP7F/sL/mIjUQtkAfgH/wARzn/WLz/zdb/8k2gA/wCI5z/rF5/5ut/+SbQAf8Rzn/WLz/zdb/8AJNoAP+I5z/rF5/5ut/8Akm0Af08/8EWP+CrP/D4H9lrx9+0uPgMf2ef+EI+Pvin4GjwSPiofi1/aX/CN/Dn4W+Pj4n/4SH/hXXww+xm8PxOGkf2J/Yl19k/sU3/9sv8A2h9isAD3/wD4KiftzH/gmz+wt8cv21B8Lv8Ahc4+C5+Ghb4a/wDCbD4cf8JJ/wALG+MPgD4TjHjFfB/jwaSdJPjv/hISD4X1L7f/AGadNLWH24ahaAHwH/wQz/4Lmf8AD6D/AIahB/Ze/wCGbB+zZ/wpPI/4XYfjD/wmn/C4T8WsfMPhL8L/AOwP+Ef/AOFXemt/21/bv/MOGnFb0A/oAoAKAP5Af+Cov/B1h/w7Z/bq+OX7FZ/YOHxpPwWHwyP/AAsr/hqA/Dk+Iz8Rvg94B+LH/Inf8M7ePf7J/sr/AITv/hHcjxPqAvTp/wDaObEX32GyAPgH/iOc/wCsXn/m63/5JtAH9PP/AARY/wCCrP8Aw+B/Za8fftLj4DH9nn/hCPj74p+Bo8Ej4qH4tf2l/wAI38Ofhb4+Pif/AISH/hXXww+xm8PxOGkf2J/Yl19k/sU3/wDbL/2h9isAD3//AIKiftzH/gmz+wt8cv21B8Lv+Fzj4Ln4aFvhr/wmw+HH/CSf8LG+MPgD4TjHjFfB/jwaSdJPjv8A4SEg+F9S+3/2adNLWH24ahaAH8g//Ec5/wBYvP8Azdb/APJNoA/r7/4bm/41af8ADy3/AIVf/wA2B/8ADc3/AApb/hNTt/5N1/4X/wD8Kv8A+Fj/APCH4zj/AIpH/hOP+EEx/wAx7/hE8/8AEjoA/kE/4jnP+sXn/m63/wCSbQAf8Rzn/WLz/wA3W/8AyTaAD/iOc/6xef8Am63/AOSbQAf8Rzn/AFi8/wDN1v8A8k2gD7+/4Jdf8HWH/Dyb9ur4G/sVj9g4fBY/GkfE0/8ACyv+GoD8Rj4cPw5+D3j74sf8id/wzt4C/tb+1f8AhBP+EdyfE+niyOof2jm+Fj9hvQD+v6gAoA//0f7+KAP4A/8Ag+c/5xef93rf++lUAfv3/wAE9v8Agnx/wS0/4da/sP8Ax0+Of7D/AOwE279gL9mr4s/GL4x/Fn9mn9nb5f8AjHbwZ4w+IHxI+JPxB8YeC1AAA1fxN4x8Y+JtXJ/4/wDXNdv2zeXtAC/8csn/AFgD/wDOd1AB/wAcsn/WAP8A853UAfjF/wAHcn7Ef7GP7Nv/AATc+Cnjn9nX9kb9l74A+NtU/bd+HHhPVPGPwX+APwp+FfinU/C158CP2ldX1DwxfeIPA3g/Q9Vu9Bu9V0PQ9XutGurttOvNR0TTb5rU3lhYlQD6h/4Mqv8AlFj8e/8As/74p/8ArO37K1AH8of/AAWp/wCHzv8Aw0H/AMFGf+Fwf8PPP+HfH/DX3xy/sf8A4WX/AMNXf8Mcf8Ks/wCGltU/4Ub/AGf/AMJR/wAWS/4QH+0v+EB/4VV9n/4kP2//AIRH/hEv9M/sOgD9Pf8Agzc/aw/Za/Zg/wCHjQ/aX/aU+AX7Pf8AwnH/AAx+fBP/AAvL4yfDn4Snxd/wjK/tP/8ACQ/8IuPH/iXQRr39g/2/4dXWxo7Xv9jHXNG+3G1/tCydgD8Zf23P+Cmn/BSHx7/wUi/a68D/ALKf/BQT9t3xl4K8Zftu/H7wn+zd4N/Z6/at+PHiLwt4t8LeI/jx4o0f4PeGPgf4f+G/ju80rXdC13Sbvw5pXw20jwJZ3enalYXmhWHhSyezewRwDd/46m/+s/n/AJ0RoA/u3/4I1f8ABPj4W/HH/gm5+zf8Uf8Agp3+xB8PvjB+3J4n/wCFv/8AC7/iL+3p+zX4d+IP7VviI6N8d/ihoHw3X4reLv2hfBmq/GLVRpXwe0vwHofgX/hML+6+wfDjT/CGn6AI/CtjoaUAf5V/wV/Z++Pf7SXirUPAv7OfwO+L/wAf/G2l+HrzxXqng74L/DXxl8U/FWneFrO/0vSL/wAU3/h/wNouu6raaFZ6tr+jaTdaxd2f9n2eoa1pli12t5f2SsAf6eX/AAaN/s+fH39m3/gm98avA37RXwP+LvwA8bar+298RvFel+DfjT8NvGXwr8Vap4Vu/gP+zVo9j4nsfD/jnRNC1W70G91XQ9a0q11m2szp13qOi6jYpeC8sL1VAP248WftA/8ABN/9qbxX4k/Yk8c/HL9iH9ovxtrXiDV/CfjL9kTxZ8SvgP8AF7xXrPir4Tahc+KfEPhjxP8AALWNb8QarqOv/DfVvA194m1jRtV8Itf+C9Q8I3euX1lp15oD3lgAY/8Aw6e/4JZ/9I1P2Av/ABDr9nb/AOdvQBzP/BTTwn4X8B/8EjP+CgngTwN4a0LwX4I8Gf8ABOX9q7wl4O8HeE9H07w14V8K+FfDf7M/jzRfDvhjwx4f0a0tdK0Lw9oel2tnpGi6NpNnZ2FhYWVnY2NnFZRolAH+eX/waN/s+fAL9pL/AIKRfGrwN+0V8D/hF8fvBOlfsQ/EXxXpfg340/Dbwb8VPCul+KrT48fs1aPY+J7Hw/450TXdJtNes9K1zWtKtdZtrMajZ6drWo2KXhs7+9VgD+5b4o/Cf/g2++B/jnXPhb8avht/wRE+EPxN8LjTR4n+HfxR8IfsIeAPHfhsa1pGm+IdF/t7wj4q03SvEGk/2poGq6PrWlC/sLQXuj6lp2o2O+yvrN2AOC/45ZP+sAf/AJzuoA+nPgr+xF/wQ5/aT8K6h46/Zz/ZH/4JSfH/AME6X4gvfCepeMfgv8A/2Qfin4V0zxVZ6fpOr33he+8Q+BvCGvaVZ69aaVruj6vd6PdXYv7Sw1vTL6SzFnfWTMAfwm/8EqvCfhPwB/wd6eIfA3gbw34f8F+CfBf7b3/BUbwn4O8HeEtGsPDvhXwp4W8OfDj9r3SfD/hjwzoGj21ppWgaFoOlWdnpGkaNpNnZ2Gn6daWdhY2UVmqKoB/qLUAFAH//0v7+KAP4A/8Ag+c/5xef93rf++lUAfv5/wA6sn/eAP8A+B3UAf5A9ABQB/p+f8Hqv/KLH4Cf9n/fCz/1nb9qmgA/4Mqv+UWPx7/7P++Kf/rO37K1AH5Bf8F9P+Dj4ftEfDz/AIKDf8Elj+xt/wAIePD3x/1f4F/8L/P7Q518Xn/DL/7U+g6z/wAJSfhUPghof2b/AITo/CT7J/Y3/CxpP+EX/t8v/bXiAabsvwD8gP8Aghn/AMEM/wDh9B/w1CT+1D/wzYf2bP8AhSfP/Ckx8Yf+E0/4XCPizj5R8Wfhf/YH/CPj4Xeut/21/bv/ADDjpxa/APA/2TvgYP2X/wDgvh+zT+zSPFX/AAnA/Z4/4K+fBz4Gf8Jt/Yn/AAjf/CXn4S/tneH/AAAPFH/COnU9e/sA6/8A2AdX/sca5rv9jm8Ww/trUDF9tlAP9nSgD+QH/gqL/wAHWH/Dtn9ur45fsVn9g4fGk/BYfDI/8LK/4agPw5PiM/Eb4PeAfix/yJ3/AAzt49/sn+yv+E7/AOEdyPE+oC9On/2jmxF99hsgD+Ij/gix/wAFWf8Ahz9+1L4+/aXHwG/4aG/4Tf4BeKfgaPBI+Kg+Ev8AZv8AwknxG+Fvj4+J/wDhIf8AhXXxP+2fYx8MRpH9if2Ja/a/7aN//bKf2f8AYr8A/p5/4jnP+sXn/m63/wCSbQAf8MNf8O8v+OtP/haH/C4P+Fgf8Zy/8MA/8IV/wr/+x/Line truncated
|
||||
webUI["html/img/m3u.png"] = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAsSwAALEsBpT2WqQAABCRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjI4ODwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+Mjg4PC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjUwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6QmFnLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOC0wNy0yOFQxOTowNzozMTwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjM8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CumjVbcAAAGWSURBVGgF7VoxTgJRFGTFaGKBFnbEcABjb0fiBego7D2ABYmn8ARKRWFNQ6gx4QRQGWJJoY2VhXGdl7Dkh7Dsx3ns3yXvJ5P97L6d92bmhwYqlcWK43gK5L2enP51NP8F7pN721wPnOITZx9qG6HxI8QIZO+9XCHeL+VQKKm8QMyxb6+iCpH528AQYs58xIQWknV8mhDxCjEXWWJCC8maT55fAmOIudpUXAYhMn8dGEHMTZqYsgiR+U+BAcTcrhNTJiEy/xHQg5jOqpjD1RsBP3+id8u3P8TUoij6SuoLIwRDfWOofjLYtteyHa1UfcvvcUT1jqpGauVuHnyAdragjv/TAkley3uhj9Y5ZhDQa2+Olgmhz4IygSWibChNZ4nQFioTWCLKhtJ0lghtoTKBJaJsKE1nidAWKhNYIsqG0nSWCG2hMoElomwoTWeJ0BYqE1giyobSdJYIbaEygSWibChNZ4nQFioTWCLKhtJ0biJzmi1/guXMrpDn/OegO3bXMuAH0TtgAvwARV3y57Q34AGoJkL+AErKZ9cqbH7AAAAAAElFTkSuQmCC"
|
||||
webUI["html/js/authentication.js"] = "ZnVuY3Rpb24gY3JlYXRlRmlyc3RBY2NvdW50KGVsbSkgewogIHZhciBlcnIgPSBmYWxzZTsKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoZWxtKTsKICBjb25zb2xlLmxvZyhkaXYpOwoKICB2YXIgZm9ybSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhdXRoZW50aWNhdGlvbicpOwogIAogIGNvbnN0IHVzZXJuYW1lICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCd1c2VybmFtZScpOwogIGNvbnN0IHBhc3N3b3JkICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwYXNzd29yZCcpOwogIGNvbnN0IGNvbmZpcm0gICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdjb25maXJtJyk7CgogIHZhciBpbnB1dHMgPSBkaXYuZ2V0RWxlbWVudHNCeVRhZ05hbWUoJ0lOUFVUJykKICBjb25zb2xlLmxvZyhjb25maXJtKTsKCiAgc3dpdGNoKGNvbmZpcm0pIHsKICAgIGNhc2UgbnVsbDogYnJlYWs7CiAgICAKICAgIGRlZmF1bHQ6IAogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGlucHV0cy5sZW5ndGg7IGkrKykgewogICAgICAgIGlmIChpbnB1dHNbaV0udmFsdWUubGVuZ3RoID09IDApIHsKICAgICAgICAgIGlucHV0c1tpXS5zdHlsZS5ib3JkZXJDb2xvciA9ICdyZWQnOwogICAgICAgICAgZXJyID0gdHJ1ZQogICAgICAgIH0KICAgICAgfQoKICAgICAgc3dpdGNoKGVycikgewogICAgICAgIGNhc2UgdHJ1ZTogcmV0dXJuOyBicmVhazsKICAgICAgICBjYXNlIGZhbHNlOiAKICAgICAgICAgIGlmIChwYXNzd29yZC52YWx1ZSAhPSBjb25maXJtLnZhbHVlKSB7CiAgICAgICAgICAgIGNvbmZpcm0uc3R5bGUuYm9yZGVyQ29sb3IgPSAncmVkJzsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgfQogICAgICAgICAgYnJlYWs7CiAgICAgIH0KICB9CgoKICAKCiAgZm9ybS5zdWJtaXQoKTsKICByZXR1cm47Cn0="
|
||||
webUI["html/js/authentication_ts.js"] = "ZnVuY3Rpb24gbG9naW4oKSB7CiAgICB2YXIgZXJyID0gZmFsc2U7CiAgICB2YXIgZGF0YSA9IG5ldyBPYmplY3QoKTsKICAgIHZhciBkaXYgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiY29udGVudCIpOwogICAgdmFyIGZvcm0gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYXV0aGVudGljYXRpb24iKTsKICAgIHZhciBpbnB1dHMgPSBkaXYuZ2V0RWxlbWVudHNCeVRhZ05hbWUoIklOUFVUIik7CiAgICBjb25zb2xlLmxvZyhpbnB1dHMpOwogICAgZm9yICh2YXIgaSA9IGlucHV0cy5sZW5ndGggLSAxOyBpID49IDA7IGktLSkgewogICAgICAgIHZhciBrZXkgPSBpbnB1dHNbaV0ubmFtZTsKICAgICAgICB2YXIgdmFsdWUgPSBpbnB1dHNbaV0udmFsdWU7CiAgICAgICAgaWYgKHZhbHVlLmxlbmd0aCA9PSAwKSB7CiAgICAgICAgICAgIGlucHV0c1tpXS5zdHlsZS5ib3JkZXJDb2xvciA9ICJyZWQiOwogICAgICAgICAgICBlcnIgPSB0cnVlOwogICAgICAgIH0KICAgICAgICBkYXRhW2tleV0gPSB2YWx1ZTsKICAgIH0KICAgIGlmIChlcnIgPT0gdHJ1ZSkgewogICAgICAgIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgICAgICAgcmV0dXJuOwogICAgfQogICAgaWYgKGRhdGEuaGFzT3duUHJvcGVydHkoImNvbmZpcm0iKSkgewogICAgICAgIGlmIChkYXRhWyJjb25maXJtIl0gIT0gZGF0YVsicGFzc3dvcmQiXSkgewogICAgICAgICAgICBhbGVydCgic2RhZnNkIik7CiAgICAgICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwYXNzd29yZCcpLnN0eWxlLmJvcmRlckNvbG9yID0gInJlZCI7CiAgICAgICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdjb25maXJtJykuc3R5bGUuYm9yZGVyQ29sb3IgPSAicmVkIjsKICAgICAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImVyciIpLmlubmVySFRNTCA9ICJ7ey5hY2NvdW50LmZhaWxlZH19IjsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgIH0KICAgIGNvbnNvbGUubG9nKGRhdGEpOwogICAgZm9ybS5zdWJtaXQoKTsKfQo="
|
||||
webUI["html/js/base.js"] = "dmFyIGNvbmZpZyAgICAgICAgPSBuZXcgT2JqZWN0KCk7CnZhciBtZW51ICAgICAgICAgID0gbmV3IE9iamVjdCgpOwp2YXIgc3ViTWVudSAgICAgICA9IG5ldyBPYmplY3QoKTsKdmFyIGFjdGl2ZVN0cmVhbXMgPSBuZXcgT2JqZWN0KCk7CnZhciB4RVBHICAgICAgICAgID0gbmV3IE9iamVjdCgpOwp2YXIgdXNlcnMgICAgICAgICA9IG5ldyBPYmplY3QoKTsKdmFyIGxvZyAgICAgICAgICAgPSBuZXcgT2JqZWN0KCk7CnZhciB1bmRvICAgICAgICAgID0gbmV3IE9iamVjdCgpOwp2YXIgd2ViU29ja2V0cyAgICA9IHRydWU7CnZhciBjbG9zZUxvZywgdmVyc2lvbiwgYWN0aXZlTWVudTsKdmFyIGNvbHVtblRvU29ydCAgPSAwCgoKaWYgKHdpbmRvdy5XZWJTb2NrZXQgPT09IHVuZGVmaW5lZCkgewogIGFsZXJ0KCJZb3VyIGJyb3dzZXIgZG9lcyBub3Qgc3VwcG9ydCBXZWJTb2NrZXRzIik7CiAgd2ViU29ja2V0cyA9IGZhbHNlOwp9IAoKZnVuY3Rpb24gcGFnZVJlYWR5KCkgewogIHZhciBkYXRhID0gbmV3IE9iamVjdCgpOwogIGRhdGFbImNtZCJdID0gImdldFNlcnZlckNvbmZpZyI7CiAgeFRlVmUoZGF0YSk7CiAgLy9zaG93TG9hZGluZ1NjcmVlbihmYWxzZSk7CgogIHZhciByZXNpemVIYW5kbGUgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgib3BlblN0cmVhbXMiKTsKICB2YXIgYm94ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoIm15U3RyZWFtc0JveCIpOwogIHJlc2l6ZUhhbmRsZS5hZGRFdmVudExpc3RlbmVyKCJtb3VzZWRvd24iLCBpbml0aWFsaXNlUmVzaXplLCBmYWxzZSk7CgogIGZ1bmN0aW9uIGluaXRpYWxpc2VSZXNpemUoZSkgewogICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoIm1vdXNlbW92ZSIsIHN0YXJ0UmVzaXppbmcsIGZhbHNlKTsKICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJtb3VzZXVwIiwgc3RvcFJlc2l6aW5nLCBmYWxzZSk7CiAgfQoKICBmdW5jdGlvbiBzdGFydFJlc2l6aW5nKGUpIHsKICAgIGJveC5zdHlsZS5oZWlnaHQgPSAoZS5jbGllbnRZIC0gYm94Lm9mZnNldFRvcCkgKyAicHgiOwogICAgICAKICAgIHZhciBlbG0gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYWxsU3RyZWFtcyIpOwogICAgaWYgKGUuY2xpZW50WSA+IDEyMCkgewogICAgICBlbG0uY2xhc3NOYW1lID0gInZpc2libGUiOyAKICAgIH0gZWxzZSB7CiAgICAgIGVsbS5jbGFzc05hbWUgPSAibm90VmlzaWJsZSI7IAogICAgfQoKICAgIGNhbGN1bGF0ZVdyYXBwZXJIZWlnaHQoKTsKCiAgfQogIGZ1bmN0aW9uIHN0b3BSZXNpemluZyhlKSB7CiAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdtb3VzZW1vdmUnLCBzdGFydFJlc2l6aW5nLCBmYWxzZSk7CiAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdtb3VzZXVwJywgc3RvcFJlc2l6aW5nLCBmYWxzZSk7CiAgICAgIGNhbGN1bGF0ZVdyYXBwZXJIZWlnaHQoKTsKICB9CgogIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJyZXNpemUiLCBmdW5jdGlvbigpewogICAgY2FsY3VsYXRlV3JhcHBlckhlaWdodCgpOwogIH0sIHRydWUpOwp9CgoKZnVuY3Rpb24gZ2V0T2JqS2V5cyhvYmopIHsKICB2YXIga2V5cyA9IG5ldyBBcnJheSgpOwoKICBmb3IgKHZhciBpIGluIG9iaikgewogICAgaWYgKG9iai5oYXNPd25Qcm9wZXJ0eShpKSkgewogICAgICBrZXlzLnB1c2goaSk7CiAgICB9CiAgfQoKICByZXR1cm4ga2V5czsKfQoKCmZ1bmN0aW9uIGNyZWF0ZUVsZW1lbnQoaXRlbSkgewogIC8vY29uc29sZS5sb2coaXRlbSk7CiAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGl0ZW1bIl9lbGVtZW50Il0pOwogIGlmIChpdGVtLmhhc093blByb3BlcnR5KCJfdGV4dCIpKSB7CiAgICAvL2VsZW1lbnQuaW5uZXJIVE1MID0gIjxwPiIgKyBpdGVtWyJfdGV4dCJdICsgIjwvcD4iOwogICAgZWxlbWVudC5pbm5lckhUTUwgPSBpdGVtWyJfdGV4dCJdOwogIH0KCiAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKGl0ZW0pOwogIGZvciAodmFyIGkgPSAwOyBpIDwga2V5cy5sZW5ndGg7IGkrKykgewogICAgaWYgKGtleXNbaV0uY2hhckF0KDApICE9ICJfIikgewogICAgICAvL2NvbnNvbGUubG9nKGtleXNbaV0sIGl0ZW1ba2V5c1tpXV0pOwogICAgICBlbGVtZW50LnNldEF0dHJpYnV0ZShrZXlzW2ldLCBpdGVtW2tleXNbaV1dKTsKICAgIH0KICB9CgogIC8vY29uc29sZS5sb2coZWxlbWVudCk7CiAgcmV0dXJuIGVsZW1lbnQ7Cn0KCmZ1bmN0aW9uIG1vZGlmeU9wdGlvbihpZCwgb3B0aW9ucywgdmFsdWVzKSB7CiAgdmFyIHNlbGVjdCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKTsKICBzZWxlY3QuaW5uZXJIVE1MID0gIiI7CgogIGZvciAodmFyIGkgPSAwOyBpIDwgb3B0aW9ucy5sZW5ndGg7IGkrKykgewogIAogICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJPUFRJT04iKQogIAogICAgZWxlbWVudC52YWx1ZSA9IHZhbHVlc1tpXTsKICAgIGVsZW1lbnQuaW5uZXJIVE1MID0gb3B0aW9uc1tpXTsKCiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChpZCkuYXBwZW5kQ2hpbGQoZWxlbWVudCk7CiAgCiAgfQoKfQoKCmZ1bmN0aW9uIHN0YXJ0V2ViU29ja2V0KCkgewogIGlmICh3ZWJTb2NrZXRzID09IGZhbHNlKSB7CiAgICByZXR1cm47CiAgfQoKICAvL3dzLnNlbmQoJ3siY21kIjogImdldFNlcnZlckNvbmZpZzEifScpOwoKfQoKZnVuY3Rpb24gY2hlY2tFcnIob2JqKSB7CiAgLy9hbGVydChvYmpbImVyciJdKQogIC8vc2NyZWVuTG9nKG9ialsiZXJyIl0sICJlcnJvciIpCiAgY29uc29sZS5sb2cob2JqKTsKICB2YXIgbmV3T2JqID0gbmV3IE9iamVjdCgpOwogIHZhciBuZXdFcnIgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RXJyWyJrZXkiXSAgID0gIkVycm9yIjsKICBuZXdFcnJbInZhbHVlIl0gPSBvYmpbImVyciJdOwogIG5ld0VyclsidHlwZSJdICA9ICJlcnJvciI7CgogIG5ld09ialswXSA9IG5ld0VycgogIHNob3dMb2cobmV3T2JqKTsKICByZXR1cm4KfQoKZnVuY3Rpb24gc2NyZWVuTG9nKG1zZywgbXNnVHlwZSwgc2hvdykgewogIHJldHVybgogIGNsZWFyVGltZW91dChjbG9zZUxvZykKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNjcmVlbkxvZyIpOwogIHZhciBuZXdNc2cgPSBuZXcgT2JqZWN0KCk7CiAgCiAgbmV3TXNnWyJfZWxlbWVudCJdID0gIlAiOwogIAogIHN3aXRjaChtc2dUeXBlKSB7CiAgICBjYXNlICJlcnJvciI6ICAgbmV3TXNnWyJjbGFzcyJdID0gImVycm9yTXNnIjsgYnJlYWs7CiAgICBjYXNlICJ3YXJuaW5nIjogbmV3TXNnWyJjbGFzcyJdID0gIndhcm5pbmdNc2ciOyBicmVhazsKICAgIC8vZGVmYXVsdDogICAgICBuZXdNc2dbImNsYXNzIl0gPSAiaW5mb01zZyIKICB9CgogIG5ld01zZ1siX3RleHQiXSA9IG1zZzsKCiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3TXNnKSk7CgogIGRpdi5zY3JvbGxUb3AgPSBkaXYuc2Nyb2xsSGVpZ2h0OwoKICBpZiAoc2hvdyA9PSBmYWxzZSkgewogICAgcmV0dXJuOwogIH0KCiAgZGl2LmNsYXNzTmFtZSA9ICIiCiAgY2xvc2VMb2cgPSBzZXRUaW1lb3V0KGNsb3NlU2NyZWVuTG9nLCAxMDAwMCk7Cn0KCgpmdW5jdGlvbiBjbG9zZVNjcmVlbkxvZygpIHsKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNjcmVlbkxvZyIpOwogIGRpdi5jbGFzc05hbWUgPSAic2NyZWVuTG9nSGlkZGVuIgp9CgpmdW5jdGlvbiBzaLine truncated
|
||||
webUI["html/js/base_ts.js"] = "dmFyIFNFUlZFUiA9IG5ldyBPYmplY3QoKTsKdmFyIEJVTEtfRURJVCA9IGZhbHNlOwp2YXIgQ09MVU1OX1RPX1NPUlQ7CnZhciBTRUFSQ0hfTUFQUElORyA9IG5ldyBPYmplY3QoKTsKdmFyIFVORE8gPSBuZXcgT2JqZWN0KCk7CnZhciBTRVJWRVJfQ09OTkVDVElPTiA9IGZhbHNlOwp2YXIgV1NfQVZBSUxBQkxFID0gZmFsc2U7Ci8vIE1lbsO8CnZhciBtZW51SXRlbXMgPSBuZXcgQXJyYXkoKTsKbWVudUl0ZW1zLnB1c2gobmV3IE1haW5NZW51SXRlbSgicGxheWxpc3QiLCAie3subWFpbk1lbnUuaXRlbS5wbGF5bGlzdH19IiwgIm0zdS5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUucGxheWxpc3R9fSIpKTsKLy9tZW51SXRlbXMucHVzaChuZXcgTWFpbk1lbnVJdGVtKCJwbXNJRCIsICJ7ey5tYWluTWVudS5pdGVtLnBtc0lEfX0iLCAibnVtYmVyLnBuZyIsICJ7ey5tYWluTWVudS5oZWFkbGluZS5wbXNJRH19IikpCm1lbnVJdGVtcy5wdXNoKG5ldyBNYWluTWVudUl0ZW0oImZpbHRlciIsICJ7ey5tYWluTWVudS5pdGVtLmZpbHRlcn19IiwgImZpbHRlci5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUuZmlsdGVyfX0iKSk7Cm1lbnVJdGVtcy5wdXNoKG5ldyBNYWluTWVudUl0ZW0oInhtbHR2IiwgInt7Lm1haW5NZW51Lml0ZW0ueG1sdHZ9fSIsICJ4bWx0di5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUueG1sdHZ9fSIpKTsKbWVudUl0ZW1zLnB1c2gobmV3IE1haW5NZW51SXRlbSgibWFwcGluZyIsICJ7ey5tYWluTWVudS5pdGVtLm1hcHBpbmd9fSIsICJtYXBwaW5nLnBuZyIsICJ7ey5tYWluTWVudS5oZWFkbGluZS5tYXBwaW5nfX0iKSk7Cm1lbnVJdGVtcy5wdXNoKG5ldyBNYWluTWVudUl0ZW0oInVzZXJzIiwgInt7Lm1haW5NZW51Lml0ZW0udXNlcnN9fSIsICJ1c2Vycy5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUudXNlcnN9fSIpKTsKbWVudUl0ZW1zLnB1c2gobmV3IE1haW5NZW51SXRlbSgic2V0dGluZ3MiLCAie3subWFpbk1lbnUuaXRlbS5zZXR0aW5nc319IiwgInNldHRpbmdzLnBuZyIsICJ7ey5tYWluTWVudS5oZWFkbGluZS5zZXR0aW5nc319IikpOwptZW51SXRlbXMucHVzaChuZXcgTWFpbk1lbnVJdGVtKCJsb2ciLCAie3subWFpbk1lbnUuaXRlbS5sb2d9fSIsICJsb2cucG5nIiwgInt7Lm1haW5NZW51LmhlYWRsaW5lLmxvZ319IikpOwptZW51SXRlbXMucHVzaChuZXcgTWFpbk1lbnVJdGVtKCJsb2dvdXQiLCAie3subWFpbk1lbnUuaXRlbS5sb2dvdXR9fSIsICJsb2dvdXQucG5nIiwgInt7Lm1haW5NZW51LmhlYWRsaW5lLmxvZ291dH19IikpOwovLyBLYXRlZ29yaWVuIGbDvHIgZGllIEVpbnN0ZWxsdW5nZW4KdmFyIHNldHRpbmdzQ2F0ZWdvcnkgPSBuZXcgQXJyYXkoKTsKc2V0dGluZ3NDYXRlZ29yeS5wdXNoKG5ldyBTZXR0aW5nc0NhdGVnb3J5SXRlbSgie3suc2V0dGluZ3MuY2F0ZWdvcnkuZ2VuZXJhbH19IiwgInh0ZXZlQXV0b1VwZGF0ZSx0dW5lcixlcGdTb3VyY2UsYXBpIikpOwpzZXR0aW5nc0NhdGVnb3J5LnB1c2gobmV3IFNldHRpbmdzQ2F0ZWdvcnlJdGVtKCJ7ey5zZXR0aW5ncy5jYXRlZ29yeS5maWxlc319IiwgInVwZGF0ZSxmaWxlcy51cGRhdGUsdGVtcC5wYXRoLGNhY2hlLmltYWdlcyx4ZXBnLnJlcGxhY2UubWlzc2luZy5pbWFnZXMiKSk7CnNldHRpbmdzQ2F0ZWdvcnkucHVzaChuZXcgU2V0dGluZ3NDYXRlZ29yeUl0ZW0oInt7LnNldHRpbmdzLmNhdGVnb3J5LnN0cmVhbWluZ319IiwgImJ1ZmZlcixidWZmZXIuc2l6ZS5rYixidWZmZXIudGltZW91dCx1c2VyLmFnZW50IikpOwpzZXR0aW5nc0NhdGVnb3J5LnB1c2gobmV3IFNldHRpbmdzQ2F0ZWdvcnlJdGVtKCJ7ey5zZXR0aW5ncy5jYXRlZ29yeS5iYWNrdXB9fSIsICJiYWNrdXAucGF0aCxiYWNrdXAua2VlcCIpKTsKc2V0dGluZ3NDYXRlZ29yeS5wdXNoKG5ldyBTZXR0aW5nc0NhdGVnb3J5SXRlbSgie3suc2V0dGluZ3MuY2F0ZWdvcnkuYXV0aGVudGljYXRpb259fSIsICJhdXRoZW50aWNhdGlvbi53ZWIsYXV0aGVudGljYXRpb24ucG1zLGF1dGhlbnRpY2F0aW9uLm0zdSxhdXRoZW50aWNhdGlvbi54bWwsYXV0aGVudGljYXRpb24uYXBpIikpOwpmdW5jdGlvbiBzaG93UG9wVXBFbGVtZW50KGVsbSkgewogICAgdmFyIGFsbEVsZW1lbnRzID0gbmV3IEFycmF5KCJwb3B1cC1jdXN0b20iKTsKICAgIGZvciAodmFyIGkgPSAwOyBpIDwgYWxsRWxlbWVudHMubGVuZ3RoOyBpKyspIHsKICAgICAgICBzaG93RWxlbWVudChhbGxFbGVtZW50c1tpXSwgZmFsc2UpOwogICAgfQogICAgc2hvd0VsZW1lbnQoZWxtLCB0cnVlKTsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gKCkgewogICAgICAgIHNob3dFbGVtZW50KCJwb3B1cCIsIHRydWUpOwogICAgfSwgMTApOwogICAgcmV0dXJuOwp9CmZ1bmN0aW9uIHNob3dFbGVtZW50KGVsbUlELCB0eXBlKSB7CiAgICB2YXIgY3NzQ2xhc3M7CiAgICBzd2l0Y2ggKHR5cGUpIHsKICAgICAgICBjYXNlIHRydWU6CiAgICAgICAgICAgIGNzc0NsYXNzID0gImJsb2NrIjsKICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgY2FzZSBmYWxzZToKICAgICAgICAgICAgY3NzQ2xhc3MgPSAibm9uZSI7CiAgICAgICAgICAgIGJyZWFrOwogICAgfQogICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoZWxtSUQpLmNsYXNzTmFtZSA9IGNzc0NsYXNzOwp9CmZ1bmN0aW9uIGNoYW5nZUJ1dHRvbkFjdGlvbihlbGVtZW50LCBidXR0b25JRCwgYXR0cmlidXRlKSB7CiAgICB2YXIgdmFsdWUgPSBlbGVtZW50Lm9wdGlvbnNbZWxlbWVudC5zZWxlY3RlZEluZGV4XS52YWx1ZTsKICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGJ1dHRvbklEKS5zZXRBdHRyaWJ1dGUoYXR0cmlidXRlLCB2YWx1ZSk7Cn0KZnVuY3Rpb24gZ2V0TG9jYWxEYXRhKGRhdGFUeXBlLCBpZCkgewogICAgdmFyIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgICBzd2l0Y2ggKGRhdGFUeXBlKSB7CiAgICAgICAgY2FzZSAibTN1IjoKICAgICAgICAgICAgZGF0YSA9IFNFUlZFUlsic2V0dGluZ3MiXVsiZmlsZXMiXVtkYXRhVHlwZV1baWRdOwogICAgICAgICAgICBicmVhazsKICAgICAgICBjYXNlICJoZGhyIjoKICAgICAgICAgICAgZGF0YSA9IFNFUlZFUlsic2V0dGluZ3MiXVsiZmlsZXMiXVtkYXRhVHlwZV1baWRdOwogICAgICAgICAgICBicmVhazsKICAgICAgICBjYXNlICJmaWx0ZXIiOgogICAgICAgIGNhc2UgImN1c3RvbS1maWx0ZXIiOgogICAgICAgIGNhc2UgImdyb3VwLXRpdGxlIjoKICAgICAgICAgICAgaWYgKGlkID09IC0xKSB7CiAgICAgICAgICAgICAgICBkYXRhWyJhY3RpdmUiXSA9IHRydWU7CiAgICAgICAgICAgICAgICBkYXRhWyJjYXNlU2Vuc2l0aXZlIl0gPSBmYWxzZTsKICAgICAgICAgICAgICAgIGRhdGFbImRlc2NyaXB0aW9uIl0gPSAiIjsKICAgICAgICAgICAgICAgIGRhdGFbImV4Y2x1ZGUiXSA9ICIiOwogICAgICAgICAgICAgICAgZGF0YVsiZmlsdGVyIl0gPSAiIjsKICAgICAgICAgICAgICAgIGRhdGFbImluY2x1ZGUiXSA9ICIiOwogICAgICAgICAgICAgICAgZGF0YVsibmFtZSJdID0gIiI7CiAgICAgICAgICAgICAgICBkYXRhWyJ0eXBlIl0gPSAiZ3JvdXAtdGl0bGUiOwogICAgICAgICAgICAgICAgU0VSVkVSWyJzZXR0aW5ncyJdWyJmaWx0ZXIiXVtpZF0gPSBkYXRhOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGRhdGEgPSBTRVJWRVJbInNldHRpbmdzIl1bImZpbHRlciJdW2lkXTsKICAgICAgICAgICAgYnJlYWs7CiLine truncated
|
||||
webUI["html/js/data.js"] = "ZnVuY3Rpb24gc2hvd0NvbmZpZyhvYmopIHsKICBjb25maWcgPSBvYmo7CiAgLy9zZXRNZW51SXRlbSgpOwogIGNyZWF0ZU1lbnUoKTsKICAvL2RvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJwYWdlIikuY2xhc3NOYW1lID0gIiI7Cn0KCnNob3dNeVN0cmVhbXMKCmZ1bmN0aW9uIHNob3dNeVN0cmVhbXMoYWxsU3RyZWFtc09iaikgewoKICB2YXIgc3RyZWFtVHlwZUtleXMgPSBnZXRPYmpLZXlzKGFsbFN0cmVhbXNPYmopCgogIGZvciAodmFyIHMgPSAwOyBzIDwgc3RyZWFtVHlwZUtleXMubGVuZ3RoOyBzKyspIHsKICAgIHZhciBzdHJlYW1UeXBlID0gc3RyZWFtVHlwZUtleXNbc107CiAgICB2YXIgb2JqID0gbmV3IE9iamVjdCgpOwogICAgb2JqID0gYWxsU3RyZWFtc09ialtzdHJlYW1UeXBlXTsKICAgIHN3aXRjaChzdHJlYW1UeXBlKSB7CiAgICAgIGNhc2UgImFjdGl2ZVN0cmVhbXMiOiBhY3RpdmVTdHJlYW1zICAgICA9IG9iajsgYnJlYWs7CiAgICB9CgogICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoc3RyZWFtVHlwZSkuaW5uZXJIVE1MID0gIiI7CgogICAgdmFyIHN0cmVhbXNPYmogICAgPSBuZXcgT2JqZWN0KCk7CiAgICB2YXIgc3RyZWFtc05hbWVzICA9IG5ldyBBcnJheSgpOwogICAgCiAgICB2YXIga2V5cyA9IGdldE9iaktleXMob2JqKQogICAgCiAgICAvLyBDcmVhdGUgT2JqZWN0IChzdHJlYW1zT2JqKSBmb3IgdGhlIHN0cmVhbXMgYW5kIHNvcnQgYnkgbmFtZSAoc3RyZWFtc05hbWVzKQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBrZXlzLmxlbmd0aDsgaSsrKSB7CiAgICAgIHZhciBuYW1lICAgID0gb2JqW2tleXNbaV1dWyJuYW1lIl07CiAgICAgIHZhciB0bXAgPSBuZXcgT2JqZWN0KCk7CiAgICAgIHZhciBzdHJlYW1LZXkgPSBnZXRPYmpLZXlzKG9ialtrZXlzW2ldXSk7CgogICAgICBmb3IgKHZhciBqID0gMDsgaiA8IHN0cmVhbUtleS5sZW5ndGg7IGorKykgewogICAgICAgIHRtcFtzdHJlYW1LZXlbal1dID0gb2JqW2tleXNbaV1dW3N0cmVhbUtleVtqXV07ICAgICAgCiAgICAgIH0KCiAgICAgIHN0cmVhbXNPYmpbbmFtZV0gPSB0bXA7CiAgICAgIHN0cmVhbXNOYW1lcy5wdXNoKG5hbWUpCiAgICB9CgogICAgc3RyZWFtc05hbWVzLnNvcnQoKTsKICAgIAogICAgLy8gQ3JlYXRlIFRhYmxlIGZvciBhY3RpdmVTdHJlYW1zCiAgICB2YXIgdGFibGUgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChzdHJlYW1UeXBlKTsKCiAgICBmb3IgKHZhciBpID0gMDsgaSA8IHN0cmVhbXNOYW1lcy5sZW5ndGg7IGkrKykgewogICAgICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgICAgIG5ld0VudHJ5WyJfZWxlbWVudCJdID0gIlRSIjsKICAgICAgdGFibGUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwogICAgICB2YXIgbGluZSA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICAgIHZhciB0bXAgPSBzdHJlYW1zT2JqW3N0cmVhbXNOYW1lc1tpXV0KICAgICAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKHRtcCkKCiAgICAgIHZhciBuZXdLZXkgPSBuZXcgT2JqZWN0KCkKICAgICAgbmV3S2V5WyJfZWxlbWVudCJdICA9ICJURCI7CiAgICAgIC8vbmV3S2V5WyJfdGV4dCJdICAgICA9IHN0cmVhbXNOYW1lc1tpXTsKICAgICAgc3dpdGNoKHN0cmVhbVR5cGUpIHsKICAgICAgICBjYXNlICJhY3RpdmVTdHJlYW1zIjogbmV3S2V5WyJfdGV4dCJdICAgICA9ICJDaGFubmVsICgrKToiOyBicmVhazsKICAgICAgICBjYXNlICJpbmFjdGl2ZVN0cmVhbXMiOiBuZXdLZXlbIl90ZXh0Il0gICA9ICJDaGFubmVsICgtKToiOyBicmVhazsKICAgICAgfQogICAgICAKICAgICAgbmV3S2V5WyJjbGFzcyJdICAgICA9ICJ0ZEtleSI7CiAgICAgIGNvbnNvbGUubG9nKCk7CgoKICAgICAgdmFyIG5ld1ZhbCA9IG5ldyBPYmplY3QoKQogICAgICBuZXdWYWxbIl9lbGVtZW50Il0gID0gIlREIjsKICAgICAgbmV3VmFsWyJfdGV4dCJdICAgICA9IHN0cmVhbXNOYW1lc1tpXTsKICAgICAgbmV3VmFsWyJjbGFzcyJdICAgICA9ICJ0ZFZhbCI7CiAgICAgIC8vbmV3VmFsWyJfdGV4dCJdICAgICA9IHZhbHVlOwogICAgICAKICAgICAgbGluZS5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0tleSkpOwogICAgICBsaW5lLmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VmFsKSk7CiAgICAgIAogICAgfQoKICB9CgogIHJldHVybgp9CgpmdW5jdGlvbiBzaG93QWN0aXZlU3RyZWFtcyhvYmopIHsKICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYWN0aXZlU3RyZWFtcyIpLmlubmVySFRNTCA9ICIiOwogIGFjdGl2ZVN0cmVhbXMgICAgID0gb2JqOwogIHZhciBzdHJlYW1zT2JqICAgID0gbmV3IE9iamVjdCgpOwogIHZhciBzdHJlYW1zTmFtZXMgID0gbmV3IEFycmF5KCk7CiAgCiAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKG9iaikKICAKICAvLyBDcmVhdGUgT2JqZWN0IChzdHJlYW1zT2JqKSBmb3IgdGhlIHN0cmVhbXMgYW5kIHNvcnQgYnkgbmFtZSAoc3RyZWFtc05hbWVzKQogIGZvciAodmFyIGkgPSAwOyBpIDwga2V5cy5sZW5ndGg7IGkrKykgewogICAgdmFyIG5hbWUgICAgPSBvYmpba2V5c1tpXV1bIm5hbWUiXTsKICAgIHZhciB0bXAgPSBuZXcgT2JqZWN0KCk7CiAgICB2YXIgc3RyZWFtS2V5ID0gZ2V0T2JqS2V5cyhvYmpba2V5c1tpXV0pOwoKICAgIGZvciAodmFyIGogPSAwOyBqIDwgc3RyZWFtS2V5Lmxlbmd0aDsgaisrKSB7CiAgICAgIHRtcFtzdHJlYW1LZXlbal1dID0gb2JqW2tleXNbaV1dW3N0cmVhbUtleVtqXV07ICAgICAgCiAgICB9CgogICAgc3RyZWFtc09ialtuYW1lXSA9IHRtcDsKICAgIHN0cmVhbXNOYW1lcy5wdXNoKG5hbWUpCiAgfQoKICBzdHJlYW1zTmFtZXMuc29ydCgpOwogIAogIC8vIENyZWF0ZSBUYWJsZSBmb3IgYWN0aXZlU3RyZWFtcwogIHZhciB0YWJsZSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJhY3RpdmVTdHJlYW1zIik7CgogIGZvciAodmFyIGkgPSAwOyBpIDwgc3RyZWFtc05hbWVzLmxlbmd0aDsgaSsrKSB7CiAgICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdFbnRyeVsiX2VsZW1lbnQiXSA9ICJUUiI7CiAgICB0YWJsZS5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0VudHJ5KSk7CiAgICB2YXIgbGluZSA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICB2YXIgdG1wID0gc3RyZWFtc09ialtzdHJlYW1zTmFtZXNbaV1dCiAgICB2YXIga2V5cyA9IGdldE9iaktleXModG1wKQoKICAgIHZhciBuZXdLZXkgPSBuZXcgT2JqZWN0KCkKICAgIG5ld0tleVsiX2VsZW1lbnQiXSAgPSAiVEQiOwogICAgLy9uZXdLZXlbIl90ZXh0Il0gICAgID0gc3RyZWFtc05hbWVzW2ldOwogICAgbmV3S2V5WyJfdGV4dCJdICAgICA9ICJDaGFubmVsOiI7CiAgICBuZXdLZXlbImNsYXNzIl0gICAgID0gInRkS2V5IjsKICAgIGNvbnNvbGUubG9nKCk7CgoKICAgIHZhciBuZXdWYWwgPSBuZXcgT2JqZWN0KCkKICAgIG5ld1ZhbFsiX2VsZW1lbnQiXSAgPSAiVEQiOwogICAgbmV3VmFsWyJfdGV4dCJdICAgICA9IHN0cmVhbXNOYW1lc1tpXTsKICAgIG5ld1ZhbFsiY2xhc3MiXSAgICAgPSAidGRWYWwiOwogICAgLy9uZXdWYWxbIl90ZXh0Il0gICAgID0gdmFsdWU7CiAgICAKICAgIGxpbmUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdLZXkpKTsKICAgIGxpbmUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdWYWwpKTsKICAgIAogIH0KCn0KCmZ1bmN0aW9uIHBhcnNlTG9ncyhvYmopIHsKILine truncated
|
||||
webUI["html/js/users.js"] = "ZnVuY3Rpb24gb3BlblVzZXJzKGVsbSkgewogIGNvbG9tblNvcnQgPSAwOwoKICB2YXIgbmV3RGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CiAgCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJJTlBVVCI7CiAgbmV3RW50cnlbInR5cGUiXSA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gPSAiTmV3IjsKICBuZXdFbnRyeVsib25jbGljayJdID0gInVzZXJEZXRhaWwoMCkiOwogIG5ld0Rpdi5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0VudHJ5KSk7CgogIHZhciBkaXYgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgic2V0dGluZ3MiKTsKCiAgIC8vIEJ1aWxkIHRhYmxlCiAgdmFyIG5ld1RhYmxlID0gbmV3IE9iamVjdCgpOwogIG5ld1RhYmxlWyJfZWxlbWVudCJdICA9ICJUQUJMRSI7CiAgbmV3VGFibGVbImlkIl0gICAgICAgID0gImlkX21hcHBpbmciOwogIG5ld1RhYmxlWyJjbGFzcyJdICAgICA9ICJ0YWJsZS1tYXBwaW5nIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdUYWJsZSkpOwoKICBzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7IAogICAgY3JlYXRlVXNlcnNUYWJsZSgpOyAKICB9LCAxMCk7Cn0KCmZ1bmN0aW9uIGNyZWF0ZVVzZXJzVGFibGUoKSB7CiAgdmFyIHRhYmxlID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlkX21hcHBpbmciKTsKICB0YWJsZS5pbm5lckhUTUwgPSAiIjsKICB2YXIgbmV3VFIgPSBuZXcgT2JqZWN0KCk7CiAgbmV3VFJbIl9lbGVtZW50Il0gPSAiVFIiOwogIG5ld1RSWyJjbGFzcyJdICAgID0gInRhYmxlLW1hcHBpbmctaGVhZGVyIjsKICB0YWJsZS5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld1RSKSk7CgogIHZhciB0ciA9IHRhYmxlLmxhc3RDaGlsZDsKICB2YXIgdHJIZWFkbGluZXMgPSBuZXcgQXJyYXkoIlVzZXJuYW1lIiwgIlBhc3N3b3JkIiwgIldFQiIsICJQTVMiLCAiTTNVIiwgIlhNTCIsICJBUEkiKQoKICBmb3IgKHZhciBpID0gMDsgaSA8IHRySGVhZGxpbmVzLmxlbmd0aDsgaSsrKSB7CiAgICB2YXIgbmV3VEQgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJURCI7CiAgICBuZXdURFsiX3RleHQiXSAgICA9IHRySGVhZGxpbmVzW2ldOwogICAgdHIuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdURCkpOwogIH0KCgogIC8vIFNvcnQgdXNlcnMKICB2YXIgdXNlcklkcyA9IGdldE9iaktleXModXNlcnMpOwoKICB2YXIgdXNlck9iaiA9IG5ldyBPYmplY3QoKTsKCiAgZm9yICh2YXIgaSA9IDA7IGkgPCB1c2VySWRzLmxlbmd0aDsgaSsrKSB7CiAgICB2YXIgdXNlcm5hbWUgPSB1c2Vyc1t1c2VySWRzW2ldXVsiZGF0YSJdWyJ1c2VybmFtZSJdOwogICAgdXNlck9ialt1c2VybmFtZV0gPSB1c2VySWRzW2ldOwogIH0KCiAgdmFyIGFsbFVzZXJzID0gZ2V0T2JqS2V5cyh1c2VyT2JqKTsKICBhbGxVc2Vycy5zb3J0KCk7CiAgLy8gLS0KCiAgZm9yICh2YXIgaSA9IDA7IGkgPCBhbGxVc2Vycy5sZW5ndGg7IGkrKykgewogICAgdmFyIHRhYmxlICAgICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJpZF9tYXBwaW5nIik7CiAgICB2YXIgdXNlcklEICAgID0gdXNlck9ialthbGxVc2Vyc1tpXV07CiAgICB2YXIgdXNlcm5hbWUgID0gYWxsVXNlcnNbaV07CiAgICB2YXIgaXRlbSAgICAgID0gdXNlcnNbdXNlcklEXVsiZGF0YSJdOwoKICAgIC8vIENyZWF0ZSBUUgogICAgdmFyIG5ld1RSID0gbmV3IE9iamVjdCgpOwogICAgbmV3VFJbIl9lbGVtZW50Il0gICAgICAgPSAiVFIiOwogICAgbmV3VFJbImNsYXNzIl0gICAgICAgICAgPSAiIjsKICAgIG5ld1RSWyJpZCJdICAgICAgICAgICAgID0gdXNlcklEOwogICAgbmV3VFJbIm9uY2xpY2siXSAgICAgICAgPSAnamF2YXNjcmlwdDogdXNlckRldGFpbCgiJyArIHVzZXJJRCArICciKTsnOwogICAgdGFibGUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdUUikpOwoKICAgIHZhciB0ciA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICAvLyBDcmVhdGUgdXNlcm5hbWUgVEQKICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgIG5ld1REWyJfZWxlbWVudCJdID0gIlAiOwogICAgbmV3VERbIl90ZXh0Il0gICAgPSB1c2VybmFtZTsKICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIHBhc3N3b3JkIFRECiAgICB2YXIgbmV3VEQgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgIG5ld1REWyJfdGV4dCJdICAgID0gIi4uLi4uIjsKICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIHdlYiBhY2Nlc3MKICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgIG5ld1REWyJfZWxlbWVudCJdID0gIlAiOwogICAgc3dpdGNoKGl0ZW1bImF1dGhlbnRpY2F0aW9uLndlYiJdKXsKICAgICAgY2FzZSB0cnVlOiBuZXdURFsiX3RleHQiXSAgICA9ICLinJMiOyBicmVhazsKICAgICAgZGVmYXVsdDogICBuZXdURFsiX3RleHQiXSAgICA9ICItIjsgYnJlYWs7CiAgICB9CiAgICBjcmVhdGVOZXdURChuZXdURCwgdHIpOwoKICAgIC8vIENyZWF0ZSBQTVMgYWNjZXNzCiAgICB2YXIgbmV3VEQgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgIHN3aXRjaChpdGVtWyJhdXRoZW50aWNhdGlvbi5wbXMiXSl7CiAgICAgIGNhc2UgdHJ1ZTogbmV3VERbIl90ZXh0Il0gICAgPSAi4pyTIjsgYnJlYWs7CiAgICAgIGRlZmF1bHQ6ICAgbmV3VERbIl90ZXh0Il0gICAgPSAiLSI7IGJyZWFrOwogICAgfQogICAgY3JlYXRlTmV3VEQobmV3VEQsIHRyKTsKCiAgICAvLyBDcmVhdGUgTTNVIGFjY2VzcwogICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgbmV3VERbIl9lbGVtZW50Il0gPSAiUCI7CiAgICBzd2l0Y2goaXRlbVsiYXV0aGVudGljYXRpb24ubTN1Il0pewogICAgICBjYXNlIHRydWU6IG5ld1REWyJfdGV4dCJdICAgID0gIuKckyI7IGJyZWFrOwogICAgICBkZWZhdWx0OiAgIG5ld1REWyJfdGV4dCJdICAgID0gIi0iOyBicmVhazsKICAgIH0KICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIFhNTFRWIGFjY2VzcwogICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgbmV3VERbIl9lbGVtZW50Il0gPSAiUCI7CiAgICBzd2l0Y2goaXRlbVsiYXV0aGVudGljYXRpb24ueG1sIl0pewogICAgICBjYXNlIHRydWU6IG5ld1REWyJfdGV4dCJdICAgID0gIuKckyI7IGJyZWFrOwogICAgICBkZWZhdWx0OiAgIG5ld1REWyJfdGV4dCJdICAgID0gIi0iOyBicmVhazsKICAgIH0KICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIEFQSSBhY2Nlc3MKICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgIG5ld1REWyJfZWxlbWVudCJdID0gIlAiOwoKICAgIHN3aXRjaChpdGVtWyJhdXRoZW50aWNhdGlvbi5hcGkiXSl7CiAgICAgIGNhc2UgdHJ1ZTogbmV3VERbIl90ZXh0Il0gICAgPSAi4pyTIjsgYnJlYWs7CiAgICAgIGRlZmF1bHQ6ICAgbmV3VERbIl90ZXh0Il0gICAgPSAiLSI7Line truncated
|
||||
|
||||
}
|
||||
|
||||
+209
-198
@@ -4,17 +4,43 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"../src/internal/authentication"
|
||||
"xteve/src/internal/authentication"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// wsUpgrader uses gorilla's default CheckOrigin: an Origin header, when
|
||||
// present, must match the request Host. That blocks cross-site websocket
|
||||
// hijacking from other pages on the LAN while still allowing non-browser
|
||||
// clients that send no Origin.
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
}
|
||||
|
||||
// wsSessionToken : session token for a websocket request. The HttpOnly
|
||||
// cookie set at login is preferred; the legacy ?Token= query parameter is
|
||||
// still accepted for older clients.
|
||||
func wsSessionToken(r *http.Request) string {
|
||||
|
||||
if c, err := r.Cookie("Token"); err == nil && len(c.Value) > 0 {
|
||||
return c.Value
|
||||
}
|
||||
|
||||
if v := r.URL.Query().Get("Token"); len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
|
||||
return "-"
|
||||
}
|
||||
|
||||
// StartWebserver : Startet den Webserver
|
||||
func StartWebserver() (err error) {
|
||||
|
||||
@@ -30,7 +56,7 @@ func StartWebserver() (err error) {
|
||||
http.HandleFunc("/api/", API)
|
||||
http.HandleFunc("/images/", Images)
|
||||
http.HandleFunc("/data_images/", DataImages)
|
||||
//http.HandleFunc("/auto/", Auto)
|
||||
http.HandleFunc("/healthz", Healthz)
|
||||
|
||||
showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port)
|
||||
|
||||
@@ -48,7 +74,17 @@ func StartWebserver() (err error) {
|
||||
|
||||
}
|
||||
|
||||
if err = http.ListenAndServe(":"+port, nil); err != nil {
|
||||
// No ReadTimeout / WriteTimeout on purpose: /stream/ responses are
|
||||
// long-lived. ReadHeaderTimeout still protects against slowloris
|
||||
// clients and IdleTimeout reaps keep-alive connections.
|
||||
var server = &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: nil,
|
||||
ReadHeaderTimeout: 15 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
if err = server.ListenAndServe(); err != nil {
|
||||
ShowError(err, 1001)
|
||||
return
|
||||
}
|
||||
@@ -80,7 +116,7 @@ func Index(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
case "/lineup.json":
|
||||
if Settings.AuthenticationPMS == true {
|
||||
if Settings.AuthenticationPMS {
|
||||
|
||||
_, err := basicAuth(r, "authentication.pms")
|
||||
if err != nil {
|
||||
@@ -113,7 +149,6 @@ func Index(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
httpStatusError(w, r, 500)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Stream : Web Server /stream/
|
||||
@@ -129,20 +164,39 @@ func Stream(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Index(streamInfo.URL, "rtsp://") != -1 || strings.Index(streamInfo.URL, "rtp://") != -1 {
|
||||
err = errors.New("RTSP and RTP streams are not supported")
|
||||
ShowError(err, 2004)
|
||||
var bufferMode = bufferModeFor(streamInfo.PlaylistID)
|
||||
|
||||
showInfo("Streaming URL:" + streamInfo.URL)
|
||||
http.Redirect(w, r, streamInfo.URL, 302)
|
||||
|
||||
showInfo("Streaming Info:URL was passed to the client")
|
||||
return
|
||||
// If an UDPxy host is set, and the stream URL is multicast (i.e. starts with 'udp://@'),
|
||||
// then streamInfo.URL needs to be rewritten to point to UDPxy.
|
||||
if Settings.UDPxy != "" && strings.HasPrefix(streamInfo.URL, "udp://@") {
|
||||
streamInfo.URL = fmt.Sprintf("http://%s/udp/%s/", Settings.UDPxy, strings.TrimPrefix(streamInfo.URL, "udp://@"))
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("Buffer:%t", Settings.Buffer))
|
||||
switch bufferMode {
|
||||
|
||||
if Settings.Buffer == true {
|
||||
case "-":
|
||||
showInfo(fmt.Sprintf("Buffer:false [%s]", bufferMode))
|
||||
|
||||
case "xteve":
|
||||
if strings.Contains(streamInfo.URL, "rtsp://") || strings.Contains(streamInfo.URL, "rtp://") {
|
||||
err = errors.New("RTSP and RTP streams are not supported")
|
||||
ShowError(err, 2004)
|
||||
|
||||
showInfo("Streaming URL:" + streamInfo.URL)
|
||||
http.Redirect(w, r, streamInfo.URL, http.StatusFound)
|
||||
|
||||
showInfo("Streaming Info:URL was passed to the client")
|
||||
return
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("Buffer:true [%s]", bufferMode))
|
||||
|
||||
default:
|
||||
showInfo(fmt.Sprintf("Buffer:true [%s]", bufferMode))
|
||||
|
||||
}
|
||||
|
||||
if bufferMode != "-" {
|
||||
showInfo(fmt.Sprintf("Buffer Size:%d KB", Settings.BufferSize))
|
||||
}
|
||||
|
||||
@@ -150,52 +204,26 @@ func Stream(w http.ResponseWriter, r *http.Request) {
|
||||
showInfo(fmt.Sprintf("Client User-Agent:%s", r.Header.Get("User-Agent")))
|
||||
|
||||
// Prüfen ob der Buffer verwendet werden soll
|
||||
switch Settings.Buffer {
|
||||
switch bufferMode {
|
||||
|
||||
case true:
|
||||
bufferingStream(streamInfo.PlaylistID, streamInfo.URL, streamInfo.Name, w, r)
|
||||
|
||||
case false:
|
||||
case "-":
|
||||
showInfo("Streaming URL:" + streamInfo.URL)
|
||||
http.Redirect(w, r, streamInfo.URL, 302)
|
||||
http.Redirect(w, r, streamInfo.URL, http.StatusFound)
|
||||
|
||||
showInfo("Streaming Info:URL was passed to the client.")
|
||||
showInfo("Streaming Info:xTeVe is no longer involved, the client connects directly to the streaming server.")
|
||||
|
||||
default:
|
||||
bufferingStream(streamInfo.PlaylistID, streamInfo.URL, streamInfo.Name, w, r)
|
||||
|
||||
}
|
||||
|
||||
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/
|
||||
func xTeVe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var requestType, groupTitle, file, content string
|
||||
var requestType, groupTitle, file, content, contentType string
|
||||
var err error
|
||||
var path = strings.TrimPrefix(r.URL.Path, "/")
|
||||
var groups = []string{}
|
||||
@@ -223,7 +251,7 @@ func xTeVe(w http.ResponseWriter, r *http.Request) {
|
||||
requestType = "m3u"
|
||||
groupTitle = r.URL.Query().Get("group-title")
|
||||
|
||||
if System.Dev == false {
|
||||
if !System.Dev {
|
||||
// false: Dateiname wird im Header gesetzt
|
||||
// true: M3U wird direkt im Browser angezeigt
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(path))
|
||||
@@ -248,11 +276,17 @@ func xTeVe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
contentType = http.DetectContentType([]byte(content))
|
||||
if strings.Contains(strings.ToLower(contentType), "xml") {
|
||||
contentType = "application/xml; charset=utf-8"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if err == nil {
|
||||
w.Write([]byte(content))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Images : Image Cache /images/
|
||||
@@ -272,7 +306,6 @@ func Images(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write(content)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DataImages : Image Pfad für Logos / Bilder die hochgeladen wurden /data_images/
|
||||
@@ -292,56 +325,49 @@ func DataImages(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write(content)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WS : Web Sockets /ws/
|
||||
func WS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var request RequestStruct
|
||||
var response ResponseStruct
|
||||
response.Status = true
|
||||
|
||||
var newToken string
|
||||
|
||||
if r.Header.Get("Origin") != "http://"+r.Host {
|
||||
httpStatusError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
|
||||
// Upgrade writes its own error response (e.g. 403 for a bad Origin).
|
||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
setGlobalDomain(r.Host)
|
||||
|
||||
// One connection serves any number of commands, one at a time, until the
|
||||
// client goes away or a write fails.
|
||||
for {
|
||||
|
||||
// Fresh structs per command: a failed command must not leak its
|
||||
// Status / Error / Base64 into the next one on the same connection.
|
||||
var request RequestStruct
|
||||
var response ResponseStruct
|
||||
response.Status = true
|
||||
|
||||
err = conn.ReadJSON(&request)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if System.ConfigurationWizard == false {
|
||||
response.ID = request.ID
|
||||
|
||||
if !System.ConfigurationWizard {
|
||||
|
||||
switch Settings.AuthenticationWEB {
|
||||
|
||||
// Token Authentication
|
||||
case true:
|
||||
|
||||
var token string
|
||||
tokens, ok := r.URL.Query()["Token"]
|
||||
|
||||
if !ok || len(tokens[0]) < 1 {
|
||||
token = "-"
|
||||
} else {
|
||||
token = tokens[0]
|
||||
}
|
||||
|
||||
newToken, err = tokenAuthentication(token)
|
||||
newToken, err = tokenAuthentication(wsSessionToken(r))
|
||||
if err != nil {
|
||||
|
||||
response.Status = false
|
||||
@@ -372,11 +398,9 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
response = setDefaultResponseData(response, false)
|
||||
if err = conn.WriteJSON(response); err != nil {
|
||||
ShowError(err, 1022)
|
||||
} else {
|
||||
return
|
||||
break
|
||||
}
|
||||
return
|
||||
continue
|
||||
|
||||
case "loadFiles":
|
||||
//response.Response = Settings.Files
|
||||
@@ -389,7 +413,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
response.OpenMenu = strconv.Itoa(indexOfString("settings", System.WEB.Menu))
|
||||
|
||||
if Settings.AuthenticationWEB == true && authenticationUpdate == false {
|
||||
if Settings.AuthenticationWEB && !authenticationUpdate {
|
||||
response.Reload = true
|
||||
}
|
||||
|
||||
@@ -453,9 +477,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
case "resetLogs":
|
||||
WebScreenLog.Log = make([]string, 0)
|
||||
WebScreenLog.Errors = 0
|
||||
WebScreenLog.Warnings = 0
|
||||
resetWebScreenLog()
|
||||
response.OpenMenu = strconv.Itoa(indexOfString("log", System.WEB.Menu))
|
||||
|
||||
case "xteveBackup":
|
||||
@@ -466,9 +488,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
case "xteveRestore":
|
||||
WebScreenLog.Log = make([]string, 0)
|
||||
WebScreenLog.Errors = 0
|
||||
WebScreenLog.Warnings = 0
|
||||
resetWebScreenLog()
|
||||
|
||||
if len(request.Base64) > 0 {
|
||||
|
||||
@@ -530,9 +550,9 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
fmt.Println("+ + + + + + + + + + +", request.Cmd)
|
||||
|
||||
var requestMap = make(map[string]interface{}) // Debug
|
||||
var requestMap = make(map[string]any) // Debug
|
||||
_ = requestMap
|
||||
if System.Dev == true {
|
||||
if System.Dev {
|
||||
fmt.Println(mapToJSON(requestMap))
|
||||
}
|
||||
|
||||
@@ -541,86 +561,77 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
response.Status = false
|
||||
response.Error = err.Error()
|
||||
response.Settings = Settings
|
||||
response.Settings = maskSettings(Settings)
|
||||
}
|
||||
|
||||
response = setDefaultResponseData(response, true)
|
||||
if System.ConfigurationWizard == true {
|
||||
if System.ConfigurationWizard {
|
||||
response.ConfigurationWizard = System.ConfigurationWizard
|
||||
}
|
||||
|
||||
if err = conn.WriteJSON(response); err != nil {
|
||||
ShowError(err, 1022)
|
||||
} else {
|
||||
break
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Healthz : Liveness endpoint for container healthchecks and monitoring.
|
||||
// Unauthenticated on purpose; it exposes only the version and whether a scan is running.
|
||||
func Healthz(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var status = map[string]any{
|
||||
"status": "ok",
|
||||
"version": System.Version + "." + System.Build,
|
||||
"scanInProgress": System.ScanInProgress == 1,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(mapToJSON(status)))
|
||||
}
|
||||
|
||||
// Web : Web Server /web/
|
||||
func Web(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var lang = make(map[string]interface{})
|
||||
var err error
|
||||
var requestFile = strings.TrimPrefix(r.URL.Path, "/web")
|
||||
requestFile = strings.TrimPrefix(requestFile, "/")
|
||||
|
||||
var requestFile = strings.Replace(r.URL.Path, "/web", "html", -1)
|
||||
var content, contentType, file string
|
||||
// Values substituted into the HTML pages (JS and CSS are served verbatim).
|
||||
var tmpl = map[string]any{"authenticationErr": ""}
|
||||
|
||||
var language LanguageUI
|
||||
setGlobalDomain(r.Host)
|
||||
|
||||
if System.Dev == true {
|
||||
if requestFile == "" {
|
||||
|
||||
lang, err = loadJSONFileToMap(fmt.Sprintf("html/lang/%s.json", Settings.Language))
|
||||
if err != nil {
|
||||
ShowError(err, 000)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var languageFile = "html/lang/en.json"
|
||||
|
||||
if value, ok := webUI[languageFile].(string); ok {
|
||||
content = GetHTMLString(value)
|
||||
lang = jsonToMap(content)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(mapToJSON(lang)), &language)
|
||||
if err != nil {
|
||||
ShowError(err, 000)
|
||||
return
|
||||
}
|
||||
|
||||
if getFilenameFromPath(requestFile) == "html" {
|
||||
var file string
|
||||
|
||||
if System.ScanInProgress == 0 {
|
||||
|
||||
if 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch System.ConfigurationWizard {
|
||||
|
||||
case true:
|
||||
file = requestFile + "configuration.html"
|
||||
Settings.AuthenticationWEB = false
|
||||
|
||||
case false:
|
||||
file = requestFile + "index.html"
|
||||
|
||||
}
|
||||
file = "index.html"
|
||||
|
||||
if System.ScanInProgress == 1 {
|
||||
file = requestFile + "maintenance.html"
|
||||
file = "maintenance.html"
|
||||
}
|
||||
|
||||
switch Settings.AuthenticationWEB {
|
||||
// The first-run wizard is reachable without a login (there is nothing
|
||||
// to protect yet); it must not switch authentication off in Settings.
|
||||
var requireLogin = Settings.AuthenticationWEB && !System.ConfigurationWizard
|
||||
|
||||
if System.ConfigurationWizard {
|
||||
file = "configuration.html"
|
||||
}
|
||||
|
||||
switch requireLogin {
|
||||
case true:
|
||||
|
||||
var username, password, confirm string
|
||||
@@ -645,7 +656,7 @@ func Web(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// Redirect, damit die Daten aus dem Browser gelöscht werden.
|
||||
w = authentication.SetCookieToken(w, token)
|
||||
http.Redirect(w, r, "/web", 301)
|
||||
http.Redirect(w, r, "/web", http.StatusMovedPermanently)
|
||||
return
|
||||
|
||||
}
|
||||
@@ -655,33 +666,32 @@ func Web(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var token, err = authentication.UserAuthentication(username, password)
|
||||
if err != nil {
|
||||
file = requestFile + "login.html"
|
||||
lang["authenticationErr"] = language.Login.Failed
|
||||
file = "login.html"
|
||||
tmpl["authenticationErr"] = "User authentication failed"
|
||||
break
|
||||
}
|
||||
|
||||
w = authentication.SetCookieToken(w, token)
|
||||
http.Redirect(w, r, "/web", 301) // Redirect, damit die Daten aus dem Browser gelöscht werden.
|
||||
http.Redirect(w, r, "/web", http.StatusMovedPermanently) // Redirect, damit die Daten aus dem Browser gelöscht werden.
|
||||
|
||||
} else {
|
||||
w = authentication.SetCookieToken(w, "-")
|
||||
http.Redirect(w, r, "/web", 301) // Redirect, damit die Daten aus dem Browser gelöscht werden.
|
||||
http.Redirect(w, r, "/web", http.StatusMovedPermanently) // Redirect, damit die Daten aus dem Browser gelöscht werden.
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case "GET":
|
||||
lang["authenticationErr"] = ""
|
||||
_, token, err := authentication.CheckTheValidityOfTheTokenFromHTTPHeader(w, r)
|
||||
|
||||
if err != nil {
|
||||
file = requestFile + "login.html"
|
||||
file = "login.html"
|
||||
break
|
||||
}
|
||||
|
||||
err = checkAuthorizationLevel(token, "authentication.web")
|
||||
if err != nil {
|
||||
file = requestFile + "login.html"
|
||||
file = "login.html"
|
||||
break
|
||||
}
|
||||
|
||||
@@ -694,59 +704,48 @@ func Web(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(allUserData) == 0 && Settings.AuthenticationWEB == true {
|
||||
file = requestFile + "create-first-user.html"
|
||||
if len(allUserData) == 0 && Settings.AuthenticationWEB {
|
||||
file = "create-first-user.html"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
requestFile = file
|
||||
|
||||
if value, ok := webUI[requestFile]; ok {
|
||||
}
|
||||
|
||||
content = GetHTMLString(value.(string))
|
||||
content, err := readWebFile(requestFile)
|
||||
if err != nil {
|
||||
httpStatusError(w, r, 404)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == "text/plain" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(requestFile))
|
||||
}
|
||||
var contentType = getContentType(requestFile)
|
||||
|
||||
} else {
|
||||
switch contentType {
|
||||
|
||||
httpStatusError(w, r, 404)
|
||||
case "text/html":
|
||||
content = []byte(parseTemplate(string(content), tmpl))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
case "text/plain":
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(requestFile))
|
||||
|
||||
default:
|
||||
// Static asset: let the browser revalidate cheaply with an ETag.
|
||||
var etag = webFileETag(requestFile, content)
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if value, ok := webUI[requestFile].(string); ok {
|
||||
|
||||
content = GetHTMLString(value)
|
||||
contentType = getContentType(requestFile)
|
||||
|
||||
if contentType == "text/plain" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(requestFile))
|
||||
}
|
||||
|
||||
} else {
|
||||
httpStatusError(w, r, 404)
|
||||
return
|
||||
}
|
||||
|
||||
contentType = getContentType(requestFile)
|
||||
|
||||
if System.Dev == true {
|
||||
// Lokale Webserver Dateien werden geladen, nur für die Entwicklung
|
||||
content, _ = readStringFromFile(requestFile)
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", contentType)
|
||||
w.WriteHeader(200)
|
||||
|
||||
if contentType == "text/html" || contentType == "application/javascript" {
|
||||
content = parseTemplate(content, lang)
|
||||
}
|
||||
|
||||
w.Write([]byte(content))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(content)
|
||||
}
|
||||
|
||||
// API : API request /api/
|
||||
@@ -810,13 +809,12 @@ func API(w http.ResponseWriter, r *http.Request) {
|
||||
response.Status = false
|
||||
response.Error = err.Error()
|
||||
w.Write([]byte(mapToJSON(response)))
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
response.Status = true
|
||||
|
||||
if Settings.API == false {
|
||||
if !Settings.API {
|
||||
httpStatusError(w, r, 423)
|
||||
return
|
||||
}
|
||||
@@ -826,7 +824,7 @@ func API(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
b, err := io.ReadAll(r.Body)
|
||||
defer r.Body.Close()
|
||||
if err != nil {
|
||||
httpStatusError(w, r, 400)
|
||||
@@ -842,7 +840,7 @@ func API(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("content-type", "application/json")
|
||||
|
||||
if Settings.AuthenticationAPI == true {
|
||||
if Settings.AuthenticationAPI {
|
||||
var token string
|
||||
switch len(request.Token) {
|
||||
case 0:
|
||||
@@ -864,8 +862,8 @@ func API(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
default:
|
||||
token, err = tokenAuthentication(request.Token)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
responseAPIError(err)
|
||||
return
|
||||
}
|
||||
@@ -935,16 +933,31 @@ func API(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err != nil {
|
||||
responseAPIError(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(mapToJSON(response)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Download : Datei Download
|
||||
func Download(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Backups contain settings.json (Plex token) and authentication.json, so
|
||||
// the download needs the same session as the web UI that created it.
|
||||
if Settings.AuthenticationWEB && !System.ConfigurationWizard {
|
||||
|
||||
_, token, err := authentication.CheckTheValidityOfTheTokenFromHTTPHeader(w, r)
|
||||
if err == nil {
|
||||
err = checkAuthorizationLevel(token, "authentication.web")
|
||||
}
|
||||
if err != nil {
|
||||
httpStatusError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var path = r.URL.Path
|
||||
var file = System.Folder.Temp + getFilenameFromPath(path)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+getFilenameFromPath(file))
|
||||
@@ -957,7 +970,6 @@ func Download(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
os.RemoveAll(System.Folder.Temp + getFilenameFromPath(path))
|
||||
w.Write([]byte(content))
|
||||
return
|
||||
}
|
||||
|
||||
func setDefaultResponseData(response ResponseStruct, data bool) (defaults ResponseStruct) {
|
||||
@@ -973,15 +985,15 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
defaults.ClientInfo.OS = System.OS
|
||||
defaults.ClientInfo.Streams = fmt.Sprintf("%d / %d", len(Data.Streams.Active), len(Data.Streams.All))
|
||||
defaults.ClientInfo.UUID = Settings.UUID
|
||||
defaults.ClientInfo.Errors = WebScreenLog.Errors
|
||||
defaults.ClientInfo.Warnings = WebScreenLog.Warnings
|
||||
defaults.Notification = System.Notification
|
||||
defaults.Log = WebScreenLog
|
||||
defaults.Log = webScreenLogSnapshot()
|
||||
defaults.ClientInfo.Errors = defaults.Log.Errors
|
||||
defaults.ClientInfo.Warnings = defaults.Log.Warnings
|
||||
defaults.Notification = notificationsSnapshot()
|
||||
|
||||
switch System.Branch {
|
||||
|
||||
case "master":
|
||||
defaults.ClientInfo.Version = fmt.Sprintf("%s", System.Version)
|
||||
defaults.ClientInfo.Version = System.Version
|
||||
|
||||
default:
|
||||
defaults.ClientInfo.Version = fmt.Sprintf("%s (%s)", System.Version, System.Build)
|
||||
@@ -989,7 +1001,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
}
|
||||
|
||||
if data == true {
|
||||
if data {
|
||||
|
||||
defaults.Users, _ = authentication.GetAllUserData()
|
||||
//defaults.DVR = System.DVRAddress
|
||||
@@ -998,7 +1010,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
defaults.ClientInfo.XEPGCount = Data.XEPG.XEPGCount
|
||||
|
||||
var XEPG = make(map[string]interface{})
|
||||
var XEPG = make(map[string]any)
|
||||
|
||||
if len(Data.Streams.Active) > 0 {
|
||||
|
||||
@@ -1007,8 +1019,8 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
} else {
|
||||
|
||||
XEPG["epgMapping"] = make(map[string]interface{})
|
||||
XEPG["xmltvMap"] = make(map[string]interface{})
|
||||
XEPG["epgMapping"] = make(map[string]any)
|
||||
XEPG["xmltvMap"] = make(map[string]any)
|
||||
|
||||
}
|
||||
|
||||
@@ -1016,7 +1028,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
}
|
||||
|
||||
defaults.Settings = Settings
|
||||
defaults.Settings = maskSettings(Settings)
|
||||
|
||||
defaults.Data.Playlist.M3U.Groups.Text = Data.Playlist.M3U.Groups.Text
|
||||
defaults.Data.Playlist.M3U.Groups.Value = Data.Playlist.M3U.Groups.Value
|
||||
@@ -1030,7 +1042,6 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
func httpStatusError(w http.ResponseWriter, r *http.Request, httpStatusCode int) {
|
||||
http.Error(w, fmt.Sprintf("%s [%d]", http.StatusText(httpStatusCode), httpStatusCode), httpStatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
func getContentType(filename string) (contentType string) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"xteve/src/internal/authentication"
|
||||
)
|
||||
|
||||
// newAuthenticatedWSServer starts the websocket handler with web
|
||||
// authentication enabled and returns the server plus a valid session token.
|
||||
func newAuthenticatedWSServer(t *testing.T) (*httptest.Server, string) {
|
||||
t.Helper()
|
||||
|
||||
if err := authentication.Init(t.TempDir()+"/authentication.json", 60); err != nil {
|
||||
t.Fatalf("authentication.Init: %v", err)
|
||||
}
|
||||
if _, err := authentication.CreateNewUser("admin", "secret"); err != nil {
|
||||
t.Fatalf("CreateNewUser: %v", err)
|
||||
}
|
||||
token, err := authentication.UserAuthentication("admin", "secret")
|
||||
if err != nil {
|
||||
t.Fatalf("UserAuthentication: %v", err)
|
||||
}
|
||||
|
||||
Settings.AuthenticationWEB = true
|
||||
System.ConfigurationWizard = false
|
||||
t.Cleanup(func() { Settings.AuthenticationWEB = false })
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(WS))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv, token
|
||||
}
|
||||
|
||||
func wsURL(srv *httptest.Server) string {
|
||||
return "ws" + strings.TrimPrefix(srv.URL, "http") + "/data/"
|
||||
}
|
||||
|
||||
// roundTrip sends one command and returns the parsed response.
|
||||
func roundTrip(t *testing.T, url string, header http.Header) ResponseStruct {
|
||||
t.Helper()
|
||||
|
||||
conn, resp, err := websocket.DefaultDialer.Dial(url, header)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v (resp=%v)", err, resp)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.WriteJSON(map[string]any{"cmd": "noop"}); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
var response ResponseStruct
|
||||
if err := conn.ReadJSON(&response); err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func TestWSRejectsCrossOrigin(t *testing.T) {
|
||||
srv, _ := newAuthenticatedWSServer(t)
|
||||
|
||||
header := http.Header{"Origin": {"http://evil.example"}}
|
||||
_, resp, err := websocket.DefaultDialer.Dial(wsURL(srv), header)
|
||||
if err == nil {
|
||||
t.Fatal("expected the cross-origin upgrade to be refused")
|
||||
}
|
||||
if resp == nil || resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("expected 403, got %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSAllowsSameOriginAndNoOrigin(t *testing.T) {
|
||||
srv, token := newAuthenticatedWSServer(t)
|
||||
|
||||
sameOrigin := http.Header{
|
||||
"Origin": {srv.URL},
|
||||
"Cookie": {"Token=" + token},
|
||||
}
|
||||
if r := roundTrip(t, wsURL(srv), sameOrigin); !r.Status {
|
||||
t.Fatalf("same-origin request with cookie should succeed, got error %q", r.Error)
|
||||
}
|
||||
|
||||
noOrigin := http.Header{"Cookie": {"Token=" + token}}
|
||||
if r := roundTrip(t, wsURL(srv), noOrigin); !r.Status {
|
||||
t.Fatalf("request without Origin (non-browser client) should succeed, got error %q", r.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSRequiresSessionToken(t *testing.T) {
|
||||
srv, token := newAuthenticatedWSServer(t)
|
||||
|
||||
r := roundTrip(t, wsURL(srv), nil)
|
||||
if r.Status || !r.Reload {
|
||||
t.Fatalf("request without a token should be refused with reload, got status=%v reload=%v", r.Status, r.Reload)
|
||||
}
|
||||
|
||||
bad := http.Header{"Cookie": {"Token=not-a-real-token"}}
|
||||
if r := roundTrip(t, wsURL(srv), bad); r.Status {
|
||||
t.Fatal("request with an unknown token should be refused")
|
||||
}
|
||||
|
||||
// Legacy clients may still pass the token as a query parameter.
|
||||
if r := roundTrip(t, wsURL(srv)+"?Token="+token, nil); !r.Status {
|
||||
t.Fatalf("legacy query-parameter token should be accepted, got error %q", r.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCookieTokenFlags(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
authentication.SetCookieToken(rec, "abc")
|
||||
|
||||
cookies := rec.Result().Cookies()
|
||||
if len(cookies) != 1 {
|
||||
t.Fatalf("expected one cookie, got %d", len(cookies))
|
||||
}
|
||||
c := cookies[0]
|
||||
if c.Name != "Token" || c.Value != "abc" {
|
||||
t.Fatalf("unexpected cookie %v", c)
|
||||
}
|
||||
if !c.HttpOnly {
|
||||
t.Error("session cookie must be HttpOnly")
|
||||
}
|
||||
if c.SameSite != http.SameSiteStrictMode {
|
||||
t.Error("session cookie must be SameSite=Strict")
|
||||
}
|
||||
if c.Path != "/" {
|
||||
t.Errorf("cookie path should be /, got %q", c.Path)
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
authentication.SetCookieToken(rec, "-")
|
||||
if c := rec.Result().Cookies()[0]; c.MaxAge >= 0 {
|
||||
t.Error("logout must clear the cookie (negative MaxAge)")
|
||||
}
|
||||
}
|
||||
|
||||
// A browser keeps one socket open and sends commands one after another; the
|
||||
// server must answer each on the same connection and echo the request id.
|
||||
func TestWSServesMultipleCommandsPerConnection(t *testing.T) {
|
||||
srv, token := newAuthenticatedWSServer(t)
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL(srv), http.Header{"Cookie": {"Token=" + token}})
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
id := strings.Repeat("x", i)
|
||||
if err := conn.WriteJSON(map[string]any{"cmd": "noop", "id": id}); err != nil {
|
||||
t.Fatalf("write %d: %v", i, err)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
var response ResponseStruct
|
||||
if err := conn.ReadJSON(&response); err != nil {
|
||||
t.Fatalf("read %d: %v (server stopped answering on the same connection)", i, err)
|
||||
}
|
||||
if !response.Status {
|
||||
t.Fatalf("command %d refused: %s", i, response.Error)
|
||||
}
|
||||
if response.ID != id {
|
||||
t.Fatalf("command %d: id %q not echoed, got %q", i, id, response.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
+420
-169
@@ -5,13 +5,19 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xteve/src/internal/imgcache"
|
||||
)
|
||||
|
||||
// Provider XMLTV Datei überprüfen
|
||||
@@ -42,6 +48,13 @@ func buildXEPG(background bool) {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
|
||||
var err error
|
||||
|
||||
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
if Settings.EpgSource == "XEPG" {
|
||||
|
||||
switch background {
|
||||
@@ -56,9 +69,11 @@ func buildXEPG(background bool) {
|
||||
cleanupXEPG()
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
go cachingImages()
|
||||
queuePlexGuideRefresh("xepg rebuild")
|
||||
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
showInfo("XEPG:" + "Ready to use")
|
||||
|
||||
cacheImagesInBackground()
|
||||
|
||||
System.ScanInProgress = 0
|
||||
|
||||
@@ -82,8 +97,11 @@ func buildXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
go cachingImages()
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
queuePlexGuideRefresh("xepg rebuild")
|
||||
|
||||
cacheImagesInBackground()
|
||||
|
||||
showInfo("XEPG:" + "Ready to use")
|
||||
|
||||
System.ScanInProgress = 0
|
||||
|
||||
@@ -105,64 +123,42 @@ func buildXEPG(background bool) {
|
||||
|
||||
}
|
||||
|
||||
// XEPG Daten aktualisieren
|
||||
func updateXEPG(background bool) {
|
||||
// cacheImagesInBackground : Bilder im Hintergrund cachen, danach XMLTV- und M3U-Datei neu erstellen
|
||||
func cacheImagesInBackground() {
|
||||
|
||||
if System.ScanInProgress == 1 {
|
||||
if !Settings.CacheImages || System.ImageCachingInProgress != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
System.ScanInProgress = 1
|
||||
go func() {
|
||||
|
||||
if Settings.EpgSource == "XEPG" {
|
||||
System.ImageCachingInProgress = 1
|
||||
showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue)))
|
||||
|
||||
switch background {
|
||||
Data.Cache.Images.Image.Caching()
|
||||
Data.Cache.Images.Image.Remove()
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
case false:
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg image cache refresh")
|
||||
|
||||
createXEPGDatabase()
|
||||
mapping()
|
||||
cleanupXEPG()
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
go func() {
|
||||
}()
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
System.ScanInProgress = 0
|
||||
|
||||
}()
|
||||
|
||||
case true:
|
||||
System.ScanInProgress = 0
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
System.ScanInProgress = 0
|
||||
|
||||
}
|
||||
|
||||
// Cache löschen
|
||||
//Data.Cache.XMLTV = nil //make(map[string]XMLTV)
|
||||
//Data.Cache.XMLTV = make(map[string]XMLTV)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Mapping Menü für die XMLTV Dateien erstellen
|
||||
func createXEPGMapping() {
|
||||
|
||||
Data.XMLTV.Files = getLocalProviderFiles("xmltv")
|
||||
Data.XMLTV.Mapping = make(map[string]interface{})
|
||||
Data.XMLTV.Mapping = make(map[string]any)
|
||||
|
||||
var tmpMap = make(map[string]interface{})
|
||||
var tmpMap = make(map[string]any)
|
||||
|
||||
var friendlyDisplayName = func(channel Channel) (displayName string) {
|
||||
var dn = channel.DisplayName
|
||||
displayName = dn[0].Value
|
||||
|
||||
switch len(dn) {
|
||||
case 1:
|
||||
@@ -176,6 +172,11 @@ func createXEPGMapping() {
|
||||
|
||||
if len(Data.XMLTV.Files) > 0 {
|
||||
|
||||
// Files that could not be read are collected here and removed after
|
||||
// the loop; mutating Data.XMLTV.Files while iterating over it used
|
||||
// to duplicate entries instead of removing them.
|
||||
var failedFiles = make(map[string]bool)
|
||||
|
||||
for i := len(Data.XMLTV.Files) - 1; i >= 0; i-- {
|
||||
|
||||
var file = Data.XMLTV.Files[i]
|
||||
@@ -189,7 +190,7 @@ func createXEPGMapping() {
|
||||
|
||||
err = getLocalXMLTV(file, &xmltv)
|
||||
if err != nil {
|
||||
Data.XMLTV.Files = append(Data.XMLTV.Files, Data.XMLTV.Files[i+1:]...)
|
||||
failedFiles[file] = true
|
||||
var errMsg = err.Error()
|
||||
err = errors.New(getProviderParameter(fileID, "xmltv", "name") + ": " + errMsg)
|
||||
ShowError(err, 000)
|
||||
@@ -199,10 +200,10 @@ func createXEPGMapping() {
|
||||
if err == nil {
|
||||
|
||||
// Daten aus der XML Datei in eine temporäre Map schreiben
|
||||
var xmltvMap = make(map[string]interface{})
|
||||
var xmltvMap = make(map[string]any)
|
||||
|
||||
for _, c := range xmltv.Channel {
|
||||
var channel = make(map[string]interface{})
|
||||
var channel = make(map[string]any)
|
||||
|
||||
channel["id"] = c.ID
|
||||
channel["display-name"] = friendlyDisplayName(*c)
|
||||
@@ -219,20 +220,20 @@ func createXEPGMapping() {
|
||||
|
||||
}
|
||||
|
||||
Data.XMLTV.Files = removeStrings(Data.XMLTV.Files, failedFiles)
|
||||
Data.XMLTV.Mapping = tmpMap
|
||||
tmpMap = make(map[string]interface{})
|
||||
|
||||
} else {
|
||||
|
||||
if System.ConfigurationWizard == false {
|
||||
if !System.ConfigurationWizard {
|
||||
showWarning(1007)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Auswahl für den Dummy erstellen
|
||||
var dummy = make(map[string]interface{})
|
||||
var times = []string{"30", "60", "90", "120"}
|
||||
var dummy = make(map[string]any)
|
||||
var times = []string{"30", "60", "90", "120", "180", "240", "360"}
|
||||
|
||||
for _, i := range times {
|
||||
|
||||
@@ -247,14 +248,14 @@ func createXEPGMapping() {
|
||||
|
||||
Data.XMLTV.Mapping["xTeVe Dummy"] = dummy
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// XEPG Datenbank erstellen / aktualisieren
|
||||
func createXEPGDatabase() (err error) {
|
||||
|
||||
var allChannelNumbers []float64
|
||||
Data.Cache.Streams.Active = []string{}
|
||||
var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit)
|
||||
Data.Cache.Streams.Active = make([]string, 0, System.UnfilteredChannelLimit)
|
||||
Data.XEPG.Channels = make(map[string]any, System.UnfilteredChannelLimit)
|
||||
|
||||
Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG)
|
||||
if err != nil {
|
||||
@@ -279,24 +280,31 @@ func createXEPGDatabase() (err error) {
|
||||
|
||||
var getFreeChannelNumber = func() (xChannelID string) {
|
||||
|
||||
var firstFreeNumber float64 = Settings.MappingFirstChannel
|
||||
sort.Float64s(allChannelNumbers)
|
||||
|
||||
newNumber:
|
||||
var firstFreeNumber = Settings.MappingFirstChannel
|
||||
|
||||
for {
|
||||
|
||||
if indexOfFloat64(firstFreeNumber, allChannelNumbers) == -1 {
|
||||
xChannelID = fmt.Sprintf("%g", firstFreeNumber)
|
||||
allChannelNumbers = append(allChannelNumbers, firstFreeNumber)
|
||||
return
|
||||
}
|
||||
|
||||
if indexOfFloat64(firstFreeNumber, allChannelNumbers) == -1 {
|
||||
xChannelID = fmt.Sprintf("%g", firstFreeNumber)
|
||||
allChannelNumbers = append(allChannelNumbers, firstFreeNumber)
|
||||
} else {
|
||||
firstFreeNumber++
|
||||
goto newNumber
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var generateHashForChannel = func(m3uID string, groupTitle string, tvgID string, tvgName string, uuidKey string, uuidValue string) string {
|
||||
hash := md5.Sum([]byte(m3uID + groupTitle + tvgID + tvgName + uuidKey + uuidValue))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + "Update database")
|
||||
|
||||
// Kanal mit fehlenden Kanalnummern löschen
|
||||
// Kanal mit fehlenden Kanalnummern löschen. Delete channel with missing channel numbers
|
||||
for id, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
@@ -306,7 +314,6 @@ func createXEPGDatabase() (err error) {
|
||||
}
|
||||
|
||||
if len(xepgChannel.XChannelID) == 0 {
|
||||
fmt.Println(mapToJSON(xepgChannel))
|
||||
delete(Data.XEPG.Channels, id)
|
||||
}
|
||||
|
||||
@@ -316,11 +323,24 @@ func createXEPGDatabase() (err error) {
|
||||
|
||||
}
|
||||
|
||||
// Make a map of the db channels based on their previously downloaded attributes -- filename, group, title, etc
|
||||
var xepgChannelsValuesMap = make(map[string]XEPGChannelStruct, System.UnfilteredChannelLimit)
|
||||
for _, v := range Data.XEPG.Channels {
|
||||
var channel XEPGChannelStruct
|
||||
err = json.Unmarshal([]byte(mapToJSON(v)), &channel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
channelHash := generateHashForChannel(channel.FileM3UID, channel.GroupTitle, channel.TvgID, channel.TvgName, channel.UUIDKey, channel.UUIDValue)
|
||||
xepgChannelsValuesMap[channelHash] = channel
|
||||
}
|
||||
|
||||
for _, dsa := range Data.Streams.Active {
|
||||
|
||||
var channelExists = false // Entscheidet ob ein Kanal neu zu Datenbank hinzugefügt werden soll.
|
||||
var channelHasUUID = false // Überprüft, ob der Kanal (Stream) eindeutige ID's besitzt
|
||||
var currentXEPGID string // Aktuelle Datenbank ID (XEPG). Wird verwendet, um den Kanal in der Datenbank mit dem Stream der M3u zu aktualisieren
|
||||
var channelExists = false // Entscheidet ob ein Kanal neu zu Datenbank hinzugefügt werden soll. Decides whether a channel should be added to the database
|
||||
var channelHasUUID = false // Überprüft, ob der Kanal (Stream) eindeutige ID's besitzt. Checks whether the channel (stream) has unique IDs
|
||||
var currentXEPGID string // Aktuelle Datenbank ID (XEPG). Wird verwendet, um den Kanal in der Datenbank mit dem Stream der M3u zu aktualisieren. Current database ID (XEPG) Used to update the channel in the database with the stream of the M3u
|
||||
|
||||
var m3uChannel M3UChannelStructXEPG
|
||||
|
||||
err = json.Unmarshal([]byte(mapToJSON(dsa)), &m3uChannel)
|
||||
@@ -328,45 +348,55 @@ func createXEPGDatabase() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
Data.Cache.Streams.Active = append(Data.Cache.Streams.Active, m3uChannel.Name)
|
||||
Data.Cache.Streams.Active = append(Data.Cache.Streams.Active, m3uChannel.Name+m3uChannel.FileM3UID)
|
||||
|
||||
// XEPG Datenbank durchlaufen um nach dem Kanal zu suchen.
|
||||
for xepg, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
if err != nil {
|
||||
return
|
||||
// Try to find the channel based on matching all known values. If that fails, then move to full channel scan
|
||||
m3uChannelHash := generateHashForChannel(m3uChannel.FileM3UID, m3uChannel.GroupTitle, m3uChannel.TvgID, m3uChannel.TvgName, m3uChannel.UUIDKey, m3uChannel.UUIDValue)
|
||||
if val, ok := xepgChannelsValuesMap[m3uChannelHash]; ok {
|
||||
channelExists = true
|
||||
currentXEPGID = val.XEPG
|
||||
if len(m3uChannel.UUIDValue) > 0 {
|
||||
channelHasUUID = true
|
||||
}
|
||||
} else {
|
||||
|
||||
// Vergleichen des Streams anhand einer UUID in der M3U mit dem Kanal in der Databank
|
||||
if len(xepgChannel.UUIDValue) > 0 && len(m3uChannel.UUIDValue) > 0 {
|
||||
// XEPG Datenbank durchlaufen um nach dem Kanal zu suchen. Run through the XEPG database to search for the channel (full scan)
|
||||
for _, dxc := range xepgChannelsValuesMap {
|
||||
|
||||
if xepgChannel.UUIDValue == m3uChannel.UUIDValue && xepgChannel.UUIDKey == m3uChannel.UUIDKey {
|
||||
if m3uChannel.FileM3UID == dxc.FileM3UID {
|
||||
|
||||
channelExists = true
|
||||
channelHasUUID = true
|
||||
currentXEPGID = xepg
|
||||
break
|
||||
dxc.FileM3UID = m3uChannel.FileM3UID
|
||||
dxc.FileM3UName = m3uChannel.FileM3UName
|
||||
|
||||
// Vergleichen des Streams anhand einer UUID in der M3U mit dem Kanal in der Databank. Compare the stream using a UUID in the M3U with the channel in the database
|
||||
if len(dxc.UUIDValue) > 0 && len(m3uChannel.UUIDValue) > 0 {
|
||||
|
||||
if dxc.UUIDValue == m3uChannel.UUIDValue && dxc.UUIDKey == m3uChannel.UUIDKey {
|
||||
|
||||
channelExists = true
|
||||
channelHasUUID = true
|
||||
currentXEPGID = dxc.XEPG
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// Vergleichen des Streams mit dem Kanal in der Databank anhand des Kanalnamens. Compare the stream to the channel in the database using the channel name
|
||||
if dxc.Name == m3uChannel.Name {
|
||||
channelExists = true
|
||||
currentXEPGID = dxc.XEPG
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// Vergleichen des Streams mit dem Kanal in der Databank anhand des Kanalnamens
|
||||
//fmt.Println(xepgChannel.Name, xepgChannel.UUIDKey, xepgChannel.UUIDValue)
|
||||
if xepgChannel.Name == m3uChannel.Name {
|
||||
channelExists = true
|
||||
currentXEPGID = xepg
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//os.Exit(0)
|
||||
|
||||
switch channelExists {
|
||||
|
||||
case true:
|
||||
// Bereits vorhandener Kanal
|
||||
var xepgChannel XEPGChannelStruct
|
||||
@@ -382,14 +412,14 @@ func createXEPGDatabase() (err error) {
|
||||
xepgChannel.Name = m3uChannel.Name
|
||||
|
||||
// Kanalname aktualisieren, nur mit Kanal ID's möglich
|
||||
if channelHasUUID == true {
|
||||
if xepgChannel.XUpdateChannelName == true {
|
||||
if channelHasUUID {
|
||||
if xepgChannel.XUpdateChannelName {
|
||||
xepgChannel.XName = m3uChannel.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Kanallogo aktualisieren. Wird bei vorhandenem Logo in der XMLTV Datei wieder überschrieben
|
||||
if xepgChannel.XUpdateChannelIcon == true {
|
||||
if xepgChannel.XUpdateChannelIcon {
|
||||
xepgChannel.TvgLogo = m3uChannel.TvgLogo
|
||||
}
|
||||
|
||||
@@ -429,7 +459,7 @@ func createXEPGDatabase() (err error) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + "Save DB file")
|
||||
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -438,10 +468,132 @@ func createXEPGDatabase() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func normalizeXEPGMatchValue(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case unicode.IsLetter(r):
|
||||
return unicode.ToLower(r)
|
||||
case unicode.IsDigit(r):
|
||||
return r
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}, value)
|
||||
}
|
||||
|
||||
func appendXEPGIssueSample(samples map[string]struct{}, value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) == 0 {
|
||||
return
|
||||
}
|
||||
samples[value] = struct{}{}
|
||||
}
|
||||
|
||||
func xepgIssueSampleText(samples map[string]struct{}, max int) string {
|
||||
if len(samples) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
list := make([]string, 0, len(samples))
|
||||
for name := range samples {
|
||||
list = append(list, name)
|
||||
}
|
||||
sort.Strings(list)
|
||||
|
||||
if len(list) > max {
|
||||
return strings.Join(list[:max], ", ") + ", ..."
|
||||
}
|
||||
|
||||
return strings.Join(list, ", ")
|
||||
}
|
||||
|
||||
func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGChannelStruct) (channelID string, channel map[string]any, ok bool) {
|
||||
var candidateValues = map[string]struct{}{}
|
||||
|
||||
addCandidate := func(value string) {
|
||||
normalized := normalizeXEPGMatchValue(value)
|
||||
if len(normalized) == 0 {
|
||||
return
|
||||
}
|
||||
candidateValues[normalized] = struct{}{}
|
||||
}
|
||||
|
||||
addCandidate(xepgChannel.XName)
|
||||
addCandidate(xepgChannel.Name)
|
||||
addCandidate(xepgChannel.TvgName)
|
||||
addCandidate(xepgChannel.TvgID)
|
||||
|
||||
if len(candidateValues) == 0 {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
if _, exists := candidateValues[normalizeXEPGMatchValue(xepgChannel.TvgID)]; exists {
|
||||
if direct, found := xmltvChannels[xepgChannel.TvgID].(map[string]any); found {
|
||||
return xepgChannel.TvgID, direct, true
|
||||
}
|
||||
}
|
||||
|
||||
var matches []struct {
|
||||
id string
|
||||
channel map[string]any
|
||||
}
|
||||
|
||||
for id, data := range xmltvChannels {
|
||||
xmltvChannel, castOK := data.(map[string]any)
|
||||
if !castOK {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, match := candidateValues[normalizeXEPGMatchValue(id)]; match {
|
||||
matches = append(matches, struct {
|
||||
id string
|
||||
channel map[string]any
|
||||
}{id: id, channel: xmltvChannel})
|
||||
continue
|
||||
}
|
||||
|
||||
displayName, hasDisplayName := xmltvChannel["display-name"].(string)
|
||||
if !hasDisplayName {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, match := candidateValues[normalizeXEPGMatchValue(displayName)]; match {
|
||||
matches = append(matches, struct {
|
||||
id string
|
||||
channel map[string]any
|
||||
}{id: id, channel: xmltvChannel})
|
||||
}
|
||||
}
|
||||
|
||||
if len(matches) != 1 {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
return matches[0].id, matches[0].channel, true
|
||||
}
|
||||
|
||||
// Kanäle automatisch zuordnen und das Mapping überprüfen
|
||||
func mapping() (err error) {
|
||||
showInfo("XEPG:" + "Map channels")
|
||||
|
||||
strictMissingEPGMode := Settings.XepgMissingEPGMode != "relaxed"
|
||||
|
||||
missingEPGCount := 0
|
||||
missingXMLTVCount := 0
|
||||
autoRemapCount := 0
|
||||
relaxedKeepCount := 0
|
||||
relaxedDummyCount := 0
|
||||
missingEPGSamples := map[string]struct{}{}
|
||||
missingXMLTVSamples := map[string]struct{}{}
|
||||
autoRemapSamples := map[string]struct{}{}
|
||||
relaxedKeepSamples := map[string]struct{}{}
|
||||
relaxedDummySamples := map[string]struct{}{}
|
||||
|
||||
for xepg, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
@@ -451,10 +603,10 @@ func mapping() (err error) {
|
||||
}
|
||||
|
||||
// Automatische Mapping für neue Kanäle. Wird nur ausgeführt, wenn der Kanal deaktiviert ist und keine XMLTV Datei und kein XMLTV Kanal zugeordnet ist.
|
||||
if xepgChannel.XActive == false {
|
||||
if !xepgChannel.XActive {
|
||||
|
||||
// Werte kann "-" sein, deswegen len < 1
|
||||
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XmltvFile) < 1 {
|
||||
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XMapping) < 1 {
|
||||
|
||||
var tvgID = xepgChannel.TvgID
|
||||
|
||||
@@ -466,16 +618,16 @@ func mapping() (err error) {
|
||||
|
||||
for file, xmltvChannels := range Data.XMLTV.Mapping {
|
||||
|
||||
if channel, ok := xmltvChannels.(map[string]interface{})[tvgID]; ok {
|
||||
if channel, ok := xmltvChannels.(map[string]any)[tvgID]; ok {
|
||||
|
||||
if channelID, ok := channel.(map[string]interface{})["id"].(string); ok {
|
||||
if channelID, ok := channel.(map[string]any)["id"].(string); ok {
|
||||
|
||||
xepgChannel.XmltvFile = file
|
||||
xepgChannel.XMapping = channelID
|
||||
xepgChannel.XActive = true
|
||||
|
||||
// Falls in der XMLTV Datei ein Logo existiert, wird dieses verwendet. Falls nicht, dann das Logo aus der M3U Datei
|
||||
if icon, ok := channel.(map[string]interface{})["icon"].(string); ok {
|
||||
if icon, ok := channel.(map[string]any)["icon"].(string); ok {
|
||||
if len(icon) > 0 {
|
||||
xepgChannel.TvgLogo = icon
|
||||
}
|
||||
@@ -495,41 +647,89 @@ func mapping() (err error) {
|
||||
}
|
||||
|
||||
// Überprüfen, ob die zugeordneten XMLTV Dateien und Kanäle noch existieren.
|
||||
if xepgChannel.XActive == true {
|
||||
if xepgChannel.XActive {
|
||||
|
||||
var mapping = xepgChannel.XMapping
|
||||
var file = xepgChannel.XmltvFile
|
||||
|
||||
if file != "xTeVe Dummy" {
|
||||
|
||||
if value, ok := Data.XMLTV.Mapping[file].(map[string]interface{}); ok {
|
||||
if value, ok := Data.XMLTV.Mapping[file].(map[string]any); ok {
|
||||
|
||||
if channel, ok := value[mapping].(map[string]interface{}); ok {
|
||||
if channel, ok := value[mapping].(map[string]any); ok {
|
||||
|
||||
// Kanallogo aktualisieren
|
||||
if logo, ok := channel["icon"].(string); ok {
|
||||
|
||||
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 {
|
||||
if xepgChannel.XUpdateChannelIcon && len(logo) > 0 {
|
||||
xepgChannel.TvgLogo = logo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
if channelID, replacementChannel, remapOK := findXEPGReplacementChannel(value, xepgChannel); remapOK {
|
||||
xepgChannel.XMapping = channelID
|
||||
|
||||
ShowError(fmt.Errorf(fmt.Sprintf("Missing EPG data: %s", xepgChannel.Name)), 0)
|
||||
showWarning(2302)
|
||||
xepgChannel.XActive = false
|
||||
if logo, ok := replacementChannel["icon"].(string); ok {
|
||||
if xepgChannel.XUpdateChannelIcon && len(logo) > 0 {
|
||||
xepgChannel.TvgLogo = logo
|
||||
}
|
||||
}
|
||||
|
||||
autoRemapCount++
|
||||
name := strings.TrimSpace(xepgChannel.Name)
|
||||
if len(name) == 0 {
|
||||
name = strings.TrimSpace(xepgChannel.XName)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
name = xepg
|
||||
}
|
||||
appendXEPGIssueSample(autoRemapSamples, name)
|
||||
} else {
|
||||
name := strings.TrimSpace(xepgChannel.Name)
|
||||
if len(name) == 0 {
|
||||
name = strings.TrimSpace(xepgChannel.XName)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
name = xepg
|
||||
}
|
||||
|
||||
if strictMissingEPGMode {
|
||||
missingEPGCount++
|
||||
appendXEPGIssueSample(missingEPGSamples, name)
|
||||
xepgChannel.XActive = false
|
||||
} else {
|
||||
relaxedKeepCount++
|
||||
appendXEPGIssueSample(relaxedKeepSamples, name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file)))
|
||||
providerName := getProviderParameter(fileID, "xmltv", "name")
|
||||
if len(strings.TrimSpace(providerName)) == 0 {
|
||||
providerName = file
|
||||
}
|
||||
|
||||
ShowError(fmt.Errorf("Missing XMLTV file: %s", getProviderParameter(fileID, "xmltv", "name")), 0)
|
||||
showWarning(2301)
|
||||
xepgChannel.XActive = false
|
||||
if strictMissingEPGMode {
|
||||
missingXMLTVCount++
|
||||
appendXEPGIssueSample(missingXMLTVSamples, providerName)
|
||||
xepgChannel.XActive = false
|
||||
} else {
|
||||
relaxedDummyCount++
|
||||
name := strings.TrimSpace(xepgChannel.Name)
|
||||
if len(name) == 0 {
|
||||
name = strings.TrimSpace(xepgChannel.XName)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
name = xepg
|
||||
}
|
||||
appendXEPGIssueSample(relaxedDummySamples, name)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -551,6 +751,28 @@ func mapping() (err error) {
|
||||
|
||||
}
|
||||
|
||||
if autoRemapCount > 0 {
|
||||
showInfo(fmt.Sprintf("XEPG:%d channel mappings were auto-remapped (examples: %s)", autoRemapCount, xepgIssueSampleText(autoRemapSamples, 8)))
|
||||
}
|
||||
|
||||
if missingEPGCount > 0 {
|
||||
showWarning(2302)
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels have missing EPG mappings and were deactivated (examples: %s)", missingEPGCount, xepgIssueSampleText(missingEPGSamples, 8)))
|
||||
}
|
||||
|
||||
if missingXMLTVCount > 0 {
|
||||
showWarning(2301)
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels reference missing XMLTV files and were deactivated (sources: %s)", missingXMLTVCount, xepgIssueSampleText(missingXMLTVSamples, 5)))
|
||||
}
|
||||
|
||||
if relaxedKeepCount > 0 {
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels kept active in relaxed mode despite missing EPG mappings (examples: %s)", relaxedKeepCount, xepgIssueSampleText(relaxedKeepSamples, 8)))
|
||||
}
|
||||
|
||||
if relaxedDummyCount > 0 {
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels will use xTeVe Dummy guide in relaxed mode because XMLTV sources were unavailable (examples: %s)", relaxedDummyCount, xepgIssueSampleText(relaxedDummySamples, 8)))
|
||||
}
|
||||
|
||||
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -562,11 +784,15 @@ func mapping() (err error) {
|
||||
// XMLTV Datei erstellen
|
||||
func createXMLTVFile() (err error) {
|
||||
|
||||
// Image Cache
|
||||
// 4edd81ab7c368208cc6448b615051b37.jpg
|
||||
var imgc = Data.Cache.Images
|
||||
|
||||
Data.Cache.ImagesFiles = []string{}
|
||||
Data.Cache.ImagesURLS = []string{}
|
||||
Data.Cache.ImagesCache = []string{}
|
||||
|
||||
files, err := ioutil.ReadDir(System.Folder.ImagesCache)
|
||||
files, err := os.ReadDir(System.Folder.ImagesCache)
|
||||
if err == nil {
|
||||
|
||||
for _, file := range files {
|
||||
@@ -580,7 +806,7 @@ func createXMLTVFile() (err error) {
|
||||
}
|
||||
|
||||
if len(Data.XMLTV.Files) == 0 && len(Data.Streams.Active) == 0 {
|
||||
Data.XEPG.Channels = make(map[string]interface{})
|
||||
Data.XEPG.Channels = make(map[string]any)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -604,12 +830,12 @@ func createXMLTVFile() (err error) {
|
||||
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
if err == nil {
|
||||
|
||||
if xepgChannel.XActive == true {
|
||||
if xepgChannel.XActive {
|
||||
|
||||
// Kanäle
|
||||
var channel Channel
|
||||
channel.ID = xepgChannel.XChannelID
|
||||
channel.Icon = Icon{Src: getCacheImageURL(xepgChannel.TvgLogo)}
|
||||
channel.Icon = Icon{Src: imgc.Image.GetURL(xepgChannel.TvgLogo)}
|
||||
channel.DisplayName = append(channel.DisplayName, DisplayName{Value: xepgChannel.XName})
|
||||
|
||||
xepgXML.Channel = append(xepgXML.Channel, &channel)
|
||||
@@ -619,9 +845,7 @@ func createXMLTVFile() (err error) {
|
||||
*tmpProgram, err = getProgramData(xepgChannel)
|
||||
if err == nil {
|
||||
|
||||
for _, program := range tmpProgram.Program {
|
||||
xepgXML.Program = append(xepgXML.Program, program)
|
||||
}
|
||||
xepgXML.Program = append(xepgXML.Program, tmpProgram.Program...)
|
||||
|
||||
}
|
||||
|
||||
@@ -635,9 +859,10 @@ func createXMLTVFile() (err error) {
|
||||
var xmlOutput = []byte(xml.Header + string(content))
|
||||
writeByteToFile(System.File.XML, xmlOutput)
|
||||
|
||||
xepgXML = XMLTV{}
|
||||
showInfo("XEPG:" + fmt.Sprintf("Compress XMLTV file (%s)", System.Compressed.GZxml))
|
||||
err = compressGZIP(&xmlOutput, System.Compressed.GZxml)
|
||||
|
||||
//saveMapToJSONFile(System.File.Images, Data.Cache.ImageCache)
|
||||
xepgXML = XMLTV{}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -647,6 +872,13 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
|
||||
var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile
|
||||
var channelID = xepgChannel.XMapping
|
||||
relaxedMissingEPGMode := Settings.XepgMissingEPGMode == "relaxed"
|
||||
fallbackToDummy := func() {
|
||||
dummyChannel := xepgChannel
|
||||
dummyChannel.XmltvFile = "xTeVe Dummy"
|
||||
dummyChannel.XMapping = "240_Minutes"
|
||||
xepgXML = createDummyProgram(dummyChannel)
|
||||
}
|
||||
|
||||
var xmltv XMLTV
|
||||
|
||||
@@ -656,6 +888,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
|
||||
err = getLocalXMLTV(xmltvFile, &xmltv)
|
||||
if err != nil {
|
||||
if relaxedMissingEPGMode {
|
||||
fallbackToDummy()
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -684,6 +920,15 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
// Category (Kategorie)
|
||||
getCategory(program, xmltvProgram, xepgChannel)
|
||||
|
||||
// Credits : (Credits)
|
||||
program.Credits = xmltvProgram.Credits
|
||||
|
||||
// Rating (Bewertung)
|
||||
program.Rating = xmltvProgram.Rating
|
||||
|
||||
// StarRating (Bewertung / Kritiken)
|
||||
program.StarRating = xmltvProgram.StarRating
|
||||
|
||||
// Country (Länder)
|
||||
program.Country = xmltvProgram.Country
|
||||
|
||||
@@ -711,25 +956,33 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
// Live
|
||||
program.Live = xmltvProgram.Live
|
||||
|
||||
// Premiere
|
||||
program.Premiere = xmltvProgram.Premiere
|
||||
|
||||
xepgXML.Program = append(xepgXML.Program, program)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(xepgXML.Program) == 0 && relaxedMissingEPGMode && xmltvFile != System.Folder.Data+"xTeVe Dummy" {
|
||||
fallbackToDummy()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Dummy Daten erstellen (createXMLTVFile)
|
||||
func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
|
||||
var imgc = Data.Cache.Images
|
||||
var currentTime = time.Now()
|
||||
var dateArray = strings.Fields(currentTime.String())
|
||||
var offset = " " + dateArray[2]
|
||||
var currentDay = currentTime.Format("20060102")
|
||||
var startTime, _ = time.Parse("20060102150405", currentDay+"000000")
|
||||
|
||||
showInfo("Create Dummy Guide:" + "Time offset" + offset + " - " + xepgChannel.XName)
|
||||
showDebug("Create Dummy Guide:"+"Time offset"+offset+" - "+xepgChannel.XName, 2)
|
||||
|
||||
var dl = strings.Split(xepgChannel.XMapping, "_")
|
||||
dummyLength, err := strconv.Atoi(dl[0])
|
||||
@@ -738,7 +991,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
return
|
||||
}
|
||||
|
||||
for d := 0; d < 4; d++ {
|
||||
for d := range 4 {
|
||||
|
||||
var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24))
|
||||
|
||||
@@ -753,10 +1006,15 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
epg.Start = epgStartTime.Format("20060102150405") + offset
|
||||
epg.Stop = epgStopTime.Format("20060102150405") + offset
|
||||
epg.Title = append(epg.Title, &Title{Value: xepgChannel.XName + " (" + epgStartTime.Weekday().String()[0:2] + ". " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04") + ")", Lang: "en"})
|
||||
epg.Desc = append(epg.Desc, &Desc{Value: "xTeVe: (" + strconv.Itoa(dummyLength) + " Minutes) " + epgStartTime.Weekday().String() + " " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04"), Lang: "en"})
|
||||
|
||||
if Settings.XepgReplaceMissingImages == true {
|
||||
poster.Src = getCacheImageURL(xepgChannel.TvgLogo)
|
||||
if len(xepgChannel.XDescription) == 0 {
|
||||
epg.Desc = append(epg.Desc, &Desc{Value: "xTeVe: (" + strconv.Itoa(dummyLength) + " Minutes) " + epgStartTime.Weekday().String() + " " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04"), Lang: "en"})
|
||||
} else {
|
||||
epg.Desc = append(epg.Desc, &Desc{Value: xepgChannel.XDescription, Lang: "en"})
|
||||
}
|
||||
|
||||
if Settings.XepgReplaceMissingImages {
|
||||
poster.Src = imgc.Image.GetURL(xepgChannel.TvgLogo)
|
||||
epg.Poster = append(epg.Poster, poster)
|
||||
}
|
||||
|
||||
@@ -797,22 +1055,23 @@ func getCategory(program *Program, xmltvProgram *Program, xepgChannel XEPGChanne
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Programm Poster Cover aus der XMLTV Datei laden
|
||||
func getPoster(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) {
|
||||
|
||||
var imgc = Data.Cache.Images
|
||||
|
||||
for _, poster := range xmltvProgram.Poster {
|
||||
poster.Src = getCacheImageURL(poster.Src)
|
||||
poster.Src = imgc.Image.GetURL(poster.Src)
|
||||
program.Poster = append(program.Poster, poster)
|
||||
}
|
||||
|
||||
if Settings.XepgReplaceMissingImages == true {
|
||||
if Settings.XepgReplaceMissingImages {
|
||||
|
||||
if len(xmltvProgram.Poster) == 0 {
|
||||
var poster Poster
|
||||
poster.Src = getCacheImageURL(xepgChannel.TvgLogo)
|
||||
poster.Src = imgc.Image.GetURL(poster.Src)
|
||||
program.Poster = append(program.Poster, poster)
|
||||
}
|
||||
|
||||
@@ -828,12 +1087,20 @@ func getEpisodeNum(program *Program, xmltvProgram *Program, xepgChannel XEPGChan
|
||||
if len(xepgChannel.XCategory) > 0 && xepgChannel.XCategory != "Movie" {
|
||||
|
||||
if len(xmltvProgram.EpisodeNum) == 0 {
|
||||
program.EpisodeNum = append(program.EpisodeNum, &EpisodeNum{Value: time.Now().Format("2006-01-02 15:04:05"), System: "original-air-date"})
|
||||
|
||||
var timeLayout = "20060102150405"
|
||||
|
||||
t, err := time.Parse(timeLayout, strings.Split(xmltvProgram.Start, " ")[0])
|
||||
if err == nil {
|
||||
program.EpisodeNum = append(program.EpisodeNum, &EpisodeNum{Value: t.Format("2006-01-02 15:04:05"), System: "original-air-date"})
|
||||
} else {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Videoparameter erstellen (createXMLTVFile)
|
||||
@@ -859,7 +1126,6 @@ func getVideo(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelSt
|
||||
|
||||
program.Video = video
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Lokale Provider XMLTV Datei laden
|
||||
@@ -906,15 +1172,26 @@ func createM3UFile() {
|
||||
ShowError(err, 000)
|
||||
}
|
||||
|
||||
saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS)
|
||||
saveMapToJSONFile(System.File.URLS, streamingURLsSnapshot())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// XEPG Datenbank bereinigen
|
||||
func cleanupXEPG() {
|
||||
|
||||
showInfo("XEPG:" + fmt.Sprintf("Cleanup database"))
|
||||
//fmt.Println(Settings.Files.M3U)
|
||||
|
||||
var sourceIDs []string
|
||||
|
||||
for source := range Settings.Files.M3U {
|
||||
sourceIDs = append(sourceIDs, source)
|
||||
}
|
||||
|
||||
for source := range Settings.Files.HDHR {
|
||||
sourceIDs = append(sourceIDs, source)
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + "Cleanup database")
|
||||
Data.XEPG.XEPGCount = 0
|
||||
|
||||
for id, dxc := range Data.XEPG.Channels {
|
||||
@@ -923,14 +1200,18 @@ func cleanupXEPG() {
|
||||
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
if err == nil {
|
||||
|
||||
if indexOfString(xepgChannel.Name, Data.Cache.Streams.Active) == -1 {
|
||||
if indexOfString(xepgChannel.Name+xepgChannel.FileM3UID, Data.Cache.Streams.Active) == -1 {
|
||||
delete(Data.XEPG.Channels, id)
|
||||
} else {
|
||||
if xepgChannel.XActive == true {
|
||||
if xepgChannel.XActive {
|
||||
Data.XEPG.XEPGCount++
|
||||
}
|
||||
}
|
||||
|
||||
if indexOfString(xepgChannel.FileM3UID, sourceIDs) == -1 {
|
||||
delete(Data.XEPG.Channels, id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -947,34 +1228,4 @@ func cleanupXEPG() {
|
||||
showWarning(2005)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Streaming URL für die Channels App generieren
|
||||
func getStreamByChannelID(channelID string) (playlistID, streamURL string, err error) {
|
||||
|
||||
err = errors.New("Channel not found")
|
||||
|
||||
for _, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
|
||||
fmt.Println(xepgChannel.XChannelID)
|
||||
|
||||
if err == nil {
|
||||
|
||||
if channelID == xepgChannel.XChannelID {
|
||||
|
||||
playlistID = xepgChannel.FileM3UID
|
||||
streamURL = xepgChannel.URL
|
||||
|
||||
return playlistID, streamURL, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,169 @@
|
||||
# xTeVe fork: review and improvement plan
|
||||
|
||||
Date: 2026-09-25
|
||||
Scope: whole repository (Go backend, TypeScript UI, Docker, CI, docs)
|
||||
Status: implemented on branch `improvements` (2026-09-26). Per-item results, deviations and what was left open are in `tasks/todo.md`.
|
||||
|
||||
## 1. Where the fork stands
|
||||
|
||||
- Upstream (`xteve-project/xTeVe`) has not moved since commit `0e999b8` (March 2021). There is nothing to merge. This fork is effectively its own project now, so decisions can be made without worrying about upstream compatibility.
|
||||
- The fork adds 26 commits, 57 files, roughly +4800/-1600 lines: UI redesign, Plex API refresh (`src/plex_api.go`), relaxed/strict XEPG mapping with auto-remap (`findXEPGReplacementChannel`), wizard-completed flag, config-dir migration in `docker/entrypoint.sh`, Drone CI, Go 1.25/1.26 modernisation.
|
||||
- `go build ./...` passes. `go vet ./...` reports four "unreachable code" warnings. One test file exists (`src/internal/m3u-parser`). staticcheck cannot run locally because the installed binary predates Go 1.25.
|
||||
- The README already lists four next steps (go:embed, CI, `/healthz`, websocket integration tests). This plan absorbs them.
|
||||
|
||||
The codebase works, and the fork's additions are sensible. The problems are mostly inherited from upstream: a security model that assumes a trusted LAN and a trusted playlist, data races in the stream buffer, and a build pipeline that depends on committed generated files. None of them show up in daily use until they do.
|
||||
|
||||
## 2. Guiding principles
|
||||
|
||||
1. **Security and crash bugs before features.** Several findings allow code execution or process crashes from data you do not control (playlists, EPG feeds, any web page open on the LAN).
|
||||
2. **Tests before refactors.** Every phase below starts by adding tests around the code it touches, so the refactor can be checked against current behaviour.
|
||||
3. **Small, reviewable PRs.** Each checklist item should be one PR. Avoid a "big bang" rewrite.
|
||||
4. **Keep vanilla TypeScript for now.** A framework migration is a 3 to 6 week rewrite and would block everything else. Revisit after Phase 5.
|
||||
5. **Every phase ends green:** `go vet`, `go test -race`, `tsc --noEmit`, Docker build.
|
||||
|
||||
## 3. Phases
|
||||
|
||||
### Phase 0: Hygiene (about half a day, no risk)
|
||||
|
||||
- Commit the pending `.gitignore` change (`.gocache/`), delete `.gocache/` from the working tree, and add `.gocache/`, `ts/`, `tasks/`, `*.md` (except what the Dockerfile needs) to `.dockerignore`.
|
||||
- Decide where `agent.md` and `skill.md` live. Recommendation: move to `.claude/` or `tasks/` and ignore them, so the build context and repo root stay clean.
|
||||
- Fix version drift: `xteve.go` says `2.2.0.0200`, `changelog-beta.md` says `0201`. Add the drift check that the changelog claims exists to `.drone.yml` (compare `grep` of both and fail on mismatch), or drop the hardcoded default and make the changelog the single source.
|
||||
- **Delete the auto-updater** (decided 2026-09-25: Docker is the only distribution channel). As shipped, a fork build will download and install an upstream binary over itself if upstream ever tags a higher build. Remove `BinaryUpdate` and the `internal/up2date` package, the calls at `xteve.go:191` and `src/maintenance.go:69`, the `GitHub` struct and `-X`-style branch/user plumbing in `xteve.go`, `config.go:62,189-190` and `info.go:25`, and the `xteveAutoUpdate` setting from `struct-system.go:305`, `ts/base_ts.ts:26`, `ts/settings_ts.ts:290,484` and `html/lang/en.json`. **Keep** `conditionalUpdateChanges`, `convertToNewFilter` and `setValueForUUID` in `src/update.go`; they are settings-schema migrations, not binary updates. Rename that file to `migrate.go` so the distinction is obvious. This also removes the `kardianos/osext` dependency.
|
||||
- Pin Go to 1.27.1 in `go.mod`, the Dockerfile and the Drone golang image (decided 2026-09-26).
|
||||
- Fix the four `go vet` unreachable-code warnings (`buffer.go:733`, `maintenance.go:78`, `webserver.go:413`, `xepg.go:356`).
|
||||
- Install a current staticcheck (or use golangci-lint) and record the baseline.
|
||||
|
||||
### Phase 1: Security (1 to 2 weeks, must do)
|
||||
|
||||
The threat model to design for: an attacker who can serve you a playlist or EPG feed, or who can get a browser on your LAN to load a page. Both are realistic for an IPTV proxy.
|
||||
|
||||
**1a. Browser-side (small, do first)**
|
||||
- Stored XSS: provider-controlled strings (channel name, group, file name, log lines, descriptions) are written with `innerHTML`. Replace with `textContent` at `ts/menu_ts.ts:534, 589, 2362-2390`, `ts/logs_ts.ts:19`, `ts/configuration_ts.ts:91`, `ts/settings_ts.ts:591`. The unauthenticated `/stream/` endpoint logs the caller's User-Agent, so this is reachable without a malicious playlist.
|
||||
- Websocket origin: `wsUpgrader.CheckOrigin` in `src/webserver.go:21` returns true. Compare `Origin` host to `r.Host` and reject mismatches. Move the token out of the query string into the cookie, and stop logging requests and responses (with tokens) to the console in `ts/network_ts.ts`.
|
||||
- Cookie flags: set `HttpOnly`, `SameSite=Strict` on the Token cookie (`src/internal/authentication/authentication.go:601`), and stop writing it from JS.
|
||||
- Serving the wizard page sets `Settings.AuthenticationWEB = false` (`src/webserver.go:650`). A GET must not mutate global settings. Gate the wizard on `wizard.completed` only.
|
||||
|
||||
**1b. Server-side input handling (small to medium)**
|
||||
- `uploadLogo` writes to a client-supplied filename with no sanitisation (`src/images.go:19`). Apply `filepath.Base` and an extension allow-list.
|
||||
- Backup restore has no zip-slip check (`src/compression.go:96`). Reject entries whose cleaned path escapes the target directory.
|
||||
- `ffmpeg.path` / `vlc.path` accept any existing file, and `ffmpeg.options` is free text. Combined with the open websocket this is remote code execution. Options: restrict paths to a configured allow-list (Docker image ships a fixed ffmpeg), or require authentication to change them, or both.
|
||||
- Put `/download/` (backups containing `authentication.json` and the Plex token) behind authentication.
|
||||
- Stop deriving `System.Domain` from the request Host header on every request (`setGlobalDomain`). Use a configured base URL with Host as a fallback only when unset.
|
||||
- Provider URLs reach ffmpeg's `-i` unchanged; reject non-http(s) schemes so `file:` and `concat:` sources are not possible.
|
||||
|
||||
**1c. Authentication (small to medium, needs migration)**
|
||||
- `SHA256(secret, salt)` ignores the salt and computes an unsalted HMAC with the password as key. Replace with bcrypt (`golang.org/x/crypto/bcrypt`), migrate each user on next successful login, and use constant-time comparison.
|
||||
- Tokens minted for URL/Basic auth calls are never evicted (`authentication.go:243, 575-585`). Plex polls constantly, so memory grows for the life of the process. Add expiry and a periodic sweep.
|
||||
- Any authenticated web user can change any other user's credentials (`src/data.go:606`). Enforce that only admins can edit other users.
|
||||
- Plex token is stored in plaintext in `settings.json` (mode 0644) and pushed to every websocket client. Write settings with 0600 and redact the token in the config payload sent to the UI.
|
||||
- Web authentication stays off by default (LAN-only deployment, decided 2026-09-25). Document this in the README security note.
|
||||
|
||||
### Phase 2: Stability and correctness in the streaming path (2 to 3 weeks)
|
||||
|
||||
This is where the "works well until it doesn't" risk sits. Start with `go test -race` tests that spin up two concurrent tuners against a local `httptest` server that streams a fake TS file, then fix:
|
||||
|
||||
- **Shared maps without locks.** `Playlist` is stored by value in a `sync.Map` but contains maps, so every copy shares them. `bufferingStream`, `connectToStreamingServer` and `killClientConnection` write those maps from different goroutines, some under `Lock`, some not (`src/buffer.go:101, 125, 189, 208, 462, 480, 575, 929`). Give `Playlist` its own mutex, store `*Playlist`, and remove the store-back of stale copies at lines 756 and 930.
|
||||
- **Tuner limit is a check-then-act race** (`buffer.go:152`). Reserve the slot under the playlist lock.
|
||||
- **`force=true` in `killClientConnection`** deletes the stream but never cleans `BufferClients`.
|
||||
- **`Data.Cache.StreamingURLS`** is written by `/lineup.json` and `/m3u` while `/stream/` reads it (`src/system.go:338-390`). Guard with an RWMutex.
|
||||
- **Deferred closes inside read loops** (`buffer.go:880, 891`, also 316, 707-737; `compression.go:106, 112`; `imgcache/cache.go:106, 118`). A multi-hour stream accumulates millions of deferred calls. Close explicitly or move the loop body into a function.
|
||||
- **ffmpeg/VLC process management** (`buffer.go:1345-1641`): timeout goroutine can block forever, the 20 s timeout cannot fire while `Read` blocks, `cmd.Start()` error ignored, `panic` inside a goroutine kills the process, double-open of the segment file leaks an fd. Rewrite around `exec.CommandContext` with the request context, which also replaces the deprecated `http.CloseNotifier`. **Decided 2026-09-25: keep VLC.** Structure the rewrite as one external-process buffer that takes a binary path plus an argument builder, with ffmpeg and VLC as two small builders. VLC then costs nothing to keep, the process-management fixes apply to both, and tests can cover the builders without VLC installed. The Docker image continues to ship ffmpeg only.
|
||||
- **Fake mutexes** in `src/screen.go:55, 115, 130` (a new `sync.RWMutex{}` per call). Use one package-level mutex for the log.
|
||||
- **Timeouts everywhere.** Replace `http.ListenAndServe` with an `http.Server` with read/write/idle timeouts, and give every outbound `http.Client` a timeout (provider fetch, logo download, update check). A hung provider currently leaves `ScanInProgress` stuck.
|
||||
- **imgcache** holds its lock during every logo download, blocking `/m3u/` and the XMLTV build. Download outside the lock.
|
||||
- **Atomic file writes.** Every save is a truncate-then-write. Write to a temp file in the same directory, fsync, rename. One helper, used by `writeByteToFile` and friends.
|
||||
- **Concrete bugs to fix while there:** `xepg.go:249` appends instead of removing (`Files[:i]`); `data.go:882` mutates a slice while ranging; `screen.go:193` drops newest log lines when full; `buffer.go:161-163` sets headers after `WriteHeader` and names one `"Content-Length:"`; `screen.go:412` evicts random notifications; unchecked type assertions and indexing in `data.go`, `backup.go`, `provider.go`, `authentication.go:89`, `screen.go:104`; API double-write after `responseAPIError` (`webserver.go:974-978`); websocket `request`/`response` structs reused across iterations so error state and stale fields leak (`webserver.go:334-335`).
|
||||
- **Error hygiene:** the `authenticationErr` closures return from the closure not the caller; remove `os.Exit`/`log.Fatal`/`panic` outside `main`; exit non-zero on fatal errors; check ignored results of `getProviderData`, `saveSettings`, `writeByteToFile`.
|
||||
|
||||
### Phase 3: Build, embed, CI, Docker (1 week, low risk, big quality-of-life win)
|
||||
|
||||
- **Replace `src/webUI.go` with `//go:embed html`.** The generated file is 783 KB, map-ordered so it changes on every run, and appears in most UI commits. Serve via `fs.Sub` and `http.FileServer`; keep template substitution for HTML only. In `-dev` mode use `os.DirFS("html")`. Delete `src/html-build.go` and `cmd/webui-gen`. Add cache headers and ETags.
|
||||
- **Drop i18n** (decided 2026-09-26). The JS currently contains Go template placeholders (`{{.settings.x.title}}`) so it cannot be bundled, cached, or run outside the server. Only English exists. Inline the English strings from `html/lang/en.json` into the TypeScript, delete `en.json` and the template pass for JS and CSS, and keep `parseTemplate` only for the HTML pages that need server values. This is a prerequisite for the next item. Mechanical approach: a one-off script that reads `en.json` and rewrites every `{{.path}}` in `ts/*.ts` with the string literal, then a review pass.
|
||||
- **Pinned TypeScript toolchain.** Add `package.json` and `tsconfig.json` (strict, ES2020 target), bundle with esbuild to one unminified `html/js/app.js`, and run `tsc --noEmit` in CI. **The bundle is committed** (decided 2026-09-26) so `go build` works from a bare clone and the Dockerfile needs no Node stage; CI rebuilds it and fails on a diff. Delete the ten unreferenced legacy files in `html/js/` (`mapping-editor.js` alone is 1465 lines) which are currently embedded in every binary.
|
||||
- **CI hardening in `.drone.yml`:** add `go vet`, golangci-lint, `gofmt -l`, `go test -race`, `tsc --noEmit`, and a check that committed build outputs are current. Publish images tagged with the version as well as `latest` and SHA. amd64 only (decided 2026-09-26); no multi-arch step.
|
||||
- **Docker runtime:** UID/GID are fixed at build time so prebuilt images cannot change them. Switch to the PUID/PGID + su-exec pattern in the entrypoint. Pin `mwader/static-ffmpeg` to a tag. Add `VOLUME`. Verify the healthcheck still passes with web auth enabled. Remove `/xteve` from `LEGACY_CONFIG_DIRS` (it is the parent of the default dir). Note in the README that SSDP needs host networking.
|
||||
- **Add `/healthz`** as the README suggests, so the healthcheck and monitoring do not depend on HDHomeRun endpoints.
|
||||
- **Docs:** write `README-DEV.md` (build, dev mode, UI toolchain, release and versioning process) and add a fork section to `README.md` (registry, compose files, env vars `XTEVE_CONFIG`/`XTEVE_PORT`/PUID/PGID, differences from upstream, security notes). Remove the upstream donation block.
|
||||
- **Code cleanup (moved from Phase 4):** deduplicate the image-caching goroutine (`xepg.go:78-93` and `123-138`), `addErrorToStream` (`buffer.go:581`, `1379`), `extractZIP`/`mapToJSON`/`randomString`; delete dead code (`Auto`, `getStreamByChannelID`, `updateXEPG`, `indexOfInt`, commented-out blocks in `struct-buffer.go` and `authentication.go`); drop `io/ioutil` (12 files), `kardianos/osext`, `rand.Seed`. Do this after go:embed lands so the diffs stay readable.
|
||||
- Note: `agent.md` references `docs/design-system/` and `tasks/design-foundation.md`, neither of which exist in this repo. Either add them or remove that section.
|
||||
|
||||
### Phase 4: Data model and performance (NOT PLANNED)
|
||||
|
||||
**Decided 2026-09-25: skipped.** The real deployment is one M3U with 174 streams and 167 XEPG channels, and that is not expected to change. None of the quadratic passes matter at that size. The list below is kept for reference only; the non-performance cleanup items (dead code, duplicates, deprecated packages) have moved to Phase 3. Revisit only if a rebuild ever takes more than a few seconds.
|
||||
|
||||
- `Data.XEPG.Channels` is `map[string]any` and every pass does `json.MarshalIndent` then `Unmarshal` per channel (`xepg.go:370, 389, 405, 462, 659, 889, 1265`). Type it as `map[string]*XEPGChannelStruct`.
|
||||
- `getProgramData` scans every programme for every channel (`xepg.go:961`). Index programmes by channel ID once per build.
|
||||
- `xepg.json` is written three times per rebuild; `settings.json` once per provider; `urls.json` on every `/lineup.json` request. Debounce and write once.
|
||||
- `filterThisStream` compiles regexps per stream per filter (`m3u.go:65, 77`). Precompile.
|
||||
- `buildM3U` uses string concatenation; use `strings.Builder`. The XMLTV output is built entirely in memory then copied; stream it to the gzip writer.
|
||||
- `Web()` re-parses the language JSON per request (`webserver.go:614-634`). Parse once.
|
||||
- Parsed XMLTV is cached forever (`xepg.go:1221`). Drop it after the build.
|
||||
- Deduplicate: image-caching goroutine (`xepg.go:78-93` and `123-138`), `addErrorToStream` (`buffer.go:581`, `1379`), `extractZIP`/`mapToJSON`/`randomString` across packages. Delete dead code (`Auto`, `getStreamByChannelID`, `updateXEPG`, `indexOfInt`, commented-out blocks in `struct-buffer.go` and `authentication.go`).
|
||||
- Modernise: drop `io/ioutil` (12 files), `kardianos/osext` (use `os.Executable`), `rand.Seed`, `http.CloseNotifier`; adopt `context`, `slog`, `errors.Is`, `slices`, `maps`.
|
||||
|
||||
### Phase 5: Frontend architecture (2 to 4 weeks, medium risk)
|
||||
|
||||
The UI redesign left a consistent visual layer. The problems are underneath it.
|
||||
|
||||
- **Websocket client:** each request opens a new socket, and a global flag silently drops any request made while one is in flight (`ts/network_ts.ts:9-11`). Replace with one persistent socket, request IDs, a promise per request, a queue, and exponential-backoff reconnect. This is what "reconnection" should mean instead of a failure counter.
|
||||
- **Stop rebuilding the whole UI on every response.** `createLayout()` re-renders everything, losing focus and scroll position. Diff by section, or at minimum re-render only the active tab.
|
||||
- **Mapping table:** renders every channel with no virtualisation or pagination. Add windowed rendering; this is the single largest UX win for big lineups.
|
||||
- **Code structure:** `menu_ts.ts` is 2472 lines with 34 string `onclick` handlers (one typo'd `javscript:` at line 551). Split into modules, use `addEventListener`, make the settings table data-driven (about 20 copy-pasted blocks in `settings_ts.ts:24-437`).
|
||||
- **Accessibility follow-ups:** buttons instead of clickable `<td>`, `<button>` instead of `<input type=button>`, replace `alert()` with the existing announcer, drop deprecated `language="javascript"`. Add a light theme via `prefers-color-scheme` using the existing `:root` tokens.
|
||||
- Merge `base.css` and `screen.css` into a layered structure (tokens, base, components, layout) so the boundary is clear.
|
||||
|
||||
### Phase 6: Features worth borrowing (optional, after 1 to 3)
|
||||
|
||||
Threadfin is the most active successor fork. Its additions that fit this codebase without a rewrite:
|
||||
|
||||
- Regex include/exclude filters (the current filter is substring based).
|
||||
- Per-playlist tuner limits and per-playlist buffer choice.
|
||||
- Bulk channel editing in the mapping table (the fork already has range-select checkboxes; this is the next step).
|
||||
- Working backup/restore with schema versioning (ties into the atomic-write work in Phase 2).
|
||||
- Dummy EPG with selectable durations.
|
||||
|
||||
## 4. Test strategy
|
||||
|
||||
There is one test today. Aim for the following minimum before each phase's refactor:
|
||||
|
||||
| Area | Test type | Phase |
|
||||
|---|---|---|
|
||||
| Websocket command dispatch | `httptest` + gorilla client, one test per `cmd` | 1 |
|
||||
| Auth: login, token expiry, bcrypt migration, user edit permissions | unit | 1 |
|
||||
| Upload/restore path traversal | unit with crafted names and zips | 1 |
|
||||
| Buffer: two concurrent tuners, tuner limit, client disconnect cleanup | integration with `-race`, fake TS server | 2 |
|
||||
| Atomic write helper: crash simulation leaves old file intact | unit | 2 |
|
||||
| XEPG mapping: strict vs relaxed, auto-remap with 0/1/2 candidates | table-driven unit on fixtures | 2, 4 |
|
||||
| M3U/XMLTV parsing and filtering | extend existing parser tests | 4 |
|
||||
| XEPG build benchmark | `go test -bench` on 2000-channel fixture | 4 |
|
||||
| TypeScript | `tsc --noEmit` in CI; consider Vitest for the websocket client once it is a module | 3, 5 |
|
||||
|
||||
## 5. What this plan deliberately does not do
|
||||
|
||||
- **No framework migration now.** A Preact/Svelte rewrite is the largest item on the list and blocks security and stability fixes. Phase 5 makes the vanilla code modular enough that a later migration can be incremental.
|
||||
- **No upstream merge.** There is nothing to merge; upstream is inactive.
|
||||
- **No new features before Phase 3.** The value is in stopping the process from crashing or being hijacked, and in making the next change cheap to ship.
|
||||
|
||||
## 6. Suggested order and rough timeline
|
||||
|
||||
| Order | Phase | Effort | Why this position |
|
||||
|---|---|---|---|
|
||||
| 1 | 0 Hygiene | 0.5 day | Free, removes foot-guns (auto-update from upstream) |
|
||||
| 2 | 1a Browser security | 1 to 2 days | Smallest fix for the worst exposure |
|
||||
| 3 | 3 go:embed + TS toolchain + CI | 1 week | Every later PR gets smaller and safer to review |
|
||||
| 4 | 1b, 1c Server security + auth | 1 week | Needs the test harness from step 3's CI |
|
||||
| 5 | 2 Streaming stability | 2 to 3 weeks | Highest complexity; do with race tests in place |
|
||||
| 6 | 5 Frontend architecture | 2 to 4 weeks | Builds on the bundled TS from step 3 |
|
||||
| 7 | 6 Features | as desired | Only once the base is solid |
|
||||
|
||||
## 7. Open questions
|
||||
|
||||
These change the shape of the work and should be settled before starting:
|
||||
|
||||
1. ~~Is the deployment always Docker on a trusted LAN, or is xTeVe ever exposed beyond it?~~ **Decided 2026-09-25: trusted LAN only.** Consequences: keep all of Phase 1a and 1b (a LAN browser or a hostile playlist is still in scope), keep bcrypt and token eviction in 1c, but do not change the auth-off default for fresh installs. Restrict `ffmpeg.path` to an allow-list rather than requiring auth to change it.
|
||||
2. ~~How large are your playlists and EPG files?~~ **Decided 2026-09-25: 174 streams / 167 channels, one M3U, stable. Phase 4 dropped.**
|
||||
3. ~~Do you use the VLC buffer?~~ **Decided 2026-09-25: ffmpeg in practice, but keep VLC as an option.** Implemented as a shared external-process buffer with per-tool argument builders (see Phase 2).
|
||||
4. ~~Is the auto-updater used?~~ **Decided 2026-09-25: Docker only. Updater deleted in Phase 0; settings migrations in `update.go` are kept.**
|
||||
5. ~~Is multi-arch (arm64) needed for the image?~~ **Decided 2026-09-26: amd64 only.**
|
||||
6. ~~Keep the language layer?~~ **Decided 2026-09-26: drop it. English strings inlined in the TypeScript.**
|
||||
7. ~~Where does the TypeScript build run?~~ **Decided 2026-09-26: commit the esbuild bundle; CI verifies freshness; no Node in Docker.**
|
||||
@@ -0,0 +1,83 @@
|
||||
# xTeVe improvement checklist
|
||||
|
||||
Detailed rationale, file references and effort estimates: `tasks/improvement-plan.md`.
|
||||
Status: all phases done on branch `improvements` (2026-09-26). Open: the `agent.md` design-system reference (user decision) and two items kept by design (Host-derived URLs, no user roles).
|
||||
|
||||
## Phase 0: Hygiene
|
||||
- [x] `.gitignore` (`.gocache/`), deleted `.gocache/`, extended `.dockerignore`
|
||||
- [x] `agent.md` / `skill.md` git-ignored and docker-ignored (left in place)
|
||||
- [x] Version drift fixed (`xteve.go` now 0201) and Drone drift check added
|
||||
- [x] Auto-updater deleted (`BinaryUpdate`, `internal/up2date`, `GitHub`/`Update` structs, `xteveAutoUpdate` + `update.url` settings, UI rows, `en.json`); migrations kept in `migrate.go`; `kardianos/osext` gone
|
||||
- [x] `go 1.27.1`; Dockerfile and Drone golang images pinned to 1.27.1
|
||||
- [x] Four `go vet` unreachable-code warnings fixed (vet clean)
|
||||
- [x] staticcheck baseline via `go run honnef.co/go/tools/cmd/staticcheck@latest ./...`: 408 findings (S1002 113, SA5008 79, S1039 67, S1038 40, S1023 35, ST1005 20, SA1019 14, SA4006 9, misc 11). Mostly style; SA5008/SA4006/SA1019 worth a pass in Phase 3 cleanup
|
||||
|
||||
## Phase 1: Security
|
||||
- [x] `textContent` for provider-controlled strings (cells, client info, in-place edits, logs, popup descriptions with an explicit static-HTML flag)
|
||||
- [x] Websocket uses gorilla's same-origin check; token read from the HttpOnly cookie (query param kept for legacy clients); payload/token console logging removed. Tests in `src/websocket_test.go`
|
||||
- [x] Cookie `HttpOnly`, `SameSite=Strict`, `Path=/`, cleared on logout
|
||||
- [x] Wizard GET no longer mutates `AuthenticationWEB`; the wizard page just bypasses login while active
|
||||
- [x] `uploadLogo` filename sanitised (base name, image extensions only)
|
||||
- [x] Zip-slip guard in `extractZIP` (also no more defer-in-loop there)
|
||||
- [x] `ffmpeg.path`/`vlc.path` must be a regular file named ffmpeg/vlc/cvlc; stream URLs handed to the external buffer must use a network scheme
|
||||
- [x] `/download/` requires the web session when web auth is on
|
||||
- [~] Host-header-derived URLs kept by design (LAN, many interfaces; URLs must match how the client reached the server)
|
||||
- [x] bcrypt for new passwords and credential changes; legacy SHA256 records verified in constant time and upgraded on first login; username compare constant-time
|
||||
- [x] Expired tokens evicted on every new session; URL/Basic auth no longer mint tokens at all (`AuthenticateUser`)
|
||||
- [~] Not applicable: xTeVe has no roles, every web user is an administrator by design (documented in README security notes)
|
||||
- [x] `settings.json` written 0600; Plex token masked in every payload to the UI, mask round-trips as "unchanged" on save
|
||||
- [x] Decide default for web auth on fresh installs (keep off; LAN only, decided 2026-09-25)
|
||||
|
||||
## Phase 2: Streaming stability
|
||||
- [x] Race tests in `src/buffer_test.go`: restream shares one provider connection, tuner limit atomic under 6 concurrent tunes, acquire/release contention, cleanup on last client; run with `-race`
|
||||
- [x] New `src/buffer_state.go`: one `bufferMu`, playlists by pointer, one `bufferStream` per stream (status, client count, error, cancel hook); downloaders keep a private working copy and publish through helpers. `BufferInformation`/`BufferClients`/`Lock` removed
|
||||
- [x] Tuner check and registration under one lock (`bufferAcquireStream`); last client out removes the stream, cancels its process and deletes its folder
|
||||
- [x] `streamingURLsMu` around `Data.Cache.StreamingURLS`; persisted from a snapshot
|
||||
- [x] No more `defer` inside the buffer read/redirect loops (compression done in Phase 1; imgcache in 2b)
|
||||
- [x] `thirdPartyBuffer` rewritten: `exec.CommandContext` cancelled when the last client leaves or the 20 s startup watchdog fires; `buildFFmpegArgs`/`buildVLCArgs` tested; no panic, `cmd.Start` checked, one file handle per segment. Client loop uses `r.Context()` instead of `CloseNotifier`
|
||||
- [x] `logMu` guards `WebScreenLog` and notifications; accessors `logAppend`/`webScreenLogSnapshot`/`notificationsSnapshot`; ring buffer keeps the newest N (was dropping them)
|
||||
- [x] `http.Server` with `ReadHeaderTimeout` 15 s and `IdleTimeout` 120 s (no write timeout, streams are long-lived); `providerHTTPClient` 5 min, `apiHTTPClient` 30 s, imgcache client 30 s, stream client with dial/TLS/header timeouts
|
||||
- [x] imgcache downloads outside the lock; cache URL bug fixed (was a filesystem path); query-string URLs no longer produce unservable file names
|
||||
- [x] `writeFileAtomic` (temp + fsync + rename) behind `writeByteToFile`, `saveMapToJSONFile`, `writePrivateFile` and the auth database
|
||||
- [x] Buffer: headers set before `WriteHeader`, bogus `Content-Length:` header gone, segments flushed as they arrive. xepg file-removal bug, dead range mutation, API double write, WS struct reuse, unchecked assertions in data/backup/provider/screen, notification eviction by age: all fixed with tests
|
||||
- [x] Error hygiene: no `panic`/`os.Exit`/`log.Fatal` outside `main`; SIGINT/SIGTERM handled in main via `src.Shutdown()`; fatal start-up errors exit 1
|
||||
|
||||
## Phase 3: Build, embed, CI, Docker
|
||||
- [x] `//go:embed` via `html/embed.go`; deleted `webUI.go`, `html-build.go`, `cmd/webui-gen`; ETag + Cache-Control on static assets
|
||||
- [x] i18n dropped: 254 placeholders inlined, `en.json` deleted, only HTML pages templated (`authenticationErr`)
|
||||
- [x] `package.json` + `ts/tsconfig.json` (tsc 5.9.3, ES2020, single outFile `html/js/app.js`, committed, CI-verified); 10 dead JS files deleted; 6 tsc errors fixed incl. a real ASI bug in the search shortcut
|
||||
- [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
|
||||
- [x] Version-tagged images: master publishes `latest`, the commit SHA and the changelog version via a `.tags` file (amd64 only)
|
||||
- [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] `README-DEV.md` and fork-specific `README.md` (about, container usage, env vars, security notes)
|
||||
- [x] staticcheck 399 -> 0; dead code and duplicates removed; `ioutil`/`rand.Seed` gone. Real bugs fixed on the way: leaked file handle per ffmpeg segment, unchecked `http.NewRequest`, migration writing `null` xepg.json, silent user-write failures, unwritable config/temp dir now fatal at start
|
||||
- [ ] Resolve missing `docs/design-system/` referenced by `agent.md` (user decision: agent.md is a personal, git-ignored file)
|
||||
|
||||
## Phase 4: Data model and performance
|
||||
Not planned (lineup is ~170 channels, decided 2026-09-25). See plan for the reference list.
|
||||
|
||||
## Phase 5: Frontend architecture
|
||||
- [x] Persistent websocket: one socket per page, queued requests with ids echoed by the server, 12 s per-request timeout, exponential-backoff reconnect, log-poll dedupe. Server keeps serving on one connection and closes it on exit (upstream leaked one open socket per request). Test: `TestWSServesMultipleCommandsPerConnection`
|
||||
- [x] `createLayout()` rebuilds the menu list only when the set of visible items changes; client info and status cards refresh in place (jsdom test keeps the same nodes and focus)
|
||||
- [~] Virtualised mapping table skipped: ~170 channels render instantly, and sorting/search operate on DOM rows, so virtualisation would add risk for no gain (same reasoning as dropping Phase 4)
|
||||
- [x] `menu_ts.ts` split into `menu.ts`, `content.ts`, `popup.ts`, `xmltv.ts` (all `_ts` suffixes dropped); all 94 string `on*` handlers replaced by `addEventListener`; settings rows generated from `SETTINGS_FIELDS`; password fields now saved (Plex token); 27 jsdom tests under `tests/` run by `npm test` in CI
|
||||
- [x] Live-region announcements alongside alerts, `lang`, single h1, `<main>` landmark; light theme via `prefers-color-scheme` with `data-theme` override (contrast measured). `<input type=button>` kept because every button style keys on it
|
||||
- [x] CSS split into tokens / base / components / layout, every colour a token, `screen.css` removed
|
||||
|
||||
## Phase 6: Optional features
|
||||
- [x] Regex filters: `regex-filter` type (validated on save, cached compile, matches name, then group, then raw line) with a popup form and table label; tested in Go and jsdom
|
||||
- [x] Per-playlist tuner limit already existed. Per-playlist buffer: `buffer` select in the playlist popup (default / none / xTeVe / FFmpeg / VLC) backed by `bufferModeFor`; tuner field stays editable when the playlist's own buffer is active
|
||||
- [x] Already present in the fork (bulk edit with shift-range selection); nothing to add
|
||||
- [x] Already present: restore checks the backup's version against `System.Compatibility`; writes are now atomic (Phase 2)
|
||||
- [x] Already present: `xTeVe Dummy` offers several durations in the mapping dropdown
|
||||
|
||||
## Review
|
||||
|
||||
### Phase 0 (2026-09-26, branch `improvements`)
|
||||
- `go build`, `go vet`, `go test ./...` all clean. `src/webUI.go` regenerated.
|
||||
- Smoke run against an empty config dir on port 34499: `/web/`, `/lineup_status.json`, `/discover.json` all 200, no errors logged.
|
||||
- Not verified locally: Docker image build (no daemon on this machine); Drone will cover it.
|
||||
- Compiled JS in `html/js/` was hand-edited to mirror the TS change because a current `tsc` does not reproduce the committed output (different shim, 5 type errors in `menu_ts.ts`). Phase 3 replaces this with a pinned toolchain.
|
||||
- `html/js/menu.js` still mentions `xteveAutoUpdate`; it is one of the ten dead legacy files scheduled for deletion in Phase 3.
|
||||
- `Settings.Branch` (`git.branch`) was kept: it still drives whether the build number is shown in the UI and XMLTV header.
|
||||
@@ -0,0 +1,218 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const vm = require("vm");
|
||||
|
||||
const {loadApp, readBundle} = require("./harness");
|
||||
const {makeServer, M3U_ID} = require("./fixture");
|
||||
|
||||
function menuKeys(document) {
|
||||
return Array.from(document.querySelectorAll("#main-menu li")).map(li => li.getAttribute("data-menu"));
|
||||
}
|
||||
|
||||
function clickMenu(document, key) {
|
||||
const item = document.querySelector('#main-menu li[data-menu="' + key + '"]');
|
||||
assert.ok(item, "menu item " + key + " is rendered");
|
||||
item.click();
|
||||
return item;
|
||||
}
|
||||
|
||||
test("bundle evaluates without throwing", () => {
|
||||
const app = loadApp();
|
||||
assert.equal(typeof app.window.createLayout, "function");
|
||||
assert.equal(typeof app.window.PageReady, "function");
|
||||
assert.equal(app.evalIn("typeof Server"), "function");
|
||||
assert.equal(app.evalIn("typeof WSClient"), "function");
|
||||
assert.equal(app.evalIn("menuItems.length"), 8);
|
||||
});
|
||||
|
||||
test("bundle evaluates against a bare window without a document", () => {
|
||||
const context = vm.createContext({
|
||||
window: {},
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
WebSocket: function() {},
|
||||
});
|
||||
context.window = context;
|
||||
assert.doesNotThrow(() => vm.runInContext(readBundle(), context, {filename: "app.js"}));
|
||||
});
|
||||
|
||||
test("createLayout hides users and logout while web authentication is off", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
|
||||
const keys = menuKeys(app.document);
|
||||
assert.deepEqual(keys, ["playlist", "filter", "xmltv", "mapping", "settings", "log"]);
|
||||
assert.equal(app.document.getElementById("version").textContent, "2.5.0");
|
||||
assert.equal(app.document.querySelectorAll("#status-cards .status-card").length, 6);
|
||||
});
|
||||
|
||||
test("createLayout shows users and logout once web authentication is on", () => {
|
||||
const app = loadApp({server: makeServer({settings: {"authentication.web": true}})});
|
||||
app.window.createLayout();
|
||||
|
||||
const keys = menuKeys(app.document);
|
||||
assert.ok(keys.includes("users"), "users item present");
|
||||
assert.ok(keys.includes("logout"), "logout item present");
|
||||
assert.equal(keys.length, 8);
|
||||
});
|
||||
|
||||
test("createLayout hides mapping and xmltv unless the EPG source is XEPG", () => {
|
||||
const app = loadApp({server: makeServer({clientInfo: {epgSource: "PMS"}, settings: {epgSource: "PMS"}})});
|
||||
app.window.createLayout();
|
||||
|
||||
const keys = menuKeys(app.document);
|
||||
assert.ok(!keys.includes("mapping"), "mapping hidden");
|
||||
assert.ok(!keys.includes("xmltv"), "xmltv hidden");
|
||||
assert.ok(keys.includes("playlist"), "playlist still present");
|
||||
});
|
||||
|
||||
test("createLayout opens the first menu item when nothing is active", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
|
||||
const active = app.document.querySelector("#main-menu li.menu-active");
|
||||
assert.ok(active, "an item is marked active");
|
||||
assert.equal(active.getAttribute("data-menu"), "playlist");
|
||||
assert.equal(app.document.body.getAttribute("data-active-menu"), "playlist");
|
||||
assert.equal(app.document.querySelectorAll("#content_table tr").length, 2, "header plus one playlist row");
|
||||
});
|
||||
|
||||
test("mapping menu renders one row per channel with channel names as text", () => {
|
||||
const server = makeServer();
|
||||
const app = loadApp({server});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "mapping");
|
||||
|
||||
const table = app.document.getElementById("content_table");
|
||||
assert.ok(table, "mapping table rendered");
|
||||
|
||||
const channelCount = Object.keys(server.xepg.epgMapping).length;
|
||||
const rows = Array.from(table.querySelectorAll("tr")).slice(1);
|
||||
assert.equal(rows.length, channelCount);
|
||||
|
||||
const ids = rows.map(tr => tr.id).sort();
|
||||
assert.deepEqual(ids, Object.keys(server.xepg.epgMapping).sort());
|
||||
|
||||
const hostile = app.document.getElementById(M3U_ID + "-2");
|
||||
assert.ok(hostile, "row for the hostile channel name exists");
|
||||
assert.equal(hostile.querySelector("b"), null, "channel name is not parsed as markup");
|
||||
const names = rows.map(tr => tr.children[3].textContent);
|
||||
assert.ok(names.includes("<b>x</b>"), "channel name appears literally");
|
||||
assert.ok(names.includes("News 24"));
|
||||
|
||||
const playlists = rows.map(tr => tr.children[4].textContent);
|
||||
assert.ok(playlists.every(p => p === "Provider A"), "playlist name resolved from the file id");
|
||||
|
||||
const guides = rows.map(tr => tr.children[6].textContent).sort();
|
||||
assert.deepEqual(guides, ["-", "Guide A", "Guide A", "Guide A", "xTeVe Dummy"]);
|
||||
|
||||
const inactive = app.document.getElementById(M3U_ID + "-5");
|
||||
assert.equal(inactive.className, "notActiveEPG");
|
||||
assert.equal(app.document.getElementById(M3U_ID + "-1").className, "activeEPG");
|
||||
|
||||
const edit = hostile.querySelector(".mapping-edit-button");
|
||||
assert.ok(edit, "edit button rendered");
|
||||
assert.equal(edit.getAttribute("aria-label"), "Edit <b>x</b>");
|
||||
});
|
||||
|
||||
test("mapping edit button opens the popup for its channel", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "mapping");
|
||||
|
||||
const edit = app.document.getElementById(M3U_ID + "-3").querySelector(".mapping-edit-button");
|
||||
edit.click();
|
||||
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
assert.equal(popup.querySelector("h3").textContent, "MAPPING");
|
||||
assert.equal(popup.querySelector('input[name="x-name"]').value, "Sports HD");
|
||||
assert.equal(popup.querySelector('select[name="x-xmltv-file"]').value, "X-1-def.xml");
|
||||
assert.equal(popup.querySelector('select[name="x-mapping"]').value, "ch3.example");
|
||||
});
|
||||
|
||||
function expectedSettingsKeys(app, webAuth) {
|
||||
const all = app.evalIn("settingsCategory.map(function(c) { return c.settingsKeys }).join(',')").split(",");
|
||||
if (webAuth) {
|
||||
return all;
|
||||
}
|
||||
return all.filter(key => !["authentication.pms", "authentication.m3u", "authentication.xml", "authentication.api"].includes(key));
|
||||
}
|
||||
|
||||
function assertSettingsControls(app, keys) {
|
||||
const panel = app.document.getElementById("content_settings");
|
||||
assert.ok(panel, "settings panel rendered");
|
||||
|
||||
const controls = Array.from(panel.querySelectorAll("input[name], select[name]"));
|
||||
assert.equal(controls.length, keys.length, "one control per settings key");
|
||||
|
||||
for (const key of keys) {
|
||||
const matches = controls.filter(c => c.getAttribute("name") === key);
|
||||
assert.equal(matches.length, 1, "exactly one control for " + key);
|
||||
}
|
||||
return controls;
|
||||
}
|
||||
|
||||
test("settings panel renders one input per settings key", () => {
|
||||
const server = makeServer();
|
||||
const app = loadApp({server});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "settings");
|
||||
|
||||
const keys = expectedSettingsKeys(app, false);
|
||||
const controls = assertSettingsControls(app, keys);
|
||||
const byName = Object.fromEntries(controls.map(c => [c.getAttribute("name"), c]));
|
||||
|
||||
assert.equal(byName["tuner"].tagName, "SELECT");
|
||||
assert.equal(byName["tuner"].value, "1");
|
||||
assert.equal(byName["buffer"].value, "ffmpeg");
|
||||
assert.equal(byName["epgSource"].value, "XEPG");
|
||||
assert.equal(byName["plex.token"].type, "password");
|
||||
assert.equal(byName["plex.token"].value, "********");
|
||||
assert.equal(byName["update"].value, "0000,1200");
|
||||
assert.equal(byName["files.update"].type, "checkbox");
|
||||
assert.equal(byName["files.update"].checked, true);
|
||||
assert.equal(byName["cache.images"].checked, false);
|
||||
assert.equal(byName["authentication.web"].checked, false);
|
||||
});
|
||||
|
||||
test("settings panel includes the per-service authentication keys when web auth is on", () => {
|
||||
const app = loadApp({server: makeServer({settings: {"authentication.web": true, "authentication.api": true}})});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "settings");
|
||||
|
||||
const keys = expectedSettingsKeys(app, true);
|
||||
const controls = assertSettingsControls(app, keys);
|
||||
const api = controls.find(c => c.getAttribute("name") === "authentication.api");
|
||||
assert.equal(api.checked, true);
|
||||
});
|
||||
|
||||
test("saveSettings sends only the changed controls", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "settings");
|
||||
|
||||
const tuner = app.document.querySelector('#content_settings select[name="tuner"]');
|
||||
tuner.value = "4";
|
||||
tuner.dispatchEvent(new app.window.Event("change", {bubbles: true}));
|
||||
|
||||
const agent = app.document.querySelector('#content_settings input[name="user.agent"]');
|
||||
agent.value = "Custom";
|
||||
agent.dispatchEvent(new app.window.Event("change", {bubbles: true}));
|
||||
|
||||
app.window.saveSettings();
|
||||
|
||||
const socket = app.sockets[app.sockets.length - 1];
|
||||
assert.ok(socket, "a websocket was opened");
|
||||
socket.readyState = 1;
|
||||
socket.onopen();
|
||||
|
||||
assert.equal(socket.sent.length, 1);
|
||||
const payload = JSON.parse(socket.sent[0]);
|
||||
assert.equal(payload.cmd, "saveSettings");
|
||||
assert.deepEqual(payload.settings, {"tuner": 4, "user.agent": "Custom"});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
"use strict";
|
||||
|
||||
// A SERVER payload shaped like the answer to getServerConfig. Every call
|
||||
// returns a fresh deep copy so tests can mutate it freely.
|
||||
|
||||
const M3U_ID = "M-1-abc";
|
||||
const XMLTV_ID = "X-1-def";
|
||||
const XMLTV_FILE = XMLTV_ID + ".xml";
|
||||
|
||||
function channel(number, name, extra) {
|
||||
return Object.assign({
|
||||
"_file.m3u.id": M3U_ID,
|
||||
"_file.m3u.name": "Provider A",
|
||||
"_file.m3u.path": "/config/data/" + M3U_ID + ".m3u",
|
||||
"_uuid.key": "CUID",
|
||||
"_uuid.value": "cuid-" + number,
|
||||
"group-title": "News",
|
||||
"name": name,
|
||||
"tvg-id": "",
|
||||
"tvg-logo": "http://logos.example/" + number + ".png",
|
||||
"tvg-name": name,
|
||||
"url": "http://stream.example/" + number,
|
||||
"x-active": true,
|
||||
"x-category": "",
|
||||
"x-channelID": String(number),
|
||||
"x-description": "",
|
||||
"x-group-title": "News",
|
||||
"x-mapping": "ch" + number + ".example",
|
||||
"x-name": name,
|
||||
"x-update-channel-icon": true,
|
||||
"x-update-channel-name": true,
|
||||
"x-xmltv-file": XMLTV_FILE,
|
||||
}, extra || {});
|
||||
}
|
||||
|
||||
function makeServer(overrides) {
|
||||
const server = {
|
||||
clientInfo: {
|
||||
arch: "darwin/arm64",
|
||||
os: "darwin",
|
||||
version: "2.5.0",
|
||||
uuid: "xteve-test-uuid",
|
||||
DVR: "192.168.1.10:34400",
|
||||
"m3u-url": "http://192.168.1.10:34400/m3u/xteve.m3u",
|
||||
"xepg-url": "http://192.168.1.10:34400/xmltv/xteve.xml",
|
||||
streams: "5",
|
||||
epgSource: "XEPG",
|
||||
xepg: "4",
|
||||
errors: "0",
|
||||
warnings: "1",
|
||||
},
|
||||
settings: {
|
||||
"api": false,
|
||||
"authentication.api": false,
|
||||
"authentication.m3u": false,
|
||||
"authentication.pms": false,
|
||||
"authentication.web": false,
|
||||
"authentication.xml": false,
|
||||
"backup.keep": 10,
|
||||
"backup.path": "/config/backup/",
|
||||
"buffer": "ffmpeg",
|
||||
"buffer.size.kb": 1024,
|
||||
"buffer.timeout": 500,
|
||||
"cache.images": false,
|
||||
"epgSource": "XEPG",
|
||||
"ffmpeg.options": "-hide_banner -loglevel error -i [URL] -c copy -f mpegts pipe:1",
|
||||
"ffmpeg.path": "/usr/bin/ffmpeg",
|
||||
"files.update": true,
|
||||
"plex.token": "********",
|
||||
"plex.url": "http://plex.example:32400",
|
||||
"temp.path": "/tmp/xteve/",
|
||||
"tuner": 1,
|
||||
"udpxy": "",
|
||||
"update": ["0000", "1200"],
|
||||
"use_plexAPI": false,
|
||||
"user.agent": "xTeVe",
|
||||
"vlc.options": "--intf dummy --no-audio --no-video --sout '#standard{access=file,mux=ts,dst=-}' [URL]",
|
||||
"vlc.path": "/usr/bin/cvlc",
|
||||
"xepg.missing.epg.mode": "strict",
|
||||
"xepg.replace.missing.images": true,
|
||||
files: {
|
||||
m3u: {
|
||||
[M3U_ID]: {
|
||||
"compatibility": {"group.title": 100, "stream.id": 100, "streams": 5, "tvg.id": 80},
|
||||
"counter.download": 3,
|
||||
"counter.error": 0,
|
||||
"description": "Main playlist",
|
||||
"file.source": "http://provider.example/list.m3u",
|
||||
"file.xteve": M3U_ID + ".m3u",
|
||||
"last.update": "2026-09-01 10:00:00",
|
||||
"name": "Provider A",
|
||||
"provider.availability": 100,
|
||||
"tuner": 2,
|
||||
"type": "m3u",
|
||||
},
|
||||
},
|
||||
hdhr: {},
|
||||
xmltv: {
|
||||
[XMLTV_ID]: {
|
||||
"compatibility": {"xmltv.channels": 40, "xmltv.programs": 12000},
|
||||
"counter.download": 3,
|
||||
"counter.error": 0,
|
||||
"description": "Main guide",
|
||||
"file.source": "http://provider.example/guide.xml",
|
||||
"file.xteve": XMLTV_FILE,
|
||||
"last.update": "2026-09-01 10:05:00",
|
||||
"name": "Guide A",
|
||||
"provider.availability": 100,
|
||||
"type": "xmltv",
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {},
|
||||
},
|
||||
xepg: {
|
||||
epgMapping: {
|
||||
[M3U_ID + "-1"]: channel(1, "News 24"),
|
||||
[M3U_ID + "-2"]: channel(2, "<b>x</b>"),
|
||||
[M3U_ID + "-3"]: channel(3, "Sports HD", {"x-group-title": "Sport", "group-title": "Sport", "x-category": "Sports"}),
|
||||
[M3U_ID + "-4"]: channel(4, "Kids TV", {"x-xmltv-file": "xTeVe Dummy", "x-mapping": "30_Minutes"}),
|
||||
[M3U_ID + "-5"]: channel(5, "Unmapped", {"x-active": false, "x-xmltv-file": "-", "x-mapping": "-"}),
|
||||
},
|
||||
xmltvMap: {
|
||||
[XMLTV_FILE]: {
|
||||
"ch1.example": {id: "ch1.example", "display-name": "News 24", icon: "http://logos.example/1.png"},
|
||||
"ch2.example": {id: "ch2.example", "display-name": "Channel Two", icon: ""},
|
||||
"ch3.example": {id: "ch3.example", "display-name": "Sports HD", icon: "http://logos.example/3.png"},
|
||||
},
|
||||
"xTeVe Dummy": {
|
||||
"30_Minutes": {id: "30_Minutes", "display-name": "30 Minutes", icon: ""},
|
||||
"60_Minutes": {id: "60_Minutes", "display-name": "60 Minutes", icon: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
StreamPreviewUI: {activeStreams: ["News 24", "Sports HD"], inactiveStreams: ["Unmapped"]},
|
||||
playlist: {m3u: {groups: {text: ["News (2)", "Sport (1)"], value: ["News", "Sport"]}}},
|
||||
},
|
||||
users: {},
|
||||
log: {log: {}},
|
||||
status: true,
|
||||
};
|
||||
|
||||
const copy = JSON.parse(JSON.stringify(server));
|
||||
if (overrides) {
|
||||
deepMerge(copy, overrides);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function deepMerge(target, source) {
|
||||
for (const key of Object.keys(source)) {
|
||||
const value = source[key];
|
||||
if (value && typeof value === "object" && !Array.isArray(value) && target[key] && typeof target[key] === "object") {
|
||||
deepMerge(target[key], value);
|
||||
} else {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {makeServer, M3U_ID, XMLTV_ID, XMLTV_FILE};
|
||||
@@ -0,0 +1,226 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {loadApp} = require("./harness");
|
||||
const {makeServer, M3U_ID, XMLTV_ID} = require("./fixture");
|
||||
|
||||
function clickMenu(document, key) {
|
||||
const item = document.querySelector('#main-menu li[data-menu="' + key + '"]');
|
||||
assert.ok(item, "menu item " + key + " is rendered");
|
||||
item.click();
|
||||
return item;
|
||||
}
|
||||
|
||||
function inlineHandlerAttributes(root) {
|
||||
const found = [];
|
||||
for (const element of root.querySelectorAll("*")) {
|
||||
for (const attr of element.attributes) {
|
||||
if (/^on/i.test(attr.name)) {
|
||||
found.push(element.tagName + "@" + attr.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function change(window, element) {
|
||||
element.dispatchEvent(new window.Event("change", {bubbles: true}));
|
||||
}
|
||||
|
||||
test("rendered pages carry no inline event handler attributes", () => {
|
||||
const app = loadApp({server: makeServer({settings: {"authentication.web": true}})});
|
||||
app.window.createLayout();
|
||||
|
||||
for (const key of ["playlist", "filter", "xmltv", "mapping", "users", "settings", "log"]) {
|
||||
clickMenu(app.document, key);
|
||||
assert.deepEqual(inlineHandlerAttributes(app.document.body), [], "no inline handlers after opening " + key);
|
||||
}
|
||||
|
||||
clickMenu(app.document, "playlist");
|
||||
app.document.getElementById(M3U_ID).click();
|
||||
assert.deepEqual(inlineHandlerAttributes(app.document.getElementById("popup-custom")), [], "no inline handlers in the m3u popup");
|
||||
});
|
||||
|
||||
test("menu items open their content through listeners", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
|
||||
clickMenu(app.document, "xmltv");
|
||||
const rows = app.document.querySelectorAll("#content_table tr");
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[1].id, XMLTV_ID);
|
||||
assert.equal(app.document.body.getAttribute("data-active-menu"), "xmltv");
|
||||
});
|
||||
|
||||
test("playlist rows open the edit popup and the New button walks the type chooser", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "playlist");
|
||||
|
||||
app.document.getElementById(M3U_ID).click();
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
assert.equal(popup.querySelector("h3").textContent, "M3U");
|
||||
assert.equal(popup.querySelector('input[name="name"]').value, "Provider A");
|
||||
assert.equal(popup.querySelector('select[name="tuner"]').value, "2");
|
||||
assert.ok(popup.querySelector('input[name="delete"]'), "existing playlist offers delete");
|
||||
|
||||
app.document.querySelector('#content-interaction input[name="playlist"]').click();
|
||||
assert.equal(popup.querySelector("h3").textContent, "PLAYLIST TYPE");
|
||||
const type = popup.querySelector("select#type");
|
||||
assert.deepEqual(Array.from(type.options).map(o => o.value), ["m3u", "hdhr"]);
|
||||
|
||||
type.value = "hdhr";
|
||||
change(app.window, type);
|
||||
app.document.getElementById("next").click();
|
||||
assert.equal(popup.querySelector("h3").textContent, "HDHR");
|
||||
assert.equal(popup.querySelector('input[name="back"]') != null, true, "new entry offers back instead of delete");
|
||||
assert.equal(popup.querySelector('input[name="delete"]'), null);
|
||||
|
||||
popup.querySelector('input[name="back"]').click();
|
||||
assert.equal(popup.querySelector("h3").textContent, "PLAYLIST TYPE");
|
||||
app.document.getElementById("next").click();
|
||||
assert.equal(popup.querySelector("h3").textContent, "M3U");
|
||||
|
||||
popup.querySelector('input[name="cancel"]').click();
|
||||
assert.equal(app.document.getElementById("popup").className, "none");
|
||||
});
|
||||
|
||||
test("filter New button offers group-title and custom filters", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "filter");
|
||||
|
||||
app.document.querySelector('#content-interaction input[name="filter"]').click();
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
assert.equal(popup.querySelector("h3").textContent, "FILTER");
|
||||
const type = popup.querySelector('select[name="type"]');
|
||||
assert.equal(type.value, "group-title");
|
||||
|
||||
type.value = "custom-filter";
|
||||
change(app.window, type);
|
||||
app.document.getElementById("next").click();
|
||||
assert.equal(popup.querySelector("h3").textContent, "CUSTOM");
|
||||
assert.equal(popup.querySelector('input[name="type"]').value, "custom-filter");
|
||||
assert.ok(popup.querySelector('input[name="filter"]'), "custom filter rule input");
|
||||
});
|
||||
|
||||
test("mapping popup marks edited controls and applies them on Done", () => {
|
||||
const server = makeServer();
|
||||
const app = loadApp({server});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "mapping");
|
||||
|
||||
const id = M3U_ID + "-1";
|
||||
app.document.getElementById(id).querySelector(".mapping-edit-button").click();
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
|
||||
const name = popup.querySelector('input[name="x-name"]');
|
||||
assert.equal(name.className, "");
|
||||
name.value = "Renamed";
|
||||
change(app.window, name);
|
||||
assert.equal(name.className, "changed");
|
||||
|
||||
const group = popup.querySelector('input[name="x-group-title"]');
|
||||
group.value = "Docs";
|
||||
change(app.window, group);
|
||||
|
||||
popup.querySelector('input[name="save"]').click();
|
||||
|
||||
const row = app.document.getElementById(id);
|
||||
assert.equal(row.children[3].textContent, "Renamed");
|
||||
assert.equal(row.children[5].textContent, "Docs");
|
||||
assert.equal(server.xepg.epgMapping[id]["x-name"], "Renamed");
|
||||
assert.equal(server.xepg.epgMapping[id]["x-group-title"], "Docs");
|
||||
assert.equal(app.document.getElementById("popup").className, "none");
|
||||
});
|
||||
|
||||
test("changing the XMLTV file in the mapping popup rebuilds the channel list", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "mapping");
|
||||
|
||||
app.document.getElementById(M3U_ID + "-1").querySelector(".mapping-edit-button").click();
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
|
||||
const file = popup.querySelector("#popup-xmltv");
|
||||
file.value = "xTeVe Dummy";
|
||||
change(app.window, file);
|
||||
|
||||
const mapping = popup.querySelector("#popup-mapping");
|
||||
const values = Array.from(mapping.options).map(o => o.value);
|
||||
assert.ok(values.includes("30_Minutes"), "dummy programs listed");
|
||||
assert.equal(mapping.className, "changed");
|
||||
assert.equal(popup.querySelector("#update-icon").className, "changed");
|
||||
|
||||
mapping.value = "60_Minutes";
|
||||
change(app.window, mapping);
|
||||
assert.equal(popup.querySelector("#active").checked, true);
|
||||
});
|
||||
|
||||
test("mapping table header sorts by channel name on click", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "mapping");
|
||||
|
||||
const header = app.document.querySelector("#content_table tr");
|
||||
const nameHeader = header.children[3];
|
||||
assert.equal(nameHeader.className, "pointer");
|
||||
nameHeader.click();
|
||||
|
||||
const names = Array.from(app.document.querySelectorAll("#content_table tr")).slice(1).map(tr => tr.children[3].textContent);
|
||||
assert.deepEqual(names, names.slice().sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1));
|
||||
});
|
||||
|
||||
test("bulk header checkbox selects every visible channel", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "mapping");
|
||||
app.document.querySelector('#content-interaction input[value="Bulk Edit"]').click();
|
||||
|
||||
const head = app.document.querySelector("#content_table tr input.bulk");
|
||||
head.click();
|
||||
|
||||
const boxes = Array.from(app.document.querySelectorAll("#content_table tr")).slice(1).map(tr => tr.querySelector("input.bulk"));
|
||||
assert.equal(boxes.length, 5);
|
||||
assert.ok(boxes.every(b => b.checked), "every row checkbox is ticked");
|
||||
assert.ok(boxes.every(b => b.className == "bulk showBulk"));
|
||||
});
|
||||
|
||||
test("filter New button offers a regular-expression filter", () => {
|
||||
const app = loadApp({server: makeServer()});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "filter");
|
||||
|
||||
app.document.querySelector('#content-interaction input[name="filter"]').click();
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
const type = popup.querySelector('select[name="type"]');
|
||||
const values = Array.from(type.options).map((o) => o.value);
|
||||
assert.deepEqual(values, ["group-title", "custom-filter", "regex-filter"]);
|
||||
|
||||
type.value = "regex-filter";
|
||||
change(app.window, type);
|
||||
app.document.getElementById("next").click();
|
||||
assert.equal(popup.querySelector("h3").textContent, "REGULAR EXPRESSION");
|
||||
assert.equal(popup.querySelector('input[name="type"]').value, "regex-filter");
|
||||
assert.ok(popup.querySelector('input[name="filter"]'), "regex rule input");
|
||||
assert.ok(popup.querySelector('input[name="caseSensitive"]'), "case sensitive checkbox");
|
||||
});
|
||||
|
||||
test("playlist popup offers a per-playlist buffer choice and keeps the tuner editable", () => {
|
||||
const server = makeServer();
|
||||
const app = loadApp({server});
|
||||
app.window.createLayout();
|
||||
clickMenu(app.document, "playlist");
|
||||
|
||||
const row = app.document.querySelector("#content_table tbody tr, #content_table tr[id]");
|
||||
assert.ok(row, "a playlist row");
|
||||
row.click();
|
||||
const popup = app.document.getElementById("popup-custom");
|
||||
const buffer = popup.querySelector('select[name="buffer"]');
|
||||
assert.ok(buffer, "buffer select");
|
||||
assert.deepEqual(Array.from(buffer.options).map((o) => o.value), ["default", "-", "xteve", "ffmpeg", "vlc"]);
|
||||
assert.equal(buffer.value, "default");
|
||||
assert.ok(popup.querySelector('select[name="tuner"]'), "tuner select shown while a buffer is active");
|
||||
});
|
||||
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user