davinci-resolve-mcp 2.104.5 → 2.104.7

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 CHANGED
@@ -2,6 +2,77 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.104.7
6
+
7
+ The DRT import thread, chased to ground by live bisection on Studio 19.1.3.7
8
+ — and the tool had been giving instructions that could not work.
9
+
10
+ **What a .drt import actually requires, measured.** A real Resolve export
11
+ re-imports; the same archive minus ONLY project.xml is refused; removing
12
+ MpFolder.xml or renaming the SeqContainer path changes nothing. Tool-authored
13
+ DRTs fail on two counts: they omit project.xml AND use a flat template
14
+ container schema (<StartFrame>/<StartTC> elements) that Resolve never wrote —
15
+ its native containers are blob-based Sm2TiTrack/Sm2TiVideoClip structures.
16
+ Worse: a refused .drt import can raise a modal error dialog that BLOCKS the
17
+ scripting call indefinitely (observed live — the call neither returns nor
18
+ times out until a human dismisses the dialog), and .drt import names the
19
+ timeline after the FILE, not the container's internal name — a third naming
20
+ authority beside FCP7 (internal name) and OTIO (timelineName option). All of
21
+ it is now a submit-tagged api_truth entry.
22
+
23
+ ### Fixed
24
+
25
+ - `import_timeline_checked` refuses tool-authored .drt/.drp BEFORE calling
26
+ Resolve — the shape is detectable from the zip alone, and refusing early is
27
+ what prevents the scripting-blocking dialog. The error names the actual
28
+ cause and points at routes that work (OTIO authoring; Resolve's own .drt
29
+ exports) instead of the old media/sanitize misdiagnosis.
30
+ - `import_from_drp` no longer reports success:true when every selected
31
+ timeline failed to import (the discarded-outcome aggregation class); a
32
+ partial import is labeled partial with a warning.
33
+ - Both extractors (`import_from_drp`'s and `drt.extract_from_drp`) now carry
34
+ the source archive's project.xml into the extracted .drt — measured as
35
+ necessary. NOT yet sufficient: a .drp-sourced native container repacked
36
+ with its project.xml was still refused on 19.1.3.7, so extraction-based
37
+ import remains unreliable on this build and is documented as such.
38
+
39
+ ### Documented
40
+
41
+ - The .prproj refusal and offline-authoring guidance no longer tell users to
42
+ author a 'drt' and import it — that instruction could never work; they
43
+ point at 'otio'/'edl'. The drt tool and drt-builder docstrings state the
44
+ authored template's actual role (offline/DB workflows, injection, parsing)
45
+ and that real-Resolve exports are the only known-importable .drt files.
46
+
47
+ ## What's New in v2.104.6
48
+
49
+ **A correction to the v2.104.2 StartFrame fix — measured against Resolve
50
+ itself.** SMPTE non-drop timecode counts NOMINAL frames: the fields multiply
51
+ by the integer base (30 for 29.97, 24 for 23.976), not the exact rate.
52
+ Measured live on Studio 19.1.3.7: a 29.97 timeline at 01:00:00:00 reads
53
+ GetStartFrame 108000 = 3600 x 30, and a 23.976 one reads 86400 = 3600 x 24.
54
+ Issue #168's reporter expected round(3600 x 30000/1001) = 107892 — they said
55
+ plainly they had patched defensively without verifying Resolve — and the
56
+ v2.104.2 fix shipped that expectation. Both the original fractional product
57
+ and the rounded 107892 were wrong; the Python converters (which always used
58
+ nominal) and the Node converters now agree.
59
+
60
+ Three Node converters move to nominal-base counting, with drop-frame
61
+ handling (semicolon timecodes) matching the Python formula:
62
+
63
+ - `drt.author`'s SeqContainer StartFrame (01:00:00:00 at 29.97 now writes
64
+ 108000; at 23.976, 86400)
65
+ - `editorial.tcToFrames` — the exact-rate product undercounted NTSC
66
+ timecode by 0.1% (108 frames per hour), which touched every EDL/AAF
67
+ source/record conversion at 29.97
68
+ - `media-inventory.tcToFrames` — whose own framesToTc was already nominal,
69
+ so the tc->frames->tc round trip was asymmetric at NTSC rates until now
70
+
71
+ The conform fixtures are integer-rate, which is how the exact-rate
72
+ convention survived: nothing in the suite exercised an NTSC timecode
73
+ conversion end to end. Regression tests now pin the measured nominal values
74
+ and a drop-frame case.
75
+
5
76
  ## What's New in v2.104.5
6
77
 
7
78
  The recent bug classes, generalized into guards — and the sweeps found the
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.104.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.104.7-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.104.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.104.7-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.104.5 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.104.7 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 33 missing capabilities, 40 bugs / unreliable behaviors.
15
+ **Totals:** 33 missing capabilities, 41 bugs / unreliable behaviors.
16
16
 
17
17
  The authoritative source is the runtime-queryable `api_truth` ledger
18
18
  (`resolve_control api_truth "<query>"`); this document is generated from
@@ -526,6 +526,14 @@ values, or automation-hostile modal prompts.
526
526
  - **Reference:** [issue #171](https://github.com/samuelgursky/davinci-resolve-mcp/issues/171)
527
527
  - **Tags:** timeline, import, silent-failure, unreliable-return
528
528
 
529
+ ### MediaPool.ImportTimelineFromFile (.drt requirements and filename naming)
530
+
531
+ - **Object:** `MediaPool`
532
+ - **Signature:** `(drtPath, {importSourceClips, ...}) -> Timeline`
533
+ - **Behavior:** Measured by bisection on Studio 19.1.3.7 against a real .drt export: (1) the archive MUST contain project.xml — the same archive minus only that entry is refused, while removing MediaPool/MpFolder.xml or renaming the SeqContainer path changes nothing; (2) the container XML must be Resolve's native blob-based schema — a syntactic SeqContainer with flat template elements is refused even beside a genuine project.xml; (3) the imported timeline is named after the FILE (real_2997.drt -> 'real_2997'), not the container's internal name — a third naming authority beside FCP7 (internal name wins) and OTIO (timelineName option wins); and (4) a refused import can raise a modal error dialog that BLOCKS the scripting call until a human dismisses it — the call neither returns nor times out.
534
+ - **Workaround / current handling:** Import only .drt files Resolve itself exported — the sufficient set is unmapped beyond that: even a .drp-sourced native container repacked WITH its project.xml was refused (measured 19.1.3.7), so extraction-based .drt import cannot be relied on either. Name the timeline by naming the FILE. Never batch speculative .drt imports unattended — one refusal can hold the whole session hostage behind its dialog. timeline.import_timeline_checked detects the authored template shape up front and refuses with this diagnosis instead of importing; for a reliable programmatic import use OTIO or FCP7 XML.
535
+ - **Tags:** timeline, import, silent-failure, headless
536
+
529
537
  ### Timeline.DeleteClips (requires the Edit page; flaky first attempt)
530
538
 
531
539
  - **Object:** `Timeline`
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.104.5"
40
+ VERSION = "2.104.7"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.104.5",
3
+ "version": "2.104.7",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -23,10 +23,21 @@ const require = createRequire(import.meta.url);
23
23
  // ── timecode ───────────────────────────────────────────────────────────
24
24
  const TC_RE = /^(\d{2}):(\d{2}):(\d{2})[:;](\d{2,3})$/;
25
25
  export function tcToFrames(tc, fps) {
26
- const m = TC_RE.exec(String(tc).trim());
26
+ const raw = String(tc).trim();
27
+ const m = TC_RE.exec(raw);
27
28
  if (!m || !fps) return null;
28
29
  const [, h, mm, s, f] = m.map(Number);
29
- return Math.round((h * 3600 + mm * 60 + s) * fps) + f;
30
+ // SMPTE timecode counts NOMINAL frames (base 30 for 29.97, 24 for 23.976)
31
+ // measured against Resolve's own GetStartFrame (Studio 19.1.3.7). The
32
+ // previous exact-rate product undercounted NTSC by 0.1% (108 frames/hour).
33
+ const nominal = Math.round(fps);
34
+ let frames = (h * 3600 + mm * 60 + s) * nominal + f;
35
+ if (raw.includes(';')) {
36
+ const dropped = Math.round(nominal * 0.0666666667);
37
+ const totalMinutes = h * 60 + mm;
38
+ frames -= dropped * (totalMinutes - Math.floor(totalMinutes / 10));
39
+ }
40
+ return frames;
30
41
  }
31
42
  const isTc = (t) => TC_RE.test(t);
32
43
 
@@ -17,10 +17,21 @@ import { probeMedia } from './ffprobe-media.mjs';
17
17
  /** "HH:MM:SS:FF" (or ';' DF) → integer frames at fps. Returns null if unparseable. */
18
18
  export function tcToFrames(tc, fps) {
19
19
  if (typeof tc !== 'string' || !fps) return null;
20
- const m = /^(\d{1,2}):(\d{2}):(\d{2})[:;](\d{2,3})$/.exec(tc.trim());
20
+ const raw = tc.trim();
21
+ const m = /^(\d{1,2}):(\d{2}):(\d{2})[:;](\d{2,3})$/.exec(raw);
21
22
  if (!m) return null;
22
23
  const [, h, mm, s, f] = m.map(Number);
23
- return Math.round((h * 3600 + mm * 60 + s) * fps) + f;
24
+ // Nominal-base counting, matching framesToTc below and Resolve's own
25
+ // GetStartFrame (measured, Studio 19.1.3.7) — the exact-rate product made
26
+ // the tc->frames->tc round trip asymmetric at NTSC rates.
27
+ const nominal = Math.round(fps);
28
+ let frames = (h * 3600 + mm * 60 + s) * nominal + f;
29
+ if (raw.includes(';')) {
30
+ const dropped = Math.round(nominal * 0.0666666667);
31
+ const totalMinutes = h * 60 + mm;
32
+ frames -= dropped * (totalMinutes - Math.floor(totalMinutes / 10));
33
+ }
34
+ return frames;
24
35
  }
25
36
 
26
37
  export function framesToTc(frames, fps) {
@@ -2,7 +2,12 @@
2
2
  * drt tool — DaVinci Resolve Timeline (.drt) format. All actions local/offline.
3
3
  *
4
4
  * parse — .drt/.drp path → { timelines, metadata, seqContainers }
5
- * author — spec → .drt bytes written to outputPath
5
+ * author — spec → .drt bytes written to outputPath. TEMPLATE SCHEMA: the
6
+ * output serves offline/DB workflows (inject_into_drp, parsing, diffing);
7
+ * Resolve's ImportTimelineFromFile REFUSES it (measured 19.1.3.7 — the
8
+ * native format is blob-based with a project.xml this shape lacks). For a
9
+ * file Resolve imports, author OTIO, or extract a REAL container with
10
+ * extract_from_drp.
6
11
  * validate — .drt path → { valid, errors }
7
12
  * inject_into_drp — graft a .drt's SeqContainers into an existing .drp
8
13
  * extract_from_drp — pull a SeqContainer out of a .drp as a .drt
@@ -154,6 +159,14 @@ export const drtTool = {
154
159
  const xml = await drpZip.file(seqEntries[idx]).async('string');
155
160
  const out = new JSZip();
156
161
  out.file('Primary1/SeqContainer1.xml', xml);
162
+ // ImportTimelineFromFile refuses a .drt without project.xml (measured
163
+ // by bisection on Studio 19.1.3.7), so carry the source's over.
164
+ const projectEntry = Object.keys(drpZip.files).find(
165
+ (n) => n === 'project.xml' || n.endsWith('/project.xml'),
166
+ );
167
+ if (projectEntry) {
168
+ out.file('project.xml', await drpZip.file(projectEntry).async('string'));
169
+ }
157
170
  out.file(
158
171
  'metadata.json',
159
172
  JSON.stringify(
@@ -34,14 +34,31 @@ test('the three formerly-wrong table rates never reappear', () => {
34
34
  assert.notStrictEqual(encodeFrameRate(59.94), '286b55e253f9ed3f');
35
35
  });
36
36
 
37
- test('StartFrame is an integer at fractional rates and startFrame wins', async () => {
37
+ test('StartFrame counts NOMINAL frames and startFrame wins', async () => {
38
+ // Measured against Resolve itself (Studio 19.1.3.7): 01:00:00:00 at 29.97
39
+ // reads GetStartFrame 108000 = 3600 x 30, and 23.976 reads 86400 = 3600 x
40
+ // 24 — SMPTE NDF counts by the integer base, not the exact rate. Both the
41
+ // pre-#168 fractional product and its first rounded fix (107892) were wrong.
38
42
  const tl = { name: 'T', videoTracks: [], audioTracks: [] };
39
- const fromTc = await buildSeqContainerFile(tl, {
43
+ const ntsc30 = await buildSeqContainerFile(tl, {
40
44
  frameRate: 30000 / 1001, startTimecode: '01:00:00:00',
41
45
  });
42
- assert.match(fromTc, /<StartFrame>107892<\/StartFrame>/);
46
+ assert.match(ntsc30, /<StartFrame>108000<\/StartFrame>/);
47
+ const ntsc24 = await buildSeqContainerFile(tl, {
48
+ frameRate: 24000 / 1001, startTimecode: '01:00:00:00',
49
+ });
50
+ assert.match(ntsc24, /<StartFrame>86400<\/StartFrame>/);
43
51
  const explicit = await buildSeqContainerFile(tl, {
44
52
  frameRate: 30000 / 1001, startTimecode: '01:00:00:00', startFrame: 99999,
45
53
  });
46
54
  assert.match(explicit, /<StartFrame>99999<\/StartFrame>/);
47
55
  });
56
+
57
+ test('drop-frame start timecode subtracts the dropped numbers', async () => {
58
+ // 00:01:00;02 DF at 29.97: minute 1 drops 2 numbers -> 1800 + 2 - 2 = 1800.
59
+ const tl = { name: 'T', videoTracks: [], audioTracks: [] };
60
+ const df = await buildSeqContainerFile(tl, {
61
+ frameRate: 30000 / 1001, startTimecode: '00:01:00;02',
62
+ });
63
+ assert.match(df, /<StartFrame>1800<\/StartFrame>/);
64
+ });
@@ -363,15 +363,27 @@ function buildMarkersFieldsBlob(markers, frameRate) {
363
363
  function timecodeToFrames(timecode, fps = 24) {
364
364
  if (!timecode || typeof timecode !== 'string') return 0;
365
365
 
366
- const parts = timecode.split(':').map(Number);
367
- if (parts.length !== 4) return 0;
366
+ const dropFrame = timecode.includes(';');
367
+ const parts = timecode.replace(/;/g, ':').split(':').map(Number);
368
+ if (parts.length !== 4 || parts.some(Number.isNaN)) return 0;
368
369
 
369
370
  const [hh, mm, ss, ff] = parts;
370
- // A frame index is an integer. At fractional rates the seconds product is
371
- // fractional (3600 x 30000/1001 = 107892.107...), and writing it raw put a
372
- // fractional <StartFrame> in the XML (issue #168). Non-drop convention:
373
- // round the seconds part, then add the frame component.
374
- return Math.round(hh * 3600 * fps + mm * 60 * fps + ss * fps) + ff;
371
+ // SMPTE timecode counts NOMINAL frames: the fields multiply by the integer
372
+ // base (30 for 29.97, 24 for 23.976), not the exact rate. Measured against
373
+ // Resolve itself (Studio 19.1.3.7): a 29.97 timeline at 01:00:00:00 reads
374
+ // GetStartFrame 108000 = 3600 x 30, and a 23.976 one reads 86400 = 3600 x
375
+ // 24. Both the pre-#168 exact-rate product (107892.107...) and its rounded
376
+ // fix (107892) were wrong; nominal is what Resolve stores.
377
+ const nominal = Math.round(fps);
378
+ let frames = (hh * 3600 + mm * 60 + ss) * nominal + ff;
379
+ if (dropFrame) {
380
+ // Drop-frame skips 2 (or 4 at 59.94) TC numbers per minute except every
381
+ // tenth minute — same formula as the Python _timecode_to_frame_id.
382
+ const dropped = Math.round(nominal * 0.0666666667);
383
+ const totalMinutes = hh * 60 + mm;
384
+ frames -= dropped * (totalMinutes - Math.floor(totalMinutes / 10));
385
+ }
386
+ return frames;
375
387
  }
376
388
 
377
389
  // =============================================================================
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * DRT builder — package a timeline-only Resolve archive.
3
3
  *
4
- * DRT and DRP share the SeqContainer/MpFolder schema; the only on-disk
5
- * difference is that DRT has no project.xml. This module reuses
6
- * drp-format's packageFullDRP with includeProjectXml: false.
4
+ * DRT and DRP share the SeqContainer/MpFolder schema. NOTE (measured on
5
+ * Studio 19.1.3.7): real Resolve .drt exports DO include a project.xml, and
6
+ * ImportTimelineFromFile refuses archives without one — and refuses this
7
+ * module's template-schema containers regardless (Resolve's native container
8
+ * is blob-based Sm2TiTrack/Sm2TiVideoClip, not the flat elements written
9
+ * here). buildDRT output therefore serves offline/DB workflows (injection,
10
+ * parsing, diffing), not direct live import. This module reuses drp-format's
11
+ * packageFullDRP with includeProjectXml: false.
7
12
  *
8
13
  * Spec shape — same as buildDRP minus the project shell:
9
14
  * {
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.104.5"
90
+ VERSION = "2.104.7"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  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 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.104.5"
14
+ VERSION = "2.104.7"
15
15
 
16
16
  import base64
17
17
  import os
@@ -624,8 +624,10 @@ reverse-subclip repair, lineage, and grade tracing with no Resolve open.
624
624
 
625
625
  - Live: timeline probe_timeline_structure / detect_gaps_overlaps /
626
626
  source_range_report / conform_boundary_report; export_timeline_checked /
627
- import_timeline_checked (drt is the only lossless project-native round-trip;
628
- EDL/FCPXML drop relationships); detect_missing_media -> build_relink_plan
627
+ import_timeline_checked (a REAL-Resolve .drt exported by Resolve or pulled
628
+ out with drt.extract_from_drp is the only lossless project-native
629
+ round-trip; tool-AUTHORED .drt files use a template schema Resolve's import
630
+ refuses; EDL/FCPXML drop relationships); detect_missing_media -> build_relink_plan
629
631
  (read-only, bounded) -> media_pool.safe_relink with approved paths only.
630
632
  - XML import via the scripting API goes OFFLINE (missing-media/generators abort);
631
633
  use import_timeline_checked with media sanitize (FCP7/FCPXML), then exact-path
@@ -7016,11 +7018,14 @@ _PRPROJ_REFUSAL = (
7016
7018
  "XML, not an interchange). Two offline routes, no Premiere needed: (1) read it with the "
7017
7019
  "advanced MCP — editorial.list_sequences / editorial.parse_interchange (format 'prproj'); "
7018
7020
  "(2) convert it to an importable interchange — editorial.convert_to_interchange "
7019
- "(target 'otio'|'edl'|'drt') — then import that here with import_timeline_checked. "
7021
+ "(target 'otio'|'edl') — then import that here with import_timeline_checked. "
7020
7022
  "Editorial timing/cuts/transitions carry over; per-clip effects/Lumetri color do not. "
7021
- "Speed/reverse carry on 'otio' and 'edl' ONLY the DRT clip schema has no per-clip "
7022
- "speed field, so 'drt' flattens every retime to 100% forward and reports them in "
7023
- "`flattened`. Prefer 'otio' or 'edl' for a cut that carries retimes. "
7023
+ "Speed/reverse carry on 'otio' and 'edl' ONLY. Do NOT pick target 'drt' for a live "
7024
+ "import: tool-AUTHORED .drt files use a template schema that "
7025
+ "ImportTimelineFromFile refuses (measured on 19.1.3.7 Resolve's native "
7026
+ "container format is blob-based and includes a project.xml the authored "
7027
+ "shape lacks); authored DRTs are for offline/DB workflows, and 'drt' also "
7028
+ "flattens every retime to 100% forward (reported in `flattened`). "
7024
7029
  "Alternatively export FCP7 XML / AAF / FCPXML from Premiere and conform that."
7025
7030
  )
7026
7031
 
@@ -7071,6 +7076,33 @@ def _rewrite_fcp7_sequence_name(xml_path: str, new_name: str):
7071
7076
  return out_path, True, None
7072
7077
 
7073
7078
 
7079
+ def _drt_looks_tool_authored(path: str) -> Optional[str]:
7080
+ """Name the tell when a .drt/.drp is the authored TEMPLATE shape, else None.
7081
+
7082
+ Real Resolve archives carry a project.xml and blob-based SeqContainers
7083
+ (Sm2TiTrack/FieldsBlob children, clip <Start> elements). The tool-authored
7084
+ template instead writes flat container-level <StartFrame>/<StartTC>
7085
+ elements and omits project.xml — and ImportTimelineFromFile refuses both
7086
+ of those (measured: a real export re-imports; the same archive minus
7087
+ project.xml, or with an authored container, does not).
7088
+ """
7089
+ import zipfile
7090
+ try:
7091
+ with zipfile.ZipFile(path) as zf:
7092
+ names = zf.namelist()
7093
+ has_project_xml = any(n == "project.xml" or n.endswith("/project.xml") for n in names)
7094
+ for n in names:
7095
+ if _SEQ_CONTAINER_RE.search(n or "") and n.endswith(".xml"):
7096
+ head = zf.read(n)[:4096].decode("utf-8", "replace")
7097
+ if "<StartFrame>" in head or "<StartTC>" in head:
7098
+ return "flat container-level <StartFrame>/<StartTC> elements"
7099
+ if not has_project_xml:
7100
+ return "no project.xml in the archive"
7101
+ except Exception:
7102
+ return None
7103
+ return None
7104
+
7105
+
7074
7106
  def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
7075
7107
  path = p.get("path")
7076
7108
  if not path:
@@ -7080,6 +7112,31 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
7080
7112
  ext = os.path.splitext(path)[1].lower()
7081
7113
  if ext == ".prproj":
7082
7114
  return _err(_PRPROJ_REFUSAL, category="invalid_input")
7115
+ if ext in {".drt", ".drp"}:
7116
+ # Refuse BEFORE calling Resolve: a refused .drt import can raise a
7117
+ # modal error dialog that blocks the scripting call indefinitely
7118
+ # (measured live — the call neither returns nor times out until a
7119
+ # human dismisses the dialog), so a post-failure diagnosis may never
7120
+ # run. The authored template shape is detectable from the zip alone.
7121
+ authored_shape = _drt_looks_tool_authored(path)
7122
+ if authored_shape:
7123
+ return _err(
7124
+ f"This {ext} carries the tool-AUTHORED template schema "
7125
+ f"({authored_shape}), which Resolve's ImportTimelineFromFile "
7126
+ "refuses — the native format is blob-based and includes a "
7127
+ "project.xml (measured on Studio 19.1.3.7). Worse, the refusal "
7128
+ "raises a modal dialog that blocks all scripting until a human "
7129
+ "dismisses it, so this import is not attempted at all.",
7130
+ category="invalid_input",
7131
+ remediation=(
7132
+ "Authored DRTs serve offline/DB workflows, not live import. "
7133
+ "For a live import, author OTIO instead "
7134
+ "(editorial.convert_to_interchange target 'otio' imports "
7135
+ "cleanly), or import a REAL Resolve .drt — one Resolve "
7136
+ "exported, or one pulled from a .drp with "
7137
+ "drt.extract_from_drp."
7138
+ ),
7139
+ )
7083
7140
  # ImportTimelineFromFile silently no-ops on the never-saved default project: it returns
7084
7141
  # nothing, creates no timeline, and reports no cause. The generic "Resolve created no
7085
7142
  # timeline" error that came back instead sent people to source-clip resolution and
@@ -7379,13 +7436,26 @@ def _drp_seq_containers(zf) -> List[Dict[str, Any]]:
7379
7436
 
7380
7437
 
7381
7438
  def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str) -> None:
7382
- """Write a minimal .drt (zip) holding one SeqContainer as Primary1/SeqContainer1.xml."""
7439
+ """Write a .drt (zip) holding one SeqContainer plus the source's project.xml.
7440
+
7441
+ ImportTimelineFromFile refuses a .drt without project.xml (measured by
7442
+ bisection on Studio 19.1.3.7: a real export minus only project.xml is
7443
+ refused; project.xml + container with no MpFolder imports). The source
7444
+ .drp/.drt carries one — copy it, or the extract cannot import.
7445
+ """
7383
7446
  import zipfile
7384
7447
 
7385
7448
  with zipfile.ZipFile(drp_path, "r") as zf:
7386
7449
  xml = zf.read(seq_entry)
7450
+ project_xml = None
7451
+ for candidate in zf.namelist():
7452
+ if candidate == "project.xml" or candidate.endswith("/project.xml"):
7453
+ project_xml = zf.read(candidate)
7454
+ break
7387
7455
  with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as out:
7388
7456
  out.writestr("Primary1/SeqContainer1.xml", xml)
7457
+ if project_xml is not None:
7458
+ out.writestr("project.xml", project_xml)
7389
7459
  out.writestr(
7390
7460
  "metadata.json",
7391
7461
  json.dumps(
@@ -7494,7 +7564,10 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
7494
7564
  imported_count += 1
7495
7565
  results.append(entry)
7496
7566
 
7497
- return _ok(
7567
+ # Top-level success must reflect the rows: _ok() over a result list whose
7568
+ # every import failed read as a successful call (the #161 aggregation
7569
+ # class — the wrapper reported what it meant to do, not what happened).
7570
+ out = _ok(
7498
7571
  drpPath=drp_path,
7499
7572
  selected=len(selected),
7500
7573
  imported=imported_count,
@@ -7502,6 +7575,19 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
7502
7575
  results=results,
7503
7576
  dry_run=bool(p.get("dry_run")),
7504
7577
  )
7578
+ if not p.get("dry_run") and selected and imported_count == 0:
7579
+ out["success"] = False
7580
+ out["error"] = (
7581
+ f"None of the {len(selected)} selected timeline(s) imported — "
7582
+ "see results[].error for each cause."
7583
+ )
7584
+ elif not p.get("dry_run") and imported_count < len(selected):
7585
+ out["partial"] = True
7586
+ out["warning"] = (
7587
+ f"{imported_count} of {len(selected)} selected timeline(s) "
7588
+ "imported; see results[].error for the failures."
7589
+ )
7590
+ return out
7505
7591
 
7506
7592
 
7507
7593
  def _timeline_by_selector(proj, p: Dict[str, Any], *, prefix: str):
@@ -23761,7 +23847,12 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
23761
23847
  metric; a conflicting relink is VETOED — reverted and reported under
23762
23848
  `sanitize.flagged` for human review, never silently applied. verify_threshold
23763
23849
  (default 0.90) sets the structural-match bar.
23764
- import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results}
23850
+ import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results, partial?}
23851
+ CAVEAT (measured 19.1.3.7): Resolve refuses extracted containers even
23852
+ repacked with the source's project.xml — the sufficient import set
23853
+ beyond Resolve's own .drt exports is unmapped, so expect failures on
23854
+ this build and read results[].error; success reflects the rows. For a
23855
+ reliable programmatic import use OTIO or FCP7 XML.
23765
23856
  Extract chosen timelines from a .drp (offline zip surgery → temp .drt each) and
23766
23857
  import each into the running Resolve. Omit the selector to import ALL. Enumerate
23767
23858
  first with the advanced MCP drt.list_sequences / editorial.list_sequences to get
@@ -1879,6 +1879,39 @@ API_TRUTH: List[Dict[str, Any]] = [
1879
1879
  "submit": "bug",
1880
1880
  "issue": 171,
1881
1881
  },
1882
+ {
1883
+ "symbol": "MediaPool.ImportTimelineFromFile (.drt requirements and filename naming)",
1884
+ "object": "MediaPool",
1885
+ "signature": "(drtPath, {importSourceClips, ...}) -> Timeline",
1886
+ "reality": "Measured by bisection on Studio 19.1.3.7 against a real "
1887
+ ".drt export: (1) the archive MUST contain project.xml — "
1888
+ "the same archive minus only that entry is refused, while "
1889
+ "removing MediaPool/MpFolder.xml or renaming the "
1890
+ "SeqContainer path changes nothing; (2) the container XML "
1891
+ "must be Resolve's native blob-based schema — a syntactic "
1892
+ "SeqContainer with flat template elements is refused even "
1893
+ "beside a genuine project.xml; (3) the imported timeline is "
1894
+ "named after the FILE (real_2997.drt -> 'real_2997'), not "
1895
+ "the container's internal name — a third naming authority "
1896
+ "beside FCP7 (internal name wins) and OTIO (timelineName "
1897
+ "option wins); and (4) a refused import can raise a modal "
1898
+ "error dialog that BLOCKS the scripting call until a human "
1899
+ "dismisses it — the call neither returns nor times out.",
1900
+ "recommended": "Import only .drt files Resolve itself exported — "
1901
+ "the sufficient set is unmapped beyond that: even a "
1902
+ ".drp-sourced native container repacked WITH its "
1903
+ "project.xml was refused (measured 19.1.3.7), so "
1904
+ "extraction-based .drt import cannot be relied on "
1905
+ "either. Name the timeline by naming the FILE. Never "
1906
+ "batch speculative .drt imports unattended — one "
1907
+ "refusal can hold the whole session hostage behind "
1908
+ "its dialog. timeline.import_timeline_checked detects "
1909
+ "the authored template shape up front and refuses "
1910
+ "with this diagnosis instead of importing; for a "
1911
+ "reliable programmatic import use OTIO or FCP7 XML.",
1912
+ "tags": ["timeline", "import", "silent-failure", "headless"],
1913
+ "submit": "bug",
1914
+ },
1882
1915
  {
1883
1916
  "symbol": "MediaPool.AppendToTimeline clipInfo recordFrame (timeline-absolute origin)",
1884
1917
  "object": "MediaPool",