davinci-resolve-mcp 2.76.0 → 2.78.0
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/CHANGELOG.md +76 -0
- package/README.md +1 -1
- package/docs/SKILL.md +17 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/aaf_probe.py +137 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +63 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,82 @@
|
|
|
2
2
|
|
|
3
3
|
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
|
|
4
4
|
|
|
5
|
+
## What's New in v2.78.0
|
|
6
|
+
|
|
7
|
+
The AAF probe reports where in the *source* an event actually lives, not where it
|
|
8
|
+
lives in the consolidated fragment the AAF happens to reference.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`srcIn`/`srcOut` were handle offsets, not positions in the take.** A
|
|
13
|
+
`SourceClip`'s `start` is an offset into whatever mob it references *directly*,
|
|
14
|
+
and for consolidated media that mob is a per-cut fragment carrying handles — not
|
|
15
|
+
the take. On a real turnover 774 of 878 events reported `srcIn <= 45`, and one
|
|
16
|
+
take used thirteen times reported `srcIn 40` all thirteen times; two cuts of one
|
|
17
|
+
take cannot both begin at frame 42 of it. A consumer placing those numbers
|
|
18
|
+
against camera originals lands on the right cut of the right take showing the
|
|
19
|
+
**wrong moment**, and the number fits inside the file, so no range check catches
|
|
20
|
+
it. The consumer measured 2 of 526 cuts matching its picture reference before
|
|
21
|
+
this.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Physical source position and timecode per event** — `srcPos`, `srcTcFrame`,
|
|
26
|
+
`srcTc`, `srcTcFps`, `srcTcDrop`. The chase sums `start` down the mob chain and
|
|
27
|
+
reads the nearest mob's timecode slot, so a consumer that links camera originals
|
|
28
|
+
can place `sourceTc − fileStartTc` instead of a fragment-relative number. Emitted
|
|
29
|
+
only when actually read: a chain that adds nothing emits nothing rather than
|
|
30
|
+
restating `srcIn`, and per-sequence `sourcePositionCoverage` counters let a
|
|
31
|
+
consumer distinguish "this AAF carries none" from "this probe is too old to emit
|
|
32
|
+
it." `srcIn`/`srcOut` are unchanged — this is additive.
|
|
33
|
+
|
|
34
|
+
Verified on the fixture: 869/878 events carry `srcPos`, 876 carry source timecode,
|
|
35
|
+
and the take used thirteen times separates into thirteen distinct positions.
|
|
36
|
+
Frame-exact against an independent witness — at the frame the turnover's own
|
|
37
|
+
picture reference burned 21:19:28:21, the probe says 21:19:28:21.
|
|
38
|
+
|
|
39
|
+
## What's New in v2.77.0
|
|
40
|
+
|
|
41
|
+
Folder addressing fails loud instead of quietly answering about whichever bin the
|
|
42
|
+
UI happens to have open. Contributed by [@billcarroll](https://github.com/billcarroll)
|
|
43
|
+
in [#116](https://github.com/samuelgursky/davinci-resolve-mcp/pull/116).
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **An unresolvable folder address no longer falls back to the current bin.**
|
|
48
|
+
`media_pool add_subfolder` and `media_pool get_timeline_mattes` resolved their
|
|
49
|
+
folder argument with `_navigate_folder(...) or fallback`, so a typo'd path was
|
|
50
|
+
dropped and the action proceeded against the current bin (or root) with a
|
|
51
|
+
`success` envelope. For a read that is a wrong answer indistinguishable from a
|
|
52
|
+
right one; for `add_subfolder` it creates the folder wherever the UI happens to
|
|
53
|
+
be pointed. All three sites now share one resolver that returns
|
|
54
|
+
`FOLDER_NOT_FOUND` / `invalid_input` when a supplied address does not resolve,
|
|
55
|
+
with remediation naming `get_subfolders`.
|
|
56
|
+
- **A bad folder path came back marked retryable.** The `folder` tool did already
|
|
57
|
+
error on an unresolvable `path`, but with a bare message, so the envelope
|
|
58
|
+
defaulted to `resolve_api_failed` and told the caller to retry an address that
|
|
59
|
+
would never resolve. It is now `invalid_input`, non-retryable — the caller's to
|
|
60
|
+
fix.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **`folder_id` is accepted as a folder address**, alongside `path`, on the
|
|
65
|
+
`folder` tool, `media_pool add_subfolder`, and `media_pool get_timeline_mattes`.
|
|
66
|
+
`get_subfolders` hands out ids, and having no way to spend them is what invited
|
|
67
|
+
agents to guess a `folder_id` argument that no action read — which was silently
|
|
68
|
+
dropped, returned the current bin's contents with `success`, and made the tools
|
|
69
|
+
look like they ignored their arguments. Omitting every address still means what
|
|
70
|
+
it did before: the current folder for the `folder` tool, the root folder for the
|
|
71
|
+
two `media_pool` actions.
|
|
72
|
+
|
|
73
|
+
### Known limitation
|
|
74
|
+
|
|
75
|
+
Only `path`/`folder_path`/`folderPath` and `folder_id`/`folderId` are recognised
|
|
76
|
+
as addresses. Any other invented key (`id`, `bin`, `folderName`) is still dropped,
|
|
77
|
+
and the action still answers about its default folder with `success`. This release
|
|
78
|
+
narrows the silent-wrong-folder class to a known key set rather than closing it;
|
|
79
|
+
closing it needs unknown-parameter rejection at the dispatch layer.
|
|
80
|
+
|
|
5
81
|
## What's New in v2.76.0
|
|
6
82
|
|
|
7
83
|
Three AAF conform-fidelity fixes found by placing a full 83-minute Avid turnover and
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
package/docs/SKILL.md
CHANGED
|
@@ -617,9 +617,25 @@ switching, and flattening remain Resolve UI workflows; see
|
|
|
617
617
|
Note: `folder path` arguments use slash notation like `"Master/SubFolder"`.
|
|
618
618
|
`"Master"` or `"/"` refers to the root folder.
|
|
619
619
|
|
|
620
|
+
Address a folder either by `path` or by `folder_id` — the id `get_subfolders`
|
|
621
|
+
returns for each entry (v2.77.0+; the same pair works for `media_pool
|
|
622
|
+
add_subfolder` via `parent_path`/`folder_id` and for `media_pool
|
|
623
|
+
get_timeline_mattes` via `folder_path`/`folder_id`). Omit both to get the
|
|
624
|
+
action's default: the current folder for the `folder` tool, the root folder for
|
|
625
|
+
those two `media_pool` actions. An address that is supplied but does not resolve
|
|
626
|
+
is a `FOLDER_NOT_FOUND` / `invalid_input` error — it never quietly falls back to
|
|
627
|
+
the current bin.
|
|
628
|
+
|
|
629
|
+
That fallback is what these tools used to do, so treat a pre-v2.77.0 server as
|
|
630
|
+
unable to tell you when it answered about the wrong folder. Note also that only
|
|
631
|
+
`path`/`folder_path`/`folderPath` and `folder_id`/`folderId` are recognised as
|
|
632
|
+
addresses: any other key you invent (`id`, `bin`, `folderName`) is still
|
|
633
|
+
silently dropped, and the action still answers about its default folder with
|
|
634
|
+
`success`. Use the documented names.
|
|
635
|
+
|
|
620
636
|
**`folder`** — Operations on a specific Media Pool folder.
|
|
621
637
|
|
|
622
|
-
Key actions: `get_clips(path?)`, `get_subfolders(path?)`, `export(path?, export_path)`,
|
|
638
|
+
Key actions: `get_clips(path?|folder_id?)`, `get_subfolders(path?|folder_id?)`, `export(path?, export_path)`,
|
|
623
639
|
`transcribe_audio(path?, use_speaker_detection?)`, `clear_transcription(path?)`,
|
|
624
640
|
`perform_audio_classification(path?)`, `analyze_for_intellisearch(path?, identify_faces?, is_better_mode?)`,
|
|
625
641
|
`analyze_for_slate(path?, marker_color?)`, `remove_motion_blur(path?, deblur_option?)` (Resolve 21+;
|
package/install.py
CHANGED
|
@@ -36,7 +36,7 @@ from src.utils.update_check import (
|
|
|
36
36
|
|
|
37
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
VERSION = "2.
|
|
39
|
+
VERSION = "2.78.0"
|
|
40
40
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
41
41
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
42
42
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -160,6 +160,133 @@ def _source_name(clip):
|
|
|
160
160
|
return _usable_name(clip) or "UNKNOWN"
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
# ── Physical source position + timecode (the "which frames" question) ─────────
|
|
164
|
+
#
|
|
165
|
+
# MEASURED 2026-08-05 on a consolidated Avid turnover: a SourceClip's own
|
|
166
|
+
# `start` is the offset into whatever mob it references DIRECTLY, and for
|
|
167
|
+
# consolidated media that is a per-cut fragment with ~40-frame handles — so 774
|
|
168
|
+
# of 878 events reported srcIn <= 45, and takes used several times all reported
|
|
169
|
+
# the SAME srcIn with different lengths. Two different cuts of one take cannot
|
|
170
|
+
# both begin at frame 42 of that take; those 42s were handles on separate
|
|
171
|
+
# fragments.
|
|
172
|
+
#
|
|
173
|
+
# The real position is the SUM of the `start` offsets down the mob chain, and
|
|
174
|
+
# the anchor that survives relinking to different media is the physical source
|
|
175
|
+
# TIMECODE: the referenced mob's timecode start plus that accumulated offset.
|
|
176
|
+
# A consumer that links camera originals can then place
|
|
177
|
+
# `sourceTc - fileStartTc` instead of a fragment-relative number that merely
|
|
178
|
+
# FITS inside the file.
|
|
179
|
+
#
|
|
180
|
+
# Emitted per event as srcPos / srcTcFrame / srcTc / srcTcFps / srcTcDrop, and
|
|
181
|
+
# only when actually found — a consumer must be able to tell "this AAF carries
|
|
182
|
+
# no source timecode" from "this probe is too old to emit it", which is what
|
|
183
|
+
# the per-sequence sourceTimecodeCoverage counters are for.
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _mob_timecode(mob, _cache={}):
|
|
187
|
+
"""(startFrame, rate, drop) from a mob's timecode slot, or None."""
|
|
188
|
+
try:
|
|
189
|
+
key = str(getattr(mob, "mob_id", "") or id(mob))
|
|
190
|
+
except Exception:
|
|
191
|
+
key = str(id(mob))
|
|
192
|
+
if key in _cache:
|
|
193
|
+
return _cache[key]
|
|
194
|
+
found = None
|
|
195
|
+
for slot in getattr(mob, "slots", None) or []:
|
|
196
|
+
try:
|
|
197
|
+
if _slot_media_kind(slot) != _TIMECODE_KIND:
|
|
198
|
+
continue
|
|
199
|
+
except Exception:
|
|
200
|
+
continue
|
|
201
|
+
tc = _timecode_component(getattr(slot, "segment", None))
|
|
202
|
+
if tc is None:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
start = int(getattr(tc, "start"))
|
|
206
|
+
rate = int(round(float(getattr(tc, "fps", 0) or 0)))
|
|
207
|
+
drop = bool(getattr(tc, "drop", False))
|
|
208
|
+
except Exception:
|
|
209
|
+
continue
|
|
210
|
+
if rate > 0 and start >= 0:
|
|
211
|
+
found = (start, rate, drop)
|
|
212
|
+
break
|
|
213
|
+
_cache[key] = found
|
|
214
|
+
return found
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _chase_source_position(clip):
|
|
218
|
+
"""Physical source position + timecode for a SourceClip's FIRST frame.
|
|
219
|
+
|
|
220
|
+
Walks clip -> referenced mob -> that mob's own SourceClip, accumulating each
|
|
221
|
+
hop's `start`. Returns (position, timecode_or_None) where `timecode` is
|
|
222
|
+
(tc_start, rate, drop, position_at_that_mob) for the NEAREST mob carrying a
|
|
223
|
+
timecode slot. Bounded and cycle-guarded like _source_name.
|
|
224
|
+
"""
|
|
225
|
+
current = clip
|
|
226
|
+
position = 0
|
|
227
|
+
timecode = None
|
|
228
|
+
seen = set()
|
|
229
|
+
for _ in range(_MAX_MOB_CHASE):
|
|
230
|
+
try:
|
|
231
|
+
position += int(getattr(current, "start", 0) or 0)
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
try:
|
|
235
|
+
mob = getattr(current, "mob", None)
|
|
236
|
+
except Exception:
|
|
237
|
+
mob = None
|
|
238
|
+
if mob is None:
|
|
239
|
+
break
|
|
240
|
+
try:
|
|
241
|
+
key = str(getattr(mob, "mob_id", "") or id(mob))
|
|
242
|
+
except Exception:
|
|
243
|
+
key = str(id(mob))
|
|
244
|
+
if key in seen:
|
|
245
|
+
break # reference cycle — stop rather than spin
|
|
246
|
+
seen.add(key)
|
|
247
|
+
if timecode is None:
|
|
248
|
+
tc = _mob_timecode(mob)
|
|
249
|
+
if tc is not None:
|
|
250
|
+
timecode = (tc[0], tc[1], tc[2], position)
|
|
251
|
+
nxt = None
|
|
252
|
+
for slot in getattr(mob, "slots", None) or []:
|
|
253
|
+
nxt = _find_source_clip(getattr(slot, "segment", None))
|
|
254
|
+
if nxt is not None:
|
|
255
|
+
break
|
|
256
|
+
if nxt is None:
|
|
257
|
+
break
|
|
258
|
+
current = nxt
|
|
259
|
+
return position, timecode
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _source_position_fields(clip):
|
|
263
|
+
"""The srcPos/srcTc* keys for an event. Empty dict when nothing is knowable."""
|
|
264
|
+
try:
|
|
265
|
+
position, timecode = _chase_source_position(clip)
|
|
266
|
+
except Exception:
|
|
267
|
+
return {}
|
|
268
|
+
fields = {}
|
|
269
|
+
try:
|
|
270
|
+
own_start = int(getattr(clip, "start", 0) or 0)
|
|
271
|
+
except Exception:
|
|
272
|
+
own_start = 0
|
|
273
|
+
if position != own_start:
|
|
274
|
+
# Only meaningful when the chain actually went deeper than the clip's
|
|
275
|
+
# own reference — otherwise srcPos would just restate srcIn.
|
|
276
|
+
fields["srcPos"] = position
|
|
277
|
+
if timecode is not None:
|
|
278
|
+
tc_start, rate, drop, at = timecode
|
|
279
|
+
fields.update(
|
|
280
|
+
{
|
|
281
|
+
"srcTcFrame": tc_start + at,
|
|
282
|
+
"srcTc": _frames_to_timecode(tc_start + at, rate, drop),
|
|
283
|
+
"srcTcFps": rate,
|
|
284
|
+
"srcTcDrop": drop,
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
return fields
|
|
288
|
+
|
|
289
|
+
|
|
163
290
|
def _emit_source_clip(clip, *, index, track, rec, fps, transition=None):
|
|
164
291
|
"""Turn a SourceClip into a normalized event. Returns (event, length)."""
|
|
165
292
|
try:
|
|
@@ -183,6 +310,7 @@ def _emit_source_clip(clip, *, index, track, rec, fps, transition=None):
|
|
|
183
310
|
"transition": transition,
|
|
184
311
|
"fps": fps,
|
|
185
312
|
}
|
|
313
|
+
event.update(_source_position_fields(clip))
|
|
186
314
|
return event, length
|
|
187
315
|
|
|
188
316
|
|
|
@@ -884,6 +1012,15 @@ def probe(path):
|
|
|
884
1012
|
"name": str(name) if name else f"Sequence {len(sequences) + 1}",
|
|
885
1013
|
"eventCount": len(events),
|
|
886
1014
|
**_sequence_start_timecode(mob, edit_fps),
|
|
1015
|
+
# How much of this sequence carries a physical source position
|
|
1016
|
+
# / timecode. Always present, so a consumer can tell "this AAF
|
|
1017
|
+
# has none" (zeros) from "this probe is too old to emit it"
|
|
1018
|
+
# (key absent) — the same reasoning as _NO_START_TIMECODE.
|
|
1019
|
+
"sourcePositionCoverage": {
|
|
1020
|
+
"events": len(events),
|
|
1021
|
+
"withSourcePosition": sum(1 for e in events if "srcPos" in e),
|
|
1022
|
+
"withSourceTimecode": sum(1 for e in events if "srcTcFrame" in e),
|
|
1023
|
+
},
|
|
887
1024
|
# Component classes we could not model, by name+count. Empty {} means
|
|
888
1025
|
# a structurally complete read; non-empty means events are INCOMPLETE.
|
|
889
1026
|
"unhandled": dict(sorted(state["unhandled"].items())),
|
package/src/granular/common.py
CHANGED
|
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
85
85
|
handlers=[logging.StreamHandler()],
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
VERSION = "2.
|
|
88
|
+
VERSION = "2.78.0"
|
|
89
89
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
90
90
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
91
91
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.78.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -2579,6 +2579,54 @@ def _find_clip_with_parent(folder, clip_id, _parent=None):
|
|
|
2579
2579
|
return found_clip, found_parent
|
|
2580
2580
|
return None, None
|
|
2581
2581
|
|
|
2582
|
+
def _find_folder_by_id(folder, folder_id):
|
|
2583
|
+
if folder.GetUniqueId() == folder_id:
|
|
2584
|
+
return folder
|
|
2585
|
+
for sub in (folder.GetSubFolderList() or []):
|
|
2586
|
+
found = _find_folder_by_id(sub, folder_id)
|
|
2587
|
+
if found:
|
|
2588
|
+
return found
|
|
2589
|
+
return None
|
|
2590
|
+
|
|
2591
|
+
|
|
2592
|
+
def _folder_from_params(mp, p, *path_keys, no_address="current"):
|
|
2593
|
+
"""Resolve the folder an action was aimed at. Returns (folder, error).
|
|
2594
|
+
|
|
2595
|
+
Naming no folder falls back to the action's documented default (see
|
|
2596
|
+
`no_address`). What must never happen is the middle case: an addressing argument was
|
|
2597
|
+
supplied, did not resolve, and the action answered about the current bin
|
|
2598
|
+
anyway. That reports success for a different question than the caller asked,
|
|
2599
|
+
and it is indistinguishable from the tool working. It cost one session an
|
|
2600
|
+
afternoon: `folder_id` is not a key any action read, so it was dropped, the
|
|
2601
|
+
current bin's clips came back, and the tools were written off as broken.
|
|
2602
|
+
|
|
2603
|
+
So: unresolvable-but-supplied is an error, and the id that `get_subfolders`
|
|
2604
|
+
hands out is accepted as an address, since being given an id and having no
|
|
2605
|
+
way to use it is what invited the guess.
|
|
2606
|
+
|
|
2607
|
+
`no_address` preserves each action's historical no-argument default: "current"
|
|
2608
|
+
for the folder tool, "root" for the media_pool actions (whose old code hit
|
|
2609
|
+
`_navigate_folder(mp, "")`, which returns root). This fix must not also
|
|
2610
|
+
change what omitting the address means.
|
|
2611
|
+
"""
|
|
2612
|
+
remediation = ("List folders with folder get_subfolders (walking down from "
|
|
2613
|
+
"path=\"Master\") and address by exact path or folder_id.")
|
|
2614
|
+
path = _first_param(p, *path_keys)
|
|
2615
|
+
if path:
|
|
2616
|
+
f = _navigate_folder(mp, path)
|
|
2617
|
+
return (f, None) if f else (None, _err(
|
|
2618
|
+
f"Folder not found: {path}", code="FOLDER_NOT_FOUND",
|
|
2619
|
+
category="invalid_input", remediation=remediation))
|
|
2620
|
+
folder_id = _first_param(p, "folder_id", "folderId")
|
|
2621
|
+
if folder_id:
|
|
2622
|
+
f = _find_folder_by_id(mp.GetRootFolder(), str(folder_id))
|
|
2623
|
+
return (f, None) if f else (None, _err(
|
|
2624
|
+
f"Folder not found: {folder_id}", code="FOLDER_NOT_FOUND",
|
|
2625
|
+
category="invalid_input", remediation=remediation))
|
|
2626
|
+
f = mp.GetRootFolder() if no_address == "root" else mp.GetCurrentFolder()
|
|
2627
|
+
return (f, None) if f else (None, _err("No current Media Pool folder"))
|
|
2628
|
+
|
|
2629
|
+
|
|
2582
2630
|
def _navigate_folder(mp, path):
|
|
2583
2631
|
root = mp.GetRootFolder()
|
|
2584
2632
|
if not path or path in ("Master", "/", ""):
|
|
@@ -16666,7 +16714,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
16666
16714
|
return _err(f"Folder not found: {p.get('path')}")
|
|
16667
16715
|
return {"success": bool(mp.SetCurrentFolder(f))}
|
|
16668
16716
|
elif action == "add_subfolder":
|
|
16669
|
-
parent =
|
|
16717
|
+
parent, folder_err = _folder_from_params(mp, p, "parent_path", "parentPath", no_address="root")
|
|
16718
|
+
if folder_err:
|
|
16719
|
+
return folder_err
|
|
16670
16720
|
f = mp.AddSubFolder(parent, p["name"])
|
|
16671
16721
|
return _ok(name=f.GetName(), id=f.GetUniqueId()) if f else _err("Failed to create subfolder")
|
|
16672
16722
|
elif action == "delete_folders":
|
|
@@ -16973,7 +17023,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
16973
17023
|
clip = _find_clip(root, p["clip_id"])
|
|
16974
17024
|
return {"mattes": mp.GetClipMatteList(clip)} if clip else _err("Clip not found")
|
|
16975
17025
|
elif action == "get_timeline_mattes":
|
|
16976
|
-
folder =
|
|
17026
|
+
folder, folder_err = _folder_from_params(mp, p, "folder_path", "folderPath", no_address="root")
|
|
17027
|
+
if folder_err:
|
|
17028
|
+
return folder_err
|
|
16977
17029
|
result = mp.GetTimelineMatteList(folder)
|
|
16978
17030
|
return {"mattes": len(result) if result else 0}
|
|
16979
17031
|
elif action == "delete_clip_mattes":
|
|
@@ -17037,6 +17089,11 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
17037
17089
|
def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
17038
17090
|
"""Operations on Media Pool folders.
|
|
17039
17091
|
|
|
17092
|
+
Address a folder with `path` ("Master/SubFolder") or with `folder_id` (the id
|
|
17093
|
+
get_subfolders returns). Omit both for the current folder. An address that is
|
|
17094
|
+
supplied but does not resolve is an error — it never falls back to the current
|
|
17095
|
+
folder.
|
|
17096
|
+
|
|
17040
17097
|
Actions:
|
|
17041
17098
|
get_clips(path?) -> {clips} — path like "Master/SubFolder", omit for current
|
|
17042
17099
|
get_name(path?) -> {name}
|
|
@@ -17057,10 +17114,9 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
17057
17114
|
if err:
|
|
17058
17115
|
return err
|
|
17059
17116
|
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
return _err(f"Folder not found: {folder_path}")
|
|
17117
|
+
f, folder_err = _folder_from_params(mp, p, "path", "folder_path", "folderPath")
|
|
17118
|
+
if folder_err:
|
|
17119
|
+
return folder_err
|
|
17064
17120
|
|
|
17065
17121
|
if action == "get_clips":
|
|
17066
17122
|
clips = f.GetClipList() or []
|