01 / BEFORE THE CODE
До кода — условия запуска.
Подключение уже проверено
Используйте локальный внешний Python и запущенный Resolve Studio. Сначала выполните руководство по подключению к Resolve. Эти примеры используют синтаксис Python 3.8+, но совместимость интерпретатора с native-библиотекой проверяется отдельно.
Явная область чтения и записи
Откройте нужный тестовый проект и выберите папку Media Pool вручную. ID в отчёте важнее совпадения имён. Во время запуска не переключайте проект, папку и таймлайн. Нет обхода всех папок проекта или автоматического поиска похожего клипа.
План перед изменением
Сохраните копию проекта вручную. Без --apply выполняется только план. Для записи нужны точные ID цели; для файлов — также digest просмотренного списка. После отправки изменяющего вызова ошибка не означает, что ничего не произошло.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
GetMediaPool()- Вход
- Без аргументов.
- Возвращает
- Объект MediaPool.
- Проверить
- Нужен открытый проект, но не таймлайн.
GetCurrentFolder()- Вход
- Без аргументов.
- Возвращает
- Текущая Folder.
- Проверить
- Проверьте ID, а не только название.
ImportMedia([paths])- Вход
- Список путей файлов/папок; пример принимает только явные файлы.
- Возвращает
- Список MediaPoolItem.
- Проверить
- Перечитайте папку и сопоставьте все входы.
GetClipList()- Вход
- Без аргументов; только выбранная папка.
- Возвращает
- Список элементов папки.
- Проверить
- None — не пустая папка; дочерние папки не обходятся.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]
03 / UNDERSTAND
ImportMedia работает с Media Pool, не с таймлайном
Цепочка объектов здесь заканчивается на project.GetMediaPool(). Текущий таймлайн не нужен: импортировать материал можно до его создания. Выбранная папка Media Pool — целевая папка импорта; корень диска с исходниками — другое понятие. Поэтому отсутствие текущего таймлайна не должно останавливать самостоятельный импорт.
В примере используется форма ImportMedia([paths]). Она возвращает объекты MediaPoolItem, а не готовые клипы на монтажных дорожках. Проверка присутствия записи в Media Pool ещё не доказывает, что файл декодируется, звук настроен верно или материал доступен другому компьютеру проекта.[3]
04 / UNDERSTAND
Пути проверяются до импорта, без молчаливого фильтра
Передавайте абсолютные пути на компьютере, где работают Python и Resolve. Относительный путь зависит от рабочей папки терминала, а путь D:/media/shot.mov не становится Linux-путём после замены разделителей. В Windows удобно писать D:/media/shot.mov либо raw-строку r"D:\media\shot.mov" в Python. Не помещайте raw-строку с одиночным обратным слешем в конце.
prepare_files проверяет обычные файлы и возможность открытия, обнаруживает повтор одного файла или подтверждённого файлового alias. Ничего не удаляется из запроса автоматически. Digest строится по упорядоченным путям и метаданным файлов; это защита от случайной смены набора между планом и запуском, не хеш содержимого и не блокировка источников.[7][8]
05 / UNDERSTAND
Дубликат — это отдельное решение, а не повод перезаписать словарь
Перед записью пример читает текущую папку. Уже найденный там исходник блокирует повторный импорт; для повторного использования есть отдельный режим чтения в статье о сопоставлении путей. Это проверка одной папки, не всей медиатеки: одинаковый файл в другом bin не будет автоматически найден. Скрипт не удаляет и не объединяет такие записи.
Не используйте zip(paths, imported) как доказательство соответствия и не подменяйте отсутствующий элемент похожим именем. Наш zip выполняется только после однозначного сопоставления. Если объект вернулся без пригодного File Path, наш узкий маршрут не может подтвердить результат и останавливается. Для последовательностей изображений и специальных источников нужен собственный контракт импорта.[3][7]
06 / UNDERSTAND
Что проверить после вызова — и когда нельзя повторять
Откройте импортированные исходники в Viewer, проверьте изображение, звук, длину и свойства. При предложении сменить FPS проекта примите решение вручную до добавления на таймлайн. Пример не управляет диалогами и не меняет настройки за вас; нативный вызов может ждать действия пользователя.
Если ImportMedia вернул None, неполный набор или вызвал исключение, часть изменений могла уже произойти. OutcomeUnknown означает остановку и самостоятельную сверку Media Pool, а не разрешение повторить импорт. Автоматических SaveProject, удаления созданных объектов, rollback и ретраев нет.[3]
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Скачайте import_media.py, resolve_api_media.py и resolve_api_connection.py. В комплекте — шесть примеров, оба helper и инструкции RU/EN. Весь код helper доступен ниже; изучите его перед --apply.
Скачать все примеры с инструкцией (.zip) ↓"""Import explicit files into the current Media Pool folder; PLAN ONLY by default."""
import argparse
from resolve_api_connection import connect
from resolve_api_media import context, import_once, add_write_arguments, expected_from, output
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+", help="Absolute local paths; all must exist")
add_write_arguments(parser, files=True)
args = parser.parse_args(argv)
# Import does not require or create a timeline.
ctx = context(connect())
report, _ = import_once(ctx, args.files, args.apply, expected_from(args), args.expect_files_sha256)
output(report)
# Review the imported entries, source FPS and project settings in Resolve.
# No SaveProject, automatic media copying, rollback or retry is performed.
if __name__ == "__main__":
main()
Media-helper — цели, сопоставление и проверка изменений
Короткий основной скрипт вызывает эту общую реализацию. Это авторский учебный код, не официальный SDK. --apply разрешает только выбранный этап. Проверки не заменяют блокировку, изоляцию процесса и систему восстановления.
"""Shared tutorial helpers. Local desktop only; no sandbox, lock or API timeout.
A native call may block. Save a test copy yourself and do not edit concurrently.
Paths in reports can be sensitive. No relink, delete, retry or rollback is performed.
Python 3.8+ syntax; live Resolve/Python compatibility is a separate check.
"""
import hashlib
import json
import math
import os
import re
from dataclasses import dataclass
from pathlib import Path
from resolve_api_connection import project_manager
MAX_ITEMS = 500 # Tutorial processing limit, NOT a Resolve limit.
class OutcomeUnknown(RuntimeError):
"""A modifying call was attempted. Inspect actual state before any retry."""
def uid(obj):
if obj is None:
raise RuntimeError("Required API object is absent")
value = obj.GetUniqueId()
if not isinstance(value, str) or not value.strip():
raise RuntimeError("Object identity is unavailable")
return value
def api_list(value, label):
# Do not turn an unavailable API result (None) into an empty collection.
if not isinstance(value, list) or len(value) > MAX_ITEMS:
raise RuntimeError(label + ": expected a list of at most 500 entries")
return value
def properties(item):
result = item.GetClipProperty()
if not isinstance(result, dict) or not all(isinstance(k, str) for k in result):
raise RuntimeError("GetClipProperty did not return a property dictionary")
return result
def path_key(raw):
"""Conservative native path key. Never lowercase or translate another OS path."""
if not isinstance(raw, str) or not raw or any(ord(c) < 32 for c in raw):
raise ValueError("A non-empty path without control characters is required")
if not os.path.isabs(raw):
raise ValueError("An absolute path in this operating system is required")
# Collapsing '..' through a symlink can point at another file: reject instead.
parts = raw.replace(os.altsep, os.sep).split(os.sep) if os.altsep else raw.split(os.sep)
if ".." in parts:
raise ValueError("Resolve parent-directory segments explicitly before using this tutorial")
return os.path.normpath(raw) # Retain case and drive/share identity.
def same_path(left, right):
left, right = path_key(left), path_key(right)
if left == right:
return True
try:
# Resolve aliases only when the local filesystem can prove identity.
return os.path.samefile(left, right)
except OSError:
return False # Unknown equivalence is not a filename-based match.
def prepare_files(raw_paths):
if not isinstance(raw_paths, list) or not 1 <= len(raw_paths) <= MAX_ITEMS:
raise ValueError("Provide 1-500 file paths")
paths, stamps = [], []
for raw in raw_paths:
path = path_key(str(Path(raw).expanduser()))
if not Path(path).is_file():
raise ValueError("Not a readable ordinary file: " + path)
with open(path, "rb"):
pass # Permission check only; not codec/decode validation.
if any(same_path(path, existing) for existing in paths):
raise ValueError("Duplicate input file/alias: " + path)
stat = os.stat(path)
paths.append(path)
stamps.append([path, stat.st_dev, stat.st_ino, stat.st_size, stat.st_mtime_ns])
digest = hashlib.sha256(json.dumps(stamps, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest()
return paths, digest # Metadata digest, NOT a content checksum or a lock.
def folder_files(raw_folder, extensions):
folder = Path(path_key(str(Path(raw_folder).expanduser())))
if not folder.is_dir():
raise ValueError("Source folder does not exist")
ext = {x.strip().lower().lstrip(".") for x in extensions.split(",") if x.strip()}
if not ext or not all(re.fullmatch(r"[a-z0-9]+", x) for x in ext):
raise ValueError("Use explicit comma-separated extensions, e.g. mov,mp4")
# Read one folder only. No recursion, symlink files, sequences or hidden entries.
entries = list(folder.iterdir())
if len(entries) > MAX_ITEMS:
raise ValueError("Folder exceeds tutorial limit of 500 entries")
candidates = [x for x in entries if not x.is_symlink() and x.is_file()
and not x.name.startswith(".") and x.suffix.lower().lstrip(".") in ext]
def natural_key(p):
parts = re.split(r"(\d+)", p.name.casefold())
return (tuple((1, int(x)) if x.isdigit() else (0, x) for x in parts), p.name)
return prepare_files([str(x) for x in sorted(candidates, key=natural_key)])
@dataclass
class Context:
resolve: object
manager: object
project: object
pool: object
folder: object
timeline: object
target: dict
def context(resolve, need_timeline=False):
manager = project_manager(resolve)
project = manager.GetCurrentProject()
if not project:
raise RuntimeError("Open a test project manually")
pool = project.GetMediaPool()
folder = pool.GetCurrentFolder() if pool else None
if not pool or not folder:
raise RuntimeError("Current Media Pool folder is unavailable")
timeline = project.GetCurrentTimeline() if need_timeline else None
target = {"project_id": uid(project), "folder_id": uid(folder)}
if need_timeline:
if not timeline:
raise RuntimeError("Open a test timeline manually")
target["timeline_id"] = uid(timeline)
return Context(resolve, manager, project, pool, folder, timeline, target)
def ensure_current(ctx):
current = context(ctx.resolve, ctx.timeline is not None)
if current.target != ctx.target:
raise RuntimeError("Active project/folder/timeline changed")
def expect_target(ctx, expected):
if not isinstance(expected, dict) or any(expected.get(k) != v for k, v in ctx.target.items()):
raise ValueError("--apply requires all exact target IDs from the reviewed plan")
ensure_current(ctx)
def folder_items(ctx):
entries = api_list(ctx.folder.GetClipList(), "Folder.GetClipList")
ids = [uid(x) for x in entries]
if len(ids) != len(set(ids)):
raise RuntimeError("Duplicate object IDs in folder result")
return entries
def order_by_paths(paths, items, allow_other=False):
"""One-to-one matching by path/file identity. Missing/ambiguous => stop."""
items = api_list(items, "MediaPoolItem list")
observed = []
for item in items:
props = properties(item)
stored = props.get("File Path")
observed.append((uid(item), item, stored if isinstance(stored, str) and stored else None))
if len({x[0] for x in observed}) != len(observed):
raise RuntimeError("Duplicate MediaPoolItem identity")
result, selected = [], set()
for path in paths:
matches = []
for ident, item, stored in observed:
if stored:
try:
match = same_path(path, stored)
except ValueError:
match = False
if match:
matches.append((ident, item))
if len(matches) != 1 or matches[0][0] in selected:
raise RuntimeError("Missing/ambiguous mapping for: " + path)
selected.add(matches[0][0]); result.append(matches[0][1])
if not allow_other and len(selected) != len(observed):
raise RuntimeError("Unrequested or pathless items in import result")
return result
def import_once(ctx, raw_paths, apply=False, expected=None, expected_digest=None):
paths, digest = prepare_files(raw_paths)
before_items = folder_items(ctx)
before_ids = {uid(x) for x in before_items}
collisions = []
for item in before_items:
stored = properties(item).get("File Path")
if isinstance(stored, str) and stored:
try:
collisions.extend(p for p in paths if same_path(p, stored))
except ValueError:
pass # Non-native/compound path is not guessed or converted.
report = {"status": "plan_only", "target": ctx.target, "files": paths,
"files_metadata_sha256": digest, "existing_in_current_folder": collisions,
"duplicate_scope": "current_folder_only", "write_attempted": False}
ensure_current(ctx)
if not apply:
return report, []
expect_target(ctx, expected)
if digest != expected_digest:
raise ValueError("File manifest differs from the reviewed --expect-files-sha256")
if collisions:
raise ValueError("Files already present in current folder. No reimport/reuse fallback")
if {uid(x) for x in folder_items(ctx)} != before_ids or prepare_files(paths)[1] != digest:
raise RuntimeError("Folder contents or input metadata changed before import")
ensure_current(ctx)
try:
imported = ctx.pool.ImportMedia(paths) # Exactly ONE call; no hidden retry.
ensure_current(ctx)
ordered = order_by_paths(paths, imported)
after_ids = {uid(x) for x in folder_items(ctx)}
returned_ids = {uid(x) for x in ordered}
if returned_ids & before_ids or after_ids != before_ids | returned_ids:
raise RuntimeError("Observed folder delta differs from import result")
except Exception as exc:
raise OutcomeUnknown("Import attempted; completeness unconfirmed. Inspect Media Pool; do not retry blindly.") from exc
report.update(status="import_observed_review_in_resolve", write_attempted=True,
mapping=[{"path": p, "media_pool_id": uid(i)} for p, i in zip(paths, ordered)])
return report, ordered
def timeline_rows(timeline):
rows = []
for track_type in ("video", "audio", "subtitle"):
count = timeline.GetTrackCount(track_type)
if type(count) is not int or not 0 <= count <= 128:
raise RuntimeError("Unexpected track count")
for index in range(1, count + 1):
locked = timeline.GetIsTrackLocked(track_type, index)
if type(locked) is not bool:
raise RuntimeError("Track lock state is unavailable")
for item in api_list(timeline.GetItemListInTrack(track_type, index), "Track items"):
source = item.GetMediaPoolItem()
start, end = item.GetStart(True), item.GetEnd(True)
if any(type(x) not in (int, float) or not math.isfinite(x) for x in (start, end)) or end <= start:
raise RuntimeError("Unreadable or invalid timeline item bounds")
rows.append({"id": uid(item), "source_id": uid(source) if source else None,
"type": track_type, "track": index, "start": start, "end": end,
"locked": locked})
if len(rows) > MAX_ITEMS:
raise RuntimeError("Timeline exceeds tutorial processing limit")
if locked:
raise RuntimeError("Unlock test timeline tracks manually before this tutorial")
if len({r["id"] for r in rows}) != len(rows):
raise RuntimeError("Duplicate timeline item identities")
return rows
def append_once(ctx, items, apply=False, expected=None):
if ctx.timeline is None:
raise RuntimeError("This operation needs an active test timeline")
items = api_list(items, "Append inputs")
ids = [uid(i) for i in items]
if not ids or len(ids) != len(set(ids)):
raise ValueError("Choose distinct MediaPoolItem IDs in the intended order")
original = timeline_rows(ctx.timeline)
report = {"status": "plan_only", "target": ctx.target, "ordered_media_pool_ids": ids,
"timeline_is_empty": not original, "write_attempted": False,
"placement": "whole items; default append; audio layout and timing require UI review"}
ensure_current(ctx)
if not apply:
return report
expect_target(ctx, expected)
if original:
raise RuntimeError("This tutorial only writes to an EMPTY test timeline")
available = {uid(x) for x in folder_items(ctx)}
if not set(ids) <= available or timeline_rows(ctx.timeline):
raise RuntimeError("Inputs/empty timeline changed before append")
ensure_current(ctx)
try:
appended = api_list(ctx.pool.AppendToTimeline(items), "AppendToTimeline")
if not appended:
raise RuntimeError("No appended objects returned")
ensure_current(ctx)
observed = timeline_rows(ctx.timeline)
returned_ids = {uid(i) for i in appended}
if len(returned_ids) != len(appended) or not returned_ids <= {r["id"] for r in observed}:
raise RuntimeError("Returned timeline objects could not be reread")
if {r["source_id"] for r in observed} != set(ids):
raise RuntimeError("Observed sources differ from the requested inputs")
order = {ident: idx for idx, ident in enumerate(ids)}
tracks = {(r["type"], r["track"]) for r in observed}
for kind, index in tracks:
track_rows = sorted((r for r in observed if (r["type"], r["track"]) == (kind, index)), key=lambda r: r["start"])
positions = [order[r["source_id"]] for r in track_rows]
if positions != sorted(positions):
raise RuntimeError("Observed source order differs on a track")
except Exception as exc:
raise OutcomeUnknown("Append attempted; result needs inspection. Do not repeat, delete or auto-rollback.") from exc
report.update(status="append_observed_review_audio_and_timing", write_attempted=True,
returned_timeline_item_ids=sorted(returned_ids), observed_items=observed)
return report
def select_ids(ctx, item_ids):
if not item_ids or len(item_ids) != len(set(item_ids)):
raise ValueError("Provide distinct MediaPoolItem IDs")
by_id = {uid(x): x for x in folder_items(ctx)}
if any(ident not in by_id for ident in item_ids):
raise ValueError("Requested item is absent from the current Media Pool folder")
return [by_id[ident] for ident in item_ids]
def add_write_arguments(parser, timeline=False, files=False):
parser.add_argument("--apply", action="store_true", help="Explicitly allow this phase to modify the TEST project")
parser.add_argument("--expect-project-id")
parser.add_argument("--expect-folder-id")
if timeline:
parser.add_argument("--expect-timeline-id")
if files:
parser.add_argument("--expect-files-sha256", help="Metadata manifest digest from reviewed plan, NOT file content hash")
def expected_from(args, timeline=False):
result = {"project_id": args.expect_project_id, "folder_id": args.expect_folder_id}
if timeline:
result["timeline_id"] = args.expect_timeline_id
return result
def output(value):
print(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False))
Общий helper подключения — посмотреть и скачать
Этот файл находит и загружает установленный мост. При импорте Python может записывать кэш байткода, а нативный вызов — зависнуть; это не песочница и не механизм тайм-аута. Используйте доверенную локальную установку.
"""Load the installed Blackmagic module; do not install a similarly named package.
This helper uses standard desktop installation paths or RESOLVE_SCRIPT_API.
It does not enable scripting, start Resolve, open projects or retry API calls.
Validate Python/native-library compatibility against your installed README.txt.
"""
from __future__ import annotations
import importlib.util
import os
from pathlib import Path
import struct
import sys
class ResolveConnectionError(RuntimeError):
"""An explicit environment, module, or application-connection failure."""
def scripting_root() -> Path:
configured = os.environ.get("RESOLVE_SCRIPT_API")
if configured is not None:
if not configured.strip() or configured != configured.strip():
raise ResolveConnectionError("RESOLVE_SCRIPT_API is empty or has outer whitespace")
if configured.startswith(('"', "'")) or configured.endswith(('"', "'")):
raise ResolveConnectionError("Do not store literal outer quotes in RESOLVE_SCRIPT_API")
root = Path(os.path.expandvars(configured)).expanduser()
elif sys.platform == "win32":
root = Path(os.environ.get("PROGRAMDATA", r"C:\ProgramData")) / "Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting"
elif sys.platform == "darwin":
root = Path("/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting")
elif sys.platform.startswith("linux"):
root = Path("/opt/resolve/Developer/Scripting")
else:
raise ResolveConnectionError("Set RESOLVE_SCRIPT_API for this platform/installation")
if not root.is_absolute():
raise ResolveConnectionError("RESOLVE_SCRIPT_API must be an absolute Scripting directory")
return root
def module_file() -> Path:
# RESOLVE_SCRIPT_API points to Scripting, not its Modules subdirectory.
return scripting_root() / "Modules" / "DaVinciResolveScript.py"
def load_resolve_module():
if struct.calcsize("P") * 8 != 64:
raise ResolveConnectionError("Use a compatible 64-bit Python interpreter")
source = module_file()
if not source.is_file():
raise ResolveConnectionError("Official DaVinciResolveScript.py not found at: " + str(source))
configured_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
if configured_lib is not None:
lib = Path(os.path.expandvars(configured_lib)).expanduser()
if not lib.is_absolute() or not lib.is_file():
raise ResolveConnectionError("RESOLVE_SCRIPT_LIB must name the installed native library file")
name = "DaVinciResolveScript"
spec = importlib.util.spec_from_file_location(name, source)
if spec is None or spec.loader is None:
raise ResolveConnectionError("Cannot create a loader for the official module")
previous = sys.modules.get(name)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
try:
spec.loader.exec_module(module)
# Blackmagic's wrapper can replace its sys.modules entry with a native module.
loaded = sys.modules[name]
if not callable(getattr(loaded, "scriptapp", None)):
raise ResolveConnectionError("Loaded module does not expose scriptapp")
return loaded
except Exception as exc:
if previous is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = previous
raise ResolveConnectionError("Official module was found, but loading it failed") from exc
def connect():
module = load_resolve_module()
try:
resolve = module.scriptapp("Resolve")
except Exception as exc:
raise ResolveConnectionError("scriptapp failed; inspect the original exception") from exc
if not resolve:
raise ResolveConnectionError("No Resolve object: check running Studio, Local access and Python environment")
return resolve
def project_manager(resolve):
manager = resolve.GetProjectManager()
if not manager:
raise ResolveConnectionError("Resolve connected, but Project Manager is unavailable")
return manager
Команды запуска
План без записи
python import_media.py "D:/API_Test/shot_01.mov" "D:/API_Test/shot_02.mov"Замените путь и все ID/digest значениями своего просмотренного плана. На macOS/Linux используйте python3 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.
Применить просмотренный план
python import_media.py "D:/API_Test/shot_01.mov" "D:/API_Test/shot_02.mov" --apply --expect-project-id "PROJECT_ID" --expect-folder-id "FOLDER_ID" --expect-files-sha256 "MANIFEST_SHA256"Замените путь и все ID/digest значениями своего просмотренного плана. На macOS/Linux используйте python3 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.
Что делает код
Список
Каждый путь проверяется до обращения к изменяющему методу. Отсутствующий файл останавливает весь план, а не исчезает из списка.
Импорт
Скрипт импортирует один список в текущую папку только после --apply и совпадения ID/digest. Папка не переключается автоматически.
Сверка
Объекты сопоставляются по путям; затем перечитывается состав папки. Непустой список сам по себе не считается полным результатом.
Проверить helper
Полная общая реализация доступна под основным примером. Изучите её область действия и условия остановки, а не только короткий файл запуска.
Файл, запись Media Pool и элемент таймлайна — разные сущности.
Проверка дубликатов ограничена текущей папкой; автоматического копирования исходников в пакет скриптов нет.
Digest отражает метаданные списка, не содержимое файлов. При неизвестном исходе сначала сверяйте Resolve.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
Файл не найден
Возможная причинаНеверный путь, другой компьютер или недоступный диск.
Следующая проверкаИсправьте весь список и снова постройте план. Не отбрасывайте файл незаметно.
ID или digest не совпали
Возможная причинаПоменялись цель, набор или метаданные файлов.
Следующая проверкаПеречитайте новый план; старое разрешение не переносится.
Existing in current folder
Возможная причинаИсходник уже сопоставлен в выбранной папке.
Следующая проверкаИспользуйте read-existing; не удаляйте клип ради прохождения проверки.
OutcomeUnknown после импорта
Возможная причинаОтвет и наблюдаемый состав не дали полного подтверждения.
Следующая проверкаОстановитесь; сверьте фактические записи и пути. Не повторяйте вызов автоматически.
PRACTICE / YOUR OWN EVIDENCE
Два файла и одна намеренная ошибка
- Возьмите два коротких обычных файла в пустую тестовую папку Media Pool; получите план без таймлайна.
- Добавьте неверный третий путь. Убедитесь, что до --apply импорт не произошёл и ошибка не скрыта.
- Исправьте путь, заново просмотрите план и примените его с точными ID/digest. Проверьте два источника в Resolve.
Все входы отражены в результате; на таймлайне ничего не добавлено; повторный импорт в ту же папку блокируется.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Нужно ли создавать Timeline перед импортом?
Нет. Пример требует только открытый проект и текущую папку Media Pool. Создание таймлайна — отдельное действие.[3]
Проверка is_file гарантирует поддержку формата?
Нет. Это проверка файловой системы, а не контейнера, кодека или декодирования в Resolve.[8]
Можно передать папку одним путём?
Контракт ImportMedia принимает и папки, но этот пример намеренно ограничен явными файлами. Контролируемый обход каталога разобран в соседней статье.[3]
Почему успешный статус ещё требует просмотра?
Сверка API подтверждает сопоставление записей, не качество декодирования, настройку FPS, аудиоканалы и сохранение проекта на диск.
SOURCES / VERSION BOUNDARIES
Источники, а не подразумеваемая сертификация.
Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.
- Blackmagic Design — DaVinci Resolve Studio 21.1 release notes
8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.
- Blackmagic Design — Support Center
Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.
- Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)
Публичная копия первичного README Blackmagic, поддерживаемая X-Raym. По ней сверены базовые сигнатуры; это не официальный хостинг и не README 21.1.1. Оригинал своей установки: Help → Documentation → Developer → Scripting → README.txt; расположение пункта зависит от версии.
- Blackmagic Design — Scripting and Automation
Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.
- Python — os.path: samefile, normpath and platform rules
Различие строкового пути и файлового объекта. В примерах нет принудительного lower(): эквивалентность разных написаний принимается только после проверки локальной файловой системой.
- Python — pathlib
Обход каталога, абсолютные пути и проверки файлов. Расширение не подтверждает поддержку кодека или успешное декодирование.