RESOLVE API / MEDIA LAB / 05

ImportMedia: map clips by source path

Same name does not mean same source. Return order is not a key.

Build a file-to-MediaPoolItem map without lowercasing, zipping return order or basename fallback. Explicit collisions, partial imports and a read-existing mode.

THE RESULT

Produce an ordered one-to-one mapping of input files to MediaPoolItems, or stop on a concrete conflict.

PYTHON / RESOLVESTUDY MODEL
01paths, digest = prepare_files(files)

The list preserves requested order. Duplicate files and proven aliases are rejected before import.

CHECK

Names are not used as IDs.

Switching steps changes the explanation. Nothing connects to Resolve or runs on your computer.

01 / BEFORE THE CODE

Set the conditions before you run.

01

Connection already checked

Use external local Python with Resolve Studio running. Complete the Resolve connection guide first. These examples use Python 3.8+ syntax; interpreter/native-library compatibility is a separate requirement.

02

An explicit reading/writing scope

Open the intended test project and select a Media Pool folder manually. Reported IDs matter more than matching display names. Do not switch projects, bins or timelines while the script runs. It does not search every project folder or choose a similar clip.

03

Plan before modifying

Save a project copy yourself. Without --apply only a plan is produced. Writes require exact target IDs; file-based phases also require the reviewed manifest digest. An error after dispatch does not mean that nothing changed.

Current Studio edition guidance and the installed scripting documentation take precedence over older console or Free-edition tutorials.[1][4][3]

02 / API CONTRACT

Which object. Which method. Which result.

MediaPoolImportMedia([paths])
Input
Explicit paths; only with --apply.
Returns
Candidate MediaPoolItems.
Check
Do not treat return indices as a verified mapping.
MediaPoolItemGetClipProperty()
Input
No arguments.
Returns
Actual object property dictionary.
Check
Use verified File Path, not fabricated aliases.
Pythonos.path.samefile(a, b)
Input
Two paths available to the local OS.
Returns
Filesystem-object identity comparison.
Check
Access failure does not prove a match.
Helperorder_by_paths(paths, items)
Input
Requested order and candidate objects.
Returns
Uniquely ordered objects or an error.
Check
Our validation helper, not a new Resolve method.

Resolve method contracts are documented by Blackmagic; Python and author helpers are labelled separately. The defensive checks are our example design. A non-empty object is not proof of full API compatibility.[3]

03 / UNDERSTAND

What goes wrong with zip and name dictionaries

zip(files, imported) assumes both list sizes and order already agree. That is exactly what still needs verification. If a file is missing from the result, positional pairing can associate subsequent paths with the wrong objects, producing a plausible-looking but incorrect edit.

camera_A/clip001.mov and camera_B/clip001.mov share only their final name. A dictionary keyed by GetName or basename loses the distinction; by_path[key] = item can hide a second conflicting object. The new helper enforces exactly one object per path and stops on collisions.[3]

04 / UNDERSTAND

Compare paths without a universal lower()

Paths follow the operating system running Python. normpath removes simple syntactic variation, but the example does not change case or translate drives, shares or foreign-OS paths. /media/A.mov is not rewritten as /media/a.mov for convenience.

Different spellings may be recognized as the same accessible file with os.path.samefile. That check uses filesystem identity. If equivalence cannot be established, uncertainty never becomes a basename match. This is not content comparison: separate copies with identical bytes remain separate file objects.[7]

05 / UNDERSTAND

Two modes: import-and-map or read existing entries

The default run previews; --apply performs one ImportMedia call and reconciles its map. A source already present in the current bin is blocked, but duplicates in other project bins are not checked. Any discrepancy after dispatch becomes OutcomeUnknown without reimport.

--read-existing never calls ImportMedia. It reads the current bin, matches each requested path and returns IDs in requested order. Unrelated bin entries are allowed, but two candidates for one path are not. It cannot be combined with --apply and never performs append, relink or renaming as a side effect.[3]

06 / UNDERSTAND

When an ordinary file-path map is insufficient

An image sequence may have a logical pattern, while a nested timeline, compound item or generator may lack a single ordinary File Path. Do not substitute an arbitrary file or sequence frame merely to complete the map. This example targets ordinary accessible files on the same machine.

Complex sources need a different identity contract: an object ID, verified sequence structure or another explicit representation. Even a correct file-path map does not prove decoding, selected proxy usage or complete linked-audio dependencies. Those require separate checks.[3]

PYTHON / COMPLETE EXAMPLE

Read the code. Then run it deliberately.

Three files in the same folder

Download import_map_by_path.py, resolve_api_media.py and resolve_api_connection.py. The bundle includes all six examples, both helpers and RU/EN instructions. Both helpers are fully visible below; read them before --apply.

Download all examples + instructions (.zip) ↓
import_map_by_path.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 — targeting, mapping and write checks

The short main script calls this shared implementation. This is our tutorial code, not the official SDK. --apply permits only the selected phase. Checks are not atomic locks, process isolation or an automatic recovery system.

resolve_api_media.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))

Shared connection helper — inspect and download

This file only locates and loads the installed bridge. Native imports can write Python bytecode caches and can block; this is not a sandbox or a timeout mechanism. Use a trusted local installation.

resolve_api_connection.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

Launch commands

Preview import and mapping

Preview import and mapping
python import_map_by_path.py "D:/A/clip001.mov" "D:/B/clip001.mov"

Replace the path and every ID/digest with values from your reviewed plan. On macOS/Linux use python3 and a native absolute path. Placeholder IDs will fail target checks.

Explicit import and mapping

Explicit import and mapping
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"

Replace the path and every ID/digest with values from your reviewed plan. On macOS/Linux use python3 and a native absolute path. Placeholder IDs will fail target checks.

Existing entries only

Existing entries only
python import_map_by_path.py "D:/B/clip001.mov" "D:/A/clip001.mov" --read-existing

Replace the path and every ID/digest with values from your reviewed plan. On macOS/Linux use python3 and a native absolute path. Placeholder IDs will fail target checks.

What the code does

  1. Input

    The list preserves requested order. Duplicate files and proven aliases are rejected before import.

  2. Candidates

    Read each object’s stored path. Missing fields, ambiguity and unexpected results are never concealed by fallback.

  3. Map

    Every input needs exactly one unused object. Output follows requested order rather than API return order.

  4. Review the helper

    The complete shared implementation is visible below the main example. Read its scope and stop conditions, not just the short entry point.

Boundaries to keep

A case-folded sorting key must not become a file-identity key.

A partial map is not passed downstream to append.

Names and paths in reports are not automatically anonymized.

DEBUG / OBSERVATION FIRST

An error is a starting point, not a diagnosis.

Missing/ambiguous mapping

Possible reasonZero or multiple candidates for a source.

Next checkInspect paths and IDs manually; never select the first candidate blindly.

Unrequested or pathless items

Possible reasonThe import returned unverified objects.

Next checkStop and inspect the actual bin; do not filter away discrepancies.

Duplicate input file/alias

Possible reasonThe same file was requested twice using one or proven equivalent paths.

Next checkClarify the request. Two edit occurrences are a separate timeline task.

Mapping failed on another OS

Possible reasonDrive/mount mapping has not been defined.

Next checkUse actual native paths; do not replace identity with lower()/basename.

PRACTICE / YOUR OWN EVIDENCE

One basename in two directories

  1. Prepare distinct A/clip001.mov and B/clip001.mov files and preview both full paths.
  2. After controlled import, run --read-existing with reversed input order.
  3. Confirm map order changes while each path remains linked to its own ID.
ACCEPT WHEN

Matching basenames remain distinct; missing or ambiguous paths stop rather than trigger fallback.

This is a checklist for your test, not a report of a test already performed on the website.

FAQ

Questions that matter in practice.

Can I just sort both lists?

No. Sorting does not fix missing items or identical-name collisions. You need a verifiable matching key.

Why not search the entire project?

The scope deliberately stays within the current bin. A full traversal needs separate duplicate, folder and partial-read policies.

Are two copies of the same video considered identical?

Not by content. The check uses path or proven filesystem identity. Byte-identical copies stored as distinct files remain separate inputs here.[7]

Can it map offline media?

This route requires accessible local inputs. Use the TimelineItem source-path guide to inspect stored offline references; do not import as a diagnostic workaround.

SOURCES / VERSION BOUNDARIES

Sources, not implied certification.

The source review, offline Python tests and UI checks are different kinds of evidence. None establishes that every example works with every Studio build, operating system or Python version. The date below is the editorial review date, not a live-test certificate.

  1. Blackmagic Design — DaVinci Resolve Studio 21.1 release notes

    8 September 2026. The release announcement assigns Python scripting to Studio. Edition requirements, not a test report for our examples.

  2. Blackmagic Design — Support Center

    Release catalogue checked on 5 October 2026: 21.1.1 is listed. The latest release is not the minimum version of every method.

  3. Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)

    Public mirror of Blackmagic’s original README, maintained by X-Raym. Used for basic signatures; not official hosting and not a 21.1.1 README. Locate your installed Developer/Scripting/README.txt through Resolve’s Help/Documentation menu; exact menu nesting can vary.

  4. Blackmagic Design — Scripting and Automation

    Official description of Studio Python/Lua scripting and integrations. Do not extend Studio capabilities to Free without checking the exact release.

  5. Python — os.path: samefile, normpath and platform rules

    A pathname string is not file identity. Examples never force lowercase; differently spelled paths are equated only when the local filesystem confirms identity.

KEEP BUILDING

Continue with a precise next task.

API / Media Pool

Import files into Media Pool with the Python API

Use ImportMedia without silently skipped files: absolute paths, selected bin, preview plan and partial-result checks. Complete Python example for Resolve Studio.

API / Media Pool

GetClipProperty: read Media Pool clip properties

Read Media Pool properties without requiring a timeline: full dictionaries, IDs, unrounded FPS text, per-item errors and an explicitly bounded JSON report.

API / Timeline

Read a TimelineItem source file path with the Python API

Read File Path for one timeline track: TimelineItem, MediaPoolItem, missing/empty paths, offline references and optional filesystem checks.

Back to the API library →MCP and AI assistants →