Files
2025-11-30 08:48:52 +08:00

312 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""
Unified media conversion tool for video, audio, and images.
Auto-detects format and applies appropriate tool (FFmpeg or ImageMagick).
Supports quality presets, batch processing, and dry-run mode.
"""
import argparse
import subprocess
import sys
from pathlib import Path
from typing import List, Optional, Tuple
# Format mappings
VIDEO_FORMATS = {'.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.m4v'}
AUDIO_FORMATS = {'.mp3', '.aac', '.m4a', '.opus', '.flac', '.wav', '.ogg'}
IMAGE_FORMATS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'}
# Quality presets
QUALITY_PRESETS = {
'web': {
'video_crf': 23,
'video_preset': 'medium',
'audio_bitrate': '128k',
'image_quality': 85
},
'archive': {
'video_crf': 18,
'video_preset': 'slow',
'audio_bitrate': '192k',
'image_quality': 95
},
'mobile': {
'video_crf': 26,
'video_preset': 'fast',
'audio_bitrate': '96k',
'image_quality': 80
}
}
def check_dependencies() -> Tuple[bool, bool]:
"""Check if ffmpeg and imagemagick are available."""
ffmpeg_available = subprocess.run(
['ffmpeg', '-version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
).returncode == 0
magick_available = subprocess.run(
['magick', '-version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
).returncode == 0
return ffmpeg_available, magick_available
def detect_media_type(file_path: Path) -> str:
"""Detect media type from file extension."""
ext = file_path.suffix.lower()
if ext in VIDEO_FORMATS:
return 'video'
elif ext in AUDIO_FORMATS:
return 'audio'
elif ext in IMAGE_FORMATS:
return 'image'
else:
return 'unknown'
def build_video_command(
input_path: Path,
output_path: Path,
preset: str = 'web'
) -> List[str]:
"""Build FFmpeg command for video conversion."""
quality = QUALITY_PRESETS[preset]
return [
'ffmpeg', '-i', str(input_path),
'-c:v', 'libx264',
'-preset', quality['video_preset'],
'-crf', str(quality['video_crf']),
'-c:a', 'aac',
'-b:a', quality['audio_bitrate'],
'-movflags', '+faststart',
'-y',
str(output_path)
]
def build_audio_command(
input_path: Path,
output_path: Path,
preset: str = 'web'
) -> List[str]:
"""Build FFmpeg command for audio conversion."""
quality = QUALITY_PRESETS[preset]
output_ext = output_path.suffix.lower()
codec_map = {
'.mp3': 'libmp3lame',
'.aac': 'aac',
'.m4a': 'aac',
'.opus': 'libopus',
'.flac': 'flac',
'.wav': 'pcm_s16le',
'.ogg': 'libvorbis'
}
codec = codec_map.get(output_ext, 'aac')
cmd = ['ffmpeg', '-i', str(input_path), '-c:a', codec]
# Add bitrate for lossy codecs
if codec not in ['flac', 'pcm_s16le']:
cmd.extend(['-b:a', quality['audio_bitrate']])
cmd.extend(['-y', str(output_path)])
return cmd
def build_image_command(
input_path: Path,
output_path: Path,
preset: str = 'web'
) -> List[str]:
"""Build ImageMagick command for image conversion."""
quality = QUALITY_PRESETS[preset]
return [
'magick', str(input_path),
'-quality', str(quality['image_quality']),
'-strip',
str(output_path)
]
def convert_file(
input_path: Path,
output_path: Path,
preset: str = 'web',
dry_run: bool = False,
verbose: bool = False
) -> bool:
"""Convert a single media file."""
media_type = detect_media_type(input_path)
if media_type == 'unknown':
print(f"Error: Unsupported format for {input_path}", file=sys.stderr)
return False
# Ensure output directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
# Build command based on media type
if media_type == 'video':
cmd = build_video_command(input_path, output_path, preset)
elif media_type == 'audio':
cmd = build_audio_command(input_path, output_path, preset)
else: # image
cmd = build_image_command(input_path, output_path, preset)
if verbose or dry_run:
print(f"Command: {' '.join(cmd)}")
if dry_run:
return True
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE if not verbose else None,
stderr=subprocess.PIPE if not verbose else None,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"Error converting {input_path}: {e}", file=sys.stderr)
if not verbose and e.stderr:
print(e.stderr.decode(), file=sys.stderr)
return False
except Exception as e:
print(f"Error converting {input_path}: {e}", file=sys.stderr)
return False
def batch_convert(
input_paths: List[Path],
output_dir: Optional[Path] = None,
output_format: Optional[str] = None,
preset: str = 'web',
dry_run: bool = False,
verbose: bool = False
) -> Tuple[int, int]:
"""Convert multiple files."""
success_count = 0
fail_count = 0
for input_path in input_paths:
if not input_path.exists():
print(f"Error: {input_path} not found", file=sys.stderr)
fail_count += 1
continue
# Determine output path
if output_dir:
output_name = input_path.stem
if output_format:
output_path = output_dir / f"{output_name}.{output_format.lstrip('.')}"
else:
output_path = output_dir / input_path.name
else:
if output_format:
output_path = input_path.with_suffix(f".{output_format.lstrip('.')}")
else:
print(f"Error: No output format specified for {input_path}", file=sys.stderr)
fail_count += 1
continue
print(f"Converting {input_path.name} -> {output_path.name}")
if convert_file(input_path, output_path, preset, dry_run, verbose):
success_count += 1
else:
fail_count += 1
return success_count, fail_count
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Unified media conversion tool for video, audio, and images.'
)
parser.add_argument(
'inputs',
nargs='+',
type=Path,
help='Input file(s) to convert'
)
parser.add_argument(
'-o', '--output',
type=Path,
help='Output file or directory for batch conversion'
)
parser.add_argument(
'-f', '--format',
help='Output format (e.g., mp4, jpg, mp3)'
)
parser.add_argument(
'-p', '--preset',
choices=['web', 'archive', 'mobile'],
default='web',
help='Quality preset (default: web)'
)
parser.add_argument(
'-n', '--dry-run',
action='store_true',
help='Show commands without executing'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Verbose output'
)
args = parser.parse_args()
# Check dependencies
ffmpeg_ok, magick_ok = check_dependencies()
if not ffmpeg_ok and not magick_ok:
print("Error: Neither ffmpeg nor imagemagick found", file=sys.stderr)
sys.exit(1)
# Handle single file vs batch conversion
if len(args.inputs) == 1 and args.output and not args.output.is_dir():
# Single file conversion
success = convert_file(
args.inputs[0],
args.output,
args.preset,
args.dry_run,
args.verbose
)
sys.exit(0 if success else 1)
else:
# Batch conversion
output_dir = args.output if args.output else Path.cwd()
if not args.output:
output_dir = None # Will convert in place with new format
success, fail = batch_convert(
args.inputs,
output_dir,
args.format,
args.preset,
args.dry_run,
args.verbose
)
print(f"\nResults: {success} succeeded, {fail} failed")
sys.exit(0 if fail == 0 else 1)
if __name__ == '__main__':
main()