01 / BEFORE THE CODE
До кода — условия запуска.
Подключение уже проверено
Используйте локальный внешний Python и запущенный Resolve Studio. Сначала выполните руководство по подключению к Resolve. Эти примеры используют синтаксис Python 3.8+, но совместимость интерпретатора с native-библиотекой проверяется отдельно.
Явная область чтения и записи
Откройте нужный тестовый проект и выберите папку Media Pool вручную. ID в отчёте важнее совпадения имён. Во время запуска не переключайте проект, папку и таймлайн. Нет обхода всех папок проекта или автоматического поиска похожего клипа.
Снимок, не изменение
Скрипт не импортирует и не перелинковывает файлы. JSON может содержать полные пути, имена и метаданные: перед публикацией обезличьте его. Частичный отчёт отмечается явно и завершается кодом 2.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
GetClipList()- Вход
- Текущий bin.
- Возвращает
- Список MediaPoolItem.
- Проверить
- Без рекурсии и обязательного таймлайна.
GetClipProperty()- Вход
- Без аргументов.
- Возвращает
- Словарь доступных свойств.
- Проверить
- Сохраните реальные ключи и значения.
GetClipProperty(propertyName)- Вход
- Точный ключ, проверенный в словаре.
- Возвращает
- Значение выбранного свойства.
- Проверить
- Пустой результат не заменяйте значением по умолчанию.
GetUniqueId()- Вход
- Без аргументов.
- Возвращает
- Идентификатор объекта Media Pool.
- Проверить
- Не совпадает по смыслу с именем файла и ID TimelineItem.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]
03 / UNDERSTAND
Исходник в Media Pool и вхождение на Timeline — разные уровни
GetClipProperty вызывается у MediaPoolItem. Для этого не нужно искать исходник через монтажную дорожку: project.GetMediaPool().GetCurrentFolder().GetClipList() даёт элементы выбранного bin напрямую. Такой отчёт работает и до создания таймлайна, а один источник не повторяется только потому, что использован в монтаже десять раз.
Если нужна связь со вхождением, сначала получите TimelineItem.GetMediaPoolItem() и затем читайте свойства источника. Но положение на дорожке, границы монтажного фрагмента и локальные трансформации относятся уже к TimelineItem; их нельзя выдавать за свойства самого файла.[3]
04 / UNDERSTAND
Сначала полный словарь, затем точечный ключ
Первый запуск должен показать доступные ключи именно вашей версии и типа материала. Не угадывайте, что FilePath, File Path и file_path взаимозаменяемы. GetClipProperty("File Path") — точечный запрос к подтверждённому ключу; отсутствие ожидаемого поля в полном словаре нужно отразить отдельно.
Непустое значение, пустая строка, отсутствующий ключ и исключение API — разные состояния. В примере ошибка одного чтения даёт status=read_error и тип исключения; она не превращается в пустой словарь. Подробности ошибки исследуйте локально, не публикуя журналы с путями и конфиденциальными метаданными.[3]
05 / UNDERSTAND
23.976 — не число 23 и не автоматически 24000/1001
Свойства часто представлены текстом. Приведение int(float(fps)) уничтожает дробную часть; преобразование через float может добавить ненужную двоичную погрешность. Пример сохраняет FPS как получено и отдельно допускает точный Fraction из строго проверенной десятичной или дробной строки.
Fraction("23.976") точно представляет эту десятичную запись, а не восстанавливает частоту камеры. Это 2997/125, тогда как 24000/1001 — другая дробь. Метки вроде 29.97 DF не разбираются молча как простое число. fps_numeric сообщает смысл преобразования; его нельзя использовать как доказанную временную базу для монтажа без отдельной проверки.[9]
06 / UNDERSTAND
Ограниченный отчёт должен честно показывать границу
По умолчанию обрабатываются первые 100 элементов возвращённого списка; --limit позволяет выбрать от 1 до 500. Порядок списка не называется хронологическим или алфавитным. --item-id выбирает один конкретный объект в текущем bin; пропавший ID вызывает ошибку, а не заменяется ближайшим именем.
folder_item_count, returned_count, truncated и item_errors позволяют отличить пустой bin от неполного отчёта. После сбора повторно проверяется активная цель, но это не атомарный снимок: параллельное редактирование может изменить свойства. Не используйте отчёт как разрешение на SetClipProperty и не считайте доступные для чтения поля автоматически изменяемыми.[3]
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Скачайте media_pool_properties.py, resolve_api_media.py и resolve_api_connection.py. В комплекте — шесть примеров, оба helper и инструкции RU/EN. Весь код helper доступен ниже; изучите его перед --apply.
Скачать все примеры с инструкцией (.zip) ↓"""Read raw properties from the CURRENT Media Pool folder, without a timeline.
JSON may contain full paths and metadata: redact before sharing. No writes to Resolve.
"""
import argparse
import re
from fractions import Fraction
from resolve_api_connection import connect
from resolve_api_media import context, folder_items, properties, uid, ensure_current, output
def numeric_fps(raw):
# Preserve the original field. This is the exact value of the returned text,
# NOT proof that a rounded 23.976 label means the rate 24000/1001.
if not isinstance(raw, str) or len(raw) > 40:
return None
text = raw.strip()
if not re.fullmatch(r"\d+(?:\.\d+)?|\d+/[1-9]\d*", text):
return None
value = Fraction(text)
if value <= 0:
return None
return {"numerator": value.numerator, "denominator": value.denominator,
"meaning": "numeric_value_of_reported_text_not_verified_source_timebase"}
def inspect_folder(ctx, item_id=None, limit=100):
if type(limit) is not int or not 1 <= limit <= 500:
raise ValueError("Use --limit 1..500")
items = folder_items(ctx)
if item_id is not None:
items = [i for i in items if uid(i) == item_id]
if len(items) != 1:
raise ValueError("Item ID is absent from the current Media Pool folder")
rows = []
for item in items[:limit]:
ident = uid(item)
try:
raw = properties(item)
rows.append({"media_pool_id": ident, "name": item.GetName(), "status": "read",
"properties": raw, "fps_numeric": numeric_fps(raw.get("FPS"))})
except Exception as exc:
# Preserve a per-item failure. Do not substitute empty properties.
rows.append({"media_pool_id": ident, "status": "read_error", "error_type": type(exc).__name__})
ensure_current(ctx)
return {"status": "read_report", "target": ctx.target, "folder_item_count": len(items),
"returned_count": len(rows), "truncated": len(items) > limit,
"item_errors": sum(r["status"] == "read_error" for r in rows), "items": rows}
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--item-id")
parser.add_argument("--limit", type=int, default=100)
args = parser.parse_args(argv)
result = inspect_folder(context(connect()), args.item_id, args.limit)
output(result)
# A partial report is not a clean success for automated downstream processing.
return 2 if result["item_errors"] or result["truncated"] else 0
if __name__ == "__main__":
raise SystemExit(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
Команды запуска
Прочитать текущий bin
python media_pool_properties.py --limit 100На macOS/Linux используйте python3. Отчёт выводится в терминал; в Resolve ничего не записывается.
Только конкретный объект
python media_pool_properties.py --item-id "MEDIA_POOL_ID"Замените путь и все ID/digest значениями своего просмотренного плана. На macOS/Linux используйте python3 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.
Что делает код
Папка
Читается одна папка Media Pool. Подпапки и монтажные вхождения не обходятся; таймлайн не требуется.
Словарь
Без аргумента возвращается словарь. Пример сохраняет реальные ключи, пустые значения и исходный текст FPS, не подставляя придуманные поля.
Отчёт
У каждого объекта есть ID и статус чтения; обрезанный или частичный отчёт выделяется отдельно.
Проверить helper
Полная общая реализация доступна под основным примером. Изучите её область действия и условия остановки, а не только короткий файл запуска.
Свойства исходника не равны параметрам его монтажного вхождения.
Полные пути и метаданные перед публикацией обезличиваются вручную.
Чтение FPS-текста не подтверждает точную временную базу исходника.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
Нет ожидаемого FPS или Resolution
Возможная причинаДругой тип источника или набор свойств версии.
Следующая проверкаИзучите полный словарь. Не подставляйте 24 fps или 1920×1080.
fps_numeric: null
Возможная причинаСтрока не является положительным простым числом/дробью.
Следующая проверкаСохраните исходное значение и выясните его смысл отдельно.
truncated: true
Возможная причинаПредел ниже числа выбранных объектов.
Следующая проверкаУвеличьте лимит в пределах примера или выберите один ID; не называйте отчёт полным.
status: read_error
Возможная причинаAPI не дал словарь или чтение выбросило исключение.
Следующая проверкаПроверьте объект на той же установке; отсутствие ответа не означает отсутствие свойств.
PRACTICE / YOUR OWN EVIDENCE
Видео, аудио и один точный ID
- В тестовом bin выберите видео и WAV. Получите отчёт до создания таймлайна и сравните ключи.
- Повторите с --limit 1: убедитесь в truncated при нескольких объектах.
- Прочитайте один --item-id и сравните сырой FPS/свойства с тем же объектом в Resolve.
Нет придуманных свойств и молчаливого округления FPS; ограничение объёма видно в отчёте.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Почему не анализируется V1?
Предмет статьи — свойства исходников Media Pool, включая ещё не смонтированные. Связь с Timeline вынесена в отдельную статью о путях.
Можно ли менять любое возвращённое поле?
Нет. Доступность чтения не гарантирует запись; часть свойств неизменяема или зависит от контекста. Этот пример не вызывает SetClipProperty.[3]
Почему не переводите ключи на русский?
Ключ нужен для точного обращения к API и сверки. Интерфейс статьи переведён, но JSON сохраняет имена полей, полученные от Resolve.
Чем отличается File Path от имени клипа?
Путь сообщает сохранённую ссылку на исходник, имя — отображаемую подпись объекта. Совпадающие имена не доказывают одинаковый источник.[3]
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 — fractions.Fraction
Точное представление прочитанного числового текста. Десятичная метка 23.976 не доказывает, что исходная частота равна 24000/1001; сырая строка сохраняется.