157 Commits
Author SHA1 Message Date
nathan 320eaa1b28 Phase 2a: buffer state under one lock, atomic tuner limit, context-driven external buffer
continuous-integration/drone/push Build encountered an error
- src/buffer_state.go replaces the two sync.Maps plus a global RWMutex with
  one bufferMu guarding a map of *Playlist. Each stream has one shared
  bufferStream (URL, folder, status, client count, error, cancel hook);
  downloaders keep a private ThisStream and publish through helpers.
  Playlist.Clients / ThisClient / ClientConnection are gone: there was one
  client counter per stream in two places that could disagree.
- bufferAcquireStream does the tuner check and the registration under the
  same lock, so two clients tuning at once cannot both pass the limit.
  bufferReleaseClient removes the stream when the last client leaves,
  cancels its process and deletes its segment folder.
- bufferingStream rewritten: waits on r.Context() instead of the deprecated
  CloseNotifier, sets Content-Type before WriteHeader (the old code set
  headers after and one was literally named "Content-Length:"), flushes
  each segment to the client, no defer inside the segment loop.
- connectToStreamingServer: shared streamHTTPClient with dial, TLS and
  response-header timeouts (no overall timeout, bodies are endless); the
  deferred Body/segment closes inside the redirect and read loops are now
  explicit closes, so a multi-hour stream no longer accumulates them.
- thirdPartyBuffer rewritten around exec.CommandContext: the process is
  killed when the last client leaves or when no usable data arrives within
  20 s (time.AfterFunc watchdog, no leaked goroutine); cmd.Start error is
  checked; no panic; one file handle per segment. ffmpeg and VLC command
  lines come from buildFFmpegArgs / buildVLCArgs, which are unit tested.
- Data.Cache.StreamingURLS is guarded by streamingURLsMu and persisted
  from a snapshot.
- Tests (src/buffer_test.go, run with -race): restream shares one provider
  connection, six concurrent tunes against a tuner limit of two, 50-way
  acquire/release contention, cleanup and cancel on last release.
2026-09-26 13:14:48 +10:00
nathan 36303fecea Phase 1b/1c: server-side input handling and authentication
continuous-integration/drone/push Build encountered an error
Input handling:
- uploadLogo: client file name reduced to a safe base name with an image
  extension (path traversal and arbitrary-extension writes closed).
- extractZIP: zip-slip guard; entries that resolve outside the target are
  refused. Per-entry closes no longer pile up as defers.
- ffmpeg.path / vlc.path must be an existing regular file named ffmpeg,
  vlc or cvlc, checked both when saved and right before exec.
- Stream URLs passed to the external buffer must use a network scheme
  (http, https, rtsp, rtmp, rtp, udp, mms); file:, concat:, pipe: are
  refused.
- /download/ (backups with settings.json and authentication.json) requires
  the web session when web authentication is enabled.
- settings.json is written 0600; the Plex token is masked in every payload
  sent to the UI and the mask round-trips as "unchanged" on save.

Authentication:
- Passwords are stored with bcrypt. Existing HMAC-SHA256 records still
  verify (constant time) and are re-hashed on the first successful login.
  Username lookups compare in constant time.
- URL (?username=&password=) and HTTP Basic authentication verify the
  credentials per request via AuthenticateUser and no longer create a
  session token, which removes the unbounded token growth under Plex
  polling. Expired sessions are evicted whenever a new one is created.
- createFirstUserForAuthentication and checkAuthorizationLevel now return
  real errors instead of calling no-op closures.

Tests: src/security_test.go and src/internal/authentication/
authentication_test.go cover each of the above.
2026-09-26 13:04:54 +10:00
nathan 4976219857 Phase 1a: browser-side security
continuous-integration/drone/push Build encountered an error
- Provider-controlled strings (channel names, groups, file names, log
  lines, in-place mapping edits, client info) are rendered with
  textContent instead of innerHTML. PopupContent.description() takes an
  explicit isHTML flag that only the static help texts pass.
- Websocket: drop the always-true CheckOrigin so gorilla's same-origin
  check applies; read the session token from the HttpOnly cookie sent with
  the handshake (the ?Token= query parameter is still accepted for older
  clients); the client no longer puts the token in the URL, rewrites the
  cookie, or console-logs request/response payloads.
- Session cookie is HttpOnly, SameSite=Strict, Path=/, session-scoped
  (expiry stays server side) and is cleared on logout.
- Serving the first-run wizard no longer sets Settings.AuthenticationWEB
  to false; the wizard page simply bypasses login while it is active.
- Upgrade failures no longer write a second error response.
- Tests: src/websocket_test.go covers cross-origin refusal, same-origin
  and no-Origin clients, missing/unknown/legacy tokens, and cookie flags.
2026-09-26 12:59:47 +10:00
nathan 504ea3f9f4 Phase 3: Go hygiene pass, runtime PUID/PGID, fork README
continuous-integration/drone/push Build encountered an error
Go:
- staticcheck 399 -> 0 with staticcheck.conf (style checks ST1000/1003/
  1005/1016/1020/1021/1022 excluded; error strings are shown in the UI).
- io/ioutil and rand.Seed removed; CloseNotifier kept with a lint-ignore
  until the Phase 2 context rewrite.
- Dead code deleted: Auto handler, getStreamByChannelID, updateXEPG,
  indexOfInt, jsonToMapInt64, removeOldSystemData, randomTime, and the
  commented-out blocks in struct-buffer.go and internal/authentication.
- Duplicates folded: cacheImagesInBackground(), one addErrorToStream().
- Bugs found by SA4006/SA5001: os.Create handle leaked per ffmpeg segment
  (buffer.go), http.NewRequest error unchecked (buffer.go), xepg.json
  migration wrote null on read error (migrate.go), WriteUserData errors
  silently dropped (authentication.go), defer Close before error check
  (buffer.go, toolchain.go). checkFilePermission results were discarded;
  an unwritable config or temp dir is now fatal at start-up.
- gofmt applied repo-wide; Drone runs gofmt check and staticcheck.

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

Docs: README rewritten for the fork (about, registry, compose, env vars,
security notes); README-DEV gains a container section.
2026-09-26 12:57:43 +10:00
nathan ddc70e170a Phase 3: embed web UI with go:embed, pinned TypeScript toolchain, drop i18n, add /healthz
continuous-integration/drone/push Build encountered an error
- html/embed.go embeds html/ (pages, css, img, js, video); src/assets.go
  serves it, with os.DirFS("html") under -dev. Static assets get an ETag
  and Cache-Control: no-cache; HTML pages are still templated (only the
  login error message is substituted now).
- Delete the generated src/webUI.go (783 KB base64), src/html-build.go and
  cmd/webui-gen; Dockerfile no longer runs a generator.
- Language layer removed: 254 {{.x}} placeholders inlined as English
  strings in ts/*.ts and the two auth pages; html/lang/en.json, the
  LanguageUI struct and the 'language' setting are gone.
- ts/tsconfig.json + package.json pin typescript 5.9.3; the seven sources
  compile (ES2020, global scripts) into one committed html/js/app.js.
  Ten unreferenced legacy scripts under html/js/ deleted; all pages load
  js/app.js.
- Fix the six type errors that blocked a clean compile, including a real
  bug: a missing semicolon in the search shortcut handler made the code
  call the result of preventDefault(), so the shortcut threw instead of
  focusing the search box.
- /healthz liveness endpoint; Dockerfile healthcheck and README use it.
- Drone: go vet, and a webui-check step that rebuilds the bundle and fails
  if the committed app.js is stale.
- README-DEV.md documents build, UI toolchain, -dev, versioning, CI.
2026-09-26 12:46:45 +10:00
nathan 51c7830067 Phase 0: remove auto-updater, pin Go 1.27.1, fix vet warnings, tidy ignores
continuous-integration/drone/push Build encountered an error
- Delete BinaryUpdate, internal/up2date, GitHub/Update structs and the
  xteveAutoUpdate / update.url settings (UI rows, en.json, defaults).
  Settings-schema migrations kept and moved to src/migrate.go.
- Drop kardianos/osext dependency.
- xteve.go version 0200 -> 0201 to match changelog; Drone now fails on drift.
- go.mod go 1.27.1; Dockerfile and Drone golang images pinned to 1.27.1.
- Fix four go vet unreachable-code warnings.
- .gitignore: .gocache/, agent.md, skill.md. .dockerignore: build context
  no longer includes caches, ts/, tasks/ or markdown except the changelog.
- Drone: publish :latest only from master; other branches publish a
  branch-named tag so a feature push cannot replace the deployed image.
- Add tasks/improvement-plan.md and tasks/todo.md.
- Regenerate src/webUI.go.
2026-09-26 12:38:02 +10:00
nathan f558a855ae go fix
continuous-integration/drone/push Build is passing
2026-02-18 15:12:33 +11:00
nathan 125b0bb35f Enhance XEPG channel mapping and settings management
continuous-integration/drone/push Build is passing
2026-02-13 16:09:00 +11:00
nathan 32c3d779c0 add edit button to mapping table and refactor cell creation
continuous-integration/drone/push Build is passing
2026-02-12 13:22:51 +11:00
nathan ce5b12d8b8 Enhance Docker configuration with UID and GID arguments for improved user permissions
continuous-integration/drone/push Build is passing
2026-02-11 19:49:23 +11:00
nathan 76183bfaa2 Refactor entrypoint script for improved configuration handling and migration logic
continuous-integration/drone/push Build is passing
2026-02-11 19:10:07 +11:00
nathan c577d354e7 Enhance configuration handling and implement wizard completion logic
continuous-integration/drone/push Build is passing
2026-02-11 17:15:54 +11:00
nathan e48a061ca0 Enhance WebSocket handling and log polling logic
continuous-integration/drone/push Build is passing
2026-02-11 16:25:48 +11:00
nathan ffd43d5217 Enhance log display behavior and menu state management
continuous-integration/drone/push Build is passing
2026-02-11 14:37:02 +11:00
nathan 9bd2b32003 Enhance WebSocket connection handling with improved timeout and error states
continuous-integration/drone/push Build is passing
2026-02-11 14:20:24 +11:00
nathan 8698983bbf updates
continuous-integration/drone/push Build is passing
2026-02-11 13:28:59 +11:00
nathan 60af423335 update UI js
continuous-integration/drone/push Build is passing
2026-02-11 13:09:41 +11:00
nathan 57b6be74e2 improve ui checkbox functionality
continuous-integration/drone/push Build is passing
2026-02-11 13:03:12 +11:00
nathan c5545cbf08 go fix for 1.26
continuous-integration/drone/push Build is passing
2026-02-11 12:53:35 +11:00
nathan 339d2d0aa5 include ffmpeg
continuous-integration/drone/push Build is passing
2026-02-11 12:42:01 +11:00
nathan a04b0ede50 fix tmp dir permissions crash
continuous-integration/drone/push Build is passing
2026-02-11 12:29:49 +11:00
nathan 84de46a2f2 cleanup version numbers
continuous-integration/drone/push Build is passing
2026-02-11 12:20:36 +11:00
nathan 5eaf5efdb6 improve Dockerfile
continuous-integration/drone/push Build is failing
2026-02-11 12:13:10 +11:00
nathan fb62353de4 fix docker image location
continuous-integration/drone/push Build is failing
2026-02-11 12:08:58 +11:00
nathan fa9d41e10c and again
continuous-integration/drone/push Build encountered an error
2026-02-11 12:07:17 +11:00
nathan e07d4dd878 again
continuous-integration/drone/push Build is failing
2026-02-11 12:01:14 +11:00
nathan 6053207034 pipeline fix
continuous-integration/drone/push Build is failing
2026-02-11 11:58:47 +11:00
nathan 6a8b4bed28 try again
continuous-integration/drone/push Build is failing
2026-02-11 11:56:30 +11:00
nathan 45685ce592 fix drone yml
continuous-integration/drone/push Build is failing
2026-02-11 11:53:44 +11:00
nathan 43a9cf5a7e bugfix
continuous-integration/drone/push Build encountered an error
2026-02-11 11:52:34 +11:00
nathan a23cc7a183 change pipeline
continuous-integration/drone/push Build is failing
2026-02-11 11:40:30 +11:00
nathan b069d5bee8 many updates 2026-02-11 11:38:26 +11:00
nathan 8cb9e43a72 Redesign UI and add first-party Docker runtime support 2026-02-11 11:04:39 +11:00
xteve-project 0e999b85b9 Update readme.md 2021-03-29 20:26:19 +02:00
xteve-project e4b3667fe0 v2.2.0.0200 2021-03-29 19:55:06 +02:00
xteve-project e44eff0645 Add go.mod and go.sum. Require go v1.16 2021-03-29 19:47:56 +02:00
xteve-project 884b427754 v2.1.3.0131 2021-01-21 20:02:12 +01:00
xteve-project 25bad13800 compile js 2021-01-21 19:50:35 +01:00
xteve-project d2456b0506 Merge branch 'fix/209/unset-category' 2021-01-21 19:38:45 +01:00
beardypig 19b9a259b1 Correctly unset the category for a channel in XEPG
Stores `""` to `x-category` when the category is unset, previosuly `"-"` was stored which would append a `<category>` to each `<programme>` for the channel.

fixes #209
2021-01-21 11:59:50 +01:00
xteve-project 441cefa3a4 v2.1.3.0130 2020-11-22 17:17:20 +01:00
xteve-project 014f5b7218 Dummy new times
Fixed searching for XMLTV file (Mapping)
2020-11-20 22:13:52 +01:00
xteve-project 0310b7e738 v2.1.2.0129 2020-10-17 14:08:25 +02:00
xteve-project 6d890cfd33 Fixed: undefined playlist 2020-10-16 16:43:56 +02:00
marmei 410cc3648f v2.1.2.0128 2020-10-09 13:00:28 +02:00
marmei 07f9cac3c5 gitignore 2020-10-03 11:57:40 +02:00
marmei f43ce0f7c5 Bug fix: Image caching (#172) 2020-10-03 11:56:50 +02:00
marmei db59f7ef37 Add XML tag: rating, credits 2020-09-26 18:35:16 +02:00
marmei b407ec5bf0 Exit disabled when GitHub is unreachable 2020-07-13 16:19:28 +02:00
marmei d8fc1aea97 Schedule can be deactivated 2020-06-03 23:11:25 +02:00
marmei 38333d65cb Add content type (BUffer): video/x-matroska 2020-05-16 09:21:34 +02:00
marmei a6a9b90937 Merge branch 'pr/136' into beta 2020-05-16 09:19:04 +02:00
marmei 2b3fe6a09d v2.1.2.0122 2020-05-15 19:43:07 +02:00
Raf a09eca59a7 cleanup channel hash map logic 2020-05-15 11:53:42 -04:00
marmei 0df3b4d755 Merge branch 'pr/136' into beta 2020-05-15 00:20:21 +02:00
marmei 5552514a1f Merge branch 'pr/134' into beta 2020-05-14 23:44:30 +02:00
Raf 5fceb1d34f update change log to match upstream beta 2020-05-14 10:15:42 -04:00
Raf fb5d0a3904 freenumbergen-start at settings.firstchannel
if free
2020-05-14 09:39:28 -04:00
Raf 28fe4dcf1c Speedup db update for large files 2020-05-14 09:39:28 -04:00
marmei bc307a7cd4 2.1.2.0121 2020-05-14 09:39:28 -04:00
marmei fd3024d7ff v2.1.2.0120 2020-05-14 09:39:28 -04:00
marmei d3f6725ba6 Update changelog-beta.md 2020-01-24 2020-05-14 09:38:30 -04:00
marmei c266012db3 Update changelog-beta.md 2020-01-13 2020-05-14 09:38:30 -04:00
5Ub-Z3r0 d602b60710 Merge branch 'master' into beta 2020-05-13 21:34:27 +02:00
5Ub-Z3r0 67b7ba6df9 Add support for proxying multicast streams through a UDPxy server.
This commit adds support for proxying multicast streams through a UDPxy
server. If the stream is multicast, and a udpxy server is set in the
configuration, then the channel URL is rewritten to use the UDPxy
service configured.

The stream URL rewriting is done regardless of the buffer option set.

Signed-off-by: 5Ub-Z3r0 <1673590+5Ub-Z3r0@users.noreply.github.com>
2020-05-13 21:26:24 +02:00
marmei 71dfe91272 New flag -dev: Activates developer mode 2020-05-13 10:25:50 +02:00
marmei 534510a4ec Websocket error 2020-03-02 17:03:56 +01:00
marmei 2d10fc9313 Merge branch 'beta' 2020-02-19 19:39:05 +01:00
marmei a63a9c0d8f 2.1.2.0121 2020-02-19 19:38:56 +01:00
marmei dd911d6e5d M3U error message changed 2020-02-19 19:35:41 +01:00
marmei c1970a8393 v2.1.2.0120 2020-02-15 09:01:25 +01:00
marmei 493d612d52 Merge branch 'beta' 2020-02-14 20:02:28 +01:00
marmei 4fc4330a94 FFmpeg changed: user-agent to user_agent 2020-02-08 11:10:14 +01:00
marmei a683533824 Update changelog-beta.md 2020-01-24 2020-01-24 19:58:00 +01:00
marmei 4b9f5826cf If no user agent is specified, the default FFmpeg or VLC user agent is used. 2020-01-24 19:48:29 +01:00
marmei ca49d70910 Update changelog-beta.md 2020-01-13 2020-01-13 23:17:55 +01:00
marmei 1b425018d4 v2.1.1.0115-beta 2020-01-13 23:04:34 +01:00
marmei 87b36c283b URL format removed from the settings 2020-01-04 17:17:28 +01:00
marmei 6da26ff4fb Merge branch 'pr/76' into beta 2020-01-04 17:01:32 +01:00
marmei cd08985e79 Set global domain for /web 2020-01-04 17:00:58 +01:00
Thomas Dall'Agnese dc04519229 Use relative path when posting HTML forms 2019-12-29 20:03:37 +01:00
Thomas Dall'Agnese c4ad96b715 Use relative path when posting HTML forms 2019-12-28 18:55:46 +01:00
Thomas Dall'Agnese 45b5e602bb Revert "Use relative path when posting HTML forms"
This reverts commit d5328f6b1a.
2019-12-28 18:43:48 +01:00
marmei 1cefbf022d Workaround for IPTVX content-type bug 2019-12-28 12:28:33 +01:00
Thomas Dall'Agnese d5328f6b1a Use relative path when posting HTML forms 2019-12-28 10:23:54 +01:00
marmei 91b80bc8bb Add xteve.xml GZIP 2019-12-16 20:23:05 +01:00
marmei aa763726a3 Fixed broken merge 2019-12-14 10:17:42 +01:00
marmei 66c01dd1fb Missing placeholder 2019-12-13 21:05:14 +01:00
marmei 03e1abbe90 v2.1.1.0110 2019-12-13 20:31:23 +01:00
marmei 469581e280 Add mapping desc. function 2019-12-13 19:01:18 +01:00
marmei 019c98996a Update changelog-beta.md 2019-12-08 2019-12-08 17:49:50 +01:00
marmei 36db927794 v2.1.0.0106 2019-12-08 17:47:54 +01:00
marmei f0a49788cc User-Agent for FFmpeg and VLC 2019-12-08 17:33:46 +01:00
marmei 72767d7dbd Update changelog-beta.md 2019-12-06 2019-12-06 21:00:44 +01:00
marmei 8eecbf2b78 Update changelog-beta.md 2019-12-06 2019-12-06 20:57:26 +01:00
marmei 1a1e37fe15 v2.1.0.0105: Settings for URI scheme 2019-12-06 20:48:59 +01:00
marmei 08f6fb60e3 Fix log entry, wrong buffer value 2019-11-15 10:09:19 +01:00
marmei eded490ac7 Update changelog-beta.md 2019-11-06 2019-11-06 18:16:14 +01:00
marmei ed770b9dbc Merge remote-tracking branch 'origin/master' 2019-11-06 18:11:21 +01:00
marmei 6129b4911a Merge pull request #53 from taeram/patch-1
Small spelling / grammar fixes.
2019-11-05 20:29:12 +01:00
Jesse Patching 477c5f30c1 Small spelling / grammar fixes. 2019-11-04 20:28:45 -07:00
marmei 65ddc6f301 v2.1.2.0101-beta 2019-11-04 18:28:41 +01:00
marmei 3ef95c1950 RV Proxy 2019-11-04 18:23:20 +01:00
marmei 3a3798cd2d WiteHeader 2019-10-23 19:02:54 +02:00
marmei 72eb7fb599 v2.1.0.0100 2019-10-11 17:59:11 +02:00
marmei 4b969b8cee Merge branch 'beta' 2019-10-11 17:54:06 +02:00
marmei 3d9266dabe XEPG performance 2019-10-11 17:53:20 +02:00
marmei 48218cda50 Update changelog-beta.md 2019-10-04 2019-10-04 19:52:44 +02:00
marmei dc42afcd05 New beta 2019-10-04 19:39:54 +02:00
marmei 20e5e1b545 Buffer RTSP performance 2019-10-04 19:39:20 +02:00
Aaron Chi 11dd830110 fix #34 2019-10-04 17:08:16 +08:00
marmei 7c87d1d5bd Wrong CVLC path 2019-09-27 22:20:19 +02:00
marmei 6cdd44357b update changelog-beta.md 2019-09-27 19:55:36 +02:00
marmei ad992eb615 Add FFmpeg and VLC support 2019-09-27 19:24:31 +02:00
marmei 11453c6053 v2.0.3.0030 2019-09-07 12:51:21 +02:00
marmei 81e8ae33d7 Merge branch 'Bugs_in_2.0.2' 2019-09-07 12:07:55 +02:00
marmei f3be0fca47 Wrong warning (root) 2019-09-05 19:37:55 +02:00
marmei 1c1c89cd74 Update changelog-beta.md 2019-09-04 2019-09-04 21:16:04 +02:00
marmei 3d73dba422 Code removed #1 2019-09-04 20:16:11 +02:00
marmei c6e74fe11c Interception of repeated save 2019-09-04 20:12:26 +02:00
marmei 18dba46c02 Bug #16 1 2019-09-01 00:24:06 +02:00
marmei efa55b39a9 Bug #16 0 2019-08-31 20:39:39 +02:00
marmei 717fa68b7e Add an error message if the filter rule is empty.
Set the streaming status to active only once.
2019-08-28 17:56:15 +02:00
marmei 878531ff79 IPTV removed as a source 2019-08-28 16:49:41 +02:00
xteve-project 792fd9a373 Merge pull request #14 from Coledunsby/master
Add support for "video/m2ts" video streams
2019-08-24 13:12:26 +02:00
marmei a1ec0287ef v2.0.2.0020 2019-08-24 13:10:08 +02:00
marmei a79e824ef8 Merge branch 'Coledunsby-master' 2019-08-24 13:04:48 +02:00
marmei c843f424fe update changelog-beta.md 2019-08-23 23:34:25 +02:00
marmei 5c6637c048 Add support for "video/m2ts" video streams 2019-08-23 23:21:41 +02:00
marmei 1062e072d6 update readme.md 2019-08-23 23:17:40 +02:00
Cole Dunsby d831a099f0 Add support for "video/m2ts" video streams 2019-08-19 22:14:53 -04:00
marmei a06baef4d3 Bug #9 - Fixed, incorrect original-air-date 2019-08-18 11:19:03 +02:00
marmei f9d1a45bbd Add original group-title to mapping editor 2019-08-17 08:57:06 +02:00
marmei 8a4fb8ba30 v2.0.1.0010 2019-08-09 19:09:40 +02:00
marmei 28dad4932b v2.0.0.0009 2019-08-09 18:02:24 +02:00
marmei 1769b1e6db v2.0.0.0009-beta 2019-08-09 17:58:34 +02:00
marmei 30ab13f871 Update changelog-beta.md 2019-08-09 2019-08-09 09:47:43 +02:00
marmei 0f37835206 v2.0.0.0008-beta #2 2019-08-09 09:38:11 +02:00
marmei 67fe80b4fd v2.0.0.0008-beta
Pull request: Error in http/https detection. (#6)
2019-08-09 09:31:31 +02:00
marmei 96e10ff51d 2019-08-09 2019-08-09 09:21:42 +02:00
marmei d550beaa01 Merge pull request #6 from hexrus/patch-1
Error in http/https detection.
2019-08-09 09:19:14 +02:00
hexrus 00ec32456c Error in http/https detection.
window.location.protocol return "https:", not "https://"
2019-08-09 09:53:55 +03:00
marmei b3600f8a14 2019-07-08 2019-08-07 13:28:47 +02:00
marmei 849493a802 Add: changelog-beta.md
Update: README.MD
2019-08-07 13:23:49 +02:00
marmei 3730d1187d v2.0.0.0007-beta
Add CLI: info
Add CLI: restore
Improves UPnP support for Plex
CSS: Add min-width for Ch.No.
Fixed missing images on cache for localhost
2019-08-07 12:35:56 +02:00
marmei 2a06bf6b01 Add HLS VOD support 2019-08-05 12:28:47 +02:00
marmei c389b990b4 v2.0.0.0002-beta 2019-08-04 18:10:42 +02:00
marmei 56f3e3389b Unused function removed 2019-08-04 10:35:41 +02:00
marmei 50e6ed274e Update schedule:
Removes all spaces from the values [ 1000] -> [1000]
2019-08-04 09:49:51 +02:00
marmei 8490187d31 v2.0.0.0001 2019-08-03 14:58:37 +02:00
marmei 4dc9dfabf2 Wizard: Add input placeholder (M3U, XMLTV)
Wizard: Alert by empty value (M3U, XMLTV)
2019-08-03 14:52:17 +02:00
marmei 5fc5f773d3 Merge remote-tracking branch 'origin/master' 2019-08-03 14:20:54 +02:00
marmei c9bc4aedbc Ignore invalid image URLs 2019-08-03 14:20:26 +02:00
xteve-project 51bab26d59 Update issue templates 2019-08-03 11:53:54 +02:00
marmei 0063dcc523 Merge branch 'pr/2' 2019-08-03 09:48:41 +02:00
marmei 23dd3ef08c New Docker from alturismo 2019-08-03 09:41:05 +02:00
Steve 9b84982bab Update README.md
Typos
2019-08-02 16:01:44 -05:00
100 changed files with 12314 additions and 12309 deletions

No files matched your search

+25
View File
@@ -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
+106
View File
@@ -0,0 +1,106 @@
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
- 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
- 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
tags:
- latest
- ${DRONE_COMMIT_SHA}
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
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Please only useful feature request.
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+9 -1
View File
@@ -1,7 +1,15 @@
.DS_Store
demo
dev
compiler
files
update_xteve*.sh
xteve
xteve.exe
de.json
de.json
.gocache/
agent.md
skill.md
node_modules/
.claude/
+57
View File
@@ -0,0 +1,57 @@
# 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.
FROM mwader/static-ffmpeg:7.1.1 AS ffmpeg
FROM alpine:3.23
# Build-time defaults for the xteve user. The entrypoint can change them at
# runtime with PUID/PGID, so these only matter when neither is set.
ARG XTEVE_UID=1000
ARG XTEVE_GID=1000
RUN apk add --no-cache ca-certificates tzdata 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"]
+61 -1
View File
@@ -1 +1,61 @@
# 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
npm run build # ts/*.ts -> html/js/app.js
npm run check # type-check only
```
File order in the bundle is fixed in `ts/tsconfig.json` and matches the old per-page script order. All pages load the same bundle.
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` 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.
+83 -67
View File
@@ -4,13 +4,27 @@
<br>
# xTeVe
## M3U Proxy for Plex DVR and Emby Live TV.
## M3U Proxy for Plex DVR and Emby Live TV.
Documentation for setup and configuration is [here](https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md).
#### Donation
* **Bitcoin:** 1c1iCe4CJPfNUXtqxKBbW2Qd2EtqRPWme
![Bitcoin](html/img/BC-QR.jpg "Bitcoin - xTeVe")
## About this fork
The upstream xTeVe project has been inactive since 2021. This fork is maintained privately for a single trusted LAN. It is Docker-only and built for amd64 only. There are no release archives and no self-update.
What it adds over upstream:
* Web UI redesign with a mobile and accessibility pass (responsive navigation, keyboard flow, focus visibility, ARIA announcements, contrast).
* Plex API guide refresh: after a lineup or XEPG update xTeVe can ask Plex to reload the DVR guide. New settings: `use_plexAPI`, `plex.url`, `plex.token`.
* Strict or relaxed handling of channels whose EPG source went missing in XEPG, with automatic re-mapping.
* A wizard-completed flag so the setup wizard is not shown again on restart.
* First-party Docker image with a static ffmpeg, runtime `PUID`/`PGID`, and a healthcheck.
* Drone CI: vet, tests, web bundle check, Dockerfile lint, compose validation, image publishing.
* Web UI embedded in the binary with `go:embed`.
* Unauthenticated `GET /healthz` endpoint for container health checks.
* The self-updater and the GitHub branch switching are removed.
The plan for further work is in `tasks/improvement-plan.md`.
## Requirements
### Plex
@@ -23,7 +37,7 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
* Emby Client with Live-TV support
* Emby Premiere
---
---
## Features
@@ -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,80 +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)
Tags:
Including:
- Guide2go: XMLTV grabber for Schedules Direct
* `latest` and `<commit sha>` from the `master` branch.
* `<branch name>` and `<commit sha>` from every other branch.
```bash
docker pull registry.coadcorp.com/nathan/xteve:latest
```
**Created by LeeD:**
[xTeVe / Guide2go / Zap2XML](https://hub.docker.com/r/dnsforge/xteve)
The image is linux/amd64 only.
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
### Start with Docker Compose
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.
---
## Build from source code [Go / Golang]
## Security notes
#### Requirements
* Go (go1.12.4 or newer)
#### Dependancys
* [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 dependancys
```
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
```
* 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.
---
## Fork without pull request :mega:
When creating a fork, the xTeVe GitHub account must be changed from the sorce 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.
+104
View File
@@ -0,0 +1,104 @@
#### 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">
```
#### 2.0.0.0008-beta
##### Fixes
- Pull request #6 [Error in http/https detection] window.location.protocol return "https:", not "https://"
#### 2.0.0.0007-beta
```diff
+ Buffer HLS: Add VOD tag from M3U8
+ CLI: Add new arguments [-restore]
+ CLI: Add new arguments [-info]
```
##### Fixes
- Missing images with caching for localhost URL
#### 2.0.0.0001-beta
```diff
+ Wizard: Add HTML input placeholder (M3U, XMLTV)
+ Wizard: Alert by empty value (M3U, XMLTV)
+ Image caching: Ignore invalid image URLs
```
+26
View File
@@ -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
+31
View File
@@ -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
+201
View File
@@ -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}" "$@"
+14
View File
@@ -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
)
+14
View File
@@ -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=
+11 -14
View File
@@ -6,23 +6,19 @@
<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>
<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" role="main" 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">&nbsp;</td>
@@ -46,13 +42,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>
</html>
+15 -16
View File
@@ -6,42 +6,41 @@
<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>
<script src="js/app.js"></script>
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
<div id="box">
<main id="box" role="main" 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>
</html>
+618 -268
View File
File diff suppressed because it is too large. Load diff
+878 -444
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -1,3 +0,0 @@
h1 {
color: green;
}
+13
View File
@@ -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
+38 -34
View File
@@ -2,31 +2,27 @@
<html>
<head>
<meta charset="utf-8">
<!---
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-->
<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>
<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 +36,62 @@
</div>
-->
<div id="menu-wrapper" class="layout-left">
<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>
<h2 id="shell-title">xTeVe Control Panel</h2>
<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">&nbsp;</td>
<td id="version" class="tdVal" data-label="xTeVe">&nbsp;</td>
<td class="tdKey">OS:</td>
<td id="os" class="tdVal">&nbsp;</td>
<td id="os" class="tdVal" data-label="OS">&nbsp;</td>
<td class="tdKey phone">DVR IP:</td>
<td id="DVR" class="tdVal phone">&nbsp;</td>
<td id="DVR" class="tdVal phone" data-label="DVR IP">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">UUID:</td>
<td id="uuid" class="tdVal">&nbsp;</td>
<td id="uuid" class="tdVal" data-label="UUID">&nbsp;</td>
<td class="tdKey">Arch:</td>
<td id="arch" class="tdVal">&nbsp;</td>
<td id="arch" class="tdVal" data-label="Arch">&nbsp;</td>
<td class="tdKey phone">M3U URL:</td>
<td id="m3u-url" class="tdVal phone">&nbsp;</td>
<td id="m3u-url" class="tdVal phone" data-label="M3U URL">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">Available Streams:</td>
<td id="streams" class="tdVal">&nbsp;</td>
<td id="streams" class="tdVal" data-label="Available Streams">&nbsp;</td>
<td class="tdKey">EPG Source:</td>
<td id="epgSource" class="tdVal">&nbsp;</td>
<td id="epgSource" class="tdVal" data-label="EPG Source">&nbsp;</td>
<td class="tdKey phone">XEPG URL:</td>
<td id="xepg-url" class="tdVal phone">&nbsp;</td>
<td id="xepg-url" class="tdVal phone" data-label="XEPG URL">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">XEPG Channels:</td>
<td id="xepg" class="tdVal">&nbsp;</td>
<td id="xepg" class="tdVal" data-label="XEPG Channels">&nbsp;</td>
<td class="tdKey">Errors:</td>
<td id="errors" class="tdVal">&nbsp;</td>
<td id="errors" class="tdVal" data-label="Errors">&nbsp;</td>
<td class="tdKey">Warnings:</td>
<td id="warnings" class="tdVal">&nbsp;</td>
<td id="warnings" class="tdVal" data-label="Warnings">&nbsp;</td>
</tr>
</table>
<div id="status-cards" class="dashboard-cards" role="list" aria-live="polite" aria-label="System summary"></div>
<div id="myStreamsBox" class="notVisible">
<div id="myStreamsBox" class="notVisible" aria-live="polite" aria-label="Stream details">
<div id="allStreams">
<table id="activeStreams"></table>
@@ -97,12 +100,13 @@
</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>
</body>
</html>
</html>
+3406
View File
File diff suppressed because it is too large. Load diff
-42
View File
@@ -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;
}
-32
View File
@@ -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
View File
@@ -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
}
-473
View File
@@ -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());
}
-40
View File
@@ -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();
}
-294
View File
@@ -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;
}
-140
View File
@@ -1,140 +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("class", "wizard");
input.id = key;
doc.appendChild(input);
description = "{{.wizard.m3u.description}}";
break;
case "xmltv":
var input = content.createInput("text", key, "");
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;
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
View File
@@ -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]
}
}
-379
View File
@@ -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
View File
@@ -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;
}
}
-42
View File
@@ -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
View File
@@ -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);
}
}
-1747
View File
File diff suppressed because it is too large. Load diff
-105
View File
@@ -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();
}
-442
View File
@@ -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);
}
-341
View File
@@ -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);
}
-419
View File
@@ -1,419 +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"
},
"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": "Passoword",
"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"
},
"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",
"description": "Local or remote playlists"
},
"xmltv": {
"title": "XMLTV File",
"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"
}
}
}
+13 -14
View File
@@ -6,41 +6,40 @@
<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>
<script src="js/app.js"></script>
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
<div id="box">
<main id="box" role="main" 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>
</html>
</html>
+2 -2
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="css/base.css" type="text/css">
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
@@ -27,4 +27,4 @@
</div>
</body>
</html>
</html>
+27
View File
@@ -0,0 +1,27 @@
{
"name": "xteve-webui",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xteve-webui",
"devDependencies": {
"typescript": "5.9.3"
}
},
"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"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"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"
},
"devDependencies": {
"typescript": "5.9.3"
}
}
+52
View File
@@ -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
View File
@@ -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")
}
+209 -127
View File
@@ -1,191 +1,273 @@
package src
import (
b64 "encoding/base64"
"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(input string) (newWebURL string, err error) {
func xteveRestore(archive string) (newWebURL string, err error) {
var newPort, oldPort string
var newPort, oldPort, backupVersion, tmpRestore string
// Base64 Json String in base64 umwandeln
b64data := input[strings.IndexByte(input, ',')+1:]
tmpRestore = System.Folder.Temp + "restore" + string(os.PathSeparator)
// Base64 in bytes umwandeln und speichern
sDec, err := b64.StdEncoding.DecodeString(b64data)
err = checkFolder(tmpRestore)
if err != nil {
return
}
if err != nil {
return
}
// Zip Archiv in tmp entpacken
err = extractZIP(archive, tmpRestore)
if err != nil {
return
}
var archive = System.Folder.Temp + "restore.zip"
// 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
}
err = writeByteToFile(archive, sDec)
if err != nil {
return
}
backupVersion = newConfig["version"].(string)
if backupVersion < System.Compatibility {
err = errors.New(getErrMsg(1013))
return
}
// Zip Archiv 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 zu überprüfen
newConfig, err := loadJSONFileToMap(System.Folder.Config + "settings.json")
if err != nil {
ShowError(err, 0)
return
}
// Neue Config laden um den Port und die Version zu überprüfen
newConfig, err = loadJSONFileToMap(System.Folder.Config + "settings.json")
if err != nil {
ShowError(err, 0)
return
}
newPort = newConfig["port"].(string)
oldPort = Settings.Port
newPort = newConfig["port"].(string)
oldPort = Settings.Port
if newPort == oldPort {
if newPort == oldPort {
if err != nil {
ShowError(err, 0)
}
if err != nil {
ShowError(err, 0)
}
loadSettings()
loadSettings()
err := Init()
if err != nil {
ShowError(err, 0)
return "", err
}
err := Init()
if err != nil {
ShowError(err, 0)
return "", err
}
err = StartSystem(true)
if err != nil {
ShowError(err, 0)
return "", err
}
err = StartSystem(true)
if err != nil {
ShowError(err, 0)
return "", err
}
return "", err
}
return "", err
}
var url = System.URLBase + "/web/"
newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1)
var url = System.URLBase + "/web/"
newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1)
return
os.RemoveAll(tmpRestore)
return
}
func xteveRestoreFromWeb(input string) (newWebURL string, err error) {
// Base64 Json String in base64 umwandeln
b64data := input[strings.IndexByte(input, ',')+1:]
// Base64 in bytes umwandeln und speichern
sDec, err := b64.StdEncoding.DecodeString(b64data)
if err != nil {
return
}
var archive = System.Folder.Temp + "restore.zip"
err = writeByteToFile(archive, sDec)
if err != nil {
return
}
newWebURL, err = xteveRestore(archive)
return
}
// XteveRestoreFromCLI : Wiederherstellung über die Kommandozeile
func XteveRestoreFromCLI(archive string) (err error) {
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()
fmt.Print("All data will be replaced with those from the backup. Should the files be restored? [yes|no]:")
fmt.Scanln(&confirm)
switch strings.ToLower(confirm) {
case "yes":
break
case "no":
return
default:
fmt.Println("Invalid input")
return
}
if len(System.Folder.Config) > 0 {
err = checkFilePermission(System.Folder.Config)
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))
}
return
}
+756 -798
View File
File diff suppressed because it is too large. Load diff
+246
View File
@@ -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
}
+285
View File
@@ -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
View File
@@ -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
}
+36 -36
View File
@@ -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))
@@ -83,9 +76,16 @@ func Init() (err error) {
return
}
if len(System.Flag.Restore) > 0 {
// Einstellungen werden über CLI wiederhergestellt. Weitere Initialisierung ist nicht notwendig.
return
}
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
@@ -104,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 {
@@ -113,6 +113,7 @@ func Init() (err error) {
}
showInfo(fmt.Sprintf("Version:%s Build: %s", System.Version, System.Build))
showInfo(fmt.Sprintf("Database Version:%s", System.DBVersion))
showInfo(fmt.Sprintf("System IP Addresses:IPv4: %d | IPv6: %d", len(System.IPAddressesV4), len(System.IPAddressesV6)))
showInfo("Hostname:" + System.Hostname)
showInfo(fmt.Sprintf("System Folder:%s", getPlatformPath(System.Folder.Config)))
@@ -127,7 +128,6 @@ func Init() (err error) {
// Bedingte Update Änderungen durchführen
err = conditionalUpdateChanges()
if err != nil {
ShowError(err, 0)
return
}
@@ -142,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
@@ -155,18 +156,21 @@ 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
}
// DLNA Server starten
go SSDP()
// Branch festlegen
System.Branch = Settings.Branch
if System.Dev == true {
if System.Dev {
System.Branch = "Development"
}
@@ -174,27 +178,22 @@ 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()
if err != nil {
return
}
loadHTMLMap()
return
}
@@ -211,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 {
+247 -84
View File
@@ -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,27 +37,64 @@ 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":
// Die Formatierung der Uhrzeit überprüfen (0000 - 2359)
for _, i := range newSettings[key].([]interface{}) {
// Leerzeichen aus den Werten entfernen und Formatierung der Uhrzeit überprüfen (0000 - 2359)
var newUpdateTimes = make([]string, 0)
_, err := time.Parse("1504", i.(string))
for _, v := range value.([]any) {
v = strings.Replace(v.(string), " ", "", -1)
_, err := time.Parse("1504", v.(string))
if err != nil {
ShowError(err, 1012)
return Settings, err
}
newUpdateTimes = append(newUpdateTimes, v.(string))
}
value = newUpdateTimes
case "cache.images":
cacheImages = true
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))
@@ -86,26 +127,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)
@@ -120,7 +179,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
return
}
if Settings.AuthenticationWEB == false {
if !Settings.AuthenticationWEB {
Settings.AuthenticationAPI = false
Settings.AuthenticationM3U = false
@@ -130,12 +189,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 {
@@ -146,33 +232,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()
@@ -181,6 +280,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
}
if triggerPlexGuideReload {
queuePlexGuideRefresh("settings change")
}
}
return
@@ -189,8 +292,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
@@ -212,7 +315,7 @@ 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 {
@@ -221,15 +324,15 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
// Neue Providerdatei
dataID = indicator + randomString(19)
data.(map[string]interface{})["new"] = true
data.(map[string]any)["new"] = true
filesMap[dataID] = data
} else {
// Bereits vorhandene Providerdatei
for key, value := range data.(map[string]interface{}) {
for key, value := range data.(map[string]any) {
var oldData = filesMap[dataID].(map[string]interface{})
var oldData = filesMap[dataID].(map[string]any)
oldData[key] = value
}
@@ -250,11 +353,11 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
}
// Neue Providerdatei
if _, ok := data.(map[string]interface{})["new"]; ok {
if _, ok := data.(map[string]any)["new"]; ok {
reloadData = true
err = getProviderData(fileType, dataID)
delete(data.(map[string]interface{}), "new")
delete(data.(map[string]any), "new")
if err != nil {
delete(filesMap, dataID)
@@ -263,7 +366,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
@@ -275,7 +378,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
return
}
if reloadData == true {
if reloadData {
err = buildDatabaseDVR()
if err != nil {
@@ -296,7 +399,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 {
@@ -326,7 +429,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 {
@@ -349,15 +452,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
@@ -381,23 +484,37 @@ 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
}
}
if oldData, ok := filterMap[dataID].(map[string]any); ok {
oldData[key] = value
}
}
@@ -426,6 +543,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
@@ -438,8 +560,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
}
@@ -448,7 +606,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 {
@@ -468,26 +626,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
}
@@ -537,11 +695,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
@@ -604,6 +762,7 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
}
buildXEPG(false)
System.ScanInProgress = 0
}
@@ -611,6 +770,10 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
}
if nextStep == 10 {
Settings.WizardCompleted = true
}
err = saveSettings(Settings)
if err != nil {
return
@@ -671,9 +834,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{}
@@ -694,8 +857,8 @@ func buildDatabaseDVR() (err error) {
for n, 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)
@@ -741,11 +904,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++
}
@@ -821,7 +980,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)
}
@@ -829,20 +988,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)
}
@@ -854,6 +1013,10 @@ func buildDatabaseDVR() (err error) {
sort.Strings(Data.StreamPreviewUI.Active)
sort.Strings(Data.StreamPreviewUI.Inactive)
if Settings.EpgSource != "XEPG" {
queuePlexGuideRefresh("lineup update")
}
return
}
@@ -861,7 +1024,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 {
@@ -889,7 +1052,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":
@@ -902,7 +1065,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
@@ -920,7 +1083,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":
@@ -933,7 +1096,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
+6 -6
View File
@@ -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
}
-147
View File
@@ -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)
}
}
+20 -138
View File
@@ -1,153 +1,35 @@
package src
import (
b64 "encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
b64 "encoding/base64"
"fmt"
"strings"
)
func getCacheImageURL(url string) (cacheImageURL string) {
url = strings.Trim(url, "\r\n")
var urlMD5 = getMD5(url)
var fileExtension = filepath.Ext(url)
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesFiles) == -1 {
Data.Cache.ImagesFiles = append(Data.Cache.ImagesFiles, urlMD5+fileExtension)
}
if Settings.CacheImages == false || System.ImageCachingInProgress == 1 {
return url
}
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(url, System.Domain+"/images/") == false {
if indexOfString(url, Data.Cache.ImagesURLS) == -1 {
Data.Cache.ImagesURLS = append(Data.Cache.ImagesURLS, url)
}
}
cacheImageURL = url
}
return
}
func cachingImages() {
if Settings.CacheImages == false || System.ImageCachingInProgress == 1 {
return
}
System.ImageCachingInProgress = 1
showInfo("Image Caching:Images are cached")
for _, url := range Data.Cache.ImagesURLS {
if len(url) > 0 {
cacheImage(url)
}
}
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(url string) {
var debug string
var urlMD5 = getMD5(url)
var fileExtension = filepath.Ext(url)
debug = fmt.Sprintf("Image Caching:File: %s Download: %s", urlMD5+fileExtension, url)
showDebug(debug, 1)
resp, err := http.Get(url)
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
}
+95
View File
@@ -0,0 +1,95 @@
package src
import (
"fmt"
"strings"
)
// ShowSystemInfo : Systeminformationen anzeigen
func ShowSystemInfo() {
fmt.Print("Creating the information takes a moment...")
err := buildDatabaseDVR()
if err != nil {
ShowError(err, 0)
return
}
buildXEPG(false)
fmt.Println("OK")
println()
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.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("IPv4 Addresses:")
for i, ipv4 := range System.IPAddressesV4 {
switch count := i; {
case count < 10:
fmt.Printf(" %d. %s\n", count, ipv4)
case count < 100:
fmt.Printf(" %d. %s\n", count, ipv4)
}
}
println()
fmt.Println("IPv6 Addresses:")
for i, ipv4 := range System.IPAddressesV6 {
switch count := i; {
case count < 10:
fmt.Printf(" %d. %s\n", count, ipv4)
case count < 100:
fmt.Printf(" %d. %s\n", count, ipv4)
}
}
println("---")
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("---")
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("---")
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("---")
fmt.Println("Settings [Backup]")
fmt.Printf("Folder (backup): %s\n", Settings.BackupPath)
fmt.Printf("Backup Keep: %d\n", Settings.BackupKeep)
}
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))
}
}
+177
View File
@@ -0,0 +1,177 @@
package imgcache
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
)
// Cache : Cache strcut
type Cache struct {
path string
cacheURL string
caching bool
images map[string]string
Queue []string
Cache []string
Image imageFunc
sync.RWMutex
}
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{}
var queue []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
}
c.Image.Caching = func() {
c.Lock()
defer c.Unlock()
var filename string
for _, src := range c.Queue {
resp, err := http.Get(src)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
continue
}
filename = fmt.Sprintf("%s%s%s%s", c.path, string(os.PathSeparator), strToMD5(src), filepath.Ext(src))
file, err := os.Create(filename)
if err != nil {
continue
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
continue
}
u, err := url.Parse(src)
if err == nil {
c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))] = c.cacheURL + filename
}
queue = append(queue, src)
}
for _, q := range queue {
c.Queue = removeStringFromSlice(q, c.Queue)
}
}
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
}
+34
View File
@@ -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
}
+55 -55
View File
@@ -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
}
+7 -1
View File
@@ -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
+118 -200
View File
@@ -1,267 +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
}
// MakeInterfaceFromM3U2 :
func MakeInterfaceFromM3U2(byteStream []byte) (allChannels []interface{}, err error) {
var content = string(byteStream)
//var allChannels = make([]interface{}, 0)
func indexOfString(element string, data []string) int {
var channels = strings.Split(content, "#EXTINF")
for k, v := range data {
if element == v {
return k
}
}
var parseMetaData = func(metaData string) map[string]string {
var values string // Save all values in a key
var channel = make(map[string]string)
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
//var exceptForChannelName = `(,[^.$\n]*|,[^.$\r]*)`
var exceptForChannelName = `(,[^\n]*|,[^\r]*)`
var exceptForStreamingURL = `(\n.*?\n|\r.*?\r|\n.*?\z|\r.*?\z)`
//var exceptForStreamingURL = `^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?`
// Parse all parameters
p := regexp.MustCompile(exceptForParameter)
var parameter = p.FindAllString(metaData, -1)
//fmt.Println(parameter)
for _, i := range parameter {
var remove = i
i = strings.Replace(i, `"`, "", -1)
if strings.Contains(i, "=") {
var item = strings.Split(i, "=")
switch strings.Contains(item[0], "tvg") {
case true:
channel[strings.ToLower(item[0])] = item[1]
case false:
channel[item[0]] = item[1]
}
switch strings.Contains(item[1], "://") {
case false:
values = values + item[1] + " "
}
}
metaData = strings.Replace(metaData, remove, "", 1)
}
// Parse channel name (after the comma)
n := regexp.MustCompile(exceptForChannelName)
var name = n.FindAllString(metaData, 1)
//name[len(name) - 1] = strings.Replace(name[len(name) - 1], `\r`, "", -1)
var channelName string
if len(name) == 0 {
if v, ok := channel["tvg-name"]; ok {
channelName = v
}
} else {
channelName = name[len(name)-1][1:len(name[len(name)-1])]
}
channelName = strings.Replace(channelName, `"`, "", -1)
var replacer = strings.NewReplacer("\n", "", "\r", "")
channel["name"] = replacer.Replace(channelName)
values = values + channelName + " "
// Parse streaming URL
u := regexp.MustCompile(exceptForStreamingURL)
var streamingURL = u.FindAllString(metaData, -1)
var url = strings.Replace(streamingURL[0], "\n", "", -1)
url = strings.Replace(url, "\r", "", -1)
url = strings.Trim(url, "\r\n")
channel["url"] = url
channel["_values"] = values
// Search for a unique ID
for key, value := range channel {
if !strings.Contains(strings.ToLower(key), "tvg-id") {
if strings.Contains(strings.ToLower(key), "id") {
channel["_uuid.key"] = key
channel["_uuid.value"] = value
break
}
}
}
return channel
}
if strings.Contains(channels[0], "#EXTM3U") {
for _, thisStream := range channels {
if !strings.Contains(thisStream, "#EXTM3U") {
var channel = parseMetaData(thisStream)
allChannels = append(allChannels, channel)
}
}
} else {
err = errors.New("No valid m3u file")
}
return
return -1
}
-129
View File
@@ -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
}
-271
View File
@@ -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
}
+14 -8
View File
@@ -9,11 +9,11 @@ import (
"strconv"
"strings"
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,7 +34,7 @@ func parsePlaylist(filename, fileType string) (channels []interface{}, err error
}
// Streams filtern
func filterThisStream(s interface{}) (status bool) {
func filterThisStream(s any) (status bool) {
status = false
var stream = s.(map[string]string)
@@ -43,6 +43,10 @@ func filterThisStream(s interface{}) (status bool) {
for _, filter := range Data.Filter {
if filter.Rule == "" {
continue
}
var group, name, search string
var exclude, include string
var match = false
@@ -110,18 +114,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 +181,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 +191,7 @@ func buildM3U(groups []string) (m3u string, err error) {
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err == nil {
if xepgChannel.XActive == true {
if xepgChannel.XActive {
if len(groups) > 0 {
@@ -219,7 +224,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
View File
@@ -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
View File
@@ -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
}
+363
View File
@@ -0,0 +1,363 @@
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)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
status = resp.StatusCode
body, err = io.ReadAll(resp.Body)
if err != nil {
return
}
return
}
+26 -18
View File
@@ -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,7 +142,7 @@ func getProviderData(fileType, fileID string) (err error) {
for dataID, d := range dataMap {
var data = d.(map[string]interface{})
var data = d.(map[string]any)
var fileSource = data["file.source"].(string)
newProvider = false
@@ -152,7 +152,7 @@ func getProviderData(fileType, fileID string) (err error) {
}
// Wenn eine ID vorhanden ist und nicht mit der aus der Datanbank übereinstimmt, wird die Aktualisierung übersprungen (goto)
if len(fileID) > 0 && newProvider == false {
if len(fileID) > 0 && !newProvider {
if dataID != fileID {
goto Done
}
@@ -204,7 +204,7 @@ func getProviderData(fileType, fileID string) (err error) {
ShowError(err, 000)
var downloadErr = err
if newProvider == false {
if !newProvider {
// Prüfen ob ältere Datei vorhanden ist
var file = System.Folder.Data + dataID + fileExtension
@@ -220,8 +220,8 @@ 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
@@ -236,11 +236,11 @@ 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 {
@@ -282,15 +282,23 @@ 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())
client := &http.Client{}
resp, err := client.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
}
@@ -314,7 +322,7 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e
}
body, err = ioutil.ReadAll(resp.Body)
body, err = io.ReadAll(resp.Body)
if err != nil {
return
}
+76 -63
View File
@@ -12,7 +12,11 @@ import (
func showInfo(str string) {
var max = 22
if System.Flag.Info {
return
}
var max = 23
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
@@ -35,7 +39,6 @@ func showInfo(str string) {
}
return
}
func showDebug(str string, level int) {
@@ -44,7 +47,7 @@ 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
@@ -69,12 +72,11 @@ func showDebug(str string, level int) {
}
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,7 +103,6 @@ func showHighlight(str string) {
addNotification(notification)
return
}
func showWarning(errCode int) {
@@ -117,7 +118,6 @@ func showWarning(errCode int) {
WebScreenLog.Warnings++
mutex.Unlock()
return
}
// ShowError : Zeigt die Fehlermeldungen in der Konsole
@@ -135,7 +135,6 @@ func ShowError(err error, errCode int) {
WebScreenLog.Errors++
mutex.Unlock()
return
}
func printLogOnScreen(logMsg string, logType string) {
@@ -207,7 +206,6 @@ func logCleanUp() {
WebScreenLog.Log = logs
return
}
// Fehlercodes
@@ -220,151 +218,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 (%s), file must be at least version %s", System.File.Settings, System.Compatibility)
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 = "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)
+135
View File
@@ -0,0 +1,135 @@
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 {
var filename = getPlatformFile(file)
if err := os.WriteFile(filename, data, 0600); err != nil {
return err
}
// WriteFile keeps the mode of an existing file; tighten it.
return os.Chmod(filename, 0600)
}
+210
View File
@@ -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)
}
}
+49 -46
View File
@@ -1,69 +1,72 @@
package src
import (
"fmt"
"log"
"os"
"os/signal"
"time"
"fmt"
"log"
"os"
"os/signal"
"time"
"github.com/koron/go-ssdp"
"github.com/koron/go-ssdp"
)
// SSDP : SSPD / DLNA Server
func SSDP() {
func SSDP() (err error) {
showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP))
if !Settings.SSDP || System.Flag.Info {
return
}
if Settings.SSDP == false {
return
}
showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP))
time.Sleep(10 * time.Second)
ad, err := ssdp.Advertise(
"upnp:"+System.AppName, // send as "ST"
System.DeviceID+"::upnp:"+System.AppName, // send as "USN"
System.URLBase+"/device.xml", // send as "LOCATION"
System.AppName, // send as "SERVER"
1800) // send as "maxAge" in "CACHE-CONTROL"
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
if err != nil {
ShowError(err, 000)
}
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"
// Debug SSDP
if System.Flag.Debug == 3 {
ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags)
}
if err != nil {
return
}
var aliveTick <-chan time.Time
var ai = 10
// Debug SSDP
if System.Flag.Debug == 3 {
ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags)
}
if ai > 0 {
aliveTick = time.Tick(time.Duration(ai) * time.Second)
} else {
aliveTick = make(chan time.Time)
}
go func(adv *ssdp.Advertiser) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
aliveTick := time.Tick(300 * time.Second)
loop:
loop:
for {
for {
select {
select {
case <-aliveTick:
err = adv.Alive()
if err != nil {
ShowError(err, 0)
adv.Bye()
adv.Close()
break loop
}
case <-aliveTick:
ad.Alive()
case <-quit:
os.Exit(0)
break loop
case <-quit:
adv.Bye()
adv.Close()
os.Exit(0)
break loop
}
}
}
}
ad.Bye()
ad.Close()
}(ad)
return
}
+9 -20
View File
@@ -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
@@ -68,12 +62,13 @@ type ThisStream struct {
// Segment : URL Segmente (HLS / M3U8)
type Segment struct {
Duration float64
Info bool
Sequence int64
URL string
Version int
Wait float64
Duration float64
Info bool
PlaylistType string
Sequence int64
URL string
Version int
Wait float64
StreamInf struct {
AverageBandwidth int
@@ -93,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
View File
@@ -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 {
+127 -122
View File
@@ -1,5 +1,7 @@
package src
import "xteve/src/internal/imgcache"
// SystemStruct : Beinhaltet alle Systeminformationen
type SystemStruct struct {
Addresses struct {
@@ -8,17 +10,30 @@ type SystemStruct struct {
XML string
}
APIVersion string
AppName string
ARCH string
Branch string
Build string
Compatibility string
ConfigurationWizard bool
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
@@ -30,11 +45,17 @@ type SystemStruct struct {
XML string
}
Compressed struct {
GZxml string
}
Flag struct {
Branch string
Debug int
Port string
SSDP bool
Branch string
Debug int
Info bool
Port string
Restore string
SSDP bool
}
Folder struct {
@@ -56,7 +77,6 @@ type SystemStruct struct {
Name string
OS string
ScanInProgress int
TimeForAutoUpdate string
Notification map[string]Notification
@@ -68,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
@@ -126,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
}
}
@@ -151,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"`
@@ -238,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"`
}
+53 -43
View File
@@ -3,14 +3,14 @@ package src
// RequestStruct : Anfragen über die Websocket Schnittstelle
type RequestStruct struct {
// Befehle an xTeVe
Cmd string `json:"cmd,required"`
Cmd string `json:"cmd"`
// Benutzer
DeleteUser bool `json:"deleteUser,omitempty"`
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 +18,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 +28,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,7 +68,7 @@ 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)
@@ -69,46 +79,46 @@ type ResponseStruct struct {
DVR string `json:"DVR"`
EpgSource string `json:"epgSource"`
Errors int `json:"errors"`
M3U string `json:"m3u-url,required"`
M3U string `json:"m3u-url"`
OS string `json:"os"`
Streams string `json:"streams"`
UUID string `json:"uuid"`
Version string `json:"version"`
Warnings int `json:"warnings"`
XEPGCount int64 `json:"xepg"`
XML string `json:"xepg-url,required"`
} `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 +135,7 @@ type APIRequestStruct struct {
type APIResponseStruct struct {
EpgSource string `json:"epg.source,omitempty"`
Error string `json:"err,omitempty"`
Status bool `json:"status,required"`
Status bool `json:"status"`
StreamsActive int64 `json:"streams.active,omitempty"`
StreamsAll int64 `json:"streams.all,omitempty"`
StreamsXepg int64 `json:"streams.xepg,omitempty"`
@@ -139,7 +149,7 @@ type APIResponseStruct struct {
// WebScreenLogStruct : Logs werden im RAM gespeichert und für das Webinterface bereitgestellt
type WebScreenLogStruct struct {
Errors int `json:"errors,required"`
Log []string `json:"log,required"`
Warnings int `json:"warnings,required"`
Errors int `json:"errors"`
Log []string `json:"log"`
Warnings int `json:"warnings"`
}
+53 -1
View File
@@ -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"`
+115 -23
View File
@@ -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"] = "XEPG"
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["version"] = System.Version
defaults["xteveAutoUpdate"] = true
defaults["temp.path"] = System.Folder.Temp
defaults["udpxy"] = ""
defaults["version"] = System.DBVersion
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,13 +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
@@ -179,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!"
}
@@ -198,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
@@ -228,7 +316,6 @@ func setGlobalDomain(domain string) {
System.Addresses.XML = getErrMsg(2106)
}
return
}
// UUID generieren
@@ -250,7 +337,6 @@ func setDeviceID() {
System.DeviceID = fmt.Sprintf("%s:%d", id, Settings.Tuner)
}
return
}
// Provider Streaming-URL zu xTeVe Streaming-URL konvertieren
@@ -259,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)
}
@@ -298,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)
+77 -43
View File
@@ -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,7 +204,7 @@ func saveMapToJSONFile(file string, tmpMap interface{}) error {
return err
}
err = ioutil.WriteFile(filename, []byte(jsonString), 0644)
err = os.WriteFile(filename, []byte(jsonString), 0644)
if err != nil {
return err
}
@@ -186,19 +212,20 @@ func saveMapToJSONFile(file string, tmpMap interface{}) error {
return nil
}
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
}
@@ -206,10 +233,12 @@ func loadJSONFileToMap(file string) (tmpMap map[string]interface{}, err error) {
func readByteFromFile(file string) (content []byte, err error) {
f, err := os.Open(getPlatformFile(file))
if err != nil {
return
}
defer f.Close()
content, err = ioutil.ReadAll(f)
f.Close()
content, err = io.ReadAll(f)
return
}
@@ -217,7 +246,7 @@ func readByteFromFile(file string) (content []byte, err error) {
func writeByteToFile(file string, data []byte) (err error) {
var filename = getPlatformFile(file)
err = ioutil.WriteFile(filename, data, 0644)
err = os.WriteFile(filename, data, 0644)
return
}
@@ -232,7 +261,7 @@ func readStringFromFile(file string) (str string, err error) {
return
}
content, err = ioutil.ReadFile(filename)
content, err = os.ReadFile(filename)
if err != nil {
ShowError(err, 0)
return
@@ -276,6 +305,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 +345,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 +381,6 @@ func indexOfFloat64(element float64, data []float64) int {
return -1
}
func indexOfInt(element int, data []int) int {
for k, v := range data {
if element == v {
return (k)
}
}
return -1
}
func getMD5(str string) string {
md5Hasher := md5.New()
-273
View File
@@ -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"] = "1.9.0"
err = saveMapToJSONFile(System.File.Settings, settingsMap)
if err != nil {
return
}
goto checkVersion
} else {
err = errors.New(getErrMsg(1030))
return
}
case "1.9.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
}
+15
View File
@@ -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
}
-54
View File
@@ -1,54 +0,0 @@
package src
var webUI = make(map[string]interface{})
func loadHTMLMap() {
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/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/js/.DS_Store"] = "AAAAAUJ1ZDEAABAAAAAIAAAAEAAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAABAAAAQAAAAAEAAACAAAAAAQAAAQAAAAABAAACAAAAAAEAAAQAAAAAAAAAAAEAABAAAAAAAQAAIAAAAAABAABAAAAAAAEAAIAAAAAAAQABAAAAAAABAAIAAAAAAAEABAAAAAAAAQAIAAAAAAABABAAAAAAAAEAIAAAAAAAAQBAAAAAAAABAIAAAAAAAAEBAAAAAAAAAQIAAAAAAAABBAAAAAAAAAEIAAAAAAAAARAAAAAAAAABIAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
webUI["html/lang/en.json"] = "ewogICJtYWluTWVudSI6IHsKICAgICJpdGVtIjp7CiAgICAgICJwbGF5bGlzdCI6ICJQbGF5bGlzdCIsCiAgICAgICJwbXNJRCI6ICJQTVMgSUQiLAogICAgICAiZmlsdGVyIjogIkZpbHRlciIsCiAgICAgICJ4bWx0diI6ICJYTUxUViIsCiAgICAgICJtYXBwaW5nIjogIk1hcHBpbmciLAogICAgICAidXNlcnMiOiAiVXNlcnMiLAogICAgICAic2V0dGluZ3MiOiAiU2V0dGluZ3MiLAogICAgICAibG9nIjogIkxvZyIsCiAgICAgICJsb2dvdXQiOiAiTG9nb3V0IgogICAgfSwKICAgICJoZWFkbGluZSI6IHsKICAgICAgInBsYXlsaXN0IjogIkxvY2FsIG9yIHJlbW90ZSBwbGF5bGlzdHMiLAogICAgICAiZmlsdGVyIjogIkZpbHRlciBwbGF5bGlzdCIsCiAgICAgICJ4bWx0diI6ICJMb2NhbCBvciByZW1vdGUgWE1MVFYgZmlsZXMiLAogICAgICAibWFwcGluZyI6ICJNYXAgcGxheWxpc3QgY2hhbm5lbHMgdG8gRVBHIGNoYW5uZWxzIiwKICAgICAgInVzZXJzIjogIlVzZXIgbWFuYWdlbWVudCIsCiAgICAgICJzZXR0aW5ncyI6ICJTZXR0aW5ncyIsCiAgICAgICJsb2ciOiAiTG9nIiwKICAgICAgImxvZ291dCI6ICJMb2dvdXQiCiAgICB9CiAgfSwKICAiY29uZmlybSI6ewogICAgInJlc3RvcmUiOiAiQWxsIGRhdGEgd2lsbCBiZSByZXBsYWNlZCB3aXRoIHRob3NlIGZyb20gdGhlIGJhY2t1cC5TaG91bGQgdGhlIGZpbGVzIGJlIHJlc3RvcmVkPyIKICB9LAogICJhbGVydCI6IHsKICAgICJmaWxlTG9hZGluZ0Vycm9yIjogIkZpbGUgY291bGRuJ3QgYmUgbG9hZGVkIiwKICAgICJpbnZhbGlkQ2hhbm5lbE51bWJlciI6ICJJbnZhbGlkIGNoYW5uZWwgbnVtYmVyIgogIH0sCiAgImJ1dHRvbiI6ewogICAgImJhY2siOiAiQmFjayIsCiAgICAiYmFja3VwIjogIkJhY2t1cCIsCiAgICAiYnVsa0VkaXQiOiAiQnVsayBFZGl0IiwKICAgICJjYW5jZWwiOiAiQ2FuY2VsIiwKICAgICJkZWxldGUiOiAiRGVsZXRlIiwKICAgICJkb25lIjogIkRvbmUiLAogICAgImxvZ2luIjogIkxvZ2luIiwKICAgICJuZXciOiAiTmV3IiwKICAgICJuZXh0IjogIk5leHQiLAogICAgInJlc3RvcmUiOiAiUmVzdG9yZSIsCiAgICAic2F2ZSI6ICJTYXZlIiwKICAgICJzZWFyY2giOiAiU2VhcmNoIiwKICAgICJ1cGRhdGUiOiAiVXBkYXRlIiwKICAgICJjcmFldGVBY2NvdW50IjogIkNyZWF0ZSBBY2NvdW50IiwKICAgICJyZXNldGxvZ3MiOiAiUmVzZXQgTG9ncyIsCiAgICAidXBsb2FkTG9nbyI6ICJVcGxvYWQgTG9nbyIKICB9LAogICJmaWx0ZXIiOiB7CiAgICAidGFibGUiOiB7CiAgICAgICJuYW1lIjogIkZpbHRlciBOYW1lIiwKICAgICAgInR5cGUiOiAiRmlsdGVyIFR5cGUiLAogICAgICAiZmlsdGVyIjogIkZpbHRlciIKICAgIH0sCiAgICAiY3VzdG9tIjogIkN1c3RvbSIsCiAgICAiZ3JvdXAiOiAiR3JvdXAiLAogICAgIm5hbWUiOiB7CiAgICAgICJ0aXRsZSI6ICJGaWx0ZXIgTmFtZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJGaWx0ZXIgbmFtZSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgImRlc2NyaXB0aW9uIjogewogICAgICAidGl0bGUiOiAiRGVzY3JpcHRpb24iLAogICAgICAicGxhY2Vob2xkZXIiOiAiRGVzY3JpcHRpb24iLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJ0eXBlIjogewogICAgICAidGl0bGUiOiAiVHlwZSIsCiAgICAgICJncm91cFRpdGxlIjogIkdyb3VwIFRpdGxlIiwKICAgICAgImN1c3RvbUZpbHRlciI6ICJDdXN0b20gRmlsdGVyIgogICAgfSwKICAgICJjYXNlU2Vuc2l0aXZlIjogewogICAgICAidGl0bGUiOiAiQ2FzZSBTZW5zaXRpdmUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAiZmlsdGVyUnVsZSI6IHsKICAgICAgInRpdGxlIjogIkZpbHRlciBSdWxlIiwKICAgICAgInBsYWNlaG9sZGVyIjogIlNwb3J0IHtIRH0gIXtFUyxJVH0iLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWx0ZXJHcm91cCI6IHsKICAgICAgInRpdGxlIjogIkdyb3VwIFRpdGxlIiwKICAgICAgInBsYWNlaG9sZGVyIjogIiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJTZWxlY3QgYSBNM1UgZ3JvdXAuIChDb3VudGVyKTxicj5DaGFuZ2luZyB0aGUgZ3JvdXAgdGl0bGUgaW4gdGhlIE0zVSBpbnZhbGlkYXRlcyB0aGUgZmlsdGVyLiIKICAgIH0sCiAgICAiaW5jbHVkZSI6IHsKICAgICAgInRpdGxlIjogIkluY2x1ZGUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiRkhELFVIRCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJDaGFubmVsIG5hbWUgbXVzdCBpbmNsdWRlLjxicj4oQ29tbWEgc2VwYXJhdGVkKSBDb21tYSBtZWFucyBvciIKICAgIH0sCiAgICAiZXhjbHVkZSI6IHsKICAgICAgInRpdGxlIjogIkV4Y2x1ZGUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiRVMsSVQiLAogICAgICAiZGVzY3JpcHRpb24iOiAiQ2hhbm5lbCBuYW1lIG11c3Qgbm90IGNvbnRhaW4uPGJyPihDb21tYSBzZXBhcmF0ZWQpIENvbW1hIG1lYW5zIG9yIgogICAgfQoKICB9LAogICJwbGF5bGlzdCI6IHsKICAgICJ0YWJsZSI6IHsKICAgICAgInBsYXlsaXN0IjogIlBsYXlsaXN0IiwKICAgICAgInR1bmVyIjogIlR1bmVyIiwKICAgICAgImxhc3RVcGRhdGUiOiAiTGFzdCBVcGRhdGUiLAogICAgICAiYXZhaWxhYmlsaXR5IjogIkF2YWlsYWJpbGl0eSIsCiAgICAgICJ0eXBlIjogIlR5cGUiLAogICAgICAic3RyZWFtcyI6ICJTdHJlYW1zIiwKICAgICAgImdyb3VwVGl0bGUiOiAiZ3JvdXAtdGl0bGUiLAogICAgICAidHZnSUQiOiAidHZnLWlkIiwKICAgICAgInVuaXF1ZUlEIjogIlVuaXF1ZSBJRCIKICAgIH0sCiAgICAicGxheWxpc3RUeXBlIjogewogICAgICAidGl0bGUiOiAiUGxheWxpc3QgdHlwZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJ0eXBlIjogewogICAgICAidGl0bGUiOiAiVHlwZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJuYW1lIjogewogICAgICAidGl0bGUiOiAiTmFtZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJQbGF5bGlzdCBuYW1lIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAiZGVzY3JpcHRpb24iOiB7CiAgICAgICJ0aXRsZSI6ICJEZXNjcmlwdGlvbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJEZXNjcmlwdGlvbiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgImZpbGVNM1UiOiB7CiAgICAgICJ0aXRsZSI6ICJNM1UgRmlsZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJGaWxlIHBhdGggb3IgVVJMIG9mIHRoZSBNM1UiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWxlSERIUiI6IHsKICAgICAgInRpdGxlIjogIkhESG9tZVJ1biBJUCIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJJUCBhZGRyZXNzIGFuZCBwb3J0ICgxOTIuMTY4LjEuMTA6NTAwNCkiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJ0dW5lciI6IHsKICAgICAgInRpdGxlIjogIlR1bmVyIC8gU3RyZWFtcyIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiTnVtYmVyIG9mIHBhcmFsbGVsIGNvbm5lY3Rpb25zIHRoYXQgY2FuIGJlIGVzdGFibGlzaGVkIHRvIHRoZSBwcm92aWRlci4gPGJyPk9ubHkgYXZhaWxLine truncated
webUI["html/img/.DS_Store"] = "AAAAAUJ1ZDEAABAAAAAIAAAAEAAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAABAAAAQAAAAAEAAACAAAAAAQAAAQAAAAABAAACAAAAAAEAAAQAAAAAAAAAAAEAABAAAAAAAQAAIAAAAAABAABAAAAAAAEAAIAAAAAAAQABAAAAAAABAAIAAAAAAAEABAAAAAAAAQAIAAAAAAABABAAAAAAAAEAIAAAAAAAAQBAAAAAAAABAIAAAAAAAAEBAAAAAAAAAQIAAAAAAAABBAAAAAAAAAEIAAAAAAAAARAAAAAAAAABIAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
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/js/authentication_ts.js"] = "ZnVuY3Rpb24gbG9naW4oKSB7CiAgICB2YXIgZXJyID0gZmFsc2U7CiAgICB2YXIgZGF0YSA9IG5ldyBPYmplY3QoKTsKICAgIHZhciBkaXYgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiY29udGVudCIpOwogICAgdmFyIGZvcm0gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYXV0aGVudGljYXRpb24iKTsKICAgIHZhciBpbnB1dHMgPSBkaXYuZ2V0RWxlbWVudHNCeVRhZ05hbWUoIklOUFVUIik7CiAgICBjb25zb2xlLmxvZyhpbnB1dHMpOwogICAgZm9yICh2YXIgaSA9IGlucHV0cy5sZW5ndGggLSAxOyBpID49IDA7IGktLSkgewogICAgICAgIHZhciBrZXkgPSBpbnB1dHNbaV0ubmFtZTsKICAgICAgICB2YXIgdmFsdWUgPSBpbnB1dHNbaV0udmFsdWU7CiAgICAgICAgaWYgKHZhbHVlLmxlbmd0aCA9PSAwKSB7CiAgICAgICAgICAgIGlucHV0c1tpXS5zdHlsZS5ib3JkZXJDb2xvciA9ICJyZWQiOwogICAgICAgICAgICBlcnIgPSB0cnVlOwogICAgICAgIH0KICAgICAgICBkYXRhW2tleV0gPSB2YWx1ZTsKICAgIH0KICAgIGlmIChlcnIgPT0gdHJ1ZSkgewogICAgICAgIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgICAgICAgcmV0dXJuOwogICAgfQogICAgaWYgKGRhdGEuaGFzT3duUHJvcGVydHkoImNvbmZpcm0iKSkgewogICAgICAgIGlmIChkYXRhWyJjb25maXJtIl0gIT0gZGF0YVsicGFzc3dvcmQiXSkgewogICAgICAgICAgICBhbGVydCgic2RhZnNkIik7CiAgICAgICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwYXNzd29yZCcpLnN0eWxlLmJvcmRlckNvbG9yID0gInJlZCI7CiAgICAgICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdjb25maXJtJykuc3R5bGUuYm9yZGVyQ29sb3IgPSAicmVkIjsKICAgICAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImVyciIpLmlubmVySFRNTCA9ICJ7ey5hY2NvdW50LmZhaWxlZH19IjsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgIH0KICAgIGNvbnNvbGUubG9nKGRhdGEpOwogICAgZm9ybS5zdWJtaXQoKTsKfQo="
webUI["html/js/configuration_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBXaXphcmRDYXRlZ29yeSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIFdpemFyZENhdGVnb3J5KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJjb250ZW50IjsKICAgIH0KICAgIFdpemFyZENhdGVnb3J5LnByb3RvdHlwZS5jcmVhdGVDYXRlZ29yeUhlYWRsaW5lID0gZnVuY3Rpb24gKHZhbHVlKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJINCIpOwogICAgICAgIGVsZW1lbnQuaW5uZXJIVE1MID0gdmFsdWU7CiAgICAgICAgcmV0dXJuIGVsZW1lbnQ7CiAgICB9OwogICAgcmV0dXJuIFdpemFyZENhdGVnb3J5Owp9KCkpOwp2YXIgV2l6YXJkSXRlbSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uIChfc3VwZXIpIHsKICAgIF9fZXh0ZW5kcyhXaXphcmRJdGVtLCBfc3VwZXIpOwogICAgZnVuY3Rpb24gV2l6YXJkSXRlbShrZXksIGhlYWRsaW5lKSB7CiAgICAgICAgdmFyIF90aGlzID0gX3N1cGVyLmNhbGwodGhpcykgfHwgdGhpczsKICAgICAgICBfdGhpcy5oZWFkbGluZSA9IGhlYWRsaW5lOwogICAgICAgIF90aGlzLmtleSA9IGtleTsKICAgICAgICByZXR1cm4gX3RoaXM7CiAgICB9CiAgICBXaXphcmRJdGVtLnByb3RvdHlwZS5jcmVhdGVXaXphcmQgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgdmFyIGhlYWRsaW5lID0gdGhpcy5jcmVhdGVDYXRlZ29yeUhlYWRsaW5lKHRoaXMuaGVhZGxpbmUpOwogICAgICAgIHZhciBrZXkgPSB0aGlzLmtleTsKICAgICAgICB2YXIgY29udGVudCA9IG5ldyBQb3B1cENvbnRlbnQoKTsKICAgICAgICB2YXIgZGVzY3JpcHRpb247CiAgICAgICAgdmFyIGRvYyA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKHRoaXMuRG9jdW1lbnRJRCk7CiAgICAgICAgZG9jLmlubmVySFRNTCA9ICIiOwogICAgICAgIGRvYy5hcHBlbmRDaGlsZChoZWFkbGluZSk7CiAgICAgICAgc3dpdGNoIChrZXkpIHsKICAgICAgICAgICAgY2FzZSAidHVuZXIiOgogICAgICAgICAgICAgICAgdmFyIHRleHQgPSBuZXcgQXJyYXkoKTsKICAgICAgICAgICAgICAgIHZhciB2YWx1ZXMgPSBuZXcgQXJyYXkoKTsKICAgICAgICAgICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDw9IDEwMDsgaSsrKSB7CiAgICAgICAgICAgICAgICAgICAgdGV4dC5wdXNoKGkpOwogICAgICAgICAgICAgICAgICAgIHZhbHVlcy5wdXNoKGkpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgdmFyIHNlbGVjdCA9IGNvbnRlbnQuY3JlYXRlU2VsZWN0KHRleHQsIHZhbHVlcywgIjEiLCBrZXkpOwogICAgICAgICAgICAgICAgc2VsZWN0LnNldEF0dHJpYnV0ZSgiY2xhc3MiLCAid2l6YXJkIik7CiAgICAgICAgICAgICAgICBzZWxlY3QuaWQgPSBrZXk7CiAgICAgICAgICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoc2VsZWN0KTsKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uID0gInt7LndpemFyZC50dW5lci5kZXNjcmlwdGlvbn19IjsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJlcGdTb3VyY2UiOgogICAgICAgICAgICAgICAgdmFyIHRleHQgPSBbIlBNUyIsICJYRVBHIl07CiAgICAgICAgICAgICAgICB2YXIgdmFsdWVzID0gWyJQTVMiLCAiWEVQRyJdOwogICAgICAgICAgICAgICAgdmFyIHNlbGVjdCA9IGNvbnRlbnQuY3JlYXRlU2VsZWN0KHRleHQsIHZhbHVlcywgIlhFUEciLCBrZXkpOwogICAgICAgICAgICAgICAgc2VsZWN0LnNldEF0dHJpYnV0ZSgiY2xhc3MiLCAid2l6YXJkIik7CiAgICAgICAgICAgICAgICBzZWxlY3QuaWQgPSBrZXk7CiAgICAgICAgICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoc2VsZWN0KTsKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uID0gInt7LndpemFyZC5lcGdTb3VyY2UuZGVzY3JpcHRpb259fSI7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSAibTN1IjoKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCBrZXksICIiKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgiY2xhc3MiLCAid2l6YXJkIik7CiAgICAgICAgICAgICAgICBpbnB1dC5pZCA9IGtleTsKICAgICAgICAgICAgICAgIGRvYy5hcHBlbmRDaGlsZChpbnB1dCk7CiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbiA9ICJ7ey53aXphcmQubTN1LmRlc2NyaXB0aW9ufX0iOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgInhtbHR2IjoKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCBrZXksICIiKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgiY2xhc3MiLCAid2l6YXJkIik7CiAgICAgICAgICAgICAgICBpbnB1dC5pZCA9IGtleTsKICAgICAgICAgICAgICAgIGRvYy5hcHBlbmRDaGlsZChpbnB1dCk7CiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbiA9ICJ7ey53aXphcmQueG1sdHYuZGVzY3JpcHRpb259fSI7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgZGVmYXVsdDoKICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKGtleSk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICB9CiAgICAgICAgdmFyIHByZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlBSRSIpOwogICAgICAgIHByZS5pbm5lckhUTUwgPSBkZXNjcmlwdGlvbjsKICAgICAgICBkb2MuYXBwZW5kQ2hpbGQocHJlKTsKICAgICAgICBjb25zb2xlLmxvZyhoZWFkbGluZSwga2V5KTsKICAgIH07CiAgICByZXR1cm4gV2l6YXJkSXRlbTsKfShXaXphcmRDYXRlZ29yeSkpOwpmdW5jdGlvbiByZWFkeUZvckNvbmZpZ3VyYXRpb24od2l6YXJkKSB7CiAgICB2YXIgc2VydmVyID0gbmV3IFNlcnZlcigiZ2V0U2VydmVyQ29uZmlnIik7CiAgICBzZXJ2ZXIucmVxdWVzdChuZXcgT2JqZWN0KCkpOwogICAgc2hvd0VsZW1lbnQoImxvYWRpbmciLCBmYWxzZSk7CiAgICBjb25maWd1cmF0aW9uV2l6YXJkW3dpemFyZF0uY3JlYXRlV2l6YXJkKCk7Cn0KZnVuY3Rpb24gc2F2ZVdpemFyZCgpIHsKICAgIHZhciBjbWQgPSAic2F2ZVdpemFyZCI7CiAgICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImNvbnRlbnQiKLine truncated
webUI["html/js/log.js"] = "dmFyIGxvZ0ludGVydmFsCgoKZnVuY3Rpb24gdXBkYXRlTG9nKCkgewogIHZhciBkYXRhID0gbmV3IE9iamVjdCgpOwogIGRhdGFbImNtZCJdID0gImdldExvZyI7CiAgeFRlVmUoZGF0YSk7CiAgd3JpdGVMb2dJbkRpdigpOwogIHJldHVybgp9CgpmdW5jdGlvbiB3cml0ZUxvZ0luRGl2KCkgewogIHZhciBsb2dzID0gbG9nWyJsb2ciXTsKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIikubGFzdENoaWxkLmxhc3RDaGlsZDsKICBkaXYuaW5uZXJIVE1MID0gIiI7CgogIHZhciBtYXggPSA1MDsKICAKICAKICBmb3IgKHZhciBpID0gMDsgaSA8IGxvZ3MubGVuZ3RoOyBpKyspIHsKICAgIHZhciBuZXdFbnRyeSA9IG5ldyBPYmplY3QoKTsKICAgIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJQIjsKCiAgICBpZiAobG9nc1tpXS5pbmNsdWRlcygiRVJST1IiKSkgewovLyAgICAgIGNhc2UgIndhcm5pbmdzIjogIG1zZ1R5cGUgPSAid2FybmluZ01zZyI7IGJyZWFrOwogICAgICBuZXdFbnRyeVsiY2xhc3MiXSAgID0gImVycm9yTXNnIjsKICAgIH0KCiAgICBpZiAobG9nc1tpXS5pbmNsdWRlcygiV0FSTklORyIpKSB7Ci8vICAgICAgY2FzZSAid2FybmluZ3MiOiAgbXNnVHlwZSA9ICJ3YXJuaW5nTXNnIjsgYnJlYWs7CiAgICAgIG5ld0VudHJ5WyJjbGFzcyJdICAgPSAid2FybmluZ01zZyI7CiAgICB9CgogICAgbmV3RW50cnlbIl90ZXh0Il0gICAgID0gbG9nc1tpXTsKICAgIAogICAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKICB9CgogIGNhbGN1bGF0ZVdyYXBwZXJIZWlnaHQoKTsKICB2YXIgc2Nyb2xsRGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJveC13cmFwcGVyIik7CiAgc2Nyb2xsRGl2LnNjcm9sbFRvcCA9IHNjcm9sbERpdi5zY3JvbGxIZWlnaHQ7Cn0KCmZ1bmN0aW9uIHNob3dMb2cob2JqKSB7CiAgLy9sb2dJbnRlcnZhbCA9IHNldEludGVydmFsKHVwZGF0ZUxvZywgNTAwMCk7CgogIHZhciBsb2dzID0gbG9nWyJsb2ciXTsKCiAgdmFyIGRpdiA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJzZXR0aW5ncyIpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gID0gIkhSIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwogIC8vZGl2ID0gZGl2Lmxhc3RDaGlsZDsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gICAgICAgPSAiRW1wdHkgTG9nIjsKICBuZXdFbnRyeVsib25jbGljayJdICAgICA9ICJlbXB0eUxvZygpIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gICAgPSAiY29kZSI7CiAgbmV3RW50cnlbIl90ZXh0Il0gICAgICAgID0gIlVwZGF0ZSBMb2c6ICI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJjaGVja2JveCI7CiAgLy9uZXdFbnRyeVsiY2hlY2tlZCJdICAgICA9ICJjaGVja2JveCI7CiAgbmV3RW50cnlbIm9uY2xpY2siXSAgICAgPSAibG9nVXBkYXRlcyh0aGlzKSI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgCgoKICAKICB2YXIgbmV3V3JhcHBlciA9IG5ldyBPYmplY3QoKTsKICBuZXdXcmFwcGVyWyJfZWxlbWVudCJdICA9ICJESVYiOwogIG5ld1dyYXBwZXJbImlkIl0gICAgICAgID0gImJveC13cmFwcGVyIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdXcmFwcGVyKSk7CiAgZGl2ID0gZGl2Lmxhc3RDaGlsZDsKCiAgdmFyIG5ld1ByZSA9IG5ldyBPYmplY3QoKTsKICBuZXdQcmVbIl9lbGVtZW50Il0gID0gIlBSRSI7CiAgbmV3UHJlWyJpZCJdICAgICAgICA9ICJsb2dTY3JlZW4iOwogIGRpdi5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld1ByZSkpOwoKICBkaXYgPSBkaXYubGFzdENoaWxkOwoKICB3cml0ZUxvZ0luRGl2KCkKICByZXR1cm4KfQoKZnVuY3Rpb24gZW1wdHlMb2coKSB7CiAgdmFyIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgZGF0YVsiY21kIl0gPSAiZW1wdHlMb2ciOwogIHhUZVZlKGRhdGEpOwogIHJldHVybgp9CgpmdW5jdGlvbiBsb2dVcGRhdGVzKGVsbSkgewogIHN3aXRjaChlbG0uY2hlY2tlZCkgewogICAgY2FzZSBmYWxzZTogY2xlYXJJbnRlcnZhbChsb2dJbnRlcnZhbCk7IGJyZWFrOwogICAgY2FzZSB0cnVlOiBsb2dJbnRlcnZhbCA9IHNldEludGVydmFsKHVwZGF0ZUxvZywgNTAwMCk7IGJyZWFrOwogICAgCiAgfQp9"
webUI["html/js/logs_ts.js"] = "dmFyIExvZyA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIExvZygpIHsKICAgIH0KICAgIExvZy5wcm90b3R5cGUuY3JlYXRlTG9nID0gZnVuY3Rpb24gKGVudHJ5KSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJQUkUiKTsKICAgICAgICBpZiAoZW50cnkuaW5kZXhPZigiV0FSTklORyIpICE9IC0xKSB7CiAgICAgICAgICAgIGVsZW1lbnQuY2xhc3NOYW1lID0gIndhcm5pbmdNc2ciOwogICAgICAgIH0KICAgICAgICBpZiAoZW50cnkuaW5kZXhPZigiRVJST1IiKSAhPSAtMSkgewogICAgICAgICAgICBlbGVtZW50LmNsYXNzTmFtZSA9ICJlcnJvck1zZyI7CiAgICAgICAgfQogICAgICAgIGlmIChlbnRyeS5pbmRleE9mKCJERUJVRyIpICE9IC0xKSB7CiAgICAgICAgICAgIGVsZW1lbnQuY2xhc3NOYW1lID0gImRlYnVnTXNnIjsKICAgICAgICB9CiAgICAgICAgZWxlbWVudC5pbm5lckhUTUwgPSBlbnRyeTsKICAgICAgICByZXR1cm4gZWxlbWVudDsKICAgIH07CiAgICByZXR1cm4gTG9nOwp9KCkpOwpmdW5jdGlvbiBzaG93TG9ncyhib3R0b20pIHsKICAgIHZhciBsb2cgPSBuZXcgTG9nKCk7CiAgICB2YXIgbG9ncyA9IFNFUlZFUlsibG9nIl1bImxvZyJdOwogICAgdmFyIGRpdiA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJjb250ZW50X2xvZyIpOwogICAgZGl2LmlubmVySFRNTCA9ICIiOwogICAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKGxvZ3MpOwogICAga2V5cy5mb3JFYWNoKGZ1bmN0aW9uIChsb2dJRCkgewogICAgICAgIHZhciBlbnRyeSA9IGxvZy5jcmVhdGVMb2cobG9nc1tsb2dJRF0pOwogICAgICAgIGRpdi5hcHBlbmQoZW50cnkpOwogICAgfSk7CiAgICBzZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHsKICAgICAgICBpZiAoYm90dG9tID09IHRydWUpIHsKICAgICAgICAgICAgdmFyIHdyYXBwZXIgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYm94LXdyYXBwZXIiKTsKICAgICAgICAgICAgd3JhcHBlci5zY3JvbGxUb3AgPSB3cmFwcGVyLnNjcm9sbEhlaWdodDsKICAgICAgICB9CiAgICB9LCAxMCk7Cn0KZnVuY3Rpb24gcmVzZXRMb2dzKCkgewogICAgdmFyIGNtZCA9ICJyZXNldExvZ3MiOwogICAgdmFyIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgICB2YXIgc2VydmVyID0gbmV3IFNlcnZlcihjbWQpOwogICAgc2VydmVyLnJlcXVlc3QoZGF0YSk7Cn0K"
webUI["html/css/base.css"] = "KiB7CiAgLXdlYmtpdC1hcHBlYXJhbmNlOiBub25lOwogIC1tb3otYXBwZWFyYW5jZTogbm9uZTsKICAtbXMtYXBwZWFyYW5jZTogbm9uZTsKICBmb250LWZhbWlseTogIkFyaWFsIiwgc2Fucy1zZXJpZjsKICBsZXR0ZXItc3BhY2luZzogMnB4OyAKfQoKLyoKOjotd2Via2l0LXNjcm9sbGJhciB7IAogICAgZGlzcGxheTogbm9uZTsgCn0KKi8KCjo6LXdlYmtpdC1zY3JvbGxiYXIgewogIHdpZHRoOiAxMnB4OwogIGhlaWdodDogMTJweDsKfQoKIAo6Oi13ZWJraXQtc2Nyb2xsYmFyLXRyYWNrIHsKICAtd2Via2l0LWJveC1zaGFkb3c6IGluc2V0IDAgMCA2cHggcmdiYSgwLDAsMCwwLjMpOyAKICBib3JkZXItcmFkaXVzOiA1cHg7CiAgICAKfQogCjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWIgewogIGJvcmRlci1yYWRpdXM6IDVweDsKICAtd2Via2l0LWJveC1zaGFkb3c6IGluc2V0IDAgMCA2cHggcmdiYSgwLCAwLCAwLDAuNik7IAogIGJhY2tncm91bmQtY29sb3I6ICM0NDQ7Cn0KCjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWI6aG92ZXIgewogIGJhY2tncm91bmQ6ICMzMzM7IAp9Cgo6Oi13ZWJraXQtc2Nyb2xsYmFyLWNvcm5lciB7IAogIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50OyAKfQoKYSB7CiAgY29sb3I6ICMwMEU2RkY7Cn0KCmh0bWwsIGJvZHkgewogIGNvbG9yOiAjZmZmOwogIG1hcmdpbjogMHB4IGF1dG87CiAgaGVpZ2h0OiAxMDAlOwogIGZvbnQtc2l6ZTogMTRweDsKfQoKaDIgewogIGZvbnQtc2l6ZTogMjRweDsKICBsZXR0ZXItc3BhY2luZzogMnB4Owp9CgpoMyB7CiAgZm9udC1zaXplOiAyMnB4OwogIGxldHRlci1zcGFjaW5nOiAxcHg7Cn0KCmg0IHsKICBmb250LXNpemU6IDIwcHg7CiAgbGV0dGVyLXNwYWNpbmc6IDFweDsKICBsaW5lLWhlaWdodDogMS41ZW07Cgp9CgpoNSB7CiAgZm9udC1zaXplOiAxNnB4OwogIGxldHRlci1zcGFjaW5nOiAxcHg7CiAgbGluZS1oZWlnaHQ6IDEuMmVtOwogIG1hcmdpbjogMjVweCAwcHggMTBweCAwcHg7Cn0KCmhyIHsKICBib3JkZXI6IDA7CiAgaGVpZ2h0OiAxcHg7CiAgYmFja2dyb3VuZDogIzMzMzsKICBtYXJnaW46IDEwcHggMHB4Owp9CgpwIHsKICBtYXJnaW46IDJweDsKICBwYWRkaW5nOiAycHggNXB4Owp9CgpwcmUgewogIG1hcmdpbjogMHB4IDBweCA1cHggMHB4OwogIGZvbnQtc2l6ZTogMTJweDsKICBjb2xvcjogI2RkZDsKICBsZXR0ZXItc3BhY2luZzogMXB4OwogIHdoaXRlLXNwYWNlOiBwcmUtd3JhcDsKICBmb250LWZhbWlseTogbW9ub3NwYWNlOyAKICBmb250LXNpemU6IDEycHg7IAogIGZvbnQtc3R5bGU6IG5vcm1hbDsgCiAgZm9udC12YXJpYW50OiBub3JtYWw7IAogIGxpbmUtaGVpZ2h0OiAxLjZlbTsgCn0KCmxhYmVsIHsKICBtYXJnaW4tYm90dG9tOiAyMHB4OwogIGRpc3BsYXk6IGJsb2NrOwp9CgpsaSB7CiAgbGlzdC1zdHlsZS10eXBlOiBub25lOwogIGJhY2tncm91bmQtY29sb3I6ICMxMTE7CiAgcGFkZGluZzogMTBweCAyMHB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBib3JkZXItbGVmdDogc29saWQgMnB4ICMxMTE7CiAgdHJhbnNpdGlvbjogYWxsIDAuMzsKfQoKbGk6aG92ZXIgewogIGJvcmRlci1jb2xvcjogIzAwRTZGRgp9CgpzZWxlY3QgewogIGN1cnNvcjogcG9pbnRlcjsKICB3aWR0aDogY2FsYygxMDAlICsgMnB4KTsKICBib3JkZXI6IHNvbGlkIDBweCAjMDBFNkZGOwogIGJvcmRlci1yYWRpdXM6IDBweDsKICBvdXRsaW5lOiBub25lOwogIGNvbG9yOiAjZmZmOwogIHBhZGRpbmc6IDlweCAxMHB4OwogIGRpc3BsYXk6YmxvY2s7CiAgYmFja2dyb3VuZC1jb2xvcjogIzMzMzsKICBmb250LXNpemU6IDE0cHg7CiAgbWFyZ2luOiA1cHggMHB4IDVweCAwcHg7Cn0KCnNlbGVjdDpmb2N1cyB7CiAgb3V0bGluZTogbm9uZTsKfQoKaW5wdXQgewogIC13ZWJraXQtYXBwZWFyYW5jZTogbm9uZTsKICBtYXJnaW46IDVweCAwcHg7CiAgcGFkZGluZzogMi41cHggMTBweDsKICBvdXRsaW5lOiBub25lOwogIGZvbnQtc2l6ZTogMTRweDsKfQoKaW5wdXRbdHlwZT1idXR0b25dLCBpbnB1dFt0eXBlPXN1Ym1pdF0gewogIGN1cnNvcjogcG9pbnRlcjsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogIG1hcmdpbjogMTBweCAxMHB4OwogIHBhZGRpbmc6IDEwcHggMjVweDsKICBib3JkZXI6IHNvbGlkIDBweDsKICBib3JkZXItY29sb3I6ICMwMDA7CiAgYm9yZGVyLXJhZGl1czogM3B4OwogIG91dGxpbmU6IG5vbmU7CiAgY29sb3I6ICNmZmY7Cn0KCmlucHV0W3R5cGU9YnV0dG9uXTpmb2N1cyB7IAogICBvdXRsaW5lOiBub25lOwp9CgppbnB1dFt0eXBlPWJ1dHRvbl06aG92ZXIgewogIGJhY2tncm91bmQtY29sb3I6ICMwMEU2RkY7CiAgY29sb3I6ICMwMDA7Cn0KCmlucHV0W3R5cGU9YnV0dG9uXTpob3Zlci5kZWxldGUgIHsKICBiYWNrZ3JvdW5kLWNvbG9yOiByZWQ7CiAgY29sb3I6ICNmZmY7Cn0KCmlucHV0W3R5cGU9dGV4dF0sIGlucHV0W3R5cGU9c2VhcmNoXSwgaW5wdXRbdHlwZT1wYXNzd29yZF0gewogIGNvbG9yOiAjZmZmOwogIHdpZHRoOiAtd2Via2l0LWNhbGMoMTAwJSAtIDBweCk7CiAgd2lkdGg6IC1tb3otY2FsYygxMDAlIC0gMHB4KTsKICB3aWR0aDogY2FsYygxMDAlIC0gMHB4KTsKICBvdXRsaW5lOiBub25lOwogIGJvcmRlcjogc29saWQgMXB4IHRyYW5zcGFyZW50OwogIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50OwogIGJvcmRlci1ib3R0b20tY29sb3I6ICM1NTU7CiAgYm9yZGVyLXJhZGl1czogMHB4OwogIHBhZGRpbmc6IDhweCAxMHB4Owp9CgppbnB1dFt0eXBlPSJjaGVja2JveCJdIHsKICBib3JkZXI6IHNvbGlkIDFweCAjMDBFNkZGOwogIGJhY2tncm91bmQtY29sb3I6ICMzMzM7CiAgaGVpZ2h0OiAyNXB4OwogIHdpZHRoOiAyNXB4OwogIGN1cnNvcjogcG9pbnRlcjsKICAvKgogIC13ZWJraXQtYXBwZWFyYW5jZTogY2hlY2tib3g7CiAgKi8KfQoKaW5wdXRbdHlwZT0iY2hlY2tib3giXTpjaGVja2VkIHsKICBjb2xvcjogI2ZmZjsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDBFNkZGOwogIC8qZGlzcGxheTogaW5saW5lLWJsb2NrOyovCn0KCmlucHV0W3R5cGU9ImNoZWNrYm94Il06YmVmb3JlIHsKICBwb3NpdGlvbjogaW5pdGlhbDsKICBsZWZ0OiAwcHg7CiAgbWFyZ2luLWxlZnQ6IC00cHg7CiAgY29udGVudDogIiAiOwp9CgppbnB1dFt0eXBlPSJjaGVja2JveCJdOmNoZWNrZWQ6YmVmb3JlIHsKICBwb3NpdGlvbjogaW5pdGlhbDsKICBsZWZ0OiAwcHg7CiAgbWFyZ2luLWxlZnQ6IC0zcHg7CiAgY29udGVudDogIuKckyI7CiAgY29sb3I6ICMwMDA7Cn0KCgppbnB1dFt0eXBlPWJ1dHRvbl0uY2FuY2VsIHsKICAKICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDsKICBib3JkZXItY29sb3I6IHJlZDsKfQoKaW5wdXRbdHlwZT1idXR0b25dLnNhdmV7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICBmbG9hdDogcmlnaHQ7Cn0KCgppbnB1dFt0eXBlPWJ1dHRvbl0uYmxhY2ssIGlucHV0W3R5cGU9c3VibWl0XS5ibGFja3sKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogIGJvcmRlci1jb2xvcjogIzAwMDsKfQoKaW5wdXRbdHlwZT1idXR0b25dLmNlbnRlcnsKICBtYXJnaW4tcmlnaHQ6IGF1dG87CiAgbWFyZ2luLWxlZnQ6IGF1dG87CiAgYmFja2dyb3VuZC1jb2xvcjogIzAwMDsKICBib3JkZXItY29sb3I6ICMwMDA7Cn0KCi5wb2ludGVyIHsKICBjdXJzb3I6IHBvaW50ZXI7Cn0KCi5wb2ludGVyOmhvdmVyIHsKICBjb2xLine truncated
webUI["html/css/screen.css"] = "bmF2IGltZyB7CiAgZGlzcGxheTogYmxvY2s7CiAgbWF4LWhlaWdodDogMjBweDsKICBtYXgtd2lkdGg6IDIwcHg7CiAgZmxvYXQ6IGxlZnQ7Cn0KCm5hdiBwIHsKICB0ZXh0LWFsaWduOiBsZWZ0OwogIHBhZGRpbmc6IDBweCAzMHB4Owp9CgojbGF5b3V0IHsKICBkaXNwbGF5OiBibG9jazsKICBoZWlnaHQ6IDEwMCU7Cn0KCgoubGF5b3V0LWxlZnQgewogIGRpc3BsYXk6IGJsb2NrOwogIG1pbi13aWR0aDogMTUwcHg7CiAgbWF4LXdpZHRoOiAyMCU7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICBoZWlnaHQ6IGluaGVyaXQ7CiAgZmxvYXQ6IGxlZnQ7Cn0KCi5sYXlvdXQtcmlnaHQgewogIGRpc3BsYXk6IGJsb2NrOwogIGJhY2tncm91bmQtY29sb3I6ICM0NDQ7Cn0KCiNtZW51LXdyYXBwZXIgewogIGhlaWdodDogMTAwJTsKfQoKCiNsb2dvIHsKICBkaXNwbGF5OiBibG9jazsKICBtaW4td2lkdGg6IDE4MHB4OwogIHdpZHRoOiAxMDAlOwogIGhlaWdodDogMTAwcHg7CiAgYmFja2dyb3VuZDogdXJsKCIuLi9pbWcvbG9nb193XzYwMHgyMDAucG5nIik7CiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDsKICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7IAogIGJhY2tncm91bmQtc2l6ZTogMTAwJTsKfQoKCiNwYWdlIHsKICBtYXgtd2lkdGg6IDk1MHB4OwogIG1hcmdpbjogYXV0bzsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjNDQ0OwoKICAvKgogIGhlaWdodDogLXdlYmtpdC1jYWxjKDEwMCUgLSAxMzBweCk7CiAgaGVpZ2h0OiAtbW96LWNhbGMoMTAwJSAtIDEzMHB4KTsKICBoZWlnaHQ6IGNhbGMoMTAwJSAtIDEzMHB4KTsKICAqLwogIAogIG1pbi1oZWlnaHQ6IC13ZWJraXQtY2FsYygxMDAlIC0gMTIwcHgpOwogIG1pbi1oZWlnaHQ6IC1tb3otY2FsYygxMDAlIC0gMTIwcHgpOwogIG1pbi1oZWlnaHQ6IGNhbGMoMTAwJSAtIDEyMHB4KTsKICAKICAKICBib3gtc2hhZG93OiAwcHggNXB4IDVweCAjMjIyOwogIAp9CgojdWlTZXR0aW5nIHsKICBmbG9hdDogcmlnaHQ7CiAgbWFyZ2luLXJpZ2h0OiAyNXB4Owp9CgojYm94IGlucHV0W3R5cGU9dGV4dF0sICNib3ggaW5wdXRbdHlwZT1wYXNzd29yZF0gewogIHdpZHRoOiAtd2Via2l0LWNhbGMoMTAwJSAtIDIwcHgpOwogIHdpZHRoOiAtbW96LWNhbGMoMTAwJSAtIDIwcHgpOwogIHdpZHRoOiBjYWxjKDEwMCUgLSAyMHB4KTsKfQoKI2JveCBpbnB1dFt0eXBlPXN1Ym1pdF17CiAgbWFyZ2luOiA1MHB4IGF1dG87Cn0KCiNzZXR0aW5ncyB7CiAgZGlzcGxheTogYmxvY2s7CiAgcGFkZGluZzogMTBweCAxMHB4Owp9Cgojc2V0dGluZ3MgaDUgewogIG1hcmdpbjogNTBweCAwcHggMTBweCAwcHg7Cn0KCiNjb250ZW50LWludGVyYWN0aW9uIC5zZWFyY2ggewogIHdpZHRoOiAyMDBweDsKICBib3JkZXI6IDFweCBzb2xpZCAjMDAwOwogIHBhZGRpbmc6IDlweDsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzOwogIG1hcmdpbjogMTBweDsKICBmbG9hdDogcmlnaHQ7CiAgYm9yZGVyLXJhZGl1czogM3B4OwoKfQoKI215U3RyZWFtcyB7CiAgcG9zaXRpb246IGZpeGVkOwogIGJvdHRvbTogMHB4OwogIGJhY2tncm91bmQtY29sb3I6ICMxMTE7CiAgd2lkdGg6IDEwMCU7CiAgbWF4LXdpZHRoOiA5NTBweDsKCiAgLyoKICBtYXgtaGVpZ2h0OiAxMDBweDsKICAqLwogIG1hcmdpbi1ib3R0b206IDBweDsKfQoKI215U3RyZWFtcyBpbWcgewogIHdpZHRoOiA0JTsKICBwYWRkaW5nOiAycHggNXB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBmbG9hdDogcmlnaHQ7Cn0KCiNzZXR0aW5ncy1mb290ZXIgewoKfQoKCi8qIFdpemFyZCovCiNib3ggewogIGJhY2tncm91bmQtY29sb3I6ICM0NDQ7CiAgbWluLWhlaWdodDogNDAwcHg7CiAgCiAgZGlzcGxheTogZmxleDsKICBmbGV4LWRpcmVjdGlvbjogY29sdW1uOwogIGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjsKfQoKI2JveCBwewogIHBhZGRpbmc6IDEwcHggMHB4Owp9CgojYm94LWZvb3RlciB7CiAgbWFyZ2luLXRvcDogYXV0bzsKfQoKI2JveC1mb290ZXIgewogIG1hcmdpbjogYXV0bzsKICBwYWRkaW5nOiAxMHB4Owp9CgojaGVhZGxpbmUgewogIGJhY2tncm91bmQtY29sb3I6ICMyMjI7CiAgYm9yZGVyLWJvdHRvbTogc29saWQgMnB4ICMyMjI7CiAgdHJhbnNpdGlvbjogYWxsIDAuNXM7CiAgcGFkZGluZzogMTBweCAwcHg7CiAgZGlzcGxheTogYmxvY2s7Cn0KCiNjb250ZW50IHsKICBkaXNwbGF5OiBibG9jazsKICBvdmVyZmxvdzogYXV0bzsKICBwYWRkaW5nOiAxMHB4Owp9CgovKiAtLS0gKi8KCgojY2xpZW50SW5mbywgI2FjdGl2ZVN0cmVhbXMsICNpbmFjdGl2ZVN0cmVhbXMgewogIGZvbnQtZmFtaWx5OiBtb25vc3BhY2U7CiAgZGlzcGxheTogYmxvY2s7CiAgZm9udC1zaXplOiA5cHg7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICBjb2xvcjogIzAwRTZGRjsKICBib3JkZXItYm90dG9tOiBzb2xpZCAwcHg7OwogIHBhZGRpbmc6IDBweDsKICBsZXR0ZXItc3BhY2luZzogMXB4OwogIG92ZXJmbG93LXg6IGhpZGRlbjsKICBib3JkZXItc3BhY2luZzogNHB4IDRweDsKICBib3JkZXItYm90dG9tOiBzb2xpZCAxcHggIzQ0NDsKfQoKI215U3RyZWFtc0JveCB7CiAgcG9zaXRpb246IHJlbGF0aXZlOwogIHBhZGRpbmc6IDBweDsKICAvKmhlaWdodDogMTAwcHg7Ki8KICBtYXgtaGVpZ2h0OiAxNTBweDsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTExOwogIGNvbG9yOiB3aGl0ZTsKICBkaXNwbGF5OmZsZXg7CiAganVzdGlmeS1jb250ZW50OmNlbnRlcjsKICBhbGlnbi1pdGVtczpjZW50ZXI7Cn0KCiNvcGVuU3RyZWFtcyB7CiAgd2lkdGg6IDIwcHg7CiAgaGVpZ2h0OiAyMHB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBmbG9hdDogcmlnaHQ7CiAgcG9zaXRpb246IGFic29sdXRlOwogIHJpZ2h0OiAwcHg7CiAgYm90dG9tOiAwcHg7CiAgYmFja2dyb3VuZDogdXJsKCIuLi9pbWcvdG91Y2gucG5nIik7CiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTsKICAKICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBib3R0b20gcmlnaHQ7ICAKfQoKI2FsbFN0cmVhbXMgewogIHdpZHRoOiAxMDAlOwogIGhlaWdodDogMTAwJTsKICBwYWRkaW5nOiAycHg7ICAgICAKfQoKI2FjdGl2ZVN0cmVhbXMsICNpbmFjdGl2ZVN0cmVhbXMgewogIG92ZXJmbG93LXk6IHNjcm9sbDsKICB3aWR0aDogNTAlOwogIG1heC1oZWlnaHQ6IDEwMHB4OwogIGZsb2F0OiBsZWZ0Owp9CgojYWN0aXZlU3RyZWFtcyAudGRLZXksICNpbmFjdGl2ZVN0cmVhbXMgLnRkS2V5IHsKICB3aWR0aDogNzVweDsKfQoKCgoKI2luYWN0aXZlU3RyZWFtcyAudGRLZXkgewogIGNvbG9yOiByZWQ7Cn0KCiNjbGllbnRJbmZvIC50ZFZhbCwgI2xvZ0luZm8gLnRkVmFsLCAjYWN0aXZlU3RyZWFtcyAudGRWYWwsICNpbmFjdGl2ZVN0cmVhbXMgLnRkVmFsLCAjbWFwcGluZ0luZm8gLnRkVmFsewogIGNvbG9yOiAjYWFhOwogIHdoaXRlLXNwYWNlOiBpbmhlcml0Owp9CgojYm94LXdyYXBwZXIgewogIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICB3aWR0aDogMTAwJTsKICAKICBvdmVyZmxvdy15OiBzY3JvbGw7Cn0KCiNjb250ZW50X3RhYmxlLCAjbWFwcGluZy1kZXRhaWwtdGFibGUsICNjb250ZW50X3RhYmxlIHsKICBkaXNwbGF5OiB0YWJsZTsKICAKICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOwogIG92ZXJmbG93LXk6IHNjcm9sbDsKICB3aWR0aDogMTAwJTsKfQoKCiNjb250ZW50X3RhYmxlIC5jb250ZW50X3RhYLine 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/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/js/authentication.js"] = "ZnVuY3Rpb24gY3JlYXRlRmlyc3RBY2NvdW50KGVsbSkgewogIHZhciBlcnIgPSBmYWxzZTsKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoZWxtKTsKICBjb25zb2xlLmxvZyhkaXYpOwoKICB2YXIgZm9ybSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhdXRoZW50aWNhdGlvbicpOwogIAogIGNvbnN0IHVzZXJuYW1lICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCd1c2VybmFtZScpOwogIGNvbnN0IHBhc3N3b3JkICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwYXNzd29yZCcpOwogIGNvbnN0IGNvbmZpcm0gICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdjb25maXJtJyk7CgogIHZhciBpbnB1dHMgPSBkaXYuZ2V0RWxlbWVudHNCeVRhZ05hbWUoJ0lOUFVUJykKICBjb25zb2xlLmxvZyhjb25maXJtKTsKCiAgc3dpdGNoKGNvbmZpcm0pIHsKICAgIGNhc2UgbnVsbDogYnJlYWs7CiAgICAKICAgIGRlZmF1bHQ6IAogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGlucHV0cy5sZW5ndGg7IGkrKykgewogICAgICAgIGlmIChpbnB1dHNbaV0udmFsdWUubGVuZ3RoID09IDApIHsKICAgICAgICAgIGlucHV0c1tpXS5zdHlsZS5ib3JkZXJDb2xvciA9ICdyZWQnOwogICAgICAgICAgZXJyID0gdHJ1ZQogICAgICAgIH0KICAgICAgfQoKICAgICAgc3dpdGNoKGVycikgewogICAgICAgIGNhc2UgdHJ1ZTogcmV0dXJuOyBicmVhazsKICAgICAgICBjYXNlIGZhbHNlOiAKICAgICAgICAgIGlmIChwYXNzd29yZC52YWx1ZSAhPSBjb25maXJtLnZhbHVlKSB7CiAgICAgICAgICAgIGNvbmZpcm0uc3R5bGUuYm9yZGVyQ29sb3IgPSAncmVkJzsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgfQogICAgICAgICAgYnJlYWs7CiAgICAgIH0KICB9CgoKICAKCiAgZm9ybS5zdWJtaXQoKTsKICByZXR1cm47Cn0="
webUI["html/js/data.js"] = "ZnVuY3Rpb24gc2hvd0NvbmZpZyhvYmopIHsKICBjb25maWcgPSBvYmo7CiAgLy9zZXRNZW51SXRlbSgpOwogIGNyZWF0ZU1lbnUoKTsKICAvL2RvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJwYWdlIikuY2xhc3NOYW1lID0gIiI7Cn0KCnNob3dNeVN0cmVhbXMKCmZ1bmN0aW9uIHNob3dNeVN0cmVhbXMoYWxsU3RyZWFtc09iaikgewoKICB2YXIgc3RyZWFtVHlwZUtleXMgPSBnZXRPYmpLZXlzKGFsbFN0cmVhbXNPYmopCgogIGZvciAodmFyIHMgPSAwOyBzIDwgc3RyZWFtVHlwZUtleXMubGVuZ3RoOyBzKyspIHsKICAgIHZhciBzdHJlYW1UeXBlID0gc3RyZWFtVHlwZUtleXNbc107CiAgICB2YXIgb2JqID0gbmV3IE9iamVjdCgpOwogICAgb2JqID0gYWxsU3RyZWFtc09ialtzdHJlYW1UeXBlXTsKICAgIHN3aXRjaChzdHJlYW1UeXBlKSB7CiAgICAgIGNhc2UgImFjdGl2ZVN0cmVhbXMiOiBhY3RpdmVTdHJlYW1zICAgICA9IG9iajsgYnJlYWs7CiAgICB9CgogICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoc3RyZWFtVHlwZSkuaW5uZXJIVE1MID0gIiI7CgogICAgdmFyIHN0cmVhbXNPYmogICAgPSBuZXcgT2JqZWN0KCk7CiAgICB2YXIgc3RyZWFtc05hbWVzICA9IG5ldyBBcnJheSgpOwogICAgCiAgICB2YXIga2V5cyA9IGdldE9iaktleXMob2JqKQogICAgCiAgICAvLyBDcmVhdGUgT2JqZWN0IChzdHJlYW1zT2JqKSBmb3IgdGhlIHN0cmVhbXMgYW5kIHNvcnQgYnkgbmFtZSAoc3RyZWFtc05hbWVzKQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBrZXlzLmxlbmd0aDsgaSsrKSB7CiAgICAgIHZhciBuYW1lICAgID0gb2JqW2tleXNbaV1dWyJuYW1lIl07CiAgICAgIHZhciB0bXAgPSBuZXcgT2JqZWN0KCk7CiAgICAgIHZhciBzdHJlYW1LZXkgPSBnZXRPYmpLZXlzKG9ialtrZXlzW2ldXSk7CgogICAgICBmb3IgKHZhciBqID0gMDsgaiA8IHN0cmVhbUtleS5sZW5ndGg7IGorKykgewogICAgICAgIHRtcFtzdHJlYW1LZXlbal1dID0gb2JqW2tleXNbaV1dW3N0cmVhbUtleVtqXV07ICAgICAgCiAgICAgIH0KCiAgICAgIHN0cmVhbXNPYmpbbmFtZV0gPSB0bXA7CiAgICAgIHN0cmVhbXNOYW1lcy5wdXNoKG5hbWUpCiAgICB9CgogICAgc3RyZWFtc05hbWVzLnNvcnQoKTsKICAgIAogICAgLy8gQ3JlYXRlIFRhYmxlIGZvciBhY3RpdmVTdHJlYW1zCiAgICB2YXIgdGFibGUgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChzdHJlYW1UeXBlKTsKCiAgICBmb3IgKHZhciBpID0gMDsgaSA8IHN0cmVhbXNOYW1lcy5sZW5ndGg7IGkrKykgewogICAgICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgICAgIG5ld0VudHJ5WyJfZWxlbWVudCJdID0gIlRSIjsKICAgICAgdGFibGUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwogICAgICB2YXIgbGluZSA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICAgIHZhciB0bXAgPSBzdHJlYW1zT2JqW3N0cmVhbXNOYW1lc1tpXV0KICAgICAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKHRtcCkKCiAgICAgIHZhciBuZXdLZXkgPSBuZXcgT2JqZWN0KCkKICAgICAgbmV3S2V5WyJfZWxlbWVudCJdICA9ICJURCI7CiAgICAgIC8vbmV3S2V5WyJfdGV4dCJdICAgICA9IHN0cmVhbXNOYW1lc1tpXTsKICAgICAgc3dpdGNoKHN0cmVhbVR5cGUpIHsKICAgICAgICBjYXNlICJhY3RpdmVTdHJlYW1zIjogbmV3S2V5WyJfdGV4dCJdICAgICA9ICJDaGFubmVsICgrKToiOyBicmVhazsKICAgICAgICBjYXNlICJpbmFjdGl2ZVN0cmVhbXMiOiBuZXdLZXlbIl90ZXh0Il0gICA9ICJDaGFubmVsICgtKToiOyBicmVhazsKICAgICAgfQogICAgICAKICAgICAgbmV3S2V5WyJjbGFzcyJdICAgICA9ICJ0ZEtleSI7CiAgICAgIGNvbnNvbGUubG9nKCk7CgoKICAgICAgdmFyIG5ld1ZhbCA9IG5ldyBPYmplY3QoKQogICAgICBuZXdWYWxbIl9lbGVtZW50Il0gID0gIlREIjsKICAgICAgbmV3VmFsWyJfdGV4dCJdICAgICA9IHN0cmVhbXNOYW1lc1tpXTsKICAgICAgbmV3VmFsWyJjbGFzcyJdICAgICA9ICJ0ZFZhbCI7CiAgICAgIC8vbmV3VmFsWyJfdGV4dCJdICAgICA9IHZhbHVlOwogICAgICAKICAgICAgbGluZS5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0tleSkpOwogICAgICBsaW5lLmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VmFsKSk7CiAgICAgIAogICAgfQoKICB9CgogIHJldHVybgp9CgpmdW5jdGlvbiBzaG93QWN0aXZlU3RyZWFtcyhvYmopIHsKICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYWN0aXZlU3RyZWFtcyIpLmlubmVySFRNTCA9ICIiOwogIGFjdGl2ZVN0cmVhbXMgICAgID0gb2JqOwogIHZhciBzdHJlYW1zT2JqICAgID0gbmV3IE9iamVjdCgpOwogIHZhciBzdHJlYW1zTmFtZXMgID0gbmV3IEFycmF5KCk7CiAgCiAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKG9iaikKICAKICAvLyBDcmVhdGUgT2JqZWN0IChzdHJlYW1zT2JqKSBmb3IgdGhlIHN0cmVhbXMgYW5kIHNvcnQgYnkgbmFtZSAoc3RyZWFtc05hbWVzKQogIGZvciAodmFyIGkgPSAwOyBpIDwga2V5cy5sZW5ndGg7IGkrKykgewogICAgdmFyIG5hbWUgICAgPSBvYmpba2V5c1tpXV1bIm5hbWUiXTsKICAgIHZhciB0bXAgPSBuZXcgT2JqZWN0KCk7CiAgICB2YXIgc3RyZWFtS2V5ID0gZ2V0T2JqS2V5cyhvYmpba2V5c1tpXV0pOwoKICAgIGZvciAodmFyIGogPSAwOyBqIDwgc3RyZWFtS2V5Lmxlbmd0aDsgaisrKSB7CiAgICAgIHRtcFtzdHJlYW1LZXlbal1dID0gb2JqW2tleXNbaV1dW3N0cmVhbUtleVtqXV07ICAgICAgCiAgICB9CgogICAgc3RyZWFtc09ialtuYW1lXSA9IHRtcDsKICAgIHN0cmVhbXNOYW1lcy5wdXNoKG5hbWUpCiAgfQoKICBzdHJlYW1zTmFtZXMuc29ydCgpOwogIAogIC8vIENyZWF0ZSBUYWJsZSBmb3IgYWN0aXZlU3RyZWFtcwogIHZhciB0YWJsZSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJhY3RpdmVTdHJlYW1zIik7CgogIGZvciAodmFyIGkgPSAwOyBpIDwgc3RyZWFtc05hbWVzLmxlbmd0aDsgaSsrKSB7CiAgICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdFbnRyeVsiX2VsZW1lbnQiXSA9ICJUUiI7CiAgICB0YWJsZS5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0VudHJ5KSk7CiAgICB2YXIgbGluZSA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICB2YXIgdG1wID0gc3RyZWFtc09ialtzdHJlYW1zTmFtZXNbaV1dCiAgICB2YXIga2V5cyA9IGdldE9iaktleXModG1wKQoKICAgIHZhciBuZXdLZXkgPSBuZXcgT2JqZWN0KCkKICAgIG5ld0tleVsiX2VsZW1lbnQiXSAgPSAiVEQiOwogICAgLy9uZXdLZXlbIl90ZXh0Il0gICAgID0gc3RyZWFtc05hbWVzW2ldOwogICAgbmV3S2V5WyJfdGV4dCJdICAgICA9ICJDaGFubmVsOiI7CiAgICBuZXdLZXlbImNsYXNzIl0gICAgID0gInRkS2V5IjsKICAgIGNvbnNvbGUubG9nKCk7CgoKICAgIHZhciBuZXdWYWwgPSBuZXcgT2JqZWN0KCkKICAgIG5ld1ZhbFsiX2VsZW1lbnQiXSAgPSAiVEQiOwogICAgbmV3VmFsWyJfdGV4dCJdICAgICA9IHN0cmVhbXNOYW1lc1tpXTsKICAgIG5ld1ZhbFsiY2xhc3MiXSAgICAgPSAidGRWYWwiOwogICAgLy9uZXdWYWxbIl90ZXh0Il0gICAgID0gdmFsdWU7CiAgICAKICAgIGxpbmUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdLZXkpKTsKICAgIGxpbmUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdWYWwpKTsKICAgIAogIH0KCn0KCmZ1bmN0aW9uIHBhcnNlTG9ncyhvYmopIHsKILine truncated
webUI["html/js/users.js"] = "ZnVuY3Rpb24gb3BlblVzZXJzKGVsbSkgewogIGNvbG9tblNvcnQgPSAwOwoKICB2YXIgbmV3RGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CiAgCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJJTlBVVCI7CiAgbmV3RW50cnlbInR5cGUiXSA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gPSAiTmV3IjsKICBuZXdFbnRyeVsib25jbGljayJdID0gInVzZXJEZXRhaWwoMCkiOwogIG5ld0Rpdi5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0VudHJ5KSk7CgogIHZhciBkaXYgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgic2V0dGluZ3MiKTsKCiAgIC8vIEJ1aWxkIHRhYmxlCiAgdmFyIG5ld1RhYmxlID0gbmV3IE9iamVjdCgpOwogIG5ld1RhYmxlWyJfZWxlbWVudCJdICA9ICJUQUJMRSI7CiAgbmV3VGFibGVbImlkIl0gICAgICAgID0gImlkX21hcHBpbmciOwogIG5ld1RhYmxlWyJjbGFzcyJdICAgICA9ICJ0YWJsZS1tYXBwaW5nIjsKICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdUYWJsZSkpOwoKICBzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7IAogICAgY3JlYXRlVXNlcnNUYWJsZSgpOyAKICB9LCAxMCk7Cn0KCmZ1bmN0aW9uIGNyZWF0ZVVzZXJzVGFibGUoKSB7CiAgdmFyIHRhYmxlID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlkX21hcHBpbmciKTsKICB0YWJsZS5pbm5lckhUTUwgPSAiIjsKICB2YXIgbmV3VFIgPSBuZXcgT2JqZWN0KCk7CiAgbmV3VFJbIl9lbGVtZW50Il0gPSAiVFIiOwogIG5ld1RSWyJjbGFzcyJdICAgID0gInRhYmxlLW1hcHBpbmctaGVhZGVyIjsKICB0YWJsZS5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld1RSKSk7CgogIHZhciB0ciA9IHRhYmxlLmxhc3RDaGlsZDsKICB2YXIgdHJIZWFkbGluZXMgPSBuZXcgQXJyYXkoIlVzZXJuYW1lIiwgIlBhc3N3b3JkIiwgIldFQiIsICJQTVMiLCAiTTNVIiwgIlhNTCIsICJBUEkiKQoKICBmb3IgKHZhciBpID0gMDsgaSA8IHRySGVhZGxpbmVzLmxlbmd0aDsgaSsrKSB7CiAgICB2YXIgbmV3VEQgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJURCI7CiAgICBuZXdURFsiX3RleHQiXSAgICA9IHRySGVhZGxpbmVzW2ldOwogICAgdHIuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdURCkpOwogIH0KCgogIC8vIFNvcnQgdXNlcnMKICB2YXIgdXNlcklkcyA9IGdldE9iaktleXModXNlcnMpOwoKICB2YXIgdXNlck9iaiA9IG5ldyBPYmplY3QoKTsKCiAgZm9yICh2YXIgaSA9IDA7IGkgPCB1c2VySWRzLmxlbmd0aDsgaSsrKSB7CiAgICB2YXIgdXNlcm5hbWUgPSB1c2Vyc1t1c2VySWRzW2ldXVsiZGF0YSJdWyJ1c2VybmFtZSJdOwogICAgdXNlck9ialt1c2VybmFtZV0gPSB1c2VySWRzW2ldOwogIH0KCiAgdmFyIGFsbFVzZXJzID0gZ2V0T2JqS2V5cyh1c2VyT2JqKTsKICBhbGxVc2Vycy5zb3J0KCk7CiAgLy8gLS0KCiAgZm9yICh2YXIgaSA9IDA7IGkgPCBhbGxVc2Vycy5sZW5ndGg7IGkrKykgewogICAgdmFyIHRhYmxlICAgICA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJpZF9tYXBwaW5nIik7CiAgICB2YXIgdXNlcklEICAgID0gdXNlck9ialthbGxVc2Vyc1tpXV07CiAgICB2YXIgdXNlcm5hbWUgID0gYWxsVXNlcnNbaV07CiAgICB2YXIgaXRlbSAgICAgID0gdXNlcnNbdXNlcklEXVsiZGF0YSJdOwoKICAgIC8vIENyZWF0ZSBUUgogICAgdmFyIG5ld1RSID0gbmV3IE9iamVjdCgpOwogICAgbmV3VFJbIl9lbGVtZW50Il0gICAgICAgPSAiVFIiOwogICAgbmV3VFJbImNsYXNzIl0gICAgICAgICAgPSAiIjsKICAgIG5ld1RSWyJpZCJdICAgICAgICAgICAgID0gdXNlcklEOwogICAgbmV3VFJbIm9uY2xpY2siXSAgICAgICAgPSAnamF2YXNjcmlwdDogdXNlckRldGFpbCgiJyArIHVzZXJJRCArICciKTsnOwogICAgdGFibGUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdUUikpOwoKICAgIHZhciB0ciA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICAvLyBDcmVhdGUgdXNlcm5hbWUgVEQKICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgIG5ld1REWyJfZWxlbWVudCJdID0gIlAiOwogICAgbmV3VERbIl90ZXh0Il0gICAgPSB1c2VybmFtZTsKICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIHBhc3N3b3JkIFRECiAgICB2YXIgbmV3VEQgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgIG5ld1REWyJfdGV4dCJdICAgID0gIi4uLi4uIjsKICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIHdlYiBhY2Nlc3MKICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgIG5ld1REWyJfZWxlbWVudCJdID0gIlAiOwogICAgc3dpdGNoKGl0ZW1bImF1dGhlbnRpY2F0aW9uLndlYiJdKXsKICAgICAgY2FzZSB0cnVlOiBuZXdURFsiX3RleHQiXSAgICA9ICLinJMiOyBicmVhazsKICAgICAgZGVmYXVsdDogICBuZXdURFsiX3RleHQiXSAgICA9ICItIjsgYnJlYWs7CiAgICB9CiAgICBjcmVhdGVOZXdURChuZXdURCwgdHIpOwoKICAgIC8vIENyZWF0ZSBQTVMgYWNjZXNzCiAgICB2YXIgbmV3VEQgPSBuZXcgT2JqZWN0KCk7CiAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgIHN3aXRjaChpdGVtWyJhdXRoZW50aWNhdGlvbi5wbXMiXSl7CiAgICAgIGNhc2UgdHJ1ZTogbmV3VERbIl90ZXh0Il0gICAgPSAi4pyTIjsgYnJlYWs7CiAgICAgIGRlZmF1bHQ6ICAgbmV3VERbIl90ZXh0Il0gICAgPSAiLSI7IGJyZWFrOwogICAgfQogICAgY3JlYXRlTmV3VEQobmV3VEQsIHRyKTsKCiAgICAvLyBDcmVhdGUgTTNVIGFjY2VzcwogICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgbmV3VERbIl9lbGVtZW50Il0gPSAiUCI7CiAgICBzd2l0Y2goaXRlbVsiYXV0aGVudGljYXRpb24ubTN1Il0pewogICAgICBjYXNlIHRydWU6IG5ld1REWyJfdGV4dCJdICAgID0gIuKckyI7IGJyZWFrOwogICAgICBkZWZhdWx0OiAgIG5ld1REWyJfdGV4dCJdICAgID0gIi0iOyBicmVhazsKICAgIH0KICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIFhNTFRWIGFjY2VzcwogICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgbmV3VERbIl9lbGVtZW50Il0gPSAiUCI7CiAgICBzd2l0Y2goaXRlbVsiYXV0aGVudGljYXRpb24ueG1sIl0pewogICAgICBjYXNlIHRydWU6IG5ld1REWyJfdGV4dCJdICAgID0gIuKckyI7IGJyZWFrOwogICAgICBkZWZhdWx0OiAgIG5ld1REWyJfdGV4dCJdICAgID0gIi0iOyBicmVhazsKICAgIH0KICAgIGNyZWF0ZU5ld1REKG5ld1RELCB0cik7CgogICAgLy8gQ3JlYXRlIEFQSSBhY2Nlc3MKICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgIG5ld1REWyJfZWxlbWVudCJdID0gIlAiOwoKICAgIHN3aXRjaChpdGVtWyJhdXRoZW50aWNhdGlvbi5hcGkiXSl7CiAgICAgIGNhc2UgdHJ1ZTogbmV3VERbIl90ZXh0Il0gICAgPSAi4pyTIjsgYnJlYWs7CiAgICAgIGRlZmF1bHQ6ICAgbmV3VERbIl90ZXh0Il0gICAgPSAiLSI7Line truncated
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/css/.DS_Store"] = "AAAAAUJ1ZDEAABAAAAAIAAAAEAAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAABAAAAQAAAAAEAAACAAAAAAQAAAQAAAAABAAACAAAAAAEAAAQAAAAAAAAAAAEAABAAAAAAAQAAIAAAAAABAABAAAAAAAEAAIAAAAAAAQABAAAAAAABAAIAAAAAAAEABAAAAAAAAQAIAAAAAAABABAAAAAAAAEAIAAAAAAAAQBAAAAAAAABAIAAAAAAAAEBAAAAAAAAAQIAAAAAAAABBAAAAAAAAAEIAAAAAAAAARAAAAAAAAABIAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
webUI["html/css/screen2.css"] = "aDEgewogIGNvbG9yOiBncmVlbjsKfQ=="
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/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/js/menu_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBNYWluTWVudSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIE1haW5NZW51KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJtYWluLW1lbnUiOwogICAgICAgIHRoaXMuSFRNTFRhZyA9ICJMSSI7CiAgICAgICAgdGhpcy5JbWFnZVBhdGggPSAiaW1nLyI7CiAgICB9CiAgICBNYWluTWVudS5wcm90b3R5cGUuY3JlYXRlSU1HID0gZnVuY3Rpb24gKHNyYykgewogICAgICAgIHZhciBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiSU1HIik7CiAgICAgICAgZWxlbWVudC5zZXRBdHRyaWJ1dGUoInNyYyIsIHRoaXMuSW1hZ2VQYXRoICsgc3JjKTsKICAgICAgICByZXR1cm4gZWxlbWVudDsKICAgIH07CiAgICBNYWluTWVudS5wcm90b3R5cGUuY3JlYXRlVmFsdWUgPSBmdW5jdGlvbiAodmFsdWUpIHsKICAgICAgICB2YXIgZWxlbWVudCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlAiKTsKICAgICAgICBlbGVtZW50LmlubmVySFRNTCA9IHZhbHVlOwogICAgICAgIHJldHVybiBlbGVtZW50OwogICAgfTsKICAgIHJldHVybiBNYWluTWVudTsKfSgpKTsKdmFyIE1haW5NZW51SXRlbSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uIChfc3VwZXIpIHsKICAgIF9fZXh0ZW5kcyhNYWluTWVudUl0ZW0sIF9zdXBlcik7CiAgICBmdW5jdGlvbiBNYWluTWVudUl0ZW0obWVudUtleSwgdmFsdWUsIGltYWdlLCBoZWFkbGluZSkgewogICAgICAgIHZhciBfdGhpcyA9IF9zdXBlci5jYWxsKHRoaXMpIHx8IHRoaXM7CiAgICAgICAgX3RoaXMubWVudUtleSA9IG1lbnVLZXk7CiAgICAgICAgX3RoaXMudmFsdWUgPSB2YWx1ZTsKICAgICAgICBfdGhpcy5pbWdTcmMgPSBpbWFnZTsKICAgICAgICBfdGhpcy5oZWFkbGluZSA9IGhlYWRsaW5lOwogICAgICAgIHJldHVybiBfdGhpczsKICAgIH0KICAgIE1haW5NZW51SXRlbS5wcm90b3R5cGUuY3JlYXRlSXRlbSA9IGZ1bmN0aW9uICgpIHsKICAgICAgICB2YXIgaXRlbSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIkxJIik7CiAgICAgICAgaXRlbS5zZXRBdHRyaWJ1dGUoIm9uY2xpY2siLCAiamF2YXNjcmlwdDogb3BlblRoaXNNZW51KHRoaXMpIik7CiAgICAgICAgaXRlbS5zZXRBdHRyaWJ1dGUoImlkIiwgdGhpcy5pZCk7CiAgICAgICAgdmFyIGltZyA9IHRoaXMuY3JlYXRlSU1HKHRoaXMuaW1nU3JjKTsKICAgICAgICB2YXIgdmFsdWUgPSB0aGlzLmNyZWF0ZVZhbHVlKHRoaXMudmFsdWUpOwogICAgICAgIGl0ZW0uYXBwZW5kQ2hpbGQoaW1nKTsKICAgICAgICBpdGVtLmFwcGVuZENoaWxkKHZhbHVlKTsKICAgICAgICB2YXIgZG9jID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQodGhpcy5Eb2N1bWVudElEKTsKICAgICAgICBkb2MuYXBwZW5kQ2hpbGQoaXRlbSk7CiAgICAgICAgc3dpdGNoICh0aGlzLm1lbnVLZXkpIHsKICAgICAgICAgICAgY2FzZSAicGxheWxpc3QiOgogICAgICAgICAgICAgICAgdGhpcy50YWJsZUhlYWRlciA9IFsie3sucGxheWxpc3QudGFibGUucGxheWxpc3R9fSIsICJ7ey5wbGF5bGlzdC50YWJsZS50dW5lcn19IiwgInt7LnBsYXlsaXN0LnRhYmxlLmxhc3RVcGRhdGV9fSIsICJ7ey5wbGF5bGlzdC50YWJsZS5hdmFpbGFiaWxpdHl9fSAlIiwgInt7LnBsYXlsaXN0LnRhYmxlLnR5cGV9fSIsICJ7ey5wbGF5bGlzdC50YWJsZS5zdHJlYW1zfX0iLCAie3sucGxheWxpc3QudGFibGUuZ3JvdXBUaXRsZX19ICUiLCAie3sucGxheWxpc3QudGFibGUudHZnSUR9fSAlIiwgInt7LnBsYXlsaXN0LnRhYmxlLnVuaXF1ZUlEfX0gJSJdOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgInhtbHR2IjoKICAgICAgICAgICAgICAgIHRoaXMudGFibGVIZWFkZXIgPSBbInt7LnhtbHR2LnRhYmxlLmd1aWRlfX0iLCAie3sueG1sdHYudGFibGUubGFzdFVwZGF0ZX19IiwgInt7LnhtbHR2LnRhYmxlLmF2YWlsYWJpbGl0eX19ICUiLCAie3sueG1sdHYudGFibGUuY2hhbm5lbHN9fSIsICJ7ey54bWx0di50YWJsZS5wcm9ncmFtc319Il07CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSAiZmlsdGVyIjoKICAgICAgICAgICAgICAgIHRoaXMudGFibGVIZWFkZXIgPSBbInt7LmZpbHRlci50YWJsZS5uYW1lfX0iLCAie3suZmlsdGVyLnRhYmxlLnR5cGV9fSIsICJ7ey5maWx0ZXIudGFibGUuZmlsdGVyfX0iXTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJ1c2VycyI6CiAgICAgICAgICAgICAgICB0aGlzLnRhYmxlSGVhZGVyID0gWyJ7ey51c2Vycy50YWJsZS51c2VybmFtZX19IiwgInt7LnVzZXJzLnRhYmxlLnBhc3N3b3JkfX0iLCAie3sudXNlcnMudGFibGUud2VifX0iLCAie3sudXNlcnMudGFibGUucG1zfX0iLCAie3sudXNlcnMudGFibGUubTN1fX0iLCAie3sudXNlcnMudGFibGUueG1sfX0iLCAie3sudXNlcnMudGFibGUuYXBpfX0iXTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJtYXBwaW5nIjoKICAgICAgICAgICAgICAgIHRoaXMudGFibGVIZWFkZXIgPSBbIkJVTEsiLCAie3subWFwcGluZy50YWJsZS5jaE5vfX0iLCAie3subWFwcGluZy50YWJsZS5sb2dvfX0iLCAie3subWFwcGluZy50YWJsZS5jaGFubmVsTmFtZX19IiwgInt7Lm1hcHBpbmcudGFibGUucGxheWxpc3R9fSIsICJ7ey5tYXBwaW5nLnRhYmxlLmdyb3VwVGl0bGV9fSIsICJ7ey5tYXBwaW5nLnRhYmxlLnhtbHR2RmlsZX19IiwgInt7Lm1hcHBpbmcudGFibGUueG1sdHZJRH19Il07CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICB9CiAgICAgICAgLy9jb25zb2xlLmxvZyh0aGlzLm1lbnVLZXksIHRoaXMudGFibGVIZWFkZXIpOwogICAgfTsKICAgIHJldHVybiBNYWluTWVudUl0ZW07Cn0oTWFpbk1lbnUpKTsKdmFyIENvbnRlbnQgPSAvKiogQGNsYXNzICovIChmdW5jdGlvbiAoKSB7CiAgICBmdW5jdGlvbiBDb250ZW50KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJjb250ZW50IjsKICAgICAgICB0aGlzLlRhYmxlSUQgPSAiY29udGVudF90YWJsZSI7CiAgICAgICAgdGhpcy5oZWFkZXJDbGFzcyA9ICJjb250ZW50X3RhYmxlX2hlYWRlciI7CiAgICAgICAgdGhpcy5pbnRlcmFjdGlvbklEID0gImNvbnRlbnQtaW50ZXJhY3Line truncated
webUI["html/login.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvbmV0d29ya190cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvYXV0aGVudGljYXRpb25fdHMuanMiPjwvc2NyaXB0PgogIDwvaGVhZD4KCiAgICA8Ym9keT4KICAgICAgICAgIAogICAgICA8ZGl2IGlkPSJoZWFkZXIiIGNsYXNzPSJpbWdDZW50ZXIiPjwvZGl2PgogICAgICA8ZGl2IGlkPSJib3giPgogICAgICAgIDxkaXYgaWQ9ImhlYWRsaW5lIj4KICAgICAgICAgIDxoMSBpZD0iaGVhZC10ZXh0IiBjbGFzcz0iY2VudGVyIj57ey5sb2dpbi5oZWFkbGluZX19PC9oMT4KICAgICAgICA8L2Rpdj4KICAgICAgICA8cCBpZD0iZXJyIiBjbGFzcz0iZXJyb3JNc2cgY2VudGVyIj57ey5hdXRoZW50aWNhdGlvbkVycn19PC9wPiAgIAogICAgICAgIDxkaXYgaWQ9ImNvbnRlbnQiPgogICAgICAgICAgICA8Zm9ybSBpZD0iYXV0aGVudGljYXRpb24iIGFjdGlvbj0iL3dlYi8iIG1ldGhvZD0icG9zdCI+CiAgICAgICAgICAgICAgPGg1Pnt7LmxvZ2luLnVzZXJuYW1lLnRpdGxlfX06PC9oNT4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9InVzZXJuYW1lIiB0eXBlPSJ0ZXh0IiBuYW1lPSJ1c2VybmFtZSIgcGxhY2Vob2xkZXI9IlVzZXJuYW1lIiB2YWx1ZT0iIj4KICAgICAgICAgICAgICA8aDU+e3subG9naW4ucGFzc3dvcmQudGl0bGV9fTo8L2g1PgogICAgICAgICAgICAgIDxpbnB1dCBpZD0icGFzc3dvcmQiIHR5cGU9InBhc3N3b3JkIiBuYW1lPSJwYXNzd29yZCIgcGxhY2Vob2xkZXI9IlBhc3N3b3JkIiB2YWx1ZT0iIj4KICAgICAgICAgICAgPC9mb3JtPgogICAgICAgIDwvZGl2PgogICAgICAgIDxkaXYgaWQ9ImJveC1mb290ZXIiPgogICAgICAgICAgICA8aW5wdXQgaWQ9InN1Ym1pdCIgY2xhc3M9IiIgdHlwZT0iYnV0dG9uIiB2YWx1ZT0ie3suYnV0dG9uLmxvZ2lufX0iIG9uY2xpY2s9ImphdmFzY3JpcHQ6IGxvZ2luKCk7Ij4KICAgICAgICAgIDwvZGl2PgogICAgICAgIAogICAgICA8L2Rpdj4KICAgIDwvYm9keT4KPC9odG1sPg=="
webUI["html/maintenance.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgPC9oZWFkPgoKICAgIDxib2R5PgogICAgICAgICAgCiAgICAgIDxkaXYgaWQ9ImhlYWRlciIgY2xhc3M9ImltZ0NlbnRlciI+PC9kaXY+CiAgICAgIDxkaXYgaWQ9ImJveCI+CiAgICAgICAgPGRpdiBpZD0iaGVhZGxpbmUiPgogICAgICAgICAgPGgxIGlkPSJoZWFkLXRleHQiIGNsYXNzPSJjZW50ZXIiPk1haW50ZW5hbmNlPC9oMT4gICAgICAKICAgICAgICA8L2Rpdj4gIAoKICAgICAgICA8ZGl2IGlkPSJjb250ZW50Ij4KICAgICAgICAgIHhUZVZlIGlzIHVwZGF0aW5nIHRoZSBkYXRhYmFzZSwgcGxlYXNlIHRyeSBhZ2FpbiBsYXRlci4KICAgICAgICA8L2Rpdj4KCiAgICAgICAgPGRpdiBpZD0iYm94LWZvb3RlciI+CiAgICAgICAgICAKICAgICAgICA8L2Rpdj4KICAgICAgICAKICAgICAgPC9kaXY+CiAgICA8L2JvZHk+CjwvaHRtbD4="
webUI["html/.DS_Store"] = "AAAAAUJ1ZDEAACAAAAAIAAAAIAAAABAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAA0AAAABAAAQAHNwYmxvYgAAAMpicAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAABAAAAQAAAAAEAAACAAAAAAQAAAQAAAAABAAACAAAAAAEAAAQAAAAAAAAAAAEAABAAAAAAAQAAIAAAAAABAABAAAAAAAEAAIAAAAAAAQABAAAAAAABAAIAAAAAAAEABAAAAAAAAQAIAAAAAAABABAAAAAAAAEAIAAAAAAAAQBAAAAAAAABAIAAAAAAAAEBAAAAAAAAAQIAAAAAAAABBAAAAAAAAAEIAAAAAAAAARAAAAAAAAABIAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
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/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/configuaration.js"] = "dmFyIGNvbmZpZ01lbnUgPSBuZXcgT2JqZWN0KCk7CnZhciB3aXphcmQgPSBuZXcgQXJyYXkoImtleSIsICJ0dW5lciIsICJlcGdTb3VyY2UiLCAibTN1IiwgImNvbXBsZXRlIik7CnZhciBhY3RpdmVXaXphcmQ7CnZhciBkdnJJUAoKdmFyIGNvbmZpZ01lbnVfdHVuZXIgPSBuZXcgT2JqZWN0KCk7CmNvbmZpZ01lbnVfdHVuZXJbIl9lbGVtZW50Il0gICAgICA9ICJTRUxFQ1QiOwpjb25maWdNZW51X3R1bmVyWyJfbWVudVR5cGUiXSAgICAgPSAic2luZ2xlSW5wdXQiOwpjb25maWdNZW51X3R1bmVyWyJfY29uZmlnS2V5Il0gICAgPSAidHVuZXIiOwpjb25maWdNZW51X3R1bmVyWyJfbGFiZWwiXSAgICAgICAgPSAiQXZhaWxhYmxlIHR1bmVycyI7CmNvbmZpZ01lbnVfdHVuZXJbIm5hbWUiXSAgICAgICAgICA9ICJ0dW5lciI7CmNvbmZpZ01lbnVfdHVuZXJbImlkIl0gICAgICAgICAgICA9ICJUdW5lciI7CmNvbmZpZ01lbnVfdHVuZXJbInBsYWNlaG9sZGVyIl0gICA9ICJUdW5lciI7CmNvbmZpZ01lbnVfdHVuZXJbIl91c2FnZSJdICAgICAgICA9ICJUaGlzIHNldHRpbmcgaXMgb25seSB1c2VkIGJ5IFBsZXggYW5kIEVtYnkuPGJyPlRoZSBudW1iZXIgb2YgY29uY3VycmVudCBzdHJlYW1zIGFsbG93ZWQgYnkgdGhlIElQVFYgcHJvdmlkZXIuIgoKCnZhciBvcHRpb25WYWx1ZXMgPSBuZXcgQXJyYXkoKTsKZm9yICh2YXIgaSA9IDE7IGkgPD0gMTAwOyBpKyspIHsKICBvcHRpb25WYWx1ZXMucHVzaChpKQp9CmNvbmZpZ01lbnVfdHVuZXJbIl9vcHRpb25WYWx1ZXMiXSA9IG9wdGlvblZhbHVlczsKCnZhciBjb25maWdNZW51X2VwZyA9IG5ldyBPYmplY3QoKTsKY29uZmlnTWVudV9lcGdbIl9lbGVtZW50Il0gICAgICA9ICJTRUxFQ1QiOwpjb25maWdNZW51X2VwZ1siX21lbnVUeXBlIl0gICAgID0gInNpbmdsZUlucHV0IjsKY29uZmlnTWVudV9lcGdbIl9jb25maWdLZXkiXSAgICA9ICJlcGdTb3VyY2UiOwpjb25maWdNZW51X2VwZ1siX2xhYmVsIl0gICAgICAgID0gIlNlbGVjdGlvbiBvZiB0aGUgRVBHIHNvdXJjZSI7CmNvbmZpZ01lbnVfZXBnWyJuYW1lIl0gICAgICAgICAgPSAiZXBnU291cmNlIjsKY29uZmlnTWVudV9lcGdbImlkIl0gICAgICAgICAgICA9ICJFUEcgc291cmNlIjsKY29uZmlnTWVudV9lcGdbInBsYWNlaG9sZGVyIl0gICA9ICJFUEcgc291cmNlIjsKY29uZmlnTWVudV9lcGdbIl9vcHRpb25WYWx1ZXMiXSA9IG5ldyBBcnJheSgiUE1TIiwgIlhFUEciKTsKY29uZmlnTWVudV9lcGdbIl91c2FnZSJdICAgICAgICA9ICJQTVM6ICAgVXNlIEVQRyBkYXRhIGZyb20gUGxleCBvciBFbWJ5PGJyPlhFUEc6ICBVc2Ugb2YgZXh0ZXJuYWwgRVBHIGRhdGEgKFhNTFRWKTxicj4gICAgICAgU2V2ZXJhbCBYTUxUViBzb3VyY2VzIHBvc3NpYmxlPGJyPiAgICAgICBBbGxvd3MgZWRpdGluZyBhbmQgb3JkZXIgY2hhbm5lbHM8YnI+ICAgICAgIE0zVSAvIFhNTFRWIGV4cG9ydCAoSFRUUCBsaW5rIGZvciBJUFRWIGFwcHMpIgoKdmFyIGNvbmZpZ01lbnVfbTN1ID0gbmV3IE9iamVjdCgpOwpjb25maWdNZW51X20zdVsiX2VsZW1lbnQiXSAgICAgICAgPSAiSU5QVVQiOwpjb25maWdNZW51X20zdVsiX21lbnVUeXBlIl0gICAgICAgPSAiaW5wdXRBcnJheSI7CmNvbmZpZ01lbnVfbTN1WyJfY29uZmlnS2V5Il0gICAgICA9ICJmaWxlIjsKY29uZmlnTWVudV9tM3VbIl9sYWJlbCJdICAgICAgICAgID0gIk0zVSBGaWxlOiBsb2NhbCBvciByZW1vdGUiOwpjb25maWdNZW51X20zdVsibmFtZSJdICAgICAgICAgICAgPSAiZmlsZSI7CmNvbmZpZ01lbnVfbTN1WyJpZCJdICAgICAgICAgICAgICA9ICJtM3UiOwpjb25maWdNZW51X20zdVsidHlwZSJdICAgICAgICAgICAgPSAidGV4dCI7CmNvbmZpZ01lbnVfbTN1WyJwbGFjZWhvbGRlciJdICAgICA9ICJNM1UgRmlsZSI7CmNvbmZpZ01lbnVfbTN1WyJfdXNhZ2UiXSAgICAgICAgICA9ICJSZW1vdGUgcGxheWxpc3Q6IGh0dHA6Ly95b3VyLnByb3ZpZGVyLmNvbS9maWxlLm0zdTxicj5Mb2NhbCAgcGxheWxpc3Q6IC9wYXRoL3RvL2ZpbGUubTN1IgoKCmNvbmZpZ01lbnVfbTN1WyJ2YWx1ZSJdICAgICAgICAgICA9ICJodHRwOi8vd2Vic3J2LmxvY2FsOjgwODAva2FiZWwubTN1IjsKCnZhciBjb25maWdNZW51X2NvbXBsZXRlID0gbmV3IE9iamVjdCgpOwpjb25maWdNZW51X2NvbXBsZXRlWyJfZWxlbWVudCJdICAgICAgICA9ICJIMiI7CmNvbmZpZ01lbnVfY29tcGxldGVbIl9tZW51VHlwZSJdICAgICAgID0gImlucHV0QXJyYXkiOwpjb25maWdNZW51X2NvbXBsZXRlWyJfY29uZmlnS2V5Il0gICAgICA9ICJmaWxlIjsKY29uZmlnTWVudV9jb21wbGV0ZVsiX3RleHQiXSAgICAgICAgICAgPSAieFRlVmUgd2FzIHN1Y2Nlc3NmdWxseSBzZXQgdXAiOwpjb25maWdNZW51X2NvbXBsZXRlWyJuYW1lIl0gICAgICAgICAgICA9ICJjb21wbGV0ZSI7CmNvbmZpZ01lbnVfY29tcGxldGVbImlkIl0gICAgICAgICAgICAgID0gImNvbXBsZXRlIjsKY29uZmlnTWVudV9jb21wbGV0ZVsidHlwZSJdICAgICAgICAgICAgPSAidGV4dCI7CmNvbmZpZ01lbnVfY29tcGxldGVbImNsYXNzIl0gICAgICAgICAgID0gImNlbnRlciI7Cgpjb25maWdNZW51WyJ0dW5lciJdICAgICA9IGNvbmZpZ01lbnVfdHVuZXI7CmNvbmZpZ01lbnVbImVwZ1NvdXJjZSJdID0gY29uZmlnTWVudV9lcGc7CmNvbmZpZ01lbnVbIm0zdSJdICAgICAgID0gY29uZmlnTWVudV9tM3U7CmNvbmZpZ01lbnVbImNvbXBsZXRlIl0gID0gY29uZmlnTWVudV9jb21wbGV0ZTsKCmZ1bmN0aW9uIHJlYWR5Rm9yQ29uZmlndXJhdGlvbigpIHsKICB2YXIgZGF0YSA9IG5ldyBPYmplY3QoKTsKICBkYXRhWyJjbWQiXSA9ICJnZXRTZXJ2ZXJDb25maWciOwogIHhUZVZlKGRhdGEpOwogIHNob3dMb2FkaW5nU2NyZWVuKGZhbHNlKTsKfQoKZnVuY3Rpb24gY3JlYXRlQ29uZmlndXJhdGlvbihlbG0pIHsKCiAgYWN0aXZlV2l6YXJkID0gZWxtOwogIHZhciBpdGVtICA9IGNvbmZpZ01lbnVbZWxtXTsKCiAgdmFyIGRpdiAgID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImNvbnRlbnQiKTsKICBkaXYuaW5uZXJIVE1MID0gIiI7CiAgZGl2LnNldEF0dHJpYnV0ZSgiZGF0YS1jb25maWdLZXkiLCBpdGVtWyJfY29uZmlnS2V5Il0pOwogIGRpdi5zZXRBdHRyaWJ1dGUoImRhdGEtbWVudVR5cGUiLCBpdGVtWyJfbWVudVR5cGUiXSk7CiAgCiAgc3dpdGNoKGl0ZW0uaGFzT3duUHJvcGVydHkoIl9sYWJlbCIpKSB7CiAgICBjYXNlIHRydWU6CiAgICAgIHZhciBuZXdJdGVtID0gbmV3IE9iamVjdCgpOwogICAgICBuZXdJdGVtWyJfZWxlbWVudCJdID0gIkxBQkVMIjsKICAgICAgbmV3SXRlbVsiX3RleHQiXSAgICA9IGl0ZW1bIl9sYWJlbCJdOyAKICAgICAgbmV3SXRlbVsiZm9yIl0gICAgICA9IGl0ZW1bImlkIl07CiAgICAgIGRpdi5hcHBlbmRDaGlsZChjcmVhdGVFbGVtZW50KG5ld0l0ZW0pKTsKICAgICAgYnJlYWsKICB9CgogIHN3aXRjaChpdGVtWyJfZWxlbWVudCJdKSB7CiAgICBjYXNlICJTRUxFQ1QiOgogICAgICBkaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChpdGVtKSk7CiAgICAgIHZhciBzZWxlY3RFbGVtZW50ID0gZGl2LmdldEVsZW1lbnRzQnlUYWdOYW1lKCJTRUxFQ1QiKVswXTsKICAgICAgdmFyIHZhbHVlcyA9IGl0ZW1bIl9vcHRpb25WYWx1ZXMiXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCB2YWx1ZXMubGVuZ3RoOyBpKyspIHsKICAgICAgICBLine truncated
webUI["html/js/files.js"] = "ZnVuY3Rpb24gb3BlbkZpbGVzKGVsbSwgZmlsZVR5cGUpIHsKICAvL2RvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJzZXR0aW5ncyIpLmlubmVySFRNTCA9ICJUZXN0IjsKICAKICBjb2x1bW5Ub1NvcnQgPSAwOyAKICB2YXIgbmV3RGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CiAgCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJJTlBVVCI7CiAgbmV3RW50cnlbInR5cGUiXSA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gPSAiTmV3IjsKICBuZXdFbnRyeVsib25jbGljayJdID0gJ2ZpbGVEZXRhaWwoIi0iLCAiJyArIGZpbGVUeXBlICsgJyIpJzsKICBuZXdEaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbImNsYXNzIl0gPSAiYnV0dG9uIjsKICBuZXdFbnRyeVsidmFsdWUiXSA9ICJVcGRhdGUiOwogIG5ld0VudHJ5WyJvbmNsaWNrIl0gPSAiZmlsZURldGFpbCgwKSI7CiAgLy9uZXdEaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CgogICAvLyBCdWlsZCB0YWJsZQogIHZhciBuZXdUYWJsZSA9IG5ldyBPYmplY3QoKTsKICBuZXdUYWJsZVsiX2VsZW1lbnQiXSAgPSAiVEFCTEUiOwogIG5ld1RhYmxlWyJpZCJdICAgICAgICA9ICJpZF9tYXBwaW5nIjsKICBuZXdUYWJsZVsiY2xhc3MiXSAgICAgPSAidGFibGUtbWFwcGluZyI7CiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VGFibGUpKTsKCiAgc2V0VGltZW91dChmdW5jdGlvbigpeyAKICAgIGNyZWF0ZUZpbGVzVGFibGUoZmlsZVR5cGUpOyAKICB9LCAxMCk7Cgp9CgpmdW5jdGlvbiBjcmVhdGVGaWxlc1RhYmxlKGZpbGVUeXBlKSB7CiAgdmFyIHRhYmxlID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlkX21hcHBpbmciKTsKICB2YXIgYXZhaWxhYmxlRmlsZVR5cGVzID0gbmV3IEFycmF5KCk7CiAgCiAgdGFibGUuaW5uZXJIVE1MID0gIiI7CiAgdmFyIG5ld1RSID0gbmV3IE9iamVjdCgpOwogIG5ld1RSWyJfZWxlbWVudCJdID0gIlRSIjsKICBuZXdUUlsiY2xhc3MiXSAgICA9ICJ0YWJsZS1tYXBwaW5nLWhlYWRlciI7CiAgdGFibGUuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdUUikpOwoKICB2YXIgdHIgPSB0YWJsZS5sYXN0Q2hpbGQ7CgogIHN3aXRjaChmaWxlVHlwZSkgewogICAgY2FzZSAieG1sdHYiOiAKICAgICAgYXZhaWxhYmxlRmlsZVR5cGVzID0gbmV3IEFycmF5KCJ4bWx0diIpOyAKICAgICAgdmFyIHRySGVhZGxpbmVzID0gbmV3IEFycmF5KCJHdWlkZSIsICJMYXN0IFVwZGF0ZSIsICJBdmFpbGFiaWxpdHkgJSIsICJDaGFubmVscyIsICJQcm9ncmFtcyIpCiAgICAgIHZhciBjb21wYXRpYmlsaXR5S2V5cyA9IG5ldyBBcnJheSgieG1sdHYuY2hhbm5lbHMiLCAieG1sdHYucHJvZ3JhbXMiKQogICAgICBicmVhazsKCiAgICBjYXNlICJtM3UiOgogICAgICBhdmFpbGFibGVGaWxlVHlwZXMgPSBuZXcgQXJyYXkoIm0zdSIsICJoZGhyIik7IAogICAgICB2YXIgdHJIZWFkbGluZXMgPSBuZXcgQXJyYXkoIlBsYXlsaXN0IiwgIkxhc3QgVXBkYXRlIiwgIkF2YWlsYWJpbGl0eSAlIiwgIlR5cGUiLCAiU3RyZWFtcyIsICJncm91cC10aXRsZSAlIiwgInR2Zy1pZCAlIiwgIlVuaXF1ZSBJRCAlIik7CiAgICAgIHZhciBjb21wYXRpYmlsaXR5S2V5cyA9IG5ldyBBcnJheSgic3RyZWFtcyIsICJncm91cC50aXRsZSIsICJ0dmcuaWQiLCAic3RyZWFtLmlkIik7CiAgICAgIGJyZWFrOwogIH0KCiAgZm9yICh2YXIgaSA9IDA7IGkgPCB0ckhlYWRsaW5lcy5sZW5ndGg7IGkrKykgewogICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgbmV3VERbIl9lbGVtZW50Il0gPSAiVEQiOwogICAgbmV3VERbIl90ZXh0Il0gICAgPSB0ckhlYWRsaW5lc1tpXTsKICAgIHRyLmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VEQpKTsKICB9CiAgCiAgZm9yICh2YXIgaSA9IDA7IGkgPCBhdmFpbGFibGVGaWxlVHlwZXMubGVuZ3RoOyBpKyspIHsKICAgIAogICAgdmFyIGZpbGVUeXBlID0gYXZhaWxhYmxlRmlsZVR5cGVzW2ldCgogICAgdmFyIGRhdGEgPSBjb25maWdbImZpbGVzIl1bZmlsZVR5cGVdOwogICAgCiAgICB2YXIgYWxsRmlsZXMgPSBnZXRPYmpLZXlzKGRhdGEpCiAgCiAgICBmb3IgKHZhciBmID0gMDsgZiA8IGFsbEZpbGVzLmxlbmd0aDsgZisrKSB7CiAgICAgIHZhciBlbG0gICAgICAgICAgID0gZGF0YVthbGxGaWxlc1tmXV07CiAgICAgIHZhciB0YWJsZSAgICAgICAgID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlkX21hcHBpbmciKTsKICAgICAgdmFyIGZpbGVJRCAgICAgICAgPSBlbG1bImlkLnByb3ZpZGVyIl07CiAgICAgIHZhciBuYW1lICAgICAgICAgID0gZWxtWyJuYW1lIl07CiAgICAgIHZhciBsYXN0VXBkYXRlICAgID0gZWxtWyJsYXN0LnVwZGF0ZSJdOwogICAgICB2YXIgYXZhaWxhYmlsaXR5ICA9IGVsbVsicHJvdmlkZXIuYXZhaWxhYmlsaXR5Il07CiAgICAgIHZhciB0eXBlICAgICAgICAgID0gZWxtWyJ0eXBlIl0udG9VcHBlckNhc2UoKTsKICAgICAgdmFyIGNvbXBhdGliaWxpdHkgPSBlbG1bImNvbXBhdGliaWxpdHkiXTsKCiAgICAgIC8vIENyZWF0ZSBUUgogICAgICB2YXIgbmV3VFIgPSBuZXcgT2JqZWN0KCk7CiAgICAgIG5ld1RSWyJfZWxlbWVudCJdICAgICAgID0gIlRSIjsKICAgICAgbmV3VFJbImNsYXNzIl0gICAgICAgICAgPSAiIjsKICAgICAgbmV3VFJbImlkIl0gICAgICAgICAgICAgPSBmaWxlSUQ7CiAgICAgIG5ld1RSWyJvbmNsaWNrIl0gICAgICAgID0gJ2phdmFzY3JpcHQ6IGZpbGVEZXRhaWwoIicgKyBmaWxlSUQgKyAnIiwiJyArIGZpbGVUeXBlICsgJyIpOyc7CiAgICAgIHRhYmxlLmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3VFIpKTsKCiAgICAgIHZhciB0ciA9IHRhYmxlLmxhc3RDaGlsZDsKCiAgICAgIC8vIENyZWF0ZSBmaWxlIG5hbWUgVEQKICAgICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgICAgbmV3VERbIl90ZXh0Il0gICAgPSBuYW1lOwogICAgICBjcmVhdGVOZXdURChuZXdURCwgdHIpOwoKICAgICAgLy8gQ3JlYXRlIGxhc3QgdXBkYXRlIFRECiAgICAgIHZhciBuZXdURCA9IG5ldyBPYmplY3QoKTsKICAgICAgbmV3VERbIl9lbGVtZW50Il0gPSAiUCI7CiAgICAgIG5ld1REWyJfdGV4dCJdICAgID0gbGFzdFVwZGF0ZTsKICAgICAgY3JlYXRlTmV3VEQobmV3VEQsIHRyKTsKCiAgICAgIC8vIENyZWF0ZSBhdmFpbGFiaWxpdHkgVEQKICAgICAgdmFyIG5ld1REID0gbmV3IE9iamVjdCgpOwogICAgICBuZXdURFsiX2VsZW1lbnQiXSA9ICJQIjsKICAgICAgbmV3VERbIl90ZXh0Il0gICAgPSBhdmFpbGFiaWxpdHk7CiAgICAgIGNyZWF0ZU5ld1REKG5ld1RELine 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/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/base_ts.js"] = "dmFyIFNFUlZFUiA9IG5ldyBPYmplY3QoKTsKdmFyIEJVTEtfRURJVCA9IGZhbHNlOwp2YXIgQ09MVU1OX1RPX1NPUlQ7CnZhciBTRUFSQ0hfTUFQUElORyA9IG5ldyBPYmplY3QoKTsKdmFyIFVORE8gPSBuZXcgT2JqZWN0KCk7CnZhciBTRVJWRVJfQ09OTkVDVElPTiA9IGZhbHNlOwp2YXIgV1NfQVZBSUxBQkxFID0gZmFsc2U7Ci8vIE1lbsO8CnZhciBtZW51SXRlbXMgPSBuZXcgQXJyYXkoKTsKbWVudUl0ZW1zLnB1c2gobmV3IE1haW5NZW51SXRlbSgicGxheWxpc3QiLCAie3subWFpbk1lbnUuaXRlbS5wbGF5bGlzdH19IiwgIm0zdS5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUucGxheWxpc3R9fSIpKTsKLy9tZW51SXRlbXMucHVzaChuZXcgTWFpbk1lbnVJdGVtKCJwbXNJRCIsICJ7ey5tYWluTWVudS5pdGVtLnBtc0lEfX0iLCAibnVtYmVyLnBuZyIsICJ7ey5tYWluTWVudS5oZWFkbGluZS5wbXNJRH19IikpCm1lbnVJdGVtcy5wdXNoKG5ldyBNYWluTWVudUl0ZW0oImZpbHRlciIsICJ7ey5tYWluTWVudS5pdGVtLmZpbHRlcn19IiwgImZpbHRlci5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUuZmlsdGVyfX0iKSk7Cm1lbnVJdGVtcy5wdXNoKG5ldyBNYWluTWVudUl0ZW0oInhtbHR2IiwgInt7Lm1haW5NZW51Lml0ZW0ueG1sdHZ9fSIsICJ4bWx0di5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUueG1sdHZ9fSIpKTsKbWVudUl0ZW1zLnB1c2gobmV3IE1haW5NZW51SXRlbSgibWFwcGluZyIsICJ7ey5tYWluTWVudS5pdGVtLm1hcHBpbmd9fSIsICJtYXBwaW5nLnBuZyIsICJ7ey5tYWluTWVudS5oZWFkbGluZS5tYXBwaW5nfX0iKSk7Cm1lbnVJdGVtcy5wdXNoKG5ldyBNYWluTWVudUl0ZW0oInVzZXJzIiwgInt7Lm1haW5NZW51Lml0ZW0udXNlcnN9fSIsICJ1c2Vycy5wbmciLCAie3subWFpbk1lbnUuaGVhZGxpbmUudXNlcnN9fSIpKTsKbWVudUl0ZW1zLnB1c2gobmV3IE1haW5NZW51SXRlbSgic2V0dGluZ3MiLCAie3subWFpbk1lbnUuaXRlbS5zZXR0aW5nc319IiwgInNldHRpbmdzLnBuZyIsICJ7ey5tYWluTWVudS5oZWFkbGluZS5zZXR0aW5nc319IikpOwptZW51SXRlbXMucHVzaChuZXcgTWFpbk1lbnVJdGVtKCJsb2ciLCAie3subWFpbk1lbnUuaXRlbS5sb2d9fSIsICJsb2cucG5nIiwgInt7Lm1haW5NZW51LmhlYWRsaW5lLmxvZ319IikpOwptZW51SXRlbXMucHVzaChuZXcgTWFpbk1lbnVJdGVtKCJsb2dvdXQiLCAie3subWFpbk1lbnUuaXRlbS5sb2dvdXR9fSIsICJsb2dvdXQucG5nIiwgInt7Lm1haW5NZW51LmhlYWRsaW5lLmxvZ291dH19IikpOwovLyBLYXRlZ29yaWVuIGbDvHIgZGllIEVpbnN0ZWxsdW5nZW4KdmFyIHNldHRpbmdzQ2F0ZWdvcnkgPSBuZXcgQXJyYXkoKTsKc2V0dGluZ3NDYXRlZ29yeS5wdXNoKG5ldyBTZXR0aW5nc0NhdGVnb3J5SXRlbSgie3suc2V0dGluZ3MuY2F0ZWdvcnkuZ2VuZXJhbH19IiwgInh0ZXZlQXV0b1VwZGF0ZSx0dW5lcixlcGdTb3VyY2UsYXBpIikpOwpzZXR0aW5nc0NhdGVnb3J5LnB1c2gobmV3IFNldHRpbmdzQ2F0ZWdvcnlJdGVtKCJ7ey5zZXR0aW5ncy5jYXRlZ29yeS5maWxlc319IiwgInVwZGF0ZSxmaWxlcy51cGRhdGUsdGVtcC5wYXRoLGNhY2hlLmltYWdlcyx4ZXBnLnJlcGxhY2UubWlzc2luZy5pbWFnZXMiKSk7CnNldHRpbmdzQ2F0ZWdvcnkucHVzaChuZXcgU2V0dGluZ3NDYXRlZ29yeUl0ZW0oInt7LnNldHRpbmdzLmNhdGVnb3J5LnN0cmVhbWluZ319IiwgImJ1ZmZlcixidWZmZXIuc2l6ZS5rYixidWZmZXIudGltZW91dCx1c2VyLmFnZW50IikpOwpzZXR0aW5nc0NhdGVnb3J5LnB1c2gobmV3IFNldHRpbmdzQ2F0ZWdvcnlJdGVtKCJ7ey5zZXR0aW5ncy5jYXRlZ29yeS5iYWNrdXB9fSIsICJiYWNrdXAucGF0aCxiYWNrdXAua2VlcCIpKTsKc2V0dGluZ3NDYXRlZ29yeS5wdXNoKG5ldyBTZXR0aW5nc0NhdGVnb3J5SXRlbSgie3suc2V0dGluZ3MuY2F0ZWdvcnkuYXV0aGVudGljYXRpb259fSIsICJhdXRoZW50aWNhdGlvbi53ZWIsYXV0aGVudGljYXRpb24ucG1zLGF1dGhlbnRpY2F0aW9uLm0zdSxhdXRoZW50aWNhdGlvbi54bWwsYXV0aGVudGljYXRpb24uYXBpIikpOwpmdW5jdGlvbiBzaG93UG9wVXBFbGVtZW50KGVsbSkgewogICAgdmFyIGFsbEVsZW1lbnRzID0gbmV3IEFycmF5KCJwb3B1cC1jdXN0b20iKTsKICAgIGZvciAodmFyIGkgPSAwOyBpIDwgYWxsRWxlbWVudHMubGVuZ3RoOyBpKyspIHsKICAgICAgICBzaG93RWxlbWVudChhbGxFbGVtZW50c1tpXSwgZmFsc2UpOwogICAgfQogICAgc2hvd0VsZW1lbnQoZWxtLCB0cnVlKTsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gKCkgewogICAgICAgIHNob3dFbGVtZW50KCJwb3B1cCIsIHRydWUpOwogICAgfSwgMTApOwogICAgcmV0dXJuOwp9CmZ1bmN0aW9uIHNob3dFbGVtZW50KGVsbUlELCB0eXBlKSB7CiAgICB2YXIgY3NzQ2xhc3M7CiAgICBzd2l0Y2ggKHR5cGUpIHsKICAgICAgICBjYXNlIHRydWU6CiAgICAgICAgICAgIGNzc0NsYXNzID0gImJsb2NrIjsKICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgY2FzZSBmYWxzZToKICAgICAgICAgICAgY3NzQ2xhc3MgPSAibm9uZSI7CiAgICAgICAgICAgIGJyZWFrOwogICAgfQogICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoZWxtSUQpLmNsYXNzTmFtZSA9IGNzc0NsYXNzOwp9CmZ1bmN0aW9uIGNoYW5nZUJ1dHRvbkFjdGlvbihlbGVtZW50LCBidXR0b25JRCwgYXR0cmlidXRlKSB7CiAgICB2YXIgdmFsdWUgPSBlbGVtZW50Lm9wdGlvbnNbZWxlbWVudC5zZWxlY3RlZEluZGV4XS52YWx1ZTsKICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGJ1dHRvbklEKS5zZXRBdHRyaWJ1dGUoYXR0cmlidXRlLCB2YWx1ZSk7Cn0KZnVuY3Rpb24gZ2V0TG9jYWxEYXRhKGRhdGFUeXBlLCBpZCkgewogICAgdmFyIGRhdGEgPSBuZXcgT2JqZWN0KCk7CiAgICBzd2l0Y2ggKGRhdGFUeXBlKSB7CiAgICAgICAgY2FzZSAibTN1IjoKICAgICAgICAgICAgZGF0YSA9IFNFUlZFUlsic2V0dGluZ3MiXVsiZmlsZXMiXVtkYXRhVHlwZV1baWRdOwogICAgICAgICAgICBicmVhazsKICAgICAgICBjYXNlICJoZGhyIjoKICAgICAgICAgICAgZGF0YSA9IFNFUlZFUlsic2V0dGluZ3MiXVsiZmlsZXMiXVtkYXRhVHlwZV1baWRdOwogICAgICAgICAgICBicmVhazsKICAgICAgICBjYXNlICJmaWx0ZXIiOgogICAgICAgIGNhc2UgImN1c3RvbS1maWx0ZXIiOgogICAgICAgIGNhc2UgImdyb3VwLXRpdGxlIjoKICAgICAgICAgICAgaWYgKGlkID09IC0xKSB7CiAgICAgICAgICAgICAgICBkYXRhWyJhY3RpdmUiXSA9IHRydWU7CiAgICAgICAgICAgICAgICBkYXRhWyJjYXNlU2Vuc2l0aXZlIl0gPSBmYWxzZTsKICAgICAgICAgICAgICAgIGRhdGFbImRlc2NyaXB0aW9uIl0gPSAiIjsKICAgICAgICAgICAgICAgIGRhdGFbImV4Y2x1ZGUiXSA9ICIiOwogICAgICAgICAgICAgICAgZGF0YVsiZmlsdGVyIl0gPSAiIjsKICAgICAgICAgICAgICAgIGRhdGFbImluY2x1ZGUiXSA9ICIiOwogICAgICAgICAgICAgICAgZGF0YVsibmFtZSJdID0gIiI7CiAgICAgICAgICAgICAgICBkYXRhWyJ0eXBlIl0gPSAiZ3JvdXAtdGl0bGUiOwogICAgICAgICAgICAgICAgU0VSVkVSWyJzZXR0aW5ncyJdWyJmaWx0ZXIiXVtpZF0gPSBkYXRhOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGRhdGEgPSBTRVJWRVJbInNldHRpbmdzIl1bImZpbHRlciJdW2lkXTsKICAgICAgICAgICAgYnJlYWs7CiLine truncated
webUI["html/js/menu.js"] = "CmZ1bmN0aW9uIHNldE1lbnVJdGVtKCkgewoKICBtZW51ID0gbmV3IE9iamVjdCgpOwogIHN1Yk1lbnUgPSBuZXcgT2JqZWN0KCk7CgogIHZhciBtZW51X20zdSA9IG5ldyBPYmplY3QoKTsKICBtZW51X20zdVsiX21lbnVUeXBlIl0gICAgICAgPSAiaW5wdXRBcnJheSI7CiAgbWVudV9tM3VbIl9lbGVtZW50Il0gICAgICAgID0gIkxJIjsKICBtZW51X20zdVsiX2NvbmZpZ0tleSJdICAgICAgPSAiZmlsZXMubTN1IjsKICBtZW51X20zdVsiX3RleHQiXSAgICAgICAgICAgPSAiUGxheWxpc3QiOwogIG1lbnVfbTN1WyJfaWNvbiJdICAgICAgICAgICA9ICJpbWcvbTN1LnBuZyI7CiAgbWVudV9tM3VbIl9oZWFkbGluZSJdICAgICAgID0gIlBsYXlsaXN0czogTG9jYWwgb3IgcmVtb3RlIjsKICBtZW51X20zdVsiX3VzYWdlIl0gICAgICAgICAgPSAiPGI+SW5mbzwvYj48YnI+QXZhaWxhYmlsaXR5OiBGaWxlIGF2YWlsYWJpbGl0eSBpbiBwZXJjZW50PGJyPlN0cmVhbXM6ICAgICAgTnVtYmVyIG9mIHN0cmVhbXMgaW4gdGhlIGZpbGUuPGJyPmdyb3VwLXRpdGxlOiAgU3RyZWFtcyB0aGF0IGFyZSBhc3NpZ25lZCB0byBhIGdyb3VwLiBTaW1wbGlmaWVzIGZpbHRlcmluZyBzdHJlYW1zPGJyPnR2Zy1pZDogICAgICAgVGhpcyBJRCBpcyB1c2VkIGZvciBhdXRvbWF0aWMgbWFwcGluZywgbXVzdCBtYXRjaCB3aXRoIHRoZSBjaGFubmVsIElEIGluIHRoZSBYTUxUViBmaWxlLjxicj5VbmlxdWUgSUQ6ICAgIFN0cmVhbXMgd2l0aCBhIHVuaXF1ZSBJRCB0byBpZGVudGlmeSB0aGVtLiBBbGxvd3MgY2hhbm5lbCBuYW1lIGNoYW5nZXMgaW4gdGhlIE0zVSB3aXRob3V0IGxvc2luZyB0aGUgWE1MVFYgbWFwcGluZyAoUFBWIC8gbGl2ZSBldmVudHMpLjxicj48YnI+PGI+VXNhZ2UgTTNVOjwvYj48YnI+UmVtb3RlIHBsYXlsaXN0OiBodHRwOi8veW91ci5pcHR2LnByb3ZpZGVyLmNvbS9maWxlLm0zdTxicj5Mb2NhbCAgcGxheWxpc3Q6IC9wYXRoL3RvL2ZpbGUubTN1PGJyPjxicj48Yj5Vc2FnZSBIREhvbWVSdW46PC9iPjxicj5JUDogMTkyLjE2OC4xLjEwOjUwMDQ8YnI+IgogIG1lbnVfbTN1WyJuYW1lIl0gICAgICAgICAgICA9ICJmaWxlIjsKICBtZW51X20zdVsiaWQiXSAgICAgICAgICAgICAgPSAiZmlsZSI7CiAgbWVudV9tM3VbInZhbHVlIl0gICAgICAgICAgID0gbWVudV9tM3VbIm5hbWUiXTsKICBtZW51X20zdVsicGxhY2Vob2xkZXIiXSAgICAgPSAiUGxheWxpc3Q6IGxvY2FsIG9yIHJlbW90ZSI7CiAgbWVudV9tM3VbIm9uY2xpY2siXSAgICAgICAgID0gImphdmFzY3JpcHQ6IHRvZ2dsZU1lbnUodGhpcyk7IjsKICBtZW51X20zdVsiY2xhc3MiXSAgICAgICAgICAgPSAibWVudS1ub3RBY3RpdmUiOwoKCiAgdmFyIG1lbnVfZmlsdGVyID0gbmV3IE9iamVjdCgpOwogIG1lbnVfZmlsdGVyWyJfbWVudVR5cGUiXSAgICA9ICJpbnB1dEFycmF5IjsKICBtZW51X2ZpbHRlclsiX2VsZW1lbnQiXSAgICAgPSAiTEkiOwogIG1lbnVfZmlsdGVyWyJfY29uZmlnS2V5Il0gICA9ICJmaWx0ZXIiOwogIG1lbnVfZmlsdGVyWyJfdGV4dCJdICAgICAgICA9ICJGaWx0ZXIiOwogIG1lbnVfZmlsdGVyWyJfaWNvbiJdICAgICAgICA9ICJpbWcvZmlsdGVyLnBuZyI7CiAgbWVudV9maWx0ZXJbIl9oZWFkbGluZSJdICAgID0gIkZpbHRlciBieSBNM1UgcGFyYW1ldGVycywgZS5nLiBncm91cC10aXRsZSI7CiAgbWVudV9maWx0ZXJbIl91c2FnZSJdICAgICAgID0gIjxiPlVzYWdlOjwvYj48YnI+U3BvcnQgLSBBbGwgc3BvcnRzIGNoYW5uZWxzPGJyPlNwb3J0IHtIRH0gLSBBbGwgSEQgc3BvcnRzIGNoYW5uZWxzPGJyPlNwb3J0IHtIRH0gIXtFUyxERX0gLSBBbGwgSEQgc3BvcnRzIGNoYW5uZWxzLCBidXQgbm8gU3BhbmlzaCBhbmQgR2VybWFuPGJyPjxicj5UbyBmaWx0ZXIgdGhlIHN0cmVhbXMgb2YgYSBIREhvbWVSdW4sIHRoZSBwbGF5bGlzdCBuYW1lIGNhbiBiZSBlbnRlcmVkOjxicj5NeSB0dW5lciB7SER9IgogIC8vbWVudV9maWx0ZXJbIl91c2FnZSJdICAgICAgID0gIjxiPlVzYWdlOjwvYj48YnI+QWxsIHNwb3J0cyBjaGFubmVsczogU3BvcnQ8YnI+QWxsIEhEIHNwb3J0cyBjaGFubmVsczogU3BvcnQge0hEfTxicj5BbGwgSEQgc3BvcnRzIGNoYW5uZWxzLCBidXQgbm8gU3BhbmlzaCBhbmQgR2VybWFuOiBTcG9ydCB7SER9ICF7RVMsREV9IgogIG1lbnVfZmlsdGVyWyJuYW1lIl0gICAgICAgICA9ICJmaWx0ZXIiOwogIG1lbnVfZmlsdGVyWyJpZCJdICAgICAgICAgICA9ICJNM1UiOwogIG1lbnVfZmlsdGVyWyJ2YWx1ZSJdICAgICAgICA9IG1lbnVfZmlsdGVyWyJuYW1lIl07CiAgbWVudV9maWx0ZXJbInBsYWNlaG9sZGVyIl0gID0gIkZpbHRlciBzdHJlYW1zOiBTcG9ydCI7CiAgbWVudV9maWx0ZXJbIm9uY2xpY2siXSAgICAgID0gImphdmFzY3JpcHQ6IHRvZ2dsZU1lbnUodGhpcyk7IjsKICBtZW51X2ZpbHRlclsiY2xhc3MiXSAgICAgICAgPSAibWVudS1ub3RBY3RpdmUiOwoKICB2YXIgbWVudV9pZCA9IG5ldyBPYmplY3QoKTsKICBtZW51X2lkWyJfbWVudVR5cGUiXSAgICAgICAgPSAiaW5wdXRBcnJheSI7CiAgbWVudV9pZFsiX2VsZW1lbnQiXSAgICAgICAgID0gIkxJIjsKICBtZW51X2lkWyJfY29uZmlnS2V5Il0gICAgICAgPSAiaWQiOwogIG1lbnVfaWRbIl90ZXh0Il0gICAgICAgICAgICA9ICJQTVMgSUQiOwogIG1lbnVfaWRbIl9pY29uIl0gICAgICAgICAgICA9ICJpbWcvbnVtYmVyLnBuZyI7CiAgbWVudV9pZFsiX2hlYWRsaW5lIl0gICAgICAgID0gIlNldHVwIFBNUyBndWlkZSBudW1iZXIiOwogIG1lbnVfaWRbIl91c2FnZSJdICAgICAgICAgICA9ICdTb21lIHBsYXlsaXN0cyBoYXZlIHVuaXF1ZSBjaGFubmVsIElEcy48YnI+RW50ZXIgdGhlIGtleXdvcmQgb2YgdGhlIElELiBUaGUgY2hhbm5lbCBhc3NpZ25tZW50IGluIFBNUyB3aWxsIGNoYW5nZSBhcyBhIHJlc3VsdC48YnI+PGJyPmUuZy4gY2hhbm5lbElEPGJyPiNFWFRJTkY6MCB0eXBlPSJzdHJlYW0iIDxiPmNoYW5uZWxJZDwvYj49IjgxIiwgTXkgU3RyZWFtaW5nIENoYW5uZWwgSEQ8YnI+PGJyPk9ubHkgZW50ZXIgaGVyZSBpZiB5b3Uga25vdyB3aGF0IHlvdSBhcmUgZG9pbmchJwogIG1lbnVfaWRbIm5hbWUiXSAgICAgICAgICAgICA9ICJpZCI7CiAgbWVudV9pZFsiaWQiXSAgICAgICAgICAgICAgID0gImlkIjsKICBtZW51X2lkWyJ2YWx1ZSJdICAgICAgICAgICAgPSBtZW51X2lkWyJuYW1lIl07CiAgbWVudV9pZFsicGxhY2Vob2xkZXIiXSAgICAgID0gIlVuaXF1ZSBJRCBmcm9tIHRoZSBNM1UgZmlsZSI7CiAgbWVudV9pZFsib25jbGljayJdICAgICAgICAgID0gImphdmFzY3JpcHQ6IHRvZ2dsZU1lbnUodGhpcyk7IjsKICBtZW51X2lkWyJjbGFzcyJdICAgICAgICAgICAgPSAibWVudS1ub3RBY3RpdmUiOwoKCiAgdmFyIG1lbnVfeG1sdHYgPSBuZXcgT2JqZWN0KCk7CiAgbWVudV94bWx0dlsiX21lbnVUeXBlIl0gICAgID0gImlucHV0QXJyYXkiOwogIG1lbnVfeG1sdHZbIl9lbGVtZW50Il0gICAgICA9ICJMSSI7CiAgbWVudV94bWx0dlsiX2NvbmZpZ0tleSJdICAgID0gImZpbGVzLnhtbHR2IjsKICBtZW51X3htbHR2WyJfdGV4dCJdICAgICAgICAgPSAiWE1MVFYiOwogIG1lbnVfeG1sdHZbIl9pY29uIl0gICAgICAgICA9ICJpbWcveG1sdHYucG5nIjsKICBtZW51X3htbHR2WyJfaGVhZGxpbmUiXSAgICAgPSAiWE1MVFYgZmlsZXM6IExvY2FsIG9yIHJlbW90ZSI7CiAgbWVudV94bLine 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/lang/de.json"] = "ewogICJtYWluTWVudSI6IHsKICAgICJpdGVtIjp7CiAgICAgICJwbGF5bGlzdCI6ICJXaWVkZXJnYWJlbGlzdGUiLAogICAgICAicG1zSUQiOiAiUE1TIElEIiwKICAgICAgImZpbHRlciI6ICJGaWx0ZXIiLAogICAgICAieG1sdHYiOiAiWE1MVFYiLAogICAgICAibWFwcGluZyI6ICJNYXBwaW5nIiwKICAgICAgInVzZXJzIjogIkJlbnV0emVyIiwKICAgICAgInNldHRpbmdzIjogIkVpbnN0ZWxsdW5nZW4iLAogICAgICAibG9nIjogIkxvZyIsCiAgICAgICJsb2dvdXQiOiAiQWJtZWxkZW4iCiAgICB9LAogICAgImhlYWRsaW5lIjogewogICAgICAicGxheWxpc3QiOiAiTG9rYWxlIG9kZXIgZW50ZmVybnRlIFdpZWRlcmdhYmVsaXN0ZW4iLAogICAgICAiZmlsdGVyIjogIkZpbHRlcm4gdm9uIEthbsOkbGVuIiwKICAgICAgInhtbHR2IjogIkxva2FsZSBvZGVyIGVudGZlcm50ZSBYTUxUViBEYXRlaWVuIiwKICAgICAgIm1hcHBpbmciOiAiS2Fuw6RsZSBkZXIgV2llZGVyZ2FiZWxpc3RlbiBFUEcgS2Fuw6RsZW4genVvcmRuZW4iLAogICAgICAidXNlcnMiOiAiQmVudXR6ZXJ2ZXJ3YWx0dW5nIiwKICAgICAgInNldHRpbmdzIjogIkVpbnN0ZWxsdW5nZW4iLAogICAgICAibG9nIjogIkxvZyIsCiAgICAgICJsb2dvdXQiOiAiQWJtZWxkZW4iCiAgICB9CiAgfSwKICAiY29uZmlybSI6ewogICAgInJlc3RvcmUiOiAiQWxsZSBEYXRlbiB3ZXJkZW4gbWl0IGRlbmVuIGF1cyBkZW0gQmFja3VwIGVyc2V0enQuIFdpZWRlcmhlc3RlbGx1bmcgZHVyY2hmw7xocmVuPyIKICB9LAogICJhbGVydCI6IHsKICAgICJmaWxlTG9hZGluZ0Vycm9yIjogIkRhdGVpIGtvbm50ZSBuaWNodCBnZWxhZGVuIHdlcmRlbiIsCiAgICAiaW52YWxpZENoYW5uZWxOdW1iZXIiOiAiVW5nw7xsdGlnZSBLYW5hbG51bW1lciIKICB9LAogICJidXR0b24iOnsKICAgICJiYWNrIjogIlp1csO8Y2siLAogICAgImJhY2t1cCI6ICJCYWNrdXAiLAogICAgImJ1bGtFZGl0IjogIk1hc3NlbmJlYXJiZWl0dW5nIiwKICAgICJjYW5jZWwiOiAiQWJicmVjaGVuIiwKICAgICJkZWxldGUiOiAiTMO2c2NoZW4iLAogICAgImRvbmUiOiAiRmVydGlnIiwKICAgICJsb2dpbiI6ICJBbm1lbGRlbiIsCiAgICAibmV3IjogIk5ldSIsCiAgICAibmV4dCI6ICJXZWl0ZXIiLAogICAgInJlc3RvcmUiOiAiV2llZGVyaGVyc3RlbGxlbiIsCiAgICAic2F2ZSI6ICJTcGVjaWhlcm4iLAogICAgInNlYXJjaCI6ICJTdWNoZW4iLAogICAgInVwZGF0ZSI6ICJVcGRhdGUiLAogICAgImNyYWV0ZUFjY291bnQiOiAiQWNjb3VudCBlcnN0ZWxsZW4iLAogICAgInJlc2V0bG9ncyI6ICJMb2dzIGzDtnNjaGVuIiwKICAgICJ1cGxvYWRMb2dvIjogIkxvZ28gaG9jaGxhZGVuIgogIH0sCiAgImZpbHRlciI6IHsKICAgICJ0YWJsZSI6IHsKICAgICAgIm5hbWUiOiAiRmlsdGVyIE5hbWUiLAogICAgICAidHlwZSI6ICJGaWx0ZXIgVHlwIiwKICAgICAgImZpbHRlciI6ICJGaWx0ZXIiCiAgICB9LAogICAgImN1c3RvbSI6ICJCZW51dHplcmRlZmluaWVydCIsCiAgICAiZ3JvdXAiOiAiR3J1cHBlIiwKICAgICJuYW1lIjogewogICAgICAidGl0bGUiOiAiRmlsdGVyIE5hbWUiLAogICAgICAicGxhY2Vob2xkZXIiOiAiRmlsdGVyIE5hbWUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJkZXNjcmlwdGlvbiI6IHsKICAgICAgInRpdGxlIjogIkJlc2NocmVpYnVuZyIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJCZXNjaHJlaWJ1bmciLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJ0eXBlIjogewogICAgICAidGl0bGUiOiAiVHlwIiwKICAgICAgImdyb3VwVGl0bGUiOiAiR3JvdXBlbnRpdGVsIiwKICAgICAgImN1c3RvbUZpbHRlciI6ICJCZW51dHplcmRlZmluaWVydGVyIEZpbHRlciIKICAgIH0sCiAgICAiY2FzZVNlbnNpdGl2ZSI6IHsKICAgICAgInRpdGxlIjogIkdyb8OfLSAvIEtsZWluc2NocmVpYnVuZyBiZWFjaHRlbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWx0ZXJSdWxlIjogewogICAgICAidGl0bGUiOiAiRmlsdGVycmVnZWwiLAogICAgICAicGxhY2Vob2xkZXIiOiAiU3BvcnQge0hEfSAhe0VTLElUfSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICIiCiAgICB9LAogICAgImZpbHRlckdyb3VwIjogewogICAgICAidGl0bGUiOiAiR3JvdXBlbnRpdGVsIiwKICAgICAgInBsYWNlaG9sZGVyIjogIiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJHcnVwcGUgYXVzIGRlciBNM1UgYXVzd8OkaGxlbi4gKFrDpGhsZXIpPGJyPsOkbmRlcnVuZyBkZXMgR3J1cHBlbnRpdGVscyBpbiBkZXIgTTNVIG1hY2h0IGRlbiBGaWx0ZXIgdW5nw7xsdGlnLiIKICAgIH0sCiAgICAiaW5jbHVkZSI6IHsKICAgICAgInRpdGxlIjogIk11c3MgYmVpbmhhbHRlbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJGSEQsVUhEIiwKICAgICAgImRlc2NyaXB0aW9uIjogIkthbmFsbmFtZSBtdXNzIGRpZXNlIFfDtnJ0ZXIgZW50aGFsdGVuLjxicj4oS29tbWFnZXRyZW5udCkgS29tbWEgYmVkZXV0ZXQgb2RlciIKICAgIH0sCiAgICAiZXhjbHVkZSI6IHsKICAgICAgInRpdGxlIjogIkRhcmYgbmljaHQgYmVpbmhhbHRlbiIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJFUyxJVCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJLYW5hbG5hbWUgZGFyZiBkaWVzZSBXw7ZydGVyIG5pY2h0IGVudGhhbHRlbi48YnI+KEtvbW1hZ2V0cmVubnQpS29tbWEgYmVkZXV0ZXQgb2RlciIKICAgIH0KCiAgfSwKICAicGxheWxpc3QiOiB7CiAgICAidGFibGUiOiB7CiAgICAgICJwbGF5bGlzdCI6ICJXaWVkZXJnYWJlbGlzdGUiLAogICAgICAidHVuZXIiOiAiVHVuZXIiLAogICAgICAibGFzdFVwZGF0ZSI6ICJMZXR6dGUgQWt0dWFsaXNpZXJ1bmciLAogICAgICAiYXZhaWxhYmlsaXR5IjogIlZlcmbDvGdiYXJrZWl0IiwKICAgICAgInR5cGUiOiAiVHlwIiwKICAgICAgInN0cmVhbXMiOiAiU3RyZWFtcyIsCiAgICAgICJncm91cFRpdGxlIjogIkdyb3VwZW50aXRlbCIsCiAgICAgICJ0dmdJRCI6ICJ0dmctaWQiLAogICAgICAidW5pcXVlSUQiOiAiRWluZGV1dGlnZSBJRCIKICAgIH0sCiAgICAicGxheWxpc3RUeXBlIjogewogICAgICAidGl0bGUiOiAiV2llZGVyZ2FiZWxpc3RlbiBUeXAiLAogICAgICAicGxhY2Vob2xkZXIiOiAiIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAidHlwZSI6IHsKICAgICAgInRpdGxlIjogIlR5cCIsCiAgICAgICJwbGFjZWhvbGRlciI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJuYW1lIjogewogICAgICAidGl0bGUiOiAiTmFtZSIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJOYW1lIGRlciBXaWVkZXJnYWJlbGlzdGUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJkZXNjcmlwdGlvbiI6IHsKICAgICAgInRpdGxlIjogIkJlc2NocmVpYnVuZyIsCiAgICAgICJwbGFjZWhvbGRlciI6ICJCZXNjaHJlaWJ1bmciLAogICAgICAiZGVzY3JpcHRpb24iOiAiIgogICAgfSwKICAgICJmaWxlTTNVIjogewogICAgICAidGl0bGUiOiAiTTNVIERhdGVpIiwKICAgICAgInBsYWNlaG9sZGVyIjogIkRhdGVpcGZhZCBvZGVyIFVSTCBkZXIgTTNVIiwKICAgICAgImRlc2NyaXB0aW9uIjogIiIKICAgIH0sCiAgICAiZmlsZUhESFIiOiB7CiAgICAgICJ0aXRsZSI6ICJIREhvbWVSdW4Line truncated
webUI["html/create-first-user.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+IAogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvbmV0d29ya190cy5qcyI+PC9zY3JpcHQ+CiAgICA8c2NyaXB0IGxhbmd1YWdlPSJqYXZhc2NyaXB0IiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiIHNyYz0ianMvYXV0aGVudGljYXRpb25fdHMuanMiPjwvc2NyaXB0PgogIDwvaGVhZD4KCiAgICA8Ym9keT4KICAgICAgICAgIAogICAgICA8ZGl2IGlkPSJoZWFkZXIiIGNsYXNzPSJpbWdDZW50ZXIiPjwvZGl2PgogICAgICA8ZGl2IGlkPSJib3giPgogICAgICAgIDxkaXYgaWQ9ImhlYWRsaW5lIj4KICAgICAgICAgIDxoMSBpZD0iaGVhZC10ZXh0IiBjbGFzcz0iY2VudGVyIj57ey5hY2NvdW50LmhlYWRsaW5lfX08L2gxPgogICAgICAgIDwvZGl2PgogICAgICAgIDxwIGlkPSJlcnIiIGNsYXNzPSJlcnJvck1zZyBjZW50ZXIiPjwvcD4gICAKICAgICAgICA8ZGl2IGlkPSJjb250ZW50Ij4KICAgICAgICAgICAgPGZvcm0gaWQ9ImF1dGhlbnRpY2F0aW9uIiBhY3Rpb249Ii93ZWIvIiBtZXRob2Q9InBvc3QiPgogICAgICAgICAgICAgIDxoNT57ey5hY2NvdW50LnVzZXJuYW1lLnRpdGxlfX06PC9oNT4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9InVzZXJuYW1lIiB0eXBlPSJ0ZXh0IiBuYW1lPSJ1c2VybmFtZSIgcGxhY2Vob2xkZXI9IlVzZXJuYW1lIiB2YWx1ZT0iIj4KICAgICAgICAgICAgICA8aDU+e3suYWNjb3VudC5wYXNzd29yZC50aXRsZX19OjwvaDU+CiAgICAgICAgICAgICAgPGlucHV0IGlkPSJwYXNzd29yZCIgdHlwZT0icGFzc3dvcmQiIG5hbWU9InBhc3N3b3JkIiBwbGFjZWhvbGRlcj0iUGFzc3dvcmQiIHZhbHVlPSIiPgogICAgICAgICAgICAgIDxoNT57ey5hY2NvdW50LmNvbmZpcm0udGl0bGV9fTo8L2g1PgogICAgICAgICAgICAgIDxpbnB1dCBpZD0iY29uZmlybSIgIHR5cGU9InBhc3N3b3JkIiBuYW1lPSJjb25maXJtIiAgcGxhY2Vob2xkZXI9IkNvbmZpcm0iIHZhbHVlPSIiPgogICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgPC9kaXY+CiAgICAgICAgCiAgICAgICAgPGRpdiBpZD0iYm94LWZvb3RlciI+CiAgICAgICAgICA8aW5wdXQgaWQ9InN1Ym1pdCIgY2xhc3M9IiIgdHlwZT0iYnV0dG9uIiB2YWx1ZT0ie3suYnV0dG9uLmNyYWV0ZUFjY291bnR9fSIgb25jbGljaz0iamF2YXNjcmlwdDogbG9naW4oKTsiPgogICAgICAgIDwvZGl2PgogICAgICAKICAgICAgICAKICAgICAgPC9kaXY+CiAgICA8L2JvZHk+CjwvaHRtbD4="
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/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/index.html"] = "PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogICAgPCEtLS0KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wIiAvPiAKICAgIC0tPgogICAgPHRpdGxlPnhUZVZlPC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL3NjcmVlbi5jc3MiIHR5cGU9InRleHQvY3NzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iY3NzL2Jhc2UuY3NzIiB0eXBlPSJ0ZXh0L2NzcyI+CgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL25ldHdvcmtfdHMuanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL21lbnVfdHMuanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBsYW5ndWFnZT0iamF2YXNjcmlwdCIgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL3NldHRpbmdzX3RzLmpzIj48L3NjcmlwdD4KICAgIDxzY3JpcHQgbGFuZ3VhZ2U9ImphdmFzY3JpcHQiIHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSJqcy9sb2dzX3RzLmpzIj48L3NjcmlwdD4KICAgIDxzY3JpcHQgbGFuZ3VhZ2U9ImphdmFzY3JpcHQiIHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSJqcy9iYXNlX3RzLmpzIj48L3NjcmlwdD4KCiAgPC9oZWFkPgoKICAgIDxib2R5IG9ubG9hZD0iamF2YXNjcmlwdDogUGFnZVJlYWR5KCk7Ij4KICAgICAgPGRpdiBpZD0ibG9hZGluZyIgY2xhc3M9Im5vbmUiPgogICAgICAgIDxkaXYgY2xhc3M9ImxvYWRlciI+CiAgICAgICAgICAKICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CgogICAgICA8ZGl2IGlkPSJwb3B1cCIgY2xhc3M9Im5vbmUiPgoKICAgICAgICA8ZGl2IGlkPSJwb3B1cC1jdXN0b20iPjwvZGl2PgoKICAgICAgPC9kaXY+CgogICAgICA8ZGl2IGlkPSJsYXlvdXQiPgoKICAgICAgICA8IS0tCiAgICAgICAgPGRpdiBpZD0ibm90aWZpY2F0aW9uIj4KICAgICAgICAgIDxkaXYgY2xhc3M9ImVsZW1lbnQiPgogICAgICAgICAgICA8aDU+WEVQRzwvaDU+CiAgICAgICAgICAgIDxwcmU+MTEuMDUuMjAxOSAtIDIwOjIxPC9wcmU+CiAgICAgICAgICAgIDxocj4KICAgICAgICAgICAgPHA+SGFsbG8gZGFzIGlzdCBlaW4gVGVzdC4gVW5kIG5vY2ggbWVociBUZXh0LjwvcD4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICAtLT4KCiAgICAgICAgPGRpdiBpZD0ibWVudS13cmFwcGVyIiBjbGFzcz0ibGF5b3V0LWxlZnQiPgogICAgICAgICAgPGRpdiBpZD0gImJyYW5jaCI+PC9kaXY+CiAgICAgICAgICA8ZGl2IGlkPSJsb2dvIj48L2Rpdj4KICAgICAgICAgIDxuYXYgaWQ9Im1haW4tbWVudSI+PC9uYXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9ImxheW91dC1yaWdodCI+CiAgICAgICAgICA8dGFibGUgaWQ9ImNsaWVudEluZm8iIGNsYXNzPSIiPgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+eFRlVmU6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9InZlcnNpb24iIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5PUzo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0ib3MiIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5IHBob25lIj5EVlIgSVA6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9IkRWUiIgY2xhc3M9InRkVmFsIHBob25lIj4mbmJzcDs8L3RkPgogICAgICAgICAgICA8L3RyPgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+VVVJRDo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0idXVpZCIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkiPkFyY2g6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9ImFyY2giIGNsYXNzPSJ0ZFZhbCI+Jm5ic3A7PC90ZD4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5IHBob25lIj5NM1UgVVJMOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJtM3UtdXJsIiBjbGFzcz0idGRWYWwgcGhvbmUiPiZuYnNwOzwvdGQ+CiAgICAgICAgICAgICAgCiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQgY2xhc3M9InRkS2V5Ij5BdmFpbGFibGUgU3RyZWFtczo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0ic3RyZWFtcyIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkiPkVQRyBTb3VyY2U6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9ImVwZ1NvdXJjZSIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkgcGhvbmUiPlhFUEcgVVJMOjwvdGQ+CiAgICAgICAgICAgICAgPHRkIGlkPSJ4ZXBnLXVybCIgY2xhc3M9InRkVmFsIHBob25lIj4mbmJzcDs8L3RkPgogICAgICAgICAgICA8L3RyPgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+WEVQRyBDaGFubmVsczo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0ieGVwZyIgY2xhc3M9InRkVmFsIj4mbmJzcDs8L3RkPgogICAgICAgICAgICAgIDx0ZCBjbGFzcz0idGRLZXkiPkVycm9yczo8L3RkPgogICAgICAgICAgICAgIDx0ZCBpZD0iZXJyb3JzIiBjbGFzcz0idGRWYWwiPiZuYnNwOzwvdGQ+CiAgICAgICAgICAgICAgPHRkIGNsYXNzPSJ0ZEtleSI+V2FybmluZ3M6PC90ZD4KICAgICAgICAgICAgICA8dGQgaWQ9Indhcm5pbmdzIiBjbGFzcz0idGRWYWwiPiZuYnNwOzwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAKICAgICAgICAgIDxkaXYgaWQ9Im15U3RyZWFtc0JveCIgY2xhc3M9Im5vdFZpc2libGUiPgogICAgICAgICAgICA8ZGl2IGlkPSJhbGxTdHJlYW1zIj4KICAgICAgICAgICAgICA8dGFibGUgaWQ9ImFjdGl2ZVN0cmVhbXMiPjwvdGFibGU+CiAgICAgICAgICAgICAgPHRhYmxlIGlkPSJpbmFjdGl2ZVN0cmVhbXMiPjwvdGFibGU+CiAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgPCEtLTxkaXYgaWQ9Im9wZW5TdHJlYW1zIiBvbmNsaWNrPSJqYXZhc2NyaXB0OiBzaG93U3RyZWFtcygpOyI+PC9kaXY+LS0+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICAgIAogICAgICAgICAgPGRpdiBpZD0iY29udGVudCIgY2xhc3M9IiI+PC9kaXY+CiAgICAgICAgICAgIAogICAgICAgIDwvZGl2PgoKICAgICAgPC9kaXY+CiAgICAgIAogICAgPC9ib2R5Pgo8L2h0bWw+"
webUI["html/js/settings_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBTZXR0aW5nc0NhdGVnb3J5ID0gLyoqIEBjbGFzcyAqLyAoZnVuY3Rpb24gKCkgewogICAgZnVuY3Rpb24gU2V0dGluZ3NDYXRlZ29yeSgpIHsKICAgICAgICB0aGlzLkRvY3VtZW50SUQgPSAiY29udGVudF9zZXR0aW5ncyI7CiAgICB9CiAgICBTZXR0aW5nc0NhdGVnb3J5LnByb3RvdHlwZS5jcmVhdGVDYXRlZ29yeUhlYWRsaW5lID0gZnVuY3Rpb24gKHZhbHVlKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJINCIpOwogICAgICAgIGVsZW1lbnQuaW5uZXJIVE1MID0gdmFsdWU7CiAgICAgICAgcmV0dXJuIGVsZW1lbnQ7CiAgICB9OwogICAgU2V0dGluZ3NDYXRlZ29yeS5wcm90b3R5cGUuY3JlYXRlSFIgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJIUiIpOwogICAgICAgIHJldHVybiBlbGVtZW50OwogICAgfTsKICAgIFNldHRpbmdzQ2F0ZWdvcnkucHJvdG90eXBlLmNyZWF0ZVNldHRpbmdzID0gZnVuY3Rpb24gKHNldHRpbmdzS2V5KSB7CiAgICAgICAgdmFyIHNldHRpbmcgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJUUiIpOwogICAgICAgIHZhciBjb250ZW50ID0gbmV3IFBvcHVwQ29udGVudCgpOwogICAgICAgIHZhciBkYXRhID0gU0VSVkVSWyJzZXR0aW5ncyJdW3NldHRpbmdzS2V5XTsKICAgICAgICBzd2l0Y2ggKHNldHRpbmdzS2V5KSB7CiAgICAgICAgICAgIC8vIFRleHRlaW5nYWJlbgogICAgICAgICAgICBjYXNlICJ1cGRhdGUiOgogICAgICAgICAgICAgICAgdmFyIHRkTGVmdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlREIik7CiAgICAgICAgICAgICAgICB0ZExlZnQuaW5uZXJIVE1MID0gInt7LnNldHRpbmdzLnVwZGF0ZS50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAidXBkYXRlIiwgZGF0YS50b1N0cmluZygpKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgicGxhY2Vob2xkZXIiLCAie3suc2V0dGluZ3MudXBkYXRlLnBsYWNlaG9sZGVyfX0iKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgib25jaGFuZ2UiLCAiamF2YXNjcmlwdDogdGhpcy5jbGFzc05hbWUgPSAnY2hhbmdlZCciKTsKICAgICAgICAgICAgICAgIHRkUmlnaHQuYXBwZW5kQ2hpbGQoaW5wdXQpOwogICAgICAgICAgICAgICAgc2V0dGluZy5hcHBlbmRDaGlsZCh0ZExlZnQpOwogICAgICAgICAgICAgICAgc2V0dGluZy5hcHBlbmRDaGlsZCh0ZFJpZ2h0KTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlICJiYWNrdXAucGF0aCI6CiAgICAgICAgICAgICAgICB2YXIgdGRMZWZ0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHRkTGVmdC5pbm5lckhUTUwgPSAie3suc2V0dGluZ3MuYmFja3VwUGF0aC50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAiYmFja3VwLnBhdGgiLCBkYXRhKTsKICAgICAgICAgICAgICAgIGlucHV0LnNldEF0dHJpYnV0ZSgicGxhY2Vob2xkZXIiLCAie3suc2V0dGluZ3MuYmFja3VwUGF0aC5wbGFjZWhvbGRlcn19Iik7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoIm9uY2hhbmdlIiwgImphdmFzY3JpcHQ6IHRoaXMuY2xhc3NOYW1lID0gJ2NoYW5nZWQnIik7CiAgICAgICAgICAgICAgICB0ZFJpZ2h0LmFwcGVuZENoaWxkKGlucHV0KTsKICAgICAgICAgICAgICAgIHNldHRpbmcuYXBwZW5kQ2hpbGQodGRMZWZ0KTsKICAgICAgICAgICAgICAgIHNldHRpbmcuYXBwZW5kQ2hpbGQodGRSaWdodCk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSAidGVtcC5wYXRoIjoKICAgICAgICAgICAgICAgIHZhciB0ZExlZnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJURCIpOwogICAgICAgICAgICAgICAgdGRMZWZ0LmlubmVySFRNTCA9ICJ7ey5zZXR0aW5ncy50ZW1wUGF0aC50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAidGVtcC5wYXRoIiwgZGF0YSk7CiAgICAgICAgICAgICAgICBpbnB1dC5zZXRBdHRyaWJ1dGUoInBsYWNlaG9sZGVyIiwgInt7LnNldHRpbmdzLnRtcFBhdGgucGxhY2Vob2xkZXJ9fSIpOwogICAgICAgICAgICAgICAgaW5wdXQuc2V0QXR0cmlidXRlKCJvbmNoYW5nZSIsICJqYXZhc2NyaXB0OiB0aGlzLmNsYXNzTmFtZSA9ICdjaGFuZ2VkJyIpOwogICAgICAgICAgICAgICAgdGRSaWdodC5hcHBlbmRDaGlsZChpbnB1dCk7CiAgICAgICAgICAgICAgICBzZXR0aW5nLmFwcGVuZENoaWxkKHRkTGVmdCk7CiAgICAgICAgICAgICAgICBzZXR0aW5nLmFwcGVuZENoaWxkKHRkUmlnaHQpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgInVzZXIuYWdlbnQiOgogICAgICAgICAgICAgICAgdmFyIHRkTGVmdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoIlREIik7CiAgICAgICAgICAgICAgICB0ZExlZnQuaW5uZXJIVE1MID0gInt7LnNldHRpbmdzLnVzZXJBZ2VudC50aXRsZX19IiArICI6IjsKICAgICAgICAgICAgICAgIHZhciB0ZFJpZ2h0ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiVEQiKTsKICAgICAgICAgICAgICAgIHZhciBpbnB1dCA9IGNvbnRlbnQuY3JlYXRlSW5wdXQoInRleHQiLCAidXNlci5hZ2VudCIsIGRhdGEpOwogICAgICAgICAgICAgICAgaW5wdXQuc2V0QXR0cmlidXRlKCJwbGFjZWhvbGRlciIsICJ7ey5zZXR0aW5ncy51c2VyQWdlbnQucGxhY2Vob2xkZXJ9fSIpOwogICAgICAgICAgICAgICAgaW5wdXQuc2V0QXR0cmlidXRlKCJvbmNoYW5nZSIsICJqYXZhc2NyaXB0OiB0aGlzLmNsYXNzTmFtZSA9ICdjaGFuZ2Line truncated
webUI["html/js/base.js"] = "dmFyIGNvbmZpZyAgICAgICAgPSBuZXcgT2JqZWN0KCk7CnZhciBtZW51ICAgICAgICAgID0gbmV3IE9iamVjdCgpOwp2YXIgc3ViTWVudSAgICAgICA9IG5ldyBPYmplY3QoKTsKdmFyIGFjdGl2ZVN0cmVhbXMgPSBuZXcgT2JqZWN0KCk7CnZhciB4RVBHICAgICAgICAgID0gbmV3IE9iamVjdCgpOwp2YXIgdXNlcnMgICAgICAgICA9IG5ldyBPYmplY3QoKTsKdmFyIGxvZyAgICAgICAgICAgPSBuZXcgT2JqZWN0KCk7CnZhciB1bmRvICAgICAgICAgID0gbmV3IE9iamVjdCgpOwp2YXIgd2ViU29ja2V0cyAgICA9IHRydWU7CnZhciBjbG9zZUxvZywgdmVyc2lvbiwgYWN0aXZlTWVudTsKdmFyIGNvbHVtblRvU29ydCAgPSAwCgoKaWYgKHdpbmRvdy5XZWJTb2NrZXQgPT09IHVuZGVmaW5lZCkgewogIGFsZXJ0KCJZb3VyIGJyb3dzZXIgZG9lcyBub3Qgc3VwcG9ydCBXZWJTb2NrZXRzIik7CiAgd2ViU29ja2V0cyA9IGZhbHNlOwp9IAoKZnVuY3Rpb24gcGFnZVJlYWR5KCkgewogIHZhciBkYXRhID0gbmV3IE9iamVjdCgpOwogIGRhdGFbImNtZCJdID0gImdldFNlcnZlckNvbmZpZyI7CiAgeFRlVmUoZGF0YSk7CiAgLy9zaG93TG9hZGluZ1NjcmVlbihmYWxzZSk7CgogIHZhciByZXNpemVIYW5kbGUgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgib3BlblN0cmVhbXMiKTsKICB2YXIgYm94ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoIm15U3RyZWFtc0JveCIpOwogIHJlc2l6ZUhhbmRsZS5hZGRFdmVudExpc3RlbmVyKCJtb3VzZWRvd24iLCBpbml0aWFsaXNlUmVzaXplLCBmYWxzZSk7CgogIGZ1bmN0aW9uIGluaXRpYWxpc2VSZXNpemUoZSkgewogICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoIm1vdXNlbW92ZSIsIHN0YXJ0UmVzaXppbmcsIGZhbHNlKTsKICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJtb3VzZXVwIiwgc3RvcFJlc2l6aW5nLCBmYWxzZSk7CiAgfQoKICBmdW5jdGlvbiBzdGFydFJlc2l6aW5nKGUpIHsKICAgIGJveC5zdHlsZS5oZWlnaHQgPSAoZS5jbGllbnRZIC0gYm94Lm9mZnNldFRvcCkgKyAicHgiOwogICAgICAKICAgIHZhciBlbG0gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiYWxsU3RyZWFtcyIpOwogICAgaWYgKGUuY2xpZW50WSA+IDEyMCkgewogICAgICBlbG0uY2xhc3NOYW1lID0gInZpc2libGUiOyAKICAgIH0gZWxzZSB7CiAgICAgIGVsbS5jbGFzc05hbWUgPSAibm90VmlzaWJsZSI7IAogICAgfQoKICAgIGNhbGN1bGF0ZVdyYXBwZXJIZWlnaHQoKTsKCiAgfQogIGZ1bmN0aW9uIHN0b3BSZXNpemluZyhlKSB7CiAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdtb3VzZW1vdmUnLCBzdGFydFJlc2l6aW5nLCBmYWxzZSk7CiAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdtb3VzZXVwJywgc3RvcFJlc2l6aW5nLCBmYWxzZSk7CiAgICAgIGNhbGN1bGF0ZVdyYXBwZXJIZWlnaHQoKTsKICB9CgogIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJyZXNpemUiLCBmdW5jdGlvbigpewogICAgY2FsY3VsYXRlV3JhcHBlckhlaWdodCgpOwogIH0sIHRydWUpOwp9CgoKZnVuY3Rpb24gZ2V0T2JqS2V5cyhvYmopIHsKICB2YXIga2V5cyA9IG5ldyBBcnJheSgpOwoKICBmb3IgKHZhciBpIGluIG9iaikgewogICAgaWYgKG9iai5oYXNPd25Qcm9wZXJ0eShpKSkgewogICAgICBrZXlzLnB1c2goaSk7CiAgICB9CiAgfQoKICByZXR1cm4ga2V5czsKfQoKCmZ1bmN0aW9uIGNyZWF0ZUVsZW1lbnQoaXRlbSkgewogIC8vY29uc29sZS5sb2coaXRlbSk7CiAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGl0ZW1bIl9lbGVtZW50Il0pOwogIGlmIChpdGVtLmhhc093blByb3BlcnR5KCJfdGV4dCIpKSB7CiAgICAvL2VsZW1lbnQuaW5uZXJIVE1MID0gIjxwPiIgKyBpdGVtWyJfdGV4dCJdICsgIjwvcD4iOwogICAgZWxlbWVudC5pbm5lckhUTUwgPSBpdGVtWyJfdGV4dCJdOwogIH0KCiAgdmFyIGtleXMgPSBnZXRPYmpLZXlzKGl0ZW0pOwogIGZvciAodmFyIGkgPSAwOyBpIDwga2V5cy5sZW5ndGg7IGkrKykgewogICAgaWYgKGtleXNbaV0uY2hhckF0KDApICE9ICJfIikgewogICAgICAvL2NvbnNvbGUubG9nKGtleXNbaV0sIGl0ZW1ba2V5c1tpXV0pOwogICAgICBlbGVtZW50LnNldEF0dHJpYnV0ZShrZXlzW2ldLCBpdGVtW2tleXNbaV1dKTsKICAgIH0KICB9CgogIC8vY29uc29sZS5sb2coZWxlbWVudCk7CiAgcmV0dXJuIGVsZW1lbnQ7Cn0KCmZ1bmN0aW9uIG1vZGlmeU9wdGlvbihpZCwgb3B0aW9ucywgdmFsdWVzKSB7CiAgdmFyIHNlbGVjdCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKTsKICBzZWxlY3QuaW5uZXJIVE1MID0gIiI7CgogIGZvciAodmFyIGkgPSAwOyBpIDwgb3B0aW9ucy5sZW5ndGg7IGkrKykgewogIAogICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJPUFRJT04iKQogIAogICAgZWxlbWVudC52YWx1ZSA9IHZhbHVlc1tpXTsKICAgIGVsZW1lbnQuaW5uZXJIVE1MID0gb3B0aW9uc1tpXTsKCiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChpZCkuYXBwZW5kQ2hpbGQoZWxlbWVudCk7CiAgCiAgfQoKfQoKCmZ1bmN0aW9uIHN0YXJ0V2ViU29ja2V0KCkgewogIGlmICh3ZWJTb2NrZXRzID09IGZhbHNlKSB7CiAgICByZXR1cm47CiAgfQoKICAvL3dzLnNlbmQoJ3siY21kIjogImdldFNlcnZlckNvbmZpZzEifScpOwoKfQoKZnVuY3Rpb24gY2hlY2tFcnIob2JqKSB7CiAgLy9hbGVydChvYmpbImVyciJdKQogIC8vc2NyZWVuTG9nKG9ialsiZXJyIl0sICJlcnJvciIpCiAgY29uc29sZS5sb2cob2JqKTsKICB2YXIgbmV3T2JqID0gbmV3IE9iamVjdCgpOwogIHZhciBuZXdFcnIgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RXJyWyJrZXkiXSAgID0gIkVycm9yIjsKICBuZXdFcnJbInZhbHVlIl0gPSBvYmpbImVyciJdOwogIG5ld0VyclsidHlwZSJdICA9ICJlcnJvciI7CgogIG5ld09ialswXSA9IG5ld0VycgogIHNob3dMb2cobmV3T2JqKTsKICByZXR1cm4KfQoKZnVuY3Rpb24gc2NyZWVuTG9nKG1zZywgbXNnVHlwZSwgc2hvdykgewogIHJldHVybgogIGNsZWFyVGltZW91dChjbG9zZUxvZykKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNjcmVlbkxvZyIpOwogIHZhciBuZXdNc2cgPSBuZXcgT2JqZWN0KCk7CiAgCiAgbmV3TXNnWyJfZWxlbWVudCJdID0gIlAiOwogIAogIHN3aXRjaChtc2dUeXBlKSB7CiAgICBjYXNlICJlcnJvciI6ICAgbmV3TXNnWyJjbGFzcyJdID0gImVycm9yTXNnIjsgYnJlYWs7CiAgICBjYXNlICJ3YXJuaW5nIjogbmV3TXNnWyJjbGFzcyJdID0gIndhcm5pbmdNc2ciOyBicmVhazsKICAgIC8vZGVmYXVsdDogICAgICBuZXdNc2dbImNsYXNzIl0gPSAiaW5mb01zZyIKICB9CgogIG5ld01zZ1siX3RleHQiXSA9IG1zZzsKCiAgZGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3TXNnKSk7CgogIGRpdi5zY3JvbGxUb3AgPSBkaXYuc2Nyb2xsSGVpZ2h0OwoKICBpZiAoc2hvdyA9PSBmYWxzZSkgewogICAgcmV0dXJuOwogIH0KCiAgZGl2LmNsYXNzTmFtZSA9ICIiCiAgY2xvc2VMb2cgPSBzZXRUaW1lb3V0KGNsb3NlU2NyZWVuTG9nLCAxMDAwMCk7Cn0KCgpmdW5jdGlvbiBjbG9zZVNjcmVlbkxvZygpIHsKICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNjcmVlbkxvZyIpOwogIGRpdi5jbGFzc05hbWUgPSAic2NyZWVuTG9nSGlkZGVuIgp9CgpmdW5jdGlvbiBzaLine truncated
webUI["html/js/classes_ts.js"] = "dmFyIF9fZXh0ZW5kcyA9ICh0aGlzICYmIHRoaXMuX19leHRlbmRzKSB8fCAoZnVuY3Rpb24gKCkgewogICAgdmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbiAoZCwgYikgewogICAgICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHwKICAgICAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fAogICAgICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChiLmhhc093blByb3BlcnR5KHApKSBkW3BdID0gYltwXTsgfTsKICAgICAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTsKICAgIH07CiAgICByZXR1cm4gZnVuY3Rpb24gKGQsIGIpIHsKICAgICAgICBleHRlbmRTdGF0aWNzKGQsIGIpOwogICAgICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfQogICAgICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTsKICAgIH07Cn0pKCk7CnZhciBNYWluTWVudSA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIE1haW5NZW51KCkgewogICAgICAgIHRoaXMuRG9jdW1lbnRJRCA9ICJtYWluLW1lbnUiOwogICAgICAgIHRoaXMuSFRNTFRhZyA9ICJMSSI7CiAgICB9CiAgICBNYWluTWVudS5wcm90b3R5cGUuY3JlYXRlID0gZnVuY3Rpb24gKCkgewogICAgICAgIGNvbnNvbGUubG9nKHRoaXMuRG9jdW1lbnRJRCk7CiAgICB9OwogICAgcmV0dXJuIE1haW5NZW51Owp9KCkpOwp2YXIgTWFpbk1lbnVJdGVtID0gLyoqIEBjbGFzcyAqLyAoZnVuY3Rpb24gKF9zdXBlcikgewogICAgX19leHRlbmRzKE1haW5NZW51SXRlbSwgX3N1cGVyKTsKICAgIGZ1bmN0aW9uIE1haW5NZW51SXRlbSgpIHsKICAgICAgICByZXR1cm4gX3N1cGVyICE9PSBudWxsICYmIF9zdXBlci5hcHBseSh0aGlzLCBhcmd1bWVudHMpIHx8IHRoaXM7CiAgICB9CiAgICBNYWluTWVudUl0ZW0ucHJvdG90eXBlLmNyZWF0ZTIgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KHRoaXMuSFRNTFRhZyk7CiAgICAgICAgZWxlbWVudC5pbm5lclRleHQgPSB0aGlzLlZhbHVlOwogICAgICAgIGNvbnNvbGUubG9nKGVsZW1lbnQpOwogICAgfTsKICAgIHJldHVybiBNYWluTWVudUl0ZW07Cn0oTWFpbk1lbnUpKTsKZnVuY3Rpb24gcGFnZVJlYWR5KCkgewogICAgdmFyIGl0ZW0gPSBuZXcgTWFpbk1lbnVJdGVtKCk7CiAgICBpdGVtLlZhbHVlID0gIlRlc3QiOwogICAgaXRlbS5jcmVhdGUyKCk7Cn0K"
webUI["html/js/mapping-editor.js"] = "dmFyIG1hcHBpbmdFcnJvciAgPSBmYWxzZTsKdmFyIGJ1bGsgICAgICAgICAgPSBmYWxzZTsKdmFyIGJ1bGtFZGl0QWxsICAgPSBmYWxzZTsgCnZhciBzZWxlY3RPYmogICAgID0gbmV3IE9iamVjdCgpOwp2YXIgc2VhcmNoT2JqICAgICA9IG5ldyBPYmplY3QoKTsKCnZhciBidWxrSURzICAgICAgID0gbmV3IEFycmF5KCk7CnZhciBidWxrQ2hhbmdlT2JqID0gbmV3IE9iamVjdCgpOwoKZnVuY3Rpb24gY2hlY2tVbmRvKGtleSwgZWxtKSB7CiAgdmFyIHRtcCA9IG5ldyBPYmplY3QoKTsKICB0bXAgPSBlbG0KICBjb25zb2xlLmxvZygiLS0iKTsKICBpZiAodW5kby5oYXNPd25Qcm9wZXJ0eSgiZXBnTWFwcGluZyIpKSB7CiAgICB4RVBHWyJlcGdNYXBwaW5nIl0gPSBKU09OLnBhcnNlKEpTT04uc3RyaW5naWZ5KHVuZG9bImVwZ01hcHBpbmciXSkpOzsKICB9IGVsc2UgewogICAgdW5kb1siZXBnTWFwcGluZyJdID0gSlNPTi5wYXJzZShKU09OLnN0cmluZ2lmeShlbG0pKTsKICB9Cn0KCi8vdmFyIHBsZXhDYXRlZ29yaWVzID0gbmV3IEFycmF5KCItIiwgIkFjdGlvbiBzcG9ydHMiLCAiQWN0aW9uIiwgIkFkdWx0cyBvbmx5IiwgIkFkdmVudHVyZSIsICJBZXJvYmljcyIsICJBbmltYWxzIiwgIkFuaW1hdGVkIiwgIkFuaW1lIiwgIkFudGhvbG9neSIsICJBcmNoZXJ5IiwgIkFydCIsICJBcnRzL2NyYWZ0cyIsICJBdWN0aW9uIiwgIkF1dG8gcmFjaW5nIiwgIkF1dG8iLCAiQXZpYXRpb24iLCAiQXdhcmRzIiwgIkJhbGxldCIsICJCYXNlYmFsbCIsICJCYXNrZXRiYWxsIiwgIkJpY3ljbGUgcmFjaW5nIiwgIkJpY3ljbGUiLCAiQmlsbGlhcmRzIiwgIkJpb2dyYXBoeSIsICJCb2F0IHJhY2luZyIsICJCb2F0IiwgIkJvd2xpbmciLCAiQm94aW5nIiwgIkJ1cy4vZmluYW5jaWFsIiwgIkNoaWxkcmVuIiwgIkNvbGxlY3RpYmxlcyIsICJDb21lZHkgZHJhbWEiLCAiQ29tZWR5IiwgIkNvbW11bml0eSIsICJDb21wdXRlcnMiLCAiQ29uc3VtZXIiLCAiQ29va2luZyIsICJDcmltZSBkcmFtYSIsICJDcmltZSIsICJEYW5jZSIsICJEYXJrIGNvbWVkeSIsICJEZWJhdGUiLCAiRGl2aW5nIiwgIkRvY3VkcmFtYSIsICJEb2N1bWVudGFyeSIsICJEcmFtYSIsICJFZHVjYXRpb25hbCIsICJFbnRlcnRhaW5tZW50IiwgIkVudmlyb25tZW50IiwgIkVxdWVzdHJpYW4iLCAiRXJvdGljIiwgIkV2ZW50IiwgIkZhbnRhc3kiLCAiRmFzaGlvbiIsICJGZWF0dXJlIEZpbG0iLCAiRmlzaGluZyIsICJGb290YmFsbCIsICJHYW1lIHNob3ciLCAiR2FtaW5nIiwgIkdheS9sZXNiaWFuIiwgIkdvbGYiLCAiSGFuZGJhbGwiLCAiSGVhbHRoIiwgIkhpc3RvcmljYWwgZHJhbWEiLCAiSGlzdG9yeSIsICJIb2NrZXkiLCAiSG9saWRheSIsICJIb21lIGltcHJvdmVtZW50IiwgIkhvcnJvciIsICJIb3JzZSIsICJIb3VzZS9nYXJkZW4iLCAiSG93LXRvIiwgIkludGVydmlldyIsICJJbnRsIHNvY2NlciIsICJMYXciLCAiTWFydGlhbCBhcnRzIiwgIk1lZGljYWwiLCAiTWlsaXRhcnkiLCAiTWluaXNlcmllcyIsICJNaXhlZCBtYXJ0aWFsIGFydHMiLCAiTW90b3JjeWNsZSByYWNpbmciLCAiTW90b3JjeWNsZSIsICJNb3RvcnNwb3J0cyIsICJNb3VudGFpbiBiaWtpbmciLCAiTXVzaWMiLCAiTXVzaWNhbCBjb21lZHkiLCAiTXVzaWNhbCIsICJNeXN0ZXJ5IiwgIk5hdHVyZSIsICJOZXdzIiwgIk5ld3NtYWdhemluZSIsICJPbHltcGljcyIsICJPcGVyYSIsICJPdXRkb29ycyIsICJQYXJhZGUiLCAiUGFyYW5vcm1hbCIsICJQYXJlbnRpbmciLCAiUGVyZm9ybWluZyBhcnRzIiwgIlBsYXlvZmYgc3BvcnRzIiwgIlBva2VyIiwgIlBvbGl0aWNzIiwgIlBybyB3cmVzdGxpbmciLCAiUHVibGljIGFmZmFpcnMiLCAiUmVhbGl0eSIsICJSZWxpZ2lvdXMiLCAiUm9kZW8iLCAiUm9sbGVyIGRlcmJ5IiwgIlJvbWFuY2UiLCAiUm9tYW50aWMgY29tZWR5IiwgIlJ1Z2J5IiwgIlJ1bm5pbmciLCAiU2FpbGluZyIsICJTY2llbmNlIGZpY3Rpb24iLCAiU2NpZW5jZSIsICJTZWxmIGltcHJvdmVtZW50IiwgIlNlcmllcyIsICJTaG9vdGluZyIsICJTaG9wcGluZyIsICJTaG9ydCBGaWxtIiwgIlNpdGNvbSIsICJTa2lpbmciLCAiU25vb2tlciIsICJTb2FwIiwgIlNvY2NlciIsICJTcGVjaWFsIiwgIlNwb3J0cyIsICJzcG9ydHMiLCAiU3BvcnRzIGV2ZW50IiwgIlNwb3J0cyBub24tZXZlbnQiLCAiU3BvcnRzIHRhbGsiLCAiU3RhbmR1cCIsICJTdXJmaW5nIiwgIlN1c3BlbnNlIiwgIlRWIE1vdmllIiwgIlRhbGsiLCAiVGVjaG5vbG9neSIsICJUZW5uaXMiLCAiVGhlYXRlciIsICJUaHJpbGxlciIsICJUcmFjay9maWVsZCIsICJUcmF2ZWwiLCAiVHJpYXRobG9uIiwgIlZhcmlldHkiLCAiVm9sbGV5YmFsbCIsICJXYXIiLCAiV2F0ZXJzcG9ydHMiLCAiV2VhdGhlciIsICJXZXN0ZXJuIiwgIldyZXN0bGluZyIsICJZYWNodCByYWNpbmciLCAibW92aWUiLCAic2VyaWVzIiwgInNwb3J0cyIsICJ0dnNob3ciKTsKdmFyIHBsZXhDYXRlZ29yaWVzVmFsdWVzID0gbmV3IEFycmF5KCItIiwgIktpZHMiLCAiTmV3cyIsICJNb3ZpZSIsICJTZXJpZXMiLCAiU3BvcnRzIikKdmFyIHBsZXhDYXRlZ29yaWVzT3B0aW9uID0gbmV3IEFycmF5KCItIiwgIktpZHMgKEVtYnkgb25seSkiLCAiTmV3cyIsICJNb3ZpZSIsICJTZXJpZXMiLCAiU3BvcnRzIikKCgpmdW5jdGlvbiBvcGVuTWFwcGluZ0VkaXRvcihlbG0pIHsKICB2YXIgY29sdW1uVG9Tb3J0ICA9IDEKCiAgY2hlY2tVbmRvKCJlcGdNYXBwaW5nIiwgeEVQR1siZXBnTWFwcGluZyJdKQoKICB2YXIgbmV3RGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInNldHRpbmdzIik7CiAgCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICA9ICJIUiI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gICAgICAgPSAiU2F2ZSI7CiAgbmV3RW50cnlbIm9uY2xpY2siXSAgICAgPSAic2F2ZVhFUEcoKSI7CiAgbmV3RGl2LmFwcGVuZENoaWxkKGNyZWF0ZUVsZW1lbnQobmV3RW50cnkpKTsKCiAgdmFyIG5ld0VudHJ5ID0gbmV3IE9iamVjdCgpOwogIG5ld0VudHJ5WyJfZWxlbWVudCJdICAgID0gIklOUFVUIjsKICBuZXdFbnRyeVsidHlwZSJdICAgICAgICA9ICJidXR0b24iOwogIG5ld0VudHJ5WyJjbGFzcyJdICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbInZhbHVlIl0gICAgICAgPSAiQnVsayBFZGl0IjsKICBuZXdFbnRyeVsib25jbGljayJdICAgICA9ICJidWxrRWRpdCgpIjsKICBuZXdEaXYuYXBwZW5kQ2hpbGQoY3JlYXRlRWxlbWVudChuZXdFbnRyeSkpOwoKICB2YXIgbmV3RW50cnkgPSBuZXcgT2JqZWN0KCk7CiAgbmV3RW50cnlbIl9lbGVtZW50Il0gICAgPSAiSU5QVVQiOwogIG5ld0VudHJ5WyJ0eXBlIl0gICAgICAgID0gImJ1dHRvbiI7CiAgbmV3RW50cnlbImNsYXNzIl0gICAgICAgPSAiYnV0dG9uIjsKICBuZXdFbnRyeVsidmFsdWUiXSAgICAgICA9ICJTaG93IFhFUEciOwogIG5ld0VudHJ5WyJvbmNsaWNrIl0gICAgID0gInNob3dYRVBHKCkiOwogIG5ld0Rpdi5hcHBlbmRDaGlLine truncated
webUI["html/js/network_ts.js"] = "dmFyIFNlcnZlciA9IC8qKiBAY2xhc3MgKi8gKGZ1bmN0aW9uICgpIHsKICAgIGZ1bmN0aW9uIFNlcnZlcihjbWQpIHsKICAgICAgICB0aGlzLmNtZCA9IGNtZDsKICAgIH0KICAgIFNlcnZlci5wcm90b3R5cGUucmVxdWVzdCA9IGZ1bmN0aW9uIChkYXRhKSB7CiAgICAgICAgaWYgKFNFUlZFUl9DT05ORUNUSU9OID09IHRydWUpIHsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICBTRVJWRVJfQ09OTkVDVElPTiA9IHRydWU7CiAgICAgICAgY29uc29sZS5sb2coZGF0YSk7CiAgICAgICAgaWYgKHRoaXMuY21kICE9ICJ1cGRhdGVMb2ciKSB7CiAgICAgICAgICAgIHNob3dFbGVtZW50KCJsb2FkaW5nIiwgdHJ1ZSk7CiAgICAgICAgICAgIFVORE8gPSBuZXcgT2JqZWN0KCk7CiAgICAgICAgfQogICAgICAgIHN3aXRjaCAod2luZG93LmxvY2F0aW9uLnByb3RvY29sKSB7CiAgICAgICAgICAgIGNhc2UgImh0dHA6IjoKICAgICAgICAgICAgICAgIHRoaXMucHJvdG9jb2wgPSAid3M6Ly8iOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgImh0dHBzOi8vIjoKICAgICAgICAgICAgICAgIHRoaXMucHJvdG9jb2wgPSAid3NzOi8vIjsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgIH0KICAgICAgICB2YXIgdXJsID0gdGhpcy5wcm90b2NvbCArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICI6IiArIHdpbmRvdy5sb2NhdGlvbi5wb3J0ICsgIi9kYXRhLyIgKyAiP1Rva2VuPSIgKyBnZXRDb29raWUoIlRva2VuIik7CiAgICAgICAgZGF0YVsiY21kIl0gPSB0aGlzLmNtZDsKICAgICAgICB2YXIgd3MgPSBuZXcgV2ViU29ja2V0KHVybCk7CiAgICAgICAgd3Mub25vcGVuID0gZnVuY3Rpb24gKCkgewogICAgICAgICAgICBXU19BVkFJTEFCTEUgPSB0cnVlOwogICAgICAgICAgICBjb25zb2xlLmxvZygiUkVRVUVTVCAoSlMpOiIpOwogICAgICAgICAgICBjb25zb2xlLmxvZyhkYXRhKTsKICAgICAgICAgICAgY29uc29sZS5sb2coIlJFUVVFU1Q6IChKU09OKSIpOwogICAgICAgICAgICBjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShkYXRhKSk7CiAgICAgICAgICAgIHRoaXMuc2VuZChKU09OLnN0cmluZ2lmeShkYXRhKSk7CiAgICAgICAgfTsKICAgICAgICB3cy5vbmVycm9yID0gZnVuY3Rpb24gKGUpIHsKICAgICAgICAgICAgY29uc29sZS5sb2coIk5vIHdlYnNvY2tldCBjb25uZWN0aW9uIHRvIHhUZVZlIGNvdWxkIGJlIGVzdGFibGlzaGVkLiBDaGVjayB5b3VyIG5ldHdvcmsgY29uZmlndXJhdGlvbi4iKTsKICAgICAgICAgICAgU0VSVkVSX0NPTk5FQ1RJT04gPSBmYWxzZTsKICAgICAgICAgICAgaWYgKFdTX0FWQUlMQUJMRSA9PSBmYWxzZSkgewogICAgICAgICAgICAgICAgYWxlcnQoIk5vIHdlYnNvY2tldCBjb25uZWN0aW9uIHRvIHhUZVZlIGNvdWxkIGJlIGVzdGFibGlzaGVkLiBDaGVjayB5b3VyIG5ldHdvcmsgY29uZmlndXJhdGlvbi4iKTsKICAgICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgd3Mub25tZXNzYWdlID0gZnVuY3Rpb24gKGUpIHsKICAgICAgICAgICAgU0VSVkVSX0NPTk5FQ1RJT04gPSBmYWxzZTsKICAgICAgICAgICAgc2hvd0VsZW1lbnQoImxvYWRpbmciLCBmYWxzZSk7CiAgICAgICAgICAgIGNvbnNvbGUubG9nKCJSRVNQT05TRToiKTsKICAgICAgICAgICAgdmFyIHJlc3BvbnNlID0gSlNPTi5wYXJzZShlLmRhdGEpOwogICAgICAgICAgICBjb25zb2xlLmxvZyhyZXNwb25zZSk7CiAgICAgICAgICAgIGlmIChyZXNwb25zZS5oYXNPd25Qcm9wZXJ0eSgidG9rZW4iKSkgewogICAgICAgICAgICAgICAgZG9jdW1lbnQuY29va2llID0gIlRva2VuPSIgKyByZXNwb25zZVsidG9rZW4iXTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAocmVzcG9uc2VbInN0YXR1cyJdID09IGZhbHNlKSB7CiAgICAgICAgICAgICAgICBhbGVydChyZXNwb25zZVsiZXJyIl0pOwogICAgICAgICAgICAgICAgaWYgKHJlc3BvbnNlLmhhc093blByb3BlcnR5KCJyZWxvYWQiKSkgewogICAgICAgICAgICAgICAgICAgIGxvY2F0aW9uLnJlbG9hZCgpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChyZXNwb25zZS5oYXNPd25Qcm9wZXJ0eSgibG9nb1VSTCIpKSB7CiAgICAgICAgICAgICAgICB2YXIgZGl2ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImNoYW5uZWwtaWNvbiIpOwogICAgICAgICAgICAgICAgZGl2LnZhbHVlID0gcmVzcG9uc2VbImxvZ29VUkwiXTsKICAgICAgICAgICAgICAgIGRpdi5jbGFzc05hbWUgPSAiY2hhbmdlZCI7CiAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgIH0KICAgICAgICAgICAgc3dpdGNoIChkYXRhWyJjbWQiXSkgewogICAgICAgICAgICAgICAgY2FzZSAidXBkYXRlTG9nIjoKICAgICAgICAgICAgICAgICAgICBTRVJWRVJbImxvZyJdID0gcmVzcG9uc2VbImxvZyJdOwogICAgICAgICAgICAgICAgICAgIGlmIChkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiY29udGVudF9sb2ciKSkgewogICAgICAgICAgICAgICAgICAgICAgICBzaG93TG9ncyhmYWxzZSk7CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgICAgIGRlZmF1bHQ6CiAgICAgICAgICAgICAgICAgICAgU0VSVkVSID0gbmV3IE9iamVjdCgpOwogICAgICAgICAgICAgICAgICAgIFNFUlZFUiA9IHJlc3BvbnNlOwogICAgICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChyZXNwb25zZS5oYXNPd25Qcm9wZXJ0eSgib3Blbk1lbnUiKSkgewogICAgICAgICAgICAgICAgdmFyIG1lbnUgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChyZXNwb25zZVsib3Blbk1lbnUiXSk7CiAgICAgICAgICAgICAgICBtZW51LmNsaWNrKCk7CiAgICAgICAgICAgICAgICBzaG93RWxlbWVudCgicG9wdXAiLCBmYWxzZSk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKHJlc3BvbnNlLmhhc093blByb3BlcnR5KCJvcGVuTGluayIpKSB7CiAgICAgICAgICAgICAgICB3aW5kb3cubG9jYXRpb24gPSByZXNwb25zZVsib3BlbkxpbmsiXTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAocmVzcG9uc2UuaGFzT3duUHJvcGVydHkoImFsZXJ0IikpIHsKICAgICAgICAgICAgICAgIGFsZXJ0KHJlc3BvbnNlWyJhbGVydCJdKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAocmVzcG9uc2UuaGFzT3duUHJvcGVydHkoInJlbG9hZCIpKSB7CiAgICAgICAgICAgICAgICBsb2NhdGlvbi5yZWxvYWQoKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAocmVzcG9uc2UuaGFzT3duUHJvcGVydHkoIndpemFyZCIpKSB7CiAgICAgICAgICAgICAgICBjcmVhdGVMYXlvdXQoKTsKICAgICAgICAgICAgICAgIGNvbmZpZ3VyYXRpb25XaXphcmRbcmVzcG9uc2VbIndpemFyZCJdXS5jcmVhdGVXaXphcmQoKTsKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICBjcmVhdGVMYXlvdXQoKTsKICAgICAgICB9OwogICAgfTsKICAgIHJldHVybiBTZXJ2ZXI7Cn0oKSk7CmZ1bmN0aW9uIGdldENvb2tpZShuYW1lKSB7CiAgICB2YXIgdmFsdWUgPSAiOyAiICsgZG9jdW1lbnQuY29va2llOwogICAgdmFyIHBhcnRzID0gdmFLine truncated
}
+205 -185
View File
@@ -4,17 +4,42 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"net/http"
"os"
"strconv"
"strings"
"../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,11 +55,23 @@ 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)
showHighlight(fmt.Sprintf("Web Interface:%s://%s:%s/web/", System.ServerProtocol.WEB, System.IPAddress, Settings.Port))
var ips = len(System.IPAddressesV4) + len(System.IPAddressesV6) - 1
switch ips {
case 0:
showHighlight(fmt.Sprintf("Web Interface:%s://%s:%s/web/", System.ServerProtocol.WEB, System.IPAddress, Settings.Port))
case 1:
showHighlight(fmt.Sprintf("Web Interface:%s://%s:%s/web/ | xTeVe is also available via the other %d IP.", System.ServerProtocol.WEB, System.IPAddress, Settings.Port, ips))
default:
showHighlight(fmt.Sprintf("Web Interface:%s://%s:%s/web/ | xTeVe is also available via the other %d IP's.", System.ServerProtocol.WEB, System.IPAddress, Settings.Port, len(System.IPAddressesV4)+len(System.IPAddressesV6)-1))
}
if err = http.ListenAndServe(":"+port, nil); err != nil {
ShowError(err, 1001)
@@ -68,7 +105,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 {
@@ -101,7 +138,6 @@ func Index(w http.ResponseWriter, r *http.Request) {
httpStatusError(w, r, 500)
return
}
// Stream : Web Server /stream/
@@ -117,20 +153,37 @@ 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)
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 Settings.Buffer {
if Settings.Buffer == true {
case "-":
showInfo(fmt.Sprintf("Buffer:false [%s]", Settings.Buffer))
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]", Settings.Buffer))
default:
showInfo(fmt.Sprintf("Buffer:true [%s]", Settings.Buffer))
}
if Settings.Buffer != "-" {
showInfo(fmt.Sprintf("Buffer Size:%d KB", Settings.BufferSize))
}
@@ -140,49 +193,24 @@ func Stream(w http.ResponseWriter, r *http.Request) {
// Prüfen ob der Buffer verwendet werden soll
switch Settings.Buffer {
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: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{}
@@ -210,7 +238,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))
@@ -235,11 +263,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/
@@ -259,7 +293,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/
@@ -279,7 +312,6 @@ func DataImages(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write(content)
return
}
// WS : Web Sockets /ws/
@@ -291,16 +323,21 @@ func WS(w http.ResponseWriter, r *http.Request) {
var newToken string
if r.Header.Get("Origin") != "http://"+r.Host {
httpStatusError(w, r, 403)
/*
if r.Header.Get("Origin") != "http://"+r.Host {
httpStatusError(w, r, 403)
return
}
*/
// 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)
return
}
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
if err != nil {
ShowError(err, 0)
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
}
setGlobalDomain(r.Host)
for {
@@ -310,23 +347,14 @@ func WS(w http.ResponseWriter, r *http.Request) {
return
}
if System.ConfigurationWizard == false {
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
@@ -359,7 +387,6 @@ func WS(w http.ResponseWriter, r *http.Request) {
ShowError(err, 1022)
} else {
return
break
}
return
@@ -374,7 +401,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
}
@@ -447,7 +474,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
file, errNew := xteveBackup()
err = errNew
if err == nil {
response.OpenLink = System.URLBase + "/download/" + file
response.OpenLink = fmt.Sprintf("%s://%s/download/%s", System.ServerProtocol.WEB, System.Domain, file)
}
case "xteveRestore":
@@ -457,9 +484,10 @@ func WS(w http.ResponseWriter, r *http.Request) {
if len(request.Base64) > 0 {
newWebURL, err := xteveRestore(request.Base64)
newWebURL, err := xteveRestoreFromWeb(request.Base64)
if err != nil {
ShowError(err, 000)
response.Alert = err.Error()
}
if err == nil {
@@ -514,9 +542,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))
}
@@ -525,12 +553,11 @@ 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)
}
setGlobalDomain(r.Host)
response = setDefaultResponseData(response, true)
if System.ConfigurationWizard == true {
if System.ConfigurationWizard {
response.ConfigurationWizard = System.ConfigurationWizard
}
@@ -542,66 +569,62 @@ func WS(w http.ResponseWriter, r *http.Request) {
}
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)
}
var file string
} else {
if System.ScanInProgress == 0 {
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" {
if len(Data.Streams.All) == 0 && System.ScanInProgress == 0 {
System.ConfigurationWizard = true
}
switch System.ConfigurationWizard {
case true:
file = requestFile + "configuration.html"
Settings.AuthenticationWEB = false
case false:
file = requestFile + "index.html"
if !Settings.WizardCompleted && len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 {
System.ConfigurationWizard = true
}
}
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
@@ -626,7 +649,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
}
@@ -636,33 +659,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
}
@@ -675,59 +697,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/
@@ -791,13 +802,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
}
@@ -807,7 +817,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)
@@ -823,7 +833,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:
@@ -867,8 +877,6 @@ func API(w http.ResponseWriter, r *http.Request) {
case "status":
fmt.Println("-----------------------------")
os.Exit(0)
response.VersionXteve = System.Version
response.VersionAPI = System.APIVersion
response.StreamsActive = int64(len(Data.Streams.Active))
@@ -922,12 +930,26 @@ func API(w http.ResponseWriter, r *http.Request) {
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))
@@ -940,7 +962,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) {
@@ -964,7 +985,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
switch System.Branch {
case "master":
defaults.ClientInfo.Version = fmt.Sprintf("%s", System.Version)
defaults.ClientInfo.Version = System.Version
default:
defaults.ClientInfo.Version = fmt.Sprintf("%s (%s)", System.Version, System.Build)
@@ -972,7 +993,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
}
if data == true {
if data {
defaults.Users, _ = authentication.GetAllUserData()
//defaults.DVR = System.DVRAddress
@@ -981,7 +1002,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 {
@@ -990,8 +1011,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)
}
@@ -999,7 +1020,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
@@ -1013,7 +1034,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) {
+139
View File
@@ -0,0 +1,139 @@
package src
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"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)")
}
}
+457 -199
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
# Style checks deliberately kept out:
# ST1005 - error strings are shown verbatim in the web UI; wording is intentional
# ST1000/ST1003/ST1016/ST1020/ST1021/ST1022 - staticcheck's own defaults
checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
+169
View File
@@ -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: reviewed with maintainer 2026-09-25/26, all open questions resolved, ready to start Phase 0. Checklist lives 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.**
+83
View File
@@ -0,0 +1,83 @@
# xTeVe improvement checklist
Detailed rationale, file references and effort estimates: `tasks/improvement-plan.md`.
Status: Phase 0 committed on branch `improvements` 2026-09-26.
## 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`
- [ ] Real mutex for screen log
- [ ] `http.Server` with timeouts; timeouts on all outbound clients
- [ ] imgcache: download outside the lock
- [ ] Atomic write helper (temp + fsync + rename)
- [x] Buffer: headers set before `WriteHeader`, bogus `Content-Length:` header gone, segments flushed to the client as they arrive. Others in 2b
- [ ] Error hygiene: no-op closures, `os.Exit`/`panic` outside main, ignored results
## 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
- [ ] Version-tagged images (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
- [ ] Persistent websocket with request IDs, queue, backoff reconnect
- [ ] Section-level re-render instead of full `createLayout()`
- [ ] Virtualised mapping table
- [ ] Split `menu_ts.ts`; `addEventListener`; data-driven settings rows
- [ ] Accessibility follow-ups; light theme via `prefers-color-scheme`
- [ ] Consolidate CSS layers
## Phase 6: Optional features
- [ ] Regex filters
- [ ] Per-playlist tuner limit and buffer choice
- [ ] Bulk channel edit
- [ ] Versioned backup/restore
- [ ] Dummy EPG durations
## 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.
+4 -3
View File
@@ -16,6 +16,8 @@ function login() {
if (value.length == 0) {
inputs[i].style.borderColor = "red"
err = true
} else {
inputs[i].style.borderColor = ""
}
data[key] = value
@@ -30,11 +32,10 @@ function login() {
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}}"
document.getElementById("err").innerHTML = "Password does not match"
return
}
@@ -44,4 +45,4 @@ function login() {
form.submit();
}
}
+141 -36
View File
@@ -5,27 +5,28 @@ var SEARCH_MAPPING = new Object()
var UNDO = new Object()
var SERVER_CONNECTION = false
var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
var LAST_BULK_CHECKBOX:HTMLInputElement = null
// 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}}"))
menuItems.push(new MainMenuItem("playlist", "Playlist", "m3u.png", "Local or remote playlists"))
//menuItems.push(new MainMenuItem("pmsID", "PMS ID", "number.png", ""))
menuItems.push(new MainMenuItem("filter", "Filter", "filter.png", "Filter playlist"))
menuItems.push(new MainMenuItem("xmltv", "XMLTV", "xmltv.png", "Local or remote XMLTV files"))
menuItems.push(new MainMenuItem("mapping", "Mapping", "mapping.png", "Map playlist channels to EPG channels"))
menuItems.push(new MainMenuItem("users", "Users", "users.png", "User management"))
menuItems.push(new MainMenuItem("settings", "Settings", "settings.png", "Settings"))
menuItems.push(new MainMenuItem("log", "Log", "log.png", "Log"))
menuItems.push(new MainMenuItem("logout", "Logout", "logout.png", "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"))
settingsCategory.push(new SettingsCategoryItem("General", "tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("Files", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("Streaming", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
settingsCategory.push(new SettingsCategoryItem("Backup", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("Authentication", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))
function showPopUpElement(elm) {
@@ -52,7 +53,44 @@ function showElement(elmID, type) {
case false: cssClass = "none"; break;
}
document.getElementById(elmID).className = cssClass;
var element = document.getElementById(elmID)
if (element == null) {
return
}
element.className = cssClass;
}
function setConnectionState(state:string, text:string = "") {
var label:string = text
if (label == undefined || label.length == 0) {
switch (state) {
case "online":
label = "Connected"
break
case "busy":
label = "Syncing"
break
case "offline":
label = "Offline"
break
default:
label = "Connecting"
break
}
}
var indicator = document.getElementById("connection-indicator")
if (indicator == null) {
return
}
indicator.className = "status-" + state
indicator.innerText = label
}
function changeButtonAction(element, buttonID, attribute) {
@@ -145,6 +183,59 @@ function getAllSelectedChannels():string[] {
return channels
}
function scheduleChannelRangeSelection(checkbox:HTMLInputElement, event:MouseEvent) {
var shiftPressed = false
if (event != undefined && event.shiftKey == true) {
shiftPressed = true
}
// Run after the native checkbox toggle so we copy the final checked state.
setTimeout(function() {
selectChannelRange(checkbox, shiftPressed)
}, 0)
}
function selectChannelRange(checkbox:HTMLInputElement, shiftPressed:boolean) {
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
return
}
var table = document.getElementById("content_table")
if (table == null) {
return
}
var trs = table.getElementsByTagName("TR")
var visibleCheckboxes:HTMLInputElement[] = new Array()
for (var i = 1; i < trs.length; i++) {
if ((trs[i] as HTMLElement).style.display != "none") {
var bulkCheckbox = (trs[i] as HTMLTableRowElement).querySelector("input.bulk") as HTMLInputElement
if (bulkCheckbox != null) {
visibleCheckboxes.push(bulkCheckbox)
}
}
}
var currentIndex = visibleCheckboxes.indexOf(checkbox)
var previousIndex = -1
if (LAST_BULK_CHECKBOX != null) {
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX)
}
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
var start = Math.min(previousIndex, currentIndex)
var end = Math.max(previousIndex, currentIndex)
for (var i = start; i <= end; i++) {
visibleCheckboxes[i].checked = checkbox.checked
}
}
LAST_BULK_CHECKBOX = checkbox
}
function selectAllChannels() {
var bulk:Boolean = false
@@ -174,6 +265,7 @@ function selectAllChannels() {
}
LAST_BULK_CHECKBOX = null
return
}
@@ -198,6 +290,7 @@ function bulkEdit() {
(rows[i] as HTMLInputElement).checked = false
}
LAST_BULK_CHECKBOX = null
return
}
@@ -249,7 +342,7 @@ function sortTable(column) {
default: console.log(x.childNodes[0].tagName);
}
if (xValue == "" || xValue == NaN) {
if (xValue == "" || Number.isNaN(xValue)) {
xValue = i
sortObj[i] = rows[i];
@@ -310,7 +403,7 @@ function createSearchObj() {
var data = SERVER["xepg"]["epgMapping"]
var channels = getObjKeys(data)
var channelKeys:string[] = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title"]
var channelKeys:string[] = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"]
channels.forEach(id => {
@@ -331,7 +424,17 @@ function createSearchObj() {
} else {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " "
if (key == "x-xmltv-file") {
var xmltvFile = getValueFromProviderFile(data[id][key], "xmltv", "name")
if (xmltvFile != undefined) {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + xmltvFile + " "
}
} else {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " "
}
}
@@ -370,17 +473,19 @@ function searchInMapping() {
function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")){
var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
var elm = document.getElementById("box-wrapper");
if (elm != null && content != null){
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;
var contentTop = content.getBoundingClientRect().top
var freeSpace = window.innerHeight - contentTop - 26
if (freeSpace < 180) {
freeSpace = 180
}
elm.style.height = window.innerHeight - elementsHeight + "px";
elm.style.height = freeSpace + "px";
}
@@ -397,7 +502,7 @@ function changeChannelNumber(element) {
var channels = getObjKeys(data)
if (isNaN(newNumber)) {
alert("{{.alert.invalidChannelNumber}}")
alert("Invalid channel number")
return
}
@@ -409,7 +514,7 @@ function changeChannelNumber(element) {
})
for (var i = 0; i < channelNumbers.length; i++) {
if (channelNumbers.indexOf(newNumber) == -1) {
break
}
@@ -423,7 +528,7 @@ function changeChannelNumber(element) {
}
}
data[dbID]["x-channelID"] = newNumber.toString()
element.value = newNumber
@@ -462,7 +567,7 @@ function toggleChannelStatus(id:string) {
var checkbox = (document.getElementById("active") as HTMLInputElement)
status = (checkbox).checked
}
var ids:string[] = getAllSelectedChannels()
if (ids.length == 0) {
@@ -483,9 +588,9 @@ function toggleChannelStatus(id:string) {
alert(channel["x-name"] + ": Missing XMLTV file / channel")
checkbox.checked = false
}
channel["x-active"] = false
}
break
@@ -523,7 +628,7 @@ function restore() {
restore.onchange = function() {
var filename = (restore as HTMLInputElement).files[0].name
var check = confirm("File: " + filename + "\n{{.confirm.restore}}");
var check = confirm("File: " + filename + "\nAll data will be replaced with those from the backup. Should the files be restored?");
if (check == true) {
@@ -622,9 +727,9 @@ function checkUndo(key:string) {
UNDO[key] = JSON.parse(JSON.stringify(SERVER["xepg"][key]));
}
break;
default:
break;
}
@@ -647,9 +752,9 @@ function sortSelect(elem) {
elem.options[i] = tmpAry[i];
if(elem.options[i].value == selectedValue) newSelectedIndex = i;
}
elem.selectedIndex = newSelectedIndex; // Set new selected index after sorting
return;
}
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
tsc *.ts --outDir ../html/js/
+16 -8
View File
@@ -43,7 +43,7 @@ class WizardItem extends WizardCategory {
select.id = key
doc.appendChild(select)
description = "{{.wizard.tuner.description}}"
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."
break;
@@ -56,27 +56,29 @@ class WizardItem extends WizardCategory {
select.id = key
doc.appendChild(select)
description = "{{.wizard.epgSource.description}}"
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)"
break
case "m3u":
var input = content.createInput("text", key, "")
input.setAttribute("placeholder", "File path or URL of the M3U")
input.setAttribute("class", "wizard")
input.id = key
doc.appendChild(input)
description = "{{.wizard.m3u.description}}"
description = "Local or remote playlists"
break
case "xmltv":
var input = content.createInput("text", key, "")
input.setAttribute("placeholder", "File path or URL of the XMLTV")
input.setAttribute("class", "wizard")
input.id = key
doc.appendChild(input)
description = "{{.wizard.xmltv.description}}"
description = "Local or remote XMLTV file"
break
@@ -140,6 +142,12 @@ function saveWizard() {
name = (config[i] as HTMLInputElement).name
value = (config[i] as HTMLInputElement).value
if (value.length == 0) {
var msg = name.toUpperCase() + ": " + "Missing input"
alert(msg)
return
}
wizard[name] = value
break
}
@@ -163,7 +171,7 @@ function saveWizard() {
// 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}}"))
configurationWizard.push(new WizardItem("tuner", "Number of tuners"))
configurationWizard.push(new WizardItem("epgSource", "EPG Source"))
configurationWizard.push(new WizardItem("m3u", "M3U Playlist"))
configurationWizard.push(new WizardItem("xmltv", "XMLTV File"))
+12 -4
View File
@@ -16,7 +16,7 @@ class Log {
element.className = "debugMsg"
}
element.innerHTML = entry
element.textContent = entry
return element
}
@@ -29,9 +29,18 @@ function showLogs(bottom:boolean) {
var logs = SERVER["log"]["log"]
var div = document.getElementById("content_log")
var wrapper = document.getElementById("box-wrapper") as HTMLElement
var shouldStickToBottom:boolean = bottom
div.innerHTML = ""
if (wrapper != null && shouldStickToBottom == false) {
var distanceToBottom:number = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight
if (distanceToBottom < 80) {
shouldStickToBottom = true
}
}
var keys = getObjKeys(logs)
keys.forEach(logID => {
@@ -44,9 +53,8 @@ function showLogs(bottom:boolean) {
setTimeout(function(){
if (bottom == true) {
if (shouldStickToBottom == true && wrapper != null) {
var wrapper = document.getElementById("box-wrapper");
wrapper.scrollTop = wrapper.scrollHeight;
}
@@ -62,4 +70,4 @@ function resetLogs() {
var server:Server = new Server(cmd)
server.request(data)
}
}
+439 -160
View File
File diff suppressed because it is too large. Load diff
+81 -26
View File
@@ -14,34 +14,83 @@ class Server {
SERVER_CONNECTION = true
console.log(data)
if (this.cmd != "updateLog") {
showElement("loading", true)
UNDO = new Object()
setConnectionState("busy")
}
switch(window.location.protocol) {
case "http:":
this.protocol = "ws://"
break
case "https://":
case "https:":
this.protocol = "wss://"
break
}
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token")
var wsHost:string = window.location.host
if (wsHost == undefined || wsHost.length < 1) {
wsHost = window.location.hostname
}
// The session cookie (HttpOnly) is sent with the websocket handshake.
var url = this.protocol + wsHost + "/data/"
data["cmd"] = this.cmd
var requestCmd:string = data["cmd"]
var ws = new WebSocket(url)
var isLogUpdate:boolean = data["cmd"] == "updateLog"
var responseReceived:boolean = false
var requestFinished:boolean = false
var timeoutMs:number = 12000
var requestTimeout:number
var finishRequest = function(state:string, responseSuccess:boolean = false):void {
if (requestFinished == true) {
return
}
requestFinished = true
SERVER_CONNECTION = false
window.clearTimeout(requestTimeout)
if (responseSuccess == true) {
if (state == "online") {
WS_FAILURE_COUNT = 0
}
} else {
WS_FAILURE_COUNT++
}
if (isLogUpdate == false) {
showElement("loading", false)
}
if (state != "") {
setConnectionState(state)
}
}
requestTimeout = window.setTimeout(function() {
console.log("Websocket request timed out.")
var timeoutState:string = "offline"
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
timeoutState = "idle"
}
finishRequest(timeoutState, false)
try {
ws.close()
} catch (err) {
console.log(err)
}
}, timeoutMs)
ws.onopen = function() {
WS_AVAILABLE = true
console.log("REQUEST (JS):");
console.log(data)
console.log("REQUEST: (JSON)");
console.log(JSON.stringify(data))
if (data["cmd"] != "updateLog") {
setConnectionState("busy")
}
this.send(JSON.stringify(data));
@@ -50,9 +99,13 @@ class Server {
ws.onerror = function(e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.")
SERVER_CONNECTION = false
var errorState:string = "offline"
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
errorState = "idle"
}
finishRequest(errorState, false)
if (WS_AVAILABLE == false) {
if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
}
@@ -60,20 +113,13 @@ class Server {
ws.onmessage = function (e) {
SERVER_CONNECTION = false
showElement("loading", false)
responseReceived = true
finishRequest("online", true)
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) {
setConnectionState("offline")
alert(response["err"])
@@ -135,13 +181,22 @@ class Server {
createLayout()
}
ws.onclose = function() {
if (responseReceived == true) {
return
}
var closeState:string = "offline"
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
closeState = "idle"
}
finishRequest(closeState, false)
}
}
}
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
var WS_FAILURE_COUNT:number = 0
+236 -86
View File
@@ -21,11 +21,11 @@ class SettingsCategory {
// Texteingaben
case "update":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.update.title}}" + ":"
tdLeft.innerHTML = "Schedule for updating (Playlist, XMLTV, Backup)" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "update", data.toString())
input.setAttribute("placeholder", "{{.settings.update.placeholder}}")
input.setAttribute("placeholder", "0000,1000,2000")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
@@ -35,11 +35,11 @@ class SettingsCategory {
case "backup.path":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.backupPath.title}}" + ":"
tdLeft.innerHTML = "Location for automatic backups" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "backup.path", data)
input.setAttribute("placeholder", "{{.settings.backupPath.placeholder}}")
input.setAttribute("placeholder", "/mnt/data/backup/xteve/")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
@@ -49,11 +49,11 @@ class SettingsCategory {
case "temp.path":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tempPath.title}}" + ":"
tdLeft.innerHTML = "Location for the temporary files" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "temp.path", data)
input.setAttribute("placeholder", "{{.settings.tmpPath.placeholder}}")
input.setAttribute("placeholder", "/tmp/xteve/")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
@@ -63,11 +63,40 @@ class SettingsCategory {
case "user.agent":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.userAgent.title}}" + ":"
tdLeft.innerHTML = "User Agent" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "user.agent", data)
input.setAttribute("placeholder", "{{.settings.userAgent.placeholder}}")
input.setAttribute("placeholder", "xTeVe")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "plex.url":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "Plex Server URL" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "plex.url", data)
input.setAttribute("placeholder", "http://plex-host:32400")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "plex.token":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "Plex API Token" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("password", "plex.token", data)
input.setAttribute("placeholder", "Plex X-Plex-Token")
input.setAttribute("autocomplete", "off")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
@@ -77,11 +106,67 @@ class SettingsCategory {
case "buffer.timeout":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"
tdLeft.innerHTML = "Timeout for new client connections" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "buffer.timeout", data)
input.setAttribute("placeholder", "{{.settings.bufferTimeout.placeholder}}")
input.setAttribute("placeholder", "100")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "ffmpeg.path":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "FFmpeg Binary Path" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "ffmpeg.path", data)
input.setAttribute("placeholder", "/path/to/ffmpeg")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "ffmpeg.options":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "FFmpeg Options" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "ffmpeg.options", data)
input.setAttribute("placeholder", "Leave blank to set default settings")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "vlc.path":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "VLC / CVLC Binary Path" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "vlc.path", data)
input.setAttribute("placeholder", "/path/to/cvlc")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "vlc.options":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "VLC / CVLC Options" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "vlc.options", data)
input.setAttribute("placeholder", "Leave blank to set default settings")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
@@ -92,7 +177,7 @@ class SettingsCategory {
// Checkboxen
case "authentication.web":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationWEB.title}}" + ":"
tdLeft.innerHTML = "WEB Authentication" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -106,7 +191,7 @@ class SettingsCategory {
case "authentication.pms":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationPMS.title}}" + ":"
tdLeft.innerHTML = "PMS Authentication" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -120,7 +205,7 @@ class SettingsCategory {
case "authentication.m3u":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationM3U.title}}" + ":"
tdLeft.innerHTML = "M3U Authentication" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -134,7 +219,7 @@ class SettingsCategory {
case "authentication.xml":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationXML.title}}" + ":"
tdLeft.innerHTML = "XML Authentication" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -148,7 +233,7 @@ class SettingsCategory {
case "authentication.api":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationAPI.title}}" + ":"
tdLeft.innerHTML = "API Authentication" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -162,7 +247,7 @@ class SettingsCategory {
case "files.update":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.filesUpdate.title}}" + ":"
tdLeft.innerHTML = "Updates all files at startup" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -176,7 +261,7 @@ class SettingsCategory {
case "cache.images":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.cacheImages.title}}" + ":"
tdLeft.innerHTML = "Image Caching" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -190,35 +275,7 @@ class SettingsCategory {
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}}" + ":"
tdLeft.innerHTML = "Replace missing program images" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -232,7 +289,7 @@ class SettingsCategory {
case "api":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.api.title}}" + ":"
tdLeft.innerHTML = "API Interface" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -244,10 +301,40 @@ class SettingsCategory {
setting.appendChild(tdRight)
break
// Select
case "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
case "use_plexAPI":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "Use Plex API Refresh" + ":"
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 "xepg.missing.epg.mode":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "Missing EPG Handling" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["Strict (deactivate channels)", "Relaxed (keep active / dummy guide)"]
var values:any[] = ["strict", "relaxed"]
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 "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "Number of Tuners" + ":"
var tdRight = document.createElement("TD")
var text = new Array()
@@ -268,7 +355,7 @@ class SettingsCategory {
case "epgSource":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.epgSource.title}}" + ":"
tdLeft.innerHTML = "EPG Source" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["PMS", "XEPG"]
@@ -284,7 +371,7 @@ class SettingsCategory {
case "backup.keep":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.backupKeep.title}}" + ":"
tdLeft.innerHTML = "Number of backups to keep" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["5", "10", "20", "30", "40", "50"]
@@ -300,7 +387,7 @@ class SettingsCategory {
case "buffer.size.kb":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.bufferSize.title}}" + ":"
tdLeft.innerHTML = "Buffer Size" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"]
@@ -314,105 +401,168 @@ class SettingsCategory {
setting.appendChild(tdRight)
break
case "buffer":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "Stream Buffer" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["No Buffer (Client connects to the streaming server)", "xTeVe: (xTeVe connects to the streaming server)", "FFmpeg: (FFmpeg connects to the streaming server)", "VLC: (VLC connects to the streaming server)"]
var values:any[] = ["-", "xteve", "ffmpeg", "vlc"]
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 "udpxy":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "UDPxy address" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "udpxy", data)
input.setAttribute("placeholder", "host:port")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
}
return setting
}
createDescription(settingsKey:string):any {
var description = document.createElement("TR")
var text:string
switch (settingsKey) {
case "authentication.web":
text = "{{.settings.authenticationWEB.description}}"
text = "Access to the web interface only possible with credentials."
break
case "authentication.m3u":
text = "{{.settings.authenticationM3U.description}}"
text = "Downloading the xteve.m3u file via an HTTP request is only possible with authentication."
break
case "authentication.pms":
text = "{{.settings.authenticationPMS.description}}"
text = "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."
break
case "authentication.xml":
text = "{{.settings.authenticationXML.description}}"
text = "Downloading the xteve.xml file via an HTTP request is only possible with authentication"
break
case "authentication.api":
if (SERVER["settings"]["authentication.web"] == true) {
text = "{{.settings.authenticationAPI.description}}"
text = "Access to the API interface is only possible with authentication."
}
break
case "xteveAutoUpdate":
text = "{{.settings.xteveAutoUpdate.description}}"
break
case "backup.keep":
text = "{{.settings.backupKeep.description}}"
text = "Number of backups to keep. Older backups are automatically deleted."
break
case "backup.path":
text = "{{.settings.backupPath.description}}"
text = "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."
break
case "temp.path":
text = "{{.settings.tempPath.description}}"
text = "Location for the buffer files."
break
case "buffer":
text = "{{.settings.streamBuffering.description}}"
text = "Functions of the buffer:<br>- The stream is passed from xTeVe, FFmpeg or VLC to Plex, Emby or M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support<br>- RTP / RTPS support (only FFmpeg or VLC)<br>- Re-streaming<br>- Separate tuner limit for each playlist"
break
case "buffer.size.kb":
text = "{{.settings.bufferSize.description}}"
text = "Buffer size in MB.<br>M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used."
break
case "buffer.timeout":
text = "{{.settings.bufferTimeout.description}}"
text = "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds."
break
case "user.agent":
text = "{{.settings.userAgent.description}}"
text = "User Agent for HTTP requests. For every HTTP connection, this value is used for the user agent. Should only be changed if xTeVe is blocked."
break
case "plex.url":
text = "Base URL of your Plex server. Example: http://192.168.1.10:32400"
break
case "plex.token":
text = "Plex token used for authenticated API calls to refresh your DVR guide."
break
case "ffmpeg.path":
text = "Path to FFmpeg binary."
break
case "ffmpeg.options":
text = "FFmpeg options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings."
break
case "vlc.path":
text = "Path to VLC / CVLC binary."
break
case "vlc.options":
text = "VLC / CVLC options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings."
break
case "epgSource":
text = "{{.settings.epgSource.description}}"
text = "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)"
break
case "tuner":
text = "{{.settings.tuner.description}}"
text = "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."
break
case "update":
text = "{{.settings.update.description}}"
text = "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated. Leave this field empty if no updates are to be carried out."
break
case "api":
text = "{{.settings.api.description}}"
text = "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>"
break
case "use_plexAPI":
text = "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates."
break
case "files.update":
text = "{{.settings.filesUpdate.description}}"
text = "Updates all playlists, tuner and XMLTV files at startup."
break
case "cache.images":
text = "{{.settings.cacheImages.description}}"
text = "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."
break
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"
case "xepg.replace.missing.images":
text = "If the poster in the XMLTV program is missing, the channel logo will be used."
break
case "xepg.missing.epg.mode":
text = "Strict: channels are deactivated when XMLTV mappings disappear.<br>Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved."
break
case "udpxy":
text = "The address of your UDPxy server. If set, and the channel URLs in the m3u is multicast, xTeVe will rewrite it so that it is accessed via the UDPxy service."
break
default:
text = ""
break
}
var tdLeft = document.createElement("TD")
@@ -425,7 +575,7 @@ class SettingsCategory {
description.appendChild(tdLeft)
description.appendChild(tdRight)
return description
}
@@ -445,12 +595,12 @@ class SettingsCategoryItem extends SettingsCategory {
createCategory():void {
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(",")
@@ -491,7 +641,7 @@ function showSettings() {
for (let i = 0; i < settingsCategory.length; i++) {
settingsCategory[i].createCategory()
}
}
function saveSettings() {
@@ -553,7 +703,7 @@ function saveSettings() {
break
}
}
var data = new Object()
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "None",
"outFile": "../html/js/app.js",
"strict": false,
"noEmitOnError": true,
"skipLibCheck": true,
"newLine": "lf",
"removeComments": false
},
"files": [
"network_ts.ts",
"menu_ts.ts",
"settings_ts.ts",
"logs_ts.ts",
"base_ts.ts",
"configuration_ts.ts",
"authentication_ts.ts"
]
}
+49 -32
View File
@@ -13,50 +13,38 @@ import (
"runtime"
"strings"
"./src"
"xteve/src"
)
// GitHubStruct : GitHub Account. Über diesen Account werden die Updates veröffentlicht
type GitHubStruct struct {
Branch string
Repo string
Update bool
User string
}
// GitHub : GitHub Account
// If you want to fork this project, enter your Github account here. This prevents a newer version of xTeVe from updating your version.
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]
*/
// Name : Programname
// Name : Programmname
const Name = "xTeVe"
// Version : Version, die Build Nummer wird in der main func geparst.
const Version = "2.0.0.0000"
// Can be overwritten at build time: -ldflags "-X main.Version=..."
var Version = "2.2.0.0201"
// DBVersion : Datanbank Version
const DBVersion = "2.1.0"
// APIVersion : API Version
const APIVersion = "1.1.0"
// Dev : Aktiviert den Entwicklungsmodus. Für den Webserver werden dann die lokalen Dateien verwendet.
const Dev = false
var homeDirectory = fmt.Sprintf("%s%s.%s%s", src.GetUserHomeDirectory(), string(os.PathSeparator), strings.ToLower(Name), string(os.PathSeparator))
var samplePath = fmt.Sprintf("%spath%sto%sxteve%s", string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator))
var sampleRestore = fmt.Sprintf("%spath%sto%sfile%s", string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator))
var configFolder = flag.String("config", "", ": Config Folder ["+samplePath+"] (default: "+homeDirectory+")")
var port = flag.String("port", "", ": Server port [34400] (default: 34400)")
var restore = flag.String("restore", "", ": Restore from backup ["+sampleRestore+"xteve_backup.zip]")
var gitBranch = flag.String("branch", "", ": Git Branch [master|beta] (default: master)")
var debug = flag.Int("debug", 0, ": Debug level [0 - 3] (default: 0)")
var info = flag.Bool("info", false, ": Show system info")
var h = flag.Bool("h", false, ": Show help")
// Aktiviert den Entwicklungsmodus. Für den Webserver werden dann die lokalen Dateien verwendet.
var dev = flag.Bool("dev", false, ": Activates the developer mode, the source code must be available. The local files for the web interface are used.")
func main() {
// Build-Nummer von der Versionsnummer trennen
@@ -64,10 +52,8 @@ func main() {
var system = &src.System
system.APIVersion = APIVersion
system.Branch = GitHub.Branch
system.Build = build[len(build)-1:][0]
system.Dev = Dev
system.GitHub = GitHub
system.DBVersion = DBVersion
system.Name = Name
system.Version = strings.Join(build[0:len(build)-1], ".")
@@ -115,6 +101,24 @@ func main() {
return
}
system.Dev = *dev
// Systeminformationen anzeigen
if *info {
system.Flag.Info = true
err := src.Init()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
src.ShowSystemInfo()
return
}
// Webserver Port
if len(*port) > 0 {
system.Flag.Port = *port
@@ -138,13 +142,26 @@ func main() {
system.Folder.Config = *configFolder
}
err := src.Init()
if err != nil {
src.ShowError(err, 0)
// Backup wiederherstellen
if len(*restore) > 0 {
system.Flag.Restore = *restore
err := src.Init()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
err = src.XteveRestoreFromCLI(*restore)
if err != nil {
src.ShowError(err, 0)
}
os.Exit(0)
}
err = src.BinaryUpdate()
err := src.Init()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)