mirror of
https://github.com/streamwall/streamwall.git
synced 2025-12-06 01:45:37 -05:00
Persist local stream data
This commit is contained in:
@@ -1297,7 +1297,7 @@ function CustomStreamInput({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LazyChangeInput
|
<LazyChangeInput
|
||||||
value={props.label}
|
value={props.label ?? ''}
|
||||||
onChange={handleChangeLabel}
|
onChange={handleChangeLabel}
|
||||||
placeholder="Label (optional)"
|
placeholder="Label (optional)"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
|||||||
@@ -23,16 +23,21 @@ export interface ContentViewInfo {
|
|||||||
|
|
||||||
export type ContentKind = 'video' | 'audio' | 'web' | 'background' | 'overlay'
|
export type ContentKind = 'video' | 'audio' | 'web' | 'background' | 'overlay'
|
||||||
|
|
||||||
export interface StreamData extends ContentDisplayOptions {
|
export interface StreamDataContent extends ContentDisplayOptions {
|
||||||
kind: ContentKind
|
kind: ContentKind
|
||||||
link: string
|
link: string
|
||||||
label: string
|
label?: string
|
||||||
labelPosition?: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left'
|
labelPosition?: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left'
|
||||||
source?: string
|
source?: string
|
||||||
notes?: string
|
notes?: string
|
||||||
status?: string
|
status?: string
|
||||||
city?: string
|
city?: string
|
||||||
state?: string
|
state?: string
|
||||||
|
_id?: string
|
||||||
|
_dataSource?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamData extends StreamDataContent {
|
||||||
_id: string
|
_id: string
|
||||||
_dataSource: string
|
_dataSource: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,24 @@ import { promises as fsPromises } from 'fs'
|
|||||||
import { isArray } from 'lodash-es'
|
import { isArray } from 'lodash-es'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { StreamData, StreamList } from '../../../streamwall-shared/src/types'
|
import {
|
||||||
|
StreamData,
|
||||||
|
StreamDataContent,
|
||||||
|
StreamList,
|
||||||
|
} from '../../../streamwall-shared/src/types'
|
||||||
|
|
||||||
const sleep = promisify(setTimeout)
|
const sleep = promisify(setTimeout)
|
||||||
|
|
||||||
type DataSource = AsyncGenerator<StreamData[]>
|
type DataSource = AsyncGenerator<StreamDataContent[]>
|
||||||
|
|
||||||
export async function* pollDataURL(url: string, intervalSecs: number) {
|
export async function* pollDataURL(url: string, intervalSecs: number) {
|
||||||
const refreshInterval = intervalSecs * 1000
|
const refreshInterval = intervalSecs * 1000
|
||||||
let lastData = []
|
let lastData = []
|
||||||
while (true) {
|
while (true) {
|
||||||
let data: StreamData[] = []
|
let data: StreamDataContent[] = []
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url)
|
const resp = await fetch(url)
|
||||||
data = (await resp.json()) as StreamData[]
|
data = (await resp.json()) as StreamDataContent[]
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('error loading stream data', err)
|
console.warn('error loading stream data', err)
|
||||||
}
|
}
|
||||||
@@ -65,33 +69,50 @@ export async function* markDataSource(dataSource: DataSource, name: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* combineDataSources(dataSources: DataSource[]) {
|
export async function* combineDataSources(
|
||||||
|
dataSources: DataSource[],
|
||||||
|
idGen: StreamIDGenerator,
|
||||||
|
) {
|
||||||
for await (const streamLists of Repeater.latest(dataSources)) {
|
for await (const streamLists of Repeater.latest(dataSources)) {
|
||||||
const dataByURL = new Map<string, StreamData>()
|
const dataByURL = new Map<string, StreamData>()
|
||||||
for (const list of streamLists) {
|
for (const list of streamLists) {
|
||||||
for (const data of list) {
|
for (const data of list) {
|
||||||
const existing = dataByURL.get(data.link)
|
const existing = dataByURL.get(data.link)
|
||||||
dataByURL.set(data.link, { ...existing, ...data })
|
dataByURL.set(data.link, { ...existing, ...data } as StreamData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const streams: StreamList = [...dataByURL.values()]
|
|
||||||
|
const streams = idGen.process([...dataByURL.values()]) as StreamList
|
||||||
|
|
||||||
// Retain the index to speed up local lookups
|
// Retain the index to speed up local lookups
|
||||||
streams.byURL = dataByURL
|
streams.byURL = dataByURL
|
||||||
yield streams
|
yield streams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LocalStreamData extends EventEmitter {
|
interface LocalStreamDataEvents {
|
||||||
dataByURL: Map<string, Partial<StreamData>>
|
update: [StreamDataContent[]]
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
export class LocalStreamData extends EventEmitter<LocalStreamDataEvents> {
|
||||||
|
dataByURL: Map<string, StreamDataContent>
|
||||||
|
|
||||||
|
constructor(entries: StreamDataContent[] = []) {
|
||||||
super()
|
super()
|
||||||
this.dataByURL = new Map()
|
this.dataByURL = new Map()
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.link) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.dataByURL.set(entry.link, entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(url: string, data: Partial<StreamData>) {
|
update(url: string, data: Partial<StreamDataContent>) {
|
||||||
const existing = this.dataByURL.get(url)
|
const existing = this.dataByURL.get(url)
|
||||||
this.dataByURL.set(data.link ?? url, { ...existing, ...data, link: url })
|
const kind = data.kind ?? existing?.kind ?? 'video'
|
||||||
|
const updated: StreamDataContent = { ...existing, ...data, kind, link: url }
|
||||||
|
this.dataByURL.set(data.link ?? url, updated)
|
||||||
if (data.link != null && url !== data.link) {
|
if (data.link != null && url !== data.link) {
|
||||||
this.dataByURL.delete(url)
|
this.dataByURL.delete(url)
|
||||||
}
|
}
|
||||||
@@ -107,9 +128,9 @@ export class LocalStreamData extends EventEmitter {
|
|||||||
this.emit('update', [...this.dataByURL.values()])
|
this.emit('update', [...this.dataByURL.values()])
|
||||||
}
|
}
|
||||||
|
|
||||||
gen(): AsyncGenerator<StreamData[]> {
|
gen(): AsyncGenerator<StreamDataContent[]> {
|
||||||
return new Repeater(async (push, stop) => {
|
return new Repeater(async (push, stop) => {
|
||||||
await push([])
|
await push([...this.dataByURL.values()])
|
||||||
this.on('update', push)
|
this.on('update', push)
|
||||||
await stop
|
await stop
|
||||||
this.off('update', push)
|
this.off('update', push)
|
||||||
@@ -126,7 +147,7 @@ export class StreamIDGenerator {
|
|||||||
this.idSet = new Set()
|
this.idSet = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
process(streams: StreamData[]) {
|
process(streams: StreamDataContent[]) {
|
||||||
const { idMap, idSet } = this
|
const { idMap, idSet } = this
|
||||||
|
|
||||||
for (const stream of streams) {
|
for (const stream of streams) {
|
||||||
|
|||||||
@@ -250,9 +250,20 @@ async function main(argv: ReturnType<typeof parseArgs>) {
|
|||||||
callback(false)
|
callback(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const db = await loadStorage(
|
||||||
|
join(app.getPath('userData'), 'streamwall-storage.json'),
|
||||||
|
)
|
||||||
|
|
||||||
console.debug('Creating StreamWindow...')
|
console.debug('Creating StreamWindow...')
|
||||||
const idGen = new StreamIDGenerator()
|
const idGen = new StreamIDGenerator()
|
||||||
const localStreamData = new LocalStreamData()
|
|
||||||
|
const localStreamData = new LocalStreamData(db.data.localStreamData)
|
||||||
|
localStreamData.on('update', (entries) => {
|
||||||
|
db.update((data) => {
|
||||||
|
data.localStreamData = entries
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const overlayStreamData = new LocalStreamData()
|
const overlayStreamData = new LocalStreamData()
|
||||||
|
|
||||||
const streamWindowConfig = {
|
const streamWindowConfig = {
|
||||||
@@ -306,9 +317,6 @@ async function main(argv: ReturnType<typeof parseArgs>) {
|
|||||||
const stateDoc = new Y.Doc()
|
const stateDoc = new Y.Doc()
|
||||||
const viewsState = stateDoc.getMap<Y.Map<string | undefined>>('views')
|
const viewsState = stateDoc.getMap<Y.Map<string | undefined>>('views')
|
||||||
|
|
||||||
const db = await loadStorage(
|
|
||||||
join(app.getPath('userData'), 'streamwall-storage.json'),
|
|
||||||
)
|
|
||||||
if (db.data.stateDoc) {
|
if (db.data.stateDoc) {
|
||||||
console.log('Loading stateDoc from storage...')
|
console.log('Loading stateDoc from storage...')
|
||||||
try {
|
try {
|
||||||
@@ -531,11 +539,10 @@ async function main(argv: ReturnType<typeof parseArgs>) {
|
|||||||
return markDataSource(watchDataFile(path), 'toml-file')
|
return markDataSource(watchDataFile(path), 'toml-file')
|
||||||
}),
|
}),
|
||||||
markDataSource(localStreamData.gen(), 'custom'),
|
markDataSource(localStreamData.gen(), 'custom'),
|
||||||
overlayStreamData.gen(),
|
markDataSource(overlayStreamData.gen(), 'overlay'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for await (const rawStreams of combineDataSources(dataSources)) {
|
for await (const streams of combineDataSources(dataSources, idGen)) {
|
||||||
const streams = idGen.process(rawStreams)
|
|
||||||
updateState({ streams })
|
updateState({ streams })
|
||||||
updateViewsFromStateDoc()
|
updateViewsFromStateDoc()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
import type { Low } from 'lowdb'
|
import { Low, Memory } from 'lowdb'
|
||||||
import { JSONFilePreset } from 'lowdb/node'
|
import { JSONFilePreset } from 'lowdb/node'
|
||||||
|
import { StreamDataContent } from 'streamwall-shared'
|
||||||
|
|
||||||
export interface StreamwallStoredData {
|
export interface StreamwallStoredData {
|
||||||
stateDoc: string
|
stateDoc: string
|
||||||
|
localStreamData: StreamDataContent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultData: StreamwallStoredData = {
|
const defaultData: StreamwallStoredData = {
|
||||||
stateDoc: '',
|
stateDoc: '',
|
||||||
|
localStreamData: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StorageDB = Low<StreamwallStoredData>
|
export type StorageDB = Low<StreamwallStoredData>
|
||||||
|
|
||||||
export async function loadStorage(dbPath: string) {
|
export async function loadStorage(dbPath: string) {
|
||||||
const db = await JSONFilePreset<StreamwallStoredData>(dbPath, defaultData)
|
let db: StorageDB | undefined = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
db = await JSONFilePreset<StreamwallStoredData>(dbPath, defaultData)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
'Failed to load storage at',
|
||||||
|
dbPath,
|
||||||
|
' -- changes will not be persisted',
|
||||||
|
)
|
||||||
|
db = new Low<StreamwallStoredData>(new Memory(), defaultData)
|
||||||
|
}
|
||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user