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

RESOLVE API / MEDIA LAB / 05

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

Одинаковое имя — не одинаковый исходник. Порядок ответа — не ключ.

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

НА ВЫХОДЕ

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

PYTHON / RESOLVEУЧЕБНАЯ МОДЕЛЬ
01paths, digest = prepare_files(files)

Список сохраняет порядок запроса. Повторы одного файла и доступных alias отвергаются до импорта.

ПРОВЕРКА

Имена не используются как ID.

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

01 / BEFORE THE CODE

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

01

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

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

02

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

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

03

План перед изменением

Сохраните копию проекта вручную. Без --apply выполняется только план. Для записи нужны точные ID цели; для файлов — также digest просмотренного списка. После отправки изменяющего вызова ошибка не означает, что ничего не произошло.

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

02 / API CONTRACT

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

MediaPoolImportMedia([paths])
Вход
Явные пути; только при --apply.
Возвращает
Кандидаты MediaPoolItem.
Проверить
Не полагаться на индекс ответа как на соответствие.
MediaPoolItemGetClipProperty()
Вход
Без аргументов.
Возвращает
Словарь с фактическими полями объекта.
Проверить
Использовать проверенный File Path, не выдуманные aliases.
Pythonos.path.samefile(a, b)
Вход
Два пути, доступные локальной ОС.
Возвращает
Проверка идентичности файлового объекта.
Проверить
Ошибка доступа не доказывает совпадение.
Helperorder_by_paths(paths, items)
Вход
Входной порядок и список кандидатов.
Возвращает
Однозначно упорядоченные объекты или ошибка.
Проверить
Авторская проверка, не новый метод Resolve.

Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]

03 / UNDERSTAND

Что ломается в zip и словаре по имени

Код zip(files, imported) предполагает, что размер и порядок двух списков уже совпали. Но именно это ещё предстоит проверить. Если один файл не импортирован, смещение связывает каждый следующий путь не с тем объектом; непустой результат выглядит убедительно, хотя монтаж будет неверным.

Имена camera_A/clip001.mov и camera_B/clip001.mov совпадают только последней частью. Словарь по GetName или basename теряет различие, а присваивание by_path[key] = item скрывает второй конфликтующий объект. В новом helper совпадение проверяется как отношение «один путь — ровно один объект», и коллизия останавливает работу.[3]

04 / UNDERSTAND

Сравнение пути без универсального lower()

Пути обрабатываются по правилам той ОС, где выполняется Python. normpath устраняет простые синтаксические различия, но пример не меняет регистр и не переводит диски, сетевые ресурсы или пути другой ОС. Строка /media/A.mov не становится /media/a.mov ради удобства.

Разные написания можно признать одним доступным файлом через os.path.samefile. Эта проверка использует файловую систему; если подтвердить эквивалентность не удалось, ответ «не знаем» не превращается в совпадение по имени. Это не сравнение содержимого: две отдельные копии одинаковых байтов остаются разными файловыми объектами.[7]

05 / UNDERSTAND

Два режима: импорт с картой или чтение существующего

Обычный запуск без --apply показывает план, с --apply выполняет один ImportMedia и сверяет карту результата. Повторный исходник в текущем bin блокируется; отсутствие дубликата в других папках проекта не проверяется. После отправленного вызова любая нестыковка переводится в OutcomeUnknown без повторного импорта.

--read-existing вообще не вызывает ImportMedia. Он читает текущую папку, находит каждый переданный путь и возвращает ID в запрошенном порядке. Другие клипы в bin допустимы, но два кандидата на один путь — нет. Этот режим несовместим с --apply и не выполняет заодно append, relink или переименование.[3]

06 / UNDERSTAND

Когда карта по обычному пути недостаточна

Последовательность кадров может представляться логическим шаблоном, а вложенный таймлайн, compound или генератор не обязаны иметь один обычный File Path. Не подставляйте первый доступный файл и не выбирайте произвольный кадр последовательности, чтобы карта стала полной. Этот пример принимает обычные доступные файлы на той же машине.

Для сложных источников потребуется отдельная идентичность: ID объекта, подтверждённая структура последовательности или другой явный контракт. И даже корректная карта пути не подтверждает декодирование, выбранный proxy или полноту связанных аудиоисточников. За такими утверждениями нужна отдельная проверка.[3]

PYTHON / COMPLETE EXAMPLE

Прочитайте код. Затем запускайте осознанно.

Три файла в одной папке

Скачайте import_map_by_path.py, resolve_api_media.py и resolve_api_connection.py. В комплекте — шесть примеров, оба helper и инструкции RU/EN. Весь код helper доступен ниже; изучите его перед --apply.

Скачать все примеры с инструкцией (.zip) ↓
import_map_by_path.py
Скачать .py
"""Import and map inputs by proven source path, never by return order or basename."""
import argparse
from resolve_api_connection import connect
from resolve_api_media import (
    context, import_once, prepare_files, folder_items, order_by_paths,
    add_write_arguments, expected_from, output, uid, ensure_current,
)


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("files", nargs="+", help="Absolute local paths in the intended order")
    parser.add_argument("--read-existing", action="store_true", help="Only map existing entries; never import")
    add_write_arguments(parser, files=True)
    args = parser.parse_args(argv)
    if args.read_existing and args.apply:
        parser.error("--read-existing cannot be combined with --apply")
    ctx = context(connect())
    if args.read_existing:
        paths, digest = prepare_files(args.files)
        ordered = order_by_paths(paths, folder_items(ctx), allow_other=True)
        ensure_current(ctx)
        output({"status": "existing_mapping_read", "target": ctx.target,
                "files_metadata_sha256": digest, "write_attempted": False,
                "mapping": [{"path": p, "media_pool_id": uid(i)} for p, i in zip(paths, ordered)]})
    else:
        report, _ = import_once(ctx, args.files, args.apply, expected_from(args), args.expect_files_sha256)
        output(report)
    # A collision is reported, not overwritten in a one-value dictionary.
    # This is a current-folder map, not project-wide duplicate detection.


if __name__ == "__main__":
    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

Команды запуска

План импорта и карты

План импорта и карты
python import_map_by_path.py "D:/A/clip001.mov" "D:/B/clip001.mov"

Замените путь и все ID/digest значениями своего просмотренного плана. На macOS/Linux используйте python3 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.

Явный импорт с картой

Явный импорт с картой
python import_map_by_path.py "D:/A/clip001.mov" "D:/B/clip001.mov" --apply --expect-project-id "PROJECT_ID" --expect-folder-id "FOLDER_ID" --expect-files-sha256 "MANIFEST_SHA256"

Замените путь и все ID/digest значениями своего просмотренного плана. На macOS/Linux используйте python3 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.

Только существующие записи

Только существующие записи
python import_map_by_path.py "D:/B/clip001.mov" "D:/A/clip001.mov" --read-existing

Замените путь и все ID/digest значениями своего просмотренного плана. На macOS/Linux используйте python3 и абсолютный путь этой ОС. Текст-заглушка не пройдёт проверку цели.

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

  1. Вход

    Список сохраняет порядок запроса. Повторы одного файла и доступных alias отвергаются до импорта.

  2. Кандидаты

    Получаем сохранённый путь каждого объекта. Отсутствующее поле, неоднозначность или посторонний ответ не маскируются fallback.

  3. Карта

    Для каждого входа нужен ровно один ещё не использованный объект. Возвращается порядок входа, а не порядок ответа API.

  4. Проверить helper

    Полная общая реализация доступна под основным примером. Изучите её область действия и условия остановки, а не только короткий файл запуска.

Границы, которые важно сохранить

Регистронезависимый ключ сортировки не должен становиться ключом файловой идентичности.

Частичная карта не передаётся дальше в append.

Имена и пути из отчёта не обезличиваются автоматически.

DEBUG / OBSERVATION FIRST

Ошибка — отправная точка, не готовый диагноз.

Missing/ambiguous mapping

Возможная причинаНоль или несколько кандидатов на один источник.

Следующая проверкаПроверьте пути и ID вручную; не выбирайте первый совпавший объект.

Unrequested or pathless items

Возможная причинаОтвет импорта содержит неподтверждённые объекты.

Следующая проверкаОстановитесь и сверяйте фактический bin. Не скрывайте лишнее фильтром.

Duplicate input file/alias

Возможная причинаОдин файл запросили повторно под тем же или подтверждённым другим путём.

Следующая проверкаУточните запрос. Если нужны два монтажных вхождения, это отдельная задача Timeline.

На другой ОС сопоставление не получилось

Возможная причинаНе определено соответствие дисков или mount points.

Следующая проверкаИспользуйте реальные пути этой машины; не заменяйте их lower()/basename.

PRACTICE / YOUR OWN EVIDENCE

Одинаковое имя в двух каталогах

  1. Подготовьте два разных файла A/clip001.mov и B/clip001.mov. Просмотрите план с обоими полными путями.
  2. После контролируемого импорта запустите --read-existing с обратным порядком путей.
  3. Убедитесь, что поменялся порядок карты, а каждый путь остался связан со своим ID.
КРИТЕРИЙ ПРИЁМКИ

Совпадающие basename не смешиваются; отсутствующий или неоднозначный путь вызывает остановку, а не fallback.

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

FAQ

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

Можно ли просто сортировать оба списка?

Нет: сортировка не устраняет отсутствующий элемент или конфликт одинаковых имён. Нужен проверяемый ключ соответствия.

Почему не ищете по всему проекту?

Область намеренно ограничена текущим bin. Полный обход потребует отдельной политики дубликатов, папок и частичного чтения.

Две копии одного видео будут считаться одинаковыми?

Не по содержимому. Проверяется путь или подтверждённая файловая идентичность. Побайтовые копии в разных файлах — отдельные источники этого маршрута.[7]

Скрипт сможет сопоставить offline media?

Этот конкретный маршрут требует доступные локальные входы. Для проверки сохранённых путей offline-элементов используйте статью о TimelineItem и не запускайте импорт как способ диагностики.

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 — os.path: samefile, normpath and platform rules

    Различие строкового пути и файлового объекта. В примерах нет принудительного lower(): эквивалентность разных написаний принимается только после проверки локальной файловой системой.

KEEP BUILDING

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

API / Media Pool

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

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

API / Media Pool

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

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

API / Timeline

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

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

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