@mthines/reaper-mcp 0.19.0 → 0.70.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -570
- package/bridge-ui/mcp_bridge.html +129 -0
- package/bridge-ui/mcp_bridge.webmanifest +16 -0
- package/bridge-ui/mcp_bridge_icon_192.png +0 -0
- package/bridge-ui/mcp_bridge_icon_512.png +0 -0
- package/bridge-ui/mcp_bridge_sw.js +35 -0
- package/claude/agents/plugin-researcher.md +188 -0
- package/claude/knowledge/reference/aesthetics-scoring.md +48 -0
- package/{knowledge → claude/knowledge}/reference/common-mistakes.md +1 -1
- package/claude/knowledge/reference/lorekit-setup.md +36 -0
- package/claude/knowledge/reference/memory-protocol.md +377 -0
- package/{knowledge → claude/knowledge}/reference/metering.md +5 -5
- package/claude/knowledge/reference/perceived-loudness.md +182 -0
- package/{knowledge → claude/knowledge}/workflows/editing.md +4 -0
- package/{knowledge → claude/knowledge}/workflows/gain-staging.md +21 -23
- package/{knowledge → claude/knowledge}/workflows/learn-plugin.md +15 -48
- package/{knowledge → claude/knowledge}/workflows/master-bus.md +13 -1
- package/claude/knowledge/workflows/section-levels.md +138 -0
- package/{knowledge → claude/knowledge}/workflows/session-prep.md +4 -0
- package/claude/knowledge/workflows/transient-lighting.md +52 -0
- package/claude/skills/critique/SKILL.md +131 -0
- package/claude/skills/editing/SKILL.md +281 -0
- package/claude/skills/learn-plugin/SKILL.md +210 -0
- package/claude/skills/mastering/SKILL.md +151 -0
- package/claude/skills/mix-memory/SKILL.md +140 -0
- package/claude/skills/mixer/SKILL.md +253 -0
- package/claude/skills/new-extension/SKILL.md +324 -0
- package/main.js +7843 -1940
- package/package.json +17 -15
- package/reaper/CLAUDE.md +22 -4
- package/reaper/mcp_actions_lib.lua +375 -0
- package/reaper/mcp_analyzer.jsfx +27 -12
- package/reaper/mcp_arrange_lib.lua +949 -0
- package/reaper/mcp_bridge.lua +2321 -758
- package/reaper/mcp_edit_views_lib.lua +1378 -0
- package/reaper/mcp_envelopes_lib.lua +748 -0
- package/reaper/mcp_extensions_lib.lua +55 -0
- package/reaper/mcp_fxstate_lib.lua +144 -0
- package/reaper/mcp_gain_trace.jsfx +126 -0
- package/reaper/mcp_items_lib.lua +877 -0
- package/reaper/mcp_lufs_meter.jsfx +16 -13
- package/reaper/mcp_midi_lib.lua +555 -0
- package/reaper/mcp_mixing_lib.lua +1021 -0
- package/reaper/mcp_plan_lib.lua +801 -0
- package/reaper/mcp_prefs_lib.lua +127 -0
- package/reaper/mcp_project_lib.lua +1088 -0
- package/reaper/mcp_recording_lib.lua +595 -0
- package/reaper/mcp_render_lib.lua +492 -0
- package/reaper/mcp_snapshot_lib.lua +547 -120
- package/reaper/mcp_snapshot_next.lua +10 -4
- package/reaper/mcp_snapshot_prev.lua +11 -4
- package/reaper/mcp_snapshot_quick_save.lua +7 -32
- package/reaper/mcp_snapshots_ui_lib.lua +258 -0
- package/reaper/mcp_tracks_lib.lua +409 -0
- package/sidecar/analysis.py +661 -0
- package/sidecar/chords.py +96 -0
- package/sidecar/mixcheck.py +295 -0
- package/sidecar/requirements-chords.txt +6 -0
- package/sidecar/requirements-separation.txt +3 -0
- package/sidecar/requirements.txt +17 -0
- package/sidecar/separation.py +142 -0
- package/sidecar/server.py +622 -0
- package/sidecar/tests/test_analysis.py +249 -0
- package/sidecar/tests/test_chords.py +117 -0
- package/sidecar/tests/test_mixcheck.py +163 -0
- package/sidecar/tests/test_separation.py +73 -0
- package/sidecar/tests/test_server.py +464 -0
- package/sidecar/tests/test_verify.py +243 -0
- package/sidecar/tests/test_voice_render.py +268 -0
- package/sidecar/tests/test_voice_sketch.py +140 -0
- package/sidecar/verify.py +224 -0
- package/sidecar/voice_render.py +295 -0
- package/sidecar/voice_sketch.py +280 -0
- package/claude-agents/critique.md +0 -147
- package/claude-agents/editor.md +0 -184
- package/claude-agents/levels.md +0 -107
- package/claude-agents/mastering.md +0 -129
- package/claude-agents/mixer.md +0 -248
- package/claude-agents/preflight.md +0 -149
- package/claude-agents/producer.md +0 -159
- package/claude-agents/setup.md +0 -150
- package/claude-agents/stems.md +0 -135
- package/claude-rules/architecture.md +0 -39
- package/claude-rules/development.md +0 -54
- package/claude-rules/lua-bridge.md +0 -50
- package/claude-rules/testing.md +0 -42
- package/claude-skills/learn-plugin.md +0 -123
- package/knowledge/CLAUDE.md +0 -71
- package/knowledge/plugins/_template.md +0 -82
- package/knowledge/plugins/fabfilter/pro-c-2.md +0 -117
- package/knowledge/plugins/fabfilter/pro-l-2.md +0 -95
- package/knowledge/plugins/fabfilter/pro-q-3.md +0 -112
- package/knowledge/plugins/neural-dsp/helix-native.md +0 -104
- package/knowledge/plugins/stock-reaper/js-1175-compressor.md +0 -94
- package/knowledge/plugins/stock-reaper/rea-comp.md +0 -100
- package/knowledge/plugins/stock-reaper/rea-delay.md +0 -95
- package/knowledge/plugins/stock-reaper/rea-eq.md +0 -103
- package/knowledge/plugins/stock-reaper/rea-gate.md +0 -99
- package/knowledge/plugins/stock-reaper/rea-limit.md +0 -75
- package/knowledge/plugins/stock-reaper/rea-verb.md +0 -76
- package/knowledge/reference/perceived-loudness.md +0 -122
- package/reaper/mcp_snapshot_manager.lua +0 -1001
- /package/{knowledge → claude/knowledge}/genres/_template.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/electronic.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/hip-hop.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/metal.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/orchestral.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/pop.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/rock.md +0 -0
- /package/{knowledge → claude/knowledge}/reference/compression.md +0 -0
- /package/{knowledge → claude/knowledge}/reference/frequencies.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/delivery.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/drum-bus.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/low-end.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/stem-prep.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/vocal-chain.md +0 -0
|
@@ -11,15 +11,21 @@ if #snapshots == 0 then
|
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
-- Resolve current position by name (stable across add/delete/re-sort).
|
|
15
|
+
local cur = lib.get_current_name()
|
|
16
|
+
local idx = 0
|
|
17
|
+
for i, s in ipairs(snapshots) do
|
|
18
|
+
if s.name == cur then idx = i; break end
|
|
19
|
+
end
|
|
15
20
|
idx = idx + 1
|
|
16
21
|
if idx > #snapshots then idx = 1 end
|
|
17
22
|
|
|
18
23
|
local snap = snapshots[idx]
|
|
19
|
-
local ok, err = lib.restore_snapshot(snap)
|
|
24
|
+
local ok, err, _, warnings = lib.restore_snapshot(snap)
|
|
20
25
|
if ok then
|
|
21
|
-
lib.
|
|
22
|
-
|
|
26
|
+
lib.set_current_name(snap.name)
|
|
27
|
+
local w = (warnings and #warnings > 0) and (" [" .. #warnings .. " warning(s)]") or ""
|
|
28
|
+
lib.toast("Snapshot " .. idx .. "/" .. #snapshots .. ": " .. snap.name .. w)
|
|
23
29
|
else
|
|
24
30
|
lib.toast("Restore failed: " .. (err or "unknown"))
|
|
25
31
|
end
|
|
@@ -11,15 +11,22 @@ if #snapshots == 0 then
|
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
-- Resolve current position by name (stable across add/delete/re-sort).
|
|
15
|
+
local cur = lib.get_current_name()
|
|
16
|
+
local idx = 0
|
|
17
|
+
for i, s in ipairs(snapshots) do
|
|
18
|
+
if s.name == cur then idx = i; break end
|
|
19
|
+
end
|
|
20
|
+
-- If not found (idx 0), idx-1 wraps to the last snapshot.
|
|
15
21
|
idx = idx - 1
|
|
16
22
|
if idx < 1 then idx = #snapshots end
|
|
17
23
|
|
|
18
24
|
local snap = snapshots[idx]
|
|
19
|
-
local ok, err = lib.restore_snapshot(snap)
|
|
25
|
+
local ok, err, _, warnings = lib.restore_snapshot(snap)
|
|
20
26
|
if ok then
|
|
21
|
-
lib.
|
|
22
|
-
|
|
27
|
+
lib.set_current_name(snap.name)
|
|
28
|
+
local w = (warnings and #warnings > 0) and (" [" .. #warnings .. " warning(s)]") or ""
|
|
29
|
+
lib.toast("Snapshot " .. idx .. "/" .. #snapshots .. ": " .. snap.name .. w)
|
|
23
30
|
else
|
|
24
31
|
lib.toast("Restore failed: " .. (err or "unknown"))
|
|
25
32
|
end
|
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
-- MCP Snapshot: Quick Save — save a snapshot with auto-generated name, no dialog
|
|
2
|
-
-- Bind to a key for rapid iteration.
|
|
2
|
+
-- Bind to a key for rapid iteration. Named by the local time, e.g. "2026-10-30 14:32:05".
|
|
3
3
|
|
|
4
4
|
local info = debug.getinfo(1, "S")
|
|
5
5
|
local script_dir = info.source:match("@?(.*[\\/])")
|
|
6
6
|
local lib = dofile(script_dir .. "mcp_snapshot_lib.lua")
|
|
7
7
|
|
|
8
|
-
local
|
|
9
|
-
|
|
10
|
-
-- Find next available "Snapshot N" index
|
|
11
|
-
local next_idx = #snapshots + 1
|
|
12
|
-
for _, s in ipairs(snapshots) do
|
|
13
|
-
local num = s.name:match("^Snapshot (%d+)$")
|
|
14
|
-
if num then next_idx = math.max(next_idx, tonumber(num) + 1) end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
local name = "Snapshot " .. next_idx
|
|
18
|
-
lib.ensure_snapshot_dir()
|
|
19
|
-
|
|
20
|
-
local snapshot = {
|
|
21
|
-
name = name,
|
|
22
|
-
description = "",
|
|
23
|
-
timestamp = os.time() * 1000,
|
|
24
|
-
mixerState = lib.capture_mixer_state(),
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
local path = lib.snapshot_path(name)
|
|
28
|
-
local ok = lib.write_file(path, lib.json_encode(snapshot))
|
|
8
|
+
local name = lib.time_name()
|
|
9
|
+
local ok, err, track_count = lib.save_snapshot(name, "")
|
|
29
10
|
if ok then
|
|
30
|
-
--
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if s.name == name then
|
|
34
|
-
lib.set_current_index(i)
|
|
35
|
-
break
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
lib.toast("Saved: " .. name .. " (" .. #snapshot.mixerState.tracks .. " tracks)")
|
|
11
|
+
-- Make next/prev A/B start from the snapshot we just saved.
|
|
12
|
+
lib.set_current_name(name)
|
|
13
|
+
lib.toast("Saved: " .. name .. " (" .. (track_count or "?") .. " tracks)")
|
|
39
14
|
else
|
|
40
|
-
lib.toast("Save failed: " ..
|
|
15
|
+
lib.toast("Save failed: " .. (err or name))
|
|
41
16
|
end
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Snapshots on the status page (apps/bridge-ui, Snapshots view)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- The page lists the project's mixer snapshots, restores one with a click (A/B
|
|
5
|
+
-- while listening) and saves a new one, through REAPER's web interface ExtState:
|
|
6
|
+
--
|
|
7
|
+
-- snap_seen = <page clock ms> The page pings this on every poll while the view is open.
|
|
8
|
+
-- While it keeps changing, the folder is re-read about once a
|
|
9
|
+
-- second; ExtState `snapshots` is rewritten only when the list
|
|
10
|
+
-- (or the current snapshot) changed.
|
|
11
|
+
-- snap_req = <id>-<token>-<op>-<hex name>[-<hex field>…]
|
|
12
|
+
-- op: restore | delete <name>, save <name>-<description>,
|
|
13
|
+
-- edit <name>-<new name>-<description>, clear (any name: deletes every snapshot
|
|
14
|
+
-- of the project, one snapshot_delete each). The token is the bridge's per-run control
|
|
15
|
+
-- token, only readable from the status feed (REAPER's web server
|
|
16
|
+
-- sends no CORS headers), so another site can't make it count.
|
|
17
|
+
-- Name and description travel hex-encoded: no URL or `;` escaping.
|
|
18
|
+
--
|
|
19
|
+
-- A request runs as an ordinary snapshot_restore / _save / _delete / _edit command through the
|
|
20
|
+
-- bridge's dispatcher (ctx.run): logged on the page, refused in read-only or paused
|
|
21
|
+
-- mode, one undo point per restore. The answer is published in `snapshots.result`.
|
|
22
|
+
--
|
|
23
|
+
-- It also registers the keybindable snapshot scripts as actions (M.register_actions).
|
|
24
|
+
--
|
|
25
|
+
-- Loaded by mcp_bridge.lua (keeps the bridge under Lua's 200-locals limit).
|
|
26
|
+
-- =============================================================================
|
|
27
|
+
|
|
28
|
+
local M = {}
|
|
29
|
+
|
|
30
|
+
local ctx
|
|
31
|
+
local cache = {} -- path -> { size, entry }
|
|
32
|
+
local seen_value, seen_at = "", -math.huge
|
|
33
|
+
local last_scan = -math.huge
|
|
34
|
+
local last_sig
|
|
35
|
+
local result -- the answer to the page's latest request
|
|
36
|
+
local WATCH_S = 5 -- keep watching the folder this long after the page's last ping
|
|
37
|
+
local SCAN_S = 1 -- re-read the folder at most this often while watched
|
|
38
|
+
local NAME_MAX, DESC_MAX, LIST_MAX = 128, 500, 200
|
|
39
|
+
|
|
40
|
+
-- Persisted by the snapshot action scripts too (mcp_snapshot_lib.lua): next/prev continue from here
|
|
41
|
+
local CURRENT_SECTION, CURRENT_KEY = "MCP_Snapshots", "current_name"
|
|
42
|
+
|
|
43
|
+
-- ctx: api (reaper), section (ExtState section), dir() and storage() (where snapshots live),
|
|
44
|
+
-- decode (full JSON decoder), encode (JSON encoder), read_file, token() (control token),
|
|
45
|
+
-- run(cmd) -> response (the bridge's dispatcher, returns { success, data, error }).
|
|
46
|
+
function M.init(c)
|
|
47
|
+
ctx = c
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
-- The keybindable snapshot scripts setup copies next to the bridge. `reaper-mcp setup` lists them in
|
|
51
|
+
-- reaper-kb.ini while REAPER is closed (ensureSnapshotActions() in cli.ts); when REAPER was open, the
|
|
52
|
+
-- bridge adds the missing ones when it starts. Returns the scripts it added.
|
|
53
|
+
M.ACTION_SCRIPTS = { "mcp_snapshot_quick_save.lua", "mcp_snapshot_next.lua", "mcp_snapshot_prev.lua" }
|
|
54
|
+
|
|
55
|
+
function M.register_actions(api, scripts_dir, kb_text)
|
|
56
|
+
local added = {}
|
|
57
|
+
if type(api.AddRemoveReaScript) ~= "function" then return added end
|
|
58
|
+
local listed = {}
|
|
59
|
+
for line in (kb_text or ""):gmatch("[^\r\n]+") do
|
|
60
|
+
if line:sub(1, 4) == "SCR " then
|
|
61
|
+
for _, f in ipairs(M.ACTION_SCRIPTS) do
|
|
62
|
+
if line:find(f, 1, true) then listed[f] = true end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
for _, f in ipairs(M.ACTION_SCRIPTS) do
|
|
67
|
+
local path = scripts_dir .. f
|
|
68
|
+
local fh = not listed[f] and io.open(path, "rb")
|
|
69
|
+
if fh then
|
|
70
|
+
fh:close()
|
|
71
|
+
if (api.AddRemoveReaScript(true, 0, path, true) or 0) > 0 then added[#added + 1] = f end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
return added
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
local function file_size(path)
|
|
78
|
+
local f = io.open(path, "rb")
|
|
79
|
+
if not f then return nil end
|
|
80
|
+
local size = f:seek("end")
|
|
81
|
+
f:close()
|
|
82
|
+
return size
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
-- Metadata of one snapshot file, wherever its keys sit: files written by the bridge before
|
|
86
|
+
-- metadata-first saves have description after the (large) mixerState.
|
|
87
|
+
local function read_entry(path)
|
|
88
|
+
local content = ctx.read_file(path)
|
|
89
|
+
if not content then return nil end
|
|
90
|
+
local ok, snap = pcall(ctx.decode, content)
|
|
91
|
+
if not ok or type(snap) ~= "table" or type(snap.name) ~= "string" or snap.name == "" then return nil end
|
|
92
|
+
local state = type(snap.mixerState) == "table" and snap.mixerState or {}
|
|
93
|
+
local tracks = type(state.tracks) == "table" and #state.tracks or tonumber(snap.trackCount)
|
|
94
|
+
local desc = type(snap.description) == "string" and snap.description or ""
|
|
95
|
+
return {
|
|
96
|
+
name = snap.name,
|
|
97
|
+
description = desc:sub(1, DESC_MAX),
|
|
98
|
+
timestamp = tonumber(snap.timestamp) or 0,
|
|
99
|
+
trackCount = tracks,
|
|
100
|
+
scoped = state.scoped == true or snap.scoped == true,
|
|
101
|
+
fullChunk = state.fullChunk == true or snap.fullChunk == true,
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
-- Every snapshot in the folder, newest first. A file is read again only when its size changed.
|
|
106
|
+
function M.list()
|
|
107
|
+
local api, dir = ctx.api, ctx.dir()
|
|
108
|
+
api.EnumerateFiles(dir, -1) -- REAPER caches directory listings; -1 re-reads this one
|
|
109
|
+
local list, present = {}, {}
|
|
110
|
+
local i = 0
|
|
111
|
+
while true do
|
|
112
|
+
local fn = api.EnumerateFiles(dir, i)
|
|
113
|
+
if not fn then break end
|
|
114
|
+
if fn:match("%.json$") then
|
|
115
|
+
local path = dir .. fn
|
|
116
|
+
local size = file_size(path)
|
|
117
|
+
local c = cache[path]
|
|
118
|
+
if not c or c.size ~= size then
|
|
119
|
+
c = { size = size, entry = read_entry(path) }
|
|
120
|
+
cache[path] = c
|
|
121
|
+
end
|
|
122
|
+
present[path] = true
|
|
123
|
+
if c.entry then list[#list + 1] = c.entry end
|
|
124
|
+
end
|
|
125
|
+
i = i + 1
|
|
126
|
+
end
|
|
127
|
+
for path in pairs(cache) do
|
|
128
|
+
if not present[path] then cache[path] = nil end
|
|
129
|
+
end
|
|
130
|
+
table.sort(list, function(a, b)
|
|
131
|
+
if a.timestamp == b.timestamp then return a.name < b.name end
|
|
132
|
+
return a.timestamp > b.timestamp
|
|
133
|
+
end)
|
|
134
|
+
for k = LIST_MAX + 1, #list do list[k] = nil end
|
|
135
|
+
return list
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
local function current_name()
|
|
139
|
+
local v = ctx.api.GetExtState(CURRENT_SECTION, CURRENT_KEY)
|
|
140
|
+
return v ~= "" and v or nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
-- A restore from anywhere (page, agent) becomes the current snapshot, as the next/prev actions do
|
|
144
|
+
function M.after_command(cmd, response)
|
|
145
|
+
if cmd.type == "snapshot_restore" and response.success and type(cmd.params) == "table" and type(cmd.params.name) == "string" then
|
|
146
|
+
ctx.api.SetExtState(CURRENT_SECTION, CURRENT_KEY, cmd.params.name, false)
|
|
147
|
+
end
|
|
148
|
+
-- A save (page or agent) can replace a file with one of the same size: read that file again.
|
|
149
|
+
-- Deleted files drop out of the cache on the next list().
|
|
150
|
+
local data = type(response.data) == "table" and response.data or {}
|
|
151
|
+
if (cmd.type == "snapshot_save" or cmd.type == "snapshot_edit") and type(data.path) == "string" then
|
|
152
|
+
cache[data.path] = nil
|
|
153
|
+
end
|
|
154
|
+
if cmd.type == "snapshot_save" or cmd.type == "snapshot_delete" or cmd.type == "snapshot_restore" or cmd.type == "snapshot_edit" then
|
|
155
|
+
last_scan = -math.huge -- re-read on the next watched tick
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
local function unhex(s)
|
|
160
|
+
return (s:gsub("%x%x", function(h) return string.char(tonumber(h, 16)) end))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
-- Runs one page request; returns true when it was valid (and ran)
|
|
164
|
+
local function handle(req)
|
|
165
|
+
local id, token, op, rest = req:match("^(%d+)%-(%x+)%-(%a+)%-([%x%-]*)$")
|
|
166
|
+
if not id or token ~= ctx.token() then return false end
|
|
167
|
+
local fields = {}
|
|
168
|
+
for h in (rest .. "-"):gmatch("(%x*)%-") do
|
|
169
|
+
if #h % 2 ~= 0 then return false end
|
|
170
|
+
fields[#fields + 1] = unhex(h)
|
|
171
|
+
end
|
|
172
|
+
local name = (fields[1] or ""):sub(1, NAME_MAX)
|
|
173
|
+
if name == "" then return false end
|
|
174
|
+
local cmd = { id = "page-" .. id, source = "page" }
|
|
175
|
+
if op == "clear" then
|
|
176
|
+
-- Every snapshot, one ordinary snapshot_delete each (logged, refused while read-only or paused).
|
|
177
|
+
-- list() is capped, so go again until a round deletes nothing.
|
|
178
|
+
local deleted, failed, first_error = 0, 0, nil
|
|
179
|
+
local tried = {}
|
|
180
|
+
while true do
|
|
181
|
+
local progressed = false
|
|
182
|
+
for _, e in ipairs(M.list()) do
|
|
183
|
+
if not tried[e.name] then
|
|
184
|
+
tried[e.name] = true
|
|
185
|
+
progressed = true
|
|
186
|
+
local r = ctx.run({ id = "page-" .. id .. "-" .. (deleted + failed), source = "page", type = "snapshot_delete", params = { name = e.name } }) or {}
|
|
187
|
+
if r.success then deleted = deleted + 1 else failed = failed + 1; first_error = first_error or r.error end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
if not progressed then break end
|
|
191
|
+
end
|
|
192
|
+
result = {
|
|
193
|
+
id = id, op = op, name = deleted .. (deleted == 1 and " snapshot" or " snapshots"),
|
|
194
|
+
ok = failed == 0, error = failed > 0 and (failed .. " not deleted: " .. tostring(first_error)) or nil,
|
|
195
|
+
}
|
|
196
|
+
return true
|
|
197
|
+
end
|
|
198
|
+
if op == "restore" or op == "delete" then
|
|
199
|
+
cmd.type, cmd.params = "snapshot_" .. op, { name = name }
|
|
200
|
+
elseif op == "save" then
|
|
201
|
+
cmd.type, cmd.params = "snapshot_save", { name = name, description = (fields[2] or ""):sub(1, DESC_MAX) }
|
|
202
|
+
elseif op == "edit" and fields[2] and fields[2] ~= "" then
|
|
203
|
+
cmd.type = "snapshot_edit"
|
|
204
|
+
cmd.params = { name = name, newName = fields[2]:sub(1, NAME_MAX), description = (fields[3] or ""):sub(1, DESC_MAX) }
|
|
205
|
+
else
|
|
206
|
+
return false
|
|
207
|
+
end
|
|
208
|
+
local response = ctx.run(cmd) or {}
|
|
209
|
+
local data = type(response.data) == "table" and response.data or {}
|
|
210
|
+
result = {
|
|
211
|
+
id = id, op = op, name = op == "edit" and cmd.params.newName or name, ok = response.success == true,
|
|
212
|
+
error = response.error, warnings = data.warnings, tracksRestored = data.tracksRestored,
|
|
213
|
+
}
|
|
214
|
+
return true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
local function signature(storage, current, list)
|
|
218
|
+
local parts = { storage, current or "" }
|
|
219
|
+
for _, e in ipairs(list) do
|
|
220
|
+
parts[#parts + 1] = table.concat({ e.name, e.description, tostring(e.timestamp), tostring(e.trackCount),
|
|
221
|
+
tostring(e.scoped), tostring(e.fullChunk) }, "\1")
|
|
222
|
+
end
|
|
223
|
+
if result then parts[#parts + 1] = "result:" .. result.id end
|
|
224
|
+
return table.concat(parts, "\2")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
local function publish(list)
|
|
228
|
+
local storage, current = ctx.storage(), current_name()
|
|
229
|
+
local sig = signature(storage, current, list)
|
|
230
|
+
if sig == last_sig then return end
|
|
231
|
+
last_sig = sig
|
|
232
|
+
ctx.api.SetExtState(ctx.section, "snapshots", ctx.encode({
|
|
233
|
+
v = 1, storage = storage, current = current, snapshots = list, result = result,
|
|
234
|
+
}), false)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
-- Called every defer tick. now: seconds (reaper.time_precise)
|
|
238
|
+
function M.poll(now)
|
|
239
|
+
local api, section = ctx.api, ctx.section
|
|
240
|
+
local req = api.GetExtState(section, "snap_req")
|
|
241
|
+
local handled = false
|
|
242
|
+
if req ~= "" then
|
|
243
|
+
api.DeleteExtState(section, "snap_req", false)
|
|
244
|
+
handled = handle(req)
|
|
245
|
+
end
|
|
246
|
+
local ping = api.GetExtState(section, "snap_seen")
|
|
247
|
+
if ping ~= "" and ping ~= seen_value then
|
|
248
|
+
-- a fresh ping after a pause: read the folder now
|
|
249
|
+
if now - seen_at >= WATCH_S then last_scan = -math.huge end
|
|
250
|
+
seen_value, seen_at = ping, now
|
|
251
|
+
end
|
|
252
|
+
local watched = now - seen_at < WATCH_S
|
|
253
|
+
if not handled and not (watched and now - last_scan >= SCAN_S) then return end
|
|
254
|
+
last_scan = now
|
|
255
|
+
publish(M.list())
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
return M
|