1st
This commit is contained in:
commit
4a2c40da0e
134
.gitignore
vendored
Normal file
134
.gitignore
vendored
Normal file
@ -0,0 +1,134 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Output directories
|
||||
audio_output/
|
||||
video_output/
|
||||
140
README.md
Normal file
140
README.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Audio Extractor
|
||||
|
||||
A Python tool for extracting and managing audio tracks from video files using FFmpeg.
|
||||
|
||||
## Features
|
||||
|
||||
- **Extract Audio**: Extract all audio channels from video files as individual files
|
||||
- **Preserve Quality**: Maintains original bitrate and codec without re-encoding
|
||||
- **Batch Processing**: Process multiple video files from a folder
|
||||
- **Multi-track Support**: Automatically handles videos with multiple audio tracks
|
||||
- **Flexible Output**: Specify custom output folder
|
||||
|
||||
## Future Features
|
||||
|
||||
- **Add Tracks**: Add individual audio files as new tracks to video files
|
||||
- **Track Titles**: Assign custom titles/names to audio tracks
|
||||
- **Batch Operations**: Apply operations to multiple files with matching base names
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6+
|
||||
- FFmpeg installed and accessible in your PATH
|
||||
- FFprobe (usually included with FFmpeg)
|
||||
|
||||
### Install FFmpeg
|
||||
|
||||
**macOS** (using Homebrew):
|
||||
```bash
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
**Ubuntu/Debian**:
|
||||
```bash
|
||||
sudo apt-get install ffmpeg
|
||||
```
|
||||
|
||||
**Windows** (using Chocolatey):
|
||||
```bash
|
||||
choco install ffmpeg
|
||||
```
|
||||
|
||||
Or download from: https://ffmpeg.org/download.html
|
||||
|
||||
## Usage
|
||||
|
||||
### Extract Audio from a Single Video
|
||||
|
||||
```bash
|
||||
python main.py extract "path/to/video.mp4" -o ./audio_output
|
||||
```
|
||||
|
||||
### Extract Audio from All Videos in a Folder
|
||||
|
||||
```bash
|
||||
python main.py extract "./videos_folder" -o ./audio_output
|
||||
```
|
||||
|
||||
### Legacy Command Format
|
||||
|
||||
The tool also supports the original command format:
|
||||
|
||||
```bash
|
||||
python main.py --extract "target" -o output_folder
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Extract from single file:**
|
||||
```bash
|
||||
python main.py extract "movie.mp4" -o ./extracted_audio
|
||||
```
|
||||
|
||||
**Extract from entire folder:**
|
||||
```bash
|
||||
python main.py extract "./my_videos" -o "./audio_tracks"
|
||||
```
|
||||
|
||||
**Extract with default output folder (./audio_output):**
|
||||
```bash
|
||||
python main.py extract "video.mkv"
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Identifies video files** in the target path
|
||||
2. **Analyzes audio streams** using ffprobe to detect codec and bitrate information
|
||||
3. **Extracts each audio track** using FFmpeg's codec copy mode (no re-encoding)
|
||||
4. **Preserves quality** by maintaining original bitrate and codec
|
||||
5. **Names files** appropriately based on source video and track number
|
||||
|
||||
## Output
|
||||
|
||||
Extracted audio files are saved with the following naming:
|
||||
|
||||
- **Single audio track**: `video_name.aac` (or appropriate extension)
|
||||
- **Multiple audio tracks**: `video_name_audio_0.aac`, `video_name_audio_1.aac`, etc.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"ffmpeg is not installed or not found in PATH"**
|
||||
- Ensure FFmpeg is installed and the `ffmpeg` command is accessible from your terminal
|
||||
- Test with: `ffmpeg -version`
|
||||
|
||||
**"No audio streams found"**
|
||||
- The video file may not contain any audio tracks
|
||||
- Try analyzing the file with: `ffprobe "video.mp4"`
|
||||
|
||||
**Extraction fails**
|
||||
- Check that the video file is not corrupted
|
||||
- Try opening it with a media player first
|
||||
- Check disk space in the output folder
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
audio-extractor/
|
||||
├── main.py # Entry point and CLI argument parsing
|
||||
├── audio_extractor/
|
||||
│ ├── __init__.py
|
||||
│ ├── cli.py # CLI interface
|
||||
│ └── extractor.py # Core extraction logic
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Adding Features
|
||||
|
||||
To add new features:
|
||||
|
||||
1. Add command logic to `audio_extractor/extractor.py`
|
||||
2. Add CLI interface to `audio_extractor/cli.py`
|
||||
3. Add new command to the argument parser in `main.py`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
4
audio_extractor/__init__.py
Normal file
4
audio_extractor/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""Audio Extractor - FFmpeg-based audio extraction and management tool"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Audio Extractor Contributors"
|
||||
62
audio_extractor/cli.py
Normal file
62
audio_extractor/cli.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""CLI interface for audio extraction operations"""
|
||||
|
||||
from pathlib import Path
|
||||
from audio_extractor.extractor import AudioExtractor
|
||||
|
||||
|
||||
class AudioExtractorCLI:
|
||||
"""Command-line interface for audio extraction"""
|
||||
|
||||
def __init__(self):
|
||||
self.extractor = AudioExtractor()
|
||||
|
||||
def extract_audio(self, target: str, output: str) -> None:
|
||||
"""
|
||||
Extract audio from video file(s).
|
||||
|
||||
Args:
|
||||
target: Path to video file or folder containing video files
|
||||
output: Output folder path for extracted audio files
|
||||
"""
|
||||
target_path = Path(target)
|
||||
output_path = Path(output)
|
||||
|
||||
if not target_path.exists():
|
||||
raise FileNotFoundError(f"Target not found: {target}")
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if target_path.is_file():
|
||||
# Single file
|
||||
print(f"Extracting audio from: {target_path}")
|
||||
self.extractor.extract_audio_from_file(target_path, output_path)
|
||||
elif target_path.is_dir():
|
||||
# Directory - process all video files
|
||||
video_files = self.extractor.find_video_files(target_path)
|
||||
if not video_files:
|
||||
print(f"No video files found in: {target_path}")
|
||||
return
|
||||
|
||||
print(f"Found {len(video_files)} video file(s)")
|
||||
for i, video_file in enumerate(video_files, 1):
|
||||
print(f"[{i}/{len(video_files)}] Extracting audio from: {video_file.name}")
|
||||
try:
|
||||
self.extractor.extract_audio_from_file(video_file, output_path)
|
||||
except Exception as e:
|
||||
print(f" Error processing {video_file.name}: {e}")
|
||||
else:
|
||||
raise ValueError(f"Invalid target: {target}")
|
||||
|
||||
def add_audio_tracks(self, target: str, input_folder: str, output: str, title: str = None) -> None:
|
||||
"""
|
||||
Add audio tracks to video files (future feature).
|
||||
|
||||
Args:
|
||||
target: Path to folder containing audio files
|
||||
input_folder: Path to folder containing video files
|
||||
output: Output folder for processed video files
|
||||
title: Title/name for the added audio tracks
|
||||
"""
|
||||
print("Feature not yet implemented")
|
||||
# TODO: Implement add_audio_tracks functionality
|
||||
180
audio_extractor/extractor.py
Normal file
180
audio_extractor/extractor.py
Normal file
@ -0,0 +1,180 @@
|
||||
"""Core audio extraction logic using ffmpeg"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class AudioExtractor:
|
||||
"""Handles audio extraction from video files using ffmpeg"""
|
||||
|
||||
# Common video file extensions
|
||||
VIDEO_EXTENSIONS = {
|
||||
".mp4", ".mkv", ".mov", ".avi", ".flv", ".wmv", ".webm",
|
||||
".m4v", ".mpg", ".mpeg", ".3gp", ".ts", ".m2ts", ".mts"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._verify_ffmpeg_installed()
|
||||
|
||||
def _verify_ffmpeg_installed(self) -> None:
|
||||
"""Verify that ffmpeg is installed and accessible"""
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-version"],
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
raise RuntimeError(
|
||||
"ffmpeg is not installed or not found in PATH. "
|
||||
"Please install ffmpeg to use this tool."
|
||||
)
|
||||
|
||||
def find_video_files(self, folder: Path) -> List[Path]:
|
||||
"""
|
||||
Find all video files in a folder.
|
||||
|
||||
Args:
|
||||
folder: Path to folder to search
|
||||
|
||||
Returns:
|
||||
List of video file paths
|
||||
"""
|
||||
video_files = []
|
||||
for ext in self.VIDEO_EXTENSIONS:
|
||||
video_files.extend(folder.glob(f"*{ext}"))
|
||||
video_files.extend(folder.glob(f"*{ext.upper()}"))
|
||||
return sorted(set(video_files)) # Remove duplicates and sort
|
||||
|
||||
def get_stream_info(self, video_file: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Get stream information from video file using ffprobe.
|
||||
|
||||
Args:
|
||||
video_file: Path to video file
|
||||
|
||||
Returns:
|
||||
Dictionary containing stream information
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffprobe", "-v", "error",
|
||||
"-show_entries", "stream=index,codec_type,codec_name",
|
||||
"-of", "json",
|
||||
str(video_file)
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"Failed to get stream info: {e.stderr}")
|
||||
except json.JSONDecodeError:
|
||||
raise RuntimeError("Failed to parse ffprobe output")
|
||||
|
||||
def extract_audio_from_file(self, video_file: Path, output_folder: Path) -> None:
|
||||
"""
|
||||
Extract all audio tracks from a video file.
|
||||
|
||||
Args:
|
||||
video_file: Path to video file
|
||||
output_folder: Path to output folder
|
||||
"""
|
||||
if not video_file.exists():
|
||||
raise FileNotFoundError(f"Video file not found: {video_file}")
|
||||
|
||||
# Get stream information
|
||||
try:
|
||||
stream_info = self.get_stream_info(video_file)
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError(f"Could not analyze {video_file.name}: {e}")
|
||||
|
||||
# Find audio streams
|
||||
audio_streams = [
|
||||
stream for stream in stream_info.get("streams", [])
|
||||
if stream.get("codec_type") == "audio"
|
||||
]
|
||||
|
||||
if not audio_streams:
|
||||
print(f" No audio streams found in {video_file.name}")
|
||||
return
|
||||
|
||||
# Extract each audio stream
|
||||
file_stem = video_file.stem
|
||||
for stream in audio_streams:
|
||||
stream_index = stream.get("index")
|
||||
codec_name = stream.get("codec_name", "aac")
|
||||
|
||||
# Determine output file extension based on codec
|
||||
output_ext = self._get_audio_extension(codec_name)
|
||||
|
||||
# Handle multiple audio tracks
|
||||
if len(audio_streams) > 1:
|
||||
output_filename = f"{file_stem}_audio_{stream_index}.{output_ext}"
|
||||
else:
|
||||
output_filename = f"{file_stem}.{output_ext}"
|
||||
|
||||
output_path = output_folder / output_filename
|
||||
|
||||
self._extract_stream(video_file, output_path, stream_index)
|
||||
|
||||
def _get_audio_extension(self, codec_name: str) -> str:
|
||||
"""
|
||||
Get file extension based on audio codec.
|
||||
|
||||
Args:
|
||||
codec_name: FFmpeg codec name
|
||||
|
||||
Returns:
|
||||
File extension (without dot)
|
||||
"""
|
||||
extension_map = {
|
||||
"aac": "aac",
|
||||
"mp3": "mp3",
|
||||
"libmp3lame": "mp3",
|
||||
"flac": "flac",
|
||||
"opus": "opus",
|
||||
"vorbis": "ogg",
|
||||
"libvorbis": "ogg",
|
||||
"ac3": "ac3",
|
||||
"eac3": "ec3",
|
||||
"dts": "dts",
|
||||
"truehd": "thd",
|
||||
"alac": "m4a",
|
||||
"pcm_s16le": "wav",
|
||||
"pcm_s24le": "wav",
|
||||
"pcm_s32le": "wav",
|
||||
}
|
||||
return extension_map.get(codec_name, "aac")
|
||||
|
||||
def _extract_stream(self, video_file: Path, output_path: Path, stream_index: int) -> None:
|
||||
"""
|
||||
Extract a single audio stream using ffmpeg.
|
||||
|
||||
Args:
|
||||
video_file: Path to input video file
|
||||
output_path: Path to output audio file
|
||||
stream_index: Index of the audio stream to extract
|
||||
"""
|
||||
try:
|
||||
# Use ffmpeg to copy the audio codec without re-encoding
|
||||
# This preserves the original bitrate and codec
|
||||
cmd = [
|
||||
"ffmpeg", "-i", str(video_file),
|
||||
"-map", f"0:a:{stream_index}",
|
||||
"-c", "copy", # Copy codec without re-encoding
|
||||
"-y", # Overwrite output file
|
||||
str(output_path)
|
||||
]
|
||||
|
||||
subprocess.run(cmd, capture_output=True, check=True)
|
||||
print(f" ✓ Extracted: {output_path.name}")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to extract audio stream {stream_index}: {e.stderr.decode() if e.stderr else 'Unknown error'}"
|
||||
)
|
||||
117
main.py
Normal file
117
main.py
Normal file
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Audio Extractor - Extract and manage audio tracks from video files using ffmpeg
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from audio_extractor.cli import AudioExtractorCLI
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract and manage audio tracks from video files",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Extract audio from a single video file
|
||||
python main.py --extract "video.mp4" -o ./audio_output
|
||||
|
||||
# Extract audio from all videos in a folder
|
||||
python main.py --extract "./videos" -o ./audio_output
|
||||
|
||||
# Add audio tracks to video (future feature)
|
||||
python main.py --add "./audio_files" -i "./video_files" -o ./output
|
||||
"""
|
||||
)
|
||||
|
||||
# Create subcommands for better organization
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
||||
|
||||
# Extract command
|
||||
extract_parser = subparsers.add_parser("extract", help="Extract audio from video files")
|
||||
extract_parser.add_argument(
|
||||
"target",
|
||||
type=str,
|
||||
help="Path to video file or folder containing video files"
|
||||
)
|
||||
extract_parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=str,
|
||||
default="./audio_output",
|
||||
help="Output folder for extracted audio files (default: ./audio_output)"
|
||||
)
|
||||
|
||||
# Add command (future feature)
|
||||
add_parser = subparsers.add_parser("add", help="Add audio tracks to video files")
|
||||
add_parser.add_argument(
|
||||
"target",
|
||||
type=str,
|
||||
help="Path to folder containing audio files to add"
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"-i", "--input",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to folder containing video files"
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=str,
|
||||
default="./video_output",
|
||||
help="Output folder for processed video files (default: ./video_output)"
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"--title",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Title/name for the added audio tracks"
|
||||
)
|
||||
|
||||
# Also support old-style --extract flag for backwards compatibility
|
||||
parser.add_argument(
|
||||
"--extract",
|
||||
type=str,
|
||||
default=None,
|
||||
metavar="TARGET",
|
||||
help="(Legacy) Extract audio from video file or folder"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=str,
|
||||
dest="output",
|
||||
default="./audio_output",
|
||||
help="Output folder path"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Initialize CLI
|
||||
cli = AudioExtractorCLI()
|
||||
|
||||
# Handle legacy --extract flag
|
||||
if args.extract:
|
||||
args.command = "extract"
|
||||
args.target = args.extract
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if args.command == "extract":
|
||||
cli.extract_audio(args.target, args.output)
|
||||
elif args.command == "add":
|
||||
cli.add_audio_tracks(args.target, args.input, args.output, args.title)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
# Audio Extractor Requirements
|
||||
# No external Python dependencies required - uses ffmpeg system command
|
||||
# ffmpeg must be installed separately on your system
|
||||
|
||||
# For development/testing (optional):
|
||||
# pytest>=7.0
|
||||
# black>=22.0
|
||||
# pylint>=2.0
|
||||
Loading…
x
Reference in New Issue
Block a user