davinci-resolve-mcp 4.8.16 → 4.8.18
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 +33 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +70 -70
- package/src/utils/script_templates.py +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
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.18 — `copy_clip_annotations` honours `include_*="false"`
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`copy_clip_annotations` read `include_markers`, `include_flags`, and
|
|
10
|
+
`include_clip_color` with bare truthiness.** ([#268](https://github.com/samuelgursky/davinci-resolve-mcp/pull/268), @Dev-next-gen)
|
|
11
|
+
A caller sending `include_flags="false"` (or `"no"`, `"0"`, `"off"`) still had the
|
|
12
|
+
flags copied onto every target clip. All three now go through `_coerce_bool` with a
|
|
13
|
+
default of `True`. Guard test: five false spellings per parameter (ten subtests
|
|
14
|
+
failing on the previous code), plus an omitted-copies-everything check.
|
|
15
|
+
- **The same reads at the twin sites.** The timeline `copy_annotations` /
|
|
16
|
+
`move_annotations` path read `include_flags` and `include_clip_color` the same way,
|
|
17
|
+
and `_timeline_conform_snapshot` wrapped `include_markers` and
|
|
18
|
+
`include_clip_properties` in `bool()`, which also turns `"false"` into `True`. Both
|
|
19
|
+
now coerce. Guard test `tests/test_include_flag_coercion.py`: fifteen subtests
|
|
20
|
+
failing on the previous code, plus omitted-flag positive checks.
|
|
21
|
+
|
|
22
|
+
## What's New in v4.8.17 — `dry_run="false"` runs the real operation
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Handlers read `dry_run` with bare truthiness.** 61 reads in `src/server.py` and
|
|
27
|
+
the `script_plugin` media-rules generator treated `dry_run="false"` (or `"no"`,
|
|
28
|
+
`"0"`, `"off"`) as true and returned a preview, even though the destructive-op hook,
|
|
29
|
+
which already coerced the flag, had gated the call as a real mutation. Every
|
|
30
|
+
handler read now goes through `_coerce_bool` with the handler's own default, so
|
|
31
|
+
actions that preview by default keep doing so when the flag is omitted or `None`.
|
|
32
|
+
Reads that already used the media-analysis and setup coercers are unchanged.
|
|
33
|
+
Follow-up to [#266](https://github.com/samuelgursky/davinci-resolve-mcp/pull/266).
|
|
34
|
+
Guard test: `organize_clips` and `safe_import_folder` act on five false spellings
|
|
35
|
+
(ten subtests failing on the previous code), plus an AST ratchet that fails on any
|
|
36
|
+
`p.get("dry_run")` in `server.py` not wrapped in a coercer.
|
|
37
|
+
|
|
5
38
|
## What's New in v4.8.16 — `organize_clips` honours `create_missing="false"`
|
|
6
39
|
|
|
7
40
|
### Fixed
|
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.18 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
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.18"
|
|
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.18"
|
|
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.18"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -2584,11 +2584,11 @@ def _copy_annotations(tl, p: Dict[str, Any], *, move: bool = False):
|
|
|
2584
2584
|
copied += 1
|
|
2585
2585
|
else:
|
|
2586
2586
|
warnings.append({"frame": frame, "result": result})
|
|
2587
|
-
if p.get("include_flags", True) and _has_method(source, "GetFlagList") and _has_method(target, "AddFlag"):
|
|
2587
|
+
if _coerce_bool(p.get("include_flags"), True) and _has_method(source, "GetFlagList") and _has_method(target, "AddFlag"):
|
|
2588
2588
|
for flag in source.GetFlagList() or []:
|
|
2589
2589
|
if not target.AddFlag(flag):
|
|
2590
2590
|
warnings.append({"flag": flag, "result": "AddFlag returned false"})
|
|
2591
|
-
if p.get("include_clip_color", True) and _has_method(source, "GetClipColor") and _has_method(target, "SetClipColor"):
|
|
2591
|
+
if _coerce_bool(p.get("include_clip_color"), True) and _has_method(source, "GetClipColor") and _has_method(target, "SetClipColor"):
|
|
2592
2592
|
color = source.GetClipColor()
|
|
2593
2593
|
if color and not target.SetClipColor(color):
|
|
2594
2594
|
warnings.append({"clip_color": color, "result": "SetClipColor returned false"})
|
|
@@ -5351,7 +5351,7 @@ def _timeline_ripple_insert_impl(proj, tl, p: Dict[str, Any], *, resolve=None) -
|
|
|
5351
5351
|
if locked_tracks:
|
|
5352
5352
|
reasons.append(f"locked track(s) hold items that must shift: {', '.join(locked_tracks)}")
|
|
5353
5353
|
plan["infeasible_reasons"] = reasons
|
|
5354
|
-
if
|
|
5354
|
+
if _coerce_bool(p.get("dry_run"), True):
|
|
5355
5355
|
return {"success": feasible, "dry_run": True, "plan": plan}
|
|
5356
5356
|
if not feasible:
|
|
5357
5357
|
return {
|
|
@@ -6262,8 +6262,8 @@ def _timeline_item_conform_summary(item, track_type: str, track_index: int, item
|
|
|
6262
6262
|
|
|
6263
6263
|
def _timeline_conform_snapshot(tl, p: Optional[Dict[str, Any]] = None):
|
|
6264
6264
|
p = p or {}
|
|
6265
|
-
include_markers =
|
|
6266
|
-
include_clip_properties =
|
|
6265
|
+
include_markers = _coerce_bool(p.get("include_markers"), True)
|
|
6266
|
+
include_clip_properties = _coerce_bool(p.get("include_clip_properties"), False)
|
|
6267
6267
|
track_types = p.get("track_types") or ["video", "audio", "subtitle"]
|
|
6268
6268
|
if not isinstance(track_types, list):
|
|
6269
6269
|
return _err("track_types must be a list")
|
|
@@ -6477,7 +6477,7 @@ def _timeline_story_spine_report(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
6477
6477
|
snapshot = _timeline_conform_snapshot(tl, {
|
|
6478
6478
|
"track_types": p.get("track_types") or ["video", "audio", "subtitle"],
|
|
6479
6479
|
"include_markers": True,
|
|
6480
|
-
"include_clip_properties":
|
|
6480
|
+
"include_clip_properties": _coerce_bool(p.get("include_clip_properties"), False),
|
|
6481
6481
|
})
|
|
6482
6482
|
if isinstance(snapshot, dict) and snapshot.get("error"):
|
|
6483
6483
|
return snapshot
|
|
@@ -6507,7 +6507,7 @@ def _timeline_bulk_set_item_properties(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
6507
6507
|
ops = p.get("ops")
|
|
6508
6508
|
if not isinstance(ops, list) or not ops:
|
|
6509
6509
|
return _err("bulk_set_item_properties requires params.ops: non-empty list of objects")
|
|
6510
|
-
dry_run =
|
|
6510
|
+
dry_run = _coerce_bool(p.get("dry_run"), False)
|
|
6511
6511
|
readback = bool(p.get("readback", False))
|
|
6512
6512
|
results = []
|
|
6513
6513
|
for index, op in enumerate(ops):
|
|
@@ -6601,7 +6601,7 @@ def _timeline_apply_look_to_items(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
6601
6601
|
return _err("apply_look_to_items requires target_ids: non-empty list of video timeline item IDs")
|
|
6602
6602
|
targets, missing = _timeline_items_by_ids_report(tl, target_ids, track_types=("video",))
|
|
6603
6603
|
target_summaries = [_timeline_item_summary(item) for item in targets]
|
|
6604
|
-
dry_run =
|
|
6604
|
+
dry_run = _coerce_bool(p.get("dry_run"), False)
|
|
6605
6605
|
out: Dict[str, Any] = {
|
|
6606
6606
|
"targets": target_summaries,
|
|
6607
6607
|
"missing": missing,
|
|
@@ -6860,7 +6860,7 @@ def _timeline_create_variant_from_ranges(proj, source_tl, p: Dict[str, Any]) ->
|
|
|
6860
6860
|
if clip_err:
|
|
6861
6861
|
return clip_err
|
|
6862
6862
|
append_infos.append(clip_info)
|
|
6863
|
-
if p.get("dry_run", False):
|
|
6863
|
+
if _coerce_bool(p.get("dry_run"), False):
|
|
6864
6864
|
return {
|
|
6865
6865
|
"success": True,
|
|
6866
6866
|
"dry_run": True,
|
|
@@ -7350,7 +7350,7 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
|
|
|
7350
7350
|
"EXPORT_FCPXML_1_8/1_9/1_10, EXPORT_OTIO (subtypes EXPORT_NONE, "
|
|
7351
7351
|
"EXPORT_AAF_NEW, EXPORT_AAF_EXISTING, EXPORT_CDL, EXPORT_SDL, EXPORT_MISSING_CLIPS)."
|
|
7352
7352
|
)
|
|
7353
|
-
if p.get("dry_run"):
|
|
7353
|
+
if _coerce_bool(p.get("dry_run")):
|
|
7354
7354
|
return _ok(path=path, would_export=True, spec={k: v for k, v in spec.items() if k != "export_type"})
|
|
7355
7355
|
|
|
7356
7356
|
def _work():
|
|
@@ -7769,7 +7769,7 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
7769
7769
|
if sequence_name_rewritten:
|
|
7770
7770
|
import_path = rewritten_path
|
|
7771
7771
|
|
|
7772
|
-
if p.get("dry_run"):
|
|
7772
|
+
if _coerce_bool(p.get("dry_run")):
|
|
7773
7773
|
out = _ok(path=path, import_path=import_path, options=options, would_import=True)
|
|
7774
7774
|
if sanitize_report is not None:
|
|
7775
7775
|
out["sanitize"] = sanitize_report
|
|
@@ -8167,7 +8167,7 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
|
8167
8167
|
entry.update(success=False, error=f"extract failed: {e}")
|
|
8168
8168
|
results.append(entry)
|
|
8169
8169
|
continue
|
|
8170
|
-
if p.get("dry_run"):
|
|
8170
|
+
if _coerce_bool(p.get("dry_run")):
|
|
8171
8171
|
entry.update(success=True, would_import=True, drt_path=drt_path)
|
|
8172
8172
|
results.append(entry)
|
|
8173
8173
|
continue
|
|
@@ -8190,15 +8190,15 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
|
8190
8190
|
imported=imported_count,
|
|
8191
8191
|
available=available,
|
|
8192
8192
|
results=results,
|
|
8193
|
-
dry_run=
|
|
8193
|
+
dry_run=_coerce_bool(p.get("dry_run")),
|
|
8194
8194
|
)
|
|
8195
|
-
if not p.get("dry_run") and selected and imported_count == 0:
|
|
8195
|
+
if not _coerce_bool(p.get("dry_run")) and selected and imported_count == 0:
|
|
8196
8196
|
out["success"] = False
|
|
8197
8197
|
out["error"] = (
|
|
8198
8198
|
f"None of the {len(selected)} selected timeline(s) imported — "
|
|
8199
8199
|
"see results[].error for each cause."
|
|
8200
8200
|
)
|
|
8201
|
-
elif not p.get("dry_run") and imported_count < len(selected):
|
|
8201
|
+
elif not _coerce_bool(p.get("dry_run")) and imported_count < len(selected):
|
|
8202
8202
|
out["partial"] = True
|
|
8203
8203
|
out["warning"] = (
|
|
8204
8204
|
f"{imported_count} of {len(selected)} selected timeline(s) "
|
|
@@ -8902,7 +8902,7 @@ def _safe_set_audio_properties(tl, p: Dict[str, Any]):
|
|
|
8902
8902
|
original[key] = item.GetProperty(key)
|
|
8903
8903
|
except Exception as exc:
|
|
8904
8904
|
original[key] = {"error": str(exc)}
|
|
8905
|
-
if p.get("dry_run"):
|
|
8905
|
+
if _coerce_bool(p.get("dry_run")):
|
|
8906
8906
|
return _ok(would_set=properties, original=original)
|
|
8907
8907
|
results = {}
|
|
8908
8908
|
for key, value in properties.items():
|
|
@@ -9071,7 +9071,7 @@ def _safe_auto_sync_audio(mp, p: Dict[str, Any]):
|
|
|
9071
9071
|
# here makes _normalize_auto_sync_settings fall back to string enum keys,
|
|
9072
9072
|
# which AutoSyncAudio silently rejects (returns False).
|
|
9073
9073
|
settings, ignored_settings = _normalize_auto_sync_settings(dict(p.get("settings") or {}), get_resolve())
|
|
9074
|
-
if p.get("dry_run", True):
|
|
9074
|
+
if _coerce_bool(p.get("dry_run"), True):
|
|
9075
9075
|
return _ok(would_auto_sync=True, clips=_clip_summaries(clips), missing=missing, settings=settings, ignored_settings=ignored_settings)
|
|
9076
9076
|
# Read-back verification: AutoSyncAudio's boolean return is unreliable, so
|
|
9077
9077
|
# capture each clip's "Synced Audio" linkage before and after and report the
|
|
@@ -9321,7 +9321,7 @@ def _safe_create_subtitles(tl, p: Dict[str, Any]):
|
|
|
9321
9321
|
the timeline's subtitle track count before/after and report the real delta.
|
|
9322
9322
|
"""
|
|
9323
9323
|
settings, ignored = _normalize_auto_caption_settings(p.get("settings"), get_resolve())
|
|
9324
|
-
if p.get("dry_run", False):
|
|
9324
|
+
if _coerce_bool(p.get("dry_run"), False):
|
|
9325
9325
|
return _ok(would_create_subtitles=True, settings=settings, ignored_settings=ignored)
|
|
9326
9326
|
|
|
9327
9327
|
def _subtitle_track_count():
|
|
@@ -9603,7 +9603,7 @@ def _setup_multicam_timeline(proj, mp, p: Dict[str, Any]):
|
|
|
9603
9603
|
plan, plan_err = build_multicam_setup_plan(root, p, _find_clip)
|
|
9604
9604
|
if plan_err:
|
|
9605
9605
|
return plan_err
|
|
9606
|
-
if p.get("dry_run", False):
|
|
9606
|
+
if _coerce_bool(p.get("dry_run"), False):
|
|
9607
9607
|
return {
|
|
9608
9608
|
**plan,
|
|
9609
9609
|
"dry_run": True,
|
|
@@ -13504,7 +13504,7 @@ def _safe_import_media(mp, p: Dict[str, Any]):
|
|
|
13504
13504
|
errors.append(path_err)
|
|
13505
13505
|
if errors:
|
|
13506
13506
|
return _err("; ".join(errors))
|
|
13507
|
-
if p.get("dry_run"):
|
|
13507
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13508
13508
|
return _ok(would_import=paths, target_folder=p.get("target_folder"))
|
|
13509
13509
|
previous, folder_err = _set_current_folder_temporarily(mp, p.get("target_folder"))
|
|
13510
13510
|
if folder_err:
|
|
@@ -13534,7 +13534,7 @@ def _safe_import_sequence(mp, p: Dict[str, Any]):
|
|
|
13534
13534
|
suffix = "" if len(missing) <= 5 else f" (+{len(missing) - 5} more)"
|
|
13535
13535
|
return _err(f"Missing sequence frames: {sample}{suffix}")
|
|
13536
13536
|
info = {"FilePath": pattern, "StartIndex": start, "EndIndex": end}
|
|
13537
|
-
if p.get("dry_run"):
|
|
13537
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13538
13538
|
return _ok(would_import=[info], target_folder=p.get("target_folder"))
|
|
13539
13539
|
previous, folder_err = _set_current_folder_temporarily(mp, p.get("target_folder"))
|
|
13540
13540
|
if folder_err:
|
|
@@ -13550,7 +13550,7 @@ def _safe_import_folder(mp, p: Dict[str, Any]):
|
|
|
13550
13550
|
path_err = _path_error(path, must_be_dir=True)
|
|
13551
13551
|
if path_err:
|
|
13552
13552
|
return _err(path_err)
|
|
13553
|
-
if p.get("dry_run"):
|
|
13553
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13554
13554
|
return _ok(would_import_folder=path, source_clips_path=p.get("source_clips_path", ""))
|
|
13555
13555
|
return {"success": bool(mp.ImportFolderFromFile(path, p.get("source_clips_path", "")))}
|
|
13556
13556
|
|
|
@@ -13570,7 +13570,7 @@ def _organize_clips(mp, root, p: Dict[str, Any]):
|
|
|
13570
13570
|
if err:
|
|
13571
13571
|
return err
|
|
13572
13572
|
clips, missing = resolved
|
|
13573
|
-
if p.get("dry_run"):
|
|
13573
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13574
13574
|
return _ok(target_path=target_path, clips=_clip_summaries(clips), missing=missing)
|
|
13575
13575
|
return {"success": bool(mp.MoveClips(clips, target)), "moved": len(clips), "missing": missing}
|
|
13576
13576
|
|
|
@@ -13595,7 +13595,7 @@ def _copy_metadata(root, p: Dict[str, Any]):
|
|
|
13595
13595
|
if not target:
|
|
13596
13596
|
results.append({"clip_id": target_id, "success": False, "error": "Clip not found"})
|
|
13597
13597
|
continue
|
|
13598
|
-
if p.get("dry_run"):
|
|
13598
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13599
13599
|
results.append({"clip_id": target_id, "success": True, "metadata_keys": sorted(metadata.keys()), "third_party_keys": sorted(third_party.keys())})
|
|
13600
13600
|
continue
|
|
13601
13601
|
ok = bool(target.SetMetadata(metadata)) if metadata else True
|
|
@@ -13620,7 +13620,7 @@ def _normalize_metadata(root, mp, p: Dict[str, Any]):
|
|
|
13620
13620
|
results = []
|
|
13621
13621
|
for clip in clips:
|
|
13622
13622
|
clip_id = _safe_media_pool_item_id(clip)
|
|
13623
|
-
if p.get("dry_run"):
|
|
13623
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13624
13624
|
results.append({"clip_id": clip_id, "success": True, "metadata_keys": sorted(metadata.keys()), "third_party_keys": sorted(third_party.keys())})
|
|
13625
13625
|
continue
|
|
13626
13626
|
ok = bool(clip.SetMetadata(metadata)) if metadata else True
|
|
@@ -13758,7 +13758,7 @@ def _safe_relink(mp, root, p: Dict[str, Any]):
|
|
|
13758
13758
|
if err:
|
|
13759
13759
|
return err
|
|
13760
13760
|
clips, missing = resolved
|
|
13761
|
-
if p.get("dry_run"):
|
|
13761
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13762
13762
|
return _ok(folder_path=folder_path, clips=_clip_summaries(clips), missing=missing)
|
|
13763
13763
|
return {"success": bool(mp.RelinkClips(clips, folder_path)), "count": len(clips), "missing": missing}
|
|
13764
13764
|
|
|
@@ -13768,7 +13768,7 @@ def _safe_unlink(mp, root, p: Dict[str, Any]):
|
|
|
13768
13768
|
if err:
|
|
13769
13769
|
return err
|
|
13770
13770
|
clips, missing = resolved
|
|
13771
|
-
if p.get("dry_run"):
|
|
13771
|
+
if _coerce_bool(p.get("dry_run")):
|
|
13772
13772
|
return _ok(clips=_clip_summaries(clips), missing=missing)
|
|
13773
13773
|
return {"success": bool(mp.UnlinkClips(clips)), "count": len(clips), "missing": missing}
|
|
13774
13774
|
|
|
@@ -14327,7 +14327,7 @@ def _link_proxy_checked(root, p: Dict[str, Any]):
|
|
|
14327
14327
|
requested = {
|
|
14328
14328
|
"clip_id": p.get("clip_id"),
|
|
14329
14329
|
"proxy_path": proxy_path,
|
|
14330
|
-
"dry_run":
|
|
14330
|
+
"dry_run": _coerce_bool(p.get("dry_run")),
|
|
14331
14331
|
"check_compatibility": check_compatibility,
|
|
14332
14332
|
"require_compatible": bool(p.get("require_compatible")),
|
|
14333
14333
|
"expected_codec": p.get("expected_codec") or p.get("codec"),
|
|
@@ -14342,7 +14342,7 @@ def _link_proxy_checked(root, p: Dict[str, Any]):
|
|
|
14342
14342
|
}
|
|
14343
14343
|
if compatibility is not None:
|
|
14344
14344
|
preflight["compatible"] = bool(compatibility.get("compatible"))
|
|
14345
|
-
if p.get("dry_run"):
|
|
14345
|
+
if _coerce_bool(p.get("dry_run")):
|
|
14346
14346
|
out = _ok(clip=_media_pool_item_summary(clip), proxy_path=proxy_path)
|
|
14347
14347
|
if compatibility is not None:
|
|
14348
14348
|
out["compatibility"] = compatibility
|
|
@@ -14435,7 +14435,7 @@ def _link_full_resolution_checked(root, p: Dict[str, Any]):
|
|
|
14435
14435
|
path_err = _path_error(path, must_be_file=True)
|
|
14436
14436
|
if path_err:
|
|
14437
14437
|
return _err(path_err)
|
|
14438
|
-
if p.get("dry_run"):
|
|
14438
|
+
if _coerce_bool(p.get("dry_run")):
|
|
14439
14439
|
return _ok(clip=_media_pool_item_summary(clip), path=path)
|
|
14440
14440
|
return {"success": bool(clip.LinkFullResolutionMedia(path))}
|
|
14441
14441
|
|
|
@@ -14453,7 +14453,7 @@ def _set_clip_marks(root, mp, p: Dict[str, Any]):
|
|
|
14453
14453
|
results = []
|
|
14454
14454
|
for clip in clips:
|
|
14455
14455
|
clip_id = _safe_media_pool_item_id(clip)
|
|
14456
|
-
if p.get("dry_run"):
|
|
14456
|
+
if _coerce_bool(p.get("dry_run")):
|
|
14457
14457
|
results.append({"clip_id": clip_id, "success": True, "mark_in": mark_in, "mark_out": mark_out})
|
|
14458
14458
|
continue
|
|
14459
14459
|
results.append({"clip_id": clip_id, "success": bool(clip.SetMarkInOut(mark_in, mark_out, p.get("type", "all")))})
|
|
@@ -14640,7 +14640,7 @@ def _clear_clip_marks(root, mp, p: Dict[str, Any]):
|
|
|
14640
14640
|
results = []
|
|
14641
14641
|
for clip in clips:
|
|
14642
14642
|
clip_id = _safe_media_pool_item_id(clip)
|
|
14643
|
-
if p.get("dry_run"):
|
|
14643
|
+
if _coerce_bool(p.get("dry_run")):
|
|
14644
14644
|
results.append({"clip_id": clip_id, "success": True})
|
|
14645
14645
|
continue
|
|
14646
14646
|
results.append({"clip_id": clip_id, "success": bool(clip.ClearMarkInOut(p.get("type", "all")))})
|
|
@@ -14657,16 +14657,16 @@ def _copy_clip_annotations(root, p: Dict[str, Any]):
|
|
|
14657
14657
|
markers = source.GetMarkers() or {}
|
|
14658
14658
|
flags = source.GetFlagList() or []
|
|
14659
14659
|
color = source.GetClipColor()
|
|
14660
|
-
include_markers = p.get("include_markers", True)
|
|
14661
|
-
include_flags = p.get("include_flags", True)
|
|
14662
|
-
include_color = p.get("include_clip_color", True)
|
|
14660
|
+
include_markers = _coerce_bool(p.get("include_markers"), True)
|
|
14661
|
+
include_flags = _coerce_bool(p.get("include_flags"), True)
|
|
14662
|
+
include_color = _coerce_bool(p.get("include_clip_color"), True)
|
|
14663
14663
|
results = []
|
|
14664
14664
|
for target_id in target_ids:
|
|
14665
14665
|
target = _find_clip(root, str(target_id))
|
|
14666
14666
|
if not target:
|
|
14667
14667
|
results.append({"clip_id": target_id, "success": False, "error": "Clip not found"})
|
|
14668
14668
|
continue
|
|
14669
|
-
if p.get("dry_run"):
|
|
14669
|
+
if _coerce_bool(p.get("dry_run")):
|
|
14670
14670
|
results.append({"clip_id": target_id, "success": True, "markers": len(markers), "flags": len(flags), "clip_color": color})
|
|
14671
14671
|
continue
|
|
14672
14672
|
ok = True
|
|
@@ -18573,7 +18573,7 @@ def _probe_project_settings(project, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18573
18573
|
out["write_restore_probe"] = _safe_set_project_settings(project, {
|
|
18574
18574
|
"settings": settings,
|
|
18575
18575
|
"restore": True,
|
|
18576
|
-
"dry_run":
|
|
18576
|
+
"dry_run": _coerce_bool(p.get("dry_run"), False),
|
|
18577
18577
|
})
|
|
18578
18578
|
return out
|
|
18579
18579
|
|
|
@@ -18590,7 +18590,7 @@ def _safe_set_project_settings(project, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18590
18590
|
original[key] = _ser(project.GetSetting(key))
|
|
18591
18591
|
except Exception as exc:
|
|
18592
18592
|
original[key] = {"error": str(exc)}
|
|
18593
|
-
if p.get("dry_run"):
|
|
18593
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18594
18594
|
return _ok(would_set=settings, original=original, restore=p.get("restore", True))
|
|
18595
18595
|
results: Dict[str, Any] = {}
|
|
18596
18596
|
for key, value in settings.items():
|
|
@@ -18632,7 +18632,7 @@ def _safe_project_create(pm, resolve_obj, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18632
18632
|
version = resolve_obj.GetVersion() or [0]
|
|
18633
18633
|
if version[0] < 20 or (version[0] == 20 and len(version) > 2 and (version[1], version[2]) < (2, 2)):
|
|
18634
18634
|
return _err("ProjectManager.CreateProject media_location_path requires DaVinci Resolve 20.2.2+")
|
|
18635
|
-
if p.get("dry_run"):
|
|
18635
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18636
18636
|
return _ok(would_create=True, name=name, media_location_path=media_location_path)
|
|
18637
18637
|
project = pm.CreateProject(name, media_location_path) if media_location_path else pm.CreateProject(name)
|
|
18638
18638
|
return _ok(project=_project_object_summary(project), name=project.GetName()) if project else _err(f"Failed to create '{name}'")
|
|
@@ -18649,7 +18649,7 @@ def _safe_project_export(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18649
18649
|
return path_err
|
|
18650
18650
|
os.makedirs(_project_path_parent(path), exist_ok=True)
|
|
18651
18651
|
with_stills = bool(p.get("with_stills_and_luts", False))
|
|
18652
|
-
if p.get("dry_run"):
|
|
18652
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18653
18653
|
return _ok(would_export=True, name=name, path=path, with_stills_and_luts=with_stills)
|
|
18654
18654
|
return {"success": bool(pm.ExportProject(name, path, with_stills))}
|
|
18655
18655
|
|
|
@@ -18665,7 +18665,7 @@ def _safe_project_import(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18665
18665
|
invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
|
|
18666
18666
|
if invalid:
|
|
18667
18667
|
return invalid
|
|
18668
|
-
if p.get("dry_run"):
|
|
18668
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18669
18669
|
return _ok(would_import=True, path=path, name=name)
|
|
18670
18670
|
return {"success": bool(pm.ImportProject(path, name))}
|
|
18671
18671
|
|
|
@@ -18692,7 +18692,7 @@ def _safe_project_archive(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18692
18692
|
if refused:
|
|
18693
18693
|
return refused
|
|
18694
18694
|
os.makedirs(_project_path_parent(path), exist_ok=True)
|
|
18695
|
-
if p.get("dry_run"):
|
|
18695
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18696
18696
|
return _ok(
|
|
18697
18697
|
would_archive=True,
|
|
18698
18698
|
name=name,
|
|
@@ -18716,7 +18716,7 @@ def _safe_project_restore(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18716
18716
|
invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
|
|
18717
18717
|
if invalid:
|
|
18718
18718
|
return invalid
|
|
18719
|
-
if p.get("dry_run"):
|
|
18719
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18720
18720
|
return _ok(would_restore=True, path=path, name=name)
|
|
18721
18721
|
return {"success": bool(pm.RestoreProject(path, name))}
|
|
18722
18722
|
|
|
@@ -18726,7 +18726,7 @@ def _safe_project_delete(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18726
18726
|
invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
|
|
18727
18727
|
if invalid:
|
|
18728
18728
|
return invalid
|
|
18729
|
-
if p.get("dry_run"):
|
|
18729
|
+
if _coerce_bool(p.get("dry_run")):
|
|
18730
18730
|
return _ok(would_delete=True, name=name)
|
|
18731
18731
|
current = pm.GetCurrentProject()
|
|
18732
18732
|
# Route through delete_project_safely: DeleteProject silently returns False
|
|
@@ -18780,7 +18780,7 @@ def _safe_set_current_database(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18780
18780
|
if not isinstance(db_info, dict) or not db_info.get("DbType") or not db_info.get("DbName"):
|
|
18781
18781
|
return _err("db_info must include DbType and DbName")
|
|
18782
18782
|
current = _ser(pm.GetCurrentDatabase()) if _has_method(pm, "GetCurrentDatabase") else None
|
|
18783
|
-
dry_run = p.get("dry_run", True) or not _coerce_bool(p.get("allow_switch"))
|
|
18783
|
+
dry_run = _coerce_bool(p.get("dry_run"), True) or not _coerce_bool(p.get("allow_switch"))
|
|
18784
18784
|
if dry_run:
|
|
18785
18785
|
return _ok(
|
|
18786
18786
|
would_switch=True,
|
|
@@ -19116,7 +19116,7 @@ def _spec_action(r, pm, action: str, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
19116
19116
|
try:
|
|
19117
19117
|
result = _project_spec.apply_spec(
|
|
19118
19118
|
spec, executor,
|
|
19119
|
-
dry_run=
|
|
19119
|
+
dry_run=_coerce_bool(p.get("dry_run"), False),
|
|
19120
19120
|
run_hooks=run_hooks,
|
|
19121
19121
|
continue_on_error=bool(p.get("continue_on_error", False)),
|
|
19122
19122
|
run_hook=_make_spec_hook_runner() if run_hooks else None,
|
|
@@ -20244,7 +20244,7 @@ def _safe_set_render_settings(proj, p: Dict[str, Any]):
|
|
|
20244
20244
|
return err
|
|
20245
20245
|
if not validation["valid"]:
|
|
20246
20246
|
return {"success": False, "validation": validation}
|
|
20247
|
-
if p.get("dry_run"):
|
|
20247
|
+
if _coerce_bool(p.get("dry_run")):
|
|
20248
20248
|
return _ok(validation=validation)
|
|
20249
20249
|
before = _render_settings_snapshot(proj)
|
|
20250
20250
|
success = bool(proj.SetRenderSettings(settings))
|
|
@@ -20278,7 +20278,7 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
|
|
|
20278
20278
|
return err
|
|
20279
20279
|
if not validation["valid"]:
|
|
20280
20280
|
return {"success": False, "validation": validation}
|
|
20281
|
-
if p.get("dry_run"):
|
|
20281
|
+
if _coerce_bool(p.get("dry_run")):
|
|
20282
20282
|
return _ok(validation=validation, format=p.get("format"), codec=p.get("codec"))
|
|
20283
20283
|
before = _render_settings_snapshot(proj)
|
|
20284
20284
|
# Pin the base render state before layering explicit settings on top. Without
|
|
@@ -20624,7 +20624,7 @@ def _safe_quick_export(proj, p: Dict[str, Any]):
|
|
|
20624
20624
|
return err
|
|
20625
20625
|
if not validation["valid"]:
|
|
20626
20626
|
return {"success": False, "validation": validation}
|
|
20627
|
-
if p.get("dry_run") or not _coerce_bool(p.get("allow_render")):
|
|
20627
|
+
if _coerce_bool(p.get("dry_run")) or not _coerce_bool(p.get("allow_render")):
|
|
20628
20628
|
return _ok(would_render=False, preset=preset, params=params, validation=validation)
|
|
20629
20629
|
before = set()
|
|
20630
20630
|
if target_dir and os.path.isdir(target_dir):
|
|
@@ -22672,7 +22672,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
22672
22672
|
try:
|
|
22673
22673
|
# Dry run by default: the ladder can spend a dozen ffmpeg decodes per clip,
|
|
22674
22674
|
# and the plan names that budget before anyone commits to it.
|
|
22675
|
-
if p.get("dry_run", True):
|
|
22675
|
+
if _coerce_bool(p.get("dry_run"), True):
|
|
22676
22676
|
return _ok(**_grade_loop_mod.plan(source, lut, **kwargs))
|
|
22677
22677
|
return _ok(**_grade_loop_mod.run(
|
|
22678
22678
|
source, lut,
|
|
@@ -22724,7 +22724,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
22724
22724
|
release_s=float(p.get("release_s", _mix_plan_mod.DEFAULT_RELEASE_S)),
|
|
22725
22725
|
hold_s=float(p.get("hold_s", _mix_plan_mod.DEFAULT_HOLD_S)),
|
|
22726
22726
|
)
|
|
22727
|
-
if p.get("dry_run", True):
|
|
22727
|
+
if _coerce_bool(p.get("dry_run"), True):
|
|
22728
22728
|
return _ok(**_mix_plan_mod.plan(dialogue, **kwargs))
|
|
22729
22729
|
return _ok(**_mix_plan_mod.render(
|
|
22730
22730
|
dialogue,
|
|
@@ -23616,11 +23616,11 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
23616
23616
|
plan["warnings"] = warnings
|
|
23617
23617
|
if (
|
|
23618
23618
|
plan.get("capability_gaps")
|
|
23619
|
-
and not
|
|
23619
|
+
and not _coerce_bool(p.get("dry_run"), True)
|
|
23620
23620
|
and media_analysis_plan_requires_capabilities(plan)
|
|
23621
23621
|
):
|
|
23622
23622
|
return _e2_wrap(_media_analysis_missing_capabilities_response(plan))
|
|
23623
|
-
if not
|
|
23623
|
+
if not _coerce_bool(p.get("dry_run"), True):
|
|
23624
23624
|
executed = await execute_media_analysis_plan_async(
|
|
23625
23625
|
plan,
|
|
23626
23626
|
params=p,
|
|
@@ -26112,7 +26112,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
26112
26112
|
applicable.sort(key=lambda c: c["span"]["start"], reverse=True)
|
|
26113
26113
|
plan = [{"action": c["action"], "span": c["span"], "kind": c.get("kind")} for c in applicable]
|
|
26114
26114
|
|
|
26115
|
-
if p.get("dry_run", True):
|
|
26115
|
+
if _coerce_bool(p.get("dry_run"), True):
|
|
26116
26116
|
return {
|
|
26117
26117
|
"dry_run": True,
|
|
26118
26118
|
"would_apply": len(applicable),
|
|
@@ -27502,7 +27502,7 @@ def _safe_set_cdl(item, p: Dict[str, Any]):
|
|
|
27502
27502
|
return {"success": False, "validation": validation}
|
|
27503
27503
|
normalized = _normalize_cdl(validation["cdl"])
|
|
27504
27504
|
node_ok, preflight = _cdl_node_preflight(item, validation["cdl"]["NodeIndex"])
|
|
27505
|
-
if p.get("dry_run"):
|
|
27505
|
+
if _coerce_bool(p.get("dry_run")):
|
|
27506
27506
|
return _ok(validation=validation, normalized=normalized, node_preflight=preflight)
|
|
27507
27507
|
if not node_ok:
|
|
27508
27508
|
return {
|
|
@@ -27548,7 +27548,7 @@ def _safe_copy_grade(item, p: Dict[str, Any]):
|
|
|
27548
27548
|
return err
|
|
27549
27549
|
targets, missing = _timeline_items_for_grade_copy(tl, target_ids)
|
|
27550
27550
|
target_summaries = [_timeline_item_summary(target) for target in targets]
|
|
27551
|
-
if p.get("dry_run"):
|
|
27551
|
+
if _coerce_bool(p.get("dry_run")):
|
|
27552
27552
|
return _ok(targets=target_summaries, missing=missing, would_copy=not missing)
|
|
27553
27553
|
if missing:
|
|
27554
27554
|
return {"success": False, "targets": target_summaries, "missing": missing}
|
|
@@ -27582,7 +27582,7 @@ def _safe_export_lut(item, p: Dict[str, Any]):
|
|
|
27582
27582
|
export_type, type_err = _resolve_lut_export_type(p.get("type", "33ptcube"), resolve_obj)
|
|
27583
27583
|
if type_err:
|
|
27584
27584
|
return type_err
|
|
27585
|
-
if p.get("dry_run"):
|
|
27585
|
+
if _coerce_bool(p.get("dry_run")):
|
|
27586
27586
|
return _ok(path=path, type=export_type, would_export=True)
|
|
27587
27587
|
before_exists = os.path.exists(path)
|
|
27588
27588
|
success = bool(item.ExportLUT(export_type, path))
|
|
@@ -27611,7 +27611,7 @@ def _safe_apply_drx(proj, item, p: Dict[str, Any]):
|
|
|
27611
27611
|
if not _has_method(graph, "ApplyGradeFromDRX"):
|
|
27612
27612
|
return _err(f"{source} graph does not expose ApplyGradeFromDRX",
|
|
27613
27613
|
code="APPLY_DRX_UNSUPPORTED", category="unsupported")
|
|
27614
|
-
if p.get("dry_run"):
|
|
27614
|
+
if _coerce_bool(p.get("dry_run")):
|
|
27615
27615
|
return _ok(path=path, source=source, grade_mode=p.get("grade_mode", p.get("mode", 0)), would_apply=True)
|
|
27616
27616
|
# B2 — Whole-grade replacement is catastrophic; require confirmation.
|
|
27617
27617
|
if "confirm_token" not in p and "confirmToken" not in p:
|
|
@@ -27828,7 +27828,7 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
27828
27828
|
attention = [r for r in public if (r["status"] != "apply" and r["reason"] not in bulk)
|
|
27829
27829
|
or (r["status"] == "apply" and r["ambiguous"])]
|
|
27830
27830
|
|
|
27831
|
-
if p.get("dry_run"):
|
|
27831
|
+
if _coerce_bool(p.get("dry_run")):
|
|
27832
27832
|
report_path = _trace_report(p, plan, "dry-run", {"summary": summary, "resolution": public})
|
|
27833
27833
|
return _ok(dry_run=True, timeline=live_name, plan_source=plan.get("source"),
|
|
27834
27834
|
summary=summary, report_path=report_path,
|
|
@@ -27945,7 +27945,7 @@ def _grade_version_restore(item, p: Dict[str, Any]):
|
|
|
27945
27945
|
names = snapshot["local"] if version_type == 0 else snapshot["remote"]
|
|
27946
27946
|
if name not in names:
|
|
27947
27947
|
return {"success": False, "snapshot": snapshot, "error": f"Version '{name}' not found"}
|
|
27948
|
-
if p.get("dry_run"):
|
|
27948
|
+
if _coerce_bool(p.get("dry_run")):
|
|
27949
27949
|
return _ok(snapshot=snapshot, would_load=name, type=version_type)
|
|
27950
27950
|
return {"success": bool(item.LoadVersionByName(name, version_type)), "snapshot": snapshot}
|
|
27951
27951
|
|
|
@@ -28353,7 +28353,7 @@ def _bulk_match_to_hero(proj, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
28353
28353
|
remediation="Use method='copy_grade' or call safe_set_cdl per target with hand-rolled values.",
|
|
28354
28354
|
)
|
|
28355
28355
|
|
|
28356
|
-
dry_run =
|
|
28356
|
+
dry_run = _coerce_bool(p.get("dry_run"), True)
|
|
28357
28357
|
payload: Dict[str, Any] = {
|
|
28358
28358
|
"hero": hero_summary,
|
|
28359
28359
|
"proposals": proposals,
|
|
@@ -30157,7 +30157,7 @@ def _safe_add_fusion_tool(comp, p: Dict[str, Any]):
|
|
|
30157
30157
|
tool_type = p.get("tool_type")
|
|
30158
30158
|
if not tool_type:
|
|
30159
30159
|
return _err("tool_type is required")
|
|
30160
|
-
if p.get("dry_run"):
|
|
30160
|
+
if _coerce_bool(p.get("dry_run")):
|
|
30161
30161
|
return _ok(tool_type=tool_type, name=p.get("name"), would_add=True)
|
|
30162
30162
|
comp.Lock()
|
|
30163
30163
|
try:
|
|
@@ -30191,7 +30191,7 @@ def _safe_set_fusion_inputs(comp, p: Dict[str, Any]):
|
|
|
30191
30191
|
tool = comp.FindTool(tool_name)
|
|
30192
30192
|
if not tool:
|
|
30193
30193
|
return _err(f"Tool '{tool_name}' not found")
|
|
30194
|
-
if p.get("dry_run"):
|
|
30194
|
+
if _coerce_bool(p.get("dry_run")):
|
|
30195
30195
|
return _ok(tool_name=tool_name, inputs=inputs, would_set=True)
|
|
30196
30196
|
results = {}
|
|
30197
30197
|
# No comp.Lock() around value writes — see _FUSION_VALUE_WRITE_NOTE.
|
|
@@ -30225,7 +30225,7 @@ def _safe_connect_fusion_tools(comp, p: Dict[str, Any]):
|
|
|
30225
30225
|
source = comp.FindTool(source_name)
|
|
30226
30226
|
if not source:
|
|
30227
30227
|
return _err(f"Source tool '{source_name}' not found")
|
|
30228
|
-
if p.get("dry_run"):
|
|
30228
|
+
if _coerce_bool(p.get("dry_run")):
|
|
30229
30229
|
return _ok(target_tool=target_name, source_tool=source_name, input_name=input_name, would_connect=True)
|
|
30230
30230
|
comp.Lock()
|
|
30231
30231
|
try:
|
|
@@ -31763,7 +31763,7 @@ def lut(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
|
31763
31763
|
if action == "install":
|
|
31764
31764
|
if not p.get("name"):
|
|
31765
31765
|
return _err("install requires name")
|
|
31766
|
-
if p.get("dry_run"):
|
|
31766
|
+
if _coerce_bool(p.get("dry_run")):
|
|
31767
31767
|
return _ok(would_install=p["name"], writable_dir=lut_files.writable_dir())
|
|
31768
31768
|
return lut_files.install_lut(
|
|
31769
31769
|
p["name"], source=p.get("source"), source_path=p.get("source_path"),
|
|
@@ -31771,14 +31771,14 @@ def lut(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
|
31771
31771
|
if action == "remove":
|
|
31772
31772
|
if not p.get("name"):
|
|
31773
31773
|
return _err("remove requires name")
|
|
31774
|
-
if p.get("dry_run"):
|
|
31774
|
+
if _coerce_bool(p.get("dry_run")):
|
|
31775
31775
|
return _ok(would_remove=p["name"], writable_dir=lut_files.writable_dir())
|
|
31776
31776
|
return lut_files.remove_lut(p["name"])
|
|
31777
31777
|
if action == "attenuate":
|
|
31778
31778
|
for required in ("source", "strength", "name"):
|
|
31779
31779
|
if p.get(required) is None:
|
|
31780
31780
|
return _err(f"attenuate requires {required}")
|
|
31781
|
-
if p.get("dry_run"):
|
|
31781
|
+
if _coerce_bool(p.get("dry_run")):
|
|
31782
31782
|
return _ok(would_write=p["name"], source=p["source"], strength=p["strength"])
|
|
31783
31783
|
return lut_files.attenuate_lut(p["source"], p["strength"], p["name"])
|
|
31784
31784
|
except lut_files.LutPathError as exc:
|
|
@@ -32324,7 +32324,7 @@ def _safe_install_extension(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
32324
32324
|
marker = {"fuse": _FUSE_MARKER, "dctl": _DCTL_MARKER, "script": _SCRIPT_MARKER}[extension_type]
|
|
32325
32325
|
if p.get("require_marker", True) and not _source_has_marker(source, marker):
|
|
32326
32326
|
return _err(f"source must include {marker} unless require_marker=False")
|
|
32327
|
-
if p.get("dry_run"):
|
|
32327
|
+
if _coerce_bool(p.get("dry_run")):
|
|
32328
32328
|
return _ok(
|
|
32329
32329
|
would_install=True,
|
|
32330
32330
|
extension_type=extension_type,
|
|
@@ -32404,7 +32404,7 @@ def _safe_remove_extension(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
32404
32404
|
return _err(f"No {extension_type} extension named '{name}' at {path}")
|
|
32405
32405
|
if p.get("require_marker", True) and not _file_has_marker(path, marker):
|
|
32406
32406
|
return _err(f"Refusing to remove unmarked file at {path}; pass require_marker=False only if you intend this")
|
|
32407
|
-
if p.get("dry_run"):
|
|
32407
|
+
if _coerce_bool(p.get("dry_run")):
|
|
32408
32408
|
return _ok(would_remove=True, extension_type=extension_type, name=name, path=path)
|
|
32409
32409
|
try:
|
|
32410
32410
|
os.unlink(path)
|
|
@@ -18,6 +18,8 @@ See docs/authoring/script-plugin-authoring.md for the full DSL spec.
|
|
|
18
18
|
|
|
19
19
|
from typing import Any, Dict, Optional
|
|
20
20
|
|
|
21
|
+
from src.utils.bool_params import coerce_bool
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def header(name: str, kind: str, language: str) -> str:
|
|
23
25
|
"""MCP marker comment placed at the top of generated scripts."""
|
|
@@ -236,7 +238,7 @@ def media_rules(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
|
236
238
|
if language not in VALID_LANGUAGES:
|
|
237
239
|
raise ValueError(f"Invalid language '{language}'. "
|
|
238
240
|
f"Valid: {list(VALID_LANGUAGES)}")
|
|
239
|
-
dry_run =
|
|
241
|
+
dry_run = coerce_bool(options.get("dry_run"), False)
|
|
240
242
|
|
|
241
243
|
if language == "lua":
|
|
242
244
|
return _media_rules_lua(name, options, dry_run)
|