Metadata-Version: 2.4
Name: kdenlive-api
Version: 0.1.0
Summary: Python client library for Kdenlive JSON-RPC API
Project-URL: Homepage, https://github.com/IO-AtelierTech/kdenlive-automation
Project-URL: Repository, https://github.com/IO-AtelierTech/kdenlive-automation
Project-URL: Documentation, https://github.com/IO-AtelierTech/kdenlive-automation#readme
Project-URL: Issues, https://github.com/IO-AtelierTech/kdenlive-automation/issues
Project-URL: Changelog, https://github.com/IO-AtelierTech/kdenlive-automation/blob/main/CHANGELOG.md
Author-email: IO-AtelierTech <ioateliertech@gmail.com>
License: MIT
License-File: LICENSE
Keywords: automation,editing,json-rpc,kdenlive,video,websocket
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.0
Requires-Dist: websockets>=12.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# kdenlive-api

Python client library for the Kdenlive JSON-RPC WebSocket API.

## Installation

```bash
pip install kdenlive-api
```

Or with uv:

```bash
uv add kdenlive-api
```

## Requirements

- Python 3.11+
- Kdenlive with RPC server enabled (requires kdenlive-websocket fork)

## Quick Start

```python
import asyncio
from kdenlive_api import KdenliveClient

async def main():
    async with KdenliveClient() as client:
        # Get project info
        info = await client.project.get_info()
        print(f"Project: {info.name}")
        print(f"Resolution: {info.width}x{info.height}")
        print(f"FPS: {info.fps}")

        # List clips in bin
        clips = await client.bin.list_clips()
        for clip in clips:
            print(f"  - {clip.name}")

        # Get timeline info
        timeline = await client.timeline.get_info()
        print(f"Timeline duration: {timeline.duration} frames")

asyncio.run(main())
```

## Configuration

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `KDENLIVE_WS_URL` | WebSocket URL | `ws://localhost:9876` |
| `KDENLIVE_AUTH_TOKEN` | Bearer token for authentication | None |

### Programmatic Configuration

```python
from kdenlive_api import KdenliveClient

# Custom URL
client = KdenliveClient(url="ws://192.168.1.100:9876")

# With authentication
client = KdenliveClient(auth_token="your-secret-token")

# Both
client = KdenliveClient(
    url="ws://192.168.1.100:9876",
    auth_token="your-secret-token"
)
```

## API Namespaces

The client provides access to Kdenlive functionality through typed API namespaces:

### Project (`client.project`)

```python
# Get project information
info = await client.project.get_info()

# Open a project
await client.project.open("/path/to/project.kdenlive")

# Save project
await client.project.save()
await client.project.save("/path/to/new-project.kdenlive")

# Create new project
await client.project.new(profile="atsc_1080p_25")

# Close project
await client.project.close(save_changes=True)

# Undo/Redo
await client.project.undo()
await client.project.redo()
```

### Timeline (`client.timeline`)

```python
# Get timeline info
info = await client.timeline.get_info()

# Get all tracks
tracks = await client.timeline.get_tracks()

# Get clips (all or by track)
clips = await client.timeline.get_clips()
clips = await client.timeline.get_clips(track_id=0)

# Insert clip from bin
clip_id = await client.timeline.insert_clip(
    bin_clip_id="abc123",
    track_id=0,
    position=100  # frames
)

# Move clip
await client.timeline.move_clip(clip_id=1, track_id=1, position=200)

# Delete clip
await client.timeline.delete_clip(clip_id=1)

# Split clip
parts = await client.timeline.split_clip(clip_id=1, position=50)

# Resize/trim clip
await client.timeline.resize_clip(clip_id=1, in_point=10, out_point=100)

# Track management
track_id = await client.timeline.add_track("video", name="V3")
await client.timeline.delete_track(track_id=2)

# Playhead
await client.timeline.seek(position=500)
position = await client.timeline.get_position()
```

### Bin (`client.bin`)

```python
# List clips and folders
clips = await client.bin.list_clips()
clips = await client.bin.list_clips(folder_id="folder123")
folders = await client.bin.list_folders()

# Get clip details
info = await client.bin.get_clip_info(clip_id="abc123")

# Import media
clip_id = await client.bin.import_clip("/path/to/video.mp4")
clip_ids = await client.bin.import_clips([
    "/path/to/video1.mp4",
    "/path/to/video2.mp4"
], folder_id="folder123")

# Organize
folder_id = await client.bin.create_folder("Footage", parent_id=None)
await client.bin.rename_item(item_id="abc123", name="New Name")
await client.bin.move_item(item_id="abc123", target_folder_id="folder123")
await client.bin.delete_clip(clip_id="abc123")

# Markers
marker_id = await client.bin.add_clip_marker(
    clip_id="abc123",
    position=100,
    comment="Important moment"
)
await client.bin.delete_clip_marker(clip_id="abc123", marker_id=0)
```

### Effects (`client.effects`)

```python
# List available effects
effects = await client.effects.list_available()

# Get effect details
info = await client.effects.get_info(effect_id="blur")

# Apply effect to clip
instance_id = await client.effects.add(effect_id="blur", clip_id=1)

# Get effects on a clip
clip_effects = await client.effects.get_clip_effects(clip_id=1)

# Modify effect parameters
await client.effects.set_property(
    clip_id=1,
    effect_id="effect123",
    property_name="radius",
    value=10.5
)

# Enable/disable effects
await client.effects.enable(clip_id=1, effect_id="effect123")
await client.effects.disable(clip_id=1, effect_id="effect123")

# Remove effect
await client.effects.remove(clip_id=1, effect_id="effect123")

# Keyframes
keyframes = await client.effects.get_keyframes(
    clip_id=1,
    effect_id="effect123",
    property_name="radius"
)

await client.effects.set_keyframe(
    clip_id=1,
    effect_id="effect123",
    property_name="radius",
    position=50,
    value=20.0
)

await client.effects.delete_keyframe(
    clip_id=1,
    effect_id="effect123",
    property_name="radius",
    position=50
)
```

### Render (`client.render`)

```python
# Get available presets
presets = await client.render.get_presets()

# Start render
job = await client.render.start(
    preset_name="MP4-H264",
    output_path="/path/to/output.mp4"
)

# Monitor progress
status = await client.render.get_status(job_id=job.job_id)
print(f"Progress: {status.progress}%")

# Get all jobs
jobs = await client.render.get_jobs()
active = await client.render.get_active_job()

# Stop render
await client.render.stop(job_id=job.job_id)
```

### Assets (`client.asset`)

```python
# Browse categories
categories = await client.asset.list_categories()

# Search
results = await client.asset.search("blur")

# Get effects by category
effects = await client.asset.get_effects_by_category("Color")

# Favorites
favorites = await client.asset.get_favorites()
await client.asset.add_favorite(asset_id="blur")
await client.asset.remove_favorite(asset_id="blur")

# Presets
presets = await client.asset.get_presets(effect_id="blur")
await client.asset.save_preset(
    effect_id="blur",
    preset_name="My Blur",
    clip_id=1
)
```

### Transitions (`client.transition`)

```python
# List available transitions
transitions = await client.transition.list()

# Add transition between clips
info = await client.transition.add(
    transition_type="dissolve",
    from_clip_id=1,
    to_clip_id=2
)

# Remove transition
await client.transition.remove(transition_id=info.id)
```

### Compositions (`client.composition`)

```python
# List compositions
compositions = await client.composition.list()

# Add composition
info = await client.composition.add(
    composition_type="composite",
    track=0,
    position=100
)

# Remove composition
await client.composition.remove(composition_id=info.id)
```

## Notifications

Subscribe to real-time notifications from Kdenlive:

```python
async def handle_notification(method: str, params: dict):
    if method == "render.progress":
        print(f"Render progress: {params['progress']}%")
    elif method == "timeline.changed":
        print("Timeline was modified")

client = KdenliveClient()
client.on_notification(handle_notification)
await client.connect()
```

## Error Handling

```python
from kdenlive_api import (
    KdenliveError,
    ConnectionError,
    ProjectNotOpenError,
    ClipNotFoundError,
    ValidationError,
)

try:
    await client.project.get_info()
except ConnectionError:
    print("Cannot connect to Kdenlive")
except ProjectNotOpenError:
    print("No project is open")
except ClipNotFoundError as e:
    print(f"Clip not found: {e}")
except ValidationError as e:
    print(f"Invalid parameters: {e}")
except KdenliveError as e:
    print(f"Kdenlive error: {e}")
```

## Type Safety

All API responses are typed using Pydantic models:

```python
from kdenlive_api import ProjectInfo, ClipInfo, TimelineClip

info: ProjectInfo = await client.project.get_info()
print(info.width)  # IDE autocomplete works!
```

## License

MIT License - see LICENSE file for details.
