1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-27 14:33:52 +01:00

Compare commits

..

13 Commits

Author SHA1 Message Date
e14b3f8d11 some changes with the rds 2026-02-25 10:44:37 +01:00
42a20330af credit? 2026-02-24 22:28:04 +01:00
c0d1fee257 "too dig of a bick" 2026-02-24 15:27:39 +01:00
5d524eba56 some changes 2026-02-24 15:03:56 +01:00
648ef00bed sync to upstream 2026-02-24 14:44:48 +01:00
8a53bf1027 oh brother 2026-02-24 14:17:30 +01:00
722277c41f whoops 2026-02-24 14:16:50 +01:00
ee25214160 some changes again 2026-02-24 14:15:52 +01:00
1d04719580 some changes 2026-02-24 12:20:09 +01:00
1f70b58295 whoops 2026-02-24 10:01:10 +01:00
3080468415 oh 2026-02-24 09:54:20 +01:00
0ae484529d we're up to date with the terrible 2026-02-24 09:52:39 +01:00
Adam Wisher
098b6ba4e9 Fix PS exact match and update TX DB every 7 days 2026-02-24 09:18:06 +01:00
42 changed files with 1371 additions and 1251 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
node_modules/
/*.json
/serverlog.txt
/serverlog*.txt
/web/js/plugins/
/libraries/
/plugins/*

View File

@@ -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

597
package-lock.json generated
View File

@@ -1,24 +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": {
@@ -48,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",
@@ -147,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"
},
@@ -177,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"
@@ -191,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"
@@ -207,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",
@@ -230,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"
@@ -371,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",
@@ -434,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"
@@ -502,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": {
@@ -558,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",
@@ -582,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",
@@ -708,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": {
@@ -785,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",
@@ -827,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": {
@@ -883,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",
@@ -897,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": {
@@ -1039,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",
@@ -1077,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": {
@@ -1133,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": {
@@ -1164,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"
@@ -1180,6 +1168,13 @@
"node": ">=10"
}
},
"node_modules/koffi": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.7.2.tgz",
"integrity": "sha512-AWcsEKETQuELxK0Wq/aXDkDiNFFY41TxZQSrKm2Nd6HO/KTHeohPOOIlh2OfQnBXJbRjx5etpWt8cbqMUZo2sg==",
"hasInstallScript": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -1243,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": {
@@ -1309,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",
@@ -1332,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",
@@ -1410,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",
@@ -1431,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"
@@ -1463,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": {
@@ -1614,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"
@@ -1657,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",
@@ -1754,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"
}
@@ -1769,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",
@@ -1892,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"

View File

@@ -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,15 +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"
}
}

View File

@@ -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)) {
@@ -27,19 +32,18 @@ function createChatServer(storage) {
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,
@@ -47,9 +51,12 @@ function createChatServer(storage) {
lastWarn,
userCommandHistory,
'5',
'chat'
'chat',
512
);
if(!message) return;
let messageData;
try {
@@ -59,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 };

View File

@@ -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,10 +32,10 @@ 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);
@@ -42,7 +44,7 @@ const logMessage = (type, messages, verbose = false) => {
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);

View File

@@ -2,13 +2,13 @@
const RDSDecoder = require("./rds.js");
const { serverConfig } = require('./server_config');
const { fetchTx } = require('./tx_search.js');
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,
@@ -28,6 +28,7 @@ var dataToSend = {
rt_flag: '',
ims: 0,
eq: 0,
agc: 0,
ant: 0,
txInfo: {
tx: '',
@@ -70,9 +71,7 @@ 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() {
@@ -128,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) {
@@ -182,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");
});
rdsdec.decodeGroup(parseInt(modifiedData.slice(0, 4), 16), parseInt(modifiedData.slice(4, 8), 16), parseInt(modifiedData.slice(8, 12), 16), parseInt(modifiedData.slice(12, 16), 16));
rdsdec.decodeGroup(parseInt(a, 16), parseInt(b, 16), parseInt(c, 16), parseInt(d, 16), errors);
legacyRdsPiBuffer = null;
break;
}

View File

@@ -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;
}

View File

@@ -65,7 +65,7 @@ function sendUpdate() {
tuner: serverConfig.device || '',
bwLimit: bwLimit,
os: currentOs,
version: pjson.version
version: pjson.version
};
if (serverConfig.identification.token) request.token = serverConfig.identification.token;

View File

@@ -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 = "";
@@ -152,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,
@@ -173,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`;
}
@@ -186,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) {
@@ -197,7 +195,7 @@ function resolveDataBuffer(data, wss, rdsWss) {
receivedData = receivedData.slice(0, position + 1);
}
} else incompleteDataBuffer = '';
if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss);
}
@@ -207,7 +205,7 @@ 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();
@@ -252,25 +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}`);
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);
@@ -280,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);
@@ -313,15 +315,45 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
}
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return unsafe.replace(/&/g, "&amp;")
.replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#039;");
};
// 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
}

View File

@@ -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,22 +45,12 @@ const terminalWidth = readline.createInterface({
output: process.stdout
}).output.columns;
// Couldn't get figlet.js or something like that?
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');
@@ -103,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();
@@ -155,8 +113,9 @@ function connectToSerial() {
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);
@@ -175,7 +134,7 @@ function connectToSerial() {
} 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');
@@ -202,6 +161,7 @@ function connectToSerial() {
// Handle port closure
serialport.on('close', () => {
pluginsApi.setOutput(null);
logWarn('Disconnected from ' + serverConfig.xdrd.comPort + '. Attempting to reconnect.');
setTimeout(() => {
dataHandler.state.isSerialportRetrying = true;
@@ -220,6 +180,7 @@ function connectToXdrd() {
if (xdrd.wirelessConnection && configExists()) {
client.connect(xdrd.xdrdPort, xdrd.xdrdIp, () => {
logInfo('Connection to xdrd established successfully.');
pluginsApi.setOutput(client);
authFlags = {
authMsg: false,
@@ -236,9 +197,7 @@ 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();
@@ -277,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;
@@ -287,6 +246,7 @@ client.on('data', (data) => {
});
client.on('close', () => {
pluginsApi.setOutput(null);
if(serverConfig.autoShutdown === false) {
logWarn('Disconnected from xdrd. Attempting to reconnect.');
setTimeout(function () {
@@ -319,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();
@@ -344,10 +301,10 @@ 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;
@@ -358,17 +315,10 @@ wss.on('connection', (ws, request) => {
// 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');
@@ -384,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++;
}
@@ -395,19 +347,16 @@ 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) ||
@@ -469,7 +418,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--;
}
dataHandler.showOnlineUsers(currentUsers);
@@ -507,10 +458,10 @@ wss.on('connection', (ws, request) => {
}
if (tunerLockTracker.has(ws)) {
logInfo(`User who locked the tuner left. Unlocking the tuner.`);
output.write('wT0\n')
tunerLockTracker.delete(ws);
serverConfig.publicTuner = true;
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 &&
@@ -535,7 +486,7 @@ wss.on('connection', (ws, request) => {
// Additional web socket for using plugins
pluginsWss.on('connection', (ws, request) => {
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
const userCommandHistory = {};
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
@@ -572,52 +523,24 @@ 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);
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;
}
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') {
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, {}, () => {
pluginsWss.handleUpgrade(request, socket, head, (ws) => {
pluginsWss.emit('connection', ws, request);
upgradeWss.handleUpgrade(request, socket, head, (ws) => {
upgradeWss.emit('connection', ws, request);
});
});
} else socket.destroy();
@@ -648,3 +571,6 @@ helpers.checkIPv6Support((isIPv6Supported) => {
startServer(ipv6Address, true); // Start on IPv6
} else startServer(ipv4Address, false); // Start only on IPv4
});
pluginsApi.registerServerContext({ wss, pluginsWss, httpServer, serverConfig });
module.exports = { wss, pluginsWss, httpServer, serverConfig };

View File

@@ -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
View 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
};

View File

@@ -36,9 +36,9 @@ class RDSDecoder {
}
decodeGroup(blockA, blockB, blockC, blockD, error) {
const a_error = (error & 0xC0) >> 6;
const b_error = (error & 0x30) >> 4;
const c_error = (error & 0xc) >> 2;
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) {
@@ -46,12 +46,12 @@ class RDSDecoder {
this.last_pi_error = a_error;
}
if(b_error != 0) return; // B chooses what group this is, if this has errors, we are screwed
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) & 0b11111;
this.data.pty = (blockB >> 5) & 31;
if (group === 0) {
this.data.ta = (blockB >> 4) & 1;
@@ -87,18 +87,19 @@ class RDSDecoder {
}
}
if(d_error === 3) return; // Don't risk it
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] = error;
this.ps_errors[idx * 2 + 1] = error;
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:
@@ -120,18 +121,18 @@ class RDSDecoder {
this.rt1_to_clear = false;
}
if(c_error !== 3 && multiplier !== 2) {
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] = error;
this.rt1_errors[idx * multiplier + 1] = error;
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 !== 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] = error;
this.rt1_errors[idx * multiplier + offset + 1] = error;
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")
@@ -154,15 +155,15 @@ class RDSDecoder {
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] = error;
this.rt0_errors[idx * multiplier + 1] = error;
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] = error;
this.rt0_errors[idx * multiplier + offset + 1] = error;
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");

View File

@@ -598,22 +598,22 @@ const rdsEccF0F4Lut = [
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) {
@@ -621,7 +621,7 @@ function rdsEccLookup(pi, ecc) {
return range.lut[eccId][piId];
}
}
return ""
}

View File

@@ -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,6 +90,9 @@ let serverConfig = {
fmlistAdminOnly: false,
fmlistOmid: "",
},
si47xx: {
agcControl: false
},
tunnel: {
enabled: false,
username: "",
@@ -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
});
}

View File

@@ -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;

View File

@@ -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}`);

View File

@@ -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
View 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;

View File

@@ -15,12 +15,10 @@ 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 {
@@ -33,9 +31,7 @@ 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,
@@ -73,7 +69,6 @@ async function connect() {
child.on('close', (code) => {
logInfo(`Tunnel process exited with code ${code}`);
});
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}
%>

View File

@@ -293,8 +293,8 @@ pre {
top: 10px;
}
.flex-container.contains-dropdown {
z-index: 999;
.contains-dropdown {
z-index: 990;
position: relative;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

24
web/favicon.svg Normal file
View 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

View File

@@ -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' },

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 : '&nbsp;');
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();
}
}

View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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));
@@ -259,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 => {
@@ -271,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);
}
});
},

View File

@@ -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()}]`;

View File

@@ -27,8 +27,6 @@ function navigateStep(isNext) {
currentStep.hide();
targetStep.show();
updateProgressBar(targetStep);
} else if (isNext) {
submitConfig();
}
} else if (isNext) submitConfig();
updateWizardContent();
}

View File

@@ -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>

View File

@@ -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>&nbsp;</p>
<p class="text-gray">Choose your desired <strong>serial port</strong><br>&nbsp;</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' },
@@ -703,7 +705,7 @@
<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' },

View File

@@ -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">