Skip to content

Commit

Permalink
Fully functional configure live clients feature
Browse files Browse the repository at this point in the history
  • Loading branch information
dtcooper committed Aug 8, 2024
1 parent 796a767 commit 0e08559
Show file tree
Hide file tree
Showing 38 changed files with 908 additions and 179 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"package": "NODE_ENV=production npm-run-all clean build package:forge",
"make:forge": "electron-forge make",
"package:forge": "electron-forge package",
"format": "prettier --plugin prettier-plugin-organize-imports --plugin prettier-plugin-svelte --plugin prettier-plugin-tailwindcss --write . ../server/constants.json"
"format": "prettier --config .prettierrc --plugin prettier-plugin-organize-imports --plugin prettier-plugin-svelte --plugin prettier-plugin-tailwindcss --write . ../server/constants.json ../server/tomato/static/admin/tomato/configure_live_clients/configure_live_clients.js ../server/tomato/templates/admin/extra/configure_live_clients_iframe.html"
},
"repository": {
"type": "git",
Expand Down
97 changes: 89 additions & 8 deletions client/src/main/Player.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import SinglePlayRotators from "./player/SinglePlayRotators.svelte"
import { IS_DEV } from "../utils"
import { reloadPlaylistCallback } from "../stores/connection"
import { registerMessageHandler, messageServer, conn } from "../stores/connection"
import { config, userConfig } from "../stores/config"
import { singlePlayRotators, stop as stopSinglePlayRotator } from "../stores/single-play-rotators"
import { db } from "../stores/db"
Expand Down Expand Up @@ -140,7 +140,91 @@
}
}
$reloadPlaylistCallback = reloadPlaylist
let subscriptionConnectionId = null
let subscriptionLastItems = null
let subscriptionInterval
registerMessageHandler("reload-playlist", ({ notify, connection_id }) => {
console.warn("Received playlist reload from server")
if (notify) {
alert("An administrator forced a playlist refresh!", "info", 4000)
}
reloadPlaylist()
if (connection_id) {
messageServer("ack-action", { connection_id, msg: "Successfully reloaded playlist!" })
}
})
const unsubscribe = () => {
if (subscriptionConnectionId) {
if ($conn.connected) {
messageServer("unsubscribe")
}
console.log(`Admin ${subscriptionConnectionId} unsubscribed`)
clearInterval(subscriptionInterval)
subscriptionConnectionId = subscriptionLastItems = null
}
}
$: if (subscriptionConnectionId && !$conn.connected) {
console.log("Disconnected while admin was subscribed. Unsubscribing.")
unsubscribe()
}
const sendClientDataToSubscriber = () => {
if (subscriptionConnectionId) {
const serialized = items.map((item) => item.serializeForSubscriber())
messageServer("client-data", { connection_id: subscriptionConnectionId, items: serialized })
}
}
registerMessageHandler("subscribe", ({ connection_id }) => {
unsubscribe() // Unsubscribe any existing connections
console.log(`Admin ${connection_id} subscribed`)
subscriptionConnectionId = connection_id
sendClientDataToSubscriber()
subscriptionInterval = setInterval(sendClientDataToSubscriber, 15000) // Update 3 times per sec
})
registerMessageHandler("unsubscribe", unsubscribe)
registerMessageHandler("swap", ({ action, asset_id, rotator_id, generated_id, subindex, connection_id }) => {
const stopset = items.find((item) => item.type === "stopset" && item.generatedId === generated_id)
if (!stopset) {
console.warn("An swap action was requested on a stopset that doesn't exist!")
messageServer("ack-action", { connection_id, msg: `An action was requested on a stopset that doesn't exist!` })
return
}
let success = false
if (action === "delete") {
success = stopset.deleteAsset(subindex)
} else {
const asset = $db.assets.find((asset) => asset.id === asset_id)
const rotator = $db.rotators.get(rotator_id)
if (!asset || !rotator) {
console.warn("An swap action was requested on an asset/rotator that doesn't exist!")
messageServer("ack-action", {
connection_id,
msg: `An action was requested on a asset/rotator that doesn't exist!`
})
return
}
if (action === "swap") {
success = stopset.swapAsset(subindex, asset, rotator)
} else {
success = stopset.insertAsset(subindex, asset, rotator, action === "before")
}
}
updateUI()
messageServer("ack-action", {
connection_id,
msg: `${success ? "Successfully performed" : "FAILED to perform"} action of type: ${action}!`
})
})
const regenerateNextStopset = () => {
let nextStopset
Expand Down Expand Up @@ -212,13 +296,10 @@
const doAssetSwap = (stopset, subindex, asset, swapAsset) => {
if (stopset.destroyed) {
alert(`Stop set ${stopset.name} no longer active in the playlist. Can't perform swap!`, "warning")
} else if (stopset.startedPlaying && stopset.current >= subindex) {
alert(
`Asset in stop set ${stopset.name}'s index ${subindex + 1} has already been played. Can't perform swap!`,
"warning"
)
} else {
stopset.swapAsset(subindex, asset, swapAsset.rotator)
if (!stopset.swapAsset(subindex, asset, swapAsset.rotator)) {
alert(`Asset in stop set ${stopset.name}'s index ${subindex + 1} can no longer be swapped.`, "warning")
}
updateUI()
}
swap = null
Expand Down
14 changes: 13 additions & 1 deletion client/src/main/player/Buttons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { userConfig } from "../../stores/config"
import { blockSpacebarPlay } from "../../stores/player"
import { registerMessageHandler, messageServer } from "../../stores/connection"
import {
midiSetLED,
midiButtonPresses,
Expand Down Expand Up @@ -36,7 +37,7 @@
$: playDisabled =
!items.some((item) => item.type === "stopset") || (firstItem.type === "stopset" && firstItem.playing)
$: pauseDisabled = firstItem.type !== "stopset" || !firstItem.playing
$: isPaused = firstItem.type === "stopset" && !firstItem.playing
$: isPaused = firstItem.type === "stopset" && firstItem.startedPlaying && !firstItem.playing
$: skipCurrentEnabled = firstItem.type === "stopset" && firstItem.startedPlaying
let ledState
Expand All @@ -63,6 +64,17 @@
play()
}
})
registerMessageHandler("play", ({ connection_id }) => {
if (playDisabled) {
console.log("Got play message, but currently not eligible to play!")
messageServer("ack-action", { connection_id, msg: "Got play command, but currently not eligble to play!" })
} else {
console.log("Got play command from server")
messageServer("ack-action", { connection_id, msg: "Successfully started playing!" })
play()
}
})
</script>

<!-- TODO: confirm when this component is destroyed, that this listener is uninstalled -->
Expand Down
29 changes: 14 additions & 15 deletions client/src/stores/connection.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ipcRenderer } from "electron"
import ReconnectingWebSocket from "reconnecting-websocket"
import { persisted } from "svelte-local-storage-store"
import { noop } from "svelte/internal"
import { derived, get, writable } from "svelte/store"
import { protocol_version } from "../../../server/constants.json"
import { alert } from "./alerts"
Expand Down Expand Up @@ -86,30 +85,30 @@ export const logout = (error) => {
}
}

export const reloadPlaylistCallback = writable(noop)

// Functions defined for various message types we get from server after authentication
const handleMessages = {
data: async (data) => {
const { config, ...jsonData } = data
data: async ({ config, ...data }) => {
setServerConfig(config)
await syncAssetsDB(jsonData)
await syncAssetsDB(data)
updateConn({ didFirstSync: true })
},
"ack-log": (data) => {
const { success, id } = data
"ack-log": ({ success, id }) => {
if (success) {
console.log(`Acknowledged log ${id}`)
acknowledgeLog(id)
}
},
"reload-playlist": (data) => {
const { notify } = data
if (notify) {
alert("An administrator forced a playlist refresh!", "info", 4000)
}
get(reloadPlaylistCallback)()
notify: ({ msg, level, timeout, connection_id }) => {
alert(msg, level, timeout)
messageServer("ack-action", { connection_id, msg: "Successfully notified user!" })
}
}

export const registerMessageHandler = (name, handler) => {
if (Object.hasOwn(handleMessages, name)) {
console.warn(`Message handler ${name} already registered`)
}
handleMessages[name] = handler
}

export const messageServer = (type, data) => {
Expand All @@ -123,7 +122,7 @@ export const messageServer = (type, data) => {
return false
}
} else {
console.error("Tried to send a message when websocket wasn't created")
console.error(`Tried to send a ${type} message when websocket wasn't created`)
return false
}
}
Expand Down
106 changes: 102 additions & 4 deletions client/src/stores/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ class GeneratedStopsetAssetBase {
this.isSwapped = isSwapped
}

serializeForSubscriber() {
return {
playable: this.playable,
beforeActive: this.beforeActive,
afterActive: this.afterActive,
active: this.active,
rotator: {
id: this.rotator.id,
name: this.rotator.name
}
}
}

updateCallback() {
this.generatedStopset.updateCallback()
}
Expand Down Expand Up @@ -134,6 +147,17 @@ class PlayableAsset extends GeneratedStopsetAssetBase {
this.queueForSkip = false
}

serializeForSubscriber() {
return {
name: this.name,
id: this.id,
duration: this.duration,
elapsed: this.elapsed,
remaining: this.remaining,
...super.serializeForSubscriber()
}
}

static getAudioObject() {
let audio = PlayableAsset._reusableAudioObjects.find((item) => !item.__tomato_used)

Expand Down Expand Up @@ -204,7 +228,7 @@ class PlayableAsset extends GeneratedStopsetAssetBase {
}

get remaining() {
return this.duration - this.elapsed
return Math.max(this.duration - this.elapsed, 0)
}

get percentDone() {
Expand Down Expand Up @@ -306,6 +330,21 @@ export class GeneratedStopset {
this.destroyed = false
}

serializeForSubscriber() {
return {
name: this.name,
id: this.generatedId,
type: this.type,
startedPlaying: this.startedPlaying,
current: this.current,
duration: this.duration,
elapsed: this.elapsed,
remaining: this.remaining,
playing: this.playing,
items: this.items.map((item) => item.serializeForSubscriber())
}
}

get duration() {
return this.playableItems.reduce((s, item) => s + item.duration, 0)
}
Expand Down Expand Up @@ -359,12 +398,57 @@ export class GeneratedStopset {
}

swapAsset(index, asset, rotator) {
if (!this._validateIndexForAssetAction(index, "swap")) {
return false
}
const oldItem = this.items[index]
const newItem = new PlayableAsset(asset, rotator, this, index, false, true)
oldItem.unloadAudio() // Don't forget to unload its audio before we nuke it
newItem.loadAudio() // If stopset was already loaded, load audio for swapped in item
this.items[index] = newItem
if (this.loaded) {
newItem.loadAudio() // If stopset was already loaded, load audio for swapped in item
}
this.items[index] = newItem // Swap it
this.updateCallback()
return true
}

insertAsset(index, asset, rotator, before) {
if (!this._validateIndexForAssetAction(index, `insert ${before ? "before" : "after"}`)) {
return false
}
const newItem = new PlayableAsset(asset, rotator, this, index, false, true)
if (this.loaded) {
newItem.loadAudio()
}
const insertIndex = index + (before ? 0 : 1)
this.items.splice(insertIndex, 0, newItem) // Splice it in
this.items.forEach((item, i) => (item.index = i)) // Fix item indexes
return true
}

deleteAsset(index) {
if (!this._validateIndexForAssetAction(index, "delete")) {
return false
}
const oldItem = this.items[index]
oldItem.unloadAudio() // Don't forget to unload its audio before we nuke it
this.items.splice(index, 1) // Nuke it
this.items.forEach((item, i) => (item.index = i)) // Fix item indexes
return true
}

_validateIndexForAssetAction(index, description) {
if (this.startedPlaying && index <= this.current) {
console.warn(
`Stopset action ${description} on ${this.name} index ${index} cannot occur since that item is playing/played!`
)
return false
} else if (index >= this.items.length) {
console.warn(`Stopset action ${description} on ${this.name} index ${index} cannot occur since that index invalid`)
return false
} else {
return true
}
}

skip() {
Expand Down Expand Up @@ -462,8 +546,22 @@ export class Wait {
this.didLog = false
}

serializeForSubscriber() {
return {
id: this.generatedId,
type: this.type,
active: this.active,
duration: this.duration,
elapsed: this.elapsed,
overdue: this.overdue,
overtime: this.overtime,
overtimeElapsed: this.overtimeElapsed,
remaining: this.remaining
}
}

get remaining() {
return this.duration - this.elapsed
return Math.max(this.duration - this.elapsed, 0)
}

get percentDone() {
Expand Down
2 changes: 1 addition & 1 deletion client/src/stores/single-play-rotators.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ export const playFromRotator = (rotator, mediumIgnoreIds = new Set()) => {
if (asset) {
play(asset, rotator)
} else {
error(`No assets to play from ${rotator.name}!`)
error(`No assets currently airing to play from ${rotator.name}!`)
}
}
2 changes: 1 addition & 1 deletion controller/test/deps/alpinejs.js
2 changes: 1 addition & 1 deletion controller/test/deps/simple.css
Loading

0 comments on commit 0e08559

Please sign in to comment.