mirror of
https://github.com/streamwall/streamwall.git
synced 2025-12-06 01:45:37 -05:00
Add support for TOML file data source
This commit is contained in:
@@ -45,6 +45,14 @@ npm start -- --config="../streamwall.json"
|
|||||||
|
|
||||||
See `config.example.toml` for an example.
|
See `config.example.toml` for an example.
|
||||||
|
|
||||||
|
## Data sources
|
||||||
|
|
||||||
|
Streamwall can load stream data from both JSON APIs and TOML files. Data sources can be specified in a config file (see `config.example.toml` for an example) or the command line:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm start -- --json-url="https://your-site/api/streams.json" --toml-file="./streams.toml"
|
||||||
|
```
|
||||||
|
|
||||||
## Hotkeys
|
## Hotkeys
|
||||||
|
|
||||||
The following hotkeys are available with the "control" webpage focused:
|
The following hotkeys are available with the "control" webpage focused:
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ address = "http://localhost:80"
|
|||||||
password = "woke"
|
password = "woke"
|
||||||
username = "woke"
|
username = "woke"
|
||||||
|
|
||||||
|
[data]
|
||||||
|
# By default, we use the woke.net streams data source.
|
||||||
|
# You can add other URLs to fetch here:
|
||||||
|
json-url = ["https://woke.net/api/streams.json"]
|
||||||
|
|
||||||
|
# You can also specify .toml files on disk. Streamwall will reload the data whenever the file changes.
|
||||||
|
# See example.streams.toml for a sample.
|
||||||
|
#toml-file = ["./example.streams.toml"]
|
||||||
|
|
||||||
[cert]
|
[cert]
|
||||||
# SSL certificates (optional)
|
# SSL certificates (optional)
|
||||||
# If you specify an https:// URL for the "webserver" option, a certificate will be automatically generated and signed by Let's Encrypt.
|
# If you specify an https:// URL for the "webserver" option, a certificate will be automatically generated and signed by Let's Encrypt.
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -1867,6 +1867,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@repeaterjs/repeater": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-kqdrZRvTLFfvgPTeMgj8DpREYZXU+sz4/kdX2825zMNGmiaEUzM6u7A4YBoI4WzqXX4q123VftkllAw1C/4rMg=="
|
||||||
|
},
|
||||||
"@sindresorhus/is": {
|
"@sindresorhus/is": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||||
@@ -2509,7 +2514,6 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||||
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picomatch": "^2.0.4"
|
"picomatch": "^2.0.4"
|
||||||
@@ -2956,11 +2960,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"binary-extensions": {
|
"binary-extensions": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
||||||
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
|
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"bluebird": {
|
"bluebird": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
@@ -3339,8 +3341,6 @@
|
|||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
|
||||||
"integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
|
"integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"anymatch": "~3.1.1",
|
"anymatch": "~3.1.1",
|
||||||
"braces": "~3.0.2",
|
"braces": "~3.0.2",
|
||||||
@@ -3356,8 +3356,6 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.0.1"
|
||||||
}
|
}
|
||||||
@@ -3366,8 +3364,6 @@
|
|||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
}
|
}
|
||||||
@@ -3375,16 +3371,12 @@
|
|||||||
"is-number": {
|
"is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"to-regex-range": {
|
"to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
}
|
}
|
||||||
@@ -5331,7 +5323,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
|
||||||
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
|
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
@@ -5412,7 +5403,6 @@
|
|||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||||
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
}
|
}
|
||||||
@@ -5912,8 +5902,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
}
|
}
|
||||||
@@ -5998,8 +5986,7 @@
|
|||||||
"is-extglob": {
|
"is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-fullwidth-code-point": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -6021,7 +6008,6 @@
|
|||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
}
|
}
|
||||||
@@ -8972,8 +8958,7 @@
|
|||||||
"normalize-path": {
|
"normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"normalize-url": {
|
"normalize-url": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -9418,8 +9403,7 @@
|
|||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||||
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
|
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"pify": {
|
"pify": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -9934,8 +9918,6 @@
|
|||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
|
||||||
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
|
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"@repeaterjs/repeater": "^3.0.1",
|
||||||
|
"chokidar": "^3.4.0",
|
||||||
"ejs": "^3.1.3",
|
"ejs": "^3.1.3",
|
||||||
"electron": "^9.0.4",
|
"electron": "^9.0.4",
|
||||||
"koa": "^2.12.1",
|
"koa": "^2.12.1",
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import { once } from 'events'
|
||||||
|
import { promises as fsPromises } from 'fs'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
|
import { Repeater } from '@repeaterjs/repeater'
|
||||||
|
import TOML from '@iarna/toml'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
|
import chokidar from 'chokidar'
|
||||||
|
|
||||||
const sleep = promisify(setTimeout)
|
const sleep = promisify(setTimeout)
|
||||||
|
|
||||||
@@ -7,14 +12,13 @@ function filterLive(data) {
|
|||||||
return data.filter(({ status }) => status === 'Live' || status === 'Unknown')
|
return data.filter(({ status }) => status === 'Live' || status === 'Unknown')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* pollPublicData() {
|
export async function* pollDataURL(url) {
|
||||||
const publicDataURL = 'https://woke.net/api/streams.json'
|
|
||||||
const refreshInterval = 5 * 1000
|
const refreshInterval = 5 * 1000
|
||||||
let lastData = []
|
let lastData = []
|
||||||
while (true) {
|
while (true) {
|
||||||
let data = []
|
let data = []
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(publicDataURL)
|
const resp = await fetch(url)
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('error loading stream data', err)
|
console.warn('error loading stream data', err)
|
||||||
@@ -32,17 +36,52 @@ export async function* pollPublicData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function* watchDataFile(path) {
|
||||||
|
const watcher = chokidar.watch(path)
|
||||||
|
while (true) {
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
const text = await fsPromises.readFile(path)
|
||||||
|
data = TOML.parse(text)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('error reading data file', err)
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
yield data.streams || []
|
||||||
|
}
|
||||||
|
await once(watcher, 'change')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* markDataSource(dataSource, name) {
|
||||||
|
for await (const streamList of dataSource) {
|
||||||
|
for (const s of streamList) {
|
||||||
|
s._dataSource = name
|
||||||
|
}
|
||||||
|
yield streamList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* combineDataSources(dataSources) {
|
||||||
|
for await (const streamLists of Repeater.latest(dataSources)) {
|
||||||
|
yield [].concat(...streamLists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class StreamIDGenerator {
|
export class StreamIDGenerator {
|
||||||
constructor(parent) {
|
constructor() {
|
||||||
this.idMap = new Map(parent ? parent.idMap : null)
|
this.idMap = new Map()
|
||||||
this.idSet = new Set(this.idMap.values())
|
this.idSet = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
process(streams) {
|
process(streams) {
|
||||||
const { idMap, idSet } = this
|
const { idMap, idSet } = this
|
||||||
|
const localIdMap = new Map(idMap)
|
||||||
|
const localIdSet = new Set(idSet)
|
||||||
|
|
||||||
for (const stream of streams) {
|
for (const stream of streams) {
|
||||||
const { link, source, label } = stream
|
const { link, source, label, _dataSource } = stream
|
||||||
if (!idMap.has(link)) {
|
if (!localIdMap.has(link)) {
|
||||||
let counter = 0
|
let counter = 0
|
||||||
let newId
|
let newId
|
||||||
const normalizedText = (source || label || link)
|
const normalizedText = (source || label || link)
|
||||||
@@ -54,11 +93,20 @@ export class StreamIDGenerator {
|
|||||||
const counterPart = counter === 0 && textPart ? '' : counter
|
const counterPart = counter === 0 && textPart ? '' : counter
|
||||||
newId = `${textPart}${counterPart}`
|
newId = `${textPart}${counterPart}`
|
||||||
counter++
|
counter++
|
||||||
} while (idSet.has(newId))
|
} while (localIdSet.has(newId))
|
||||||
idMap.set(link, newId)
|
|
||||||
idSet.add(newId)
|
localIdMap.set(link, newId)
|
||||||
|
localIdSet.add(newId)
|
||||||
|
|
||||||
|
// Custom stream ids are not persisted so that editing them doesn't create a bunch of unused ids.
|
||||||
|
const persistId = _dataSource !== 'custom'
|
||||||
|
if (persistId) {
|
||||||
|
idMap.set(link, newId)
|
||||||
|
idSet.add(newId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
stream._id = idMap.get(link)
|
|
||||||
|
stream._id = localIdMap.get(link)
|
||||||
}
|
}
|
||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import yargs from 'yargs'
|
import yargs from 'yargs'
|
||||||
import TOML from '@iarna/toml'
|
import TOML from '@iarna/toml'
|
||||||
|
import { Repeater } from '@repeaterjs/repeater'
|
||||||
import { app, shell, session, BrowserWindow } from 'electron'
|
import { app, shell, session, BrowserWindow } from 'electron'
|
||||||
|
|
||||||
import { ensureValidURL } from '../util'
|
import { ensureValidURL } from '../util'
|
||||||
import { pollPublicData, StreamIDGenerator } from './data'
|
import {
|
||||||
|
pollDataURL,
|
||||||
|
watchDataFile,
|
||||||
|
StreamIDGenerator,
|
||||||
|
markDataSource,
|
||||||
|
combineDataSources,
|
||||||
|
} from './data'
|
||||||
import StreamWindow from './StreamWindow'
|
import StreamWindow from './StreamWindow'
|
||||||
import StreamdelayClient from './StreamdelayClient'
|
import StreamdelayClient from './StreamdelayClient'
|
||||||
import initWebServer from './server'
|
import initWebServer from './server'
|
||||||
@@ -18,6 +25,18 @@ function parseArgs() {
|
|||||||
describe: 'Background color of wall (useful for chroma-keying)',
|
describe: 'Background color of wall (useful for chroma-keying)',
|
||||||
default: '#000',
|
default: '#000',
|
||||||
})
|
})
|
||||||
|
.group(['data.json-url', 'data.toml-file'], 'Datasources')
|
||||||
|
.option('data.json-url', {
|
||||||
|
describe: 'Fetch streams from the specified URL(s)',
|
||||||
|
array: true,
|
||||||
|
default: ['https://woke.net/api/streams.json'],
|
||||||
|
})
|
||||||
|
.option('data.toml-file', {
|
||||||
|
describe: 'Fetch streams from the specified file(s)',
|
||||||
|
normalize: true,
|
||||||
|
array: true,
|
||||||
|
default: [],
|
||||||
|
})
|
||||||
.group(
|
.group(
|
||||||
[
|
[
|
||||||
'control.username',
|
'control.username',
|
||||||
@@ -92,6 +111,11 @@ async function main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const idGen = new StreamIDGenerator()
|
const idGen = new StreamIDGenerator()
|
||||||
|
let updateCustomStreams
|
||||||
|
const customStreamData = new Repeater(async (push) => {
|
||||||
|
await push([])
|
||||||
|
updateCustomStreams = push
|
||||||
|
})
|
||||||
|
|
||||||
const streamWindow = new StreamWindow({
|
const streamWindow = new StreamWindow({
|
||||||
backgroundColor: argv.backgroundColor,
|
backgroundColor: argv.backgroundColor,
|
||||||
@@ -117,10 +141,7 @@ async function main() {
|
|||||||
} else if (msg.type === 'set-view-blurred') {
|
} else if (msg.type === 'set-view-blurred') {
|
||||||
streamWindow.setViewBlurred(msg.viewIdx, msg.blurred)
|
streamWindow.setViewBlurred(msg.viewIdx, msg.blurred)
|
||||||
} else if (msg.type === 'set-custom-streams') {
|
} else if (msg.type === 'set-custom-streams') {
|
||||||
const customIDGen = new StreamIDGenerator(idGen)
|
updateCustomStreams(msg.streams)
|
||||||
clientState.customStreams = customIDGen.process(msg.streams)
|
|
||||||
streamWindow.send('state', clientState)
|
|
||||||
broadcastState(clientState)
|
|
||||||
} else if (msg.type === 'reload-view') {
|
} else if (msg.type === 'reload-view') {
|
||||||
streamWindow.reloadView(msg.viewIdx)
|
streamWindow.reloadView(msg.viewIdx)
|
||||||
} else if (msg.type === 'browse' || msg.type === 'dev-tools') {
|
} else if (msg.type === 'browse' || msg.type === 'dev-tools') {
|
||||||
@@ -190,7 +211,17 @@ async function main() {
|
|||||||
broadcastState(clientState)
|
broadcastState(clientState)
|
||||||
})
|
})
|
||||||
|
|
||||||
for await (const rawStreams of pollPublicData()) {
|
const dataSources = [
|
||||||
|
...argv.data['json-url'].map((url) =>
|
||||||
|
markDataSource(pollDataURL(url), 'json-url'),
|
||||||
|
),
|
||||||
|
...argv.data['toml-file'].map((path) =>
|
||||||
|
markDataSource(watchDataFile(path), 'toml-file'),
|
||||||
|
),
|
||||||
|
markDataSource(customStreamData, 'custom'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for await (const rawStreams of combineDataSources(dataSources)) {
|
||||||
const streams = idGen.process(rawStreams)
|
const streams = idGen.process(rawStreams)
|
||||||
clientState.streams = streams
|
clientState.streams = streams
|
||||||
streamWindow.send('state', clientState)
|
streamWindow.send('state', clientState)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ function App({ wsEndpoint }) {
|
|||||||
const [delayState, setDelayState] = useState({
|
const [delayState, setDelayState] = useState({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
})
|
})
|
||||||
const allStreams = sortBy([...streams, ...customStreams], ['_id'])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ws = new ReconnectingWebSocket(wsEndpoint, [], {
|
const ws = new ReconnectingWebSocket(wsEndpoint, [], {
|
||||||
@@ -38,17 +37,11 @@ function App({ wsEndpoint }) {
|
|||||||
ws.addEventListener('message', (ev) => {
|
ws.addEventListener('message', (ev) => {
|
||||||
const msg = JSON.parse(ev.data)
|
const msg = JSON.parse(ev.data)
|
||||||
if (msg.type === 'state') {
|
if (msg.type === 'state') {
|
||||||
const {
|
const { streams: newStreams, views, streamdelay } = msg.state
|
||||||
streams: newStreams,
|
|
||||||
views,
|
|
||||||
customStreams: newCustomStreams,
|
|
||||||
streamdelay,
|
|
||||||
} = msg.state
|
|
||||||
const newStateIdxMap = new Map()
|
const newStateIdxMap = new Map()
|
||||||
const allStreams = [...newStreams, ...newCustomStreams]
|
|
||||||
for (const viewState of views) {
|
for (const viewState of views) {
|
||||||
const { pos, content } = viewState.context
|
const { pos, content } = viewState.context
|
||||||
const stream = allStreams.find((d) => d.link === content.url)
|
const stream = newStreams.find((d) => d.link === content.url)
|
||||||
const streamId = stream?._id
|
const streamId = stream?._id
|
||||||
const state = State.from(viewState.state)
|
const state = State.from(viewState.state)
|
||||||
const isListening = state.matches(
|
const isListening = state.matches(
|
||||||
@@ -69,8 +62,8 @@ function App({ wsEndpoint }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setStateIdxMap(newStateIdxMap)
|
setStateIdxMap(newStateIdxMap)
|
||||||
setStreams(newStreams)
|
setStreams(sortBy(newStreams, ['_id']))
|
||||||
setCustomStreams(newCustomStreams)
|
setCustomStreams(newStreams.filter((s) => s._dataSource === 'custom'))
|
||||||
setDelayState(
|
setDelayState(
|
||||||
streamdelay.isConnected && {
|
streamdelay.isConnected && {
|
||||||
...streamdelay,
|
...streamdelay,
|
||||||
@@ -87,9 +80,7 @@ function App({ wsEndpoint }) {
|
|||||||
const handleSetView = useCallback(
|
const handleSetView = useCallback(
|
||||||
(idx, streamId) => {
|
(idx, streamId) => {
|
||||||
const newSpaceIdxMap = new Map(stateIdxMap)
|
const newSpaceIdxMap = new Map(stateIdxMap)
|
||||||
const stream = [...streams, ...customStreams].find(
|
const stream = streams.find((d) => d._id === streamId)
|
||||||
(d) => d._id === streamId,
|
|
||||||
)
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
const content = {
|
const content = {
|
||||||
url: stream?.link,
|
url: stream?.link,
|
||||||
@@ -109,7 +100,7 @@ function App({ wsEndpoint }) {
|
|||||||
])
|
])
|
||||||
wsRef.current.send(JSON.stringify({ type: 'set-views', views }))
|
wsRef.current.send(JSON.stringify({ type: 'set-views', views }))
|
||||||
},
|
},
|
||||||
[streams, customStreams, stateIdxMap],
|
[streams, stateIdxMap],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSetListening = useCallback((idx, listening) => {
|
const handleSetListening = useCallback((idx, listening) => {
|
||||||
@@ -270,7 +261,7 @@ function App({ wsEndpoint }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isConnected
|
{isConnected
|
||||||
? allStreams.map((row) => (
|
? streams.map((row) => (
|
||||||
<StreamLine id={row._id} row={row} onClickId={handleClickId} />
|
<StreamLine id={row._id} row={row} onClickId={handleClickId} />
|
||||||
))
|
))
|
||||||
: 'loading...'}
|
: 'loading...'}
|
||||||
|
|||||||
Reference in New Issue
Block a user