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>
This commit is contained in:
Vendored
+184
@@ -0,0 +1,184 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user