diff --git a/.gitignore b/.gitignore index 3d1d17aa..a5235ddc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ -user/ -user_old/ -user_test/ -user_premigrate/ +user*/ /dist /release dist-electron/ diff --git a/README.md b/README.md index 3fb361a0..2656147e 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,25 @@ # CastMate -CastMate is an all-in-one Broadcaster Automation Suite for Twitch. It allows you to build compelling interactive stream setups using without writing any code. It features simple click and drag automations and overlays. +CastMate is an all-in-one Broadcaster Production Suite for Twitch. It lets a streamer create viewer interactions, automate common tasks, display overlays, and plan streams. ## Triggers -In CastMate you can setup triggers to respond to twitch events like channel point redemptions, bits being cheered, gaining a follower, receiving a sub, chat messages, being raided, hype trains, and more. In response to a trigger you can setup any number of actions using the click and drag UI. +In CastMate you can setup triggers to respond to twitch events like channel point redemptions, bits being cheered, gaining a follower, receiving a sub, chat messages, being raided, hype trains, and more. -![CastMate UI Automations](docs/images/automation.png?raw=true) +You can use actions to automatically respond to triggers. Using CastMate's click and drag timeline automation system you can easily setup sounds to play, lights to change, chat to twitch, message to discord, etc. Using triggers lets you easily create an interactive stream for your audience. + +![CastMate UI Triggers](docs/images/trigger.png?raw=true) ## Profiles -A profile is meant to organize triggers together based on context. For example, a Minecraft profile might contain a set of commands which are only available to viewers when the streamer is playing Minecraft, or a "Stream Starting" profile might only be active when a specific scene is active in OBS. Multiple profiles can be active at once. +A Profile is an organizational tool to group triggers together. They also serve as a way to change what triggers are active. Profiles can be manually turned on and off, or they can be set to automatically turn on and off based custom conditions you choose. ![CastMate UI Profiles](docs/images/profile.png?raw=true) -Profiles can be activated automatically based on conditions such as the number of viewers, the currently active OBS scene, the value of a custom variable, various states within CastMate itself, and much more. Multiple conditions can be combined together using logical expressions like AND, OR, EQUAL, GREATER THAN, etc. If a channel point reward isn't used by any active profile they are automatically disabled and dissappear from view. +When a profile is off it will automatically disable the channel point rewards no longer needed, any chat commands in it will become unavailable, and ![CastMate UI Profiles](docs/images/ChannelPointRewards.gif?raw=true) -The possibilities are endless, so here are some ideas to get you started: - -- Create alternate versions of a channel point redemption that costs more to redeem when the number of viewers is higher, to encourage engagement during slow streams and to reduce spam during active streams. -- Allow text-to-speech with channel points, but only if the stream has less than some number of viewers. -- A "Stream Ending" profile which automatically starts sending social media links to chat while your "End Stream" scene is active in OBS. -- Automatically run ads whenever your "Be Right Back" scene is active. -- Have a jump scare sound effect that automatically costs triple during specific stream segments. -- Let viewers troll you with a command that plays different sounds based on the game, like enemy footsteps during a shooter or creepers during minecraft. -- Make a song request channel point redemption that's only available during a Music stream segment. -- Set up a "First" channel point redemption item that can only be claimed once per stream. -- Set up a channel point redemption that is only visible when there are exactly 69 viewers. Nice. - ## Overlays CastMate has a WYSIWYG interface for creating overlays. Create custom alerts entirely though the UI. Sync labels with CastMate's internal state automatically. Easily edit your overlays right in the UI and have their results appear immediately once saved in OBS. diff --git a/docs/images/SpellCast.png b/docs/images/SpellCast.png index ff19a6fa..2f9a4ac1 100644 Binary files a/docs/images/SpellCast.png and b/docs/images/SpellCast.png differ diff --git a/docs/images/WYSIWYG.gif b/docs/images/WYSIWYG.gif index 6a19ac36..7a97ca9a 100644 Binary files a/docs/images/WYSIWYG.gif and b/docs/images/WYSIWYG.gif differ diff --git a/docs/images/Wheel.gif b/docs/images/Wheel.gif deleted file mode 100644 index bff45919..00000000 Binary files a/docs/images/Wheel.gif and /dev/null differ diff --git a/docs/images/activation.png b/docs/images/activation.png deleted file mode 100644 index 02089826..00000000 Binary files a/docs/images/activation.png and /dev/null differ diff --git a/docs/images/automation.png b/docs/images/automation.png deleted file mode 100644 index c66aea44..00000000 Binary files a/docs/images/automation.png and /dev/null differ diff --git a/docs/images/profile.png b/docs/images/profile.png index 4b3e4664..643b7cc9 100644 Binary files a/docs/images/profile.png and b/docs/images/profile.png differ diff --git a/docs/images/trigger.png b/docs/images/trigger.png new file mode 100644 index 00000000..71a31f68 Binary files /dev/null and b/docs/images/trigger.png differ diff --git a/libs/castmate-core/src/pubsub/pubsub-service.ts b/libs/castmate-core/src/pubsub/pubsub-service.ts index 638e5948..a6008220 100644 --- a/libs/castmate-core/src/pubsub/pubsub-service.ts +++ b/libs/castmate-core/src/pubsub/pubsub-service.ts @@ -7,7 +7,6 @@ import { EventList } from "../util/events" import { onLoad, onUnload } from "../plugins/plugin" import { initingPlugin } from "../plugins/plugin-init" import { ReactiveEffect, autoRerun } from "../reactivity/reactivity" -import { coreAxios } from "../util/request-utils" const logger = usePluginLogger("pubsub") @@ -27,6 +26,7 @@ export const PubSubManager = Service( private onMessage = new EventList<(plugin: string, event: string, context: object) => any>() private onConnect = new EventList() + private onBeforeDisconnect = new EventList() constructor() {} @@ -54,11 +54,16 @@ export const PubSubManager = Service( this.disconnect() } - private disconnect() { + private async disconnect() { if (this.azSocket) { + try { + await this.onBeforeDisconnect.run() + } catch {} + + const socket = this.azSocket logger.log("Disconnecting from Cloud PubSub") - this.azSocket?.stop() this.azSocket = undefined + socket?.stop() } this.connected = false @@ -88,7 +93,7 @@ export const PubSubManager = Service( logger.log("Starting Cloud PubSub Connection") this.connecting = true - const negotiationResp = await coreAxios.get("/pubsub/negotiate", { + const negotiationResp = await axios.get("/pubsub/negotiate", { baseURL, headers: { Authorization: `Bearer ${this.token}`, @@ -121,11 +126,11 @@ export const PubSubManager = Service( const messageData = ev.message.data as { plugin: string - message: string + event: string context: object } - this.onMessage.run(messageData.plugin, messageData.message, messageData.context) + this.onMessage.run(messageData.plugin, messageData.event, messageData.context) }) this.azSocket.on("connected", async (ev) => { @@ -135,12 +140,9 @@ export const PubSubManager = Service( }) this.azSocket.on("disconnected", (ev) => { - logger.error("Lost Connection to CastMate Pubsub", ev.message) - - //ON DISCONNECT - this.connected = false this.connecting = false + logger.error("Lost Connection to CastMate Pubsub", ev.message) }) await this.azSocket.start() @@ -160,6 +162,7 @@ export const PubSubManager = Service( } try { + //logger.log("Sending", eventName, this.connected, this.connecting) await this.azSocket.sendEvent(eventName, data, "json") } catch (err) { logger.error("Cloud PubSub Error", err) @@ -193,6 +196,14 @@ export const PubSubManager = Service( unregisterOnConnect(func: () => any) { this.onConnect.unregister(func) } + + registerOnBeforeDisconnect(func: () => any) { + this.onBeforeDisconnect.register(func) + } + + unregisterOnBeforeDisconnect(func: () => any) { + this.onBeforeDisconnect.unregister(func) + } } ) @@ -228,11 +239,13 @@ export function onCloudPubSubMessage( if (activeFunc()) { if (!registered) { registered = true + //logger.log("Registering", pluginId, eventName) PubSubManager.getInstance().registerOnMessage(handler) } } else { if (registered) { registered = false + //logger.log("Unregistering", pluginId, eventName) PubSubManager.getInstance().unregisterOnMessage(handler) } } @@ -258,9 +271,19 @@ export function onCloudPubSubConnect(func: () => any) { }) } +export function onCloudPubSubBeforeDisconnect(func: () => any) { + onLoad(() => { + PubSubManager.getInstance().registerOnBeforeDisconnect(func) + }) + + onUnload(() => { + PubSubManager.getInstance().unregisterOnBeforeDisconnect(func) + }) +} + export function useSendCloudPubSubMessage(eventName: string) { if (!initingPlugin) throw new Error() - const pluginId = initingPlugin.name + const pluginId = initingPlugin.id return async (data: T) => { return await PubSubManager.getInstance().send(pluginId, eventName, data) diff --git a/libs/castmate-core/src/reactivity/reactivity.ts b/libs/castmate-core/src/reactivity/reactivity.ts index 1d06ae57..43e57f3d 100644 --- a/libs/castmate-core/src/reactivity/reactivity.ts +++ b/libs/castmate-core/src/reactivity/reactivity.ts @@ -71,7 +71,6 @@ export namespace DependencyStorage { if (key == "constructor") return undefined const alias = aliasMap.get(target) if (!alias) return undefined - logger.log("Alias Obj", alias) return alias[key] } diff --git a/libs/castmate-core/src/util/events.ts b/libs/castmate-core/src/util/events.ts index fc6a65d8..255eaacc 100644 --- a/libs/castmate-core/src/util/events.ts +++ b/libs/castmate-core/src/util/events.ts @@ -1,3 +1,5 @@ +import { globalLogger } from "../logging/logging" + export class EventList any = () => any> { private list: TFunc[] = [] @@ -23,7 +25,11 @@ export class EventList any = () => any> { //const promises = this.list.map((f) => f(...args)) //await Promise.all(promises) for (const f of this.list) { - await f(...args) + try { + await f(...args) + } catch (err) { + globalLogger.error("ERROR w EVENT LIST:", err) + } } } } diff --git a/packages/castmate/src/main/background.ts b/packages/castmate/src/main/background.ts index 6cd52af9..0fb5cb0e 100644 --- a/packages/castmate/src/main/background.ts +++ b/packages/castmate/src/main/background.ts @@ -21,7 +21,8 @@ function quit() { } function createMainWindow() { - const win = createWindow("index.html", 1600, 900) + const portable = process.env.PORTABLE_EXECUTABLE_FILE != null || process.argv.includes("--portable") + const win = createWindow("index.html", 1600, 900, { portable: String(portable) }) win.on("close", () => { //Workaround for electron bug. diff --git a/packages/castmate/src/main/electron/electron-helpers.ts b/packages/castmate/src/main/electron/electron-helpers.ts index 125398ea..3ba27bed 100644 --- a/packages/castmate/src/main/electron/electron-helpers.ts +++ b/packages/castmate/src/main/electron/electron-helpers.ts @@ -31,9 +31,13 @@ export function createWindow( if (process.env.VITE_DEV_SERVER_URL) { //get it from vite const url = path.posix.join(process.env.VITE_DEV_SERVER_URL, "html", htmlFile) - win.loadURL(url) //TODO: FORMAT URL WITH QUERY + const params = new URLSearchParams(urlQuery) + win.loadURL(`${url}?${params}`) } else { - win.loadFile(path.join(__dirname, `../../dist/html/${htmlFile}`)) + const url = path.join(__dirname, `../../dist/html/${htmlFile}`) + win.loadFile(url, { + query: urlQuery, + }) } win.addListener("maximize", () => { diff --git a/packages/castmate/src/main/migration/old-migration.ts b/packages/castmate/src/main/migration/old-migration.ts index 146b04d8..ab964d63 100644 --- a/packages/castmate/src/main/migration/old-migration.ts +++ b/packages/castmate/src/main/migration/old-migration.ts @@ -1867,6 +1867,10 @@ async function migrateOldAutomation(oldAutomation: OldAutomation): Promise boolean>("info", "isFirstTimeStart const hasUpdate = useIpcCaller<() => boolean>("info", "hasUpdate") onMounted(async () => { + const queryString = window.location.search + const urlParams = new URLSearchParams(queryString) + + const isPortable = urlParams.get("portable") + if (isPortable == "true") { + document.title = "CastMate - Portable" + } + let migrated = false if (await needsMigrate()) { migrated = true diff --git a/plugins/spellcast/main/src/api.ts b/plugins/spellcast/main/src/api.ts index 068a7d6a..69466dc0 100644 --- a/plugins/spellcast/main/src/api.ts +++ b/plugins/spellcast/main/src/api.ts @@ -10,7 +10,7 @@ function apiGet(url: string) { const token = TwitchAccount.channel.secrets.accessToken - return coreAxios.get(url, { + return axios.get(url, { baseURL, headers: { Authorization: `Bearer ${token}`, @@ -23,7 +23,7 @@ function apiPost(url: string, data: any) { const token = TwitchAccount.channel.secrets.accessToken - return coreAxios.post(url, data, { + return axios.post(url, data, { baseURL, headers: { Authorization: `Bearer ${token}`, @@ -36,7 +36,7 @@ function apiPut(url: string, data: any) { const token = TwitchAccount.channel.secrets.accessToken - return coreAxios.put(url, data, { + return axios.put(url, data, { baseURL, headers: { Authorization: `Bearer ${token}`, @@ -49,7 +49,7 @@ function apiDel(url: string) { const token = TwitchAccount.channel.secrets.accessToken - return coreAxios.delete(url, { + return axios.delete(url, { baseURL, headers: { Authorization: `Bearer ${token}`, diff --git a/plugins/spellcast/main/src/spell.ts b/plugins/spellcast/main/src/spell.ts index 6ce467a9..0dec9106 100644 --- a/plugins/spellcast/main/src/spell.ts +++ b/plugins/spellcast/main/src/spell.ts @@ -20,6 +20,7 @@ import { writeYAML, ProfileManager, onCloudPubSubConnect, + onCloudPubSubBeforeDisconnect, } from "castmate-core" import { SpellConfig, @@ -144,6 +145,13 @@ export class SpellHook extends Resource await fs.unlink(resource.filepath) } + static getByApiId(apiId: string) { + for (const spell of this.storage) { + if (spell.config.spellId == apiId) return spell + } + return undefined + } + /////////////////////// static async initialize(): Promise { @@ -294,8 +302,13 @@ export class SpellHook extends Resource async updateServer() { const apiData = await this.getApiData() - const updated = await updateSpell(this.config.spellId, apiData) - this.updateFromApi(updated) + if (this.config.spellId) { + const updated = await updateSpell(this.config.spellId, apiData) + this.updateFromApi(updated) + } else { + const created = await createSpell(apiData) + this.updateFromApi(created) + } } } @@ -320,6 +333,7 @@ export function setupSpells() { properties: { spell: { type: SpellHook, required: true }, viewer: { type: TwitchViewer, required: true }, + bits: { type: Number, required: true }, }, }, async handle(config, context, mapping) { @@ -331,19 +345,26 @@ export function setupSpells() { buttonId: string user: string userId: string + bits: number } onCloudPubSubMessage( "spellHook", () => hasActiveSpells.value, async (data) => { - const spell = SpellHook.storage.getById(data.buttonId) + const spell = SpellHook.getByApiId(data.buttonId) - if (!spell) return false + if (!spell) { + logger.log("Failed to find SpellHook", data.buttonId) + return false + } + + logger.log("Activating SpellHook", data.buttonId, spell.config.name) spellHook({ spell, viewer: data.userId, + bits: data.bits, }) return true @@ -356,12 +377,18 @@ export function setupSpells() { for (const profile of ProfileManager.getInstance().activeProfiles) { for (const trigger of profile.iterTriggers(spellHook)) { - const spell = trigger.config.spell - if (!spell) continue + const localId = trigger.config.spell as unknown as string + if (!localId) continue //We don't have to obey the enabled flag here, the server does that for us //TODO/HACK: Iter triggers doesn't deserialize from ID to Resource - activeSpells.add(spell as unknown as string) + const spell = SpellHook.storage.getById(localId) + + if (!spell) continue + + const spellId = spell.config.spellId + + activeSpells.add(spellId) } } @@ -385,28 +412,42 @@ export function setupSpells() { await updateActiveSpells() }) + onCloudPubSubBeforeDisconnect(async () => { + await setActiveSpells({ spells: [] }) + }) + onProfilesChanged(async (activeProfiles, inactiveProfiles) => { await updateActiveSpells() }) onChannelAuth(async (channel, service) => { - logger.log("Loading Spells from server...") - const spells = await getSpells() + try { + logger.log("Loading Spells from server...") + const spells = await getSpells() - const spellResources = [...SpellHook.storage] - for (const apiSpell of spells) { - const spell = spellResources.find((s) => s.config.spellId == apiSpell._id) + const spellResources = [...SpellHook.storage] - if (!spell) { - await SpellHook.recoverLocalSpell(apiSpell) + for (const spell of spellResources) { + logger.log("Loaded Spell ", spell.id, spell.config.name, spell.config.spellId) } - } - for (const spell of SpellHook.storage) { - const apiSpell = spells.find((s) => s._id == spell.config.spellId) + for (const apiSpell of spells) { + const spell = spellResources.find((s) => s.config.spellId == apiSpell._id) - await spell.initializeFromServer(apiSpell) - await spell.initializeReactivity() + if (!spell) { + logger.log("No resource found for", apiSpell._id, apiSpell.name) + await SpellHook.recoverLocalSpell(apiSpell) + } + } + + for (const spell of SpellHook.storage) { + const apiSpell = spells.find((s) => s._id == spell.config.spellId) + + await spell.initializeFromServer(apiSpell) + await spell.initializeReactivity() + } + } catch (err) { + logger.error("ERROR Fetching SpellCast Spells", err) } }) } diff --git a/plugins/time/main/src/timers.ts b/plugins/time/main/src/timers.ts index 92313fe8..6501d128 100644 --- a/plugins/time/main/src/timers.ts +++ b/plugins/time/main/src/timers.ts @@ -79,7 +79,14 @@ registerSchemaCompare(Timer, compareTimer) interface RepeatingTimer { delay?: NodeJS.Timeout + delayTime?: Duration + delayWaited?: boolean timer?: NodeJS.Timeout + timerInterval?: Duration +} + +function hasDelayed(timer: RepeatingTimer) { + return timer.delayWaited == true } export function setupTimers() { @@ -95,30 +102,62 @@ export function setupTimers() { } } - function setRepeatingTimer( + function setTimer(timer: RepeatingTimer, profileId: string, triggerId: string, validInterval: Duration) { + if (timer.timerInterval != validInterval) { + timer.delayWaited = true + timer.timerInterval = validInterval + + if (timer.timer) { + clearInterval(timer.timer) + delete timer.timer + } + + timer.timer = setInterval(() => { + timer.delayWaited = true + repeat({ profileId, triggerId }) + }, validInterval * 1000) + } + } + + function setDelay( + timer: RepeatingTimer, profileId: string, triggerId: string, - interval: Duration, - delay?: Duration - ): RepeatingTimer { - const result: RepeatingTimer = {} + delay: Duration, + validInterval: Duration + ) { + if (timer.delayTime != delay) { + timer.delayTime = delay + + if (timer.delay) { + clearTimeout(timer.delay) + delete timer.delay + } - if (delay) { - result.delay = setTimeout(() => { - result.delay = undefined + timer.delay = setTimeout(() => { + delete timer.delay + delete timer.delayTime repeat({ profileId, triggerId }) - result.timer = setInterval(() => { - repeat({ profileId, triggerId }) - }, interval * 1000) + + setTimer(timer, profileId, triggerId, validInterval) }, delay * 1000) - } else { - repeat({ profileId, triggerId }) - result.timer = setInterval(() => { - repeat({ profileId, triggerId }) - }, interval * 1000) } + } + + function updateTimer( + timer: RepeatingTimer, + profileId: string, + triggerId: string, + interval: Duration, + delay?: Duration + ) { + const validInterval = Math.max(interval ?? 1, 0.1) - return result + if (delay && !hasDelayed(timer)) { + setDelay(timer, profileId, triggerId, delay, validInterval) + } else { + setTimer(timer, profileId, triggerId, validInterval) + } } const repeat = defineTrigger({ @@ -153,11 +192,13 @@ export function setupTimers() { for (const prof of activeProfiles) { for (const trigger of prof.iterTriggers(repeat)) { const slug = `${prof.id}.${trigger.id}` - if (repeatingTimers.has(slug)) continue - repeatingTimers.set( - slug, - setRepeatingTimer(prof.id, trigger.id, trigger.config.interval, trigger.config.delay) - ) + let timer = repeatingTimers.get(slug) + if (!timer) { + timer = {} + repeatingTimers.set(slug, timer) + } + + updateTimer(timer, prof.id, trigger.id, trigger.config?.interval, trigger.config?.delay ?? 0) } } diff --git a/plugins/tplink-kasa/main/package.json b/plugins/tplink-kasa/main/package.json index b36fb2c5..af4f25ac 100644 --- a/plugins/tplink-kasa/main/package.json +++ b/plugins/tplink-kasa/main/package.json @@ -17,6 +17,6 @@ "castmate-plugin-iot-main": "workspace:^", "castmate-plugin-iot-shared": "workspace:^", "castmate-schema": "workspace:^", - "tplink-smarthome-api": "^4.2.0" + "tplink-smarthome-api": "^5.0.0" } } diff --git a/plugins/tplink-kasa/main/src/resources.ts b/plugins/tplink-kasa/main/src/resources.ts index a9a91ca3..322b438d 100644 --- a/plugins/tplink-kasa/main/src/resources.ts +++ b/plugins/tplink-kasa/main/src/resources.ts @@ -43,7 +43,7 @@ class KasaLight extends LightResource { this.parseLightState(lightState) }) - kasaBulb.startPolling(30000) + //kasaBulb.(30000) } private parseLightState(state: LightState) { @@ -109,7 +109,7 @@ class KasaPlug extends PlugResource { }) //TODO: Why is this deprecated? - kasaPlug.startPolling(30000) + //kasaPlug.startPolling(30000) } private async updateState() { @@ -158,7 +158,7 @@ export function setupLights(subnetMask: ReactiveRef) { logger.error("TP-Link Kasa Error", err) }) - client.on("discover-invalid", (err) => { + client.on("discovery-invalid", (err) => { logger.error("Kasa Discovery Invalid?", err) }) } diff --git a/plugins/twitch/main/src/category-cache.ts b/plugins/twitch/main/src/category-cache.ts index ecc9226b..48c5ea32 100644 --- a/plugins/twitch/main/src/category-cache.ts +++ b/plugins/twitch/main/src/category-cache.ts @@ -12,9 +12,13 @@ import { registerSchemaTemplate, registerSchemaUnexpose, template, + usePluginLogger, } from "castmate-core" import { TwitchAccount } from "./twitch-auth" import fuzzysort from "fuzzysort" +import { HelixGame } from "@twurple/api" + +const logger = usePluginLogger("twitch") export function setupCategoryCache() { onLoad(() => { @@ -55,6 +59,17 @@ function isDefinitelyNotTwitchId(maybeId: string) { return maybeId.match(nonDigits) != null } +function helixToCategory(g: HelixGame): TwitchCategory { + return { + id: g.id, + name: g.name, + image: g.boxArtUrl.replace("{width}", "52").replace("{height}", "72"), + [Symbol.toPrimitive]() { + return this.name + }, + } +} + export const CategoryCache = Service( class { private categoryCache = new Map() @@ -70,16 +85,7 @@ export const CategoryCache = Service( try { const result = await TwitchAccount.channel.apiClient.search.searchCategories(query) - const categories = result.data.map( - (g): TwitchCategory => ({ - id: g.id, - name: g.name, - image: g.boxArtUrl.replace("{width}", "52").replace("{height}", "72"), - [Symbol.toPrimitive]() { - this.name - }, - }) - ) + const categories = result.data.map(helixToCategory) for (const c of categories) { this.cacheCategory(c) @@ -102,14 +108,7 @@ export const CategoryCache = Service( const game = await TwitchAccount.channel.apiClient.games.getGameById(id) if (game) { - const data: TwitchCategory = { - id: game.id, - name: game.name, - image: game.boxArtUrl.replace("{width}", "52").replace("{height}", "72"), - [Symbol.toPrimitive]() { - this.name - }, - } + const data = helixToCategory(game) this.cacheCategory(data) return data } @@ -124,14 +123,7 @@ export const CategoryCache = Service( const game = await TwitchAccount.channel.apiClient.games.getGameByName(name) if (game) { - const data: TwitchCategory = { - id: game.id, - name: game.name, - image: game.boxArtUrl.replace("{width}", "52").replace("{height}", "72"), - [Symbol.toPrimitive]() { - this.name - }, - } + const data = helixToCategory(game) this.cacheCategory(data) return data } diff --git a/plugins/twitch/main/src/follows.ts b/plugins/twitch/main/src/follows.ts index d2eb3c93..8f94e5bf 100644 --- a/plugins/twitch/main/src/follows.ts +++ b/plugins/twitch/main/src/follows.ts @@ -58,6 +58,8 @@ export function setupFollows() { service.eventsub.onChannelFollow(account.twitchId, account.twitchId, async (event) => { ViewerCache.getInstance().cacheFollowEvent(event) + lastFollower.value = await ViewerCache.getInstance().getResolvedViewer(event.userId) + follow({ viewer: event.userId, }) diff --git a/plugins/twitch/main/src/info-manager.ts b/plugins/twitch/main/src/info-manager.ts index ebceb097..6f3cf74b 100644 --- a/plugins/twitch/main/src/info-manager.ts +++ b/plugins/twitch/main/src/info-manager.ts @@ -230,11 +230,14 @@ export function setupInfoManager() { await StreamInfoManager.getInstance().startManagingInfo() + logger.log("Registering Online Handlers") service.eventsub.onStreamOnline(channel.twitchId, async (event) => { + logger.log("Stream Going Online") live.value = true }) service.eventsub.onStreamOffline(channel.twitchId, async (event) => { + logger.log("Stream Going Offline") live.value = false }) }) diff --git a/plugins/twitch/renderer/src/components/category/TwitchCategoryView.vue b/plugins/twitch/renderer/src/components/category/TwitchCategoryView.vue index f323c58e..66b6dcc4 100644 --- a/plugins/twitch/renderer/src/components/category/TwitchCategoryView.vue +++ b/plugins/twitch/renderer/src/components/category/TwitchCategoryView.vue @@ -1,6 +1,6 @@