davinci-resolve-mcp 2.23.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/AGENTS.md +85 -0
- package/CHANGELOG.md +802 -0
- package/CLAUDE.md +15 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +53 -0
- package/bin/davinci-resolve-mcp.mjs +376 -0
- package/docs/README.md +56 -0
- package/docs/SKILL.md +1145 -0
- package/docs/authoring/fuse-dctl-authoring.md +242 -0
- package/docs/authoring/script-plugin-authoring.md +195 -0
- package/docs/contributing.md +82 -0
- package/docs/guides/color-decision-guide.md +387 -0
- package/docs/guides/editorial-decision-guide.md +136 -0
- package/docs/guides/media-analysis-guide.md +615 -0
- package/docs/guides/multicam-setup-guide.md +138 -0
- package/docs/install.md +198 -0
- package/docs/integrations/workflow-integrations.md +120 -0
- package/docs/kernels/README.md +28 -0
- package/docs/kernels/audio-fairlight-kernel.md +86 -0
- package/docs/kernels/color-grade-kernel.md +103 -0
- package/docs/kernels/extension-authoring-kernel.md +101 -0
- package/docs/kernels/fusion-composition-kernel.md +91 -0
- package/docs/kernels/media-pool-ingest-kernel.md +147 -0
- package/docs/kernels/project-lifecycle-kernel.md +120 -0
- package/docs/kernels/render-deliver-kernel.md +92 -0
- package/docs/kernels/review-annotation-kernel.md +110 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
- package/docs/kernels/timeline-edit-kernel.md +189 -0
- package/docs/notes/codec-plugin-notes.md +136 -0
- package/docs/notes/dctl-notes.md +234 -0
- package/docs/notes/fusion-template-notes.md +136 -0
- package/docs/notes/lut-notes.md +136 -0
- package/docs/notes/openfx-notes.md +120 -0
- package/docs/process/release-process.md +152 -0
- package/docs/reference/api-coverage.md +488 -0
- package/docs/reference/resolve_scripting_api.txt +1012 -0
- package/examples/README.md +53 -0
- package/examples/markers/README.md +81 -0
- package/examples/media/README.md +94 -0
- package/examples/timeline/README.md +98 -0
- package/install.py +1196 -0
- package/package.json +52 -0
- package/scripts/audit_api_parity.py +275 -0
- package/scripts/live_media_analysis_polish_probe.py +65 -0
- package/src/__init__.py +3 -0
- package/src/analysis_dashboard.py +4936 -0
- package/src/control_panel.py +13 -0
- package/src/granular/__init__.py +17 -0
- package/src/granular/common.py +727 -0
- package/src/granular/folder.py +287 -0
- package/src/granular/gallery.py +306 -0
- package/src/granular/graph.py +309 -0
- package/src/granular/media_pool.py +679 -0
- package/src/granular/media_pool_item.py +852 -0
- package/src/granular/media_storage.py +179 -0
- package/src/granular/project.py +1594 -0
- package/src/granular/resolve_control.py +521 -0
- package/src/granular/timeline.py +1074 -0
- package/src/granular/timeline_item.py +2251 -0
- package/src/resolve_mcp_server.py +43 -0
- package/src/server.py +15691 -0
- package/src/utils/__init__.py +3 -0
- package/src/utils/app_control.py +319 -0
- package/src/utils/audio_fairlight_live_probe.py +263 -0
- package/src/utils/cdl.py +20 -0
- package/src/utils/cloud_operations.py +192 -0
- package/src/utils/color_grade_live_probe.py +444 -0
- package/src/utils/dctl_templates.py +368 -0
- package/src/utils/extension_authoring_live_probe.py +292 -0
- package/src/utils/fuse_templates.py +1968 -0
- package/src/utils/fusion_composition_live_probe.py +284 -0
- package/src/utils/layout_presets.py +333 -0
- package/src/utils/mcp_stdio.py +32 -0
- package/src/utils/media_analysis.py +3618 -0
- package/src/utils/media_analysis_jobs.py +796 -0
- package/src/utils/media_pool_ingest_live_probe.py +592 -0
- package/src/utils/multicam.py +393 -0
- package/src/utils/object_inspection.py +287 -0
- package/src/utils/platform.py +157 -0
- package/src/utils/project_lifecycle_live_probe.py +376 -0
- package/src/utils/project_properties.py +601 -0
- package/src/utils/render_deliver_live_probe.py +384 -0
- package/src/utils/resolve_connection.py +77 -0
- package/src/utils/review_annotation_live_probe.py +352 -0
- package/src/utils/script_templates.py +1193 -0
- package/src/utils/sync_detection.py +887 -0
- package/src/utils/timeline_conform_live_probe.py +280 -0
- package/src/utils/timeline_kernel_live_probe.py +1091 -0
- package/src/utils/timeline_kernel_probe.py +185 -0
- package/src/utils/timeline_title_text.py +87 -0
- package/src/utils/update_check.py +610 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Platform-specific functionality for DaVinci Resolve MCP Server
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import platform
|
|
9
|
+
|
|
10
|
+
def get_platform():
|
|
11
|
+
"""Identify the current operating system platform.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: 'windows', 'darwin' (macOS), or 'linux'
|
|
15
|
+
"""
|
|
16
|
+
system = platform.system().lower()
|
|
17
|
+
if system == 'darwin':
|
|
18
|
+
return 'darwin'
|
|
19
|
+
elif system == 'windows':
|
|
20
|
+
return 'windows'
|
|
21
|
+
elif system == 'linux':
|
|
22
|
+
return 'linux'
|
|
23
|
+
return system
|
|
24
|
+
|
|
25
|
+
def get_resolve_paths():
|
|
26
|
+
"""Get platform-specific paths for DaVinci Resolve scripting API.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
dict: Dictionary containing api_path, lib_path, and modules_path
|
|
30
|
+
"""
|
|
31
|
+
platform_name = get_platform()
|
|
32
|
+
|
|
33
|
+
if platform_name == 'darwin': # macOS
|
|
34
|
+
api_path = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
|
|
35
|
+
lib_path = "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
|
|
36
|
+
modules_path = os.path.join(api_path, "Modules")
|
|
37
|
+
|
|
38
|
+
elif platform_name == 'windows': # Windows
|
|
39
|
+
program_files = os.environ.get('PROGRAMDATA', 'C:\\ProgramData')
|
|
40
|
+
program_files_64 = os.environ.get('PROGRAMFILES', 'C:\\Program Files')
|
|
41
|
+
|
|
42
|
+
api_path = os.path.join(program_files, 'Blackmagic Design', 'DaVinci Resolve', 'Support', 'Developer', 'Scripting')
|
|
43
|
+
lib_path = os.path.join(program_files_64, 'Blackmagic Design', 'DaVinci Resolve', 'fusionscript.dll')
|
|
44
|
+
modules_path = os.path.join(api_path, "Modules")
|
|
45
|
+
|
|
46
|
+
elif platform_name == 'linux': # Linux (not fully implemented)
|
|
47
|
+
# Default locations for Linux - these may need to be adjusted
|
|
48
|
+
api_path = "/opt/resolve/Developer/Scripting"
|
|
49
|
+
lib_path = "/opt/resolve/libs/fusionscript.so"
|
|
50
|
+
modules_path = os.path.join(api_path, "Modules")
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
# Fallback to macOS paths if unknown platform
|
|
54
|
+
api_path = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
|
|
55
|
+
lib_path = "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
|
|
56
|
+
modules_path = os.path.join(api_path, "Modules")
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"api_path": api_path,
|
|
60
|
+
"lib_path": lib_path,
|
|
61
|
+
"modules_path": modules_path
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def get_resolve_plugin_paths():
|
|
65
|
+
"""Get platform-specific paths for Resolve plugin install dirs.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
dict: {
|
|
69
|
+
'fuses_dir': Fusion Fuses directory,
|
|
70
|
+
'dctl_dir': LUT directory (where regular .dctl files live),
|
|
71
|
+
'aces_idt_dir': ACES IDT transforms (separate scan path; restart
|
|
72
|
+
required after install),
|
|
73
|
+
'aces_odt_dir': ACES ODT transforms (same caveat),
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
76
|
+
platform_name = get_platform()
|
|
77
|
+
home = os.path.expanduser("~")
|
|
78
|
+
|
|
79
|
+
# NOTE: The Fuse SDK doc (June 2023) lists Fuses under "Support/Fusion/Fuses"
|
|
80
|
+
# on macOS, but the directory Resolve actually scans is the sibling
|
|
81
|
+
# "Fusion/Fuses" (without "Support") — verified live against Resolve Studio
|
|
82
|
+
# 20.3.2.9 by writing test fuses to both paths and observing which loaded.
|
|
83
|
+
# Per-platform conventions also differ from the SDK doc; we follow the
|
|
84
|
+
# canonical Fusion user-data layout where every Fusion user directory
|
|
85
|
+
# (Macros, Templates, Scripts, Modules, Fuses, ...) lives directly under
|
|
86
|
+
# the platform's Fusion user root.
|
|
87
|
+
if platform_name == 'darwin':
|
|
88
|
+
support = os.path.join(home, "Library", "Application Support",
|
|
89
|
+
"Blackmagic Design", "DaVinci Resolve")
|
|
90
|
+
fuses_dir = os.path.join(support, "Fusion", "Fuses")
|
|
91
|
+
dctl_dir = os.path.join(support, "LUT")
|
|
92
|
+
aces_root = os.path.join(support, "ACES Transforms")
|
|
93
|
+
elif platform_name == 'windows':
|
|
94
|
+
appdata = os.environ.get('APPDATA', os.path.join(home, 'AppData', 'Roaming'))
|
|
95
|
+
fuses_dir = os.path.join(appdata, 'Blackmagic Design', 'DaVinci Resolve',
|
|
96
|
+
'Support', 'Fusion', 'Fuses')
|
|
97
|
+
dctl_dir = os.path.join(appdata, 'Blackmagic Design', 'DaVinci Resolve',
|
|
98
|
+
'Support', 'LUT')
|
|
99
|
+
aces_root = os.path.join(appdata, 'Blackmagic Design', 'DaVinci Resolve',
|
|
100
|
+
'Support', 'ACES Transforms')
|
|
101
|
+
elif platform_name == 'linux':
|
|
102
|
+
base = os.path.join(home, '.local', 'share', 'DaVinciResolve')
|
|
103
|
+
fuses_dir = os.path.join(base, 'Fusion', 'Fuses')
|
|
104
|
+
dctl_dir = os.path.join(base, 'LUT')
|
|
105
|
+
aces_root = os.path.join(base, 'ACES Transforms')
|
|
106
|
+
else:
|
|
107
|
+
support = os.path.join(home, "Library", "Application Support",
|
|
108
|
+
"Blackmagic Design", "DaVinci Resolve")
|
|
109
|
+
fuses_dir = os.path.join(support, "Fusion", "Fuses")
|
|
110
|
+
dctl_dir = os.path.join(support, "LUT")
|
|
111
|
+
aces_root = os.path.join(support, "ACES Transforms")
|
|
112
|
+
|
|
113
|
+
# Resolve scans these subdirs of Fusion/Scripts/ at startup and exposes
|
|
114
|
+
# them in Workspace → Scripts → <category>. Categories per Resolve docs.
|
|
115
|
+
if platform_name == 'darwin':
|
|
116
|
+
scripts_root = os.path.join(support, "Fusion", "Scripts")
|
|
117
|
+
elif platform_name == 'windows':
|
|
118
|
+
scripts_root = os.path.join(appdata, 'Blackmagic Design', 'DaVinci Resolve',
|
|
119
|
+
'Support', 'Fusion', 'Scripts')
|
|
120
|
+
elif platform_name == 'linux':
|
|
121
|
+
scripts_root = os.path.join(base, 'Fusion', 'Scripts')
|
|
122
|
+
else:
|
|
123
|
+
scripts_root = os.path.join(support, "Fusion", "Scripts")
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"fuses_dir": fuses_dir,
|
|
127
|
+
"dctl_dir": dctl_dir,
|
|
128
|
+
"aces_idt_dir": os.path.join(aces_root, "IDT"),
|
|
129
|
+
"aces_odt_dir": os.path.join(aces_root, "ODT"),
|
|
130
|
+
"scripts_root": scripts_root,
|
|
131
|
+
# Category subdirs Resolve actually scans (verified live):
|
|
132
|
+
"scripts_categories": ("Edit", "Color", "Deliver", "Comp",
|
|
133
|
+
"Tool", "Utility", "Views"),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def setup_environment():
|
|
138
|
+
"""Set up environment variables for DaVinci Resolve scripting.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
bool: True if setup was successful, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
paths = get_resolve_paths()
|
|
145
|
+
|
|
146
|
+
os.environ["RESOLVE_SCRIPT_API"] = paths["api_path"]
|
|
147
|
+
os.environ["RESOLVE_SCRIPT_LIB"] = paths["lib_path"]
|
|
148
|
+
|
|
149
|
+
# Add modules path to Python's path if it's not already there
|
|
150
|
+
if paths["modules_path"] not in sys.path:
|
|
151
|
+
sys.path.append(paths["modules_path"])
|
|
152
|
+
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"Error setting up environment: {str(e)}")
|
|
157
|
+
return False
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live Project / Database / Archive boundary probe."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
19
|
+
if not isinstance(result, dict):
|
|
20
|
+
raise AssertionError(f"{label}: expected dict, got {result!r}")
|
|
21
|
+
if result.get("error"):
|
|
22
|
+
raise AssertionError(f"{label}: {result['error']}")
|
|
23
|
+
if "success" in result and result["success"] is not True:
|
|
24
|
+
raise AssertionError(f"{label}: expected success=True, got {result!r}")
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _record_tool_result(
|
|
29
|
+
recorder: ProbeRecorder,
|
|
30
|
+
category: str,
|
|
31
|
+
name: str,
|
|
32
|
+
result: Dict[str, Any],
|
|
33
|
+
*,
|
|
34
|
+
expected_status: Optional[str] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
if not isinstance(result, dict):
|
|
37
|
+
recorder.record(category, name, "error", details={"reason": "non-dict result", "result": repr(result)})
|
|
38
|
+
return
|
|
39
|
+
if result.get("error"):
|
|
40
|
+
recorder.record(category, name, expected_status or "error", details={"reason": result.get("error")}, evidence=result)
|
|
41
|
+
return
|
|
42
|
+
if "success" in result and result["success"] is not True:
|
|
43
|
+
recorder.record(
|
|
44
|
+
category,
|
|
45
|
+
name,
|
|
46
|
+
expected_status or "partially_supported",
|
|
47
|
+
details={"reason": "success returned false"},
|
|
48
|
+
evidence=result,
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
recorder.record(category, name, expected_status or "supported", evidence=result)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _delete_disposable_project(server, name: str) -> Dict[str, Any]:
|
|
55
|
+
result = server.project_manager(
|
|
56
|
+
"safe_project_delete",
|
|
57
|
+
{"name": name, "close_current": True, "save_current": True},
|
|
58
|
+
)
|
|
59
|
+
if result.get("error") and "currently open" in result.get("error", ""):
|
|
60
|
+
result = server.project_manager("safe_project_delete", {"name": name})
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _first_setting_value(snapshot: Dict[str, Any], keys: list[str]) -> Optional[Dict[str, Any]]:
|
|
65
|
+
settings = snapshot.get("settings")
|
|
66
|
+
if not isinstance(settings, dict):
|
|
67
|
+
return None
|
|
68
|
+
for key in keys:
|
|
69
|
+
value = settings.get(key)
|
|
70
|
+
if value not in (None, ""):
|
|
71
|
+
return {key: value}
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, Any]:
|
|
76
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
work_dir = Path(tempfile.mkdtemp(prefix="mcp_project_lifecycle_probe_"))
|
|
78
|
+
timestamp = int(time.time())
|
|
79
|
+
project_name = f"_mcp_project_lifecycle_probe_{timestamp}"
|
|
80
|
+
imported_name = f"_mcp_project_lifecycle_import_{timestamp}"
|
|
81
|
+
restored_name = f"_mcp_project_lifecycle_restore_{timestamp}"
|
|
82
|
+
archive_restored_name = f"_mcp_project_lifecycle_archive_restore_{timestamp}"
|
|
83
|
+
timeline_name = "Project Lifecycle Probe Timeline"
|
|
84
|
+
folder_name = f"_mcp_project_folder_{timestamp}"
|
|
85
|
+
layout_name = f"_mcp_layout_probe_{timestamp}"
|
|
86
|
+
layout_import_name = f"_mcp_layout_import_{timestamp}"
|
|
87
|
+
cleanup_projects: list[str] = []
|
|
88
|
+
cleanup_layouts: list[str] = []
|
|
89
|
+
delete_results: Dict[str, Any] = {}
|
|
90
|
+
recorder = ProbeRecorder()
|
|
91
|
+
|
|
92
|
+
metadata: Dict[str, Any] = {
|
|
93
|
+
"title": "Project / Database / Archive Kernel Capability Probe",
|
|
94
|
+
"timestamp_utc": utc_timestamp(),
|
|
95
|
+
"python": sys.version,
|
|
96
|
+
"platform": platform.platform(),
|
|
97
|
+
"output_dir": str(output_dir),
|
|
98
|
+
"work_dir": str(work_dir),
|
|
99
|
+
"project_name": project_name,
|
|
100
|
+
"imported_project_name": imported_name,
|
|
101
|
+
"restored_project_name": restored_name,
|
|
102
|
+
"archive_restored_project_name": archive_restored_name,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
version = _require_success("resolve_control.get_version", server.resolve_control("get_version"))
|
|
107
|
+
metadata.update(
|
|
108
|
+
{
|
|
109
|
+
"product": version.get("product"),
|
|
110
|
+
"version": version.get("version"),
|
|
111
|
+
"version_string": version.get("version_string"),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
print(f"Connected to {metadata['product']} {metadata['version_string']}")
|
|
115
|
+
|
|
116
|
+
_record_tool_result(recorder, "capabilities", "project_capabilities_pre_create", server.project_manager("project_capabilities"))
|
|
117
|
+
_record_tool_result(recorder, "database", "database_capabilities_pre_create", server.project_manager("database_capabilities"))
|
|
118
|
+
db_caps = server.project_manager("database_capabilities")
|
|
119
|
+
if isinstance(db_caps, dict) and db_caps.get("current"):
|
|
120
|
+
_record_tool_result(
|
|
121
|
+
recorder,
|
|
122
|
+
"database",
|
|
123
|
+
"safe_set_current_database_dry_run",
|
|
124
|
+
server.project_manager("safe_set_current_database", {"db_info": db_caps["current"]}),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
create_result = server.project_manager("safe_project_create", {"name": project_name})
|
|
128
|
+
_require_success("project_manager.safe_project_create", create_result)
|
|
129
|
+
cleanup_projects.append(project_name)
|
|
130
|
+
print(f"Created disposable project: {project_name}")
|
|
131
|
+
|
|
132
|
+
_record_tool_result(recorder, "lifecycle", "safe_project_create", create_result)
|
|
133
|
+
_record_tool_result(recorder, "lifecycle", "get_current", server.project_manager("get_current"))
|
|
134
|
+
_record_tool_result(recorder, "lifecycle", "create_empty_timeline", server.media_pool("create_timeline", {"name": timeline_name}))
|
|
135
|
+
_record_tool_result(recorder, "lifecycle", "save_project_before_export", server.project_manager("save"))
|
|
136
|
+
_record_tool_result(recorder, "lifecycle", "probe_project_lifecycle", server.project_manager("probe_project_lifecycle"))
|
|
137
|
+
_record_tool_result(recorder, "settings", "project_settings_snapshot", server.project_manager("project_settings_snapshot"))
|
|
138
|
+
|
|
139
|
+
snapshot = server.project_manager("project_settings_snapshot")
|
|
140
|
+
setting_payload = _first_setting_value(
|
|
141
|
+
snapshot if isinstance(snapshot, dict) else {},
|
|
142
|
+
["timelineResolutionWidth", "timelineResolutionHeight", "timelineFrameRate"],
|
|
143
|
+
)
|
|
144
|
+
if setting_payload:
|
|
145
|
+
_record_tool_result(
|
|
146
|
+
recorder,
|
|
147
|
+
"settings",
|
|
148
|
+
"safe_set_project_settings_same_value_restore",
|
|
149
|
+
server.project_manager("safe_set_project_settings", {"settings": setting_payload, "restore": True}),
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
recorder.record(
|
|
153
|
+
"settings",
|
|
154
|
+
"safe_set_project_settings_same_value_restore",
|
|
155
|
+
"not_applicable",
|
|
156
|
+
details={"reason": "No non-empty candidate project setting was readable"},
|
|
157
|
+
evidence=snapshot,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
_record_tool_result(
|
|
161
|
+
recorder,
|
|
162
|
+
"settings",
|
|
163
|
+
"probe_project_settings_try_write_dry_run",
|
|
164
|
+
server.project_manager("probe_project_settings", {"try_write": True, "dry_run": True}),
|
|
165
|
+
)
|
|
166
|
+
_record_tool_result(recorder, "presets", "preset_lifecycle_probe", server.project_manager("preset_lifecycle_probe"))
|
|
167
|
+
|
|
168
|
+
drp_path = work_dir / "project-lifecycle-export.drp"
|
|
169
|
+
archive_path = work_dir / "project-lifecycle-archive.dra"
|
|
170
|
+
archive_folder_path = work_dir / "project-lifecycle-archive-folder"
|
|
171
|
+
_record_tool_result(
|
|
172
|
+
recorder,
|
|
173
|
+
"lifecycle",
|
|
174
|
+
"safe_project_export",
|
|
175
|
+
server.project_manager("safe_project_export", {"name": project_name, "path": str(drp_path)}),
|
|
176
|
+
)
|
|
177
|
+
if drp_path.exists():
|
|
178
|
+
import_result = server.project_manager("safe_project_import", {"name": imported_name, "path": str(drp_path)})
|
|
179
|
+
if import_result.get("success"):
|
|
180
|
+
cleanup_projects.append(imported_name)
|
|
181
|
+
_record_tool_result(recorder, "lifecycle", "safe_project_import", import_result)
|
|
182
|
+
|
|
183
|
+
restore_result = server.project_manager("safe_project_restore", {"name": restored_name, "path": str(drp_path)})
|
|
184
|
+
if restore_result.get("success"):
|
|
185
|
+
cleanup_projects.append(restored_name)
|
|
186
|
+
_record_tool_result(recorder, "lifecycle", "safe_project_restore_from_drp", restore_result)
|
|
187
|
+
else:
|
|
188
|
+
recorder.record(
|
|
189
|
+
"lifecycle",
|
|
190
|
+
"safe_project_import",
|
|
191
|
+
"not_applicable",
|
|
192
|
+
details={"reason": "DRP export path was not created"},
|
|
193
|
+
)
|
|
194
|
+
recorder.record(
|
|
195
|
+
"lifecycle",
|
|
196
|
+
"safe_project_restore_from_drp",
|
|
197
|
+
"not_applicable",
|
|
198
|
+
details={"reason": "DRP export path was not created"},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
archive_file_result = server.project_manager(
|
|
202
|
+
"safe_project_archive",
|
|
203
|
+
{
|
|
204
|
+
"name": project_name,
|
|
205
|
+
"path": str(archive_path),
|
|
206
|
+
"src_media": False,
|
|
207
|
+
"render_cache": False,
|
|
208
|
+
"proxy_media": False,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
_record_tool_result(
|
|
212
|
+
recorder,
|
|
213
|
+
"archive",
|
|
214
|
+
"safe_project_archive_no_media_dra_path",
|
|
215
|
+
archive_file_result,
|
|
216
|
+
expected_status=None if archive_file_result.get("success") else "partially_supported",
|
|
217
|
+
)
|
|
218
|
+
archive_folder_result = server.project_manager(
|
|
219
|
+
"safe_project_archive",
|
|
220
|
+
{
|
|
221
|
+
"name": project_name,
|
|
222
|
+
"path": str(archive_folder_path),
|
|
223
|
+
"src_media": False,
|
|
224
|
+
"render_cache": False,
|
|
225
|
+
"proxy_media": False,
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
_record_tool_result(
|
|
229
|
+
recorder,
|
|
230
|
+
"archive",
|
|
231
|
+
"safe_project_archive_no_media_folder_path",
|
|
232
|
+
archive_folder_result,
|
|
233
|
+
expected_status=None if archive_folder_result.get("success") else "partially_supported",
|
|
234
|
+
)
|
|
235
|
+
archive_restore_source = archive_path if archive_file_result.get("success") else archive_folder_path
|
|
236
|
+
if archive_file_result.get("success") or archive_folder_result.get("success"):
|
|
237
|
+
archive_restore = server.project_manager(
|
|
238
|
+
"safe_project_restore",
|
|
239
|
+
{"name": archive_restored_name, "path": str(archive_restore_source)},
|
|
240
|
+
)
|
|
241
|
+
if archive_restore.get("success"):
|
|
242
|
+
cleanup_projects.append(archive_restored_name)
|
|
243
|
+
_record_tool_result(
|
|
244
|
+
recorder,
|
|
245
|
+
"archive",
|
|
246
|
+
"safe_project_restore_from_archive",
|
|
247
|
+
archive_restore,
|
|
248
|
+
expected_status=None if archive_restore.get("success") else "partially_supported",
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
recorder.record(
|
|
252
|
+
"archive",
|
|
253
|
+
"safe_project_restore_from_archive",
|
|
254
|
+
"not_applicable",
|
|
255
|
+
details={"reason": "No archive path was created successfully"},
|
|
256
|
+
)
|
|
257
|
+
_record_tool_result(
|
|
258
|
+
recorder,
|
|
259
|
+
"archive",
|
|
260
|
+
"safe_project_archive_rejects_media_flags",
|
|
261
|
+
server.project_manager(
|
|
262
|
+
"safe_project_archive",
|
|
263
|
+
{"name": project_name, "path": str(work_dir / "reject.dra"), "src_media": True, "dry_run": True},
|
|
264
|
+
),
|
|
265
|
+
expected_status="unsupported",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
_record_tool_result(recorder, "folders", "folder_list_before", server.project_manager_folders("list"))
|
|
269
|
+
_record_tool_result(recorder, "folders", "folder_create", server.project_manager_folders("create", {"name": folder_name}))
|
|
270
|
+
_record_tool_result(recorder, "folders", "folder_open", server.project_manager_folders("open", {"name": folder_name}))
|
|
271
|
+
_record_tool_result(recorder, "folders", "folder_get_current", server.project_manager_folders("get_current"))
|
|
272
|
+
_record_tool_result(recorder, "folders", "folder_goto_parent", server.project_manager_folders("goto_parent"))
|
|
273
|
+
_record_tool_result(recorder, "folders", "folder_delete", server.project_manager_folders("delete", {"name": folder_name}))
|
|
274
|
+
|
|
275
|
+
for page in ["media", "cut", "edit", "fusion", "color", "fairlight", "deliver"]:
|
|
276
|
+
_record_tool_result(
|
|
277
|
+
recorder,
|
|
278
|
+
"app_state",
|
|
279
|
+
f"open_page_{page}",
|
|
280
|
+
server.resolve_control("open_page", {"page": page}),
|
|
281
|
+
expected_status=None,
|
|
282
|
+
)
|
|
283
|
+
_record_tool_result(recorder, "app_state", "get_page", server.resolve_control("get_page"))
|
|
284
|
+
keyframe_mode = server.resolve_control("get_keyframe_mode")
|
|
285
|
+
_record_tool_result(recorder, "app_state", "get_keyframe_mode", keyframe_mode)
|
|
286
|
+
if isinstance(keyframe_mode, dict) and "mode" in keyframe_mode:
|
|
287
|
+
_record_tool_result(
|
|
288
|
+
recorder,
|
|
289
|
+
"app_state",
|
|
290
|
+
"set_keyframe_mode_same_value",
|
|
291
|
+
server.resolve_control("set_keyframe_mode", {"mode": keyframe_mode["mode"]}),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
layout_path = work_dir / "layout-preset.layout"
|
|
295
|
+
layout_save = server.layout_presets("save", {"name": layout_name})
|
|
296
|
+
if layout_save.get("success"):
|
|
297
|
+
cleanup_layouts.append(layout_name)
|
|
298
|
+
_record_tool_result(recorder, "presets", "layout_save", layout_save)
|
|
299
|
+
_record_tool_result(recorder, "presets", "layout_update", server.layout_presets("update", {"name": layout_name}))
|
|
300
|
+
_record_tool_result(recorder, "presets", "layout_load", server.layout_presets("load", {"name": layout_name}))
|
|
301
|
+
_record_tool_result(recorder, "presets", "layout_export", server.layout_presets("export", {"name": layout_name, "path": str(layout_path)}))
|
|
302
|
+
if layout_path.exists():
|
|
303
|
+
layout_import = server.layout_presets("import_preset", {"name": layout_import_name, "path": str(layout_path)})
|
|
304
|
+
if layout_import.get("success"):
|
|
305
|
+
cleanup_layouts.append(layout_import_name)
|
|
306
|
+
_record_tool_result(recorder, "presets", "layout_import", layout_import)
|
|
307
|
+
else:
|
|
308
|
+
recorder.record(
|
|
309
|
+
"presets",
|
|
310
|
+
"layout_import",
|
|
311
|
+
"not_applicable",
|
|
312
|
+
details={"reason": "layout export path was not created"},
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
preset_probe = server.project_manager("preset_lifecycle_probe")
|
|
316
|
+
render_presets = []
|
|
317
|
+
if isinstance(preset_probe, dict):
|
|
318
|
+
render_presets = preset_probe.get("render_presets", {}).get("items", []) or []
|
|
319
|
+
if render_presets:
|
|
320
|
+
render_preset_name = render_presets[0].get("Name") if isinstance(render_presets[0], dict) else render_presets[0]
|
|
321
|
+
if render_preset_name:
|
|
322
|
+
_record_tool_result(
|
|
323
|
+
recorder,
|
|
324
|
+
"presets",
|
|
325
|
+
"render_preset_export",
|
|
326
|
+
server.render_presets("export_render", {"name": render_preset_name, "path": str(work_dir / "render-preset.xml")}),
|
|
327
|
+
expected_status=None,
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
recorder.record(
|
|
331
|
+
"presets",
|
|
332
|
+
"render_preset_export",
|
|
333
|
+
"not_applicable",
|
|
334
|
+
details={"reason": "No render presets were listed"},
|
|
335
|
+
evidence=preset_probe,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
_record_tool_result(recorder, "report", "project_boundary_report", server.project_manager("project_boundary_report"))
|
|
339
|
+
|
|
340
|
+
if keep_open:
|
|
341
|
+
server.project_manager("save")
|
|
342
|
+
print(f"LEFT PROJECTS OPEN FOR INSPECTION: {cleanup_projects}")
|
|
343
|
+
cleanup_projects = []
|
|
344
|
+
|
|
345
|
+
finally:
|
|
346
|
+
for layout_name_to_delete in reversed(cleanup_layouts):
|
|
347
|
+
delete_results[f"layout:{layout_name_to_delete}"] = server.layout_presets("delete", {"name": layout_name_to_delete})
|
|
348
|
+
print(f"Deleted layout preset {layout_name_to_delete}: {delete_results[f'layout:{layout_name_to_delete}']}")
|
|
349
|
+
if not keep_open:
|
|
350
|
+
for disposable_project in reversed(cleanup_projects):
|
|
351
|
+
delete_results[f"project:{disposable_project}"] = _delete_disposable_project(server, disposable_project)
|
|
352
|
+
print(f"Deleted disposable project {disposable_project}: {delete_results[f'project:{disposable_project}']}")
|
|
353
|
+
|
|
354
|
+
metadata["cleanup"] = delete_results
|
|
355
|
+
report = recorder.to_report(
|
|
356
|
+
metadata,
|
|
357
|
+
{
|
|
358
|
+
"json": str(output_dir / "project-lifecycle-probe.json"),
|
|
359
|
+
"markdown": str(output_dir / "project-lifecycle-probe.md"),
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
json_path = output_dir / "project-lifecycle-probe.json"
|
|
363
|
+
markdown_path = output_dir / "project-lifecycle-probe.md"
|
|
364
|
+
json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
|
|
365
|
+
markdown_path.write_text(render_markdown_report(report), encoding="utf-8")
|
|
366
|
+
print(f"Wrote JSON report: {json_path}")
|
|
367
|
+
print(f"Wrote Markdown report: {markdown_path}")
|
|
368
|
+
print(f"Counts: {json.dumps(report['counts'], sort_keys=True)}")
|
|
369
|
+
if not keep_open:
|
|
370
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
371
|
+
print(f"Removed project lifecycle work directory: {work_dir}")
|
|
372
|
+
|
|
373
|
+
failed_cleanup = {key: value for key, value in delete_results.items() if value.get("success") is not True and not value.get("error", "").startswith("No ")}
|
|
374
|
+
if failed_cleanup:
|
|
375
|
+
raise AssertionError(f"Cleanup failed: {failed_cleanup!r}")
|
|
376
|
+
return report
|