davinci-resolve-mcp 4.8.1 → 4.8.3
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 +131 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +8 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +126 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,137 @@
|
|
|
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 v4.8.3 — nested folder ids resolve for delete and move
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`media_pool.delete_folders` and `media_pool.move_folders` resolve
|
|
10
|
+
`folder_ids` anywhere in the Media Pool tree.** Both actions scanned only the
|
|
11
|
+
root folder's direct children, so an id belonging to any nested folder came
|
|
12
|
+
back `{"error":{"message":"No folders found"}}` — even though the caller was
|
|
13
|
+
holding the id `GetUniqueId()` had just handed them, and `mp.DeleteFolders()`
|
|
14
|
+
on the resolved object worked fine. Both now use the existing recursive
|
|
15
|
+
`_find_folder_by_id` walk through a shared `_folders_from_ids` resolver.
|
|
16
|
+
- **An unresolved id fails the call instead of being dropped** — the same
|
|
17
|
+
all-or-nothing rule 4.8.2 applied to the clip actions. Previously only an
|
|
18
|
+
all-empty result errored, so a mixed batch deleted or moved the subset that
|
|
19
|
+
happened to resolve and answered `{"success": true}`; `move_folders` did not
|
|
20
|
+
even stop at an empty result and answered `success: true` after
|
|
21
|
+
`MoveFolders([], target)`. Unresolved ids now return `FOLDER_NOT_FOUND` /
|
|
22
|
+
`invalid_input` naming both the unresolved and the resolved ids in `state`,
|
|
23
|
+
with nothing deleted or moved.
|
|
24
|
+
- An empty or non-list `folder_ids` returns `INVALID_FOLDER_IDS` instead of
|
|
25
|
+
iterating a bare string character by character; a missing one keeps the
|
|
26
|
+
surface-wide `MISSING_FOLDER_IDS`. The root (Master) folder — newly reachable
|
|
27
|
+
now that the search is recursive — is refused with
|
|
28
|
+
`ROOT_FOLDER_NOT_ELIGIBLE` before Resolve sees it. `move_folders`' "Target
|
|
29
|
+
folder not found" now carries `FOLDER_NOT_FOUND` / `invalid_input` too.
|
|
30
|
+
|
|
31
|
+
### Validation
|
|
32
|
+
|
|
33
|
+
- New `tests/test_media_pool_folder_ids.py` pins both actions against a
|
|
34
|
+
three-level-deep folder, the partial-batch case, and the boundary inputs.
|
|
35
|
+
Against the 4.8.2 code 10 of its 11 tests fail; all 11 pass after.
|
|
36
|
+
- Offline suite: 3,732 tests run, 84 skipped, 11 errors — the same environment
|
|
37
|
+
gaps as in 4.8.2 (`numpy` and `requests` absent from the venv, `node_modules`
|
|
38
|
+
not installed). None touch `media_pool`. Drift guards, api-parity,
|
|
39
|
+
api-limitations and read/write symmetry clean; tool counts unchanged.
|
|
40
|
+
- Live reproduction on Resolve Studio 21.1.0.14 (reporter): deleting
|
|
41
|
+
`Master/OUTDOORS/1_FOOTAGE/wetransfer_dscf1065-mov_2022-01-31_1207` by its
|
|
42
|
+
`GetUniqueId()` returned `No folders found`, while `mp.DeleteFolders()` with
|
|
43
|
+
the recursively resolved folder object succeeded.
|
|
44
|
+
- **Live-validated on Resolve Studio 21.1.0.14** with the new
|
|
45
|
+
`tests/live_nested_folder_ids_check.py`
|
|
46
|
+
(`venv/bin/python tests/live_nested_folder_ids_check.py`) against a real project
|
|
47
|
+
of 144 folders and 2,600 clips, probing a folder seven levels below Master.
|
|
48
|
+
`delete_folders` on its id reached `confirmation_required` naming exactly that
|
|
49
|
+
folder; `move_folders` resolved it and its current parent as the target;
|
|
50
|
+
partial and all-missing batches came back `FOLDER_NOT_FOUND` for both actions,
|
|
51
|
+
Master `ROOT_FOLDER_NOT_ELIGIBLE`, bare string and empty list
|
|
52
|
+
`INVALID_FOLDER_IDS`, no key `MISSING_FOLDER_IDS`. The check is strictly
|
|
53
|
+
non-destructive, with the same tripwire as the 4.8.2 clip check: the MediaPool
|
|
54
|
+
it hands the actions forwards only `GetRootFolder` / `GetCurrentFolder` and
|
|
55
|
+
intercepts every other method, so `DeleteFolders` / `MoveFolders` never reach
|
|
56
|
+
Resolve whatever the code does; token gating is forced on and no token is
|
|
57
|
+
passed; move probes aim at the folder's current parent; the destructive hook's
|
|
58
|
+
analysis-root writes are disabled and its logs go to a temp directory. Every
|
|
59
|
+
folder's parent and every clip's folder read back identical after the run.
|
|
60
|
+
- The same live check against the 4.8.2 code fails with 14 findings and
|
|
61
|
+
reproduces the reported bug: the depth-7 id came back `No folders found` from
|
|
62
|
+
`delete_folders`, and the tripwire intercepted five `MoveFolders` calls with an
|
|
63
|
+
empty folder list, each answered as a plain result rather than an error. The
|
|
64
|
+
pool was unchanged after that run too.
|
|
65
|
+
|
|
66
|
+
## What's New in v4.8.2 — a clip id that resolves to nothing fails the whole clip batch
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **`media_pool` `delete_clips`, `move_clips`, `relink` and `unlink` no longer act on
|
|
71
|
+
the part of a batch that happened to resolve.** All four resolved `clip_ids` with
|
|
72
|
+
`[_find_clip(root, cid) for cid in ...]` and then dropped every miss, so Resolve was
|
|
73
|
+
handed whatever was left. `delete_clips` errored only when *every* id missed
|
|
74
|
+
(`"No clips found"`); a mixed batch deleted the subset that resolved and answered
|
|
75
|
+
`{"success": true}`, and with confirm tokens on, the preview and the token covered
|
|
76
|
+
that subset too. `move_clips`, `relink` and `unlink` did not check for an empty
|
|
77
|
+
result at all: an all-missing batch reached `MoveClips([], target)` /
|
|
78
|
+
`RelinkClips([], path)` / `UnlinkClips([])` and reported whatever Resolve's bool
|
|
79
|
+
said. The caller could not tell a partial change from a whole one.
|
|
80
|
+
- A new `_clips_from_ids` resolver fails the call before anything reaches Resolve
|
|
81
|
+
(and before a confirm token is issued) with `CLIP_NOT_FOUND` /
|
|
82
|
+
`invalid_input`, naming the ids in `error.state.unresolved_clip_ids` and
|
|
83
|
+
`error.state.resolved_clip_ids`. Nothing is deleted, moved, relinked or unlinked.
|
|
84
|
+
- An empty or non-list `clip_ids` returns `INVALID_CLIP_IDS` instead of iterating a
|
|
85
|
+
bare string character by character; a missing one keeps the surface-wide
|
|
86
|
+
`MISSING_CLIP_IDS`.
|
|
87
|
+
|
|
88
|
+
### Not changed
|
|
89
|
+
|
|
90
|
+
- The same drop-the-misses pattern remains in four other `media_pool` actions, which
|
|
91
|
+
this release does not touch: `create_timeline_from_clips` (simple `clip_ids` mode)
|
|
92
|
+
builds the timeline from the subset, `append_to_timeline` (legacy `clip_ids` mode)
|
|
93
|
+
appends the subset and verifies against the resolved count, `export_metadata`
|
|
94
|
+
exports the subset — and with every id missing calls `ExportMetadata(path, [])`,
|
|
95
|
+
whose behaviour on an empty list is not measured — and `auto_sync_audio` syncs the
|
|
96
|
+
subset. The granular server's `delete_media_pool_clips` and `move_clips_to_folder`
|
|
97
|
+
have the same partial-batch behaviour (they do report a count). The `safe_*` /
|
|
98
|
+
`organize_clips` family reports its misses in `missing` rather than dropping them
|
|
99
|
+
silently.
|
|
100
|
+
|
|
101
|
+
### Validation
|
|
102
|
+
|
|
103
|
+
- New `tests/test_media_pool_clip_ids.py` pins all four actions against a fake
|
|
104
|
+
Media Pool that records every mutation call: a partial batch, an all-missing
|
|
105
|
+
batch, a partial `delete_clips` under confirm-token gating (no token is issued),
|
|
106
|
+
and the missing / empty / bare-string `clip_ids` shapes, plus the happy path with
|
|
107
|
+
a clip two folders deep. Against the pre-fix code 5 of its 10 tests fail
|
|
108
|
+
(17 subtests — every partial and all-missing case, the token case and both
|
|
109
|
+
shape cases); the 5 that pin preserved behaviour pass on both. All 10 pass after.
|
|
110
|
+
- Offline suite: 3,721 tests run, 84 skipped, 11 errors — all environment gaps on the
|
|
111
|
+
machine that ran it (`numpy` and `requests` absent from the venv, so five test
|
|
112
|
+
modules fail to import and one LUT test cannot do arithmetic; `node_modules` not
|
|
113
|
+
installed, so five `test_offline_fallback` cases cannot load `jszip`). None touch
|
|
114
|
+
`media_pool`. Drift guards, api-parity, api-limitations and read/write symmetry
|
|
115
|
+
clean; `test_static_undefined_names` skipped because pyflakes is not installed.
|
|
116
|
+
- **Live-validated on Resolve Studio 21.1.0.14** with the new
|
|
117
|
+
`tests/live_clip_ids_check.py` (`venv/bin/python tests/live_clip_ids_check.py`)
|
|
118
|
+
against a real project of 144 folders and 2,600 clips, probing a clip seven
|
|
119
|
+
folders deep. Partial and all-missing batches came back `CLIP_NOT_FOUND` for all
|
|
120
|
+
four actions, with no confirm token for the partial delete; bare string and
|
|
121
|
+
empty list → `INVALID_CLIP_IDS`, no key → `MISSING_CLIP_IDS`; a whole batch still
|
|
122
|
+
resolved to the real clip. The check is strictly non-destructive: the MediaPool it
|
|
123
|
+
hands the actions forwards only `GetRootFolder` / `GetCurrentFolder` and
|
|
124
|
+
intercepts every other method, so `DeleteClips` / `MoveClips` / `RelinkClips` /
|
|
125
|
+
`UnlinkClips` never reach Resolve whatever the code does; token gating is forced
|
|
126
|
+
on and no token is passed; the destructive hook's analysis-root writes are
|
|
127
|
+
disabled and its logs go to a temp directory. Every clip's folder, every folder's
|
|
128
|
+
parent and the probe clip's File Path read back identical after the run.
|
|
129
|
+
- The same live check against the pre-fix code fails with 18 findings: the tripwire
|
|
130
|
+
intercepted 10 MediaPool mutations the old code sent — `MoveClips`,
|
|
131
|
+
`RelinkClips` and `UnlinkClips` with just the probe clip for the partial batches,
|
|
132
|
+
the same three with an empty list for the all-missing batches, `UnlinkClips([])`
|
|
133
|
+
for `clip_ids: []` — and the partial `delete_clips` was issued a confirm token.
|
|
134
|
+
The pool was unchanged after that run too.
|
|
135
|
+
|
|
5
136
|
## What's New in v4.8.1 — text modifiers (Follower) attach to TextPlus inputs
|
|
6
137
|
|
|
7
138
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v4.8.
|
|
15
|
+
> 本翻译对应 v4.8.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/docs/SKILL.md
CHANGED
|
@@ -922,6 +922,14 @@ Key actions: `get_root_folder`, `get_current_folder`, `set_current_folder(path)`
|
|
|
922
922
|
`setup_multicam_timeline(name, clip_ids|angles, sync_mode?, include_audio?, dry_run?)`,
|
|
923
923
|
`get_selected`, `set_selected(clip_id)`, `export_metadata(path, clip_ids?)`
|
|
924
924
|
|
|
925
|
+
`delete_clips`, `move_clips`, `relink` and `unlink` are all-or-nothing: if any id
|
|
926
|
+
in `clip_ids` matches no clip, the call fails with `CLIP_NOT_FOUND` (the
|
|
927
|
+
unresolved and resolved ids are in `error.state`) and no clip is changed. Drop the
|
|
928
|
+
stale ids and retry; do not read a partial batch as done. `delete_folders(folder_ids)`
|
|
929
|
+
and `move_folders(folder_ids, target_path)` work the same way with
|
|
930
|
+
`FOLDER_NOT_FOUND`; they resolve `folder_ids` at any depth (pass the ids
|
|
931
|
+
`folder get_subfolders` returns) and refuse the Master folder itself.
|
|
932
|
+
|
|
925
933
|
Media Pool / Ingest kernel actions (v2.8.0+) add safer agent-facing workflows:
|
|
926
934
|
`ingest_capabilities`, `probe_media_pool`, `probe_ingest_item`,
|
|
927
935
|
`safe_import_media`, `safe_import_sequence`, `safe_import_folder`,
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.8.
|
|
40
|
+
VERSION = "4.8.3"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
93
93
|
handlers=[logging.StreamHandler()],
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
VERSION = "4.8.
|
|
96
|
+
VERSION = "4.8.3"
|
|
97
97
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
98
98
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
99
99
|
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 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.8.
|
|
14
|
+
VERSION = "4.8.3"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -2912,6 +2912,45 @@ def _find_clip(folder, clip_id):
|
|
|
2912
2912
|
return None
|
|
2913
2913
|
|
|
2914
2914
|
|
|
2915
|
+
def _clips_from_ids(root, clip_ids, *, verb):
|
|
2916
|
+
"""Resolve Media Pool clip unique IDs to clip objects. Returns (clips, error).
|
|
2917
|
+
|
|
2918
|
+
Unresolved ids abort the call instead of being dropped. The raw clip actions
|
|
2919
|
+
used to filter misses out and hand Resolve whatever was left, so a mixed batch
|
|
2920
|
+
deleted, moved, relinked or unlinked the subset that happened to resolve and
|
|
2921
|
+
answered plain {"success": true} — indistinguishable from every id having
|
|
2922
|
+
resolved. move_clips/relink/unlink did not even stop at an empty result:
|
|
2923
|
+
MoveClips([], target) moved nothing and still reported success.
|
|
2924
|
+
|
|
2925
|
+
A non-list is refused rather than iterated: a bare string used to become one
|
|
2926
|
+
id per character, each matching nothing and each silently dropped.
|
|
2927
|
+
"""
|
|
2928
|
+
if not isinstance(clip_ids, list) or not clip_ids:
|
|
2929
|
+
return None, _err(
|
|
2930
|
+
"clip_ids must be a non-empty list of Media Pool clip unique IDs",
|
|
2931
|
+
code="INVALID_CLIP_IDS", category="invalid_input",
|
|
2932
|
+
remediation="Pass clip_ids as a list, e.g. params={\"clip_ids\": [\"<id>\"]}.")
|
|
2933
|
+
clips, resolved, unresolved = [], [], []
|
|
2934
|
+
for cid in clip_ids:
|
|
2935
|
+
cid = str(cid)
|
|
2936
|
+
found = _find_clip(root, cid)
|
|
2937
|
+
if found is None:
|
|
2938
|
+
unresolved.append(cid)
|
|
2939
|
+
else:
|
|
2940
|
+
clips.append(found)
|
|
2941
|
+
resolved.append(cid)
|
|
2942
|
+
if unresolved:
|
|
2943
|
+
return None, _err(
|
|
2944
|
+
f"Clip(s) not found: {', '.join(unresolved)}",
|
|
2945
|
+
code="CLIP_NOT_FOUND", category="invalid_input",
|
|
2946
|
+
reason=(f"{len(unresolved)} of {len(clip_ids)} clip_ids matched no clip "
|
|
2947
|
+
f"anywhere in the Media Pool; nothing was {verb}."),
|
|
2948
|
+
remediation=("List clips with folder get_clips (or media_pool get_selected) "
|
|
2949
|
+
"and pass only ids that still exist."),
|
|
2950
|
+
state={"unresolved_clip_ids": unresolved, "resolved_clip_ids": resolved})
|
|
2951
|
+
return clips, None
|
|
2952
|
+
|
|
2953
|
+
|
|
2915
2954
|
def _find_clip_with_parent(folder, clip_id, _parent=None):
|
|
2916
2955
|
"""Return (clip, parent_folder) for clip_id, searching recursively.
|
|
2917
2956
|
|
|
@@ -2928,6 +2967,13 @@ def _find_clip_with_parent(folder, clip_id, _parent=None):
|
|
|
2928
2967
|
return found_clip, found_parent
|
|
2929
2968
|
return None, None
|
|
2930
2969
|
|
|
2970
|
+
|
|
2971
|
+
_FOLDER_ID_REMEDIATION = (
|
|
2972
|
+
"List folders with folder get_subfolders (walking down from "
|
|
2973
|
+
"path=\"Master\") and address by exact path or folder_id."
|
|
2974
|
+
)
|
|
2975
|
+
|
|
2976
|
+
|
|
2931
2977
|
def _find_folder_by_id(folder, folder_id):
|
|
2932
2978
|
if folder.GetUniqueId() == folder_id:
|
|
2933
2979
|
return folder
|
|
@@ -2938,6 +2984,57 @@ def _find_folder_by_id(folder, folder_id):
|
|
|
2938
2984
|
return None
|
|
2939
2985
|
|
|
2940
2986
|
|
|
2987
|
+
def _folders_from_ids(root, folder_ids, *, verb):
|
|
2988
|
+
"""Resolve Media Pool folder unique IDs to folder objects. Returns (folders, error).
|
|
2989
|
+
|
|
2990
|
+
Searches the WHOLE tree. The ids callers hold come from GetUniqueId() /
|
|
2991
|
+
folder.get_subfolders, which hand them out at every depth, so the shallow
|
|
2992
|
+
scan of root's direct children this replaced answered "No folders found" for
|
|
2993
|
+
any nested folder — verified live on Resolve Studio 21.1.0.14, where
|
|
2994
|
+
deleting Master/OUTDOORS/1_FOOTAGE/<clip bin> by its own id failed while
|
|
2995
|
+
mp.DeleteFolders() on the recursively resolved object worked.
|
|
2996
|
+
|
|
2997
|
+
Unresolved ids abort the call instead of being dropped. Skipping them let a
|
|
2998
|
+
partial match delete or move the subset it did find and answer plain
|
|
2999
|
+
{"success": true}, which is indistinguishable from every id having resolved.
|
|
3000
|
+
|
|
3001
|
+
The root (Master) folder is refused rather than passed to Resolve: it cannot
|
|
3002
|
+
be deleted or moved, and the shallow search could never return it.
|
|
3003
|
+
"""
|
|
3004
|
+
if not isinstance(folder_ids, list) or not folder_ids:
|
|
3005
|
+
return None, _err(
|
|
3006
|
+
"folder_ids must be a non-empty list of folder unique IDs",
|
|
3007
|
+
code="INVALID_FOLDER_IDS", category="invalid_input",
|
|
3008
|
+
remediation=_FOLDER_ID_REMEDIATION)
|
|
3009
|
+
root_id = root.GetUniqueId()
|
|
3010
|
+
folders, unresolved, named_root = [], [], False
|
|
3011
|
+
for fid in folder_ids:
|
|
3012
|
+
fid = str(fid)
|
|
3013
|
+
if fid == root_id:
|
|
3014
|
+
named_root = True
|
|
3015
|
+
continue
|
|
3016
|
+
found = _find_folder_by_id(root, fid)
|
|
3017
|
+
if found is None:
|
|
3018
|
+
unresolved.append(fid)
|
|
3019
|
+
else:
|
|
3020
|
+
folders.append(found)
|
|
3021
|
+
if named_root:
|
|
3022
|
+
return None, _err(
|
|
3023
|
+
f"The root (Master) folder cannot be {verb}",
|
|
3024
|
+
code="ROOT_FOLDER_NOT_ELIGIBLE", category="invalid_input",
|
|
3025
|
+
state={"root_folder_id": root_id})
|
|
3026
|
+
if unresolved:
|
|
3027
|
+
return None, _err(
|
|
3028
|
+
f"Folder(s) not found: {', '.join(unresolved)}",
|
|
3029
|
+
code="FOLDER_NOT_FOUND", category="invalid_input",
|
|
3030
|
+
reason=(f"{len(unresolved)} of {len(folder_ids)} folder_ids matched no folder "
|
|
3031
|
+
f"anywhere in the Media Pool; nothing was {verb}."),
|
|
3032
|
+
remediation=_FOLDER_ID_REMEDIATION,
|
|
3033
|
+
state={"unresolved_folder_ids": unresolved,
|
|
3034
|
+
"resolved_folder_ids": [f.GetUniqueId() for f in folders]})
|
|
3035
|
+
return folders, None
|
|
3036
|
+
|
|
3037
|
+
|
|
2941
3038
|
def _folder_from_params(mp, p, *path_keys, no_address="current"):
|
|
2942
3039
|
"""Resolve the folder an action was aimed at. Returns (folder, error).
|
|
2943
3040
|
|
|
@@ -2958,8 +3055,7 @@ def _folder_from_params(mp, p, *path_keys, no_address="current"):
|
|
|
2958
3055
|
`_navigate_folder(mp, "")`, which returns root). This fix must not also
|
|
2959
3056
|
change what omitting the address means.
|
|
2960
3057
|
"""
|
|
2961
|
-
remediation =
|
|
2962
|
-
"path=\"Master\") and address by exact path or folder_id.")
|
|
3058
|
+
remediation = _FOLDER_ID_REMEDIATION
|
|
2963
3059
|
path = _first_param(p, *path_keys)
|
|
2964
3060
|
if path:
|
|
2965
3061
|
f = _navigate_folder(mp, path)
|
|
@@ -21049,7 +21145,10 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21049
21145
|
add_subfolder(name, parent_path?) -> {success, name, id}
|
|
21050
21146
|
delete_folders(folder_ids) -> {success}
|
|
21051
21147
|
DESTRUCTIVE. Deletes folders + every clip they contain.
|
|
21148
|
+
folder_ids resolve at ANY depth; an id matching nothing fails the whole
|
|
21149
|
+
call (FOLDER_NOT_FOUND) rather than deleting the ids that did resolve.
|
|
21052
21150
|
move_folders(folder_ids, target_path) -> {success}
|
|
21151
|
+
folder_ids resolve at ANY depth; unresolved ids fail the whole call.
|
|
21053
21152
|
refresh() -> {success}
|
|
21054
21153
|
create_timeline(name, if_exists?) -> {success, name, id}
|
|
21055
21154
|
— if_exists: version (default), reuse, or fail
|
|
@@ -21086,6 +21185,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21086
21185
|
Example: [{"FilePath": "frame_%03d.dpx", "StartIndex": 1, "EndIndex": 100}]
|
|
21087
21186
|
delete_clips(clip_ids) -> {success}
|
|
21088
21187
|
DESTRUCTIVE. Removes clips from the Media Pool (does not touch source files).
|
|
21188
|
+
An id matching no clip fails the whole call (CLIP_NOT_FOUND) rather than
|
|
21189
|
+
deleting the ids that did resolve; the same holds for move_clips, relink
|
|
21190
|
+
and unlink.
|
|
21089
21191
|
move_clips(clip_ids, target_path) -> {success}
|
|
21090
21192
|
relink(clip_ids, folder_path) -> {success}
|
|
21091
21193
|
UNSAFE. No dry_run. Prefer safe_relink.
|
|
@@ -21154,14 +21256,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21154
21256
|
f = mp.AddSubFolder(parent, p["name"])
|
|
21155
21257
|
return _ok(name=f.GetName(), id=f.GetUniqueId()) if f else _err("Failed to create subfolder")
|
|
21156
21258
|
elif action == "delete_folders":
|
|
21157
|
-
folders = []
|
|
21158
|
-
|
|
21159
|
-
|
|
21160
|
-
for sub in (root.GetSubFolderList() or []):
|
|
21161
|
-
if sub.GetUniqueId() == fid:
|
|
21162
|
-
folders.append(sub)
|
|
21163
|
-
if not folders:
|
|
21164
|
-
return _err("No folders found")
|
|
21259
|
+
folders, folders_err = _folders_from_ids(root, p["folder_ids"], verb="deleted")
|
|
21260
|
+
if folders_err:
|
|
21261
|
+
return folders_err
|
|
21165
21262
|
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
21166
21263
|
return _issue_confirm_token(
|
|
21167
21264
|
action="media_pool.delete_folders", params=p,
|
|
@@ -21177,12 +21274,12 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21177
21274
|
elif action == "move_folders":
|
|
21178
21275
|
target = _navigate_folder(mp, p["target_path"])
|
|
21179
21276
|
if not target:
|
|
21180
|
-
return _err(f"Target folder not found: {p['target_path']}"
|
|
21181
|
-
|
|
21182
|
-
|
|
21183
|
-
|
|
21184
|
-
|
|
21185
|
-
|
|
21277
|
+
return _err(f"Target folder not found: {p['target_path']}",
|
|
21278
|
+
code="FOLDER_NOT_FOUND", category="invalid_input",
|
|
21279
|
+
remediation=_FOLDER_ID_REMEDIATION)
|
|
21280
|
+
folders, folders_err = _folders_from_ids(root, p["folder_ids"], verb="moved")
|
|
21281
|
+
if folders_err:
|
|
21282
|
+
return folders_err
|
|
21186
21283
|
return {"success": bool(mp.MoveFolders(folders, target))}
|
|
21187
21284
|
elif action == "refresh":
|
|
21188
21285
|
return {"success": bool(mp.RefreshFolders())}
|
|
@@ -21393,10 +21490,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21393
21490
|
result = mp.ImportMedia(paths)
|
|
21394
21491
|
return {"imported": len(result) if result else 0}
|
|
21395
21492
|
elif action == "delete_clips":
|
|
21396
|
-
clips =
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
return _err("No clips found")
|
|
21493
|
+
clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="deleted")
|
|
21494
|
+
if clips_err:
|
|
21495
|
+
return clips_err
|
|
21400
21496
|
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
21401
21497
|
return _issue_confirm_token(
|
|
21402
21498
|
action="media_pool.delete_clips", params=p,
|
|
@@ -21413,16 +21509,19 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21413
21509
|
target = _navigate_folder(mp, p["target_path"])
|
|
21414
21510
|
if not target:
|
|
21415
21511
|
return _err(f"Target folder not found: {p['target_path']}")
|
|
21416
|
-
clips =
|
|
21417
|
-
|
|
21512
|
+
clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="moved")
|
|
21513
|
+
if clips_err:
|
|
21514
|
+
return clips_err
|
|
21418
21515
|
return {"success": bool(mp.MoveClips(clips, target))}
|
|
21419
21516
|
elif action == "relink":
|
|
21420
|
-
clips =
|
|
21421
|
-
|
|
21517
|
+
clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="relinked")
|
|
21518
|
+
if clips_err:
|
|
21519
|
+
return clips_err
|
|
21422
21520
|
return {"success": bool(mp.RelinkClips(clips, p["folder_path"]))}
|
|
21423
21521
|
elif action == "unlink":
|
|
21424
|
-
clips =
|
|
21425
|
-
|
|
21522
|
+
clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="unlinked")
|
|
21523
|
+
if clips_err:
|
|
21524
|
+
return clips_err
|
|
21426
21525
|
return {"success": bool(mp.UnlinkClips(clips))}
|
|
21427
21526
|
elif action == "export_metadata":
|
|
21428
21527
|
clip_ids = p.get("clip_ids")
|