ВИДЕОМОНТАЖЁРDAVINCI RESOLVE · ВИДЕО · AI

RESOLVE API / MEDIA LAB / 04

GetClipProperty: свойства исходника через Python API

Сохраните реальные ключи и значения. Интерпретируйте их отдельно.

Получение свойств Media Pool без обязательного Timeline: полный словарь, ID, FPS без округления, ошибки отдельных элементов и явно ограниченный JSON-отчёт.

НА ВЫХОДЕ

Получить исходные словари свойств элементов выбранной папки, не изменяя интерпретацию клипов.

PYTHON / RESOLVEУЧЕБНАЯ МОДЕЛЬ
01items = folder.GetClipList()

Читается одна папка Media Pool. Подпапки и монтажные вхождения не обходятся; таймлайн не требуется.

ПРОВЕРКА

Область отчёта явно известна.

Переключатели меняют объяснение. Ничего не подключается к Resolve и не запускается на вашем компьютере.

01 / BEFORE THE CODE

До кода — условия запуска.

01

Подключение уже проверено

Используйте локальный внешний Python и запущенный Resolve Studio. Сначала выполните руководство по подключению к Resolve. Эти примеры используют синтаксис Python 3.8+, но совместимость интерпретатора с native-библиотекой проверяется отдельно.

02

Явная область чтения и записи

Откройте нужный тестовый проект и выберите папку Media Pool вручную. ID в отчёте важнее совпадения имён. Во время запуска не переключайте проект, папку и таймлайн. Нет обхода всех папок проекта или автоматического поиска похожего клипа.

03

Снимок, не изменение

Скрипт не импортирует и не перелинковывает файлы. JSON может содержать полные пути, имена и метаданные: перед публикацией обезличьте его. Частичный отчёт отмечается явно и завершается кодом 2.

Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]

02 / API CONTRACT

Какой объект. Какой метод. Какой результат.

FolderGetClipList()
Вход
Текущий bin.
Возвращает
Список MediaPoolItem.
Проверить
Без рекурсии и обязательного таймлайна.
MediaPoolItemGetClipProperty()
Вход
Без аргументов.
Возвращает
Словарь доступных свойств.
Проверить
Сохраните реальные ключи и значения.
MediaPoolItemGetClipProperty(propertyName)
Вход
Точный ключ, проверенный в словаре.
Возвращает
Значение выбранного свойства.
Проверить
Пустой результат не заменяйте значением по умолчанию.
MediaPoolItemGetUniqueId()
Вход
Без аргументов.
Возвращает
Идентификатор объекта 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) ↓
media_pool_properties.py
Скачать .py
"""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 разрешает только выбранный этап. Проверки не заменяют блокировку, изоляцию процесса и систему восстановления.

resolve_api_media.py
Скачать .py
"""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 может записывать кэш байткода, а нативный вызов — зависнуть; это не песочница и не механизм тайм-аута. Используйте доверенную локальную установку.

resolve_api_connection.py
Скачать .py
"""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

Прочитать текущий 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 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.

Что делает код

  1. Папка

    Читается одна папка Media Pool. Подпапки и монтажные вхождения не обходятся; таймлайн не требуется.

  2. Словарь

    Без аргумента возвращается словарь. Пример сохраняет реальные ключи, пустые значения и исходный текст FPS, не подставляя придуманные поля.

  3. Отчёт

    У каждого объекта есть ID и статус чтения; обрезанный или частичный отчёт выделяется отдельно.

  4. Проверить 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

  1. В тестовом bin выберите видео и WAV. Получите отчёт до создания таймлайна и сравните ключи.
  2. Повторите с --limit 1: убедитесь в truncated при нескольких объектах.
  3. Прочитайте один --item-id и сравните сырой FPS/свойства с тем же объектом в Resolve.
КРИТЕРИЙ ПРИЁМКИ

Нет придуманных свойств и молчаливого округления FPS; ограничение объёма видно в отчёте.

Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.

FAQ

Вопросы, которые возникают на практике.

Почему не анализируется V1?

Предмет статьи — свойства исходников Media Pool, включая ещё не смонтированные. Связь с Timeline вынесена в отдельную статью о путях.

Можно ли менять любое возвращённое поле?

Нет. Доступность чтения не гарантирует запись; часть свойств неизменяема или зависит от контекста. Этот пример не вызывает SetClipProperty.[3]

Почему не переводите ключи на русский?

Ключ нужен для точного обращения к API и сверки. Интерфейс статьи переведён, но JSON сохраняет имена полей, полученные от Resolve.

Чем отличается File Path от имени клипа?

Путь сообщает сохранённую ссылку на исходник, имя — отображаемую подпись объекта. Совпадающие имена не доказывают одинаковый источник.[3]

SOURCES / VERSION BOUNDARIES

Источники, а не подразумеваемая сертификация.

Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.

  1. Blackmagic Design — DaVinci Resolve Studio 21.1 release notes

    8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.

  2. Blackmagic Design — Support Center

    Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.

  3. 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; расположение пункта зависит от версии.

  4. Blackmagic Design — Scripting and Automation

    Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.

  5. Python — fractions.Fraction

    Точное представление прочитанного числового текста. Десятичная метка 23.976 не доказывает, что исходная частота равна 24000/1001; сырая строка сохраняется.

KEEP BUILDING

Продолжайте с конкретной задачей.

API / Media Pool

Импорт файлов в Media Pool через Python API

ImportMedia без скрытых пропусков: абсолютные пути, выбранная папка, план, проверка результата и частичного импорта. Полный Python-пример для Resolve Studio.

API / Timeline

Путь исходного файла у TimelineItem через Python API

Как прочитать File Path у клипов выбранной дорожки: TimelineItem, MediaPoolItem, пустые и отсутствующие пути, offline media и необязательная проверка файла.

API / Media Pool

ImportMedia: сопоставление клипов по пути

Надёжная карта файлов и MediaPoolItem: без lower(), zip по ответу и подстановки по имени. Явные коллизии, частичный импорт и отдельный read-existing режим.

Вернуться в библиотеку API →MCP и AI-ассистенты →