Python library¶
The library is the source of truth — the CLI, daemon, and MCP server are all thin shells over it.
Connecting¶
from dvr import Resolve
r = Resolve() # auto-launches Resolve if needed
r = Resolve(auto_launch=False) # raise if Resolve isn't running
r = Resolve(timeout=60) # wait up to 60s for the connection
The connection handles macOS's LAN-IP quirk and timeouts every underlying API call so a hung Resolve can't deadlock you.
Domain accessors¶
r.app # page, version, product, quit
r.project # list, current, create, load, ensure, delete, archive, export, import
r.timeline # list, current, get, ensure, create, switch, delete (project-scoped)
r.render # queue, presets, formats, codecs, submit, watch, status, stop
r.storage # filesystem-side: volumes, file lists, bulk import
Within a project:
project = r.project.ensure("MyShow")
project.timeline # same as r.timeline once project is current
project.media # MediaPool
project.gallery # stills, PowerGrades
project.set_setting(key, value)
Within a timeline:
tl = r.timeline.current
tl.tracks("video") # all video tracks
tl.track("video", 2) # V2
tl.clips("video") # ClipQuery over all video clips
tl.clips("video").where(lambda c: c.duration > 48)
tl.markers() # {frame: {...}}
tl.add_marker(120, color="Red", name="check sync")
Within a clip:
clip = tl.clips("video").first()
clip.inspect() # full state
clip.set_property("Pan", 0.25) # transform/composite/retime
clip.color.set_cdl(slope=(1, 1, 1, 0.95)) # color page operations
clip.color.export_lut("/Volumes/luts/grade.cube", size=33)
clip.fusion.add() # add a Fusion comp
clip.takes.add(asset) # alternate takes
clip.replace("/Volumes/new_source.mov") # relink, preserves grades
Idempotent context managers¶
with r.project.use("MyShow") as project:
with project.timeline.use("Edit_v2") as tl:
# ...
pass
# previous project + timeline restored on exit
# Scoped project setting flips — restored on exit, even on exception.
with project.setting_context("colorAcesODT", "Rec.709 BT.1886"):
r.render.submit_and_wait(...)
# previous colorAcesODT value restored
Querying¶
# Find clips matching a predicate.
short_clips = tl.clips("video").where(lambda c: c.duration < 24)
print(len(short_clips))
for clip in short_clips:
clip.add_marker(color="Red", name="too short")
# Compose queries.
v2_long = tl.clips("video").where(lambda c: c.track_index == 2 and c.duration > 48)
Renders¶
job = r.render.submit(
target_dir="/Volumes/out",
custom_name="MyShow_v2",
format="mov",
codec="ProRes4444XQ",
)
job.wait() # block with stall detection
print(job.output_path)
# Or stream events:
for event in r.render.watch([job.id]):
print(event)
# One-shot: submit, block, return the rendered path.
output = r.render.submit_and_wait(
target_dir="/Volumes/out",
custom_name="MyShow_v2",
format="mov",
codec="ProRes4444XQ",
)
# Normalized status snapshot — same payload as RenderJob.poll().
snap = r.render.status(job.id)
print(snap["status"], snap["percent"], snap["error"])
# Safe between shots — bounded queue cleanup with timeout, even after
# image-sequence (EXR / DPX) jobs leave the queue stuck at 100%.
r.render.clear()
Media imports¶
# Idempotent: returns the existing pool clip if the path is already
# imported, otherwise imports it. Useful when many shots come from one
# master and you don't want a duplicate Media Pool entry per shot.
clip = project.media.find_or_import("/Volumes/raw/master_v003.mov")
# IMF (Interoperable Master Format) — pass the OV folder, not the CPL.
clips = project.media.import_imf("/Volumes/deliveries/IMF_OV/")
Interchange¶
from dvr import interchange
interchange.export(tl, "out.fcpxml", format="fcpxml-1.10")
interchange.export(tl, "out.edl", format="edl-cdl")
interchange.export(tl, "out.aaf", format="aaf")
print(interchange.export_formats()) # all 21 supported names
new_tl = interchange.import_(project.media, "incoming.aaf")
Audio and Gallery¶
from dvr import audio, gallery
audio.set_voice_isolation(tl, enabled=True, amount=70)
audio.apply_fairlight_preset(project, "Dialogue Smooth")
g = gallery.gallery_for(project)
album = g.create_still_album("Hero shots")
album.import_stills(["/Volumes/stills/01.png"])
Errors¶
Every failure raises a subclass of dvr.errors.DvrError. See Errors and diagnostics.
Type hints¶
The library ships with py.typed and is checked in mypy --strict mode. Domain wrappers that wrap the inherently-Any Resolve handles relax warn_return_any for ergonomics; everywhere else, types are precise.