aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LICENSE.md21
-rw-r--r--README.md145
-rw-r--r--install.sh33
-rw-r--r--mediaconv.py598
-rw-r--r--requirements.txt5
5 files changed, 802 insertions, 0 deletions
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..12e957a
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Avitld
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7d14424
--- /dev/null
+++ b/README.md
@@ -0,0 +1,145 @@
+# MediaConv
+
+A simple but powerful Python CLI tool for converting media files using FFmpeg. Supports batch conversion, parallel processing, and configurable settings.
+
+## Features
+
+- Convert between various audio and video formats
+- Batch conversion with parallel processing
+- Configurable quality settings
+- Support for audio-to-video conversion with static images or GIFs
+
+## Installation
+
+### Quick Install
+
+The easiest way to install MediaConv is using the provided installation script:
+
+```bash
+# Make the script executable
+chmod +x install.sh
+
+# Run the installation script
+sudo ./install.sh
+```
+
+This will:
+1. Install required Python packages
+2. Copy the script to `/usr/local/bin`
+3. Make it executable
+4. Check for FFmpeg installation
+
+After installation, you can use `mediaconv` from anywhere in your system.
+
+### Manual Installation
+
+1. Ensure you have Python 3.6+ installed
+2. Install FFmpeg:
+ - Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
+ - Linux (Debian): `sudo apt-get install ffmpeg`
+ - Linux (Arch): `sudo pacman -S ffmpeg`
+ - FreeBSD: `sudo pkg install ffmpeg`
+ - macOS: `brew install ffmpeg`
+3. Install required Python packages:
+ ```bash
+ pip install -r requirements.txt
+ ```
+4. Copy `mediaconv.py` to a directory in your PATH or run it directly
+
+## Usage
+
+### Basic Usage
+
+```bash
+mediaconv input.wav --to mp3
+```
+
+### Audio-to-Video Conversion
+
+```bash
+mediaconv input.mp3 --to mp4 --image cover.jpg
+```
+
+### Batch Conversion
+
+```bash
+mediaconv *.wav --to mp3 --output-dir converted
+```
+
+### Advanced Settings
+
+#### Audio Settings
+- `--audio-bitrate`: Audio bitrate (e.g., 192k)
+- `--sample-rate`: Sample rate (e.g., 44100)
+- `--channels`: Number of channels (1 or 2)
+- `--quality`: Codec-specific quality setting
+ - MP3: 0-9 (lower is better)
+ - OGG: -1 to 10 (higher is better)
+ - WebM/Opus: 0-10 (higher is better)
+
+#### Video Settings
+- `--video-bitrate`: Video bitrate (e.g., 2M)
+- `--crf`: Quality factor
+ - H.264: 0-51 (lower is better)
+ - VP9: 0-63 (lower is better)
+- `--preset`: Encoding preset
+ - H.264: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow
+ - VP9: 0-5 (higher is faster, lower is better quality)
+- `--profile`: H.264 profile (baseline, main, high)
+- `--level`: H.264 level (e.g., 4.1)
+- `--tile-columns`: VP9 tile columns (1-4)
+- `--frame-parallel`: VP9 frame parallel mode (0 or 1)
+
+#### GIF Settings
+- `--duration`: Duration in seconds (max 15)
+
+### Other Options
+
+- `--output-dir`: Specify output directory
+- `--output-name`: Custom output filename (single file only)
+- `--overwrite`: Overwrite existing files
+- `--dry-run`: Show commands without executing
+- `--max-workers`: Maximum parallel conversions (default: 4)
+- `--add-suffix`: Add "_converted" suffix to output files
+- `--test`: Run environment checks
+
+## Examples
+
+### Convert WAV to MP3 with high quality
+```bash
+mediaconv input.wav --to mp3 --bitrate 320k --quality 0
+```
+
+### Convert to WebM with VP9
+```bash
+mediaconv input.mp4 --to webm --crf 30 --tile-columns 2 --frame-parallel 1
+```
+
+### Convert to H.264 with specific profile
+```bash
+mediaconv input.mp4 --to mp4 --crf 23 --profile high --level 4.1
+```
+
+### Convert audio to video with static image
+```bash
+mediaconv input.mp3 --to mp4 --image cover.jpg --crf 23
+```
+
+### Audio Formats
+- MP3
+- WAV
+- FLAC
+- OGG
+- M4A
+
+### Video Formats
+- MP4
+- AVI
+- MKV
+- MOV
+- WEBM
+- GIF
+
+## License
+
+This project uses the MIT license. Read [LICENSE](/LICENSE.md) for more info
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..3d71114
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+if [ "$EUID" -ne 0 ]; then
+ echo "Please run as root"
+ exit 1
+fi
+
+if ! command -v python3 &> /dev/null; then
+ echo "Python 3 is not installed. Please install Python 3 first."
+ exit 1
+fi
+
+if ! command -v pip3 &> /dev/null; then
+ echo "pip3 is not installed. Please install pip3 first."
+ exit 1
+fi
+
+echo "Installing required Python packages..."
+pip3 install -r requirements.txt
+
+echo "Installing mediaconv to /usr/local/bin..."
+cp mediaconv.py /usr/local/bin/mediaconv
+chmod +x /usr/local/bin/mediaconv
+
+if ! command -v ffmpeg &> /dev/null; then
+ echo "FFmpeg is not installed. Please install FFmpeg:"
+ echo " - Debian/Ubuntu: sudo apt-get install ffmpeg"
+ echo " - Arch Linux: sudo pacman -S ffmpeg"
+ echo " - FreeBSD: sudo pkg install ffmpeg"
+ echo " - macOS: brew install ffmpeg"
+fi
+
+echo "Installation complete!" \ No newline at end of file
diff --git a/mediaconv.py b/mediaconv.py
new file mode 100644
index 0000000..dd6954f
--- /dev/null
+++ b/mediaconv.py
@@ -0,0 +1,598 @@
+#!/usr/bin/env python3
+
+import argparse
+import os
+import subprocess
+import sys
+import logging
+import magic
+import concurrent.futures
+from pathlib import Path
+from typing import Optional, Dict, List, Tuple, Set
+from tqdm import tqdm
+from concurrent.futures import ThreadPoolExecutor
+
+def setup_logging(verbose: bool = False) -> logging.Logger:
+ logger = logging.getLogger('mediaconv')
+ logger.setLevel(logging.DEBUG if verbose else logging.INFO)
+
+ handler = logging.StreamHandler()
+ handler.setLevel(logging.DEBUG if verbose else logging.INFO)
+ formatter = logging.Formatter('%(levelname)s: %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+ return logger
+
+class MediaConverter:
+ SUPPORTED_FORMATS = {
+ 'audio': ['mp3', 'wav', 'flac', 'ogg', 'm4a'],
+ 'video': ['mp4', 'avi', 'mkv', 'mov', 'gif', 'webm']
+ }
+
+ DEFAULT_SETTINGS = {
+ 'audio': {
+ 'bitrate': '192k',
+ 'sample_rate': '44100',
+ 'channels': '2'
+ },
+ 'video': {
+ 'bitrate': '192k',
+ 'sample_rate': '44100',
+ 'channels': '2',
+ 'crf': '23',
+ 'preset': 'medium'
+ }
+ }
+
+ def __init__(self):
+ self.logger = setup_logging()
+
+ if not self._check_ffmpeg():
+ raise EnvironmentError("FFmpeg is not installed. Please install FFmpeg first.")
+ if not self._check_magic():
+ raise EnvironmentError("python-magic is not installed. Please install python-magic first.")
+
+ def _check_ffmpeg(self) -> bool:
+ try:
+ subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True)
+ return True
+ except (subprocess.SubprocessError, FileNotFoundError):
+ return False
+
+ def _check_magic(self) -> bool:
+ try:
+ magic.Magic(mime=True)
+ return True
+ except (ImportError, AttributeError):
+ return False
+
+ def _detect_media_type(self, file_path: str) -> Tuple[str, str]:
+ try:
+ mime = magic.Magic(mime=True)
+ file_type = mime.from_file(file_path)
+
+ if file_type.startswith('audio/'):
+ return 'audio', file_path.split('.')[-1].lower()
+ elif file_type.startswith('video/'):
+ return 'video', file_path.split('.')[-1].lower()
+ else:
+ raise ValueError(f"Unsupported file type: {file_type}")
+ except Exception as e:
+ raise ValueError(f"Error detecting file type: {str(e)}")
+
+ def _validate_input_file(self, input_file: str) -> None:
+ if not os.path.exists(input_file):
+ raise FileNotFoundError(f"Input file not found: {input_file}")
+ if not os.access(input_file, os.R_OK):
+ raise PermissionError(f"Cannot read input file: {input_file}")
+
+ def _validate_output_format(self, media_type: str, output_format: str) -> None:
+ output_format = output_format.lower()
+
+ if media_type == 'video' and output_format in self.SUPPORTED_FORMATS['audio']:
+ return
+
+ if output_format not in self.SUPPORTED_FORMATS[media_type]:
+ raise ValueError(f"Unsupported output format for {media_type}: {output_format}")
+
+ def _generate_output_filename(
+ self,
+ input_path: Path,
+ output_format: str,
+ output_name: Optional[str] = None,
+ add_suffix: bool = False,
+ output_dir: Optional[str] = None
+ ) -> Path:
+ if output_name:
+ output_path = Path(output_name).with_suffix(f'.{output_format}')
+ else:
+ stem = input_path.stem
+ if add_suffix:
+ stem = f"{stem}_converted"
+ output_path = input_path.with_name(f"{stem}.{output_format}")
+
+ if output_dir:
+ output_dir_path = Path(output_dir)
+ output_dir_path.mkdir(parents=True, exist_ok=True)
+ return output_dir_path / output_path.name
+
+ return output_path
+
+ def _build_ffmpeg_command(
+ self,
+ input_file: str,
+ output_file: str,
+ settings: Optional[Dict[str, str]] = None,
+ image_file: Optional[str] = None,
+ overwrite: bool = False
+ ) -> List[str]:
+ cmd = ['ffmpeg']
+ if overwrite:
+ cmd.append('-y')
+ output_file_str = str(output_file)
+
+ if image_file and output_file_str.endswith(('.mp4', '.mkv', '.avi', '.mov', '.webm')):
+ is_gif = image_file.lower().endswith('.gif')
+
+ if is_gif:
+ cmd.extend(['-stream_loop', '-1', '-i', image_file])
+ else:
+ cmd.extend(['-loop', '1', '-i', image_file])
+
+ cmd.extend(['-i', input_file])
+ cmd.extend(['-map', '0:v:0', '-map', '1:a:0'])
+ cmd.extend(['-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2'])
+
+ if is_gif:
+ cmd.extend(['-r', '25'])
+
+ if output_file_str.endswith('.webm'):
+ cmd.extend(['-c:v', 'libvpx-vp9'])
+ cmd.extend(['-crf', '30'])
+ cmd.extend(['-b:v', '0'])
+ cmd.extend(['-c:a', 'libopus'])
+ else:
+ cmd.extend(['-c:v', 'libx264', '-tune', 'stillimage'])
+ cmd.extend(['-c:a', 'aac'])
+
+ cmd.extend(['-pix_fmt', 'yuv420p', '-shortest'])
+ else:
+ cmd.extend(['-i', input_file])
+ if output_file_str.endswith(('.mp3', '.wav', '.flac', '.ogg', '.m4a')):
+ cmd.append('-vn')
+
+ if settings:
+ if output_file_str.endswith(('.mp3', '.wav', '.flac', '.ogg', '.m4a', '.webm')):
+ if output_file_str.endswith('.webm'):
+ cmd.extend(['-c:a', 'libopus'])
+ elif output_file_str.endswith('.mp3'):
+ cmd.extend(['-c:a', 'libmp3lame'])
+ elif output_file_str.endswith('.flac'):
+ cmd.extend(['-c:a', 'flac'])
+ elif output_file_str.endswith('.ogg'):
+ cmd.extend(['-c:a', 'libvorbis'])
+ elif output_file_str.endswith('.m4a'):
+ cmd.extend(['-c:a', 'aac'])
+
+ if 'audio_bitrate' in settings:
+ cmd.extend(['-b:a', settings['audio_bitrate']])
+ if 'sample_rate' in settings:
+ cmd.extend(['-ar', settings['sample_rate']])
+ if 'channels' in settings:
+ cmd.extend(['-ac', settings['channels']])
+
+ if output_file_str.endswith('.mp3') and 'quality' in settings:
+ cmd.extend(['-q:a', settings['quality']])
+ elif output_file_str.endswith('.ogg') and 'quality' in settings:
+ cmd.extend(['-q:a', settings['quality']])
+ elif output_file_str.endswith('.webm') and 'quality' in settings:
+ cmd.extend(['-vbr', settings['quality']])
+
+ elif output_file_str.endswith(('.mp4', '.mkv', '.avi', '.mov', '.webm')):
+ if output_file_str.endswith('.webm'):
+ cmd.extend(['-c:v', 'libvpx-vp9'])
+ else:
+ cmd.extend(['-c:v', 'libx264'])
+
+ if 'video_bitrate' in settings:
+ cmd.extend(['-b:v', settings['video_bitrate']])
+ if 'audio_bitrate' in settings:
+ cmd.extend(['-b:a', settings['audio_bitrate']])
+ if 'crf' in settings:
+ if output_file_str.endswith('.webm'):
+ crf = min(63, max(0, int(settings['crf'])))
+ cmd.extend(['-crf', str(crf)])
+ else:
+ crf = min(51, max(0, int(settings['crf'])))
+ cmd.extend(['-crf', str(crf)])
+ if 'preset' in settings:
+ if output_file_str.endswith('.webm'):
+ cmd.extend(['-cpu-used', settings['preset']])
+ else:
+ cmd.extend(['-preset', settings['preset']])
+
+ if output_file_str.endswith('.webm'):
+ if 'tile-columns' in settings:
+ cmd.extend(['-tile-columns', settings['tile-columns']])
+ if 'frame-parallel' in settings:
+ cmd.extend(['-frame-parallel', settings['frame-parallel']])
+ else:
+ if 'profile' in settings:
+ cmd.extend(['-profile:v', settings['profile']])
+ if 'level' in settings:
+ cmd.extend(['-level', settings['level']])
+
+ elif output_file_str.endswith('.gif'):
+ if 'duration' in settings:
+ cmd.extend(['-t', settings['duration']])
+ cmd.extend([
+ '-vf', 'fps=15,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=256[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
+ '-loop', '0'
+ ])
+
+ cmd.append(output_file_str)
+ return cmd
+
+ def convert(
+ self,
+ input_file: str,
+ output_format: str,
+ settings: Optional[Dict[str, str]] = None,
+ image_file: Optional[str] = None,
+ add_suffix: bool = False,
+ dry_run: bool = False,
+ overwrite: bool = False,
+ output_name: Optional[str] = None,
+ output_dir: Optional[str] = None
+ ) -> str:
+ try:
+ if not os.path.exists(input_file):
+ raise FileNotFoundError(f"Input file not found: {input_file}")
+
+ if image_file and not os.path.exists(image_file):
+ raise FileNotFoundError(f"Image file not found: {image_file}")
+
+ if output_format == 'gif':
+ try:
+ probe_cmd = ['ffprobe', '-v', 'error', '-select_streams', 'v', '-show_entries', 'stream=codec_type', '-of', 'default=noprint_wrappers=1:nokey=1', input_file]
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
+ if not result.stdout.strip():
+ raise ValueError(f"Input file '{input_file}' does not contain any video stream")
+ except subprocess.CalledProcessError as e:
+ raise RuntimeError(f"Error checking video stream: {e.stderr}")
+
+ if output_format in ['mp3', 'wav', 'flac', 'ogg', 'm4a']:
+ try:
+ probe_cmd = ['ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=codec_type', '-of', 'default=noprint_wrappers=1:nokey=1', input_file]
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
+ if not result.stdout.strip():
+ raise ValueError(f"Input file '{input_file}' does not contain any audio stream")
+ except subprocess.CalledProcessError as e:
+ raise RuntimeError(f"Error checking audio stream: {e.stderr}")
+
+ output_file = self._generate_output_filename(
+ Path(input_file),
+ output_format,
+ output_name=output_name,
+ add_suffix=add_suffix,
+ output_dir=output_dir
+ )
+
+ if os.path.exists(output_file) and not overwrite and not dry_run:
+ while True:
+ response = input(f"File {output_file} already exists. Overwrite? [y/N]: ").lower()
+ if response in ['y', 'yes']:
+ overwrite = True
+ break
+ elif response in ['n', 'no', '']:
+ raise FileExistsError(f"Output file already exists: {output_file}")
+ else:
+ print("Please answer 'y' or 'n'")
+
+ if overwrite and os.path.exists(output_file):
+ try:
+ os.remove(output_file)
+ except PermissionError:
+ import time
+ time.sleep(1)
+ try:
+ os.remove(output_file)
+ except PermissionError as e:
+ raise RuntimeError(f"Cannot overwrite file {output_file}: {str(e)}")
+
+ cmd = self._build_ffmpeg_command(input_file, output_file, settings, image_file, overwrite)
+
+ if dry_run:
+ print(f"Would execute: {' '.join(cmd)}")
+ return str(output_file)
+
+ try:
+ duration_cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', input_file]
+ duration = float(subprocess.run(duration_cmd, capture_output=True, text=True, check=True).stdout.strip())
+ except (subprocess.CalledProcessError, ValueError):
+ duration = None
+
+ try:
+ if duration is not None:
+ with tqdm(total=100, desc="Converting", unit="%") as pbar:
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ bufsize=1
+ )
+
+ stderr_output = []
+ def read_stderr():
+ for line in process.stderr:
+ stderr_output.append(line)
+ if 'time=' in line:
+ try:
+ time_str = line.split('time=')[1].split()[0]
+ h, m, s = time_str.split(':')
+ time_sec = float(h) * 3600 + float(m) * 60 + float(s)
+ progress = min(100, round((time_sec / duration) * 100))
+ pbar.update(progress - pbar.n)
+ except (ValueError, ZeroDivisionError):
+ continue
+
+ import threading
+ stderr_thread = threading.Thread(target=read_stderr)
+ stderr_thread.daemon = True
+ stderr_thread.start()
+
+ process.wait()
+ stderr_thread.join(timeout=1)
+
+ if process.returncode != 0:
+ error_msg = ''.join(stderr_output)
+ raise RuntimeError(f"Conversion failed: {error_msg}")
+ else:
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
+ if result.stderr:
+ print(f"FFmpeg output: {result.stderr}")
+
+ return str(output_file)
+ except subprocess.CalledProcessError as e:
+ error_msg = e.stderr if e.stderr else str(e)
+ raise RuntimeError(f"Conversion failed: {error_msg}")
+ except Exception as e:
+ self.logger.error("Error converting %s: %s", input_file, str(e))
+ raise
+
+ def convert_batch(
+ self,
+ input_files: List[str],
+ output_format: str,
+ output_dir: Optional[str] = None,
+ settings: Optional[Dict[str, str]] = None,
+ overwrite: bool = False,
+ dry_run: bool = False,
+ max_workers: int = 4,
+ add_suffix: bool = False
+ ) -> Dict[str, List[str]]:
+ self.logger.info("Starting batch conversion of %d files", len(input_files))
+
+ results = {'success': [], 'failed': []}
+
+ if dry_run:
+ for input_file in input_files:
+ try:
+ output_file = self.convert(
+ input_file=input_file,
+ output_format=output_format,
+ settings=settings,
+ add_suffix=add_suffix,
+ dry_run=True,
+ overwrite=overwrite,
+ output_dir=output_dir
+ )
+ results['success'].append(output_file)
+ except Exception as e:
+ self.logger.error("Failed to convert %s: %s", input_file, str(e))
+ results['failed'].append(input_file)
+ return results
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
+ futures = []
+ for input_file in input_files:
+ future = executor.submit(
+ self.convert,
+ input_file=input_file,
+ output_format=output_format,
+ settings=settings,
+ add_suffix=add_suffix,
+ overwrite=overwrite,
+ output_dir=output_dir
+ )
+ futures.append((input_file, future))
+
+ with tqdm(total=len(futures), desc="Converting files", unit="file") as pbar:
+ for input_file, future in futures:
+ try:
+ output_file = future.result()
+ if output_file:
+ results['success'].append(output_file)
+ else:
+ results['failed'].append(input_file)
+ except Exception as e:
+ self.logger.error("Failed to convert %s: %s", input_file, str(e))
+ results['failed'].append(input_file)
+ finally:
+ pbar.update(1)
+
+ success_count = len(results['success'])
+ failed_count = len(results['failed'])
+ self.logger.info("Batch conversion complete: %d succeeded, %d failed",
+ success_count, failed_count)
+
+ if failed_count > 0:
+ self.logger.error("\nFailed conversions:")
+ for failed_file in results['failed']:
+ self.logger.error(" - %s", failed_file)
+
+ return results
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='Convert media files using FFmpeg')
+ parser.add_argument('input_files', nargs='+', help='Input file(s) to convert')
+ parser.add_argument('--to', required=True, help='Output format (e.g., mp3, wav, mp4)')
+ parser.add_argument('--output-dir', help='Output directory')
+ parser.add_argument('--output-name', help='Custom output filename (for single input)')
+
+ parser.add_argument('--audio-bitrate', help='Audio bitrate (e.g., 192k)')
+ parser.add_argument('--video-bitrate', help='Video bitrate (e.g., 2M)')
+ parser.add_argument('--sample-rate', help='Audio sample rate (e.g., 44100)')
+ parser.add_argument('--channels', choices=['1', '2'], help='Number of audio channels')
+ parser.add_argument('--quality', help='Audio quality (0-9 for MP3, 0-10 for OGG)')
+
+ parser.add_argument('--crf', help='Video quality (0-51 for H.264, 0-63 for VP9)')
+ parser.add_argument('--preset', choices=['ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'],
+ help='FFmpeg preset')
+ parser.add_argument('--profile', choices=['baseline', 'main', 'high', 'high10', 'high422', 'high444'],
+ help='H.264 profile')
+ parser.add_argument('--level', help='H.264 level (e.g., 4.0, 4.1)')
+
+ parser.add_argument('--tile-columns', help='WebM tile columns (0-4)')
+ parser.add_argument('--frame-parallel', choices=['0', '1'], help='WebM frame parallel')
+
+ parser.add_argument('--duration', help='Duration in seconds for GIF conversion (max 15)')
+
+ parser.add_argument('--overwrite', action='store_true',
+ help='Overwrite existing output files (default: False)')
+ parser.add_argument('--dry-run', action='store_true',
+ help='Show commands without executing')
+ parser.add_argument('--max-workers', type=int, default=4,
+ help='Maximum number of parallel conversions')
+ parser.add_argument('--add-suffix', action='store_true',
+ help='Add "_converted" suffix to output files')
+ parser.add_argument('--test', action='store_true',
+ help='Run environment checks and exit')
+ parser.add_argument('--image', help='Image file to use when converting audio to video')
+
+ args = parser.parse_args()
+
+ if args.to == 'gif' and args.duration:
+ try:
+ duration = float(args.duration)
+ if duration <= 0 or duration > 15:
+ print("Error: GIF duration must be between 0 and 15 seconds")
+ return 1
+ except ValueError:
+ print("Error: Duration must be a valid number")
+ return 1
+
+ for input_file in args.input_files:
+ if not os.path.exists(input_file):
+ print(f"Error: Input file not found: {input_file}")
+ return 1
+ if not os.access(input_file, os.R_OK):
+ print(f"Error: Cannot read input file: {input_file}")
+ return 1
+
+ if args.output_dir:
+ try:
+ os.makedirs(args.output_dir, exist_ok=True)
+ if not os.access(args.output_dir, os.W_OK):
+ print(f"Error: Cannot write to output directory: {args.output_dir}")
+ return 1
+ except Exception as e:
+ print(f"Error creating output directory: {e}")
+ return 1
+
+ if args.output_name and len(args.input_files) > 1:
+ print("Warning: --output-name is ignored for batch conversion")
+
+ try:
+ converter = MediaConverter()
+
+ if args.test:
+ print("Environment check passed!")
+ return 0
+
+ settings = {}
+ if args.audio_bitrate:
+ settings['audio_bitrate'] = args.audio_bitrate
+ if args.video_bitrate:
+ settings['video_bitrate'] = args.video_bitrate
+ if args.sample_rate:
+ settings['sample_rate'] = args.sample_rate
+ if args.channels:
+ settings['channels'] = args.channels
+ if args.quality:
+ settings['quality'] = args.quality
+
+ if args.crf:
+ settings['crf'] = args.crf
+ if args.preset:
+ settings['preset'] = args.preset
+ if args.profile:
+ settings['profile'] = args.profile
+ if args.level:
+ settings['level'] = args.level
+
+ if args.tile_columns:
+ settings['tile-columns'] = args.tile_columns
+ if args.frame_parallel:
+ settings['frame-parallel'] = args.frame_parallel
+
+ if args.duration and args.to == 'gif':
+ settings['duration'] = args.duration
+
+ if len(args.input_files) == 1:
+ try:
+ output_file = converter.convert(
+ input_file=args.input_files[0],
+ output_format=args.to,
+ settings=settings,
+ image_file=args.image,
+ add_suffix=args.add_suffix,
+ dry_run=args.dry_run,
+ overwrite=args.overwrite,
+ output_name=args.output_name,
+ output_dir=args.output_dir
+ )
+ if output_file:
+ print(f"Successfully converted to: {output_file}")
+ return 0 if output_file else 1
+ except FileExistsError as e:
+ print(f"Error: {e}")
+ print("Use --overwrite to force overwrite existing files")
+ return 1
+ except Exception as e:
+ print(f"Error: {str(e)}")
+ return 1
+ else:
+ results = converter.convert_batch(
+ input_files=args.input_files,
+ output_format=args.to,
+ output_dir=args.output_dir,
+ settings=settings,
+ overwrite=args.overwrite,
+ dry_run=args.dry_run,
+ max_workers=args.max_workers,
+ add_suffix=args.add_suffix
+ )
+
+ success_count = len(results['success'])
+ failed_count = len(results['failed'])
+ print(f"Converted {success_count} out of {len(args.input_files)} files. {failed_count} failed.")
+
+ if failed_count > 0:
+ print("\nFailed conversions:")
+ for failed_file in results['failed']:
+ print(f" - {failed_file}")
+
+ return 0 if success_count == len(args.input_files) else 1
+
+ except EnvironmentError as e:
+ print(f"Environment error: {e}")
+ return 1
+ except Exception as e:
+ print(f"Error: {e}")
+ return 1
+
+if __name__ == '__main__':
+ sys.exit(main()) \ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..b296561
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+pathlib>=1.0.1
+typing-extensions>=4.0.0
+python-magic>=0.4.27
+python-magic-bin>=0.4.14; sys_platform == 'win32'
+tqdm>=4.65.0 \ No newline at end of file