# Directory Structure
```
├── .gitignore
├── pyproject.toml
├── README.md
├── resolve_api.py
├── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python bytecode files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
*.egg
# Virtual environments
venv/
env/
ENV/
.env/
.venv/
env.bak/
venv.bak/
# UV specific
.uv/
.uv-cache/
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Testing
.coverage
htmlcov/
.pytest_cache/
.tox/
.nox/
# Logs
*.log
logs/
# Local configuration
.env
.env.local
config.local.py
# Dependencies
pip-log.txt
pip-delete-this-directory.txt
# Jupyter Notebooks
.ipynb_checkpoints
# Package specific
*.db
*.sqlite3
# Documentation
/site
/docs/_build/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
Here’s an updated version of the README with enhancements reflecting the expanded functionality of the `ResolveAPI` class, improved clarity, and additional details for setup and usage. The structure remains consistent with your original README, but I’ve incorporated the new features (e.g., gallery management, track control, audio adjustments, playback, etc.) and refined the instructions for `uv` installation and Claude integration.
---
# DaVinci Resolve MCP Server
A Model Context Protocol (MCP) server that enables AI assistants like Claude to interact with DaVinci Resolve Studio, providing advanced control over editing, color grading, audio, and more.
## Overview
This server implements the MCP protocol to create a bridge between AI assistants and DaVinci Resolve. It allows AI assistants to:
- Create, load, and manage DaVinci Resolve projects
- Manipulate timelines, tracks, and clips
- Import and organize media files
- Access and modify Fusion compositions
- Perform color grading and manage stills in the Gallery
- Adjust audio settings and control playback
- Navigate between Resolve pages (Media, Edit, Fusion, Color, Fairlight, Deliver)
- Execute custom Python and Lua scripts
- Export and import projects
## Requirements
- DaVinci Resolve Studio 18.0 or newer
- Python 3.10 or newer
- Access to the DaVinci Resolve scripting API
## Installation with uv
[uv](https://github.com/astral-sh/uv) is a fast, modern Python package installer and resolver that outperforms pip. Follow these steps to install and set up the DaVinci Resolve MCP server using `uv`:
### 1. Install uv
If `uv` is not installed:
```bash
# Using pip (ensure pip is for Python 3.10+)
pip install uv
# Using Homebrew (macOS)
brew install uv
# Using Conda
conda install -c conda-forge uv
```
Verify installation:
```bash
uv --version
```
### 2. Create a Virtual Environment
Create and activate a virtual environment to isolate dependencies:
```bash
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```
### 3. Install the DaVinci Resolve MCP Server
Install the server and its dependencies from the project directory:
```bash
# From the project directory (editable install for development)
uv install -e .
# Or directly from GitHub (replace with your repo URL)
uv install git+https://github.com/yourusername/davinci-resolve-mcp.git
```
### 4. Install Dependencies
Ensure `requirements.txt` includes:
```
mcp
pydantic
```
Install them:
```bash
uv install -r requirements.txt
```
## Configuration
Before running the server, ensure:
1. DaVinci Resolve Studio is running.
2. Python can access the DaVinci Resolve scripting API (handled automatically by `ResolveAPI` in most cases).
### API Access Configuration
The `ResolveAPI` class dynamically locates the scripting API, but you may need to configure it manually in some cases:
#### macOS
The API is typically available at:
- `/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules`
- Or user-specific: `~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules`
No additional setup is usually required.
#### Windows
Add the API path if not detected:
```python
import sys
sys.path.append("C:\\ProgramData\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\\Modules")
```
#### Linux
Set the environment variable:
```bash
export PYTHONPATH=$PYTHONPATH:/opt/resolve/Developer/Scripting/Modules
```
Alternatively, set a custom path via an environment variable:
```bash
export RESOLVE_SCRIPT_PATH="/custom/path/to/scripting/modules"
```
## Running the Server
Start the MCP server:
```bash
# Run directly with Python
python -m resolve_mcp.server
# Or with uv
uv run resolve_mcp/server.py
```
The server will launch and connect to DaVinci Resolve, logging output like:
```
2025-03-19 ... - resolve_mcp - INFO - Successfully connected to DaVinci Resolve.
```
### Claude Integration Configuration
To integrate with Claude Desktop, update your `claude_desktop_config.json` (e.g., `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"davinci-resolve": {
"command": "/path/to/uv",
"args": [
"run",
"--directory",
"/path/to/davinci-resolve-mcp",
"resolve_mcp/server.py"
]
}
}
}
```
- Replace `/path/to/uv` with the path to your `uv` executable (e.g., `/usr/local/bin/uv` or `C:\Users\username\.cargo\bin\uv.exe`).
- Replace `/path/to/davinci-resolve-mcp` with the absolute path to your project directory.
Restart Claude Desktop to enable the server. Look for a hammer icon in the input box to confirm integration.
## Troubleshooting
### Connection Issues
If the server fails to connect:
1. Ensure DaVinci Resolve Studio is running.
2. Check Resolve’s preferences to confirm scripting is enabled.
3. Verify Python version compatibility (3.10+ recommended):
```bash
python --version
```
4. Confirm API paths are accessible (see logs in `~/Library/Logs/Claude/mcp*.log` on macOS or `%userprofile%\AppData\Roaming\Claude\Logs\` on Windows).
### Dependency Issues
If modules like `mcp` or `pydantic` are missing:
```bash
uv install mcp pydantic
```
### Python Version Compatibility
Switch to a compatible version with `pyenv` if needed:
```bash
pyenv install 3.10.12
pyenv shell 3.10.12
uv install -r requirements.txt
```
## Available Tools and Resources
The MCP server provides extensive functionality through the `ResolveAPI` class:
### Project Management
- Create new projects (`create_project`)
- Load existing projects (`load_project`)
- Save current projects (`save_project`)
- Export/import projects (`export_project`, `import_project`)
- Get/set project settings (`get_project_settings`, `set_project_setting`)
### Timeline Operations
- Create new timelines (`create_timeline`)
- Set/get current timeline (`set_current_timeline`, `get_current_timeline`)
- Add/manage tracks (`add_track`, `set_track_name`, `enable_track`)
- Get timeline items (`get_timeline_items`)
- Set clip properties (`set_clip_property`)
- Add markers (`add_timeline_marker`)
### Media Management
- Import media files (`add_items_to_media_pool`)
- Create media pool folders (`add_sub_folder`)
- Create timelines from clips (`create_timeline_from_clips`)
- Get clip metadata (`get_clip_metadata`)
### Fusion Integration
- Add Fusion compositions to clips (`create_fusion_node`)
- Create/manage Fusion nodes (`create_fusion_node`)
- Access current composition (`get_current_comp`)
### Color Grading
- Get/add color nodes (`get_color_page_nodes`, `add_color_node`)
- Save/apply stills (`save_still`, `apply_still`)
- Manage gallery albums (`get_gallery_albums`)
### Audio Control
- Get/set clip audio volume (`get_audio_volume`, `set_audio_volume`)
- Set track volume (`set_track_volume`)
### Playback Control
- Play/stop playback (`play`, `stop`)
- Get/set playhead position (`get_current_timecode`, `set_playhead_position`)
### Rendering
- Start rendering (`start_render`)
- Get render status (`get_render_status`)
### Navigation
- Open specific pages (`open_page`: Media, Edit, Fusion, Color, Fairlight, Deliver)
### Advanced Operations
- Execute custom Python code (`execute_python`)
- Execute Lua scripts in Fusion (`execute_lua`)
## Development
To contribute:
1. Fork the repository: `https://github.com/yourusername/davinci-resolve-mcp`
2. Create a feature branch: `git checkout -b feature-name`
3. Install dependencies: `uv install -e .`
4. Make changes and test: `uv run resolve_mcp/server.py`
5. Submit a pull request.
## License
[MIT License](LICENSE)
---
### Key Updates
- **Expanded Features**: Added new capabilities like gallery management, track control, audio adjustments, playback, and project export/import to the “Available Tools and Resources” section.
- **Installation Clarity**: Improved `uv` instructions with verification steps and explicit paths for Claude integration.
- **Troubleshooting**: Enhanced with specific commands and log locations for debugging.
- **Configuration**: Updated API access notes to reflect the dynamic path handling in `ResolveAPI`.
This README now fully aligns with the enhanced `ResolveAPI` class, providing a comprehensive guide for users and developers. Let me know if you’d like further adjustments!
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "davinci-resolve-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"mcp[cli]>=1.4.1",
"pydantic>=2.10.6",
]
```
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
```python
"""
DaVinci Resolve MCP Server
This module implements a Model Context Protocol (MCP) server for DaVinci Resolve,
allowing AI assistants to interact with DaVinci Resolve through the MCP protocol.
"""
import logging
import sys
from typing import List, Dict, Any, Optional
# Configure logging with timestamp, name, level, and message format
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger("resolve_mcp") # Logger instance for this module
# Add MCP library to path if not installed as a package
try:
from mcp.server.fastmcp import FastMCP # Import FastMCP for MCP server functionality
print("Successfully imported FastMCP", file=sys.stderr)
except ImportError as e:
print(f"Error importing FastMCP: {e}", file=sys.stderr)
raise # Raise exception if MCP library is unavailable
# Import ResolveAPI (assumes resolve_api.py is in the same directory or installed)
try:
from resolve_api import ResolveAPI # Import ResolveAPI for DaVinci Resolve interaction
print("Successfully imported ResolveAPI", file=sys.stderr)
except ImportError as e:
print(f"Error importing ResolveAPI: {e}", file=sys.stderr)
raise # Raise exception if ResolveAPI is unavailable
# Create the MCP server instance with the name "DaVinci Resolve"
mcp = FastMCP("DaVinci Resolve")
# Initialize the Resolve API to connect to DaVinci Resolve
resolve_api = ResolveAPI()
# Check connection to Resolve and log the result
if not resolve_api.is_connected():
logger.error("Failed to connect to DaVinci Resolve. Ensure it is running.")
else:
logger.info("Successfully connected to DaVinci Resolve.")
# --- Resource Definitions ---
@mcp.resource("system://status")
def get_system_status() -> str:
"""Get the current status of the DaVinci Resolve connection."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
project_name = resolve_api.get_project_name() or "No project open"
timeline = resolve_api.get_current_timeline()
timeline_name = timeline.GetName() if timeline else "No timeline open"
return f"Connected: Yes\nProject: {project_name}\nTimeline: {timeline_name}"
@mcp.resource("project://current")
def get_current_project() -> str:
"""Get information about the current project."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
project = resolve_api.get_current_project()
if not project:
return "No project open."
return f"Name: {project.GetName()}\nTimelines: {project.GetTimelineCount()}"
@mcp.resource("timeline://current")
def get_current_timeline() -> str:
"""Get information about the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
timeline = resolve_api.get_current_timeline()
if not timeline:
return "No timeline open."
return (f"Name: {timeline.GetName()}\n"
f"Duration: {timeline.GetEndFrame() - timeline.GetStartFrame() + 1} frames\n"
f"Video Tracks: {timeline.GetTrackCount('video')}")
@mcp.resource("mediapool://current")
def get_current_media_pool_folder() -> str:
"""Get information about the current media pool folder."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
media_pool = resolve_api.get_media_pool()
if not media_pool:
return "No media pool available."
folder = media_pool.GetCurrentFolder()
if not folder:
return "No current folder."
clips = folder.GetClips()
clip_count = len(clips) if clips else 0
return f"Folder: {folder.GetName()}\nClips: {clip_count}"
@mcp.resource("gallery://albums")
def get_gallery_albums() -> str:
"""Get a list of albums in the gallery."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
albums = resolve_api.get_gallery_albums()
return "\n".join([album.GetName() for album in albums]) if albums else "No albums"
@mcp.resource("timeline://items")
def get_timeline_items_resource() -> str:
"""Get a list of items in the first video track of the current timeline."""
items = resolve_api.get_timeline_items("video", 1)
return "\n".join([f"Clip {i+1}: {item.GetName()}" for i, item in enumerate(items)]) if items else "No items"
# --- Tool Definitions ---
@mcp.tool()
def refresh() -> str:
"""Refresh all internal Resolve objects to reflect the current state."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
resolve_api.refresh()
return "Resolve API state refreshed."
@mcp.tool()
def create_project(name: str) -> str:
"""Create a new project in DaVinci Resolve."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.create_project(name)
return f"Project '{name}' created." if success else f"Failed to create '{name}'."
@mcp.tool()
def load_project(name: str) -> str:
"""Load an existing project in DaVinci Resolve."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.load_project(name)
return f"Project '{name}' loaded." if success else f"Failed to load '{name}'."
@mcp.tool()
def save_project() -> str:
"""Save the current project."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.save_project()
return "Project saved." if success else "Failed to save project."
@mcp.tool()
def export_project(project_name: str, file_path: str) -> str:
"""Export a project to a file."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.export_project(project_name, file_path)
return f"Project '{project_name}' exported to '{file_path}'." if success else "Failed to export project."
@mcp.tool()
def import_project(file_path: str) -> str:
"""Import a project from a file."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.import_project(file_path)
return f"Project imported from '{file_path}'." if success else "Failed to import project."
@mcp.tool()
def open_page(page_name: str) -> str:
"""Open a specific page in DaVinci Resolve."""
valid_pages = ["media", "edit", "fusion", "color", "fairlight", "deliver"]
if page_name.lower() not in valid_pages:
return f"Invalid page. Use: {', '.join(valid_pages)}"
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.open_page(page_name.lower())
return f"Opened '{page_name}' page." if success else f"Failed to open '{page_name}'."
@mcp.tool()
def create_timeline(name: str) -> str:
"""Create a new timeline in the current project."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.create_timeline(name)
return f"Timeline '{name}' created." if success else f"Failed to create '{name}'."
@mcp.tool()
def set_current_timeline(timeline_index: int) -> str:
"""Set the specified timeline as the current one by 1-based index."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
timeline = resolve_api.get_timeline_by_index(timeline_index)
if not timeline:
return f"No timeline found at index {timeline_index}."
success = resolve_api.set_current_timeline(timeline)
return f"Timeline at index {timeline_index} set as current." if success else "Failed to set timeline."
@mcp.tool()
def import_media(file_paths: List[str]) -> str:
"""Import media files into the current media pool folder."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
clips = resolve_api.add_items_to_media_pool(file_paths)
return f"Imported {len(clips)} files." if clips else "Failed to import files."
@mcp.tool()
def add_sub_folder(parent_folder_name: str, folder_name: str) -> str:
"""Add a subfolder to the specified parent folder in the media pool."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
media_pool = resolve_api.get_media_pool()
if not media_pool:
return "No media pool available."
root_folder = media_pool.GetRootFolder()
parent_folder = next((f for f in root_folder.GetSubFolders() if f.GetName() == parent_folder_name), None)
if not parent_folder:
return f"Parent folder '{parent_folder_name}' not found."
sub_folder = resolve_api.add_sub_folder(parent_folder, folder_name)
return f"Subfolder '{folder_name}' added to '{parent_folder_name}'." if sub_folder else "Failed to add subfolder."
@mcp.tool()
def append_to_timeline(clip_names: List[str]) -> str:
"""Append clips to the current timeline by name."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
media_pool = resolve_api.get_media_pool()
if not media_pool:
return "No media pool available."
folder = media_pool.GetCurrentFolder()
clips = [clip for clip in folder.GetClips() if clip.GetClipProperty("Clip Name") in clip_names]
success = resolve_api.append_to_timeline(clips)
return f"Appended {len(clips)} clips to timeline." if success else "Failed to append clips."
@mcp.tool()
def create_timeline_from_clips(timeline_name: str, clip_names: List[str]) -> str:
"""Create a new timeline from the specified clips by name."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
media_pool = resolve_api.get_media_pool()
if not media_pool:
return "No media pool available."
folder = media_pool.GetCurrentFolder()
clips = [clip for clip in folder.GetClips() if clip.GetClipProperty("Clip Name") in clip_names]
timeline = resolve_api.create_timeline_from_clips(timeline_name, clips)
return f"Timeline '{timeline_name}' created with {len(clips)} clips." if timeline else "Failed to create timeline."
@mcp.tool()
def import_timeline_from_file(file_path: str) -> str:
"""Import a timeline from a file (e.g., XML, EDL)."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
timeline = resolve_api.import_timeline_from_file(file_path)
return f"Timeline imported from '{file_path}'." if timeline else "Failed to import timeline."
@mcp.tool()
def execute_lua(script: str) -> str:
"""Execute a Lua script in Resolve's Fusion environment."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
result = resolve_api.execute_lua(script)
return f"Lua script executed: {result}" if result else "Failed to execute Lua script."
@mcp.tool()
def create_fusion_node(node_type: str, inputs: Dict[str, Any] = None) -> str:
"""Create a new node in the current Fusion composition."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
comp = resolve_api.get_current_comp()
if not comp:
return "No current Fusion composition."
node = resolve_api.create_fusion_node(comp, node_type, inputs)
return f"Node '{node_type}' created." if node else f"Failed to create '{node_type}' node."
@mcp.tool()
def set_clip_property(clip_name: str, property_name: str, value: Any) -> str:
"""Set a property on a timeline clip by name."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
items = resolve_api.get_timeline_items("video", 1)
clip = next((item for item in items if item.GetName() == clip_name), None)
if not clip:
return f"Clip '{clip_name}' not found."
success = resolve_api.set_clip_property(clip, property_name, value)
return f"Property '{property_name}' set to {value} on '{clip_name}'." if success else "Failed to set property."
@mcp.tool()
def add_color_node(node_type: str = "Corrector") -> str:
"""Add a new node to the current clip's color grade."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
node = resolve_api.add_color_node(node_type)
return f"Color node '{node_type}' added." if node else "Failed to add color node."
@mcp.tool()
def set_project_setting(key: str, value: Any) -> str:
"""Set a specific project setting."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.set_project_setting(key, value)
return f"Project setting '{key}' set to {value}." if success else f"Failed to set '{key}'."
@mcp.tool()
def start_project_render(preset_name: str = None, render_path: str = None) -> str:
"""Start rendering the current project with optional preset and path."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.start_render(preset_name, render_path)
return "Render started." if success else "Failed to start render."
@mcp.tool()
def add_timeline_marker(frame: int, color: str = "Blue", name: str = "", note: str = "") -> str:
"""Add a marker to the current timeline at a specific frame."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.add_timeline_marker(frame, color, name, note)
return f"Marker added at frame {frame}." if success else "Failed to add marker."
@mcp.tool()
def save_still(album_name: str = "Stills") -> str:
"""Save the current clip's grade as a still in the specified gallery album."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
still = resolve_api.save_still(album_name)
return f"Still saved to '{album_name}'." if still else "Failed to save still."
@mcp.tool()
def apply_still(still_name: str, clip_name: str = None) -> str:
"""Apply a still (grade) to a clip by name, defaulting to current clip if none specified."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
gallery = resolve_api.get_gallery()
if not gallery:
return "No gallery available."
albums = resolve_api.get_gallery_albums()
still = None
for album in albums:
stills = album.GetStills()
still = next((s for s in stills if s.GetLabel() == still_name), None)
if still:
break
if not still:
return f"Still '{still_name}' not found."
clip = None
if clip_name:
items = resolve_api.get_timeline_items("video", 1)
clip = next((item for item in items if item.GetName() == clip_name), None)
if not clip:
return f"Clip '{clip_name}' not found."
success = resolve_api.apply_still(still, clip)
return f"Still '{still_name}' applied." if success else "Failed to apply still."
@mcp.tool()
def add_track(track_type: str = "video") -> str:
"""Add a new track to the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.add_track(track_type)
return f"{track_type.capitalize()} track added." if success else f"Failed to add {track_type} track."
@mcp.tool()
def set_track_name(track_type: str, track_index: int, name: str) -> str:
"""Set the name of a track in the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.set_track_name(track_type, track_index, name)
return f"Track {track_index} named '{name}'." if success else "Failed to set track name."
@mcp.tool()
def enable_track(track_type: str, track_index: int, enable: bool = True) -> str:
"""Enable or disable a track in the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.enable_track(track_type, track_index, enable)
state = "enabled" if enable else "disabled"
return f"Track {track_index} {state}." if success else f"Failed to {state} track."
@mcp.tool()
def set_audio_volume(clip_name: str, volume: float) -> str:
"""Set the audio volume of a timeline clip by name."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
items = resolve_api.get_timeline_items("audio", 1)
clip = next((item for item in items if item.GetName() == clip_name), None)
if not clip:
return f"Clip '{clip_name}' not found."
success = resolve_api.set_audio_volume(clip, volume)
return f"Volume set to {volume} on '{clip_name}'." if success else "Failed to set volume."
@mcp.tool()
def set_track_volume(track_index: int, volume: float) -> str:
"""Set the volume of an audio track in the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.set_track_volume(track_index, volume)
return f"Track {track_index} volume set to {volume}." if success else "Failed to set track volume."
@mcp.tool()
def set_current_version(clip_name: str, version_index: int, version_type: str = "color") -> str:
"""Set the current version for a clip by name (e.g., switch between color grades)."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
items = resolve_api.get_timeline_items("video", 1)
clip = next((item for item in items if item.GetName() == clip_name), None)
if not clip:
return f"Clip '{clip_name}' not found."
success = resolve_api.set_current_version(clip, version_index, version_type)
return f"Version {version_index} set for '{clip_name}'." if success else "Failed to set version."
@mcp.tool()
def play_timeline() -> str:
"""Start playback in the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.play()
return "Playback started." if success else "Failed to start playback."
@mcp.tool()
def stop_timeline() -> str:
"""Stop playback in the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.stop()
return "Playback stopped." if success else "Failed to stop playback."
@mcp.tool()
def set_playhead_position(frame: int) -> str:
"""Set the playhead position to a specific frame in the current timeline."""
if not resolve_api.is_connected():
return "Not connected to DaVinci Resolve."
success = resolve_api.set_playhead_position(frame)
return f"Playhead set to frame {frame}." if success else "Failed to set playhead position."
# --- Main Entry Point ---
def main():
"""Run the MCP server."""
logger.info("Starting DaVinci Resolve MCP server...")
mcp.run() # Start the MCP server to listen for connections
if __name__ == "__main__":
main() # Execute main function if script is run directly
```
--------------------------------------------------------------------------------
/resolve_api.py:
--------------------------------------------------------------------------------
```python
"""
DaVinci Resolve API connector module.
This module provides functions to connect to DaVinci Resolve's Python API
and interact with its various components, such as projects, timelines, media pools, and more.
"""
import sys
import os
import platform
import logging
from typing import Optional, Dict, List, Any, Union, Tuple
# Configure logging with a standard format including timestamp, logger name, level, and message
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger("ResolveAPI") # Logger instance for this module
class ResolveAPI:
"""Class to handle connection and interaction with DaVinci Resolve API."""
def __init__(self):
"""
Initialize the ResolveAPI class and establish a connection to DaVinci Resolve.
Sets up internal references to Resolve objects (e.g., project manager, media pool).
"""
self.resolve = None # Main Resolve application object
self.fusion = None # Fusion object for compositing
self.project_manager = None # Project manager object
self.current_project = None # Current project object
self.media_storage = None # Media storage object
self.media_pool = None # Media pool object for the current project
self._connect_to_resolve() # Attempt to connect to Resolve on initialization
def _find_scripting_module(self) -> Optional[str]:
"""
Dynamically locate the DaVinciResolveScript module path based on the operating system.
Checks for a custom path via environment variable, then falls back to default locations.
Returns:
Optional[str]: Path to the scripting module if found, None otherwise.
"""
custom_path = os.environ.get("RESOLVE_SCRIPT_PATH") # Check for user-defined path
if custom_path and os.path.exists(custom_path):
return custom_path
# Default paths for Resolve scripting module by OS
base_paths = {
"Windows": os.path.join(os.environ.get("PROGRAMDATA", "C:\\ProgramData"), "Blackmagic Design", "DaVinci Resolve", "Support", "Developer", "Scripting", "Modules"),
"Darwin": ["/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules",
os.path.join(os.path.expanduser("~"), "Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules")],
"Linux": "/opt/resolve/Developer/Scripting/Modules"
}
system = platform.system() # Get current OS
paths = base_paths.get(system, []) if system != "Darwin" else base_paths["Darwin"]
for path in ([paths] if isinstance(paths, str) else paths): # Handle single path or list
if os.path.exists(path) and path not in sys.path:
sys.path.append(path) # Add to Python path if not already present
return path
return None # Return None if no valid path is found
def _connect_to_resolve(self) -> None:
"""
Establish a connection to DaVinci Resolve by importing its scripting module.
Initializes core objects (e.g., project manager, media pool) if successful.
"""
script_path = self._find_scripting_module() # Find the scripting module path
if not script_path:
logger.error("No valid Resolve scripting module path found")
return
try:
import DaVinciResolveScript as dvr_script # Import the Resolve scripting API
self.resolve = dvr_script.scriptapp("Resolve") # Connect to Resolve app
logger.info(f"Connected to Resolve using {script_path}")
except ImportError:
logger.error(f"Failed to import DaVinciResolveScript from {script_path}")
self.resolve = None
if self.resolve: # If connection is successful, initialize other objects
self.project_manager = self.resolve.GetProjectManager()
self.current_project = self.project_manager.GetCurrentProject()
self.media_storage = self.resolve.GetMediaStorage()
self.fusion = self.resolve.Fusion()
self.media_pool = self.current_project.GetMediaPool() if self.current_project else None
def refresh(self) -> None:
"""
Refresh all internal Resolve objects to ensure they reflect the current state.
Useful if Resolve's state changes externally (e.g., project switch).
"""
if not self.resolve: # Reconnect if not already connected
self._connect_to_resolve()
if self.resolve:
self.project_manager = self.resolve.GetProjectManager() # Update project manager
self.current_project = self.project_manager.GetCurrentProject() # Update current project
self.media_storage = self.resolve.GetMediaStorage() # Update media storage
self.fusion = self.resolve.Fusion() # Update Fusion object
self.media_pool = self.current_project.GetMediaPool() if self.current_project else None # Update media pool
logger.info("Refreshed Resolve API state")
def is_connected(self) -> bool:
"""
Check if the API is connected to DaVinci Resolve.
Returns:
bool: True if connected, False otherwise.
"""
return self.resolve is not None
def get_project_manager(self):
"""
Get the project manager object.
Returns:
Any: ProjectManager object or None if not connected.
"""
return self.project_manager
def get_current_project(self):
"""
Get the current project object, refreshing it from the project manager.
Returns:
Any: Current Project object or None if no project is open.
"""
if self.project_manager:
self.current_project = self.project_manager.GetCurrentProject()
return self.current_project
def get_media_storage(self):
"""
Get the media storage object.
Returns:
Any: MediaStorage object or None if not connected.
"""
return self.media_storage
def get_media_pool(self):
"""
Get the media pool object for the current project, refreshing it if needed.
Returns:
Any: MediaPool object or None if no project is open.
"""
if self.current_project:
self.media_pool = self.current_project.GetMediaPool()
return self.media_pool
def get_fusion(self):
"""
Get the Fusion object for compositing tasks.
Returns:
Any: Fusion object or None if not connected.
"""
return self.fusion
def open_page(self, page_name: str) -> bool:
"""
Open a specific page in DaVinci Resolve (e.g., "edit", "color").
Args:
page_name (str): The name of the page to open (valid: "media", "edit", "fusion", "color", "fairlight", "deliver").
Returns:
bool: True if successful, False if not connected or invalid page.
"""
if not self.resolve:
return False
valid_pages = ["media", "edit", "fusion", "color", "fairlight", "deliver"]
if page_name.lower() not in valid_pages:
return False
self.resolve.OpenPage(page_name.lower()) # Open the specified page
return True
def create_project(self, project_name: str) -> bool:
"""
Create a new project with the given name.
Args:
project_name (str): Name of the project to create.
Returns:
bool: True if successful, False if project manager is unavailable or creation fails.
"""
if not self.project_manager:
return False
new_project = self.project_manager.CreateProject(project_name)
if new_project: # If creation succeeds, update internal state
self.current_project = new_project
self.media_pool = self.current_project.GetMediaPool()
return True
return False
def load_project(self, project_name: str) -> bool:
"""
Load an existing project by name.
Args:
project_name (str): Name of the project to load.
Returns:
bool: True if successful, False if project manager is unavailable or project doesn't exist.
"""
if not self.project_manager:
return False
loaded_project = self.project_manager.LoadProject(project_name)
if loaded_project: # If loading succeeds, update internal state
self.current_project = loaded_project
self.media_pool = self.current_project.GetMediaPool()
return True
return False
def save_project(self) -> bool:
"""
Save the current project.
Returns:
bool: True if successful, False if no project is open.
"""
if not self.current_project:
return False
return self.current_project.SaveProject()
def get_project_name(self) -> Optional[str]:
"""
Get the name of the current project.
Returns:
Optional[str]: Project name or None if no project is open.
"""
if not self.current_project:
return None
return self.current_project.GetName()
def create_timeline(self, timeline_name: str) -> bool:
"""
Create a new empty timeline in the current project.
Args:
timeline_name (str): Name of the timeline to create.
Returns:
bool: True if successful, False if media pool is unavailable or creation fails.
"""
if not self.media_pool:
return False
new_timeline = self.media_pool.CreateEmptyTimeline(timeline_name)
return new_timeline is not None
def get_current_timeline(self):
"""
Get the current timeline in the current project.
Returns:
Any: Timeline object or None if no project is open.
"""
if not self.current_project:
return None
return self.current_project.GetCurrentTimeline()
def get_timeline_count(self) -> int:
"""
Get the number of timelines in the current project.
Returns:
int: Number of timelines, or 0 if no project is open.
"""
if not self.current_project:
return 0
return self.current_project.GetTimelineCount()
def get_timeline_by_index(self, index: int):
"""
Get a timeline by its 1-based index.
Args:
index (int): 1-based index of the timeline.
Returns:
Any: Timeline object or None if no project or invalid index.
"""
if not self.current_project:
return None
return self.current_project.GetTimelineByIndex(index)
def set_current_timeline(self, timeline) -> bool:
"""
Set the specified timeline as the current one.
Args:
timeline: Timeline object to set as current.
Returns:
bool: True if successful, False if no project is open.
"""
if not self.current_project:
return False
return self.current_project.SetCurrentTimeline(timeline)
def get_mounted_volumes(self) -> List[str]:
"""
Get a list of mounted volumes in the media storage.
Returns:
List[str]: List of volume paths, empty if media storage is unavailable.
"""
if not self.media_storage:
return []
return self.media_storage.GetMountedVolumes()
def get_sub_folders(self, folder_path: str) -> List[str]:
"""
Get a list of subfolders in the specified folder path.
Args:
folder_path (str): Path to the folder.
Returns:
List[str]: List of subfolder paths, empty if media storage is unavailable.
"""
if not self.media_storage:
return []
return self.media_storage.GetSubFolders(folder_path)
def get_files(self, folder_path: str) -> List[str]:
"""
Get a list of files in the specified folder path.
Args:
folder_path (str): Path to the folder.
Returns:
List[str]: List of file paths, empty if media storage is unavailable.
"""
if not self.media_storage:
return []
return self.media_storage.GetFiles(folder_path)
def add_items_to_media_pool(self, file_paths: List[str]) -> List[Any]:
"""
Add media files to the current media pool.
Args:
file_paths (List[str]): List of file paths to add.
Returns:
List[Any]: List of added media pool items, empty if media storage or pool is unavailable.
"""
if not self.media_storage or not self.media_pool:
return []
return self.media_storage.AddItemsToMediaPool(file_paths)
def get_root_folder(self):
"""
Get the root folder of the media pool.
Returns:
Any: Root folder object or None if media pool is unavailable.
"""
if not self.media_pool:
return None
return self.media_pool.GetRootFolder()
def get_current_folder(self):
"""
Get the current folder in the media pool.
Returns:
Any: Current folder object or None if media pool is unavailable.
"""
if not self.media_pool:
return None
return self.media_pool.GetCurrentFolder()
def add_sub_folder(self, parent_folder, folder_name: str):
"""
Add a subfolder to the specified parent folder in the media pool.
Args:
parent_folder: Parent folder object.
folder_name (str): Name of the subfolder to create.
Returns:
Any: Created subfolder object or None if media pool is unavailable or creation fails.
"""
if not self.media_pool:
return None
return self.media_pool.AddSubFolder(parent_folder, folder_name)
def get_folder_clips(self, folder) -> List[Any]:
"""
Get a list of clips in the specified folder.
Args:
folder: Folder object.
Returns:
List[Any]: List of media pool items, empty if folder is invalid.
"""
if not folder:
return []
return folder.GetClips()
def get_folder_name(self, folder) -> Optional[str]:
"""
Get the name of the specified folder.
Args:
folder: Folder object.
Returns:
Optional[str]: Folder name or None if folder is invalid.
"""
if not folder:
return None
return folder.GetName()
def get_folder_sub_folders(self, folder) -> List[Any]:
"""
Get a list of subfolders in the specified folder.
Args:
folder: Folder object.
Returns:
List[Any]: List of subfolder objects, empty if folder is invalid.
"""
if not folder:
return []
return folder.GetSubFolders()
def append_to_timeline(self, clips: List[Any]) -> bool:
"""
Append clips to the current timeline.
Args:
clips (List[Any]): List of media pool items to append.
Returns:
bool: True if successful, False if media pool is unavailable.
"""
if not self.media_pool:
return False
return self.media_pool.AppendToTimeline(clips)
def create_timeline_from_clips(self, timeline_name: str, clips: List[Any]):
"""
Create a new timeline from the specified clips.
Args:
timeline_name (str): Name of the new timeline.
clips (List[Any]): List of media pool items to include.
ACC Returns:
Any: Created timeline object or None if media pool is unavailable.
"""
if not self.media_pool:
return None
return self.media_pool.CreateTimelineFromClips(timeline_name, clips)
def import_timeline_from_file(self, file_path: str):
"""
Import a timeline from a file (e.g., XML, EDL).
Args:
file_path (str): Path to the timeline file.
Returns:
Any: Imported timeline object or None if media pool is unavailable.
"""
if not self.media_pool:
return None
return self.media_pool.ImportTimelineFromFile(file_path)
def execute_lua(self, script: str) -> Any:
"""
Execute a Lua script in Resolve's Fusion environment.
Args:
script (str): Lua script to execute.
Returns:
Any: Result of the script execution or None if Fusion is unavailable.
"""
if not self.fusion:
return None
return self.fusion.Execute(script)
def create_fusion_node(self, node_type: str, inputs: Dict[str, Any] = None) -> Any:
"""
Create a new node in the current Fusion composition.
Args:
node_type (str): Type of node to create (e.g., "Blur", "ColorCorrector").
inputs (Dict[str, Any], optional): Dictionary of input parameters for the node.
Returns:
Any: Created node object or None if Fusion or composition is unavailable.
"""
try:
comp = fusion.GetCurrentComp()
if not comp:
print("No Fusion composition found.")
return None
# Include position parameters (x, y)
node = comp.AddTool(node_type, 0, 0)
if not node:
print(f"Error creating {node_type} node.")
return None
# Set input parameters if provided
if inputs and node:
for key, value in inputs.items():
# Use SetInput method instead of dictionary-style assignment
node.SetInput(key, value)
print(f"{node_type} node created successfully.")
return node
except Exception as e:
print(f"Error creating Fusion node: {e}")
return None
def get_current_comp(self) -> Any:
"""
Get the current Fusion composition.
Returns:
Any: Current composition object or None if Fusion is unavailable.
"""
if not self.fusion:
return None
try:
return self.fusion.CurrentComp
except Exception as e:
logger.error(f"Error getting current composition: {e}")
return None
# New methods with enhanced functionality
def get_timeline_items(self, track_type: str = "video", track_index: int = 1) -> List[Any]:
"""
Get items (clips) from a specific track in the current timeline.
Args:
track_type (str): Type of track ("video", "audio", "subtitle"), defaults to "video".
track_index (int): 1-based index of the track, defaults to 1.
Returns:
List[Any]: List of timeline items, empty if no timeline or track is invalid.
"""
timeline = self.get_current_timeline()
if not timeline:
logger.warning("No current timeline available")
return []
try:
items = timeline.GetItemListInTrack(track_type, track_index)
return items if items else []
except Exception as e:
logger.error(f"Failed to get timeline items: {e}")
return []
def set_clip_property(self, clip, property_name: str, value: Any) -> bool:
"""
Set a property on a timeline clip (e.g., "Pan", "ZoomX").
Args:
clip: Timeline item object.
property_name (str): Name of the property to set.
value: Value to assign to the property.
Returns:
bool: True if successful, False if clip is invalid or property set fails.
"""
if not clip:
return False
try:
return clip.SetProperty(property_name, value)
except Exception as e:
logger.error(f"Failed to set clip property {property_name}: {e}")
return False
def get_color_page_nodes(self) -> List[Any]:
"""
Get all nodes in the current clip's color grade on the Color page.
Returns:
List[Any]: List of node objects, empty if no timeline or clip is available.
"""
timeline = self.get_current_timeline()
if not timeline:
return []
clip = timeline.GetCurrentVideoItem()
if not clip:
logger.warning("No current clip on Color page")
return []
try:
return clip.GetNodeGraph().GetNodes()
except Exception as e:
logger.error(f"Failed to get color nodes: {e}")
return []
def add_color_node(self, node_type: str = "Corrector") -> Optional[Any]:
"""
Add a new node to the current clip's color grade.
Args:
node_type (str): Type of node to add (e.g., "Corrector", "Layer"), defaults to "Corrector".
Returns:
Optional[Any]: Created node object or None if no timeline or clip is available.
"""
timeline = self.get_current_timeline()
if not timeline:
return None
clip = timeline.GetCurrentVideoItem()
if not clip:
return None
try:
node_graph = clip.GetNodeGraph()
return node_graph.AddNode(node_type)
except Exception as e:
logger.error(f"Failed to add color node: {e}")
return None
def get_project_settings(self) -> Dict[str, Any]:
"""
Get the current project's settings (e.g., frame rate, resolution).
Returns:
Dict[str, Any]: Dictionary of project settings, empty if no project is open.
"""
if not self.current_project:
return {}
try:
return self.current_project.GetSetting()
except Exception as e:
logger.error(f"Failed to get project settings: {e}")
return {}
def set_project_setting(self, key: str, value: Any) -> bool:
"""
Set a specific project setting.
Args:
key (str): Setting key (e.g., "timelineFrameRate").
value: Value to set for the key.
Returns:
bool: True if successful, False if no project or setting fails.
"""
if not self.current_project:
return False
try:
return self.current_project.SetSetting(key, value)
except Exception as e:
logger.error(f"Failed to set project setting {key}: {e}")
return False
def start_render(self, preset_name: str = None, render_path: str = None) -> bool:
"""
Start rendering the current project with an optional preset and output path.
Args:
preset_name (str, optional): Name of the render preset to use.
render_path (str, optional): Output directory for the render.
Returns:
bool: True if render starts successfully, False if no project or render fails.
"""
if not self.current_project:
return False
try:
if preset_name:
self.current_project.LoadRenderPreset(preset_name) # Load render preset if specified
if render_path:
self.current_project.SetRenderSettings({"TargetDir": render_path}) # Set output path
return self.current_project.StartRendering()
except Exception as e:
logger.error(f"Failed to start render: {e}")
return False
def get_render_status(self) -> Dict[str, Any]:
"""
Get the current render status of the project.
Returns:
Dict[str, Any]: Status info (e.g., "IsRenderInProgress", "CompletionPercentage"), empty if no project.
"""
if not self.current_project:
return {}
try:
return {
"IsRenderInProgress": self.current_project.IsRenderingInProgress(),
"CompletionPercentage": self.current_project.GetRenderingProgress()
}
except Exception as e:
logger.error(f"Failed to get render status: {e}")
return {}
def add_timeline_marker(self, frame: int, color: str = "Blue", name: str = "", note: str = "") -> bool:
"""
Add a marker to the current timeline at a specific frame.
Args:
frame (int): Frame number for the marker.
color (str): Marker color (e.g., "Blue", "Red"), defaults to "Blue".
name (str): Marker name, defaults to empty string.
note (str): Marker note, defaults to empty string.
Returns:
bool: True if successful, False if no timeline or addition fails.
"""
timeline = self.get_current_timeline()
if not timeline:
return False
try:
return timeline.AddMarker(frame, color, name, note, 1) # Duration of 1 frame
except Exception as e:
logger.error(f"Failed to add marker: {e}")
return False
def get_clip_metadata(self, clip) -> Dict[str, Any]:
"""
Get metadata for a specific clip (e.g., frame rate, resolution).
Args:
clip: Media pool item or timeline item.
Returns:
Dict[str, Any]: Metadata dictionary, empty if clip is invalid.
"""
if not clip:
return {}
try:
return clip.GetMetadata()
except Exception as e:
logger.error(f"Failed to get clip metadata: {e}")
return {}
def get_gallery(self) -> Any:
"""
Get the Gallery object for the current project, used for managing stills and grades.
Returns:
Any: Gallery object or None if no project is open.
"""
if not self.current_project:
logger.warning("No current project available")
return None
try:
return self.current_project.GetGallery()
except Exception as e:
logger.error(f"Failed to get gallery: {e}")
return None
def get_gallery_albums(self) -> List[Any]:
"""
Get all albums in the gallery.
Returns:
List[Any]: List of GalleryAlbum objects, empty if gallery is unavailable.
"""
gallery = self.get_gallery()
if not gallery:
return []
try:
return gallery.GetGalleryAlbumList()
except Exception as e:
logger.error(f"Failed to get gallery albums: {e}")
return []
def save_still(self, album_name: str = "Stills") -> Optional[Any]:
"""
Save the current clip's grade as a still in the specified gallery album.
Args:
album_name (str): Name of the album to save the still in, defaults to "Stills".
Returns:
Optional[Any]: Saved GalleryStill object or None if saving fails.
"""
gallery = self.get_gallery()
timeline = self.get_current_timeline()
if not gallery or not timeline:
return None
clip = timeline.GetCurrentVideoItem()
if not clip:
logger.warning("No current clip to save still from")
return None
try:
album = gallery.GetAlbum(album_name)
if not album:
album = gallery.CreateEmptyAlbum(album_name) # Create album if it doesn't exist
return clip.SaveAsStill(album)
except Exception as e:
logger.error(f"Failed to save still: {e}")
return None
def apply_still(self, still, clip=None) -> bool:
"""
Apply a still (grade) to a clip, defaulting to the current clip if none specified.
Args:
still: GalleryStill object to apply.
clip: Timeline item to apply the still to (optional).
Returns:
bool: True if successful, False if still or clip is invalid.
"""
if not still:
return False
target_clip = clip or self.get_current_timeline().GetCurrentVideoItem() if self.get_current_timeline() else None
if not target_clip:
logger.warning("No clip to apply still to")
return False
try:
return target_clip.ApplyGradeFromStill(still)
except Exception as e:
logger.error(f"Failed to apply still: {e}")
return False
def add_track(self, track_type: str = "video") -> bool:
"""
Add a new track to the current timeline.
Args:
track_type (str): Type of track to add ("video", "audio", "subtitle"), defaults to "video".
Returns:
bool: True if successful, False if no timeline or addition fails.
"""
timeline = self.get_current_timeline()
if not timeline:
return False
try:
return timeline.AddTrack(track_type)
except Exception as e:
logger.error(f"Failed to add {track_type} track: {e}")
return False
def set_track_name(self, track_type: str, track_index: int, name: str) -> bool:
"""
Set the name of a track in the current timeline.
Args:
track_type (str): Type of track ("video", "audio", "subtitle").
track_index (int): 1-based index of the track.
name (str): New name for the track.
Returns:
bool: True if successful, False if no timeline or naming fails.
"""
timeline = self.get_current_timeline()
if not timeline:
return False
try:
return timeline.SetTrackName(track_type, track_index, name)
except Exception as e:
logger.error(f"Failed to set track name: {e}")
return False
def enable_track(self, track_type: str, track_index: int, enable: bool = True) -> bool:
"""
Enable or disable a track in the current timeline.
Args:
track_type (str): Type of track ("video", "audio", "subtitle").
track_index (int): 1-based index of the track.
enable (bool): True to enable, False to disable, defaults to True.
Returns:
bool: True if successful, False if no timeline or enabling fails.
"""
timeline = self.get_current_timeline()
if not timeline:
return False
try:
return timeline.SetTrackEnable(track_type, track_index, enable)
except Exception as e:
logger.error(f"Failed to set track enable state: {e}")
return False
def get_audio_volume(self, clip) -> Optional[float]:
"""
Get the audio volume of a timeline clip.
Args:
clip: Timeline item with audio.
Returns:
Optional[float]: Volume level (e.g., 0.0 to 1.0) or None if clip is invalid.
"""
if not clip:
return None
try:
return clip.GetAudioVolume()
except Exception as e:
logger.error(f"Failed to get audio volume: {e}")
return None
def set_audio_volume(self, clip, volume: float) -> bool:
"""
Set the audio volume of a timeline clip.
Args:
clip: Timeline item with audio.
volume (float): Volume level to set (e.g., 0.0 to 1.0).
Returns:
bool: True if successful, False if clip is invalid or setting fails.
"""
if not clip:
return False
try:
return clip.SetAudioVolume(volume)
except Exception as e:
logger.error(f"Failed to set audio volume: {e}")
return False
def set_track_volume(self, track_index: int, volume: float) -> bool:
"""
Set the volume of an audio track in the current timeline.
Args:
track_index (int): 1-based index of the audio track.
volume (float): Volume level to set (e.g., 0.0 to 1.0).
Returns:
bool: True if successful, False if no timeline or setting fails.
"""
timeline = self.get_current_timeline()
if not timeline:
return False
try:
return timeline.SetTrackVolume("audio", track_index, volume)
except Exception as e:
logger.error(f"Failed to set track volume: {e}")
return False
def get_version_count(self, clip, version_type: str = "color") -> int:
"""
Get the number of versions (e.g., color grades) for a clip.
Args:
clip: Timeline item.
version_type (str): Type of version ("color" or "fusion"), defaults to "color".
Returns:
int: Number of versions, 0 if clip is invalid.
"""
if not clip:
return 0
try:
return clip.GetVersionCount(version_type)
except Exception as e:
logger.error(f"Failed to get version count: {e}")
return 0
def set_current_version(self, clip, version_index: int, version_type: str = "color") -> bool:
"""
Set the current version for a clip (e.g., switch between color grades).
Args:
clip: Timeline item.
version_index (int): 0-based index of the version to set.
version_type (str): Type of version ("color" or "fusion"), defaults to "color".
Returns:
bool: True if successful, False if clip is invalid or setting fails.
"""
if not clip:
return False
try:
return clip.SetCurrentVersion(version_index, version_type)
except Exception as e:
logger.error(f"Failed to set current version: {e}")
return False
def play(self) -> bool:
"""
Start playback in DaVinci Resolve.
Returns:
bool: True if successful, False if not connected or playback fails.
"""
if not self.resolve:
return False
try:
self.resolve.Play()
return True
except Exception as e:
logger.error(f"Failed to start playback: {e}")
return False
def stop(self) -> bool:
"""
Stop playback in DaVinci Resolve.
Returns:
bool: True if successful, False if not connected or stop fails.
"""
if not self.resolve:
return False
try:
self.resolve.Stop()
return True
except Exception as e:
logger.error(f"Failed to stop playback: {e}")
return False
def get_current_timecode(self) -> Optional[str]:
"""
Get the current playback timecode in Resolve.
Returns:
Optional[str]: Timecode string (e.g., "01:00:00:00") or None if not connected.
"""
if not self.resolve:
return None
try:
return self.resolve.GetCurrentTimecode()
except Exception as e:
logger.error(f"Failed to get current timecode: {e}")
return None
def set_playhead_position(self, frame: int) -> bool:
"""
Set the playhead position to a specific frame in the current timeline.
Args:
frame (int): Frame number to set the playhead to.
Returns:
bool: True if successful, False if no timeline or setting fails.
"""
timeline = self.get_current_timeline()
if not timeline:
return False
try:
return timeline.SetCurrentTimecode(timeline.GetTimecodeFromFrame(frame))
except Exception as e:
logger.error(f"Failed to set playhead position: {e}")
return False
def export_project(self, project_name: str, file_path: str) -> bool:
"""
Export a project to a file (e.g., .drp file).
Args:
project_name (str): Name of the project to export.
file_path (str): Destination file path for the exported project.
Returns:
bool: True if successful, False if project manager is unavailable or export fails.
"""
if not self.project_manager:
return False
try:
return self.project_manager.ExportProject(project_name, file_path)
except Exception as e:
logger.error(f"Failed to export project: {e}")
return False
def import_project(self, file_path: str) -> bool:
"""
Import a project from a file (e.g., .drp file).
Args:
file_path (str): Path to the project file to import.
Returns:
bool: True if successful, False if project manager is unavailable or import fails.
"""
if not self.project_manager:
return False
try:
return self.project_manager.ImportProject(file_path)
except Exception as e:
logger.error(f"Failed to import project: {e}")
return False
```