Using mkv2cast as a Python Package
mkv2cast can be imported and used as a Python library in your scripts, allowing you to programmatically convert video files with full control over the conversion process.
Installation
First, ensure mkv2cast is installed:
pip install mkv2cast
Basic Usage
The simplest way to use mkv2cast programmatically is with the convert_file function:
from mkv2cast import convert_file, Config
from pathlib import Path
# Use Config.for_library() for optimal library usage
config = Config.for_library(hw="auto")
# Convert a single file
success, output_path, message = convert_file(Path("movie.mkv"), cfg=config)
if success:
if output_path:
print(f"Converted to: {output_path}")
else:
print(f"Skipped: {message}")
else:
print(f"Failed: {message}")
The convert_file function returns a tuple of:
success(bool): Whether the operation succeededoutput_path(Path | None): Path to output file if created, None if skippedmessage(str): Status message explaining the result
Script Mode and Library Usage
When using mkv2cast as a library, you should disable UI features that are designed
for interactive CLI usage. The easiest way is to use Config.for_library():
from mkv2cast import Config
# Recommended: auto-disables progress bars, notifications, and Rich UI
config = Config.for_library(
hw="vaapi",
crf=20,
# ... other options
)
What gets disabled:
progress=False: No progress barsnotify=False: No desktop notificationspipeline=False: No Rich UI (uses simple mode)
Automatic Detection:
mkv2cast can automatically detect script mode using is_script_mode():
from mkv2cast import is_script_mode
if is_script_mode():
print("Running in script mode")
Script mode is detected when:
sys.stdoutis not a TTY (piped or redirected)NO_COLORenvironment variable is setMKV2CAST_SCRIPT_MODE=1environment variable is set
Configuration
Create a custom configuration to control encoding settings:
from mkv2cast import Config, convert_file
from pathlib import Path
# Create custom configuration
config = Config(
hw="vaapi", # Use VAAPI hardware acceleration
crf=20, # Quality setting (lower = better quality)
preset="slow", # Encoding preset
container="mp4", # Output container format
suffix=".cast", # Output file suffix
notify=False # Disable notifications in scripts
)
# Convert with custom config
success, output_path, message = convert_file(
Path("movie.mkv"),
cfg=config
)
Available Configuration Options
The Config class supports all command-line options:
config = Config(
# Output settings
suffix=".cast",
container="mkv", # or "mp4"
# Hardware acceleration
hw="auto", # "auto", "nvenc", "qsv", "vaapi", "cpu"
vaapi_device="/dev/dri/renderD128",
vaapi_qp=23,
qsv_quality=23,
nvenc_cq=23,
# Encoding quality
crf=20,
preset="slow", # "ultrafast" to "veryslow"
abr="192k",
# Codec decisions
skip_when_ok=True,
force_h264=False,
allow_hevc=False,
force_aac=False,
keep_surround=False,
# Audio/Subtitle selection
audio_lang="fre,fra,fr", # Comma-separated language codes
audio_track=None, # Explicit track index (0-based)
subtitle_lang="fre,eng",
subtitle_track=None,
prefer_forced_subs=True,
no_subtitles=False,
# Integrity checks
integrity_check=True,
stable_wait=3,
deep_check=False,
# Notifications
notify=True,
notify_on_success=True,
notify_on_failure=True,
)
Analyzing Files
Before converting, you can analyze a file to see what transcoding is needed:
from mkv2cast import decide_for, pick_backend
from pathlib import Path
# Analyze a file
decision = decide_for(Path("movie.mkv"))
print(f"Video codec: {decision.vcodec}")
print(f"Audio codec: {decision.acodec}")
print(f"Needs video transcode: {decision.need_v}")
print(f"Needs audio transcode: {decision.need_a}")
print(f"Reason: {decision.reason_v}")
print(f"Video profile: {decision.vprof}")
print(f"Video level: {decision.vlevel}")
print(f"Is HDR: {decision.vhdr}")
print(f"Audio language: {decision.alang}")
print(f"Audio channels: {decision.ach}")
# Check available backend
backend = pick_backend()
print(f"Best backend: {backend}")
The Decision object contains detailed information about the file:
need_v: Whether video transcoding is neededneed_a: Whether audio transcoding is neededvcodec: Source video codec nameacodec: Source audio codec nameaidx: Selected audio track indexsidx: Selected subtitle track indexreason_v: Explanation of video decisionAnd more…
Progress Callbacks
Instead of parsing JSON output, you can use progress callbacks directly. This is the recommended approach for integrating mkv2cast into larger applications.
Basic Progress Callback
from mkv2cast import convert_file, Config
from pathlib import Path
def on_progress(filepath: Path, progress: dict):
"""Called during conversion with progress updates."""
stage = progress.get("stage", "unknown")
percent = progress.get("progress_percent", 0)
fps = progress.get("fps", 0)
eta = progress.get("eta_seconds", 0)
print(f"{filepath.name}: {stage} - {percent:.1f}% @ {fps:.1f}fps, ETA: {eta:.0f}s")
config = Config.for_library(hw="auto")
success, output, msg = convert_file(
Path("movie.mkv"),
cfg=config,
progress_callback=on_progress
)
Progress Dictionary Fields
The callback receives a dictionary with the following fields:
Field |
Type |
Description |
|---|---|---|
|
str |
Current stage: “checking”, “encoding”, “done”, “skipped”, “failed” |
|
float |
Progress percentage (0-100) |
|
float |
Current encoding FPS |
|
float |
Estimated time remaining in seconds |
|
str |
Current bitrate (e.g., “2500kbits/s”) |
|
str |
Encoding speed relative to playback (e.g., “2.5x”) |
|
int |
Current position in milliseconds |
|
int |
Total duration in milliseconds |
|
str or None |
Error message if stage is “failed” |
Advanced: Progress with Logging
import logging
from mkv2cast import convert_file, Config
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def on_progress(filepath: Path, progress: dict):
stage = progress["stage"]
percent = progress["progress_percent"]
if stage == "checking":
logger.info(f"Checking integrity: {filepath.name}")
elif stage == "encoding":
logger.info(f"Encoding {filepath.name}: {percent:.1f}%")
elif stage == "done":
logger.info(f"Complete: {filepath.name}")
elif stage == "failed":
logger.error(f"Failed {filepath.name}: {progress.get('error')}")
elif stage == "skipped":
logger.info(f"Skipped: {filepath.name}")
config = Config.for_library(hw="vaapi")
convert_file(Path("movie.mkv"), cfg=config, progress_callback=on_progress)
Batch Processing (Sequential)
Process multiple files sequentially with custom logic:
from pathlib import Path
from mkv2cast import convert_file, Config
config = Config.for_library(hw="auto", container="mkv")
input_dir = Path("/media/videos")
output_dir = Path("/media/converted")
for mkv_file in input_dir.glob("**/*.mkv"):
success, output, msg = convert_file(
mkv_file,
cfg=config,
output_dir=output_dir
)
if success and output:
print(f"OK {mkv_file.name} -> {output.name}")
elif not success:
print(f"FAIL {mkv_file.name}: {msg}")
else:
print(f"SKIP {mkv_file.name}: {msg}")
Batch Processing with Multi-threading
For parallel processing of multiple files, use convert_batch():
from mkv2cast import convert_batch, Config
from pathlib import Path
config = Config.for_library(
hw="vaapi",
encode_workers=2, # 2 parallel encoders
)
def on_progress(filepath: Path, progress: dict):
"""Thread-safe callback for progress updates."""
percent = progress.get("progress_percent", 0)
stage = progress.get("stage", "")
print(f"{filepath.name}: {stage} {percent:.1f}%")
files = list(Path("/media/videos").glob("*.mkv"))
results = convert_batch(
files,
cfg=config,
progress_callback=on_progress,
output_dir=Path("/media/converted")
)
# Check results
success_count = sum(1 for s, _, _ in results.values() if s)
fail_count = len(results) - success_count
print(f"Done: {success_count} converted, {fail_count} failed")
Important: The callback should be thread-safe when using convert_batch()
as it may be called from multiple threads simultaneously.
Multi-threading Configuration
The number of parallel workers is controlled by encode_workers:
config = Config.for_library(
encode_workers=4, # 4 parallel encoding threads
)
Setting encode_workers=0 (default) uses auto-detection based on system resources.
Advanced: Building Custom Commands
For more control, you can build FFmpeg commands manually:
from mkv2cast import decide_for, pick_backend, build_transcode_cmd
from pathlib import Path
import subprocess
input_file = Path("movie.mkv")
output_file = Path("movie.h264.cast.mkv")
config = Config()
# Analyze file
decision = decide_for(input_file, config)
# Select backend
backend = pick_backend(config)
# Build command (log_path is optional)
cmd, stage = build_transcode_cmd(
input_file,
decision,
backend,
output_file,
log_path=None, # Optional: Path to log file
cfg=config
)
# Run manually
result = subprocess.run(cmd)
if result.returncode == 0:
print(f"{stage} completed successfully")
Working with History
Access conversion history programmatically:
from mkv2cast import HistoryDB, get_app_dirs
# Get history database
dirs = get_app_dirs()
history = HistoryDB(dirs["state"])
# Get recent conversions
recent = history.get_recent(20)
for entry in recent:
status = entry.get("status", "unknown")
input_path = entry.get("input_path", "unknown")
started = entry.get("started_at", "unknown")
print(f"[{started}] {status}: {input_path}")
# Get statistics
stats = history.get_stats()
by_status = stats.get("by_status", {})
total = sum(by_status.values())
print(f"Total conversions: {total}")
for status, count in by_status.items():
print(f" {status}: {count}")
# Get average encode time
avg_time = stats.get("avg_encode_time", 0)
print(f"Average encode time: {avg_time:.1f}s")
Loading Configuration Files
Load settings from configuration files:
from mkv2cast import load_config_file, get_app_dirs, Config
# Get config directory
dirs = get_app_dirs()
# Load config file (TOML or INI)
file_config = load_config_file(dirs["config"])
# Create base config
config = Config()
# Manually apply file config to Config instance
if "encoding" in file_config:
encoding = file_config["encoding"]
if "backend" in encoding:
config.hw = encoding["backend"]
if "crf" in encoding:
config.crf = encoding["crf"]
if "preset" in encoding:
config.preset = encoding["preset"]
if "abr" in encoding:
config.abr = encoding["abr"]
if "output" in file_config:
output = file_config["output"]
if "suffix" in output:
config.suffix = output["suffix"]
if "container" in output:
config.container = output["container"]
# Now config contains values from file
print(f"Hardware: {config.hw}")
print(f"CRF: {config.crf}")
Getting Application Directories
Get XDG-compliant directories used by mkv2cast:
from mkv2cast import get_app_dirs
dirs = get_app_dirs()
print(f"Config: {dirs['config']}") # ~/.config/mkv2cast
print(f"State: {dirs['state']}") # ~/.local/state/mkv2cast
print(f"Logs: {dirs['logs']}") # ~/.local/state/mkv2cast/logs
print(f"Cache: {dirs['cache']}") # ~/.cache/mkv2cast
print(f"Tmp: {dirs['tmp']}") # ~/.cache/mkv2cast/tmp
Sending Notifications
Send desktop notifications from your scripts:
from mkv2cast import send_notification
# Send a notification
send_notification(
title="Conversion Complete",
message="Successfully converted 5 files",
urgency="normal" # "low", "normal", "critical"
)
Internationalization
Setup language for messages:
from mkv2cast import setup_i18n, _
# Setup French translations
setup_i18n("fr")
# Use translations
print(_("Conversion complete")) # "Conversion terminée"
Available languages: en, fr, es, it, de.
Complete Example Script
Here’s a complete example that processes files with progress callbacks and parallel processing:
#!/usr/bin/env python3
"""Batch convert MKV files with progress tracking."""
import sys
import threading
from pathlib import Path
from mkv2cast import convert_batch, Config, get_app_dirs
# Thread-safe progress tracking
progress_lock = threading.Lock()
file_progress = {}
def on_progress(filepath: Path, progress: dict):
"""Thread-safe progress callback."""
with progress_lock:
file_progress[filepath.name] = progress
# Print current status
stage = progress.get("stage", "")
percent = progress.get("progress_percent", 0)
if stage == "encoding":
fps = progress.get("fps", 0)
eta = progress.get("eta_seconds", 0)
print(f"\r{filepath.name}: {percent:.1f}% @ {fps:.1f}fps, ETA: {eta:.0f}s", end="")
elif stage in ("done", "skipped", "failed"):
print(f"\n{filepath.name}: {stage.upper()}")
def main():
# Configuration optimized for library usage
config = Config.for_library(
hw="auto",
container="mkv",
crf=20,
preset="slow",
encode_workers=2, # 2 parallel encoders
)
# Input/output directories
input_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
output_dir = input_dir / "converted"
output_dir.mkdir(exist_ok=True)
# Collect files
files = list(input_dir.glob("**/*.mkv"))
if not files:
print("No MKV files found.")
return 0
print(f"Processing {len(files)} files...")
# Process files in parallel
results = convert_batch(
files,
cfg=config,
progress_callback=on_progress,
output_dir=output_dir
)
# Count results
converted = sum(1 for s, o, _ in results.values() if s and o)
skipped = sum(1 for s, o, _ in results.values() if s and not o)
failed = sum(1 for s, _, _ in results.values() if not s)
# Print summary
print(f"\nSummary:")
print(f" Converted: {converted}")
print(f" Skipped: {skipped}")
print(f" Failed: {failed}")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())
Advanced: Async Integration
For asyncio-based applications, you can wrap convert_batch() in an executor:
import asyncio
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from mkv2cast import convert_batch, Config
async def convert_async(files: list[Path], config: Config):
"""Run batch conversion in a thread pool."""
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
result = await loop.run_in_executor(
executor,
lambda: convert_batch(files, cfg=config)
)
return result
# Usage
async def main():
config = Config.for_library(hw="vaapi")
files = [Path("movie1.mkv"), Path("movie2.mkv")]
results = await convert_async(files, config)
print(f"Converted: {sum(1 for s, _, _ in results.values() if s)}")
asyncio.run(main())
Advanced: Webhook Integration
Send conversion events to a webhook:
import requests
from pathlib import Path
from mkv2cast import convert_file, Config
WEBHOOK_URL = "https://your-server.com/api/conversion-events"
def webhook_callback(filepath: Path, progress: dict):
"""Send progress to webhook."""
try:
requests.post(WEBHOOK_URL, json={
"file": str(filepath),
"stage": progress.get("stage"),
"progress": progress.get("progress_percent"),
"eta": progress.get("eta_seconds"),
}, timeout=5)
except Exception:
pass # Don't let webhook errors affect conversion
config = Config.for_library(hw="auto")
convert_file(Path("movie.mkv"), cfg=config, progress_callback=webhook_callback)
JSON Progress Output
For integration with web UIs or monitoring tools, use the JSONProgressOutput class or the --json-progress CLI flag:
CLI Usage:
mkv2cast --json-progress movie.mkv
This outputs JSON events to stdout:
{"version":"1.0","event":"start","overall":{"total_files":1,"backend":"vaapi"}}
{"version":"1.0","event":"file_start","file":"movie.mkv"}
{"version":"1.0","event":"progress","files":{"movie.mkv":{"progress_percent":45.2,"fps":120.5}}}
{"version":"1.0","event":"file_done","file":"movie.mkv","status":"done"}
{"version":"1.0","event":"complete"}
Python Usage:
import json
import subprocess
from typing import Generator, Dict, Any
def stream_progress(filepath: str) -> Generator[Dict[str, Any], None, None]:
"""Stream JSON progress events from mkv2cast."""
proc = subprocess.Popen(
["mkv2cast", "--json-progress", filepath],
stdout=subprocess.PIPE,
text=True
)
for line in proc.stdout:
yield json.loads(line)
# Example: Display progress
for event in stream_progress("movie.mkv"):
if event["event"] == "progress":
for filename, data in event.get("files", {}).items():
percent = data.get("progress_percent", 0)
fps = data.get("fps", 0)
eta = data.get("eta_seconds", 0)
print(f"{filename}: {percent:.1f}% @ {fps:.1f}fps, ETA: {eta:.0f}s")
Using JSONProgressOutput Directly:
from mkv2cast import JSONProgressOutput
import sys
# Create a JSON progress output
json_out = JSONProgressOutput(stream=sys.stdout)
# Signal start
json_out.start(total_files=5, backend="vaapi", encode_workers=1, integrity_workers=2)
# Update file progress
from pathlib import Path
filepath = Path("movie.mkv")
json_out.file_queued(filepath, duration_ms=3600000) # 1 hour
json_out.file_encoding_start(filepath)
json_out.file_progress(
filepath,
frame=1000,
fps=120.5,
time_ms=60000, # 1 minute
bitrate="2500kbits/s",
speed="2.5x"
)
json_out.file_done(filepath, output_path=Path("movie.h264.cast.mkv"))
json_out.complete()
JSON Event Types:
start: Processing started, includes total files and backend infofile_checking: Integrity check started for a filefile_start: Encoding started for a fileprogress: Progress update with percentage, FPS, ETAfile_done: File processing completed (done, skipped, or failed)complete: All processing finished
Progress Data Fields:
{
"filename": "movie.mkv",
"filepath": "/path/to/movie.mkv",
"status": "encoding", # queued, checking, encoding, done, skipped, failed
"progress_percent": 45.2,
"current_time_ms": 1620000,
"duration_ms": 3600000,
"fps": 120.5,
"speed": "2.5x",
"bitrate": "2500kbits/s",
"eta_seconds": 30.5,
"started_at": 1704067200.0,
"finished_at": null,
"output_path": null,
"error": null
}
API Reference
For complete API documentation, see API Reference.