2026-01-10 12:53:01 -05:00

156 lines
6.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
AV1 Batch Video Transcoder
Main entry point for batch video encoding with intelligent audio and resolution handling.
"""
import argparse
import csv
from pathlib import Path
from core.config_helper import load_config_xml
from core.logger_helper import setup_logger
from core.process_manager import process_folder
# =============================
# PATH NORMALIZATION
# =============================
def normalize_input_path(input_path: str, path_mappings: dict) -> Path:
"""
Normalize input path from various formats to Windows path.
Supports:
- Windows paths: "P:\\tv\\show" or "P:/tv/show"
- Linux paths: "/mnt/plex/tv/show" (maps to Windows equivalent if mapping exists)
- Mixed separators: "P:/tv\\show"
Args:
input_path: Path string from user input
path_mappings: Dict mapping Windows paths to Linux paths from config
Returns:
Path object pointing to the actual local folder
"""
# First, try to map Linux paths to Windows paths (reverse mapping)
# If user provides "/mnt/plex/tv", find the mapping and convert to "P:\\tv"
if isinstance(path_mappings, list):
# New format: list of dicts
for mapping in path_mappings:
if isinstance(mapping, dict):
win_path = mapping.get("from")
linux_path = mapping.get("to")
if linux_path and input_path.lower().startswith(linux_path.lower()):
# Found a matching Linux path, convert to Windows
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
result = Path(win_path) / relative if relative else Path(win_path)
logger.info(f"Path mapping: {input_path} -> {result}")
print(f" Mapped Linux path {input_path} to {result}")
return result
else:
# Old format: dict (for backwards compatibility)
for win_path, linux_path in path_mappings.items():
if input_path.lower().startswith(linux_path.lower()):
# Found a matching Linux path, convert to Windows
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
result = Path(win_path) / relative if relative else Path(win_path)
logger.info(f"Path mapping: {input_path} -> {result}")
print(f" Mapped Linux path {input_path} to {result}")
return result
# No mapping found, use path as-is (normalize separators to Windows)
# Convert forward slashes to backslashes for Windows
normalized = input_path.replace("/", "\\")
result = Path(normalized)
logger.info(f"Using path as-is: {result}")
return result
# =============================
# Setup
# =============================
LOG_FOLDER = Path(__file__).parent / "logs"
logger = setup_logger(LOG_FOLDER)
TRACKER_FILE = Path(__file__).parent / "conversion_tracker.csv"
if not TRACKER_FILE.exists():
with open(TRACKER_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
"type", "show", "filename", "original_size_MB", "processed_size_MB", "percentage",
"source_resolution", "target_resolution", "audio_streams", "cq_value", "method"
])
# =============================
# MAIN
# =============================
def main():
parser = argparse.ArgumentParser(
description="Batch AV1 encode videos with intelligent audio and resolution handling",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s "C:\\Videos\\Movies" # CQ mode (preserve resolution, 4K->1080p)
%(prog)s "C:\\Videos\\TV" --r 720 --m bitrate # Force 720p, bitrate mode
%(prog)s "C:\\Videos\\Anime" --cq 28 --r 1080 # Force 1080p, CQ=28
%(prog)s "C:\\Videos\\Low-Res" --m compression --r 480 # Force 480p, try CQ then bitrate for compression
"""
)
parser.add_argument("folder", help="Input folder containing video files")
parser.add_argument("--cq", type=int, help="Override default CQ value")
parser.add_argument(
"--m", "--mode", dest="transcode_mode", default="cq",
choices=["cq", "bitrate", "compression"],
help="Encode mode: 'cq' (constant quality only), 'bitrate' (bitrate only), or 'compression' (try CQ then bitrate fallback)"
)
parser.add_argument(
"--encoder", dest="encoder", default="hevc",
choices=["hevc", "av1"],
help="Video encoder: 'hevc' for HEVC NVENC 10-bit (default), 'av1' for AV1 NVENC 8-bit"
)
parser.add_argument(
"--r", "--resolution", dest="resolution", default=None,
choices=["480", "720", "1080"],
help="Force target resolution (if not specified: 4K->1080p, else preserve)"
)
parser.add_argument(
"--test", dest="test_mode", default=False, action="store_true",
help="Test mode: encode only first file, show ratio, don't move or delete (default: False)"
)
parser.add_argument(
"--language", dest="audio_language", default=None,
help="Tag audio streams with language code (e.g., eng, spa, fra). If not set, audio language is unchanged"
)
parser.add_argument(
"--filter-audio", dest="filter_audio", default=None, action="store_true",
help="Interactive audio selection: show audio streams and let user choose which to keep (overrides config setting)"
)
parser.add_argument(
"--audio-select", dest="audio_select", default=None,
help="Pre-select audio streams to keep (comma-separated, e.g., 1,2). Skips interactive prompt. Requires --filter-audio"
)
parser.add_argument(
"--strip-all-titles", dest="strip_all_titles", default=False, action="store_true",
help="Strip title metadata from all audio tracks (default: False)"
)
args = parser.parse_args()
# Load configuration
config_path = Path(__file__).parent / "config.xml"
config = load_config_xml(config_path)
# Normalize input path (handle Linux paths, mixed separators, etc.)
folder = normalize_input_path(args.folder, config.get("path_mappings", {}))
# Verify folder exists
if not folder.exists():
print(f"❌ Folder not found: {folder}")
logger.error(f"Folder not found: {folder}")
return
# Process folder
process_folder(folder, args.cq, args.transcode_mode, args.resolution, config, TRACKER_FILE, args.test_mode, args.audio_language, args.filter_audio, args.audio_select, args.encoder, args.strip_all_titles)
if __name__ == "__main__":
main()