Files
christianandClaude Opus 5.5 1a658875bc Resi cue countdowns on ProPresenter and in Companion
The Resi -> ProPresenter bridge, its installer and site kit, and the
Companion module resi-studio-cues, as running at North Attleboro.

Bridge (resi_to_propresenter.py)
- Follows a Resi decoder's playhead and keeps ProPresenter's NextCue timer
  counting down to the next cue; optional cue name as a stage message, sent
  once per cue (ProPresenter flashes it on every send) and off by default.
- Playhead estimate no longer learns a cadence from the partial first gap
  after a start, so a 30s Resi doesn't read as paused half the time.
- Backs off on 429, holds 2 min before retrying a failed startup login, and
  logs Resi's actual reason (wrong password, USER_LOCKED, rate limit).
- --setup asks for the login, decoder, ProPresenter and timer.

install_bridge.sh installs it as a login service (outside ~/Documents,
which launchd may not read); make_kit.sh builds the kit for another site.

Companion module resi-studio-cues 0.1.4 (source/, package committed)
- Same playhead fix; login and request hold-off (15s doubling to 5 min);
  a 401/403 on a fresh session is a permission, not a reason to sign in
  again; init never fails on a Resi problem, so Companion can't restart it
  into a login loop.

Tests: test_playhead.py, test_stage_message.py, test_rate_limit.py and
source/backoff_test.js run offline; test_end_to_end.py drives a real
ProPresenter and refuses to run without an explicit flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:11:01 -04:00

184 lines
6.6 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpdateActions = UpdateActions;
function UpdateActions(self) {
self.setActionDefinitions({
refresh_profile: {
name: 'Refresh profile (users/me)',
options: [],
callback: async () => {
await self.refreshProfileNow();
},
},
refresh_lists: {
name: 'Refresh venues/players list',
options: [],
callback: async () => {
await self.refreshListsNow();
},
},
add_cue_at_position: {
name: 'Add cue at player position (or custom time)',
options: [
{
id: 'name',
type: 'textinput',
label: 'Cue name',
default: 'Web Cue',
useVariables: true,
},
{
id: 'privateCue',
type: 'checkbox',
label: 'Private cue',
default: false,
},
{
id: 'positionMode',
type: 'dropdown',
label: 'Position source',
default: 'player',
choices: [
{ id: 'player', label: 'Player position (current)' },
{ id: 'custom', label: 'Custom time' },
],
},
{
id: 'customPosition',
type: 'textinput',
label: 'Custom position (HH:MM:SS or HH:MM:SS.mmm)',
default: '00:00:00.000',
useVariables: true,
},
],
callback: async (evt) => {
const name = (await self.parseVariablesInString(String(evt.options.name ?? ''))).trim();
const privateCue = Boolean(evt.options.privateCue);
const positionMode = String(evt.options.positionMode ?? 'player');
const customPosition = (await self.parseVariablesInString(String(evt.options.customPosition ?? ''))).trim();
await self.addCueFromPosition({
name,
privateCue,
positionMode,
customPosition,
});
},
},
delete_cue_by_name: {
name: 'Delete cue by name',
options: [
{
id: 'cueName',
type: 'textinput',
label: 'Cue name',
default: '',
useVariables: true,
},
{
id: 'matchMode',
type: 'dropdown',
label: 'Match mode',
default: 'exact',
choices: [
{ id: 'exact', label: 'Exact (case-insensitive)' },
{ id: 'contains', label: 'Contains (case-insensitive)' },
],
},
{
id: 'deleteMode',
type: 'dropdown',
label: 'Delete',
default: 'first',
choices: [
{ id: 'first', label: 'First match only' },
{ id: 'all', label: 'All matches' },
],
},
{
id: 'onlyMine',
type: 'checkbox',
label: 'Only delete cues created by this user',
default: true,
},
],
callback: async (evt) => {
const cueName = await self.parseVariablesInString(String(evt.options.cueName ?? ''));
const matchMode = String(evt.options.matchMode ?? 'exact');
const deleteMode = String(evt.options.deleteMode ?? 'first');
const onlyMine = Boolean(evt.options.onlyMine);
await self.deleteCueByName({
name: cueName,
matchMode,
deleteMode,
onlyMine,
});
},
},
select_decoder: {
name: 'Select decoder/player',
options: [
{
id: 'playerId',
type: 'dropdown',
label: 'Decoder / Player',
default: '',
choices: self.getPlayerChoices(),
},
],
callback: async (evt) => {
await self.selectPlayer(String(evt.options.playerId || ''));
},
},
resync_to_cue: {
name: 'Resync playhead to cue (use on the button that fires it)',
options: [
{
id: 'cueName',
type: 'textinput',
label: 'Cue name',
default: '',
useVariables: true,
},
],
callback: async (evt) => {
const cueName = await self.parseVariablesInString(String(evt.options.cueName ?? ''));
self.resyncToCue(cueName);
},
},
resync_to_next_cue: {
name: 'Resync playhead to the NEXT cue',
options: [],
callback: async () => {
self.resyncToNextCue();
},
},
lookup_cue_by_name: {
name: 'Lookup cue by name (stores in lookup_* variables)',
options: [
{
id: 'cueName',
type: 'textinput',
label: 'Cue name',
default: '',
useVariables: true,
},
{
id: 'matchMode',
type: 'dropdown',
label: 'Match mode',
default: 'exact',
choices: [
{ id: 'exact', label: 'Exact (case-insensitive)' },
{ id: 'contains', label: 'Contains (case-insensitive)' },
],
},
],
callback: async (evt) => {
const cueName = await self.parseVariablesInString(String(evt.options.cueName ?? ''));
const mode = String(evt.options.matchMode ?? 'exact');
self.lookupCueByName(cueName, mode);
},
},
});
}
//# sourceMappingURL=actions.js.map