You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 14:11:59 +01:00
Compare commits
17 Commits
d0a26d2346
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e14b3f8d11
|
|||
|
42a20330af
|
|||
|
c0d1fee257
|
|||
|
5d524eba56
|
|||
|
648ef00bed
|
|||
|
8a53bf1027
|
|||
|
722277c41f
|
|||
|
ee25214160
|
|||
|
1d04719580
|
|||
|
1f70b58295
|
|||
|
3080468415
|
|||
|
0ae484529d
|
|||
|
|
098b6ba4e9
|
||
|
03ff93cd39
|
|||
|
ebe9c8bbe8
|
|||
|
df215edf9e
|
|||
|
410c39a6b8
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
node_modules/
|
||||
/*.json
|
||||
/serverlog.txt
|
||||
/serverlog*.txt
|
||||
/web/js/plugins/
|
||||
/libraries/
|
||||
/plugins/*
|
||||
|
||||
@@ -31,9 +31,7 @@ FM-DX Webserver is a cross-platform web server designed for FM DXers who want to
|
||||
|
||||
This project utilizes these libraries:
|
||||
|
||||
- [3LAS](https://github.com/jojobond/3LAS) library by JoJoBond for Low Latency Audio Streaming.
|
||||
- [flat-flags](https://github.com/luishdez/flat-flags) library by luishdez for RDS country flags.
|
||||
- [librdsparser](https://github.com/kkonradpl/librdsparser) library by Konrad Kosmatka for RDS parsing.
|
||||
|
||||
All of these libraries are already bundled with the webserver.
|
||||
|
||||
|
||||
2
index.js
2
index.js
@@ -3,7 +3,7 @@ require('./server/index.js');
|
||||
/**
|
||||
* FM-DX Webserver
|
||||
*
|
||||
* Github repo: https://github.com/NoobishSVK/fm-dx-webserver
|
||||
* Github repo: https://github.com/KubaPro010/fm-dx-webserver
|
||||
* Server files: /server
|
||||
* Client files (web): /web
|
||||
* Plugin files: /plugins
|
||||
|
||||
592
package-lock.json
generated
592
package-lock.json
generated
@@ -1,25 +1,26 @@
|
||||
{
|
||||
"name": "fm-dx-webserver",
|
||||
"version": "1.3.12",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fm-dx-webserver",
|
||||
"version": "1.3.12",
|
||||
"version": "1.4.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "2.0.0",
|
||||
"body-parser": "2.2.0",
|
||||
"ejs": "3.1.10",
|
||||
"express": "5.1.0",
|
||||
"express-session": "1.18.2",
|
||||
"ffmpeg-static": "5.2.0",
|
||||
"@mapbox/node-pre-gyp": "2.0.3",
|
||||
"body-parser": "2.2.2",
|
||||
"ejs": "4.0.1",
|
||||
"express": "5.2.1",
|
||||
"express-session": "1.19.0",
|
||||
"ffmpeg-static": "5.3.0",
|
||||
"figlet": "^1.10.0",
|
||||
"http": "0.0.1-security",
|
||||
"koffi": "2.7.2",
|
||||
"net": "1.0.2",
|
||||
"serialport": "12.0.0",
|
||||
"ws": "8.18.1"
|
||||
"serialport": "13.0.0",
|
||||
"ws": "8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@derhuerst/http-basic": {
|
||||
@@ -49,9 +50,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz",
|
||||
"integrity": "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz",
|
||||
"integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3",
|
||||
@@ -148,28 +149,30 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz",
|
||||
"integrity": "sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz",
|
||||
"integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"@serialport/parser-readline": "11.0.0",
|
||||
"debug": "4.3.4",
|
||||
"node-addon-api": "7.0.0",
|
||||
"node-gyp-build": "4.6.0"
|
||||
"@serialport/parser-readline": "12.0.0",
|
||||
"debug": "4.4.0",
|
||||
"node-addon-api": "8.3.0",
|
||||
"node-gyp-build": "4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz",
|
||||
"integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==",
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
|
||||
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -178,11 +181,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz",
|
||||
"integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==",
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
|
||||
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "11.0.0"
|
||||
"@serialport/parser-delimiter": "12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -192,11 +196,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -208,19 +213,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/node-gyp-build": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
|
||||
"integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@serialport/bindings-interface": {
|
||||
"version": "1.2.2",
|
||||
@@ -231,136 +227,148 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-byte-length": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-12.0.0.tgz",
|
||||
"integrity": "sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz",
|
||||
"integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-cctalk": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-12.0.0.tgz",
|
||||
"integrity": "sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz",
|
||||
"integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-delimiter": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
|
||||
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz",
|
||||
"integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-inter-byte-timeout": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-12.0.0.tgz",
|
||||
"integrity": "sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz",
|
||||
"integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-packet-length": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-12.0.0.tgz",
|
||||
"integrity": "sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz",
|
||||
"integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-readline": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
|
||||
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz",
|
||||
"integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "12.0.0"
|
||||
"@serialport/parser-delimiter": "13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-ready": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-12.0.0.tgz",
|
||||
"integrity": "sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz",
|
||||
"integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-regex": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-12.0.0.tgz",
|
||||
"integrity": "sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz",
|
||||
"integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-slip-encoder": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-12.0.0.tgz",
|
||||
"integrity": "sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz",
|
||||
"integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-spacepacket": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-12.0.0.tgz",
|
||||
"integrity": "sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz",
|
||||
"integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-12.0.0.tgz",
|
||||
"integrity": "sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz",
|
||||
"integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"debug": "4.3.4"
|
||||
"debug": "4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -372,9 +380,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "10.17.60",
|
||||
@@ -435,54 +444,49 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
"qs": "^6.14.1",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -503,12 +507,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
@@ -559,21 +566,6 @@
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
@@ -583,27 +575,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
@@ -709,18 +689,18 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
|
||||
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
"jake": "^10.9.1"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">=0.12.18"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -786,18 +766,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
@@ -828,22 +809,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz",
|
||||
"integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"cookie": "~0.7.2",
|
||||
"cookie-signature": "~1.0.7",
|
||||
"debug": "~2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.1.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"safe-buffer": "~5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie": {
|
||||
@@ -884,10 +869,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ffmpeg-static": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz",
|
||||
"integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
||||
"integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@derhuerst/http-basic": "^8.2.0",
|
||||
"env-paths": "^2.2.0",
|
||||
@@ -898,31 +884,31 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"node_modules/figlet": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
|
||||
"integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"commander": "^14.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"figlet": "bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">= 17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",
|
||||
"integrity": "sha512-ct/ckWBV/9Dg3MlvCXsLcSUyoWwv9mCKqlhLNB2DAuXR/NZolSXlQqP5dyy6guWlPXBhodZyZ5lGPQcbQDxrEQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
@@ -1040,14 +1026,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1078,18 +1056,23 @@
|
||||
"integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g=="
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-response-object": {
|
||||
@@ -1134,15 +1117,19 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
@@ -1165,14 +1152,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.8.7",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
|
||||
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"minimatch": "^3.1.2"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
@@ -1185,7 +1172,8 @@
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.7.2.tgz",
|
||||
"integrity": "sha512-AWcsEKETQuELxK0Wq/aXDkDiNFFY41TxZQSrKm2Nd6HO/KTHeohPOOIlh2OfQnBXJbRjx5etpWt8cbqMUZo2sg==",
|
||||
"hasInstallScript": true
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
@@ -1250,14 +1238,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
@@ -1316,9 +1308,13 @@
|
||||
"integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ=="
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz",
|
||||
"integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA=="
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
|
||||
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
@@ -1339,6 +1335,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
|
||||
@@ -1417,6 +1424,12 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -1438,9 +1451,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -1470,18 +1483,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
||||
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
@@ -1621,38 +1634,40 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serialport": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialport/-/serialport-12.0.0.tgz",
|
||||
"integrity": "sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz",
|
||||
"integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/binding-mock": "10.2.2",
|
||||
"@serialport/bindings-cpp": "12.0.1",
|
||||
"@serialport/parser-byte-length": "12.0.0",
|
||||
"@serialport/parser-cctalk": "12.0.0",
|
||||
"@serialport/parser-delimiter": "12.0.0",
|
||||
"@serialport/parser-inter-byte-timeout": "12.0.0",
|
||||
"@serialport/parser-packet-length": "12.0.0",
|
||||
"@serialport/parser-readline": "12.0.0",
|
||||
"@serialport/parser-ready": "12.0.0",
|
||||
"@serialport/parser-regex": "12.0.0",
|
||||
"@serialport/parser-slip-encoder": "12.0.0",
|
||||
"@serialport/parser-spacepacket": "12.0.0",
|
||||
"@serialport/stream": "12.0.0",
|
||||
"debug": "4.3.4"
|
||||
"@serialport/bindings-cpp": "13.0.0",
|
||||
"@serialport/parser-byte-length": "13.0.0",
|
||||
"@serialport/parser-cctalk": "13.0.0",
|
||||
"@serialport/parser-delimiter": "13.0.0",
|
||||
"@serialport/parser-inter-byte-timeout": "13.0.0",
|
||||
"@serialport/parser-packet-length": "13.0.0",
|
||||
"@serialport/parser-readline": "13.0.0",
|
||||
"@serialport/parser-ready": "13.0.0",
|
||||
"@serialport/parser-regex": "13.0.0",
|
||||
"@serialport/parser-slip-encoder": "13.0.0",
|
||||
"@serialport/parser-spacepacket": "13.0.0",
|
||||
"@serialport/stream": "13.0.0",
|
||||
"debug": "4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/serialport/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -1664,9 +1679,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialport/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.0",
|
||||
@@ -1761,9 +1777,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -1776,17 +1793,6 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||
@@ -1899,9 +1905,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fm-dx-webserver",
|
||||
"version": "1.3.12",
|
||||
"version": "1.4.0a",
|
||||
"description": "FM DX Webserver",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -12,16 +12,17 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "2.0.0",
|
||||
"body-parser": "2.2.0",
|
||||
"ejs": "3.1.10",
|
||||
"express": "5.1.0",
|
||||
"express-session": "1.18.2",
|
||||
"ffmpeg-static": "5.2.0",
|
||||
"@mapbox/node-pre-gyp": "2.0.3",
|
||||
"body-parser": "2.2.2",
|
||||
"ejs": "4.0.1",
|
||||
"express": "5.2.1",
|
||||
"express-session": "1.19.0",
|
||||
"ffmpeg-static": "5.3.0",
|
||||
"figlet": "^1.10.0",
|
||||
"http": "0.0.1-security",
|
||||
"koffi": "2.7.2",
|
||||
"net": "1.0.2",
|
||||
"serialport": "12.0.0",
|
||||
"ws": "8.18.1"
|
||||
"serialport": "13.0.0",
|
||||
"ws": "8.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,20 @@ const { serverConfig } = require('./server_config');
|
||||
const { logChat } = require('./console');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
function heartbeat() { // WebSocket heartbeat helper
|
||||
this.isAlive = true;
|
||||
}
|
||||
|
||||
function createChatServer(storage) {
|
||||
if (!serverConfig.webserver.chatEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (!serverConfig.webserver.chatEnabled) return null;
|
||||
|
||||
const chatWss = new WebSocket.Server({ noServer: true });
|
||||
|
||||
chatWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
ws.isAlive = true;
|
||||
ws.on('pong', heartbeat);
|
||||
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
@@ -23,26 +28,22 @@ function createChatServer(storage) {
|
||||
storage.chatHistory.forEach((message) => {
|
||||
const historyMessage = { ...message, history: true };
|
||||
|
||||
if (!request.session?.isAdminAuthenticated) {
|
||||
delete historyMessage.ip;
|
||||
}
|
||||
|
||||
if (!request.session?.isAdminAuthenticated) delete historyMessage.ip;
|
||||
ws.send(JSON.stringify(historyMessage));
|
||||
});
|
||||
|
||||
const ipMessage = {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'clientIp',
|
||||
ip: clientIp,
|
||||
admin: request.session?.isAdminAuthenticated
|
||||
};
|
||||
}));
|
||||
|
||||
ws.send(JSON.stringify(ipMessage));
|
||||
|
||||
const userCommands = {};
|
||||
let lastWarn = { time: 0 };
|
||||
|
||||
ws.on('message', (message) => {
|
||||
helpers.antispamProtection(
|
||||
message = helpers.antispamProtection(
|
||||
message,
|
||||
clientIp,
|
||||
ws,
|
||||
@@ -50,9 +51,12 @@ function createChatServer(storage) {
|
||||
lastWarn,
|
||||
userCommandHistory,
|
||||
'5',
|
||||
'chat'
|
||||
'chat',
|
||||
512
|
||||
);
|
||||
|
||||
if(!message) return;
|
||||
|
||||
let messageData;
|
||||
|
||||
try {
|
||||
@@ -62,60 +66,59 @@ function createChatServer(storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Chat message:", messageData);
|
||||
|
||||
delete messageData.admin;
|
||||
delete messageData.ip;
|
||||
delete messageData.time;
|
||||
|
||||
if (messageData.nickname != null) {
|
||||
messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
|
||||
}
|
||||
if (messageData.nickname != null) messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
|
||||
|
||||
messageData.ip = clientIp;
|
||||
|
||||
const now = new Date();
|
||||
messageData.time =
|
||||
String(now.getHours()).padStart(2, '0') +
|
||||
":" +
|
||||
String(now.getMinutes()).padStart(2, '0');
|
||||
messageData.time = String(now.getHours()).padStart(2, '0') + ":" + String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
|
||||
|
||||
if (request.session?.isAdminAuthenticated === true) {
|
||||
messageData.admin = true;
|
||||
}
|
||||
|
||||
if (messageData.nickname?.length > 32) {
|
||||
messageData.nickname = messageData.nickname.substring(0, 32);
|
||||
}
|
||||
|
||||
if (messageData.message?.length > 255) {
|
||||
messageData.message = messageData.message.substring(0, 255);
|
||||
}
|
||||
if (request.session?.isAdminAuthenticated === true) messageData.admin = true;
|
||||
if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32);
|
||||
if (messageData.message?.length > 255) messageData.message = messageData.message.substring(0, 255);
|
||||
|
||||
storage.chatHistory.push(messageData);
|
||||
if (storage.chatHistory.length > 50) {
|
||||
storage.chatHistory.shift();
|
||||
}
|
||||
if (storage.chatHistory.length > 50) storage.chatHistory.shift();
|
||||
|
||||
logChat(messageData);
|
||||
|
||||
chatWss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
const responseMessage = { ...messageData };
|
||||
|
||||
if (!request.session?.isAdminAuthenticated) {
|
||||
delete responseMessage.ip;
|
||||
}
|
||||
if (!request.session?.isAdminAuthenticated) delete responseMessage.ip;
|
||||
|
||||
client.send(JSON.stringify(responseMessage));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
ws.isAlive = false;
|
||||
});
|
||||
});
|
||||
|
||||
return chatWss; // ← VERY IMPORTANT
|
||||
/**
|
||||
* We will not always be receiving data, so some proxies may terminate the connection, this prevents it.
|
||||
*/
|
||||
const interval = setInterval(() => {
|
||||
chatWss.clients.forEach((ws) => {
|
||||
if (ws.isAlive === false) return ws.terminate();
|
||||
ws.isAlive = false;
|
||||
ws.ping();
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
chatWss.on('close', () => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
return chatWss;
|
||||
}
|
||||
|
||||
module.exports = { createChatServer };
|
||||
@@ -3,7 +3,9 @@ const fs = require('fs').promises;
|
||||
const verboseMode = process.argv.includes('--debug');
|
||||
const verboseModeFfmpeg = process.argv.includes('--ffmpegdebug');
|
||||
|
||||
const LOG_FILE = 'serverlog.txt';
|
||||
const LOG_FILE = process.argv.includes('--config') && process.argv[process.argv.indexOf('--config') + 1]
|
||||
? `serverlog_${process.argv[process.argv.indexOf('--config') + 1]}.txt`
|
||||
: 'serverlog.txt';
|
||||
const ANSI_ESCAPE_CODE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||
const MAX_LOG_LINES = 5000;
|
||||
const FLUSH_INTERVAL = 60000;
|
||||
@@ -30,21 +32,19 @@ const getCurrentTime = () => {
|
||||
|
||||
const removeANSIEscapeCodes = (str) => str.replace(ANSI_ESCAPE_CODE_PATTERN, ''); // Strip ANSI escape codes from a string
|
||||
|
||||
const logMessage = (type, messages, verbose = false) => {
|
||||
const logMessage = (type, messages) => {
|
||||
const logMessage = `${getCurrentTime()} ${MESSAGE_PREFIX[type]} ${messages.join(' ')}`;
|
||||
|
||||
if (type === 'DEBUG' && verboseMode || type === 'FFMPEG' && verboseModeFfmpeg || type !== 'DEBUG' && type !== 'FFMPEG') {
|
||||
if ((type === 'DEBUG' && verboseMode) || (type === 'FFMPEG' && verboseModeFfmpeg) || type !== 'DEBUG' && type !== 'FFMPEG') {
|
||||
logs.push(logMessage);
|
||||
if (logs.length > maxConsoleLogLines) logs.shift();
|
||||
console.log(logMessage);
|
||||
}
|
||||
|
||||
if(type !== 'FFMPEG') {
|
||||
appendLogToBuffer(logMessage);
|
||||
}
|
||||
if(type !== 'FFMPEG') appendLogToBuffer(logMessage);
|
||||
};
|
||||
|
||||
const logDebug = (...messages) => logMessage('DEBUG', messages, verboseMode);
|
||||
const logDebug = (...messages) => logMessage('DEBUG', messages);
|
||||
const logChat = (message) => logMessage('CHAT', [`${message.nickname} (${message.ip}) sent a chat message: ${message.message}`]);
|
||||
const logError = (...messages) => logMessage('ERROR', messages);
|
||||
const logFfmpeg = (...messages) => logMessage('FFMPEG', messages, verboseModeFfmpeg);
|
||||
|
||||
@@ -1,219 +1,14 @@
|
||||
/* Libraries / Imports */
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const koffi = require('koffi');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const platform = os.platform();
|
||||
const cpuArchitecture = os.arch();
|
||||
const { configName, serverConfig, configUpdate, configSave } = require('./server_config');
|
||||
let unicode_type;
|
||||
let shared_Library;
|
||||
|
||||
if (platform === 'win32') {
|
||||
unicode_type = 'int16_t';
|
||||
arch_type = (cpuArchitecture === 'x64' ? 'mingw64' : 'mingw32');
|
||||
shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.dll");
|
||||
} else if (platform === 'linux') {
|
||||
unicode_type = 'int32_t';
|
||||
arch_type = (cpuArchitecture === 'x64' ? 'x86_64' :
|
||||
(cpuArchitecture === 'ia32' ? 'x86' :
|
||||
(cpuArchitecture === 'arm64' ? 'aarch64' : cpuArchitecture)));
|
||||
shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.so");
|
||||
} else if (platform === 'darwin') {
|
||||
unicode_type = 'int32_t';
|
||||
shared_Library=path.join(__dirname, "libraries", "macos", "librdsparser.dylib");
|
||||
}
|
||||
|
||||
const lib = koffi.load(shared_Library);
|
||||
const { fetchTx } = require('./tx_search.js');
|
||||
|
||||
koffi.proto('void callback_pi(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_pty(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_tp(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_ta(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_ms(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_ecc(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_country(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_af(void *rds, uint32_t af, void *user_data)');
|
||||
koffi.proto('void callback_ps(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_rt(void *rds, int flag, void *user_data)');
|
||||
koffi.proto('void callback_ptyn(void *rds, void *user_data)');
|
||||
koffi.proto('void callback_ct(void *rds, void *ct, void *user_data)');
|
||||
|
||||
const rdsparser = {
|
||||
new: lib.func('void* rdsparser_new()'),
|
||||
free: lib.func('void rdsparser_free(void *rds)'),
|
||||
clear: lib.func('void rdsparser_clear(void *rds)'),
|
||||
parse_string: lib.func('bool rdsparser_parse_string(void *rds, const char *input)'),
|
||||
set_text_correction: lib.func('void rdsparser_set_text_correction(void *rds, uint8_t text, uint8_t type, uint8_t error)'),
|
||||
set_text_progressive: lib.func('void rdsparser_set_text_progressive(void *rds, uint8_t string, uint8_t state)'),
|
||||
get_pi: lib.func('int32_t rdsparser_get_pi(void *rds)'),
|
||||
get_pty: lib.func('int8_t rdsparser_get_pty(void *rds)'),
|
||||
get_tp: lib.func('int8_t rdsparser_get_tp(void *rds)'),
|
||||
get_ta: lib.func('int8_t rdsparser_get_ta(void *rds)'),
|
||||
get_ms: lib.func('int8_t rdsparser_get_ms(void *rds)'),
|
||||
get_ecc: lib.func('int16_t rdsparser_get_ecc(void *rds)'),
|
||||
get_country: lib.func('int rdsparser_get_country(void *rds)'),
|
||||
get_ps: lib.func('void* rdsparser_get_ps(void *rds)'),
|
||||
get_rt: lib.func('void* rdsparser_get_rt(void *rds, int flag)'),
|
||||
get_ptyn: lib.func('void* rdsparser_get_ptyn(void *rds)'),
|
||||
register_pi: lib.func('void rdsparser_register_pi(void *rds, void *cb)'),
|
||||
register_pty: lib.func('void rdsparser_register_pty(void *rds, void *cb)'),
|
||||
register_tp: lib.func('void rdsparser_register_tp(void *rds, void *cb)'),
|
||||
register_ta: lib.func('void rdsparser_register_ta(void *rds, void *cb)'),
|
||||
register_ms: lib.func('void rdsparser_register_ms(void *rds, void *cb)'),
|
||||
register_ecc: lib.func('void rdsparser_register_ecc(void *rds, void *cb)'),
|
||||
register_country: lib.func('void rdsparser_register_country(void *rds, void *cb)'),
|
||||
register_af: lib.func('void rdsparser_register_af(void *rds, void *cb)'),
|
||||
register_ps: lib.func('void rdsparser_register_ps(void *rds, void *cb)'),
|
||||
register_rt: lib.func('void rdsparser_register_rt(void *rds, void *cb)'),
|
||||
register_ptyn: lib.func('void rdsparser_register_ptyn(void *rds, void *cb)'),
|
||||
register_ct: lib.func('void rdsparser_register_ct(void *rds, void *cb)'),
|
||||
string_get_content: lib.func(unicode_type + '* rdsparser_string_get_content(void *string)'),
|
||||
string_get_errors: lib.func('uint8_t* rdsparser_string_get_errors(void *string)'),
|
||||
string_get_length: lib.func('uint8_t rdsparser_string_get_length(void *string)'),
|
||||
ct_get_year: lib.func('uint16_t rdsparser_ct_get_year(void *ct)'),
|
||||
ct_get_month: lib.func('uint8_t rdsparser_ct_get_month(void *ct)'),
|
||||
ct_get_day: lib.func('uint8_t rdsparser_ct_get_day(void *ct)'),
|
||||
ct_get_hour: lib.func('uint8_t rdsparser_ct_get_hour(void *ct)'),
|
||||
ct_get_minute: lib.func('uint8_t rdsparser_ct_get_minute(void *ct)'),
|
||||
ct_get_offset: lib.func('int8_t rdsparser_ct_get_offset(void *ct)'),
|
||||
pty_lookup_short: lib.func('const char* rdsparser_pty_lookup_short(int8_t pty, bool rbds)'),
|
||||
pty_lookup_long: lib.func('const char* rdsparser_pty_lookup_long(int8_t pty, bool rbds)'),
|
||||
country_lookup_name: lib.func('const char* rdsparser_country_lookup_name(int country)'),
|
||||
country_lookup_iso: lib.func('const char* rdsparser_country_lookup_iso(int country)')
|
||||
}
|
||||
|
||||
const callbacks = {
|
||||
pi: koffi.register(rds => (
|
||||
value = rdsparser.get_pi(rds)
|
||||
//console.log('PI: ' + value.toString(16).toUpperCase())
|
||||
), 'callback_pi*'),
|
||||
|
||||
pty: koffi.register(rds => (
|
||||
value = rdsparser.get_pty(rds),
|
||||
dataToSend.pty = value
|
||||
), 'callback_pty*'),
|
||||
|
||||
tp: koffi.register(rds => (
|
||||
value = rdsparser.get_tp(rds),
|
||||
dataToSend.tp = value
|
||||
), 'callback_tp*'),
|
||||
|
||||
ta: koffi.register(rds => (
|
||||
value = rdsparser.get_ta(rds),
|
||||
dataToSend.ta = value
|
||||
), 'callback_ta*'),
|
||||
|
||||
ms: koffi.register(rds => (
|
||||
value = rdsparser.get_ms(rds),
|
||||
dataToSend.ms = value
|
||||
), 'callback_ms*'),
|
||||
|
||||
af: koffi.register((rds, value) => (
|
||||
dataToSend.af.push(value)
|
||||
), 'callback_af*'),
|
||||
|
||||
ecc: koffi.register(rds => (
|
||||
value = rdsparser.get_ecc(rds),
|
||||
dataToSend.ecc = value
|
||||
), 'callback_ecc*'),
|
||||
|
||||
country: koffi.register(rds => (
|
||||
value = rdsparser.get_country(rds),
|
||||
display = rdsparser.country_lookup_name(value),
|
||||
iso = rdsparser.country_lookup_iso(value),
|
||||
dataToSend.country_name = display,
|
||||
dataToSend.country_iso = iso
|
||||
), 'callback_country*'),
|
||||
|
||||
ps: koffi.register(rds => (
|
||||
ps = rdsparser.get_ps(rds),
|
||||
dataToSend.ps = decode_unicode(ps),
|
||||
dataToSend.ps_errors = decode_errors(ps)
|
||||
), 'callback_ps*'),
|
||||
|
||||
rt: koffi.register((rds, flag) => {
|
||||
const rt = rdsparser.get_rt(rds, flag);
|
||||
|
||||
if (flag === 0) {
|
||||
dataToSend.rt0 = decode_unicode(rt);
|
||||
dataToSend.rt0_errors = decode_errors(rt);
|
||||
}
|
||||
|
||||
if (flag === 1) {
|
||||
dataToSend.rt1 = decode_unicode(rt);
|
||||
dataToSend.rt1_errors = decode_errors(rt);
|
||||
}
|
||||
dataToSend.rt_flag = flag;
|
||||
}, 'callback_rt*'),
|
||||
|
||||
ptyn: koffi.register((rds, flag) => (
|
||||
value = decode_unicode(rdsparser.get_ptyn(rds))
|
||||
/*console.log('PTYN: ' + value)*/
|
||||
), 'callback_ptyn*'),
|
||||
|
||||
ct: koffi.register((rds, ct) => (
|
||||
year = rdsparser.ct_get_year(ct),
|
||||
month = String(rdsparser.ct_get_month(ct)).padStart(2, '0'),
|
||||
day = String(rdsparser.ct_get_day(ct)).padStart(2, '0'),
|
||||
hour = String(rdsparser.ct_get_hour(ct)).padStart(2, '0'),
|
||||
minute = String(rdsparser.ct_get_minute(ct)).padStart(2, '0'),
|
||||
offset = rdsparser.ct_get_offset(ct),
|
||||
tz_sign = (offset >= 0 ? '+' : '-'),
|
||||
tz_hour = String(Math.abs(Math.floor(offset / 60))).padStart(2, '0'),
|
||||
tz_minute = String(Math.abs(offset % 60)).padStart(2, '0')
|
||||
//console.log('CT: ' + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ' (' + tz_sign + tz_hour + ':' + tz_minute + ')')
|
||||
), 'callback_ct*')
|
||||
};
|
||||
|
||||
let rds = rdsparser.new()
|
||||
rdsparser.set_text_correction(rds, 0, 0, 2);
|
||||
rdsparser.set_text_correction(rds, 0, 1, 2);
|
||||
rdsparser.set_text_correction(rds, 1, 0, 2);
|
||||
rdsparser.set_text_correction(rds, 1, 1, 2);
|
||||
rdsparser.set_text_progressive(rds, 0, 1)
|
||||
rdsparser.set_text_progressive(rds, 1, 1)
|
||||
rdsparser.register_pi(rds, callbacks.pi);
|
||||
rdsparser.register_pty(rds, callbacks.pty);
|
||||
rdsparser.register_tp(rds, callbacks.tp);
|
||||
rdsparser.register_ta(rds, callbacks.ta);
|
||||
rdsparser.register_ms(rds, callbacks.ms);
|
||||
rdsparser.register_ecc(rds, callbacks.ecc);
|
||||
rdsparser.register_country(rds, callbacks.country);
|
||||
rdsparser.register_af(rds, callbacks.af);
|
||||
rdsparser.register_ps(rds, callbacks.ps);
|
||||
rdsparser.register_rt(rds, callbacks.rt);
|
||||
rdsparser.register_ptyn(rds, callbacks.ptyn);
|
||||
rdsparser.register_ct(rds, callbacks.ct);
|
||||
|
||||
const decode_unicode = function(string) {
|
||||
let length = rdsparser.string_get_length(string);
|
||||
if (length) {
|
||||
let content = rdsparser.string_get_content(string);
|
||||
let array = koffi.decode(content, unicode_type + ' [' + length + ']');
|
||||
return String.fromCodePoint.apply(String, array);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const decode_errors = function(string) {
|
||||
let length = rdsparser.string_get_length(string);
|
||||
if (length) {
|
||||
let errors = rdsparser.string_get_errors(string);
|
||||
let array = koffi.decode(errors, 'uint8_t [' + length + ']');
|
||||
return Uint8Array.from(array).toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const RDSDecoder = require("./rds.js");
|
||||
const { serverConfig } = require('./server_config');
|
||||
|
||||
const fetchTx = require('./tx_search.js');
|
||||
const updateInterval = 75;
|
||||
|
||||
// Initialize the data object
|
||||
var dataToSend = {
|
||||
pi: '?',
|
||||
freq: 87.500.toFixed(3),
|
||||
freq: (87.500).toFixed(3),
|
||||
sig: 0,
|
||||
sigRaw: '',
|
||||
sigTop: -Infinity,
|
||||
@@ -233,6 +28,7 @@ var dataToSend = {
|
||||
rt_flag: '',
|
||||
ims: 0,
|
||||
eq: 0,
|
||||
agc: 0,
|
||||
ant: 0,
|
||||
txInfo: {
|
||||
tx: '',
|
||||
@@ -251,6 +47,8 @@ var dataToSend = {
|
||||
users: 0,
|
||||
};
|
||||
|
||||
const rdsdec = new RDSDecoder(dataToSend);
|
||||
|
||||
const filterMappings = {
|
||||
'G11': { eq: 1, ims: 1 },
|
||||
'G01': { eq: 0, ims: 1 },
|
||||
@@ -264,8 +62,6 @@ var lastUpdateTime = Date.now();
|
||||
const initialData = { ...dataToSend };
|
||||
const resetToDefault = dataToSend => Object.assign(dataToSend, initialData);
|
||||
|
||||
// Serialport reconnect variables
|
||||
const ServerStartTime = process.hrtime();
|
||||
var serialportUpdateTime = process.hrtime();
|
||||
let checkSerialport = false;
|
||||
let rdsTimeoutTimer = null;
|
||||
@@ -275,15 +71,13 @@ function rdsReceived() {
|
||||
clearTimeout(rdsTimeoutTimer);
|
||||
rdsTimeoutTimer = null;
|
||||
}
|
||||
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) {
|
||||
rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
|
||||
}
|
||||
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
|
||||
}
|
||||
|
||||
function rdsReset() {
|
||||
resetToDefault(dataToSend);
|
||||
dataToSend.af.length = 0;
|
||||
rdsparser.clear(rds);
|
||||
rdsdec.clear();
|
||||
if (rdsTimeoutTimer) {
|
||||
clearTimeout(rdsTimeoutTimer);
|
||||
rdsTimeoutTimer = null;
|
||||
@@ -307,17 +101,13 @@ function handleData(wss, receivedData, rdsWss) {
|
||||
rdsReceived();
|
||||
modifiedData = receivedLine.slice(1);
|
||||
legacyRdsPiBuffer = modifiedData;
|
||||
if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') {
|
||||
dataToSend.pi = modifiedData;
|
||||
}
|
||||
if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') dataToSend.pi = modifiedData;
|
||||
break;
|
||||
case receivedLine.startsWith('T'): // Frequency
|
||||
modifiedData = receivedLine.substring(1).split(",")[0];
|
||||
|
||||
rdsReset();
|
||||
if((modifiedData / 1000).toFixed(3) == dataToSend.freq) {
|
||||
return; // Prevent tune spamming using scrollwheel
|
||||
}
|
||||
if((modifiedData / 1000).toFixed(3) == dataToSend.freq) return; // Prevent tune spamming using scrollwheel
|
||||
|
||||
parsedValue = parseFloat(modifiedData);
|
||||
|
||||
@@ -337,6 +127,10 @@ function handleData(wss, receivedData, rdsWss) {
|
||||
initialData.ant = receivedLine.substring(1);
|
||||
rdsReset();
|
||||
break;
|
||||
case receivedLine.startsWith('A'): // AGC
|
||||
dataToSend.agc = receivedLine.substring(1);
|
||||
initialData.agc = receivedLine.substring(1);
|
||||
break;
|
||||
case receivedLine.startsWith('G'): // EQ / iMS (RF+/IF+)
|
||||
const mapping = filterMappings[receivedLine];
|
||||
if (mapping) {
|
||||
@@ -360,8 +154,8 @@ function handleData(wss, receivedData, rdsWss) {
|
||||
processSignal(receivedLine, true, true);
|
||||
break;
|
||||
case receivedLine.startsWith('SM'):
|
||||
processSignal(receivedLine, false, true);
|
||||
break;
|
||||
processSignal(receivedLine, false, true);
|
||||
break;
|
||||
case receivedLine.startsWith('R'): // RDS HEX
|
||||
rdsReceived();
|
||||
modifiedData = receivedLine.slice(1);
|
||||
@@ -372,8 +166,7 @@ function handleData(wss, receivedData, rdsWss) {
|
||||
var errorsNew = 0;
|
||||
var pi;
|
||||
|
||||
if (legacyRdsPiBuffer !== null &&
|
||||
legacyRdsPiBuffer.length >= 4) {
|
||||
if(legacyRdsPiBuffer !== null && legacyRdsPiBuffer.length >= 4) {
|
||||
pi = legacyRdsPiBuffer.slice(0, 4);
|
||||
// PI message does not carry explicit information about
|
||||
// error correction, but this is a good substitute.
|
||||
@@ -392,19 +185,21 @@ function handleData(wss, receivedData, rdsWss) {
|
||||
modifiedData += errorsNew.toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
const a = modifiedData.slice(0, 4);
|
||||
const b = modifiedData.slice(4, 8);
|
||||
const c = modifiedData.slice(8, 12);
|
||||
const d = modifiedData.slice(12, 16);
|
||||
const errors = parseInt(modifiedData.slice(-2), 16);
|
||||
rdsWss.clients.forEach((client) => {
|
||||
const errors = parseInt(modifiedData.slice(-2), 16);
|
||||
let data = (((errors & 0xC0) == 0) ? modifiedData.slice(0, 4) : '----');
|
||||
data += (((errors & 0x30) == 0) ? modifiedData.slice(4, 8) : '----');
|
||||
data += (((errors & 0x0C) == 0) ? modifiedData.slice(8, 12) : '----');
|
||||
data += (((errors & 0x03) == 0) ? modifiedData.slice(12, 16) : '----');
|
||||
let data = ((((errors >> 6) & 3) < 3) ? a : '----');
|
||||
data += ((((errors >> 4) & 3) < 3) ? b : '----');
|
||||
data += ((((errors >> 2) & 3) < 3) ? c : '----');
|
||||
data += (((errors & 3) < 3) ? d : '----');
|
||||
|
||||
const newDataString = "G:\r\n" + data + "\r\n\r\n";
|
||||
const finalBuffer = Buffer.from(newDataString, 'utf-8');
|
||||
client.send(finalBuffer);
|
||||
client.send("G:\r\n" + data + "\r\n\r\n");
|
||||
});
|
||||
|
||||
rdsparser.parse_string(rds, modifiedData);
|
||||
rdsdec.decodeGroup(parseInt(a, 16), parseInt(b, 16), parseInt(c, 16), parseInt(d, 16), errors);
|
||||
legacyRdsPiBuffer = null;
|
||||
break;
|
||||
}
|
||||
@@ -434,15 +229,15 @@ function handleData(wss, receivedData, rdsWss) {
|
||||
console.log("Error fetching Tx info:", error);
|
||||
});
|
||||
|
||||
// Send the updated data to the client
|
||||
const dataToSendJSON = JSON.stringify(dataToSend);
|
||||
if (currentTime - lastUpdateTime >= updateInterval) {
|
||||
wss.clients.forEach((client) => {
|
||||
client.send(dataToSendJSON);
|
||||
});
|
||||
lastUpdateTime = Date.now();
|
||||
serialportUpdateTime = process.hrtime();
|
||||
}
|
||||
// Send the updated data to the client
|
||||
const dataToSendJSON = JSON.stringify(dataToSend);
|
||||
if (currentTime - lastUpdateTime >= updateInterval) {
|
||||
wss.clients.forEach((client) => {
|
||||
client.send(dataToSendJSON);
|
||||
});
|
||||
lastUpdateTime = Date.now();
|
||||
serialportUpdateTime = process.hrtime();
|
||||
}
|
||||
}
|
||||
|
||||
// Serialport retry code when port is open but communication is lost (additional code in index.js)
|
||||
@@ -469,10 +264,7 @@ async function checkSerialPortStatus() {
|
||||
while (!checkSerialport) {
|
||||
const ServerElapsedSeconds = process.hrtime(ServerStartTime)[0];
|
||||
|
||||
if (ServerElapsedSeconds > 10) {
|
||||
checkSerialport = true;
|
||||
}
|
||||
|
||||
if (ServerElapsedSeconds > 10) checkSerialport = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
@@ -507,11 +299,8 @@ function processSignal(receivedData, st, stForced) {
|
||||
|
||||
// Convert highestSignal to a number for comparison
|
||||
var highestSignal = parseFloat(dataToSend.sigTop);
|
||||
if (signal > highestSignal) {
|
||||
dataToSend.sigTop = signal.toString(); // Convert back to string for consistency
|
||||
}
|
||||
}
|
||||
|
||||
if (signal > highestSignal) dataToSend.sigTop = signal.toString(); // Convert back to string for consistency
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -11,14 +11,15 @@ const { parseAudioDevice } = require('./stream/parser');
|
||||
const { configName, serverConfig, configUpdate, configSave, configExists, configPath } = require('./server_config');
|
||||
const helpers = require('./helpers');
|
||||
const storage = require('./storage');
|
||||
const { logInfo, logDebug, logWarn, logError, logFfmpeg, logs } = require('./console');
|
||||
const tunerProfiles = require('./tuner_profiles');
|
||||
const { logInfo, logs } = require('./console');
|
||||
const dataHandler = require('./datahandler');
|
||||
const fmdxList = require('./fmdx_list');
|
||||
const { allPluginConfigs } = require('./plugins');
|
||||
const allPluginConfigs = require('./plugins');
|
||||
|
||||
// Endpoints
|
||||
router.get('/', (req, res) => {
|
||||
let requestIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
let requestIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
|
||||
const normalizedIp = requestIp?.replace(/^::ffff:/, '');
|
||||
const ipList = (normalizedIp || '').split(',').map(ip => ip.trim()).filter(Boolean); // in case there are multiple IPs (proxy), we need to check all of them
|
||||
@@ -48,7 +49,13 @@ router.get('/', (req, res) => {
|
||||
isAdminAuthenticated: true,
|
||||
videoDevices: result.audioDevices,
|
||||
audioDevices: result.videoDevices,
|
||||
serialPorts: serialPorts
|
||||
serialPorts: serialPorts,
|
||||
serialPorts: serialPorts,
|
||||
tunerProfiles: tunerProfiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
label: profile.label,
|
||||
detailsHtml: helpers.parseMarkdown(profile.details || '')
|
||||
}))
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,6 +75,8 @@ router.get('/', (req, res) => {
|
||||
tuningUpperLimit: serverConfig.webserver.tuningUpperLimit,
|
||||
chatEnabled: serverConfig.webserver.chatEnabled,
|
||||
device: serverConfig.device,
|
||||
tunerProfiles,
|
||||
si47xxAgcControl: !!serverConfig.si47xx?.agcControl,
|
||||
noPlugins,
|
||||
plugins: serverConfig.plugins,
|
||||
fmlist_integration: serverConfig.extras.fmlistIntegration,
|
||||
@@ -78,7 +87,8 @@ router.get('/', (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/403', (req, res) => {
|
||||
res.render('403');
|
||||
const reason = req.query.reason || null;
|
||||
res.render('403', { reason });
|
||||
})
|
||||
|
||||
router.get('/wizard', (req, res) => {
|
||||
@@ -101,7 +111,12 @@ router.get('/wizard', (req, res) => {
|
||||
isAdminAuthenticated: req.session.isAdminAuthenticated,
|
||||
videoDevices: result.audioDevices,
|
||||
audioDevices: result.videoDevices,
|
||||
serialPorts: serialPorts
|
||||
serialPorts: serialPorts,
|
||||
tunerProfiles: tunerProfiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
label: profile.label,
|
||||
detailsHtml: helpers.parseMarkdown(profile.details || '')
|
||||
}))
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -135,20 +150,25 @@ router.get('/wizard', (req, res) => {
|
||||
|
||||
const updatedConfig = loadConfig(); // Reload the config every time
|
||||
res.render('setup', {
|
||||
isAdminAuthenticated: req.session.isAdminAuthenticated,
|
||||
videoDevices: result.audioDevices,
|
||||
audioDevices: result.videoDevices,
|
||||
serialPorts: serialPorts,
|
||||
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
|
||||
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
|
||||
processUptime: formattedProcessUptime,
|
||||
consoleOutput: logs,
|
||||
plugins: allPluginConfigs,
|
||||
enabledPlugins: updatedConfig.plugins,
|
||||
onlineUsers: dataHandler.dataToSend.users,
|
||||
connectedUsers: storage.connectedUsers,
|
||||
device: serverConfig.device,
|
||||
banlist: updatedConfig.webserver.banlist // Updated banlist from the latest config
|
||||
isAdminAuthenticated: req.session.isAdminAuthenticated,
|
||||
videoDevices: result.audioDevices,
|
||||
audioDevices: result.videoDevices,
|
||||
serialPorts: serialPorts,
|
||||
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
|
||||
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
|
||||
processUptime: formattedProcessUptime,
|
||||
consoleOutput: logs,
|
||||
plugins: allPluginConfigs,
|
||||
enabledPlugins: updatedConfig.plugins,
|
||||
onlineUsers: dataHandler.dataToSend.users,
|
||||
connectedUsers: storage.connectedUsers,
|
||||
device: serverConfig.device,
|
||||
banlist: updatedConfig.webserver.banlist, // Updated banlist from the latest config
|
||||
tunerProfiles: tunerProfiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
label: profile.label,
|
||||
detailsHtml: helpers.parseMarkdown(profile.details || '')
|
||||
}))
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -156,43 +176,37 @@ router.get('/wizard', (req, res) => {
|
||||
|
||||
|
||||
router.get('/rds', (req, res) => {
|
||||
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
|
||||
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
|
||||
});
|
||||
|
||||
router.get('/rdsspy', (req, res) => {
|
||||
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
|
||||
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
|
||||
});
|
||||
|
||||
router.get('/api', (req, res) => {
|
||||
const { ps_errors, rt0_errors, rt1_errors, ims, eq, ant, st_forced, previousFreq, txInfo, ...dataToSend } = dataHandler.dataToSend;
|
||||
const { ps_errors, rt0_errors, rt1_errors, ims, eq, ant, st_forced, previousFreq, txInfo, rdsMode, ...dataToSend } = dataHandler.dataToSend;
|
||||
res.json({
|
||||
...dataToSend,
|
||||
txInfo: txInfo,
|
||||
ps_errors: ps_errors,
|
||||
ant: ant
|
||||
ant: ant,
|
||||
rbds: serverConfig.webserver.rdsMode
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
|
||||
const MAX_ATTEMPTS = 25;
|
||||
const MAX_ATTEMPTS = 15;
|
||||
const WINDOW_MS = 15 * 60 * 1000;
|
||||
|
||||
const authenticate = (req, res, next) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const now = Date.now();
|
||||
|
||||
if (!loginAttempts[ip]) {
|
||||
loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||
} else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) {
|
||||
loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||
}
|
||||
if (!loginAttempts[ip]) loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||
else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||
|
||||
if (loginAttempts[ip].count >= MAX_ATTEMPTS) {
|
||||
return res.status(403).json({
|
||||
message: 'Too many login attempts. Please try again later.'
|
||||
});
|
||||
}
|
||||
if (loginAttempts[ip].count >= MAX_ATTEMPTS) return res.status(403).json({message: 'Too many login attempts. Please try again later.'});
|
||||
|
||||
const { password } = req.body;
|
||||
|
||||
@@ -230,17 +244,16 @@ router.get('/logout', (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/kick', (req, res) => {
|
||||
const ipAddress = req.query.ip; // Extract the IP address parameter from the query string
|
||||
const ipAddress = req.query.ip;
|
||||
// Terminate the WebSocket connection for the specified IP address
|
||||
if(req.session.isAdminAuthenticated) {
|
||||
helpers.kickClient(ipAddress);
|
||||
}
|
||||
if(req.session.isAdminAuthenticated) helpers.kickClient(ipAddress);
|
||||
setTimeout(() => {
|
||||
res.redirect('/setup');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
router.get('/addToBanlist', (req, res) => {
|
||||
if (!req.session.isAdminAuthenticated) return;
|
||||
const ipAddress = req.query.ip;
|
||||
const location = 'Unknown';
|
||||
const date = Date.now();
|
||||
@@ -248,32 +261,24 @@ router.get('/addToBanlist', (req, res) => {
|
||||
|
||||
userBanData = [ipAddress, location, date, reason];
|
||||
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') {
|
||||
serverConfig.webserver.banlist = [];
|
||||
}
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
|
||||
|
||||
if (req.session.isAdminAuthenticated) {
|
||||
serverConfig.webserver.banlist.push(userBanData);
|
||||
configSave();
|
||||
res.json({ success: true, message: 'IP address added to banlist.' });
|
||||
helpers.kickClient(ipAddress);
|
||||
} else {
|
||||
res.status(403).json({ success: false, message: 'Unauthorized access.' });
|
||||
}
|
||||
serverConfig.webserver.banlist.push(userBanData);
|
||||
configSave();
|
||||
res.json({ success: true, message: 'IP address added to banlist.' });
|
||||
helpers.kickClient(ipAddress);
|
||||
});
|
||||
|
||||
router.get('/removeFromBanlist', (req, res) => {
|
||||
if (!req.session.isAdminAuthenticated) return;
|
||||
|
||||
const ipAddress = req.query.ip;
|
||||
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') {
|
||||
serverConfig.webserver.banlist = [];
|
||||
}
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
|
||||
|
||||
const banIndex = serverConfig.webserver.banlist.findIndex(ban => ban[0] === ipAddress);
|
||||
|
||||
if (banIndex === -1) {
|
||||
return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
|
||||
}
|
||||
if (banIndex === -1) return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
|
||||
|
||||
serverConfig.webserver.banlist.splice(banIndex, 1);
|
||||
configSave();
|
||||
@@ -285,19 +290,14 @@ router.get('/removeFromBanlist', (req, res) => {
|
||||
router.post('/saveData', (req, res) => {
|
||||
const data = req.body;
|
||||
let firstSetup;
|
||||
if(req.session.isAdminAuthenticated || configExists() === false) {
|
||||
if(req.session.isAdminAuthenticated || !configExists()) {
|
||||
configUpdate(data);
|
||||
fmdxList.update();
|
||||
|
||||
if(configExists() === false) {
|
||||
firstSetup = true;
|
||||
}
|
||||
if(!configExists()) firstSetup = true;
|
||||
logInfo('Server config changed successfully.');
|
||||
if(firstSetup === true) {
|
||||
res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.');
|
||||
} else {
|
||||
res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.');
|
||||
}
|
||||
if(firstSetup === true) res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.');
|
||||
else res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -309,9 +309,8 @@ router.get('/getData', (req, res) => {
|
||||
if(req.session.isAdminAuthenticated) {
|
||||
// Check if the file exists
|
||||
fs.access(configPath, fs.constants.F_OK, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
if (err) console.log(err);
|
||||
else {
|
||||
// File exists, send it as the response
|
||||
res.sendFile(path.join(__dirname, '../' + configName + '.json'));
|
||||
}
|
||||
@@ -324,9 +323,7 @@ router.get('/getDevices', (req, res) => {
|
||||
parseAudioDevice((result) => {
|
||||
res.json(result);
|
||||
});
|
||||
} else {
|
||||
res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
} else res.status(403).json({ error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
/* Static data are being sent through here on connection - these don't change when the server is running */
|
||||
@@ -371,9 +368,7 @@ function canLog(id) {
|
||||
}
|
||||
}
|
||||
|
||||
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) {
|
||||
return false; // Deny logging if less than 60 minutes have passed
|
||||
}
|
||||
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) return false; // Deny logging if less than 60 minutes have passed
|
||||
logHistory[id] = now; // Update with the current timestamp
|
||||
return true;
|
||||
}
|
||||
@@ -463,6 +458,7 @@ router.get('/tunnelservers', async (req, res) => {
|
||||
{ value: "eu", host: "eu.fmtuner.org", label: "Europe" },
|
||||
{ value: "us", host: "us.fmtuner.org", label: "Americas" },
|
||||
{ value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" },
|
||||
{ value: "pldx", host: "pldx.fmtuner.org", label: "Poland (k201)" },
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* Libraries / Imports */
|
||||
const fs = require('fs');
|
||||
const fetch = require('node-fetch');
|
||||
const { logDebug, logError, logInfo, logWarn } = require('./console');
|
||||
const { serverConfig, configUpdate, configSave } = require('./server_config');
|
||||
const { logDebug, logInfo, logWarn } = require('./console');
|
||||
const { serverConfig, configSave } = require('./server_config');
|
||||
var pjson = require('../package.json');
|
||||
var os = require('os');
|
||||
|
||||
@@ -23,23 +22,15 @@ function send(request) {
|
||||
fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.token)
|
||||
{
|
||||
if (data.success && data.token) {
|
||||
if (!serverConfig.identification.token)
|
||||
{
|
||||
logInfo("Registered to FM-DX Server Map successfully.");
|
||||
serverConfig.identification.token = data.token;
|
||||
configSave();
|
||||
}
|
||||
else
|
||||
{
|
||||
logDebug("FM-DX Server Map update successful.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error'));
|
||||
}
|
||||
else logDebug("FM-DX Server Map update successful.");
|
||||
} else logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error'));
|
||||
})
|
||||
.catch(error => {
|
||||
logWarn("Failed to update FM-DX Server Map: " + error);
|
||||
@@ -47,10 +38,7 @@ function send(request) {
|
||||
}
|
||||
|
||||
function sendKeepalive() {
|
||||
if (!serverConfig.identification.token)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!serverConfig.identification.token) return;
|
||||
|
||||
const request = {
|
||||
token: serverConfig.identification.token,
|
||||
@@ -64,9 +52,7 @@ function sendUpdate() {
|
||||
let currentOs = os.type() + ' ' + os.release();
|
||||
|
||||
let bwLimit = '';
|
||||
if (serverConfig.webserver.tuningLimit === true) {
|
||||
bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz';
|
||||
}
|
||||
if (serverConfig.webserver.tuningLimit === true) bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz';
|
||||
|
||||
const request = {
|
||||
status: ((serverConfig.lockToAdmin == 'true' || serverConfig.publicTuner == 'false') ? 2 : 1),
|
||||
@@ -79,40 +65,23 @@ function sendUpdate() {
|
||||
tuner: serverConfig.device || '',
|
||||
bwLimit: bwLimit,
|
||||
os: currentOs,
|
||||
version: pjson.version
|
||||
version: pjson.version
|
||||
};
|
||||
|
||||
if (serverConfig.identification.token)
|
||||
{
|
||||
request.token = serverConfig.identification.token;
|
||||
}
|
||||
if (serverConfig.identification.token) request.token = serverConfig.identification.token;
|
||||
|
||||
if (serverConfig.identification.proxyIp.length)
|
||||
{
|
||||
request.url = serverConfig.identification.proxyIp;
|
||||
}
|
||||
else
|
||||
{
|
||||
request.port = serverConfig.webserver.webserverPort;
|
||||
}
|
||||
if (serverConfig.identification.proxyIp.length) request.url = serverConfig.identification.proxyIp;
|
||||
else request.port = serverConfig.webserver.webserverPort;
|
||||
|
||||
send(request);
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
|
||||
if (!serverConfig.identification.broadcastTuner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (timeoutID !== null) clearTimeout(timeoutID);
|
||||
if (!serverConfig.identification.broadcastTuner) return;
|
||||
|
||||
sendUpdate();
|
||||
timeoutID = setInterval(sendKeepalive, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
update
|
||||
};
|
||||
module.exports.update = update;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const net = require('net');
|
||||
@@ -5,43 +7,43 @@ const crypto = require('crypto');
|
||||
const dataHandler = require('./datahandler');
|
||||
const storage = require('./storage');
|
||||
const consoleCmd = require('./console');
|
||||
const { serverConfig, configExists, configSave } = require('./server_config');
|
||||
const { serverConfig, configSave } = require('./server_config');
|
||||
|
||||
function parseMarkdown(parsed) {
|
||||
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
|
||||
|
||||
var grayTextRegex = /--(.*?)--/g;
|
||||
parsed = parsed.replace(grayTextRegex, '<span class="text-gray">$1</span>');
|
||||
|
||||
|
||||
var boldRegex = /\*\*(.*?)\*\*/g;
|
||||
parsed = parsed.replace(boldRegex, '<strong>$1</strong>');
|
||||
|
||||
|
||||
var italicRegex = /\*(.*?)\*/g;
|
||||
parsed = parsed.replace(italicRegex, '<em>$1</em>');
|
||||
|
||||
|
||||
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
||||
parsed = parsed.replace(linkRegex, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
|
||||
parsed = parsed.replace(/\n/g, '<br>');
|
||||
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function removeMarkdown(parsed) {
|
||||
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
|
||||
|
||||
var grayTextRegex = /--(.*?)--/g;
|
||||
parsed = parsed.replace(grayTextRegex, '$1');
|
||||
|
||||
|
||||
var boldRegex = /\*\*(.*?)\*\*/g;
|
||||
parsed = parsed.replace(boldRegex, '$1');
|
||||
|
||||
|
||||
var italicRegex = /\*(.*?)\*/g;
|
||||
parsed = parsed.replace(italicRegex, '$1');
|
||||
|
||||
|
||||
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
||||
parsed = parsed.replace(linkRegex, '$1');
|
||||
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -93,9 +95,7 @@ let bannedASCache = { data: null, timestamp: 0 };
|
||||
|
||||
function fetchBannedAS(callback) {
|
||||
const now = Date.now();
|
||||
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) {
|
||||
return callback(null, bannedASCache.data);
|
||||
}
|
||||
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) return callback(null, bannedASCache.data);
|
||||
|
||||
const req = https.get("https://fmdx.org/banned_as.json", { family: 4 }, (banResponse) => {
|
||||
let banData = "";
|
||||
@@ -137,9 +137,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||
|
||||
fetchBannedAS((error, bannedAS) => {
|
||||
if (error) {
|
||||
console.error("Error fetching banned AS list:", error);
|
||||
}
|
||||
if (error) console.error("Error fetching banned AS list:", error);
|
||||
|
||||
if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
|
||||
const now = Date.now();
|
||||
@@ -154,9 +152,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
}
|
||||
|
||||
const userLocation =
|
||||
locationInfo.country === undefined
|
||||
? "Unknown"
|
||||
: `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
|
||||
locationInfo.country === undefined ? "Unknown" : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
|
||||
|
||||
storage.connectedUsers.push({
|
||||
ip: clientIp,
|
||||
@@ -165,9 +161,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
instance: ws,
|
||||
});
|
||||
|
||||
consoleCmd.logInfo(
|
||||
`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
|
||||
);
|
||||
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`);
|
||||
|
||||
callback("User allowed");
|
||||
});
|
||||
@@ -177,11 +171,11 @@ function formatUptime(uptimeInSeconds) {
|
||||
const secondsInMinute = 60;
|
||||
const secondsInHour = secondsInMinute * 60;
|
||||
const secondsInDay = secondsInHour * 24;
|
||||
|
||||
|
||||
const days = Math.floor(uptimeInSeconds / secondsInDay);
|
||||
const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour);
|
||||
const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute);
|
||||
|
||||
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
@@ -190,7 +184,7 @@ let incompleteDataBuffer = '';
|
||||
function resolveDataBuffer(data, wss, rdsWss) {
|
||||
var receivedData = incompleteDataBuffer + data.toString();
|
||||
const isIncomplete = (receivedData.slice(-1) != '\n');
|
||||
|
||||
|
||||
if (isIncomplete) {
|
||||
const position = receivedData.lastIndexOf('\n');
|
||||
if (position < 0) {
|
||||
@@ -200,13 +194,9 @@ function resolveDataBuffer(data, wss, rdsWss) {
|
||||
incompleteDataBuffer = receivedData.slice(position + 1);
|
||||
receivedData = receivedData.slice(0, position + 1);
|
||||
}
|
||||
} else {
|
||||
incompleteDataBuffer = '';
|
||||
}
|
||||
|
||||
if (receivedData.length) {
|
||||
dataHandler.handleData(wss, receivedData, rdsWss);
|
||||
};
|
||||
} else incompleteDataBuffer = '';
|
||||
|
||||
if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss);
|
||||
}
|
||||
|
||||
function kickClient(ipAddress) {
|
||||
@@ -215,15 +205,13 @@ function kickClient(ipAddress) {
|
||||
if (targetClient && targetClient.instance) {
|
||||
// Send a termination message to the client
|
||||
targetClient.instance.send('KICK');
|
||||
|
||||
|
||||
// Close the WebSocket connection after a short delay to allow the client to receive the message
|
||||
setTimeout(() => {
|
||||
targetClient.instance.close();
|
||||
consoleCmd.logInfo(`Web client kicked (${ipAddress})`);
|
||||
}, 500);
|
||||
} else {
|
||||
consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`);
|
||||
}
|
||||
} else consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`);
|
||||
}
|
||||
|
||||
function checkIPv6Support(callback) {
|
||||
@@ -232,11 +220,8 @@ function checkIPv6Support(callback) {
|
||||
server.listen(0, '::1', () => {
|
||||
server.close(() => callback(true));
|
||||
}).on('error', (error) => {
|
||||
if (error.code === 'EADDRNOTAVAIL') {
|
||||
callback(false);
|
||||
} else {
|
||||
callback(false);
|
||||
}
|
||||
if (error.code === 'EADDRNOTAVAIL') callback(false);
|
||||
else callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,27 +250,31 @@ function checkLatency(host, port = 80, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) {
|
||||
const command = message.toString();
|
||||
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName, maxPayloadSize = 1024 * 1024) {
|
||||
const rawCommand = message.toString();
|
||||
const command = rawCommand.replace(/[\r\n]+/g, '');
|
||||
const now = Date.now();
|
||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||
if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
|
||||
|
||||
// Initialize user command history if not present
|
||||
if (!userCommandHistory[clientIp]) {
|
||||
userCommandHistory[clientIp] = [];
|
||||
if (command.length > maxPayloadSize) {
|
||||
consoleCmd.logWarn(`Command from \x1b[90m${normalizedClientIp}\x1b[0m on \x1b[90m/${endpointName}\x1b[0m exceeded maximum payload size (${parseInt(command.length / 1024)} KB / ${parseInt(maxPayloadSize / 1024)} KB).`);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
// Initialize user command history if not present
|
||||
if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = [];
|
||||
|
||||
// Record the current timestamp for the user
|
||||
userCommandHistory[clientIp].push(now);
|
||||
|
||||
|
||||
// Remove timestamps older than 20 ms from the history
|
||||
userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
|
||||
|
||||
|
||||
// Check if there are 8 or more commands in the last 20 ms
|
||||
if (userCommandHistory[clientIp].length >= 8) {
|
||||
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
|
||||
|
||||
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
|
||||
|
||||
// Check if the normalized IP is already in the banlist
|
||||
const isAlreadyBanned = serverConfig.webserver.banlist.some(banEntry => banEntry[0] === normalizedClientIp);
|
||||
|
||||
@@ -295,18 +284,16 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
|
||||
consoleCmd.logInfo(`User \x1b[90m${normalizedClientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
|
||||
configSave();
|
||||
}
|
||||
|
||||
ws.close(1008, 'Bot-like behavior detected');
|
||||
return command; // Return command value before closing connection
|
||||
|
||||
ws.close(1008, 'Bot-like behavior detected');
|
||||
return command; // Return command value before closing connection
|
||||
}
|
||||
|
||||
// Update the last message time for general spam detection
|
||||
lastMessageTime = now;
|
||||
|
||||
// Initialize command history for rate-limiting checks
|
||||
if (!userCommands[command]) {
|
||||
userCommands[command] = [];
|
||||
}
|
||||
if (!userCommands[command]) userCommands[command] = [];
|
||||
|
||||
// Record the current timestamp for this command
|
||||
userCommands[command].push(now);
|
||||
@@ -328,15 +315,45 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
|
||||
}
|
||||
|
||||
const escapeHtml = (unsafe) => {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
return unsafe.replace(/&/g, "&")
|
||||
.replace(/</g, "<").replace(/>/g, ">")
|
||||
.replace(/"/g, """).replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Start plugins with delay
|
||||
function startPluginsWithDelay(plugins, delay) {
|
||||
plugins.forEach((pluginPath, index) => {
|
||||
setTimeout(() => {
|
||||
const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path
|
||||
consoleCmd.logInfo(`-----------------------------------------------------------------`);
|
||||
consoleCmd.logInfo(`Plugin ${pluginName} loaded successfully!`);
|
||||
require(pluginPath);
|
||||
}, delay * index);
|
||||
});
|
||||
|
||||
// Add final log line after all plugins are loaded
|
||||
setTimeout(() => {
|
||||
consoleCmd.logInfo(`-----------------------------------------------------------------`);
|
||||
}, delay * plugins.length);
|
||||
}
|
||||
|
||||
// Function to find server files based on the plugins listed in config
|
||||
function findServerFiles(plugins) {
|
||||
let results = [];
|
||||
plugins.forEach(plugin => {
|
||||
// Remove .js extension if present
|
||||
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
|
||||
|
||||
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
|
||||
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, checkLatency, antispamProtection, escapeHtml
|
||||
authenticateWithXdrd, parseMarkdown, handleConnect,
|
||||
removeMarkdown, formatUptime, resolveDataBuffer,
|
||||
kickClient, checkIPv6Support, checkLatency,
|
||||
antispamProtection, escapeHtml, findServerFiles,
|
||||
startPluginsWithDelay
|
||||
}
|
||||
428
server/index.js
428
server/index.js
@@ -1,4 +1,3 @@
|
||||
// Library imports
|
||||
const express = require('express');
|
||||
const endpoints = require('./endpoints');
|
||||
const session = require('express-session');
|
||||
@@ -8,60 +7,28 @@ const readline = require('readline');
|
||||
const app = express();
|
||||
const httpServer = http.createServer(app);
|
||||
const WebSocket = require('ws');
|
||||
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
||||
const rdsWss = new WebSocket.Server({ noServer: true });
|
||||
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const net = require('net');
|
||||
const client = new net.Socket();
|
||||
const { SerialPort } = require('serialport');
|
||||
const tunnel = require('./tunnel');
|
||||
const { createChatServer } = require('./chat');
|
||||
const { createAudioServer } = require('./stream/ws.js');
|
||||
const figlet = require('figlet');
|
||||
|
||||
// File imports
|
||||
const helpers = require('./helpers');
|
||||
const { findServerFiles, startPluginsWithDelay } = helpers;
|
||||
|
||||
const dataHandler = require('./datahandler');
|
||||
const fmdxList = require('./fmdx_list');
|
||||
const { logError, logInfo, logWarn } = require('./console');
|
||||
const storage = require('./storage');
|
||||
const { serverConfig, configExists } = require('./server_config');
|
||||
const pluginsApi = require('./plugins_api');
|
||||
const pjson = require('../package.json');
|
||||
|
||||
// Function to find server files based on the plugins listed in config
|
||||
function findServerFiles(plugins) {
|
||||
let results = [];
|
||||
plugins.forEach(plugin => {
|
||||
// Remove .js extension if present
|
||||
if (plugin.endsWith('.js')) {
|
||||
plugin = plugin.slice(0, -3);
|
||||
}
|
||||
|
||||
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
|
||||
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) {
|
||||
results.push(pluginPath);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// Start plugins with delay
|
||||
function startPluginsWithDelay(plugins, delay) {
|
||||
plugins.forEach((pluginPath, index) => {
|
||||
setTimeout(() => {
|
||||
const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path
|
||||
logInfo(`-----------------------------------------------------------------`);
|
||||
logInfo(`Plugin ${pluginName} loaded successfully!`);
|
||||
require(pluginPath);
|
||||
}, delay * index);
|
||||
});
|
||||
|
||||
// Add final log line after all plugins are loaded
|
||||
setTimeout(() => {
|
||||
logInfo(`-----------------------------------------------------------------`);
|
||||
}, delay * plugins.length);
|
||||
}
|
||||
const client = new net.Socket();
|
||||
const wss = new WebSocket.Server({ noServer: true });
|
||||
const rdsWss = new WebSocket.Server({ noServer: true });
|
||||
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
||||
|
||||
// Get all plugins from config and find corresponding server files
|
||||
const plugins = findServerFiles(serverConfig.plugins);
|
||||
@@ -78,21 +45,12 @@ const terminalWidth = readline.createInterface({
|
||||
output: process.stdout
|
||||
}).output.columns;
|
||||
|
||||
|
||||
console.log(`\x1b[32m
|
||||
_____ __ __ ______ __ __ __ _
|
||||
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
|
||||
| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__|
|
||||
| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ |
|
||||
|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_|
|
||||
`);
|
||||
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
|
||||
console.log('\x1b[32m' + figlet.textSync("FM-DX Webserver"));
|
||||
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org + KubaPro010\x1b[0m');
|
||||
console.log("v" + pjson.version)
|
||||
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
||||
|
||||
const chatWss = createChatServer(storage);
|
||||
const audioWss = createAudioServer();
|
||||
// Start ffmpeg
|
||||
const audioWss = require('./stream/ws.js');
|
||||
require('./stream/index');
|
||||
require('./plugins');
|
||||
|
||||
@@ -102,12 +60,13 @@ let timeoutAntenna;
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
const sessionMiddleware = session({
|
||||
secret: 'GTce3tN6U8odMwoI',
|
||||
secret: 'GTce3tN6U8odMwoI', // Cool
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
app.use(sessionMiddleware);
|
||||
app.use(bodyParser.json());
|
||||
const chatWss = createChatServer(storage);
|
||||
|
||||
connectToXdrd();
|
||||
connectToSerial();
|
||||
@@ -124,21 +83,17 @@ setInterval(() => {
|
||||
logWarn('Communication lost from ' + serverConfig.xdrd.comPort + ', force closing serialport.');
|
||||
setTimeout(() => {
|
||||
serialport.close((err) => {
|
||||
if (err) {
|
||||
logError('Error closing serialport: ', err.message);
|
||||
}
|
||||
if (err) logError('Error closing serialport: ', err.message);
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
|
||||
}
|
||||
} else logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Serial Connection
|
||||
function connectToSerial() {
|
||||
if (serverConfig.xdrd.wirelessConnection === false) {
|
||||
|
||||
if (serverConfig.xdrd.wirelessConnection === true) return;
|
||||
|
||||
// Configure the SerialPort with DTR and RTS options
|
||||
serialport = new SerialPort({
|
||||
path: serverConfig.xdrd.comPort,
|
||||
@@ -157,13 +112,14 @@ if (serverConfig.xdrd.wirelessConnection === false) {
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
logInfo('Using COM device: ' + serverConfig.xdrd.comPort);
|
||||
|
||||
logInfo('Using serial port: ' + serverConfig.xdrd.comPort);
|
||||
dataHandler.state.isSerialportAlive = true;
|
||||
pluginsApi.setOutput(serialport);
|
||||
setTimeout(() => {
|
||||
serialport.write('x\n');
|
||||
}, 3000);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
serialport.write('Q0\n');
|
||||
serialport.write('M0\n');
|
||||
@@ -175,34 +131,25 @@ if (serverConfig.xdrd.wirelessConnection === false) {
|
||||
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||
} else if (dataHandler.state.lastFrequencyAlive && dataHandler.state.isSerialportRetrying) { // Serialport retry code when port is open but communication is lost
|
||||
serialport.write('T' + (dataHandler.state.lastFrequencyAlive * 1000) + '\n');
|
||||
} else {
|
||||
serialport.write('T87500\n');
|
||||
}
|
||||
} else serialport.write('T87500\n');
|
||||
dataHandler.state.isSerialportRetrying = false;
|
||||
|
||||
serialport.write('A0\n');
|
||||
if (serverConfig.device === 'si47xx') serialport.write('A0\n');
|
||||
serialport.write('F-1\n');
|
||||
serialport.write('W0\n');
|
||||
serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n');
|
||||
// cEQ and iMS combinations
|
||||
if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") {
|
||||
serialport.write("G00\n"); // Both Disabled
|
||||
} else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") {
|
||||
serialport.write(`G10\n`);
|
||||
} else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") {
|
||||
serialport.write(`G01\n`);
|
||||
} else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") {
|
||||
serialport.write("G11\n"); // Both Enabled
|
||||
}
|
||||
if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") serialport.write("G00\n"); // Both Disabled
|
||||
else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") serialport.write(`G10\n`);
|
||||
else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") serialport.write(`G01\n`);
|
||||
else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") serialport.write("G11\n"); // Both Enabled
|
||||
// Handle stereo mode
|
||||
if (serverConfig.stereoStartup === "1") {
|
||||
serialport.write("B1\n"); // Mono
|
||||
}
|
||||
serverConfig.audio.startupVolume
|
||||
? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n')
|
||||
if (serverConfig.stereoStartup === "1") serialport.write("B1\n"); // Mono
|
||||
serverConfig.audio.startupVolume
|
||||
? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n')
|
||||
: serialport.write('Y100\n');
|
||||
}, 6000);
|
||||
|
||||
|
||||
serialport.on('data', (data) => {
|
||||
helpers.resolveDataBuffer(data, wss, rdsWss);
|
||||
});
|
||||
@@ -214,6 +161,7 @@ if (serverConfig.xdrd.wirelessConnection === false) {
|
||||
|
||||
// Handle port closure
|
||||
serialport.on('close', () => {
|
||||
pluginsApi.setOutput(null);
|
||||
logWarn('Disconnected from ' + serverConfig.xdrd.comPort + '. Attempting to reconnect.');
|
||||
setTimeout(() => {
|
||||
dataHandler.state.isSerialportRetrying = true;
|
||||
@@ -222,7 +170,6 @@ if (serverConfig.xdrd.wirelessConnection === false) {
|
||||
});
|
||||
return serialport;
|
||||
}
|
||||
}
|
||||
|
||||
// xdrd connection
|
||||
let authFlags = {};
|
||||
@@ -233,7 +180,8 @@ function connectToXdrd() {
|
||||
if (xdrd.wirelessConnection && configExists()) {
|
||||
client.connect(xdrd.xdrdPort, xdrd.xdrdIp, () => {
|
||||
logInfo('Connection to xdrd established successfully.');
|
||||
|
||||
pluginsApi.setOutput(client);
|
||||
|
||||
authFlags = {
|
||||
authMsg: false,
|
||||
firstClient: false,
|
||||
@@ -247,11 +195,9 @@ function connectToXdrd() {
|
||||
|
||||
client.on('data', (data) => {
|
||||
const { xdrd } = serverConfig;
|
||||
|
||||
|
||||
helpers.resolveDataBuffer(data, wss, rdsWss);
|
||||
if (authFlags.authMsg == true && authFlags.messageCount > 1) {
|
||||
return;
|
||||
}
|
||||
if (authFlags.authMsg == true && authFlags.messageCount > 1) return;
|
||||
|
||||
authFlags.messageCount++;
|
||||
const receivedData = data.toString();
|
||||
@@ -266,9 +212,8 @@ client.on('data', (data) => {
|
||||
if (line.startsWith('a')) {
|
||||
authFlags.authMsg = true;
|
||||
logWarn('Authentication with xdrd failed. Is your password set correctly?');
|
||||
} else if (line.startsWith('o1,')) {
|
||||
authFlags.firstClient = true;
|
||||
} else if (line.startsWith('T') && line.length <= 7) {
|
||||
} else if (line.startsWith('o1,')) authFlags.firstClient = true;
|
||||
else if (line.startsWith('T') && line.length <= 7) {
|
||||
const freq = line.slice(1) / 1000;
|
||||
dataHandler.dataToSend.freq = freq.toFixed(3);
|
||||
} else if (line.startsWith('OK')) {
|
||||
@@ -291,7 +236,7 @@ client.on('data', (data) => {
|
||||
client.write(serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? 'T' + Math.round(serverConfig.defaultFreq * 1000) + '\n' : 'T87500\n');
|
||||
dataHandler.initialData.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3);
|
||||
dataHandler.dataToSend.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3);
|
||||
client.write('A0\n');
|
||||
if (serverConfig.device === 'si47xx') serialport.write('A0\n');
|
||||
client.write(serverConfig.audio.startupVolume ? 'Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n' : 'Y100\n');
|
||||
serverConfig.webserver.rdsMode ? client.write('D1\n') : client.write('D0\n');
|
||||
return;
|
||||
@@ -301,14 +246,13 @@ client.on('data', (data) => {
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
pluginsApi.setOutput(null);
|
||||
if(serverConfig.autoShutdown === false) {
|
||||
logWarn('Disconnected from xdrd. Attempting to reconnect.');
|
||||
setTimeout(function () {
|
||||
connectToXdrd();
|
||||
}, 2000)
|
||||
} else {
|
||||
logWarn('Disconnected from xdrd.');
|
||||
}
|
||||
} else logWarn('Disconnected from xdrd.');
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
@@ -335,9 +279,6 @@ app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, '../web'));
|
||||
app.use('/', endpoints);
|
||||
|
||||
/**
|
||||
* WEBSOCKET BLOCK
|
||||
*/
|
||||
const tunerLockTracker = new WeakMap();
|
||||
const ipConnectionCounts = new Map(); // Per-IP limit variables
|
||||
const ipLogTimestamps = new Map();
|
||||
@@ -360,33 +301,24 @@ setInterval(() => {
|
||||
|
||||
wss.on('connection', (ws, request) => {
|
||||
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
||||
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
let clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||
|
||||
|
||||
if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientIp && clientIp.includes(',')) {
|
||||
clientIp = clientIp.split(',')[0].trim();
|
||||
}
|
||||
if (clientIp && clientIp.includes(',')) clientIp = clientIp.split(',')[0].trim();
|
||||
|
||||
// Per-IP limit connection open
|
||||
if (clientIp) {
|
||||
const isLocalIp = (
|
||||
clientIp === '127.0.0.1' ||
|
||||
clientIp === '::1' ||
|
||||
clientIp === '::ffff:127.0.0.1' ||
|
||||
clientIp.startsWith('192.168.') ||
|
||||
clientIp.startsWith('10.') ||
|
||||
clientIp.startsWith('172.16.')
|
||||
);
|
||||
clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1' ||
|
||||
clientIp.startsWith('192.168.') || clientIp.startsWith('10.') || clientIp.startsWith('172.16.'));
|
||||
if (!isLocalIp) {
|
||||
if (!ipConnectionCounts.has(clientIp)) {
|
||||
ipConnectionCounts.set(clientIp, 0);
|
||||
}
|
||||
if (!ipConnectionCounts.has(clientIp)) ipConnectionCounts.set(clientIp, 0);
|
||||
const currentCount = ipConnectionCounts.get(clientIp);
|
||||
if (currentCount >= MAX_CONNECTIONS_PER_IP) {
|
||||
ws.close(1008, 'Too many open connections from this IP');
|
||||
@@ -402,7 +334,9 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers++;
|
||||
}
|
||||
|
||||
@@ -413,51 +347,46 @@ wss.on('connection', (ws, request) => {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
|
||||
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
|
||||
serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
|
||||
}
|
||||
});
|
||||
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
|
||||
});
|
||||
|
||||
const userCommands = {};
|
||||
let lastWarn = { time: 0 };
|
||||
|
||||
ws.on('message', (message) => {
|
||||
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
|
||||
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text', 16 * 1024);
|
||||
|
||||
if (!clientIp.includes("127.0.0.1")) {
|
||||
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
|
||||
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
|
||||
((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
|
||||
logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command: ${command.slice(0, 64)}.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (command.includes("\'")) {
|
||||
return;
|
||||
}
|
||||
if (command.includes("\'")) return;
|
||||
|
||||
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
|
||||
|
||||
if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) {
|
||||
switch (command) {
|
||||
case 'wL1':
|
||||
if (isAdminAuthenticated) serverConfig.lockToAdmin = true;
|
||||
case 'wL1':
|
||||
if (isAdminAuthenticated) serverConfig.lockToAdmin = true;
|
||||
break;
|
||||
case 'wL0':
|
||||
if (isAdminAuthenticated) serverConfig.lockToAdmin = false;
|
||||
case 'wL0':
|
||||
if (isAdminAuthenticated) serverConfig.lockToAdmin = false;
|
||||
break;
|
||||
case 'wT0':
|
||||
serverConfig.publicTuner = true;
|
||||
if(!isAdminAuthenticated) tunerLockTracker.delete(ws);
|
||||
case 'wT0':
|
||||
serverConfig.publicTuner = true;
|
||||
if(!isAdminAuthenticated) tunerLockTracker.delete(ws);
|
||||
break;
|
||||
case 'wT1':
|
||||
case 'wT1':
|
||||
serverConfig.publicTuner = false;
|
||||
if(!isAdminAuthenticated) tunerLockTracker.set(ws, true);
|
||||
break;
|
||||
default:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -465,15 +394,11 @@ wss.on('connection', (ws, request) => {
|
||||
if (command.startsWith('T')) {
|
||||
const tuneFreq = Number(command.slice(1)) / 1000;
|
||||
const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver;
|
||||
|
||||
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) return;
|
||||
}
|
||||
|
||||
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) {
|
||||
output.write(`${command}\n`);
|
||||
}
|
||||
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) output.write(`${command}\n`);
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
@@ -484,7 +409,7 @@ wss.on('connection', (ws, request) => {
|
||||
clientIp === '::1' ||
|
||||
clientIp === '::ffff:127.0.0.1' ||
|
||||
clientIp.startsWith('192.168.') ||
|
||||
clientIp.startsWith('10.') ||
|
||||
clientIp.startsWith('10.') ||
|
||||
clientIp.startsWith('172.16.')
|
||||
);
|
||||
if (!isLocalIp) {
|
||||
@@ -493,98 +418,75 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers--;
|
||||
}
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
|
||||
const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);
|
||||
if (index !== -1) {
|
||||
storage.connectedUsers.splice(index, 1);
|
||||
}
|
||||
const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);
|
||||
if (index !== -1) storage.connectedUsers.splice(index, 1);
|
||||
|
||||
if (currentUsers === 0) {
|
||||
storage.connectedUsers = [];
|
||||
if (currentUsers === 0) {
|
||||
storage.connectedUsers = [];
|
||||
|
||||
if (serverConfig.bwAutoNoUsers === "1") {
|
||||
output.write("W0\n"); // Auto BW 'Enabled'
|
||||
}
|
||||
if (serverConfig.bwAutoNoUsers === "1") output.write("W0\n"); // Auto BW 'Enabled'
|
||||
|
||||
// cEQ and iMS combinations
|
||||
if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") {
|
||||
output.write("G00\n"); // Both Disabled
|
||||
} else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") {
|
||||
output.write(`G0${dataHandler.dataToSend.ims}\n`);
|
||||
} else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") {
|
||||
output.write(`G${dataHandler.dataToSend.eq}0\n`);
|
||||
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") {
|
||||
output.write(`G1${dataHandler.dataToSend.ims}\n`);
|
||||
} else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") {
|
||||
output.write(`G${dataHandler.dataToSend.eq}1\n`);
|
||||
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") {
|
||||
output.write("G10\n"); // Only cEQ enabled
|
||||
} else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") {
|
||||
output.write("G01\n"); // Only iMS enabled
|
||||
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") {
|
||||
output.write("G11\n"); // Both Enabled
|
||||
}
|
||||
// cEQ and iMS combinations
|
||||
if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") output.write("G00\n"); // Both Disabled
|
||||
else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") output.write(`G0${dataHandler.dataToSend.ims}\n`);
|
||||
else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") output.write(`G${dataHandler.dataToSend.eq}0\n`);
|
||||
else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") output.write(`G1${dataHandler.dataToSend.ims}\n`);
|
||||
else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") output.write(`G${dataHandler.dataToSend.eq}1\n`);
|
||||
else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") output.write("G10\n"); // Only cEQ enabled
|
||||
else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") output.write("G01\n"); // Only iMS enabled
|
||||
else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") output.write("G11\n"); // Both Enabled
|
||||
|
||||
// Handle stereo mode
|
||||
if (serverConfig.stereoNoUsers === "1") {
|
||||
output.write("B0\n");
|
||||
} else if (serverConfig.stereoNoUsers === "2") {
|
||||
output.write("B1\n");
|
||||
}
|
||||
// Handle stereo mode
|
||||
if (serverConfig.stereoNoUsers === "1") output.write("B0\n");
|
||||
else if (serverConfig.stereoNoUsers === "2") output.write("B1\n");
|
||||
|
||||
// Handle Antenna selection
|
||||
if (timeoutAntenna) clearTimeout(timeoutAntenna);
|
||||
timeoutAntenna = setTimeout(() => {
|
||||
if (serverConfig.antennaNoUsers === "1") {
|
||||
output.write("Z0\n");
|
||||
} else if (serverConfig.antennaNoUsers === "2") {
|
||||
output.write("Z1\n");
|
||||
} else if (serverConfig.antennaNoUsers === "3") {
|
||||
output.write("Z2\n");
|
||||
} else if (serverConfig.antennaNoUsers === "4") {
|
||||
output.write("Z3\n");
|
||||
}
|
||||
}, serverConfig.antennaNoUsersDelay ? 15000 : 0);
|
||||
}
|
||||
// Handle Antenna selection
|
||||
if (timeoutAntenna) clearTimeout(timeoutAntenna);
|
||||
timeoutAntenna = setTimeout(() => {
|
||||
if (serverConfig.antennaNoUsers === "1") output.write("Z0\n");
|
||||
else if (serverConfig.antennaNoUsers === "2") output.write("Z1\n");
|
||||
else if (serverConfig.antennaNoUsers === "3") output.write("Z2\n");
|
||||
else if (serverConfig.antennaNoUsers === "4") output.write("Z3\n");
|
||||
}, serverConfig.antennaNoUsersDelay ? 15000 : 0);
|
||||
}
|
||||
|
||||
if (tunerLockTracker.has(ws)) {
|
||||
logInfo(`User who locked the tuner left. Unlocking the tuner.`);
|
||||
output.write('wT0\n')
|
||||
tunerLockTracker.delete(ws);
|
||||
serverConfig.publicTuner = true;
|
||||
}
|
||||
if (tunerLockTracker.has(ws)) {
|
||||
logInfo(`User who locked the tuner left. Unlocking the tuner.`);
|
||||
output.write('wT0\n')
|
||||
tunerLockTracker.delete(ws);
|
||||
serverConfig.publicTuner = true;
|
||||
}
|
||||
|
||||
if (currentUsers === 0 && serverConfig.enableDefaultFreq === true &&
|
||||
serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
|
||||
setTimeout(function() {
|
||||
if (currentUsers === 0) {
|
||||
output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
|
||||
dataHandler.resetToDefault(dataHandler.dataToSend);
|
||||
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||
dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
if (currentUsers === 0 && serverConfig.enableDefaultFreq === true &&
|
||||
serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
|
||||
setTimeout(function() {
|
||||
if (currentUsers === 0) {
|
||||
output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
|
||||
dataHandler.resetToDefault(dataHandler.dataToSend);
|
||||
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||
dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) {
|
||||
client.write('X\n');
|
||||
}
|
||||
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) client.write('X\n');
|
||||
|
||||
if (code !== 1008) {
|
||||
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
|
||||
}
|
||||
if (code !== 1008) logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
|
||||
});
|
||||
|
||||
ws.on('error', console.error);
|
||||
});
|
||||
|
||||
// Additional web socket for using plugins
|
||||
pluginsWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
pluginsWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
@@ -600,7 +502,7 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
|
||||
let messageData;
|
||||
|
||||
try {
|
||||
try { // JS Requires the try statement to have braces, unlike the if statement. This extends the huge list of proofs that this is a fucking toy language
|
||||
messageData = JSON.parse(message); // Attempt to parse the JSON
|
||||
} catch (error) {
|
||||
// console.error("Failed to parse message:", error); // Log the error
|
||||
@@ -611,9 +513,7 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
|
||||
// Broadcast the message to all other clients
|
||||
pluginsWss.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(modifiedMessage); // Send the message to all clients
|
||||
}
|
||||
if (client.readyState === WebSocket.OPEN) client.send(modifiedMessage); // Send the message to all clients
|
||||
});
|
||||
});
|
||||
|
||||
@@ -622,58 +522,28 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Websocket register for /text, /audio and /chat paths
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
if (request.url === '/text') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/audio') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
audioWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
audioWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
chatWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/rds' || request.url === '/rdsspy') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
rdsWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
rdsWss.emit('connection', ws, request);
|
||||
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
|
||||
// Anti-spam tracking for each client
|
||||
const userCommands = {};
|
||||
let lastWarn = { time: 0 };
|
||||
|
||||
ws.on('message', function incoming(message) {
|
||||
// Anti-spam
|
||||
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/data_plugins') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
pluginsWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
pluginsWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Websocket register for /text, /audio and /chat paths
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
var upgradeWss = undefined;
|
||||
if (request.url === '/text') upgradeWss = wss;
|
||||
else if (request.url === '/audio') upgradeWss = audioWss;
|
||||
else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) upgradeWss = chatWss;
|
||||
else if (request.url === '/rds' || request.url === '/rdsspy') upgradeWss = rdsWss;
|
||||
else if (request.url === '/data_plugins') upgradeWss = pluginsWss;
|
||||
|
||||
if(upgradeWss) {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
upgradeWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
upgradeWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else socket.destroy();
|
||||
});
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user
|
||||
@@ -691,18 +561,16 @@ helpers.checkIPv6Support((isIPv6Supported) => {
|
||||
|
||||
const startServer = (address, isIPv6) => {
|
||||
httpServer.listen(port, address, () => {
|
||||
if (!isIPv6 && !configExists()) {
|
||||
logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`);
|
||||
} else {
|
||||
logServerStart(address, isIPv6);
|
||||
}
|
||||
if (!isIPv6 && !configExists()) logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`);
|
||||
else logServerStart(address, isIPv6);
|
||||
});
|
||||
};
|
||||
|
||||
if (isIPv6Supported) {
|
||||
startServer(ipv4Address, false); // Start on IPv4
|
||||
startServer(ipv6Address, true); // Start on IPv6
|
||||
} else {
|
||||
startServer(ipv4Address, false); // Start only on IPv4
|
||||
}
|
||||
} else startServer(ipv4Address, false); // Start only on IPv4
|
||||
});
|
||||
|
||||
pluginsApi.registerServerContext({ wss, pluginsWss, httpServer, serverConfig });
|
||||
module.exports = { wss, pluginsWss, httpServer, serverConfig };
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const consoleCmd = require('./console');
|
||||
const { serverConfig } = require('./server_config');
|
||||
|
||||
// Function to read all .js files in a directory
|
||||
function readJSFiles(dir) {
|
||||
@@ -11,7 +10,6 @@ function readJSFiles(dir) {
|
||||
|
||||
// Function to parse plugin config from a file
|
||||
function parsePluginConfig(filePath) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const pluginConfig = {};
|
||||
|
||||
// Assuming pluginConfig is a JavaScript object defined in each .js file
|
||||
@@ -31,9 +29,7 @@ function parsePluginConfig(filePath) {
|
||||
}
|
||||
|
||||
// Check if the destination directory exists, if not, create it
|
||||
if (!fs.existsSync(destinationDir)) {
|
||||
fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
|
||||
}
|
||||
if (!fs.existsSync(destinationDir)) fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
|
||||
|
||||
const destinationFile = path.join(destinationDir, path.basename(sourcePath));
|
||||
|
||||
@@ -41,9 +37,7 @@ function parsePluginConfig(filePath) {
|
||||
if (process.platform !== 'win32') {
|
||||
// On Linux, create a symlink
|
||||
try {
|
||||
if (fs.existsSync(destinationFile)) {
|
||||
fs.unlinkSync(destinationFile); // Remove existing file/symlink
|
||||
}
|
||||
if (fs.existsSync(destinationFile)) fs.unlinkSync(destinationFile); // Remove existing file/symlink
|
||||
fs.symlinkSync(sourcePath, destinationFile);
|
||||
setTimeout(function() {
|
||||
consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
|
||||
@@ -52,9 +46,7 @@ function parsePluginConfig(filePath) {
|
||||
console.error(`Error creating symlink at ${destinationFile}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`Error: frontEndPath is not defined in ${filePath}`);
|
||||
}
|
||||
} else console.error(`Error: frontEndPath is not defined in ${filePath}`);
|
||||
} catch (err) {
|
||||
console.error(`Error parsing plugin config from ${filePath}: ${err.message}`);
|
||||
}
|
||||
@@ -71,9 +63,7 @@ function collectPluginConfigs() {
|
||||
jsFiles.forEach(file => {
|
||||
const filePath = path.join(pluginsDir, file);
|
||||
const config = parsePluginConfig(filePath);
|
||||
if (Object.keys(config).length > 0) {
|
||||
pluginConfigs.push(config);
|
||||
}
|
||||
if (Object.keys(config).length > 0) pluginConfigs.push(config);
|
||||
});
|
||||
|
||||
return pluginConfigs;
|
||||
@@ -81,9 +71,7 @@ function collectPluginConfigs() {
|
||||
|
||||
// Ensure the web/js/plugins directory exists
|
||||
const webJsPluginsDir = path.join(__dirname, '../web/js/plugins');
|
||||
if (!fs.existsSync(webJsPluginsDir)) {
|
||||
fs.mkdirSync(webJsPluginsDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(webJsPluginsDir)) fs.mkdirSync(webJsPluginsDir, { recursive: true });
|
||||
|
||||
// Main function to create symlinks/junctions for plugins
|
||||
function createLinks() {
|
||||
@@ -93,13 +81,8 @@ function createLinks() {
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, create a junction
|
||||
try {
|
||||
if (fs.existsSync(destinationPluginsDir)) {
|
||||
fs.rmSync(destinationPluginsDir, { recursive: true });
|
||||
}
|
||||
if (fs.existsSync(destinationPluginsDir)) fs.rmSync(destinationPluginsDir, { recursive: true });
|
||||
fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction');
|
||||
setTimeout(function() {
|
||||
//consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`);
|
||||
}
|
||||
@@ -110,6 +93,4 @@ function createLinks() {
|
||||
const allPluginConfigs = collectPluginConfigs();
|
||||
createLinks();
|
||||
|
||||
module.exports = {
|
||||
allPluginConfigs
|
||||
};
|
||||
module.exports = allPluginConfigs;
|
||||
|
||||
137
server/plugins_api.js
Normal file
137
server/plugins_api.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// plugins_api.js
|
||||
// Shared API for server plugins:
|
||||
// - Provides privileged/admin command access
|
||||
// - Exposes server-side hooks for inter-plugin communication
|
||||
// - Optionally broadcasts events to connected plugin WebSocket clients
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const { logWarn, logError } = require('./console');
|
||||
|
||||
let output = null;
|
||||
let wss = null;
|
||||
let pluginsWss = null;
|
||||
let httpServer = null;
|
||||
let serverConfig = null;
|
||||
|
||||
// ---- internal plugin event bus ----
|
||||
|
||||
const pluginEvents = new EventEmitter();
|
||||
// prevent accidental memory leak warnings
|
||||
pluginEvents.setMaxListeners(50);
|
||||
|
||||
// ---- registration server side ----
|
||||
|
||||
function registerServerContext(ctx) {
|
||||
if (ctx.wss) wss = ctx.wss;
|
||||
if (ctx.pluginsWss) pluginsWss = ctx.pluginsWss;
|
||||
if (ctx.httpServer) httpServer = ctx.httpServer;
|
||||
if (ctx.serverConfig) serverConfig = ctx.serverConfig;
|
||||
}
|
||||
|
||||
function setOutput(newOutput) {
|
||||
output = newOutput;
|
||||
}
|
||||
|
||||
function clearOutput() {
|
||||
output = null;
|
||||
}
|
||||
|
||||
// ---- accessors plugin side ----
|
||||
|
||||
function getWss() {
|
||||
return wss;
|
||||
}
|
||||
|
||||
function getPluginsWss() {
|
||||
return pluginsWss;
|
||||
}
|
||||
|
||||
function getHttpServer() {
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function getServerConfig() {
|
||||
return serverConfig;
|
||||
}
|
||||
|
||||
// ---- privileged command path ----
|
||||
|
||||
async function sendPrivilegedCommand(command, isPluginInternal = false) {
|
||||
const maxWait = 10000;
|
||||
const interval = 500;
|
||||
let waited = 0;
|
||||
|
||||
while (!output && waited < maxWait) {
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
waited += interval;
|
||||
}
|
||||
|
||||
if (!output) {
|
||||
logError(`[Privileged Send] Timeout waiting for output (${command})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPluginInternal) {
|
||||
output.write(`${command}\n`);
|
||||
//logInfo(`[Privileged Plugin] Command sent: ${command}`); // Debug
|
||||
return true;
|
||||
}
|
||||
|
||||
logWarn(`[Privileged Send] Rejected (not internal): ${command.slice(0, 64)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- plugin hook API ----
|
||||
|
||||
function emitPluginEvent(event, payload, opts = {}) {
|
||||
pluginEvents.emit(event, payload);
|
||||
|
||||
// Stop here unless option to broadcast to clients if true
|
||||
if (opts.broadcast === false) return;
|
||||
|
||||
// Broadcast to connected plugin WebSocket clients if available
|
||||
if (pluginsWss) {
|
||||
const message = JSON.stringify({ type: event, value: payload });
|
||||
pluginsWss.clients.forEach((client) => {
|
||||
if (client.readyState === client.OPEN) {
|
||||
try {
|
||||
// Send event to client
|
||||
client.send(message);
|
||||
} catch (err) {
|
||||
logWarn(`[plugins_api] Failed to send ${event} to client: ${err.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onPluginEvent(event, handler) {
|
||||
pluginEvents.on(event, handler);
|
||||
}
|
||||
|
||||
function offPluginEvent(event, handler) {
|
||||
pluginEvents.off(event, handler);
|
||||
}
|
||||
|
||||
// ---- exports ----
|
||||
|
||||
module.exports = {
|
||||
// server registration
|
||||
registerServerContext,
|
||||
setOutput,
|
||||
clearOutput,
|
||||
|
||||
// server context access
|
||||
getWss,
|
||||
getPluginsWss,
|
||||
getHttpServer,
|
||||
getServerConfig,
|
||||
|
||||
// privileged control
|
||||
sendPrivilegedCommand,
|
||||
|
||||
// inter-plugin hooks
|
||||
emitPluginEvent,
|
||||
onPluginEvent,
|
||||
offPluginEvent
|
||||
};
|
||||
185
server/rds.js
Normal file
185
server/rds.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const { rdsEccLookup, iso, countries } = require("./rds_country.js")
|
||||
|
||||
class RDSDecoder {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.clear()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.data.pi = '?';
|
||||
this.ps = Array(8).fill(' ');
|
||||
this.ps_errors = Array(8).fill("0");
|
||||
this.rt0 = Array(64).fill(' ');
|
||||
this.rt0_errors = Array(64).fill("0");
|
||||
this.rt1 = Array(64).fill(' ');
|
||||
this.rt1_errors = Array(64).fill("0");
|
||||
this.data.ps = '';
|
||||
this.data.rt1 = '';
|
||||
this.data.rt0 = '';
|
||||
this.data.pty = 0;
|
||||
this.data.tp = 0;
|
||||
this.data.ta = 0;
|
||||
this.data.ms = -1;
|
||||
this.data.rt_flag = 0;
|
||||
this.rt1_to_clear = false;
|
||||
this.rt0_to_clear = false;
|
||||
this.data.ecc = null;
|
||||
this.data.country_name = ""
|
||||
this.data.country_iso = "UN"
|
||||
|
||||
this.af_len = 0;
|
||||
this.data.af = []
|
||||
this.af_am_follows = false;
|
||||
|
||||
this.last_pi_error = 0;
|
||||
}
|
||||
|
||||
decodeGroup(blockA, blockB, blockC, blockD, error) {
|
||||
const a_error = (error >> 6) & 3;
|
||||
const b_error = (error >> 4) & 3;
|
||||
const c_error = (error >> 2) & 3;
|
||||
const d_error = error & 3;
|
||||
|
||||
if(this.last_pi_error > a_error) {
|
||||
this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0');
|
||||
this.last_pi_error = a_error;
|
||||
}
|
||||
|
||||
if(b_error !== 0) return; // B chooses what group this is, if this has errors, we are screwed
|
||||
|
||||
const group = (blockB >> 12) & 0xF;
|
||||
const version = (blockB >> 11) & 0x1;
|
||||
this.data.tp = Number((blockB >> 10) & 1);
|
||||
this.data.pty = (blockB >> 5) & 31;
|
||||
|
||||
if (group === 0) {
|
||||
this.data.ta = (blockB >> 4) & 1;
|
||||
this.data.ms = (blockB >> 3) & 1;
|
||||
|
||||
if(version === 0 && c_error !== 3) {
|
||||
var af_high = blockC >> 8;
|
||||
var af_low = blockC & 0xFF;
|
||||
var BASE = 224;
|
||||
var FILLER = 205;
|
||||
var AM_FOLLOWS = 250;
|
||||
|
||||
if(af_high >= BASE && af_high <= (BASE+25)) {
|
||||
this.af_len = af_high-BASE;
|
||||
if(this.af_len !== this.data.af.length) {
|
||||
this.data.af = [];
|
||||
this.af_am_follows = false;
|
||||
|
||||
if(af_low != FILLER && af_low != AM_FOLLOWS) this.data.af.push((af_low+875)*100)
|
||||
else if(af_low == AM_FOLLOWS) this.af_am_follows = true;
|
||||
}
|
||||
} else if(this.data.af.length != this.af_len) {
|
||||
if(!(af_high == AM_FOLLOWS || this.af_am_follows)) {
|
||||
var freq = (af_high+875)*100;
|
||||
if(!this.data.af.includes(freq)) this.data.af.push(freq);
|
||||
}
|
||||
if(this.af_am_follows) this.af_am_follows = false;
|
||||
if(!(af_high == AM_FOLLOWS || af_low == FILLER || af_low == AM_FOLLOWS)) {
|
||||
var freq = (af_low+875)*100;
|
||||
if(!this.data.af.includes(freq)) this.data.af.push(freq);
|
||||
}
|
||||
if(af_low == AM_FOLLOWS) this.af_am_follows = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(d_error > 2) return; // Don't risk it
|
||||
|
||||
const idx = blockB & 0x3;
|
||||
|
||||
this.ps[idx * 2] = String.fromCharCode(blockD >> 8);
|
||||
this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF);
|
||||
this.ps_errors[idx * 2] = Math.ceil(d_error * (10/3));
|
||||
this.ps_errors[idx * 2 + 1] = Math.ceil(d_error * (10/3));
|
||||
|
||||
this.data.ps = this.ps.join('');
|
||||
this.data.ps_errors = this.ps_errors.join(',');
|
||||
} else if (group === 1 && version === 0) {
|
||||
if(c_error > 2) return;
|
||||
var variant_code = (blockC >> 12) & 0x7;
|
||||
switch (variant_code) {
|
||||
case 0:
|
||||
this.data.ecc = blockC & 0xff;
|
||||
this.data.country_name = rdsEccLookup(blockA, this.data.ecc);
|
||||
if(this.data.country_name.length === 0) this.data.country_iso = "UN";
|
||||
else this.data.country_iso = iso[countries.indexOf(this.data.country_name)]
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
} else if (group === 2) {
|
||||
const idx = blockB & 0b1111;
|
||||
this.rt_ab = Boolean((blockB >> 4) & 1);
|
||||
var multiplier = (version == 0) ? 4 : 2;
|
||||
if(this.rt_ab) {
|
||||
if(this.rt1_to_clear) {
|
||||
this.rt1 = Array(64).fill(' ');
|
||||
this.rt1_errors = Array(64).fill("0");
|
||||
this.rt1_to_clear = false;
|
||||
}
|
||||
|
||||
if(c_error < 2 && multiplier !== 2) {
|
||||
this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8);
|
||||
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
|
||||
this.rt1_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
|
||||
this.rt1_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
|
||||
}
|
||||
if(d_error < 2) {
|
||||
var offset = (multiplier == 2) ? 0 : 2;
|
||||
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
|
||||
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
|
||||
this.rt1_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
|
||||
this.rt1_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
|
||||
}
|
||||
|
||||
var i = this.rt1.indexOf("\r")
|
||||
while(i != -1) {
|
||||
this.rt1[i] = " ";
|
||||
i = this.rt1.indexOf("\r");
|
||||
}
|
||||
|
||||
this.data.rt1 = this.rt1.join('');
|
||||
this.data.rt1_errors = this.rt1_errors.join(',');
|
||||
this.data.rt_flag = 1;
|
||||
this.rt0_to_clear = true;
|
||||
} else {
|
||||
if(this.rt0_to_clear) {
|
||||
this.rt0 = Array(64).fill(' ');
|
||||
this.rt0_errors = Array(64).fill("0");
|
||||
this.rt0_to_clear = false;
|
||||
}
|
||||
|
||||
if(c_error !== 3 && multiplier !== 2) {
|
||||
this.rt0[idx * multiplier] = String.fromCharCode(blockC >> 8);
|
||||
this.rt0[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
|
||||
this.rt0_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
|
||||
this.rt0_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
|
||||
}
|
||||
if(d_error !== 3) {
|
||||
var offset = (multiplier == 2) ? 0 : 2;
|
||||
this.rt0[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
|
||||
this.rt0[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
|
||||
this.rt0_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
|
||||
this.rt0_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
|
||||
}
|
||||
|
||||
var i = this.rt0.indexOf("\r");
|
||||
while(i != -1) {
|
||||
this.rt0[i] = " ";
|
||||
i = this.rt0.indexOf("\r");
|
||||
}
|
||||
|
||||
this.data.rt0 = this.rt0.join('');
|
||||
this.data.rt0_errors = this.rt0_errors.join(',');
|
||||
this.data.rt_flag = 0;
|
||||
this.rt1_to_clear = true;
|
||||
}
|
||||
} else {
|
||||
// console.log(group, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = RDSDecoder;
|
||||
632
server/rds_country.js
Normal file
632
server/rds_country.js
Normal file
@@ -0,0 +1,632 @@
|
||||
var countries = [
|
||||
"Albania",
|
||||
"Estonia",
|
||||
"Algeria",
|
||||
"Ethiopia",
|
||||
"Andorra",
|
||||
"Angola",
|
||||
"Finland",
|
||||
"Armenia",
|
||||
"France",
|
||||
"Ascension Island",
|
||||
"Gabon",
|
||||
"Austria",
|
||||
"Gambia",
|
||||
"Azerbaijan",
|
||||
"Georgia",
|
||||
"Germany",
|
||||
"Bahrein",
|
||||
"Ghana",
|
||||
"Belarus",
|
||||
"Gibraltar",
|
||||
"Belgium",
|
||||
"Greece",
|
||||
"Benin",
|
||||
"Guinea",
|
||||
"Bosnia Herzegovina",
|
||||
"Guinea-Bissau",
|
||||
"Botswana",
|
||||
"Hungary",
|
||||
"Bulgaria",
|
||||
"Iceland",
|
||||
"Burkina Faso",
|
||||
"Iraq",
|
||||
"Burundi",
|
||||
"Ireland",
|
||||
"Cabinda",
|
||||
"Israel",
|
||||
"Cameroon",
|
||||
"Italy",
|
||||
"Jordan",
|
||||
"Cape Verde",
|
||||
"Kazakhstan",
|
||||
"Central African Republic",
|
||||
"Kenya",
|
||||
"Chad",
|
||||
"Kosovo",
|
||||
"Comoros",
|
||||
"Kuwait",
|
||||
"DR Congo",
|
||||
"Kyrgyzstan",
|
||||
"Republic of Congo",
|
||||
"Latvia",
|
||||
"Cote d'Ivoire",
|
||||
"Lebanon",
|
||||
"Croatia",
|
||||
"Lesotho",
|
||||
"Cyprus",
|
||||
"Liberia",
|
||||
"Czechia",
|
||||
"Libya",
|
||||
"Denmark",
|
||||
"Liechtenstein",
|
||||
"Djiboutia",
|
||||
"Lithuania",
|
||||
"Egypt",
|
||||
"Luxembourg",
|
||||
"Equatorial Guinea",
|
||||
"Macedonia",
|
||||
"Eritrea",
|
||||
"Madagascar",
|
||||
"Seychelles",
|
||||
"Malawi",
|
||||
"Sierra Leone",
|
||||
"Mali",
|
||||
"Slovakia",
|
||||
"Malta",
|
||||
"Slovenia",
|
||||
"Mauritania",
|
||||
"Somalia",
|
||||
"Mauritius",
|
||||
"South Africa",
|
||||
"Moldova",
|
||||
"South Sudan",
|
||||
"Monaco",
|
||||
"Spain",
|
||||
"Mongolia",
|
||||
"Sudan",
|
||||
"Montenegro",
|
||||
"Swaziland",
|
||||
"Morocco",
|
||||
"Sweden",
|
||||
"Mozambique",
|
||||
"Switzerland",
|
||||
"Namibia",
|
||||
"Syria",
|
||||
"Netherlands",
|
||||
"Tajikistan",
|
||||
"Niger",
|
||||
"Tanzania",
|
||||
"Nigeria",
|
||||
"Togo",
|
||||
"Norway",
|
||||
"Tunisia",
|
||||
"Oman",
|
||||
"Turkey",
|
||||
"Palestine",
|
||||
"Turkmenistan",
|
||||
"Poland",
|
||||
"Uganda",
|
||||
"Portugal",
|
||||
"Ukraine",
|
||||
"Qatar",
|
||||
"United Arab Emirates",
|
||||
"Romania",
|
||||
"United Kingdom",
|
||||
"Russia",
|
||||
"Uzbekistan",
|
||||
"Rwanda",
|
||||
"Vatican",
|
||||
"San Marino",
|
||||
"Western Sahara",
|
||||
"Sao Tome and Principe",
|
||||
"Yemen",
|
||||
"Saudi Arabia",
|
||||
"Zambia",
|
||||
"Senegal",
|
||||
"Zimbabwe",
|
||||
"Serbia",
|
||||
"Anguilla",
|
||||
"Guyana",
|
||||
"Antigua and Barbuda",
|
||||
"Haiti",
|
||||
"Argentina",
|
||||
"Honduras",
|
||||
"Aruba",
|
||||
"Jamaica",
|
||||
"Bahamas",
|
||||
"Martinique",
|
||||
"Barbados",
|
||||
"Mexico",
|
||||
"Belize",
|
||||
"Montserrat",
|
||||
"Brazil/Bermuda",
|
||||
"Brazil/AN",
|
||||
"Bolivia",
|
||||
"Nicaragua",
|
||||
"Brazil",
|
||||
"Panama",
|
||||
"Canada",
|
||||
"Paraguay",
|
||||
"Cayman Islands",
|
||||
"Peru",
|
||||
"Chile",
|
||||
"USA/VI/PR",
|
||||
"Colombia",
|
||||
"St. Kitts",
|
||||
"Costa Rica",
|
||||
"St. Lucia",
|
||||
"Cuba",
|
||||
"St. Pierre and Miquelon",
|
||||
"Dominica",
|
||||
"St. Vincent",
|
||||
"Dominican Republic",
|
||||
"Suriname",
|
||||
"El Salvador",
|
||||
"Trinidad and Tobago",
|
||||
"Turks and Caicos islands",
|
||||
"Falkland Islands",
|
||||
"Greenland",
|
||||
"Uruguay",
|
||||
"Grenada",
|
||||
"Venezuela",
|
||||
"Guadeloupe",
|
||||
"Virgin Islands",
|
||||
"Guatemala",
|
||||
"Afghanistan",
|
||||
"South Korea",
|
||||
"Laos",
|
||||
"Australia Capital Territory",
|
||||
"Macao",
|
||||
"Australia New South Wales",
|
||||
"Malaysia",
|
||||
"Australia Victoria",
|
||||
"Maldives",
|
||||
"Australia Queensland",
|
||||
"Marshall Islands",
|
||||
"Australia South Australia",
|
||||
"Micronesia",
|
||||
"Australia Western Australia",
|
||||
"Myanmar",
|
||||
"Australia Tasmania",
|
||||
"Nauru",
|
||||
"Australia Northern Territory",
|
||||
"Nepal",
|
||||
"Bangladesh",
|
||||
"New Zealand",
|
||||
"Bhutan",
|
||||
"Pakistan",
|
||||
"Brunei Darussalam",
|
||||
"Papua New Guinea",
|
||||
"Cambodia",
|
||||
"Philippines",
|
||||
"China",
|
||||
"Samoa",
|
||||
"Singapore",
|
||||
"Solomon Islands",
|
||||
"Fiji",
|
||||
"Sri Lanka",
|
||||
"Hong Kong",
|
||||
"Taiwan",
|
||||
"India",
|
||||
"Thailand",
|
||||
"Indonesia",
|
||||
"Tonga",
|
||||
"Iran",
|
||||
"Vanuatu",
|
||||
"Japan",
|
||||
"Vietnam",
|
||||
"Kiribati",
|
||||
"North Korea",
|
||||
"Brazil/Equator"
|
||||
]
|
||||
|
||||
var iso = [
|
||||
"AL",
|
||||
"EE",
|
||||
"DZ",
|
||||
"ET",
|
||||
"AD",
|
||||
"AO",
|
||||
"FI",
|
||||
"AM",
|
||||
"FR",
|
||||
"SH",
|
||||
"GA",
|
||||
"AT",
|
||||
"GM",
|
||||
"AZ",
|
||||
"GE",
|
||||
"DE",
|
||||
"BH",
|
||||
"GH",
|
||||
"BY",
|
||||
"GI",
|
||||
"BE",
|
||||
"GR",
|
||||
"BJ",
|
||||
"GN",
|
||||
"BA",
|
||||
"GW",
|
||||
"BW",
|
||||
"HU",
|
||||
"BG",
|
||||
"IS",
|
||||
"BF",
|
||||
"IQ",
|
||||
"BI",
|
||||
"IE",
|
||||
"--",
|
||||
"IL",
|
||||
"CM",
|
||||
"IT",
|
||||
"JO",
|
||||
"CV",
|
||||
"KZ",
|
||||
"CF",
|
||||
"KE",
|
||||
"TD",
|
||||
"XK",
|
||||
"KM",
|
||||
"KW",
|
||||
"CD",
|
||||
"KG",
|
||||
"CG",
|
||||
"LV",
|
||||
"CI",
|
||||
"LB",
|
||||
"HR",
|
||||
"LS",
|
||||
"CY",
|
||||
"LR",
|
||||
"CZ",
|
||||
"LY",
|
||||
"DK",
|
||||
"LI",
|
||||
"DJ",
|
||||
"LT",
|
||||
"EG",
|
||||
"LU",
|
||||
"GQ",
|
||||
"MK",
|
||||
"ER",
|
||||
"MG",
|
||||
"SC",
|
||||
"MW",
|
||||
"SL",
|
||||
"ML",
|
||||
"SK",
|
||||
"MT",
|
||||
"SI",
|
||||
"MR",
|
||||
"SO",
|
||||
"MU",
|
||||
"ZA",
|
||||
"MD",
|
||||
"SS",
|
||||
"MC",
|
||||
"ES",
|
||||
"MN",
|
||||
"SD",
|
||||
"ME",
|
||||
"SZ",
|
||||
"MA",
|
||||
"SE",
|
||||
"MZ",
|
||||
"CH",
|
||||
"NA",
|
||||
"SY",
|
||||
"NL",
|
||||
"TJ",
|
||||
"NE",
|
||||
"TZ",
|
||||
"NG",
|
||||
"TG",
|
||||
"NO",
|
||||
"TN",
|
||||
"OM",
|
||||
"TR",
|
||||
"PS",
|
||||
"TM",
|
||||
"PL",
|
||||
"UG",
|
||||
"PT",
|
||||
"UA",
|
||||
"QA",
|
||||
"AE",
|
||||
"RO",
|
||||
"GB",
|
||||
"RU",
|
||||
"UZ",
|
||||
"RW",
|
||||
"VA",
|
||||
"SM",
|
||||
"EH",
|
||||
"ST",
|
||||
"YE",
|
||||
"SA",
|
||||
"ZM",
|
||||
"SN",
|
||||
"ZW",
|
||||
"RS",
|
||||
"AI",
|
||||
"GY",
|
||||
"AG",
|
||||
"HT",
|
||||
"AR",
|
||||
"HN",
|
||||
"AW",
|
||||
"JM",
|
||||
"BS",
|
||||
"MQ",
|
||||
"BB",
|
||||
"MX",
|
||||
"BZ",
|
||||
"MS",
|
||||
"--",
|
||||
"--",
|
||||
"BO",
|
||||
"NI",
|
||||
"BR",
|
||||
"PA",
|
||||
"CA",
|
||||
"PY",
|
||||
"KY",
|
||||
"PE",
|
||||
"CL",
|
||||
"--",
|
||||
"CO",
|
||||
"KN",
|
||||
"CR",
|
||||
"LC",
|
||||
"CU",
|
||||
"PM",
|
||||
"DM",
|
||||
"VC",
|
||||
"DO",
|
||||
"SR",
|
||||
"SN",
|
||||
"TT",
|
||||
"TB",
|
||||
"FK",
|
||||
"GL",
|
||||
"UY",
|
||||
"GD",
|
||||
"VE",
|
||||
"GP",
|
||||
"VG",
|
||||
"GT",
|
||||
"AF",
|
||||
"KR",
|
||||
"LA",
|
||||
"AU",
|
||||
"MO",
|
||||
"AU",
|
||||
"MY",
|
||||
"AU",
|
||||
"MV",
|
||||
"AU",
|
||||
"MH",
|
||||
"AU",
|
||||
"FM",
|
||||
"AU",
|
||||
"MM",
|
||||
"AU",
|
||||
"NR",
|
||||
"AU",
|
||||
"NP",
|
||||
"BD",
|
||||
"NZ",
|
||||
"BT",
|
||||
"PK",
|
||||
"BN",
|
||||
"PG",
|
||||
"KH",
|
||||
"PH",
|
||||
"CN",
|
||||
"WS",
|
||||
"SG",
|
||||
"SB",
|
||||
"FJ",
|
||||
"LK",
|
||||
"HK",
|
||||
"TW",
|
||||
"IN",
|
||||
"TH",
|
||||
"ID",
|
||||
"TO",
|
||||
"IR",
|
||||
"VU",
|
||||
"JP",
|
||||
"VN",
|
||||
"KI",
|
||||
"KP",
|
||||
"--"
|
||||
]
|
||||
|
||||
// RDS ECC Lookup Tables - Converted from C to JavaScript
|
||||
|
||||
const rdsEccA0A6Lut = [
|
||||
// A0
|
||||
[
|
||||
"USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR",
|
||||
"USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR",
|
||||
"USA/VI/PR", null, "USA/VI/PR", "USA/VI/PR", null
|
||||
],
|
||||
// A1
|
||||
[
|
||||
null, null, null, null, null,
|
||||
null, null, null, null, null,
|
||||
"Canada", "Canada", "Canada", "Canada", "Greenland"
|
||||
],
|
||||
// A2
|
||||
[
|
||||
"Anguilla", "Antigua and Barbuda", "Brazil/Equator", "Falkland Islands", "Barbados",
|
||||
"Belize", "Cayman Islands", "Costa Rica", "Cuba", "Argentina",
|
||||
"Brazil", "Brazil/Bermuda", "Brazil/AN", "Guadeloupe", "Bahamas"
|
||||
],
|
||||
// A3
|
||||
[
|
||||
"Bolivia", "Colombia", "Jamaica", "Martinique", null,
|
||||
"Paraguay", "Nicaragua", null, "Panama", "Dominica",
|
||||
"Dominican Republic", "Chile", "Grenada", "Turks and Caicos islands", "Guyana"
|
||||
],
|
||||
// A4
|
||||
[
|
||||
"Guatemala", "Honduras", "Aruba", null, "Montserrat",
|
||||
"Trinidad and Tobago", "Peru", "Suriname", "Uruguay", "St. Kitts",
|
||||
"St. Lucia", "El Salvador", "Haiti", "Venezuela", "Virgin Islands"
|
||||
],
|
||||
// A5
|
||||
[
|
||||
null, null, null, null, null,
|
||||
null, null, null, null, null,
|
||||
"Mexico", "St. Vincent", "Mexico", "Mexico", "Mexico"
|
||||
],
|
||||
// A6
|
||||
[
|
||||
null, null, null, null, null,
|
||||
null, null, null, null, null,
|
||||
null, null, null, null, "St. Pierre and Miquelon"
|
||||
]
|
||||
];
|
||||
|
||||
const rdsEccD0D4Lut = [
|
||||
// D0
|
||||
[
|
||||
"Cameroon", "Central African Republic", "Djiboutia", "Madagascar", "Mali",
|
||||
"Angola", "Equatorial Guinea", "Gabon", "Guinea", "South Africa",
|
||||
"Burkina Faso", "Republic of Congo", "Togo", "Benin", "Malawi"
|
||||
],
|
||||
// D1
|
||||
[
|
||||
"Namibia", "Liberia", "Ghana", "Mauritania", "Sao Tome and Principe",
|
||||
"Cape Verde", "Senegal", "Gambia", "Burundi", "Ascension Island",
|
||||
"Botswana", "Comoros", "Tanzania", "Ethiopia", "Nigeria"
|
||||
],
|
||||
// D2
|
||||
[
|
||||
"Sierra Leone", "Zimbabwe", "Mozambique", "Uganda", "Swaziland",
|
||||
"Kenya", "Somalia", "Niger", "Chad", "Guinea-Bissau",
|
||||
"DR Congo", "Cote d'Ivoire", null, "Zambia", "Eritrea"
|
||||
],
|
||||
// D3
|
||||
[
|
||||
null, null, "Western Sahara", "Cabinda", "Rwanda",
|
||||
"Lesotho", null, "Seychelles", null, "Mauritius",
|
||||
null, "Sudan", null, null, null
|
||||
],
|
||||
// D4
|
||||
[
|
||||
null, null, null, null, null,
|
||||
null, null, null, null, "South Sudan",
|
||||
null, null, null, null, null
|
||||
]
|
||||
];
|
||||
|
||||
const rdsEccE0E5Lut = [
|
||||
// E0
|
||||
[
|
||||
"Germany", "Algeria", "Andorra", "Israel", "Italy",
|
||||
"Belgium", "Russia", "Palestine", "Albania", "Austria",
|
||||
"Hungary", "Malta", "Germany", null, "Egypt"
|
||||
],
|
||||
// E1
|
||||
[
|
||||
"Greece", "Cyprus", "San Marino", "Switzerland", "Jordan",
|
||||
"Finland", "Luxembourg", "Bulgaria", "Denmark", "Gibraltar",
|
||||
"Iraq", "United Kingdom", "Libya", "Romania", "France"
|
||||
],
|
||||
// E2
|
||||
[
|
||||
"Morocco", "Czechia", "Poland", "Vatican", "Slovakia",
|
||||
"Syria", "Tunisia", null, "Liechtenstein", "Iceland",
|
||||
"Monaco", "Lithuania", "Serbia", "Spain", "Norway"
|
||||
],
|
||||
// E3
|
||||
[
|
||||
"Montenegro", "Ireland", "Turkey", null, "Tajikistan",
|
||||
null, null, "Netherlands", "Latvia", "Lebanon",
|
||||
"Azerbaijan", "Croatia", "Kazakhstan", "Sweden", "Belarus"
|
||||
],
|
||||
// E4
|
||||
[
|
||||
"Moldova", "Estonia", "Macedonia", null, null,
|
||||
"Ukraine", "Kosovo", "Portugal", "Slovenia", "Armenia",
|
||||
"Uzbekistan", "Georgia", null, "Turkmenistan", "Bosnia Herzegovina"
|
||||
],
|
||||
// E5
|
||||
[
|
||||
null, null, "Kyrgyzstan", null, null,
|
||||
null, null, null, null, null,
|
||||
null, null, null, null, null
|
||||
]
|
||||
];
|
||||
|
||||
const rdsEccF0F4Lut = [
|
||||
// F0
|
||||
[
|
||||
"Australia Capital Territory", "Australia New South Wales", "Australia Victoria", "Australia Queensland", "Australia South Australia",
|
||||
"Australia Western Australia", "Australia Tasmania", "Australia Northern Territory", "Saudi Arabia", "Afghanistan",
|
||||
"Myanmar", "China", "North Korea", "Bahrein", "Malaysia"
|
||||
],
|
||||
// F1
|
||||
[
|
||||
"Kiribati", "Bhutan", "Bangladesh", "Pakistan", "Fiji",
|
||||
"Oman", "Nauru", "Iran", "New Zealand", "Solomon Islands",
|
||||
"Brunei Darussalam", "Sri Lanka", "Taiwan", "South Korea", "Hong Kong"
|
||||
],
|
||||
// F2
|
||||
[
|
||||
"Kuwait", "Qatar", "Cambodia", "Samoa", "India",
|
||||
"Macao", "Vietnam", "Philippines", "Japan", "Singapore",
|
||||
"Maldives", "Indonesia", "United Arab Emirates", "Nepal", "Vanuatu"
|
||||
],
|
||||
// F3
|
||||
[
|
||||
"Laos", "Thailand", "Tonga", null, null,
|
||||
null, null, "China", "Papua New Guinea", null,
|
||||
"Yemen", null, null, "Micronesia", "Mongolia"
|
||||
],
|
||||
// F4
|
||||
[
|
||||
null, null, null, null, null,
|
||||
null, null, null, "China", null,
|
||||
"Marshall Islands", null, null, null, null
|
||||
]
|
||||
];
|
||||
|
||||
function rdsEccLookup(pi, ecc) {
|
||||
const PI_UNKNOWN = -1;
|
||||
|
||||
const piCountry = (pi >> 12) & 0xF;
|
||||
|
||||
if (pi === PI_UNKNOWN || piCountry === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const piId = piCountry - 1;
|
||||
|
||||
const eccRanges = [
|
||||
{ min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut },
|
||||
{ min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut },
|
||||
{ min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut },
|
||||
{ min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut }
|
||||
];
|
||||
|
||||
// Check each range
|
||||
for (const range of eccRanges) {
|
||||
if (ecc >= range.min && ecc <= range.max) {
|
||||
const eccId = ecc - range.min;
|
||||
return range.lut[eccId][piId];
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rdsEccLookup,
|
||||
iso,
|
||||
countries
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Libraries / Imports */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { logDebug, logError, logInfo, logWarn } = require('./console');
|
||||
const { logError, logInfo } = require('./console');
|
||||
|
||||
let configName = 'config';
|
||||
|
||||
@@ -90,11 +90,14 @@ let serverConfig = {
|
||||
fmlistAdminOnly: false,
|
||||
fmlistOmid: "",
|
||||
},
|
||||
si47xx: {
|
||||
agcControl: false
|
||||
},
|
||||
tunnel: {
|
||||
enabled: false,
|
||||
username: "",
|
||||
token: "",
|
||||
region: "eu",
|
||||
region: "pldx",
|
||||
lowLatencyMode: false,
|
||||
subdomain: "",
|
||||
httpName: "",
|
||||
@@ -130,15 +133,9 @@ let serverConfig = {
|
||||
function addMissingFields(target, source) {
|
||||
Object.keys(source).forEach(function(key) {
|
||||
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
||||
if (!target[key]) {
|
||||
target[key] = {}; // Create missing object
|
||||
}
|
||||
if (!target[key]) target[key] = {}; // Create missing object
|
||||
addMissingFields(target[key], source[key]); // Recursively add missing fields
|
||||
} else {
|
||||
if (target[key] === undefined) {
|
||||
target[key] = source[key]; // Add missing fields only
|
||||
}
|
||||
}
|
||||
} else if(target[key] === undefined) target[key] = source[key]; // Add missing fields only
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,13 +143,9 @@ function addMissingFields(target, source) {
|
||||
function deepMerge(target, source) {
|
||||
Object.keys(source).forEach(function(key) {
|
||||
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
||||
if (!target[key] || typeof target[key] !== 'object') {
|
||||
target[key] = {}; // Ensure target[key] is an object before merging
|
||||
}
|
||||
if (!target[key] || typeof target[key] !== 'object') target[key] = {}; // Ensure target[key] is an object before merging
|
||||
deepMerge(target[key], source[key]); // Recursively merge objects
|
||||
} else {
|
||||
target[key] = source[key]; // Overwrite or add the value
|
||||
}
|
||||
} else target[key] = source[key]; // Overwrite or add the value
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,8 @@ function checkFFmpeg() {
|
||||
});
|
||||
|
||||
checkFFmpegProcess.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve('ffmpeg');
|
||||
} else {
|
||||
resolve(require('ffmpeg-static'));
|
||||
}
|
||||
if (code === 0) resolve('ffmpeg');
|
||||
else resolve(require('ffmpeg-static'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
|
||||
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
||||
|
||||
const sampleRate = Number(this?.Server?.SampleRate || serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
|
||||
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); // Maybe even do 32 khz, we do not need higher than 15 khz precision
|
||||
|
||||
const channels = Number(this?.Server?.Channels || serverConfig.audio.audioChannels || 2);
|
||||
const channels = Number(serverConfig.audio.audioChannels || 2);
|
||||
|
||||
let ffmpeg = null;
|
||||
let restartTimer = null;
|
||||
@@ -48,7 +48,7 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
|
||||
...inputArgs,
|
||||
|
||||
"-thread_queue_size", "1024",
|
||||
"-thread_queue_size", "1536",
|
||||
"-ar", String(sampleRate),
|
||||
"-ac", String(channels),
|
||||
|
||||
@@ -139,4 +139,4 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
logError(`${consoleLogTitle} Error: ${err.message}`);
|
||||
});
|
||||
|
||||
module.exports.audio_pipe = audio_pipe;
|
||||
module.exports = audio_pipe;
|
||||
@@ -28,9 +28,7 @@ function parseAudioDevice(options, callback) {
|
||||
const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1));
|
||||
|
||||
matches.forEach(match => {
|
||||
if (typeof match === 'string') {
|
||||
audioDevices.push({ name: match });
|
||||
}
|
||||
if (typeof match === 'string') audioDevices.push({ name: match });
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error reading file: ${err.message}`);
|
||||
@@ -74,9 +72,7 @@ function parseAudioDevice(options, callback) {
|
||||
if (platform === 'win32' && line.search(/Alternative\sname/) > -1) {
|
||||
const lastDevice = deviceList[deviceList.length - 1];
|
||||
const alt = line.match(alternativeName);
|
||||
if (lastDevice && alt) {
|
||||
lastDevice.alternativeName = alt[1];
|
||||
}
|
||||
if (lastDevice && alt) lastDevice.alternativeName = alt[1];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,11 +103,8 @@ function parseAudioDevice(options, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
if (callbackExists) {
|
||||
execute();
|
||||
} else {
|
||||
return new Promise(execute);
|
||||
}
|
||||
if (callbackExists) execute();
|
||||
else return new Promise(execute);
|
||||
}
|
||||
|
||||
module.exports = { parseAudioDevice };
|
||||
@@ -1,40 +1,28 @@
|
||||
const WebSocket = require('ws');
|
||||
const { serverConfig } = require('../server_config');
|
||||
const { audio_pipe } = require('./index.js');
|
||||
const { PassThrough } = require('stream');
|
||||
const audio_pipe = require('./index.js');
|
||||
|
||||
function createAudioServer() {
|
||||
const audioWss = new WebSocket.Server({ noServer: true });
|
||||
const audioWss = new WebSocket.Server({ noServer: true, skipUTF8Validation: true });
|
||||
|
||||
audioWss.on('connection', (ws, request) => {
|
||||
const clientIp =
|
||||
request.headers['x-forwarded-for'] ||
|
||||
request.connection.remoteAddress;
|
||||
audioWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
audio_pipe.on('data', (chunk) => {
|
||||
audioWss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false});
|
||||
});
|
||||
});
|
||||
|
||||
audio_pipe.on('data', (chunk) => {
|
||||
audioWss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(chunk, {
|
||||
binary: true,
|
||||
compress: false
|
||||
});
|
||||
}
|
||||
});
|
||||
audio_pipe.on('end', () => {
|
||||
audioWss.clients.forEach((client) => {
|
||||
client.close(1001, "Audio stream ended");
|
||||
});
|
||||
});
|
||||
|
||||
audio_pipe.on('end', () => {
|
||||
audioWss.clients.forEach((client) => {
|
||||
client.close(1001, "Audio stream ended");
|
||||
});
|
||||
});
|
||||
|
||||
return audioWss;
|
||||
}
|
||||
|
||||
module.exports = { createAudioServer };
|
||||
module.exports = audioWss;
|
||||
84
server/tuner_profiles.js
Normal file
84
server/tuner_profiles.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const tunerProfiles = [
|
||||
{
|
||||
id: 'tef',
|
||||
label: 'TEF668x',
|
||||
fmBandwidths: [
|
||||
{ value: 0, label: 'Auto' },
|
||||
{ value: 56000, label: '56 kHz' },
|
||||
{ value: 64000, label: '64 kHz' },
|
||||
{ value: 72000, label: '72 kHz' },
|
||||
{ value: 84000, label: '84 kHz' },
|
||||
{ value: 97000, label: '97 kHz' },
|
||||
{ value: 114000, label: '114 kHz' },
|
||||
{ value: 133000, label: '133 kHz' },
|
||||
{ value: 151000, label: '151 kHz' },
|
||||
{ value: 184000, label: '184 kHz' },
|
||||
{ value: 200000, label: '200 kHz' },
|
||||
{ value: 217000, label: '217 kHz' },
|
||||
{ value: 236000, label: '236 kHz' },
|
||||
{ value: 254000, label: '254 kHz' },
|
||||
{ value: 287000, label: '287 kHz' },
|
||||
{ value: 311000, label: '311 kHz' }
|
||||
],
|
||||
details: ''
|
||||
},
|
||||
{
|
||||
id: 'xdr',
|
||||
label: 'XDR (F1HD / S10HDiP)',
|
||||
fmBandwidths: [
|
||||
{ value: 0, value2: -1, label: 'Auto' },
|
||||
{ value: 55000, value2: 0, label: '55 kHz' },
|
||||
{ value: 73000, value2: 1, label: '73 kHz' },
|
||||
{ value: 90000, value2: 2, label: '90 kHz' },
|
||||
{ value: 108000, value2: 3, label: '108 kHz' },
|
||||
{ value: 125000, value2: 4, label: '125 kHz' },
|
||||
{ value: 142000, value2: 5, label: '142 kHz' },
|
||||
{ value: 159000, value2: 6, label: '159 kHz' },
|
||||
{ value: 177000, value2: 7, label: '177 kHz' },
|
||||
{ value: 194000, value2: 8, label: '194 kHz' },
|
||||
{ value: 211000, value2: 9, label: '211 kHz' },
|
||||
{ value: 229000, value2: 10, label: '229 kHz' },
|
||||
{ value: 246000, value2: 11, label: '246 kHz' },
|
||||
{ value: 263000, value2: 12, label: '263 kHz' },
|
||||
{ value: 281000, value2: 13, label: '281 kHz' },
|
||||
{ value: 298000, value2: 14, label: '298 kHz' },
|
||||
{ value: 309000, value2: 15, label: '309 kHz' }
|
||||
],
|
||||
details: ''
|
||||
},
|
||||
{
|
||||
id: 'sdr',
|
||||
label: 'SDR (RTL-SDR / AirSpy)',
|
||||
fmBandwidths: [
|
||||
{ value: 0, label: 'Auto' },
|
||||
{ value: 4000, label: '4 kHz' },
|
||||
{ value: 8000, label: '8 kHz' },
|
||||
{ value: 10000, label: '10 kHz' },
|
||||
{ value: 20000, label: '20 kHz' },
|
||||
{ value: 30000, label: '30 kHz' },
|
||||
{ value: 50000, label: '50 kHz' },
|
||||
{ value: 75000, label: '75 kHz' },
|
||||
{ value: 100000, label: '100 kHz' },
|
||||
{ value: 125000, label: '125 kHz' },
|
||||
{ value: 150000, label: '150 kHz' },
|
||||
{ value: 175000, label: '175 kHz' },
|
||||
{ value: 200000, label: '200 kHz' },
|
||||
{ value: 225000, label: '225 kHz' }
|
||||
],
|
||||
details: ''
|
||||
},
|
||||
{
|
||||
id: 'si47xx',
|
||||
label: 'Si47XX (Si4735 / Si4732)',
|
||||
fmBandwidths: [
|
||||
{ value: 0, label: 'Auto' },
|
||||
{ value: 40000, label: '40 kHz' },
|
||||
{ value: 60000, label: '60 kHz' },
|
||||
{ value: 84000, label: '84 kHz' },
|
||||
{ value: 110000, label: '110 kHz' }
|
||||
],
|
||||
details: ''
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = tunerProfiles;
|
||||
@@ -15,19 +15,15 @@ const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F
|
||||
async function connect() {
|
||||
if (serverConfig.tunnel?.enabled === true) {
|
||||
const librariesDir = path.resolve(__dirname, '../libraries');
|
||||
if (!await fileExists(librariesDir)) {
|
||||
await fs.mkdir(librariesDir);
|
||||
}
|
||||
if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir);
|
||||
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
|
||||
if (!await fileExists(frpcPath)) {
|
||||
logInfo('frpc binary required for tunnel is not available. Downloading now...');
|
||||
logInfo('frpc binary, required for tunnel is not available. Downloading now...');
|
||||
const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : '');
|
||||
|
||||
try {
|
||||
const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName);
|
||||
if (res.status === 404) {
|
||||
throw new Error('404 error');
|
||||
}
|
||||
if (res.status === 404) throw new Error('404 error');
|
||||
const stream = fs2.createWriteStream(frpcPath);
|
||||
await finished(Readable.fromWeb(res.body).pipe(stream));
|
||||
} catch (err) {
|
||||
@@ -35,13 +31,11 @@ async function connect() {
|
||||
return;
|
||||
}
|
||||
logInfo('Downloading of frpc is completed.')
|
||||
if (os.platform() === 'linux' || os.platform() === 'darwin') {
|
||||
await fs.chmod(frpcPath, 0o770);
|
||||
}
|
||||
if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770);
|
||||
}
|
||||
const cfg = ejs.render(frpcConfigTemplate, {
|
||||
cfg: serverConfig.tunnel,
|
||||
host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : serverConfig.tunnel.region + ".fmtuner.org",
|
||||
host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : ((serverConfig.tunnel.region == "pldx") ? "pldx.duckdns.org" : (serverConfig.tunnel.region + ".fmtuner.org")),
|
||||
server: {
|
||||
port: serverConfig.webserver.webserverPort
|
||||
}
|
||||
@@ -62,15 +56,10 @@ async function connect() {
|
||||
if (line.includes('connect to server error')) {
|
||||
const reason = line.substring(line.indexOf(': ')+2);
|
||||
logError('Failed to connect to tunnel, reason: ' + reason);
|
||||
} else if (line.includes('invalid user or token')) {
|
||||
logError('Failed to connect to tunnel, reason: invalid user or token');
|
||||
} else if (line.includes('start proxy success')) {
|
||||
logInfo('Tunnel established successfully');
|
||||
} else if (line.includes('login to server success')) {
|
||||
logInfo('Connection to tunnel server was successful');
|
||||
} else {
|
||||
logDebug('Tunnel log:', line);
|
||||
}
|
||||
} else if (line.includes('invalid user or token')) logError('Failed to connect to tunnel, reason: invalid user or token');
|
||||
else if (line.includes('start proxy success')) logInfo('Tunnel established successfully');
|
||||
else if (line.includes('login to server success')) logInfo('Connection to tunnel server was successful');
|
||||
else logDebug('Tunnel log:', line);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
@@ -80,7 +69,6 @@ async function connect() {
|
||||
child.on('close', (code) => {
|
||||
logInfo(`Tunnel process exited with code ${code}`);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ const { serverConfig } = require('./server_config');
|
||||
const consoleCmd = require('./console');
|
||||
|
||||
let localDb = {};
|
||||
let nextLocalDbUpdate = 0;
|
||||
const localDbUpdateInterval = 7 * 24 * 60 * 60 * 1000; // 7-day database update interval
|
||||
let awaitingTxInfo = true;
|
||||
let lastFetchTime = 0;
|
||||
let piFreqIndex = {}; // Indexing for speedier PI+Freq combinations
|
||||
const fetchInterval = 1000;
|
||||
@@ -71,20 +74,19 @@ if (serverConfig.identification.gpsMode) {
|
||||
// Function to build local TX database from FMDX Maps endpoint.
|
||||
async function buildTxDatabase() {
|
||||
if (Latitude.length > 0 && Longitude.length > 0) {
|
||||
let awaitingTxInfo = true;
|
||||
awaitingTxInfo = true;
|
||||
while (awaitingTxInfo) {
|
||||
try {
|
||||
consoleCmd.logInfo('Fetching transmitter database...');
|
||||
const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
headers: {'Accept': 'application/json'}
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
localDb = await response.json();
|
||||
buildPiFreqIndex();
|
||||
consoleCmd.logInfo('Transmitter database successfully loaded.');
|
||||
nextLocalDbUpdate = Date.now() + localDbUpdateInterval;
|
||||
awaitingTxInfo = false;
|
||||
} catch (error) {
|
||||
consoleCmd.logError("Failed to fetch transmitter database:", error);
|
||||
@@ -92,9 +94,7 @@ async function buildTxDatabase() {
|
||||
consoleCmd.logInfo('Retrying transmitter database download...');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
|
||||
}
|
||||
} else consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
|
||||
}
|
||||
|
||||
// Function to build index map of PI+Freq combinations
|
||||
@@ -158,8 +158,7 @@ function getStateBoundingBox(coordinates) {
|
||||
|
||||
// Function to check if a city (lat, lon) falls within the bounding box of a state
|
||||
function isCityInState(lat, lon, boundingBox) {
|
||||
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat &&
|
||||
lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
|
||||
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat && lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
|
||||
}
|
||||
|
||||
// Function to check if a city (lat, lon) is inside any US state and return the state name
|
||||
@@ -168,9 +167,7 @@ function getStateForCoordinates(lat, lon) {
|
||||
|
||||
for (const feature of usStatesGeoJson.features) {
|
||||
const boundingBox = getStateBoundingBox(feature.geometry.coordinates);
|
||||
if (isCityInState(lat, lon, boundingBox)) {
|
||||
return feature.properties.name; // Return the state's name if city is inside bounding box
|
||||
}
|
||||
if (isCityInState(lat, lon, boundingBox)) return feature.properties.name; // Return the state's name if city is inside bounding box
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -185,7 +182,7 @@ function getStateForCoordinates(lat, lon) {
|
||||
*/
|
||||
function validPsCompare(rdsPs, stationPs) {
|
||||
if (typeof stationPs !== 'string' || typeof rdsPs !== 'string') {
|
||||
consoleCmd.logError(`Invalid TX values. stationPs: ${stationPs}, rdsPs: ${rdsPs}`);
|
||||
consoleCmd.logDebug(`Invalid TX values. stationPs: ${stationPs}, rdsPs: ${rdsPs}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -207,31 +204,22 @@ function validPsCompare(rdsPs, stationPs) {
|
||||
for (let i = 0; i < standardizedRdsPs.length; i++) {
|
||||
// Skip this position if the character in standardizedRdsPs is an underscore.
|
||||
if (standardizedRdsPs[i] === '_') continue;
|
||||
if (token[i] === standardizedRdsPs[i]) {
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
if (matchCount >= minMatchLen) {
|
||||
return true;
|
||||
if (token[i] === standardizedRdsPs[i]) matchCount++;
|
||||
}
|
||||
if (matchCount >= minMatchLen) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function evaluateStation(station, esMode) {
|
||||
let weightDistance = station.distanceKm;
|
||||
if (esMode && station.distanceKm > 700) {
|
||||
weightDistance = Math.abs(station.distanceKm - 1500) + 200;
|
||||
}
|
||||
if (esMode && station.distanceKm > 700) weightDistance = Math.abs(station.distanceKm - 1500) + 200;
|
||||
let erp = station.erp && station.erp > 0 ? station.erp : 1;
|
||||
let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0;
|
||||
let score = 0;
|
||||
// If ERP is 1W, use a simpler formula to avoid zero-scoring.
|
||||
if (erp === 0.001) {
|
||||
score = erp / station.distanceKm;
|
||||
} else {
|
||||
score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
|
||||
}
|
||||
if (erp === 0.001) score = erp / station.distanceKm;
|
||||
else score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
|
||||
return score;
|
||||
}
|
||||
|
||||
@@ -240,6 +228,10 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
let match = null;
|
||||
let multiMatches = [];
|
||||
const now = Date.now();
|
||||
if (now > nextLocalDbUpdate && !awaitingTxInfo) {
|
||||
consoleCmd.logInfo('Time to update transmitter database.');
|
||||
buildTxDatabase();
|
||||
}
|
||||
freq = parseFloat(freq);
|
||||
|
||||
if (
|
||||
@@ -272,6 +264,15 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
stations: locData.stations.filter(station => validPsCompare(rdsPs, station.ps))
|
||||
})).filter(locData => locData.stations.length > 0);
|
||||
}
|
||||
|
||||
if (filteredLocations.length > 1) {
|
||||
const extraFilteredLocations = filteredLocations.map(locData => ({
|
||||
...locData,
|
||||
stations: locData.stations.filter(station => (station.ps?.toLowerCase() === rdsPs.replace(/ /g, '_').toLowerCase()))
|
||||
})).filter(locData => locData.stations.length > 0);
|
||||
|
||||
if (extraFilteredLocations.length > 0) filteredLocations = extraFilteredLocations;
|
||||
}
|
||||
|
||||
for (let loc of filteredLocations) {
|
||||
loc = Object.assign(loc, loc.stations[0]);
|
||||
@@ -287,12 +288,8 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
loc => loc.distanceKm < 700 && loc.erp >= 10
|
||||
);
|
||||
let esMode = false;
|
||||
if (!tropoPriority) {
|
||||
esMode = checkEs();
|
||||
}
|
||||
for (let loc of filteredLocations) {
|
||||
loc.score = evaluateStation(loc, esMode);
|
||||
}
|
||||
if (!tropoPriority) esMode = checkEs();
|
||||
for (let loc of filteredLocations) loc.score = evaluateStation(loc, esMode);
|
||||
// Sort by score in descending order
|
||||
filteredLocations.sort((a, b) => b.score - a.score);
|
||||
match = filteredLocations[0];
|
||||
@@ -306,11 +303,9 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (match.itu === 'USA') {
|
||||
if (match.itu == 'USA') { // Also known as Dumbfuckinstan. they should not go to hell, but hell+ (it is NOT better)
|
||||
const state = getStateForCoordinates(match.lat, match.lon);
|
||||
if (state) {
|
||||
match.state = state; // Add state to matchingCity
|
||||
}
|
||||
if (state) match.state = state; // Add state to matchingCity
|
||||
}
|
||||
const result = {
|
||||
station: match.detectedByPireg
|
||||
@@ -343,9 +338,7 @@ function checkEs() {
|
||||
const now = Date.now();
|
||||
const url = "https://fmdx.org/includes/tools/get_muf.php";
|
||||
|
||||
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) {
|
||||
return esSwitchCache.esSwitch;
|
||||
}
|
||||
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) return esSwitchCache.esSwitch;
|
||||
|
||||
if (Latitude > 20) {
|
||||
esSwitchCache.lastCheck = now;
|
||||
@@ -372,15 +365,12 @@ function haversine(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const dLat = deg2rad(lat2 - lat1);
|
||||
const dLon = deg2rad(lon2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const distance = R * c;
|
||||
|
||||
const y = Math.sin(dLon) * Math.cos(deg2rad(lat2));
|
||||
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) -
|
||||
Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
|
||||
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
|
||||
const azimuth = Math.atan2(y, x);
|
||||
const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360;
|
||||
|
||||
@@ -394,6 +384,4 @@ function deg2rad(deg) {
|
||||
return deg * (Math.PI / 180);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchTx
|
||||
};
|
||||
module.exports = fetchTx;
|
||||
17
web/403.ejs
17
web/403.ejs
@@ -3,8 +3,11 @@
|
||||
<head>
|
||||
<title>Unauthorized - FM-DX Webserver</title>
|
||||
<link href="css/entry.css" type="text/css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css"
|
||||
rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
|
||||
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="icon" type="image/png" href="favicon2.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
@@ -13,16 +16,20 @@
|
||||
<div class="panel-100 no-bg">
|
||||
<img class="top-10" src="./images/openradio_logo_neutral.png" height="64px">
|
||||
<h2 class="text-monospace text-light text-center">[403]</h2>
|
||||
|
||||
|
||||
<div class="panel-100 p-10">
|
||||
<br>
|
||||
<i class="text-big fa-solid fa-exclamation-triangle color-4"></i>
|
||||
<p>
|
||||
There's a possibility you were kicked by the system.<br>
|
||||
Please try again later.</p>
|
||||
</div>
|
||||
|
||||
<% if (reason) { %>
|
||||
<p><strong>Reason:</strong> too dig of a bick</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,62 +1,12 @@
|
||||
<%
|
||||
let options = [];
|
||||
|
||||
if (device === 'tef') {
|
||||
options = [
|
||||
{ value: 0, label: 'Auto' },
|
||||
{ value: 56000, label: '56 kHz' },
|
||||
{ value: 64000, label: '64 kHz' },
|
||||
{ value: 72000, label: '72 kHz' },
|
||||
{ value: 84000, label: '84 kHz' },
|
||||
{ value: 97000, label: '97 kHz' },
|
||||
{ value: 114000, label: '114 kHz' },
|
||||
{ value: 133000, label: '133 kHz' },
|
||||
{ value: 151000, label: '151 kHz' },
|
||||
{ value: 184000, label: '184 kHz' },
|
||||
{ value: 200000, label: '200 kHz' },
|
||||
{ value: 217000, label: '217 kHz' },
|
||||
{ value: 236000, label: '236 kHz' },
|
||||
{ value: 254000, label: '254 kHz' },
|
||||
{ value: 287000, label: '287 kHz' },
|
||||
{ value: 311000, label: '311 kHz' }
|
||||
];
|
||||
} else if (device === 'xdr') {
|
||||
options = [
|
||||
{ value: 0, value2: -1, label: 'Auto' },
|
||||
{ value: 55000, value2: 0, label: '55 kHz' },
|
||||
{ value: 73000, value2: 1, label: '73 kHz' },
|
||||
{ value: 90000, value2: 2, label: '90 kHz' },
|
||||
{ value: 108000, value2: 3, label: '108 kHz' },
|
||||
{ value: 125000, value2: 4, label: '125 kHz' },
|
||||
{ value: 142000, value2: 5, label: '142 kHz' },
|
||||
{ value: 159000, value2: 6, label: '159 kHz' },
|
||||
{ value: 177000, value2: 7, label: '177 kHz' },
|
||||
{ value: 194000, value2: 8, label: '194 kHz' },
|
||||
{ value: 211000, value2: 9, label: '211 kHz' },
|
||||
{ value: 229000, value2: 10, label: '229 kHz' },
|
||||
{ value: 246000, value2: 11, label: '246 kHz' },
|
||||
{ value: 263000, value2: 12, label: '263 kHz' },
|
||||
{ value: 281000, value2: 13, label: '281 kHz' },
|
||||
{ value: 298000, value2: 14, label: '298 kHz' },
|
||||
{ value: 309000, value2: 15, label: '309 kHz' }
|
||||
];
|
||||
} else if (device === 'sdr') {
|
||||
options = [
|
||||
{ value: 0, label: 'Auto' },
|
||||
{ value: 4000, label: '4 kHz' },
|
||||
{ value: 8000, label: '8 kHz' },
|
||||
{ value: 10000, label: '10 kHz' },
|
||||
{ value: 20000, label: '20 kHz' },
|
||||
{ value: 30000, label: '30 kHz' },
|
||||
{ value: 50000, label: '50 kHz' },
|
||||
{ value: 75000, label: '75 kHz' },
|
||||
{ value: 100000, label: '100 kHz' },
|
||||
{ value: 125000, label: '125 kHz' },
|
||||
{ value: 150000, label: '150 kHz' },
|
||||
{ value: 175000, label: '175 kHz' },
|
||||
{ value: 200000, label: '200 kHz' },
|
||||
{ value: 225000, label: '225 kHz' }
|
||||
];
|
||||
const profile = Array.isArray(tunerProfiles)
|
||||
? tunerProfiles.find((item) => item.id === device)
|
||||
: null;
|
||||
|
||||
if (Array.isArray(profile?.fmBandwidths)) {
|
||||
options = profile.fmBandwidths;
|
||||
}
|
||||
%>
|
||||
|
||||
|
||||
@@ -293,8 +293,8 @@ pre {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.flex-container.contains-dropdown {
|
||||
z-index: 999;
|
||||
.contains-dropdown {
|
||||
z-index: 990;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB |
24
web/favicon.svg
Normal file
24
web/favicon.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<!-- Outer hollow circle -->
|
||||
<circle
|
||||
cx="64"
|
||||
cy="64"
|
||||
r="54"
|
||||
fill="none"
|
||||
stroke="#A7A88B"
|
||||
stroke-width="20"
|
||||
/>
|
||||
|
||||
<!-- Inner hollow circle -->
|
||||
<circle
|
||||
cx="64"
|
||||
cy="64"
|
||||
r="22"
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="18"
|
||||
/>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -15,7 +15,7 @@
|
||||
<script src="js/libs/chartjs-adapter-luxon.umd.min.js"></script>
|
||||
<script src="js/libs/chartjs-plugin-streaming.min.js"></script>
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta property="og:title" content="FM-DX WebServer [<%= tunerName %>]">
|
||||
@@ -94,6 +94,7 @@
|
||||
<% if (device == 'tef') { %>TEF668x<% } %>
|
||||
<% if (device == 'xdr') { %>Sony XDR<% } %>
|
||||
<% if (device == 'sdr') { %>SDR<% } %>
|
||||
<% if (device == 'si47xx') { %>SI47XX<% } %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="color-3 m-10 text-medium">
|
||||
@@ -228,6 +229,17 @@
|
||||
<div class="panel-50 no-bg br-0 h-100 m-0 button-eq">
|
||||
<% if (device == 'tef') { %><button style="border-radius: 15px 0px 0px 15px;" class="data-eq hide-phone tooltip" aria-label="EQ Filter" data-tooltip="<strong>The cEQ filter can reduce bandwidth below 56 kHz.</strong><br><br>Useful for weak stations next to strong ones,<br>although it may pick up more interference."><span class="text-bold">cEQ</span></button><% } %>
|
||||
<% if (device == 'xdr') { %><button style="border-radius: 15px 0px 0px 15px;" class="data-eq hide-phone tooltip" aria-label="RF+ Filter" data-tooltip="<strong>The RF+ filter increases gain by 5dB</strong>"><span class="text-bold">RF+</span></button><% } %>
|
||||
<% if (device == 'si47xx' && si47xxAgcControl) { %>
|
||||
<div class="no-bg dropdown dropdown-up data-agc hide-phone w-150" id="data-agc" style="margin-right: 15px !important;">
|
||||
<input type="text" placeholder="AGC" readonly tabindex="0">
|
||||
<ul class="options open-top" tabindex="-1">
|
||||
<li data-value="0" class="option" tabindex="0">Auto AGC</li>
|
||||
<li data-value="1" class="option" tabindex="0">High</li>
|
||||
<li data-value="3" class="option" tabindex="0">Medium</li>
|
||||
<li data-value="2" class="option" tabindex="0">Low</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="panel-50 no-bg br-0 h-100 m-0 button-ims">
|
||||
<% if (device == 'tef') { %><button style="border-radius: 0px 15px 15px 0px;" class="data-ims hide-phone tooltip" aria-label="iMS + Filter" data-tooltip="<strong>The iMS filter reduces multipath audio artifacts.</strong><br><br>It's recommended to leave it on most of the time."><span class="text-bold">iMS</span></button><% } %>
|
||||
@@ -248,8 +260,8 @@
|
||||
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1" aria-label="Volume slider">
|
||||
</span>
|
||||
<% if (bwSwitch) { %>
|
||||
<%- include('_bwSwitch', { device: device, id: 'data-bw', cssClass: 'panel-50 dropdown-up m-0 w-150 m-left-15', cssClassOptions: 'open-top' }) %>
|
||||
<% } %>
|
||||
<%- include('_bwSwitch', { device: device, tunerProfiles: tunerProfiles, id: 'data-bw', cssClass: 'panel-50 dropdown-up m-0 w-150 m-left-15', cssClassOptions: 'open-top' }) %>
|
||||
<% } %>
|
||||
<% if (fmlist_integration == true && (fmlist_adminOnly == false || isTuneAuthenticated)) { %>
|
||||
<button class="tooltip bg-color-4 mini-popup log-fmlist" data-tooltip="<strong>LOG TO FMLIST</strong><br>Clicking this button logs the current station to FMLIST's visual logbook." aria-label="Log to FMLIST" style="width: 80px; height: 48px;margin-left: 15px !important;">
|
||||
<i class="fa-solid fa-flag fa-lg"></i>
|
||||
@@ -353,16 +365,27 @@
|
||||
<div class="flex-phone">
|
||||
<% if (bwSwitch) { %>
|
||||
<div style="max-height: 48px;width: 50%;margin-right: 5px;">
|
||||
<%- include('_bwSwitch', { device: device, id: 'data-bw-phone', cssClass: 'panel-100-real', cssClassOptions: 'text-center open-bottom' }) %>
|
||||
<%- include('_bwSwitch', { device: device, tunerProfiles: tunerProfiles, id: 'data-bw-phone', cssClass: 'panel-100-real', cssClassOptions: 'text-center open-bottom' }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p class="flex-phone flex-center">Filters</p>
|
||||
<div class="flex-container flex-phone flex-center">
|
||||
<% if (device == 'tef') { %><button class="data-eq tooltip p-10 m-right-5" style="height: 48px" aria-label="EQ Filter" data-tooltip="<strong>The cEQ filter can reduce bandwidth below 56 kHz.</strong><br><br>Useful for weak stations next to strong ones,<br>although it may pick up more interference."><span class="text-bold">cEQ</span></button><% } %>
|
||||
<% if (device == 'xdr') { %><button class="data-eq tooltip p-10 m-right-5" aria-label="RF+ Filter" data-tooltip="<strong>The RF+ filter increases gain by 5dB</strong>"><span class="text-bold">RF+</span></button><% } %>
|
||||
<% if (device == 'si47xx' && si47xxAgcControl) { %>
|
||||
<div class="no-bg dropdown data-agc w-150" id="data-agc-phone" style="max-height: 48px;">
|
||||
<input type="text" placeholder="AGC" readonly tabindex="0" style="border-radius: 15px;">
|
||||
<ul class="options open-top" tabindex="-1">
|
||||
<li data-value="0" class="option" tabindex="0">Auto AGC</li>
|
||||
<li data-value="1" class="option" tabindex="0">High</li>
|
||||
<li data-value="3" class="option" tabindex="0">Medium</li>
|
||||
<li data-value="2" class="option" tabindex="0">Low</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (device == 'tef') { %><button class="data-ims tooltip p-10 m-left-5" style="height: 48px;" aria-label="iMS + Filter" data-tooltip="<strong>The iMS filter reduces multipath audio artifacts.</strong><br><br>It's recommended to leave it on most of the time."><span class="text-bold">iMS</span></button><% } %>
|
||||
<% if (device == 'xdr') { %><button class="data-ims tooltip p-10 m-left-5" aria-label="IF+ Filter" data-tooltip="<strong>The IF+ filter increases gain by 6dB</strong>"><span class="text-bold">IF+</span></button><% } %>
|
||||
</div>
|
||||
@@ -422,14 +445,14 @@
|
||||
|
||||
<div style="width: calc(50% - 32px);text-align: center;">
|
||||
<button class="users-online-container" aria-label="Online users" style="display: inline-block;"><i class="fa-solid fa-user"></i> <span class="users-online"></span></button>
|
||||
|
||||
|
||||
<% if (chatEnabled) { %>
|
||||
<button class="chatbutton m-10" aria-label="Chatbox" style="display: inline-block;"><i class="fa-solid fa-message"></i></button>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="width: 64px;text-align: center;">
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style="width: calc(50% - 32px);text-align: center;">
|
||||
@@ -446,7 +469,7 @@
|
||||
<h1 class="top-25">Settings</h1>
|
||||
|
||||
<div class="panel-full flex-center no-bg m-0">
|
||||
<%- include('_components', { component: 'dropdown', id: 'theme-selector', inputId: 'theme-selector-input', label: 'Theme', cssClass: '', placeholder: 'Default',
|
||||
<%- include('_components', { component: 'dropdown', id: 'theme-selector', inputId: 'theme-selector-input', label: 'Theme', cssClass: '', placeholder: 'Default',
|
||||
options: [
|
||||
{ value: 'theme1', label: 'Mint' },
|
||||
{ value: 'theme2', label: 'Cappuccino' },
|
||||
@@ -463,7 +486,7 @@
|
||||
|
||||
<% if (device !== 'sdr') { %>
|
||||
<div class="panel-full flex-center no-bg m-0">
|
||||
<%- include('_components', { component: 'dropdown', id: 'signal-selector', inputId: 'signal-selector-input', label: 'Signal units', cssClass: '', placeholder: 'dBf',
|
||||
<%- include('_components', { component: 'dropdown', id: 'signal-selector', inputId: 'signal-selector-input', label: 'Signal units', cssClass: '', placeholder: 'dBf',
|
||||
options: [
|
||||
{ value: 'dbf', label: 'dBf' },
|
||||
{ value: 'dbuv', label: 'dBuV' },
|
||||
|
||||
@@ -27,11 +27,8 @@ function destroyStream() {
|
||||
|
||||
function OnConnectivityCallback(isConnected) {
|
||||
console.log("Connectivity changed:", isConnected);
|
||||
if (Stream) {
|
||||
Stream.Volume = $('#volumeSlider').val();
|
||||
} else {
|
||||
console.warn("Stream is not initialized.");
|
||||
}
|
||||
if (Stream) Stream.Volume = $('#volumeSlider').val();
|
||||
else console.warn("Stream is not initialized.");
|
||||
}
|
||||
|
||||
|
||||
@@ -44,18 +41,14 @@ function OnPlayButtonClick(_ev) {
|
||||
shouldReconnect = false;
|
||||
destroyStream();
|
||||
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
|
||||
if (isAppleiOS && 'audioSession' in navigator) {
|
||||
navigator.audioSession.type = "none";
|
||||
}
|
||||
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "none";
|
||||
} else {
|
||||
console.log("Starting stream...");
|
||||
shouldReconnect = true;
|
||||
createStream();
|
||||
Stream.Start();
|
||||
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
|
||||
if (isAppleiOS && 'audioSession' in navigator) {
|
||||
navigator.audioSession.type = "playback";
|
||||
}
|
||||
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "playback";
|
||||
}
|
||||
|
||||
$playbutton.addClass('bg-gray').prop('disabled', true);
|
||||
@@ -70,9 +63,7 @@ function updateVolume() {
|
||||
newVolumeGlobal = newVolume;
|
||||
console.log("Volume updated to:", newVolume);
|
||||
Stream.Volume = newVolume;
|
||||
} else {
|
||||
console.warn("Stream is not initialized.");
|
||||
}
|
||||
} else console.warn("Stream is not initialized.");
|
||||
}
|
||||
|
||||
$(document).ready(Init);
|
||||
|
||||
@@ -3,18 +3,13 @@ function tuneUp() {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
getCurrentFreq();
|
||||
let addVal = 0;
|
||||
if (currentFreq < 0.52) {
|
||||
addVal = 9 - (Math.round(currentFreq*1000) % 9);
|
||||
} else if (currentFreq < 1.71) {
|
||||
if (currentFreq < 0.52) addVal = 9 - (Math.round(currentFreq*1000) % 9);
|
||||
else if (currentFreq < 1.71) {
|
||||
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting
|
||||
addVal = 9 - (Math.round(currentFreq*1000) % 9);
|
||||
} else if (currentFreq < 29.6) {
|
||||
addVal = 5 - (Math.round(currentFreq*1000) % 5);
|
||||
} else if (currentFreq >= 65.9 && currentFreq < 74) {
|
||||
addVal = 30 - ((Math.round(currentFreq*1000) - 65900) % 30);
|
||||
} else {
|
||||
addVal = 100 - (Math.round(currentFreq*1000) % 100);
|
||||
}
|
||||
} else if (currentFreq < 29.6) addVal = 5 - (Math.round(currentFreq*1000) % 5);
|
||||
else if (currentFreq >= 65.9 && currentFreq < 74) addVal = 30 - ((Math.round(currentFreq*1000) - 65900) % 30);
|
||||
else addVal = 100 - (Math.round(currentFreq*1000) % 100);
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + addVal));
|
||||
}
|
||||
}
|
||||
@@ -23,18 +18,13 @@ function tuneDown() {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
getCurrentFreq();
|
||||
let subVal = 0;
|
||||
if (currentFreq < 0.52) {
|
||||
if (currentFreq < 0.52) subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
|
||||
else if (currentFreq < 1.71) {
|
||||
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting (Americans use 10, because of dumbfuckinstan)
|
||||
subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
|
||||
} else if (currentFreq < 1.71) {
|
||||
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting
|
||||
subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
|
||||
} else if (currentFreq < 29.6) {
|
||||
subVal = (Math.round(currentFreq*1000) % 5 == 0) ? 5 : (Math.round(currentFreq*1000) % 5);
|
||||
} else if (currentFreq > 65.9 && currentFreq <= 74) {
|
||||
subVal = ((Math.round(currentFreq*1000) - 65900) % 30 == 0) ? 30 : ((Math.round(currentFreq*1000) - 65900) % 30);
|
||||
} else {
|
||||
subVal = (Math.round(currentFreq*1000) % 100 == 0) ? 100 : (Math.round(currentFreq*1000) % 100);
|
||||
}
|
||||
} else if (currentFreq < 29.6) subVal = (Math.round(currentFreq*1000) % 5 == 0) ? 5 : (Math.round(currentFreq*1000) % 5);
|
||||
else if (currentFreq > 65.9 && currentFreq <= 74) subVal = ((Math.round(currentFreq*1000) - 65900) % 30 == 0) ? 30 : ((Math.round(currentFreq*1000) - 65900) % 30);
|
||||
else subVal = (Math.round(currentFreq*1000) % 100 == 0) ? 100 : (Math.round(currentFreq*1000) % 100);
|
||||
socket.send("T" + (Math.round(currentFreq*1000) - subVal));
|
||||
}
|
||||
}
|
||||
@@ -52,6 +42,6 @@ function getCurrentFreq() {
|
||||
currentFreq = $('#data-frequency').text();
|
||||
currentFreq = parseFloat(currentFreq).toFixed(3);
|
||||
currentFreq = parseFloat(currentFreq);
|
||||
|
||||
|
||||
return currentFreq;
|
||||
}
|
||||
|
||||
@@ -9,42 +9,40 @@ $(document).ready(function() {
|
||||
const chatIdentityNickname = $('#chat-identity-nickname');
|
||||
const chatNicknameInput = $('#chat-nickname');
|
||||
const chatNicknameSave = $('#chat-nickname-save');
|
||||
|
||||
|
||||
$(".chatbutton").on("click", function () {
|
||||
togglePopup("#popup-panel-chat");
|
||||
chatMessages.scrollTop(chatMessages[0].scrollHeight);
|
||||
});
|
||||
|
||||
|
||||
// Function to generate a random string
|
||||
function generateRandomString(length) {
|
||||
const characters = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Load nickname from localStorage on page load
|
||||
let savedNickname = localStorage.getItem('nickname') || `User ${generateRandomString(5)}`;
|
||||
chatNicknameInput.val(savedNickname);
|
||||
chatIdentityNickname.text(savedNickname);
|
||||
|
||||
|
||||
chatSocket.onmessage = function(event) {
|
||||
const messageData = JSON.parse(event.data);
|
||||
const isAdmin = messageData.admin ? '<span style="color: #bada55">[ADMIN]</span>' : '';
|
||||
|
||||
|
||||
if (messageData.type === 'clientIp') {
|
||||
chatIdentityNickname.html(isAdmin).append(document.createTextNode(" " + savedNickname));
|
||||
chatIdentityNickname.attr('title', messageData.ip);
|
||||
} else {
|
||||
const chatMessage = `
|
||||
<span class="color-2">[${messageData.time}]</span>
|
||||
${isAdmin} <strong class="color-5" title="${typeof messageData.ip !== "undefined" ? 'IP Address: ' + messageData.ip : ''}">${messageData.nickname}</strong>:
|
||||
${isAdmin} <strong class="color-5" title="${typeof messageData.ip !== "undefined" ? 'IP Address: ' + messageData.ip : ''}">${messageData.nickname}</strong>:
|
||||
<span style="color: var(--color-text-2);">${$('<div/>').text(messageData.message).html()}</span><br>
|
||||
`;
|
||||
chatMessages.append(chatMessage);
|
||||
|
||||
|
||||
if (chatMessages.is(':visible')) {
|
||||
setTimeout(function() {
|
||||
chatMessages.scrollTop(chatMessages[0].scrollHeight);
|
||||
@@ -59,7 +57,7 @@ $(document).ready(function() {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('.chat-send-message-btn').click(sendMessage);
|
||||
chatNicknameSave.click(function() {
|
||||
const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(5)}`;
|
||||
@@ -68,34 +66,32 @@ $(document).ready(function() {
|
||||
chatIdentityNickname.text(savedNickname);
|
||||
chatNicknameInput.blur();
|
||||
});
|
||||
|
||||
|
||||
chatButton.click(function() {
|
||||
chatMessageCount = 0;
|
||||
chatMessagesCount.text(chatMessageCount);
|
||||
chatButton.removeClass('blink').addClass('bg-color-1');
|
||||
chatSendInput.focus();
|
||||
|
||||
|
||||
setTimeout(function() {
|
||||
chatMessages.scrollTop(chatMessages[0].scrollHeight);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
|
||||
chatNicknameInput.keypress(function(event) {
|
||||
if (event.which === 13) {
|
||||
chatNicknameSave.trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
chatSendInput.keypress(function(event) {
|
||||
if (event.which === 13) {
|
||||
sendMessage();
|
||||
}
|
||||
if (event.which === 13) sendMessage();
|
||||
});
|
||||
|
||||
|
||||
function sendMessage() {
|
||||
const nickname = savedNickname || `Anonymous User ${generateRandomString(5)}`;
|
||||
const message = chatSendInput.val().trim();
|
||||
|
||||
|
||||
if (message) {
|
||||
const messageData = { nickname, message };
|
||||
chatSocket.send(JSON.stringify(messageData));
|
||||
|
||||
@@ -28,7 +28,7 @@ function submitConfig() {
|
||||
function fetchConfig() {
|
||||
$.getJSON("./getData")
|
||||
.done(data => {
|
||||
configData = data;
|
||||
configData = data;
|
||||
populateFields(configData);
|
||||
initVolumeSlider();
|
||||
initConnectionToggle();
|
||||
@@ -38,9 +38,7 @@ function fetchConfig() {
|
||||
|
||||
function populateFields(data, prefix = "") {
|
||||
$.each(data, (key, value) => {
|
||||
if (value === null) {
|
||||
value = ""; // Convert null to an empty string
|
||||
}
|
||||
if (value === null) value = ""; // Convert null to an empty string
|
||||
|
||||
let id = `${prefix}${prefix ? "-" : ""}${key}`;
|
||||
const $element = $(`#${id}`);
|
||||
@@ -50,16 +48,13 @@ function populateFields(data, prefix = "") {
|
||||
$element.find('option').each(function() {
|
||||
const $option = $(this);
|
||||
const dataName = $option.data('name');
|
||||
if (value.includes(dataName)) {
|
||||
$option.prop('selected', true);
|
||||
} else {
|
||||
$option.prop('selected', false);
|
||||
}
|
||||
if (value.includes(dataName)) $option.prop('selected', true);
|
||||
else $option.prop('selected', false);
|
||||
});
|
||||
|
||||
$element.trigger('change');
|
||||
}
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
@@ -68,11 +63,8 @@ function populateFields(data, prefix = "") {
|
||||
const arrayId = `${id}-${index + 1}`;
|
||||
const $arrayElement = $(`#${arrayId}`);
|
||||
|
||||
if ($arrayElement.length) {
|
||||
$arrayElement.val(item);
|
||||
} else {
|
||||
console.log(`Element with id ${arrayId} not found`);
|
||||
}
|
||||
if ($arrayElement.length) $arrayElement.val(item);
|
||||
else console.log(`Element with id ${arrayId} not found`);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
@@ -92,9 +84,7 @@ function populateFields(data, prefix = "") {
|
||||
const $dropdownOption = $element.siblings('ul.options').find(`li[data-value="${value}"]`);
|
||||
$element.val($dropdownOption.length ? $dropdownOption.text() : value);
|
||||
$element.attr('data-value', value);
|
||||
} else {
|
||||
$element.val(value);
|
||||
}
|
||||
} else $element.val(value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,9 +101,7 @@ function updateConfigData(data, prefix = "") {
|
||||
if ($presetElement.length) {
|
||||
data[key].push($presetElement.val() || null); // Allow null if necessary
|
||||
index++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -123,16 +111,12 @@ function updateConfigData(data, prefix = "") {
|
||||
const $selectedOptions = $element.find('option:selected');
|
||||
$selectedOptions.each(function() {
|
||||
const dataName = $(this).attr('data-name');
|
||||
if (dataName) {
|
||||
data[key].push(dataName);
|
||||
}
|
||||
if (dataName) data[key].push(dataName);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
return updateConfigData(value, id);
|
||||
}
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) return updateConfigData(value, id);
|
||||
|
||||
if ($element.length) {
|
||||
const newValue = $element.attr("data-value") ?? $element.val() ?? null;
|
||||
|
||||
@@ -23,30 +23,38 @@ $(document).ready(function() {
|
||||
|
||||
switch($currentDropdown.attr('id')) {
|
||||
case 'data-ant':
|
||||
socket.send("Z" + $(event.currentTarget).attr('data-value'));
|
||||
resetRDS(getCurrentFreq()); // Reset RDS when change antenna input
|
||||
break;
|
||||
socket.send("Z" + $(event.currentTarget).attr('data-value'));
|
||||
resetRDS(getCurrentFreq()); // Reset RDS when change antenna input
|
||||
break;
|
||||
case 'data-ant-phone':
|
||||
socket.send("Z" + $(event.currentTarget).attr('data-value'));
|
||||
resetRDS(getCurrentFreq()); // Reset RDS when change antenna input
|
||||
break;
|
||||
socket.send("Z" + $(event.currentTarget).attr('data-value'));
|
||||
resetRDS(getCurrentFreq()); // Reset RDS when change antenna input
|
||||
break;
|
||||
case 'data-bw':
|
||||
legacyBwValue = $(event.currentTarget).attr('data-value2') || "";
|
||||
socket.send("F" + legacyBwValue);
|
||||
socket.send("W" + $(event.currentTarget).attr('data-value'));
|
||||
$currentDropdown.find('input').val($(event.currentTarget).text());
|
||||
break;
|
||||
legacyBwValue = $(event.currentTarget).attr('data-value2') || "";
|
||||
socket.send("F" + legacyBwValue);
|
||||
socket.send("W" + $(event.currentTarget).attr('data-value'));
|
||||
$currentDropdown.find('input').val($(event.currentTarget).text());
|
||||
break;
|
||||
case 'data-bw-phone':
|
||||
legacyBwValue = $(event.currentTarget).attr('data-value2') || "";
|
||||
socket.send("F" + legacyBwValue);
|
||||
socket.send("W" + $(event.currentTarget).attr('data-value'));
|
||||
$currentDropdown.find('input').val($(event.currentTarget).text());
|
||||
break;
|
||||
legacyBwValue = $(event.currentTarget).attr('data-value2') || "";
|
||||
socket.send("F" + legacyBwValue);
|
||||
socket.send("W" + $(event.currentTarget).attr('data-value'));
|
||||
$currentDropdown.find('input').val($(event.currentTarget).text());
|
||||
break;
|
||||
case 'data-agc':
|
||||
socket.send("A" + $(event.currentTarget).attr('data-value'));
|
||||
$currentDropdown.find('input').val($(event.currentTarget).text());
|
||||
break;
|
||||
case 'data-agc-phone':
|
||||
socket.send("A" + $(event.currentTarget).attr('data-value'));
|
||||
$currentDropdown.find('input').val($(event.currentTarget).text());
|
||||
break;
|
||||
default:
|
||||
$currentDropdown.find('input')
|
||||
.val($(event.currentTarget).text())
|
||||
.attr('data-value', $(event.currentTarget).data('value'));
|
||||
break;
|
||||
$currentDropdown.find('input')
|
||||
.val($(event.currentTarget).text())
|
||||
.attr('data-value', $(event.currentTarget).data('value'));
|
||||
break;
|
||||
}
|
||||
|
||||
// Use setTimeout to delay class removal
|
||||
@@ -72,24 +80,24 @@ $(document).ready(function() {
|
||||
const $options = currentDropdown.find('.option');
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
currentIndex = (currentIndex + 1) % $options.length;
|
||||
$options.eq(currentIndex).focus();
|
||||
break;
|
||||
event.preventDefault();
|
||||
currentIndex = (currentIndex + 1) % $options.length;
|
||||
$options.eq(currentIndex).focus();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
currentIndex = (currentIndex - 1 + $options.length) % $options.length;
|
||||
$options.eq(currentIndex).focus();
|
||||
break;
|
||||
event.preventDefault();
|
||||
currentIndex = (currentIndex - 1 + $options.length) % $options.length;
|
||||
$options.eq(currentIndex).focus();
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
$options.eq(currentIndex).click();
|
||||
break;
|
||||
event.preventDefault();
|
||||
$options.eq(currentIndex).click();
|
||||
break;
|
||||
case 'Escape':
|
||||
currentDropdown.removeClass('opened');
|
||||
currentDropdown = null;
|
||||
currentIndex = -1;
|
||||
break;
|
||||
currentDropdown.removeClass('opened');
|
||||
currentDropdown = null;
|
||||
currentIndex = -1;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,9 +106,7 @@ $(document).ready(function() {
|
||||
$listOfOptions.on('click', selectOption);
|
||||
$dropdowns.on('click', 'input', toggleDropdown);
|
||||
$dropdowns.on('keydown', 'input', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
toggleDropdown(event);
|
||||
}
|
||||
if (event.key === 'Enter') toggleDropdown(event);
|
||||
});
|
||||
$dropdowns.on('keydown', '.option', navigateOptions);
|
||||
|
||||
|
||||
321
web/js/main.js
321
web/js/main.js
@@ -25,23 +25,23 @@ const europe_programmes = [
|
||||
const usa_programmes = [
|
||||
"No PTY", "News", "Information", "Sports", "Talk", "Rock", "Classic Rock",
|
||||
"Adults Hits", "Soft Rock", "Top 40", "Country", "Oldies", "Soft Music",
|
||||
"Nostalgia", "Jazz", "Classical", "Rhythm and Blues", "Soft Rhythm and Blues",
|
||||
"Nostalgia", "Jazz", "Classical", "Rhythm and Blues", "Soft Rhythm and Blues",
|
||||
"Language", "Religious Music", "Religious Talk", "Personality", "Public", "College",
|
||||
"Spanish Talk", "Spanish Music", "Hip Hop", "", "", "Weather", "Emergency Test", "Emergency"
|
||||
"Spanish Talk", "Spanish Music", "Hip Hop", "", "", "Weather", "Emergency Test", "Emergency"
|
||||
];
|
||||
|
||||
const rdsMode = localStorage.getItem('rdsMode');
|
||||
|
||||
$(document).ready(function () {
|
||||
const signalToggle = $("#signal-units-toggle");
|
||||
|
||||
|
||||
var $panel = $('.admin-quick-dashboard');
|
||||
var panelWidth = $panel.outerWidth();
|
||||
|
||||
|
||||
$(document).mousemove(function(e) {
|
||||
var mouseX = e.pageX;
|
||||
var panelLeft = parseInt($panel.css('left'));
|
||||
|
||||
|
||||
if (mouseX <= 10 || (panelLeft === 4 && mouseX <= 100)) {
|
||||
$panel.css('left', '4px');
|
||||
} else {
|
||||
@@ -50,10 +50,10 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
fillPresets();
|
||||
|
||||
|
||||
signalToggle.on("change", function () {
|
||||
const signalText = localStorage.getItem('signalUnit');
|
||||
|
||||
|
||||
if (signalText == 'dbuv') {
|
||||
signalText.text('dBµV');
|
||||
} else if (signalText == 'dbf') {
|
||||
@@ -62,7 +62,7 @@ $(document).ready(function () {
|
||||
signalText.text('dBm');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Check if device is an iPhone to prevent zoom on button press
|
||||
if (/iPhone|iPod|iPad/.test(navigator.userAgent) && !window.MSStream) {
|
||||
// Handle touchstart for buttons to prevent zoom
|
||||
@@ -89,9 +89,9 @@ $(document).ready(function () {
|
||||
$viewportMeta.attr('content', content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const textInput = $('#commandinput');
|
||||
|
||||
|
||||
textInput.on('change blur', function (event) {
|
||||
const inputValue = Number(textInput.val());
|
||||
// Check if the user agent contains 'iPhone'
|
||||
@@ -101,18 +101,18 @@ $(document).ready(function () {
|
||||
textInput.val('');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
textInput.on('keyup', function (event) {
|
||||
|
||||
|
||||
if (event.key !== 'Backspace' && localStorage.getItem('extendedFreqRange') != "true") {
|
||||
let inputValue = textInput.val();
|
||||
inputValue = inputValue.replace(/[^0-9.]/g, '');
|
||||
|
||||
|
||||
if (inputValue.includes("..")) {
|
||||
inputValue = inputValue.slice(0, inputValue.lastIndexOf('.')) + inputValue.slice(inputValue.lastIndexOf('.') + 1);
|
||||
textInput.val(inputValue);
|
||||
}
|
||||
|
||||
|
||||
if (!inputValue.includes(".")) {
|
||||
if (inputValue.startsWith('10') && inputValue.length > 2) {
|
||||
inputValue = inputValue.slice(0, 3) + '.' + inputValue.slice(3);
|
||||
@@ -130,31 +130,31 @@ $(document).ready(function () {
|
||||
textInput.val('');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.onkeydown = function(event) {
|
||||
if (!event.repeat) {
|
||||
checkKey(event);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
let lastExecutionTime = 0;
|
||||
const throttleDelay = 100; // Time in ms
|
||||
$('#freq-container').on('wheel keypress', function (e) {
|
||||
e.preventDefault();
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
if (now - lastExecutionTime < throttleDelay) {
|
||||
// Ignore this event as it's within the throttle delay
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
lastExecutionTime = now; // Update the last execution time
|
||||
|
||||
|
||||
getCurrentFreq();
|
||||
var delta = e.originalEvent.deltaY;
|
||||
var adjustment = 0;
|
||||
|
||||
|
||||
if (e.shiftKey) {
|
||||
adjustment = e.altKey ? 1 : 0.01;
|
||||
} else if (e.ctrlKey) {
|
||||
@@ -167,21 +167,21 @@ $(document).ready(function () {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
var newFreq = currentFreq + (delta > 0 ? -adjustment : adjustment);
|
||||
socket.send("T" + (Math.round(newFreq * 1000)));
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
setInterval(getServerTime, 10000);
|
||||
getServerTime();
|
||||
setInterval(sendPingRequest, 5000);
|
||||
sendPingRequest();
|
||||
|
||||
|
||||
$("#tuner-name").click(function() {
|
||||
showTunerDescription();
|
||||
});
|
||||
|
||||
|
||||
var freqUpButton = $('#freq-up')[0];
|
||||
var freqDownButton = $('#freq-down')[0];
|
||||
var psContainer = $('#ps-container')[0];
|
||||
@@ -189,19 +189,19 @@ $(document).ready(function () {
|
||||
var piCodeContainer = $('#pi-code-container')[0];
|
||||
var freqContainer = $('#freq-container')[0];
|
||||
var txContainer = $('#data-station-container')[0];
|
||||
|
||||
|
||||
$(".data-eq").click(function () {
|
||||
toggleButtonState("eq");
|
||||
});
|
||||
|
||||
|
||||
$(".data-ims").click(function () {
|
||||
toggleButtonState("ims");
|
||||
});
|
||||
|
||||
|
||||
$("#volumeSlider").on('mouseup', function() {
|
||||
$('#volumeSlider').blur();
|
||||
})
|
||||
|
||||
|
||||
$(freqUpButton).on("click", tuneUp);
|
||||
$(freqDownButton).on("click", tuneDown);
|
||||
$(psContainer).on("click", copyPs);
|
||||
@@ -212,37 +212,37 @@ $(document).ready(function () {
|
||||
$(freqContainer).on("click", function () {
|
||||
textInput.focus();
|
||||
});
|
||||
|
||||
|
||||
//FMLIST logging
|
||||
$('.popup-content').on('click', function(event) {
|
||||
event.stopPropagation();
|
||||
$('.popup-content').removeClass('show');
|
||||
});
|
||||
|
||||
|
||||
$('.log-fmlist').on('click', function() {
|
||||
const logKey = 'fmlistLogChoice';
|
||||
const logTimestampKey = 'fmlistLogTimestamp';
|
||||
const expirationTime = 10 * 60 * 1000;
|
||||
const logKey = 'fmlistLogChoice';
|
||||
const logTimestampKey = 'fmlistLogTimestamp';
|
||||
const expirationTime = 10 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
const storedChoice = localStorage.getItem(logKey);
|
||||
const storedTimestamp = localStorage.getItem(logTimestampKey);
|
||||
|
||||
|
||||
if (storedChoice && storedTimestamp && (now - storedTimestamp < expirationTime)) {
|
||||
sendLog(storedChoice);
|
||||
sendLog(storedChoice);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (parsedData.txInfo.dist > 700) {
|
||||
$('.log-fmlist .mini-popup-content').addClass('show'); // Show popup if no valid choice
|
||||
|
||||
|
||||
$('.log-fmlist-sporadice').off('click').on('click', function () {
|
||||
localStorage.setItem(logKey, './log_fmlist?type=sporadice');
|
||||
localStorage.setItem(logTimestampKey, now);
|
||||
if(parsedData.txInfo.dist > 700) sendLog('./log_fmlist?type=sporadice');
|
||||
$('.log-fmlist .mini-popup-content').removeClass('show');
|
||||
});
|
||||
|
||||
|
||||
$('.log-fmlist-tropo').off('click').on('click', function () {
|
||||
localStorage.setItem(logKey, './log_fmlist?type=tropo');
|
||||
localStorage.setItem(logTimestampKey, now);
|
||||
@@ -250,9 +250,9 @@ $(document).ready(function () {
|
||||
$('.log-fmlist .mini-popup-content').removeClass('show');
|
||||
});
|
||||
} else {
|
||||
sendLog('./log_fmlist');
|
||||
sendLog('./log_fmlist');
|
||||
}
|
||||
|
||||
|
||||
function sendLog(endpoint) {
|
||||
$.ajax({
|
||||
url: endpoint,
|
||||
@@ -262,7 +262,7 @@ $(document).ready(function () {
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage;
|
||||
|
||||
|
||||
switch (xhr.status) {
|
||||
case 429:
|
||||
errorMessage = xhr.responseText;
|
||||
@@ -273,7 +273,7 @@ $(document).ready(function () {
|
||||
default:
|
||||
errorMessage = xhr.statusText || 'An error occurred';
|
||||
}
|
||||
|
||||
|
||||
sendToast('error', 'Log failed', errorMessage, false, true);
|
||||
}
|
||||
});
|
||||
@@ -290,7 +290,7 @@ function getServerTime() {
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
const serverTimeUtc = data.serverTime;
|
||||
|
||||
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
@@ -299,32 +299,32 @@ function getServerTime() {
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
|
||||
|
||||
const serverOptions = {
|
||||
...options,
|
||||
timeZone: 'Etc/UTC'
|
||||
};
|
||||
|
||||
|
||||
const formattedServerTime = new Date(serverTimeUtc).toLocaleString(navigator.language ? navigator.language : 'en-US', serverOptions);
|
||||
|
||||
$("#server-time").text(formattedServerTime);
|
||||
|
||||
$("#server-time").text(formattedServerTime);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("Error fetching server time:", errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sendPingRequest() {
|
||||
const timeoutDuration = 5000;
|
||||
const startTime = new Date().getTime();
|
||||
|
||||
|
||||
const fetchWithTimeout = (url, options, timeout = timeoutDuration) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timerTimeout = setTimeout(() => {
|
||||
reject(new Error('Request timed out'));
|
||||
}, timeout);
|
||||
|
||||
|
||||
fetch(url, options)
|
||||
.then(response => {
|
||||
clearTimeout(timerTimeout);
|
||||
@@ -336,7 +336,7 @@ function sendPingRequest() {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
fetchWithTimeout('./ping', { cache: 'no-store' }, timeoutDuration)
|
||||
.then(response => {
|
||||
const endTime = new Date().getTime();
|
||||
@@ -354,7 +354,7 @@ function sendPingRequest() {
|
||||
pingTimeLimit = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function handleMessage(message) {
|
||||
messageData = JSON.parse(message.data.length);
|
||||
socket.removeEventListener('message', handleMessage);
|
||||
@@ -362,7 +362,7 @@ function sendPingRequest() {
|
||||
socket.addEventListener('message', handleMessage);
|
||||
messageLength = messageData;
|
||||
messageData = 0;
|
||||
|
||||
|
||||
// Force reconnection if no WebSocket data after several queries
|
||||
if (messageLength === 0) {
|
||||
messageCounter++;
|
||||
@@ -375,7 +375,7 @@ function sendPingRequest() {
|
||||
} else {
|
||||
messageCounter = 0;
|
||||
}
|
||||
|
||||
|
||||
// Automatic reconnection on WebSocket close with cooldown
|
||||
const now = Date.now();
|
||||
if (
|
||||
@@ -421,12 +421,12 @@ function handleWebSocketMessage(event) {
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
parsedData = JSON.parse(event.data);
|
||||
|
||||
|
||||
resetDataTimeout();
|
||||
updatePanels(parsedData);
|
||||
|
||||
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
data.push(averageSignal);
|
||||
@@ -500,11 +500,11 @@ function initCanvas() {
|
||||
beginAtZero: false,
|
||||
grace: 0.25,
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
display: false // Hide default labels
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
display: false, // Hide default grid lines
|
||||
},
|
||||
},
|
||||
@@ -513,11 +513,11 @@ function initCanvas() {
|
||||
beginAtZero: false,
|
||||
grace: 0.25,
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
display: false // Hide default labels for the right axis
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
display: false, // No grid for right axis
|
||||
}
|
||||
}
|
||||
@@ -558,21 +558,21 @@ function initCanvas() {
|
||||
case "dbm": adjustedTickValue = tick.value - 120; break;
|
||||
default: adjustedTickValue = tick.value; break;
|
||||
}
|
||||
|
||||
|
||||
if (isMiddleTick) { adjustedY += 3; }
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(adjustedTickValue.toFixed(1), leftX + 25, adjustedY);
|
||||
ctx.fillText(adjustedTickValue.toFixed(1), leftX + 25, adjustedY);
|
||||
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(adjustedTickValue.toFixed(1), rightX - 25, adjustedY); // Right side
|
||||
});
|
||||
|
||||
|
||||
const gridLineWidth = 0.5; // Make the lines thinner to avoid overlapping text
|
||||
const adjustedGridTop = chartArea.top + offset;
|
||||
const adjustedGridBottom = chartArea.bottom - offset;
|
||||
const middleY = chartArea.top + chartArea.height / 2;
|
||||
const padding = 45; // 30px inward on both sides
|
||||
|
||||
|
||||
// Helper function to draw a horizontal line
|
||||
function drawGridLine(y) {
|
||||
ctx.beginPath();
|
||||
@@ -582,12 +582,12 @@ function initCanvas() {
|
||||
ctx.lineWidth = gridLineWidth;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
|
||||
// Draw the three horizontal grid lines
|
||||
drawGridLine(adjustedGridTop);
|
||||
drawGridLine(adjustedGridBottom);
|
||||
drawGridLine(middleY);
|
||||
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}]
|
||||
@@ -634,12 +634,12 @@ socket.onmessage = (event) => {
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
parsedData = JSON.parse(event.data);
|
||||
|
||||
|
||||
resetDataTimeout();
|
||||
updatePanels(parsedData);
|
||||
|
||||
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
data.push(averageSignal);
|
||||
@@ -661,7 +661,7 @@ function processString(string, errors) {
|
||||
const alpha_range = 50;
|
||||
const max_error = 10;
|
||||
errors = errors?.split(',');
|
||||
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
alpha = parseInt(errors[i]) * (alpha_range / (max_error + 1));
|
||||
if (alpha) {
|
||||
@@ -670,27 +670,27 @@ function processString(string, errors) {
|
||||
output += escapeHTML(string[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function checkKey(e) {
|
||||
e = e || window.event;
|
||||
|
||||
|
||||
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ($('#password:focus').length > 0
|
||||
|| $('#chat-send-message:focus').length > 0
|
||||
|| $('#volumeSlider:focus').length > 0
|
||||
|| $('#chat-nickname:focus').length > 0
|
||||
|| $('.option:focus').length > 0) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
getCurrentFreq();
|
||||
|
||||
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
switch (e.keyCode) {
|
||||
case 66: // Back to previous frequency
|
||||
@@ -717,9 +717,9 @@ function checkKey(e) {
|
||||
let $dropdown = $(".data-ant");
|
||||
let $input = $dropdown.find("input");
|
||||
let $options = $dropdown.find("ul.options .option");
|
||||
|
||||
|
||||
if ($options.length === 0) return; // No antennas available
|
||||
|
||||
|
||||
// Find the currently selected antenna
|
||||
let currentText = $input.val().trim();
|
||||
let currentIndex = $options.index($options.filter(function () {
|
||||
@@ -727,15 +727,15 @@ function checkKey(e) {
|
||||
}));
|
||||
|
||||
console.log(currentIndex, currentText);
|
||||
|
||||
|
||||
// Cycle to the next option
|
||||
let nextIndex = (currentIndex + 1) % $options.length;
|
||||
let $nextOption = $options.eq(nextIndex);
|
||||
|
||||
|
||||
// Update UI
|
||||
$input.attr("placeholder", $nextOption.text());
|
||||
$input.data("value", $nextOption.data("value"));
|
||||
|
||||
|
||||
let socketMessage = "Z" + $nextOption.data("value");
|
||||
socket.send(socketMessage);
|
||||
break;
|
||||
@@ -769,7 +769,7 @@ async function copyPs() {
|
||||
var signal = $('#data-signal').text();
|
||||
var signalDecimal = $('#data-signal-decimal').text();
|
||||
var signalUnit = $('.signal-units').eq(0).text();
|
||||
|
||||
|
||||
try {
|
||||
await copyToClipboard(frequency + " - " + pi + " | " + ps + " [" + signal + signalDecimal + " " + signalUnit + "]");
|
||||
} catch (error) {
|
||||
@@ -785,7 +785,7 @@ async function copyTx() {
|
||||
const stationItu = $('#data-station-itu').text();
|
||||
const stationDistance = $('#data-station-distance').text();
|
||||
const stationErp = $('#data-station-erp').text();
|
||||
|
||||
|
||||
try {
|
||||
await copyToClipboard(frequency + " - " + pi + " | " + stationName + " [" + stationCity + ", " + stationItu + "] - " + stationDistance + " | " + stationErp + " kW");
|
||||
} catch (error) {
|
||||
@@ -796,7 +796,7 @@ async function copyTx() {
|
||||
async function copyRt() {
|
||||
var rt0 = $('#data-rt0 span').text();
|
||||
var rt1 = $('#data-rt1 span').text();
|
||||
|
||||
|
||||
try {
|
||||
await copyToClipboard("[0] RT: " + rt0 + "\n[1] RT: " + rt1);
|
||||
} catch (error) {
|
||||
@@ -818,10 +818,10 @@ function copyToClipboard(textToCopy) {
|
||||
'position': 'absolute',
|
||||
'left': '-999999px'
|
||||
});
|
||||
|
||||
|
||||
$('body').prepend(textArea);
|
||||
textArea.select();
|
||||
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (error) {
|
||||
@@ -837,9 +837,9 @@ function findOnMaps() {
|
||||
var pi = $('#data-pi').text();
|
||||
var latitude = localStorage.getItem('qthLongitude');
|
||||
var longitude = localStorage.getItem('qthLatitude');
|
||||
|
||||
|
||||
frequency > 74 ? frequency = frequency.toFixed(1) : null;
|
||||
|
||||
|
||||
var url = `https://maps.fmdx.org/#qth=${longitude},${latitude}&freq=${frequency}&findPi=${pi}`;
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
@@ -849,33 +849,33 @@ function updateSignalUnits(parsedData, averageSignal) {
|
||||
const signalUnit = localStorage.getItem('signalUnit');
|
||||
let currentSignal;
|
||||
let highestSignal = parsedData.sigTop;
|
||||
|
||||
|
||||
currentSignal = averageSignal
|
||||
let signalText = $('.signal-units');
|
||||
let signalValue;
|
||||
|
||||
|
||||
switch (signalUnit) {
|
||||
case 'dbuv':
|
||||
signalValue = currentSignal - 11.25;
|
||||
highestSignal = highestSignal - 11.25;
|
||||
signalText.text('dBµV');
|
||||
break;
|
||||
|
||||
|
||||
case 'dbm':
|
||||
signalValue = currentSignal - 120;
|
||||
highestSignal = highestSignal - 120;
|
||||
signalText.text('dBm');
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
signalValue = currentSignal;
|
||||
signalText.text('dBf');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const formatted = (Math.round(signalValue * 10) / 10).toFixed(1);
|
||||
const [integerPart, decimalPart] = formatted.split('.');
|
||||
|
||||
|
||||
$('#data-signal-highest').text(Number(highestSignal).toFixed(1));
|
||||
$('#data-signal').text(integerPart);
|
||||
$('#data-signal-decimal').text('.' + decimalPart);
|
||||
@@ -890,6 +890,7 @@ const $dataSt = $('.data-st');
|
||||
const $dataRt0 = $('#data-rt0 span');
|
||||
const $dataRt1 = $('#data-rt1 span');
|
||||
const $dataAntInput = $('.data-ant input');
|
||||
const $dataAgcInput = $('.data-agc input');
|
||||
const $dataBwInput = $('.data-bw input');
|
||||
const $dataStationContainer = $('#data-station-container');
|
||||
const $dataTp = $('.data-tp');
|
||||
@@ -901,17 +902,17 @@ const $dataPty = $('.data-pty');
|
||||
// Throttling function to limit the frequency of updates
|
||||
function throttle(fn, wait) {
|
||||
let isThrottled = false, savedArgs, savedThis;
|
||||
|
||||
|
||||
function wrapper() {
|
||||
if (isThrottled) {
|
||||
savedArgs = arguments;
|
||||
savedThis = this;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
fn.apply(this, arguments);
|
||||
isThrottled = true;
|
||||
|
||||
|
||||
setTimeout(function() {
|
||||
isThrottled = false;
|
||||
if (savedArgs) {
|
||||
@@ -920,7 +921,7 @@ function throttle(fn, wait) {
|
||||
}
|
||||
}, wait);
|
||||
}
|
||||
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -967,18 +968,18 @@ const updateDataElements = throttle(function(parsedData) {
|
||||
updateTextIfChanged($dataFrequency, parsedData.freq);
|
||||
$commandInput.attr("aria-label", "Current frequency: " + parsedData.freq);
|
||||
updateHtmlIfChanged($dataPi, parsedData.pi === '?' ? "<span class='opacity-half'>?</span>" : parsedData.pi);
|
||||
|
||||
|
||||
if ($('#ps-underscores').is(':checked')) {
|
||||
parsedData.ps = parsedData.ps.replace(/\s/g, '_');
|
||||
}
|
||||
updateHtmlIfChanged($dataPs, parsedData.ps === '?' ? "<span class='opacity-half'>?</span>" : processString(parsedData.ps, parsedData.ps_errors));
|
||||
|
||||
|
||||
if(parsedData.st) {
|
||||
$dataSt.parent().removeClass('opacity-half');
|
||||
} else {
|
||||
$dataSt.parent().addClass('opacity-half');
|
||||
}
|
||||
|
||||
|
||||
if(parsedData.stForced) {
|
||||
if (!parsedData.st) {
|
||||
stereoColor = 'gray';
|
||||
@@ -991,29 +992,28 @@ const updateDataElements = throttle(function(parsedData) {
|
||||
$('.data-st.circle1').css('left', '0px');
|
||||
$('.data-st.circle2').css('display', 'block');
|
||||
}
|
||||
|
||||
|
||||
updateHtmlIfChanged($dataRt0, processString(parsedData.rt0, parsedData.rt0_errors));
|
||||
updateHtmlIfChanged($dataRt1, processString(parsedData.rt1, parsedData.rt1_errors));
|
||||
|
||||
|
||||
updateTextIfChanged($dataPty, rdsMode == 'true' ? usa_programmes[parsedData.pty] : europe_programmes[parsedData.pty]);
|
||||
|
||||
|
||||
if (parsedData.rds === true) {
|
||||
$flagDesktopCointainer.css('background-color', 'var(--color-2-transparent)');
|
||||
} else {
|
||||
$flagDesktopCointainer.css('background-color', 'var(--color-1-transparent)');
|
||||
}
|
||||
|
||||
|
||||
$('.data-flag').html(`<i title="${parsedData.country_name}" class="flag-sm flag-sm-${parsedData.country_iso}"></i>`);
|
||||
$('.data-flag-big').html(`<i title="${parsedData.country_name}" class="flag-md flag-md-${parsedData.country_iso}"></i>`);
|
||||
|
||||
|
||||
$dataAntInput.val($('.data-ant li[data-value="' + parsedData.ant + '"]').first().text());
|
||||
|
||||
if (parsedData.bw < 500) {
|
||||
$dataBwInput.val($('.data-bw li[data-value2="' + parsedData.bw + '"]').first().text());
|
||||
} else {
|
||||
$dataBwInput.val($('.data-bw li[data-value="' + parsedData.bw + '"]').first().text());
|
||||
}
|
||||
|
||||
|
||||
if (typeof parsedData.agc !== 'undefined') $dataAgcInput.val($('.data-agc li[data-value="' + parsedData.agc + '"]').first().text());
|
||||
|
||||
if (parsedData.bw < 500) $dataBwInput.val($('.data-bw li[data-value2="' + parsedData.bw + '"]').first().text());
|
||||
else $dataBwInput.val($('.data-bw li[data-value="' + parsedData.bw + '"]').first().text());
|
||||
|
||||
if (parsedData.txInfo.tx.length > 1) {
|
||||
updateTextIfChanged($('#data-station-name'), parsedData.txInfo.tx.replace(/%/g, '%25'));
|
||||
updateTextIfChanged($('#data-station-erp'), parsedData.txInfo.erp);
|
||||
@@ -1028,17 +1028,12 @@ const updateDataElements = throttle(function(parsedData) {
|
||||
updateHtmlIfChanged($('#alternative-txes'), altTxInfo);
|
||||
updateTextIfChanged($('#data-station-distance'), txDistance);
|
||||
$dataStationContainer.css('display', 'block');
|
||||
} else {
|
||||
$dataStationContainer.removeAttr('style');
|
||||
}
|
||||
|
||||
if(parsedData.txInfo.tx.length > 1 && parsedData.txInfo.dist > 150 && parsedData.txInfo.dist < 4000) {
|
||||
$('.log-fmlist').removeAttr('disabled').removeClass('btn-disabled cursor-disabled');
|
||||
} else {
|
||||
$('.log-fmlist').attr('disabled', 'true').addClass('btn-disabled cursor-disabled');
|
||||
}
|
||||
} else $dataStationContainer.removeAttr('style');
|
||||
|
||||
if(parsedData.txInfo.tx.length > 1 && parsedData.txInfo.dist > 150 && parsedData.txInfo.dist < 4000) $('.log-fmlist').removeAttr('disabled').removeClass('btn-disabled cursor-disabled');
|
||||
else $('.log-fmlist').attr('disabled', 'true').addClass('btn-disabled cursor-disabled');
|
||||
updateHtmlIfChanged($('#data-regular-pi'), parsedData.txInfo.reg === true ? parsedData.txInfo.pi : ' ');
|
||||
|
||||
|
||||
if (updateCounter % 8 === 0) {
|
||||
$dataTp.html(parsedData.tp === 0 ? "<span class='opacity-half'>TP</span>" : "TP");
|
||||
$dataTa.html(parsedData.ta === 0 ? "<span class='opacity-half'>TA</span>" : "TA");
|
||||
@@ -1050,7 +1045,7 @@ const updateDataElements = throttle(function(parsedData) {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (updateCounter % 30 === 0) {
|
||||
$dataPs.attr('aria-label', parsedData.ps);
|
||||
$dataRt0.attr('aria-label', parsedData.rt0);
|
||||
@@ -1063,36 +1058,35 @@ let isEventListenerAdded = false;
|
||||
|
||||
function updatePanels(parsedData) {
|
||||
updateCounter = (updateCounter % 10000) + 1; // Count to 10000 then reset back to 1
|
||||
|
||||
|
||||
signalData.push(parsedData.sig);
|
||||
if (signalData.length > 8) {
|
||||
signalData.shift(); // Remove the oldest element
|
||||
}
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
|
||||
|
||||
const sortedAf = parsedData.af.sort(compareNumbers);
|
||||
const scaledArray = sortedAf.map(element => element / 1000);
|
||||
|
||||
|
||||
const listContainer = $('#af-list');
|
||||
const scrollTop = listContainer.scrollTop();
|
||||
let ul = listContainer.find('ul');
|
||||
|
||||
|
||||
if (!ul.length) {
|
||||
ul = $('<ul></ul>');
|
||||
listContainer.append(ul);
|
||||
}
|
||||
|
||||
|
||||
if (updateCounter % 3 === 0) {
|
||||
|
||||
updateButtonState("data-eq", parsedData.eq);
|
||||
updateButtonState("data-ims", parsedData.ims);
|
||||
|
||||
|
||||
// Only update #af-list on every 3rd call
|
||||
ul.html('');
|
||||
const listItems = scaledArray.map(createListItem);
|
||||
ul.append(listItems);
|
||||
|
||||
|
||||
// Add the event listener only once
|
||||
if (!isEventListenerAdded) {
|
||||
ul.on('click', 'a', function () {
|
||||
@@ -1101,10 +1095,10 @@ function updatePanels(parsedData) {
|
||||
});
|
||||
isEventListenerAdded = true;
|
||||
}
|
||||
|
||||
|
||||
listContainer.scrollTop(scrollTop);
|
||||
}
|
||||
|
||||
|
||||
updateDataElements(parsedData);
|
||||
updateSignalUnits(parsedData, averageSignal);
|
||||
$('.users-online').text(parsedData.users);
|
||||
@@ -1116,11 +1110,11 @@ function createListItem(element) {
|
||||
|
||||
function updateButtonState(buttonId, value) {
|
||||
var button = $("#" + buttonId);
|
||||
|
||||
|
||||
if (button.length === 0) {
|
||||
button = $("." + buttonId);
|
||||
}
|
||||
|
||||
|
||||
if (button.length > 0) {
|
||||
if (value == 0) {
|
||||
button.hasClass("btn-disabled") ? null : button.addClass("btn-disabled");
|
||||
@@ -1152,7 +1146,7 @@ function toggleForcedStereo() {
|
||||
|
||||
function toggleLock(buttonSelector, activeMessage, inactiveMessage, activeLabel, inactiveLabel) {
|
||||
let $lockButton = $(buttonSelector);
|
||||
|
||||
|
||||
if ($lockButton.hasClass('active')) {
|
||||
socket.send(inactiveMessage);
|
||||
$lockButton.attr('aria-label', inactiveLabel);
|
||||
@@ -1166,17 +1160,17 @@ function toggleLock(buttonSelector, activeMessage, inactiveMessage, activeLabel,
|
||||
|
||||
function showTunerDescription() {
|
||||
let parentDiv = $("#tuner-name").parent();
|
||||
|
||||
|
||||
if (!$("#dashboard-panel-description").is(":visible")) {
|
||||
parentDiv.css("border-radius", "15px 15px 0 0");
|
||||
}
|
||||
|
||||
|
||||
$("#dashboard-panel-description").slideToggle(300, function() {
|
||||
if (!$(this).is(":visible")) {
|
||||
parentDiv.css("border-radius", "");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#tuner-name i").toggleClass("rotated");
|
||||
|
||||
if ($(window).width() < 768) {
|
||||
@@ -1187,29 +1181,29 @@ function showTunerDescription() {
|
||||
function initTooltips(target = null) {
|
||||
// Define scope: all tooltips or specific one if target is provided
|
||||
const tooltips = target ? $(target) : $('.tooltip');
|
||||
|
||||
|
||||
// Unbind existing event handlers before rebinding to avoid duplication
|
||||
tooltips.off('mouseenter mouseleave');
|
||||
|
||||
|
||||
tooltips.hover(function () {
|
||||
if ($(this).closest('.popup-content').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var tooltipText = $(this).data('tooltip');
|
||||
var placement = $(this).data('tooltip-placement') || 'top'; // Default to 'top'
|
||||
|
||||
|
||||
// Clear existing timeouts
|
||||
$(this).data('timeout', setTimeout(() => {
|
||||
$('.tooltip-wrapper').remove();
|
||||
|
||||
|
||||
var tooltip = $(`
|
||||
<div class="tooltip-wrapper">
|
||||
<div class="tooltiptext">${tooltipText}</div>
|
||||
</div>
|
||||
`);
|
||||
$('body').append(tooltip);
|
||||
|
||||
|
||||
var tooltipEl = $('.tooltiptext');
|
||||
var tooltipWidth = tooltipEl.outerWidth();
|
||||
var tooltipHeight = tooltipEl.outerHeight();
|
||||
@@ -1217,7 +1211,7 @@ function initTooltips(target = null) {
|
||||
var targetOffset = targetEl.offset();
|
||||
var targetWidth = targetEl.outerWidth();
|
||||
var targetHeight = targetEl.outerHeight();
|
||||
|
||||
|
||||
// Compute position
|
||||
var posX, posY;
|
||||
switch (placement) {
|
||||
@@ -1239,7 +1233,7 @@ function initTooltips(target = null) {
|
||||
posY = targetOffset.top - tooltipHeight - 10;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Apply positioning
|
||||
tooltipEl.css({ top: posY, left: posX, opacity: 1 });
|
||||
|
||||
@@ -1247,32 +1241,32 @@ function initTooltips(target = null) {
|
||||
if ((/Mobi|Android|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent)) && ('ontouchstart' in window || navigator.maxTouchPoints)) {
|
||||
setTimeout(() => { $('.tooltiptext').remove(); }, 5000);
|
||||
}
|
||||
|
||||
|
||||
}, 300));
|
||||
}, function () {
|
||||
clearTimeout($(this).data('timeout'));
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
$('.tooltip-wrapper').fadeOut(300, function () {
|
||||
$(this).remove();
|
||||
$(this).remove();
|
||||
});
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
|
||||
$('.popup-content').off('mouseenter').on('mouseenter', function () {
|
||||
clearTimeout($('.tooltip').data('timeout'));
|
||||
$('.tooltip-wrapper').fadeOut(300, function () {
|
||||
$(this).remove();
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function fillPresets() {
|
||||
let hasAnyPreset = false;
|
||||
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
let presetText = localStorage.getItem(`preset${i}`);
|
||||
|
||||
|
||||
if (presetText != "null") {
|
||||
hasAnyPreset = true;
|
||||
$(`#preset${i}-text`).text(presetText);
|
||||
@@ -1283,9 +1277,8 @@ function initTooltips(target = null) {
|
||||
$(`#preset${i}`).hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!hasAnyPreset) {
|
||||
$('#preset1').parent().hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ $(document).ready(function() {
|
||||
var modalPanel = $(".modal-panel");
|
||||
var openBtn = $(".settings");
|
||||
var closeBtn = $(".closeModal, .closeModalButton");
|
||||
|
||||
|
||||
initPopups();
|
||||
|
||||
|
||||
openBtn.on("click", function() {
|
||||
openModal(modalPanel);
|
||||
});
|
||||
|
||||
|
||||
closeBtn.on("click", closeModal);
|
||||
|
||||
|
||||
function openModal(panel) {
|
||||
modal.css("display", "block");
|
||||
panel.css("display", "block");
|
||||
@@ -20,7 +20,7 @@ $(document).ready(function() {
|
||||
modal.css("opacity", 1);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
|
||||
function closeModal() {
|
||||
modal.css("opacity", 0);
|
||||
setTimeout(function() {
|
||||
@@ -28,24 +28,20 @@ $(document).ready(function() {
|
||||
$("body").removeClass("modal-open"); // Enable body scrolling
|
||||
}, 300);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$(document).on("click", function(event) { // Close the modal when clicking outside of it
|
||||
if ($(event.target).is(modal)) {
|
||||
closeModal();
|
||||
}
|
||||
if ($(event.target).is(modal)) closeModal();
|
||||
});
|
||||
|
||||
|
||||
$(document).on("keydown", function(event) { // Close the modal when pressing ESC key
|
||||
if (event.key === "Escape") {
|
||||
closeModal();
|
||||
}
|
||||
if (event.key === "Escape") closeModal();
|
||||
});
|
||||
|
||||
$(".tuner-mobile-settings").on("click", function () {
|
||||
togglePopup("#popup-panel-mobile-settings");
|
||||
});
|
||||
|
||||
|
||||
$("#data-station-others").on("click", function () {
|
||||
togglePopup("#popup-panel-transmitters");
|
||||
});
|
||||
@@ -54,13 +50,13 @@ $(document).ready(function() {
|
||||
function initPopups() {
|
||||
$(".popup-window").draggable({
|
||||
handle: ".popup-header",
|
||||
containment: "body"
|
||||
containment: "body"
|
||||
}).resizable({
|
||||
minHeight: 330,
|
||||
minWidth: 350,
|
||||
containment: "body"
|
||||
});
|
||||
|
||||
|
||||
$(".popup-close").on("click", function () {
|
||||
$(".popup-window").fadeOut(200);
|
||||
});
|
||||
@@ -69,9 +65,8 @@ function initPopups() {
|
||||
function togglePopup(targetSelector) {
|
||||
const $target = $(targetSelector);
|
||||
|
||||
if ($target.is(":visible")) {
|
||||
$target.fadeOut(200);
|
||||
} else {
|
||||
if ($target.is(":visible")) $target.fadeOut(200);
|
||||
else {
|
||||
$(".popup-window").fadeOut(200);
|
||||
$target.fadeIn(200);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,27 @@ function getQueryParameter(name) {
|
||||
return urlParams.get(name);
|
||||
}
|
||||
|
||||
function updateFavicon(color) {
|
||||
function rgbToHex(rgb) {
|
||||
const result = rgb.match(/\d+/g);
|
||||
return "#" + result.slice(0, 3).map(x =>(+x).toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
const hex = rgbToHex(color);
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<circle cx="64" cy="64" r="54" fill="none" stroke="${hex}" stroke-width="20"/>
|
||||
<circle cx="64" cy="64" r="22" fill="none" stroke="white" stroke-width="18"/>
|
||||
</svg>`;
|
||||
|
||||
const base64 = btoa(svg);
|
||||
|
||||
$('#favicon').attr(
|
||||
'href',
|
||||
`data:image/svg+xml;base64,${base64}`
|
||||
);
|
||||
}
|
||||
|
||||
function setTheme(themeName) {
|
||||
const themeColors = themes[themeName];
|
||||
if (themeColors) {
|
||||
@@ -94,6 +115,7 @@ function setTheme(themeName) {
|
||||
$(':root').css('--color-text', themeColors[2]);
|
||||
$(':root').css('--color-text-2', textColor2);
|
||||
$('.wrapper-outer').css('background-color', backgroundColorWithOpacity);
|
||||
updateFavicon(themeColors[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,9 +189,7 @@ function loadInitialSettings() {
|
||||
if(signalParameter && !localStorage.getItem('signalUnit')) {
|
||||
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + signalParameter + '"]').text());
|
||||
localStorage.setItem('signalUnit', signalParameter);
|
||||
} else {
|
||||
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
|
||||
}
|
||||
} else signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
|
||||
|
||||
signalSelector.on('click', '.option', (event) => {
|
||||
const selectedSignalUnit = $(event.target).data('value');
|
||||
|
||||
@@ -24,11 +24,10 @@ function mapCreate() {
|
||||
if (!(typeof map == "object")) {
|
||||
map = L.map('map', {
|
||||
center: [40, 0],
|
||||
zoom: 3
|
||||
zoom: 3,
|
||||
worldCopyJump: true
|
||||
});
|
||||
} else {
|
||||
map.setZoom(3).panTo([40, 0]);
|
||||
}
|
||||
} else map.setZoom(3).panTo([40, 0]);
|
||||
|
||||
L.tileLayer(tilesURL, {
|
||||
attribution: mapAttrib,
|
||||
@@ -55,9 +54,9 @@ function mapCreate() {
|
||||
$('#identification-lon').val(ev.latlng.lng.toFixed(6));
|
||||
|
||||
if (typeof pin == "object") {
|
||||
pin.setLatLng(ev.latlng);
|
||||
pin.setLatLng(ev.latlng.wrap());
|
||||
} else {
|
||||
pin = L.marker(ev.latlng, { riseOnHover: true, draggable: true }).addTo(map);
|
||||
pin = L.marker(ev.latlng.wrap(), { riseOnHover: true, draggable: true }).addTo(map);
|
||||
pin.on('dragend', function(ev) {
|
||||
$('#identification-lat').val(ev.target.getLatLng().lat.toFixed(6));
|
||||
$('#identification-lon').val(ev.target.getLatLng().lng.toFixed(6));
|
||||
@@ -116,11 +115,8 @@ function initBanlist() {
|
||||
data: { ip: ipAddress, reason: reason },
|
||||
success: function(response) {
|
||||
// Refresh the page if the request was successful
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
console.error('Failed to add to banlist');
|
||||
}
|
||||
if (response.success) location.reload();
|
||||
else console.error('Failed to add to banlist');
|
||||
},
|
||||
error: function() {
|
||||
console.error('Error occurred during the request');
|
||||
@@ -262,8 +258,8 @@ function checkTunnelServers() {
|
||||
url: '/tunnelservers',
|
||||
method: 'GET',
|
||||
success: function(servers) {
|
||||
const $options = $('#tunnel-server ul.options');
|
||||
const $input = $('#tunnel-serverSelect');
|
||||
const $options = $('#tunnel-regionselect ul.options');
|
||||
const $input = $('#tunnel-region');
|
||||
const selectedValue = $input.val(); // currently selected value (label or value?)
|
||||
|
||||
servers.forEach(server => {
|
||||
@@ -274,9 +270,7 @@ function checkTunnelServers() {
|
||||
|
||||
// If this li is the currently selected one, update input text too
|
||||
// Note: input.val() holds the label, so match by label is safer
|
||||
if ($li.text() === selectedValue || server.value === selectedValue) {
|
||||
$input.val(server.label);
|
||||
}
|
||||
if ($li.text() === selectedValue || server.value === selectedValue) $input.val(server.label);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
const versionDate = new Date('Nov 30, 2025 23:00:00');
|
||||
const currentVersion = `v1.3.12 [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`;
|
||||
const versionDate = new Date('Feb 24, 2026 15:00:00');
|
||||
const currentVersion = `v1.4.0a [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`;
|
||||
@@ -27,8 +27,6 @@ function navigateStep(isNext) {
|
||||
currentStep.hide();
|
||||
targetStep.show();
|
||||
updateProgressBar(targetStep);
|
||||
} else if (isNext) {
|
||||
submitConfig();
|
||||
}
|
||||
} else if (isNext) submitConfig();
|
||||
updateWizardContent();
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="wrapper-outer wrapper-full">
|
||||
<div id="wrapper">
|
||||
<div class="panel-100 no-bg">
|
||||
<img class="top-25" src="favicon.png" height="64px">
|
||||
<img class="top-25" src="favicon.svg" height="64px">
|
||||
<p>You are currently not logged in as an administrator and therefore can't change the settings.</p>
|
||||
<p>Please login below.</p>
|
||||
</div>
|
||||
|
||||
126
web/setup.ejs
126
web/setup.ejs
@@ -8,7 +8,7 @@
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
@@ -109,7 +109,7 @@
|
||||
<a href="https://dnschecker.org/ip-location.php?ip=<%= user.ip.replace('::ffff:', '') %>" target="_blank">
|
||||
<%= user.ip.replace('::ffff:', '') %>
|
||||
</a>
|
||||
</td>
|
||||
</td>
|
||||
<td><%= user.location %></td>
|
||||
<td><%= user.time %></td>
|
||||
<td><a href="./kick?ip=<%= user.ip %>">Kick</a></td>
|
||||
@@ -121,10 +121,10 @@
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-100-real p-bottom-20">
|
||||
<h3>Quick settings</h3>
|
||||
@@ -132,7 +132,7 @@
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Unlocked Tuner', id: 'publicTuner'}) %>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Admin lock', id: 'lockToAdmin'}) %><br>
|
||||
</div>
|
||||
|
||||
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Tune password', id: 'password-tunePass', password: true}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Admin password', id: 'password-adminPass', password: true}) %><br>
|
||||
</div>
|
||||
@@ -156,7 +156,7 @@
|
||||
|
||||
<div class="panel-full tab-content no-bg m-0" id="audio" role="tabpanel">
|
||||
<h2>Audio settings</h2>
|
||||
|
||||
|
||||
<div class="flex-container contains-dropdown">
|
||||
<div class="panel-33 p-bottom-20">
|
||||
<h3>Device</h3>
|
||||
@@ -181,14 +181,14 @@
|
||||
label: `${device.name}`
|
||||
}))
|
||||
]
|
||||
}) %>
|
||||
}) %>
|
||||
</div>
|
||||
<div class="panel-33 p-bottom-20">
|
||||
<h3>Channels</h3>
|
||||
<p>Audio channel count.<br>
|
||||
<span class="text-gray">Choose between Mono / Stereo.</span>
|
||||
</p>
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-channels-dropdown', inputId: 'audio-audioChannels', label: 'Audio channels', cssClass: '', placeholder: 'Stereo',
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-channels-dropdown', inputId: 'audio-audioChannels', label: 'Audio channels', cssClass: '', placeholder: 'Stereo',
|
||||
options: [
|
||||
{ value: '2', label: 'Stereo' },
|
||||
{ value: '1', label: 'Mono' }
|
||||
@@ -200,7 +200,7 @@
|
||||
<p>The bitrate of the mp3 audio.<br>
|
||||
<span class="text-gray">Minimum: 64 Kbps • Maximum: 320 Kbps</span>
|
||||
</p>
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-quality-dropdown', inputId: 'audio-audioBitrate', label: 'Audio quality', cssClass: '', placeholder: '128kbps (standard)',
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-quality-dropdown', inputId: 'audio-audioBitrate', label: 'Audio quality', cssClass: '', placeholder: '128kbps (standard)',
|
||||
options: [
|
||||
{ value: '64k', label: '64kbps (lowest quality)' },
|
||||
{ value: '96k', label: '96kbps (low quality)' },
|
||||
@@ -215,8 +215,8 @@
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Volume</h3>
|
||||
<p>This option will boost the audio volume globally, recommended for the Headless TEF.</p>
|
||||
<h3>Audio boost</h3>
|
||||
<p>This option will boost the audio volume. Use if the output is too quiet.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Audio Boost', id: 'audio-audioBoost'}) %>
|
||||
</div>
|
||||
<div class="panel-50 p-bottom-20">
|
||||
@@ -251,11 +251,11 @@
|
||||
</div>
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Design</h3>
|
||||
<h4>Background image</h4>
|
||||
<%- include('_components', {component: 'text', cssClass: 'br-15', placeholder: 'Direct image link', label: 'Image link', id: 'webserver-bgImage'}) %><br>
|
||||
<h4>Background image</h4>
|
||||
<%- include('_components', {component: 'text', cssClass: 'br-15', placeholder: 'Direct image link', label: 'Image link', id: 'webserver-bgImage'}) %><br>
|
||||
|
||||
<h4 class="top-25">Themes</h4>
|
||||
<%- include('_components', { component: 'dropdown', id: 'server-theme-selector', inputId: 'webserver-defaultTheme', label: 'Default server theme', cssClass: '', placeholder: 'Default',
|
||||
<%- include('_components', { component: 'dropdown', id: 'server-theme-selector', inputId: 'webserver-defaultTheme', label: 'Default server theme', cssClass: '', placeholder: 'Default',
|
||||
options: [
|
||||
{ value: 'theme1', label: 'Mint' },
|
||||
{ value: 'theme2', label: 'Cappuccino' },
|
||||
@@ -280,17 +280,17 @@
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 1', id: 'antennas-ant1-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant A', label: 'Antenna 1 name', id: 'antennas-ant1-name'}) %><br>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'top-25', label: 'Antenna 2', id: 'antennas-ant2-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant B', label: 'Antenna 2 name', id: 'antennas-ant2-name'}) %><br>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 3', id: 'antennas-ant3-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant C', label: 'Antenna 3 name', id: 'antennas-ant3-name'}) %><br>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 4', id: 'antennas-ant4-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant D', label: 'Antenna 4 name', id: 'antennas-ant4-name'}) %><br>
|
||||
@@ -337,7 +337,7 @@
|
||||
<div class="panel-50 p-bottom-20" style="padding-left: 20px; padding-right: 20px; padding-bottom: 80px;">
|
||||
<h3>Transmitter Search Algorithm</h3>
|
||||
<p>Different modes may help with more accurate transmitter identification depending on your region.</p>
|
||||
<%- include('_components', { component: 'dropdown', id: 'server-tx-id-algo', inputId: 'webserver-txIdAlgorithm', label: 'Transmitter ID Algorithm', cssClass: '', placeholder: 'Algorithm 1',
|
||||
<%- include('_components', { component: 'dropdown', id: 'server-tx-id-algo', inputId: 'webserver-txIdAlgorithm', label: 'Transmitter ID Algorithm', cssClass: '', placeholder: 'Algorithm 1',
|
||||
options: [
|
||||
{ value: '0', label: 'Algorithm 1' },
|
||||
{ value: '1', label: 'Algorithm 2' },
|
||||
@@ -351,23 +351,22 @@
|
||||
|
||||
<div class="panel-full m-0 tab-content no-bg" id="tuner" role="tabpanel">
|
||||
<h2>Tuner settings</h2>
|
||||
<div class="flex-container contains-dropdown">
|
||||
<div class="panel-33 p-bottom-20">
|
||||
<div class="panel-100 p-bottom-20 contains-dropdown" style="z-index: 991;">
|
||||
<h3>Device type</h3>
|
||||
<%- include('_components', { component: 'dropdown', id: 'device-selector', inputId: 'device', label: 'Device', cssClass: '', placeholder: 'TEF668x / TEA685x',
|
||||
options: [
|
||||
{ value: 'tef', label: 'TEF668x / TEA685x' },
|
||||
{ value: 'xdr', label: 'XDR (F1HD / S10HDiP)' },
|
||||
{ value: 'sdr', label: 'SDR (RTL-SDR / AirSpy)' },
|
||||
{ value: 'other', label: 'Other' }
|
||||
]
|
||||
}) %><br>
|
||||
|
||||
<div class="flex-center" style="max-width: 520px; margin: 10px auto 0;">
|
||||
<%- include('_components', { component: 'dropdown', id: 'device-selector', inputId: 'device', label: 'Device', cssClass: '', placeholder: 'TEF668x / TEA685x',
|
||||
options: tunerProfiles.map(profile => ({
|
||||
value: profile.id,
|
||||
label: profile.label
|
||||
}))
|
||||
}) %><br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-33 p-bottom-20" style="padding-right: 20px; padding-left: 20px;">
|
||||
<div class="flex-container contains-dropdown">
|
||||
<div class="panel-100 p-bottom-20" style="padding-right: 20px; padding-left: 20px;">
|
||||
<h3>Connection type</h3>
|
||||
<p class="text-gray">If you want to choose the COM port directly, choose "Direct".<br>If you use xdrd or your receiver is connected via Wi-Fi, choose TCP/IP.</p>
|
||||
<p class="text-gray">If you want to choose the serial port directly, choose "Direct".<br>If you use xdrd or your receiver is connected via Wi-Fi, choose TCP/IP.</p>
|
||||
<div class="auto top-10">
|
||||
<label class="toggleSwitch nolabel" onclick="">
|
||||
<input id="xdrd-wirelessConnection" type="checkbox" tabindex="0" aria-label="Connection type"/>
|
||||
@@ -375,29 +374,25 @@
|
||||
<span>
|
||||
<span class="left-span">Direct</span>
|
||||
<span class="right-span">TCP/IP</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-33 p-bottom-20">
|
||||
<h3>Device / Server</h3>
|
||||
|
||||
<div id="tuner-usb">
|
||||
<p class="text-gray">Choose your desired <strong>COM port</strong><br> </p>
|
||||
<p class="text-gray">Choose your desired <strong>serial port</strong><br> </p>
|
||||
<%- include('_components', {
|
||||
component: 'dropdown',
|
||||
id: 'deviceList',
|
||||
inputId: 'xdrd-comPort',
|
||||
label: 'USB Device',
|
||||
label: 'Serial port',
|
||||
cssClass: '',
|
||||
placeholder: 'Choose your USB device',
|
||||
placeholder: 'Choose your serial port',
|
||||
options: serialPorts.map(serialPort => ({
|
||||
value: serialPort.path,
|
||||
label: `${serialPort.path} - ${serialPort.friendlyName}`
|
||||
}))
|
||||
}) %>
|
||||
}) %>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="tuner-wireless">
|
||||
<p class="text-gray">If you are connecting your tuner <strong>wirelessly</strong>, enter the tuner IP. <br> If you use <strong>xdrd</strong>, use 127.0.0.1 as your IP.</p>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', label: 'xdrd IP address', id: 'xdrd-xdrdIp'}) %>
|
||||
@@ -414,7 +409,7 @@
|
||||
<input type="range" id="audio-startupVolume" min="0" max="1" step="0.01" value="1" aria-label="Startup Volume slider">
|
||||
</div>
|
||||
<h4 class="top-10 text-gray" id="volume-percentage-value"></h4>
|
||||
|
||||
|
||||
<hr>
|
||||
<h4 class="bottom-20">Default frequency</h4>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Default frequency for first client', id: 'enableDefaultFreq'}) %><br>
|
||||
@@ -433,6 +428,13 @@
|
||||
<p>Toggling this option will put the tuner to sleep when no clients are connected.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Auto-shutdown', id: 'autoShutdown'}) %><br>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 no-bg">
|
||||
<h4>SI47XX AGC control</h4>
|
||||
<p>Allow users to change SI47XX AGC mode from the main UI.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Enable AGC control', id: 'si47xx-agcControl'}) %><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -466,7 +468,7 @@
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Basic info</h3>
|
||||
|
||||
|
||||
<p>Set your tuner name and description here.<br>This info will be visible to anyone who tunes in. </p>
|
||||
<div class="panel-full no-bg" style="padding-left: 20px; padding-right: 20px;">
|
||||
<label for="identification-tunerName" style="width: 100%;max-width: 768px; margin:auto;">Webserver name:</label>
|
||||
@@ -484,7 +486,7 @@
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Broadcast to map', id: 'identification-broadcastTuner'}) %><br>
|
||||
<%- include('_components', {component: 'text', cssClass: 'br-15', placeholder: 'Your e-mail or Discord...', label: 'Owner contact', id: 'identification-contact'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'br-15', label: 'Proxy address', id: 'identification-proxyIp'}) %>
|
||||
|
||||
|
||||
<p>Check your tuner at <strong><a href="https://servers.fmdx.org" target="_blank" class="color-4">servers.fmdx.org</a></strong>.</p>
|
||||
<p class="text-small text-gray">By activating the <strong>Broadcast to map</strong> option,<br>you agree to the <a href="https://fmdx.org/projects/webserver.php#rules" target="_blank">Terms of Service</a>.</p>
|
||||
</div>
|
||||
@@ -496,7 +498,7 @@
|
||||
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Latitude', id: 'identification-lat'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Longitude', id: 'identification-lon'}) %>
|
||||
|
||||
|
||||
<div id="map"></div>
|
||||
<br>
|
||||
</div>
|
||||
@@ -506,7 +508,7 @@
|
||||
<h2>User management</h2>
|
||||
<div class="panel-100">
|
||||
<h3>Chat options</h3>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Chat', id: 'webserver-chatEnabled'}) %>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Chat', id: 'webserver-chatEnabled'}) %>
|
||||
</div>
|
||||
|
||||
<div class="panel-100 p-bottom-20">
|
||||
@@ -571,33 +573,33 @@
|
||||
<p>These settings will be applied after a server launch or restart.</p>
|
||||
<div class="flex-container flex-center p-20">
|
||||
<% if (device === 'tef') { %>
|
||||
<%- include('_components', { component: 'dropdown', id: 'ceqStartup-dropdown', inputId: 'ceqStartup', label: 'cEQ', cssClass: '', placeholder: 'Disabled',
|
||||
<%- include('_components', { component: 'dropdown', id: 'ceqStartup-dropdown', inputId: 'ceqStartup', label: 'cEQ', cssClass: '', placeholder: 'Disabled',
|
||||
options: [
|
||||
{ value: '0', label: 'Disabled' },
|
||||
{ value: '1', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<%- include('_components', { component: 'dropdown', id: 'imsStartup-dropdown', inputId: 'imsStartup', label: 'iMS', cssClass: '', placeholder: 'Disabled',
|
||||
<%- include('_components', { component: 'dropdown', id: 'imsStartup-dropdown', inputId: 'imsStartup', label: 'iMS', cssClass: '', placeholder: 'Disabled',
|
||||
options: [
|
||||
{ value: '0', label: 'Disabled' },
|
||||
{ value: '1', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<% } else if (device === 'xdr') { %>
|
||||
<%- include('_components', { component: 'dropdown', id: 'rfStartup-dropdown', inputId: 'ceqStartup', label: 'RF+', cssClass: '', placeholder: 'Disabled',
|
||||
<%- include('_components', { component: 'dropdown', id: 'rfStartup-dropdown', inputId: 'ceqStartup', label: 'RF+', cssClass: '', placeholder: 'Disabled',
|
||||
options: [
|
||||
{ value: '0', label: 'Disabled' },
|
||||
{ value: '1', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<%- include('_components', { component: 'dropdown', id: 'ifStartup-dropdown', inputId: 'imsStartup', label: 'IF+', cssClass: '', placeholder: 'Disabled',
|
||||
<%- include('_components', { component: 'dropdown', id: 'ifStartup-dropdown', inputId: 'imsStartup', label: 'IF+', cssClass: '', placeholder: 'Disabled',
|
||||
options: [
|
||||
{ value: '0', label: 'Disabled' },
|
||||
{ value: '1', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<% } %>
|
||||
<%- include('_components', { component: 'dropdown', id: 'stereoStartup-dropdown', inputId: 'stereoStartup', label: 'Stereo Mode', cssClass: '', placeholder: 'Stereo (Default)',
|
||||
<%- include('_components', { component: 'dropdown', id: 'stereoStartup-dropdown', inputId: 'stereoStartup', label: 'Stereo Mode', cssClass: '', placeholder: 'Stereo (Default)',
|
||||
options: [
|
||||
{ value: '0', label: 'Stereo (Default)' },
|
||||
{ value: '1', label: 'Mono' },
|
||||
@@ -605,7 +607,7 @@
|
||||
}) %><br>
|
||||
</div>
|
||||
<div class="panel-100-real p-bottom-20 no-bg">
|
||||
<%- include('_components', { component: 'dropdown', id: 'antennaStartup-dropdown', inputId: 'antennaStartup', label: 'Antenna', cssClass: '', placeholder: 'Antenna 0 (Default)',
|
||||
<%- include('_components', { component: 'dropdown', id: 'antennaStartup-dropdown', inputId: 'antennaStartup', label: 'Antenna', cssClass: '', placeholder: 'Antenna 0 (Default)',
|
||||
options: [
|
||||
{ value: '0', label: 'Antenna 0 (Default)' },
|
||||
{ value: '1', label: 'Antenna 1' },
|
||||
@@ -621,21 +623,21 @@
|
||||
<h3>Empty server defaults</h3>
|
||||
<p>These settings will apply once the last user disconnects from the server, so the server can be ready for a new user with default settings.</p>
|
||||
<div class="flex-container flex-center p-20">
|
||||
<%- include('_components', { component: 'dropdown', id: 'bwAutoNoUsers-dropdown', inputId: 'bwAutoNoUsers', label: 'Auto BW', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'bwAutoNoUsers-dropdown', inputId: 'bwAutoNoUsers', label: 'Auto BW', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<% if (device === 'tef') { %>
|
||||
<%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Disabled' },
|
||||
{ value: '2', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<%- include('_components', { component: 'dropdown', id: 'imsNoUsers-dropdown', inputId: 'imsNoUsers', label: 'iMS', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'imsNoUsers-dropdown', inputId: 'imsNoUsers', label: 'iMS', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Disabled' },
|
||||
@@ -643,14 +645,14 @@
|
||||
]
|
||||
}) %><br>
|
||||
<% } else if (device === 'xdr') { %>
|
||||
<%- include('_components', { component: 'dropdown', id: 'rfNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'RF+', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'rfNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'RF+', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Disabled' },
|
||||
{ value: '2', label: 'Enabled' },
|
||||
]
|
||||
}) %><br>
|
||||
<%- include('_components', { component: 'dropdown', id: 'ifNoUsers-dropdown', inputId: 'imsNoUsers', label: 'IF+', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'ifNoUsers-dropdown', inputId: 'imsNoUsers', label: 'IF+', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Disabled' },
|
||||
@@ -658,7 +660,7 @@
|
||||
]
|
||||
}) %><br>
|
||||
<% } %>
|
||||
<%- include('_components', { component: 'dropdown', id: 'stereoNoUsers-dropdown', inputId: 'stereoNoUsers', label: 'Stereo Mode', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'stereoNoUsers-dropdown', inputId: 'stereoNoUsers', label: 'Stereo Mode', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Stereo' },
|
||||
@@ -668,7 +670,7 @@
|
||||
</div>
|
||||
<div class="panel-100-real p-bottom-20 no-bg">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Delayed Antenna Change', id: 'antennaNoUsersDelay'}) %><br>
|
||||
<%- include('_components', { component: 'dropdown', id: 'antennaNoUsers-dropdown', inputId: 'antennaNoUsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged',
|
||||
<%- include('_components', { component: 'dropdown', id: 'antennaNoUsers-dropdown', inputId: 'antennaNoUsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged',
|
||||
options: [
|
||||
{ value: '0', label: 'Unchanged' },
|
||||
{ value: '1', label: 'Antenna 0' },
|
||||
@@ -699,14 +701,16 @@
|
||||
<div class="panel-100 p-bottom-20">
|
||||
<h3>Tunnel</h3>
|
||||
<p>When you become an <a href="https://buymeacoffee.com/fmdx" target="_blank"><strong>FMDX.org supporter</strong></a>, you can host your webserver without the need of a public IP address & port forwarding.<br>
|
||||
When you become a supporter, you can message the Founders on Discord for your login details.</p>
|
||||
When you become a supporter, you can message the Founders on Discord for your login details.</p><br>
|
||||
<p>You can also get an tunnel from kuba201 discord, one of the contributors of this version of the application.</p>
|
||||
<h4>Main tunnel settings</h4>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %><br>
|
||||
<%- include('_components', { component: 'dropdown', id: 'tunnel-server', inputId: 'tunnel-serverSelect', label: 'Official server region', cssClass: '', placeholder: 'Europe',
|
||||
<%- include('_components', { component: 'dropdown', id: 'tunnel-regionSelect', inputId: 'tunnel-region', label: 'Official server region', cssClass: '', placeholder: 'Europe',
|
||||
options: [
|
||||
{ value: 'eu', label: 'Europe' },
|
||||
{ value: 'us', label: 'Americas' },
|
||||
{ value: 'sg', label: 'Asia & Oceania' },
|
||||
{ value: 'pldx', label: 'Poland (k201)' },
|
||||
]
|
||||
}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Username', id: 'tunnel-username'}) %>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
@@ -47,15 +47,13 @@
|
||||
|
||||
<h3 class="settings-heading">Tuner type</h3>
|
||||
<p class="m-0">Settings a proper device type ensures that the correct interface and settings will load.</p>
|
||||
<div class="panel-100 no-bg flex-center">
|
||||
<%- include('_components', { component: 'dropdown', id: 'device-selector', inputId: 'device', label: 'Device', cssClass: '', placeholder: 'TEF668x / TEA685x',
|
||||
options: [
|
||||
{ value: 'tef', label: 'TEF668x / TEA685x' },
|
||||
{ value: 'xdr', label: 'XDR (F1HD / S10HDiP)' },
|
||||
{ value: 'sdr', label: 'SDR (RTL-SDR / AirSpy)' },
|
||||
{ value: 'other', label: 'Other' }
|
||||
]
|
||||
}) %><br>
|
||||
<div class="panel-100 no-bg flex-center" style="max-width: 520px; margin: 10px auto 0;">
|
||||
<%- include('_components', { component: 'dropdown', id: 'device-selector', inputId: 'device', label: 'Device', cssClass: '', placeholder: 'TEF668x / TEA685x',
|
||||
options: tunerProfiles.map(profile => ({
|
||||
value: profile.id,
|
||||
label: profile.label
|
||||
}))
|
||||
}) %><br>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<h3 class="settings-heading">Tuner connection</h3>
|
||||
@@ -66,25 +64,25 @@
|
||||
<span>
|
||||
<span class="left-span">Direct</span>
|
||||
<span class="right-span">TCP/IP</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="tuner-usb" class="top-25">
|
||||
<p>It's time to choose your USB device.</p>
|
||||
|
||||
<p>It's time to choose your serial port.</p>
|
||||
|
||||
<div class="panel-100 no-bg flex-center">
|
||||
<%- include('_components', {
|
||||
component: 'dropdown',
|
||||
id: 'deviceList',
|
||||
inputId: 'xdrd-comPort',
|
||||
label: 'USB Device',
|
||||
label: 'Serial port',
|
||||
cssClass: '',
|
||||
placeholder: 'Choose your USB device',
|
||||
placeholder: 'Choose your serial port',
|
||||
options: serialPorts.map(serialPort => ({
|
||||
value: serialPort.path,
|
||||
label: `${serialPort.path} - ${serialPort.friendlyName}`
|
||||
}))
|
||||
}) %>
|
||||
}) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
@@ -105,7 +103,7 @@
|
||||
<p class="m-0">In this section, we will set up the audio.<br>
|
||||
Choose the audio port your tuner is connected to and desired audio settings here.</p>
|
||||
<p class="text-gray">Recommended defaults have already been set for the audio quality, you can keep them as-is.</p>
|
||||
|
||||
|
||||
<div class="panel-100 no-bg p-bottom-20 flex-container flex-center">
|
||||
<%- include('_components', {
|
||||
component: 'dropdown',
|
||||
@@ -124,16 +122,16 @@
|
||||
label: `${device.name}`
|
||||
}))
|
||||
]
|
||||
}) %>
|
||||
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-channels-dropdown', inputId: 'audio-audioChannels', label: 'Audio channels', cssClass: '', placeholder: 'Stereo',
|
||||
}) %>
|
||||
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-channels-dropdown', inputId: 'audio-audioChannels', label: 'Audio channels', cssClass: '', placeholder: 'Stereo',
|
||||
options: [
|
||||
{ value: '2', label: 'Stereo' },
|
||||
{ value: '1', label: 'Mono' }
|
||||
]
|
||||
}) %>
|
||||
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-quality-dropdown', inputId: 'audio-audioBitrate', label: 'Audio quality', cssClass: '', placeholder: '128kbps (standard)',
|
||||
|
||||
<%- include('_components', { component: 'dropdown', id: 'audio-quality-dropdown', inputId: 'audio-audioBitrate', label: 'Audio quality', cssClass: '', placeholder: '128kbps (standard)',
|
||||
options: [
|
||||
{ value: '64k', label: '64kbps (lowest quality)' },
|
||||
{ value: '96k', label: '96kbps (low quality)' },
|
||||
@@ -170,7 +168,7 @@
|
||||
<br>
|
||||
<label for="identification-tunerDesc" style="width: 100%;max-width: 768px; margin: auto;">Webserver description:</label>
|
||||
<textarea id="identification-tunerDesc" name="webserver-desc" class="br-15" placeholder="Fill the server description here. You can put useful info here such as your antenna setup. You can use simple markdown." maxlength="255"></textarea>
|
||||
|
||||
|
||||
<h3 class="settings-heading">Location</h3>
|
||||
<p>Location info is useful for automatic identification of stations using RDS.</p>
|
||||
<div class="panel-100 no-bg flex-container flex-center">
|
||||
|
||||
Reference in New Issue
Block a user