#!/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 import shlex 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, get_default_cq # ============================= # 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 # ============================= # BATCH PROCESSING # ============================= def parse_batch_file(file_path: Path) -> list: """ Parse batch file (.txt or .csv) and extract paths with optional per-row parameters. Formats: - Simple list (.txt): One path per line, optional space-separated parameters Example: P:\movies\Movie1 --r 720 --cq 28 - CSV (.csv): First column is path, remaining columns are optional parameters Example: "P:\movies\Movie1","--r 720","--cq 28" Args: file_path: Path to batch file Returns: List of tuples: [(path_str, params_str), ...] """ batch_items = [] if file_path.suffix.lower() == ".csv": # CSV format with open(file_path, "r", encoding="utf-8") as f: reader = csv.reader(f) for row_idx, row in enumerate(reader): if not row or not row[0].strip(): continue # Skip empty rows path_str = row[0].strip() # Skip header row (if it starts with "path") if row_idx == 0 and path_str.lower().startswith("path"): continue # Combine remaining columns as parameters params = " ".join(col.strip() for col in row[1:] if col.strip()) batch_items.append((path_str, params)) else: # Simple list format (.txt or others) with open(file_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() # Skip empty lines and comments if not line or line.startswith("#"): continue # Split path from parameters # Handle quoted paths like: "C:\path with spaces" --r 720 if line.startswith('"'): # Find closing quote end_quote = line.find('"', 1) if end_quote != -1: path_str = line[1:end_quote] params = line[end_quote+1:].strip() else: path_str = line params = "" else: # Unquoted path, split on first space parts = line.split(None, 1) path_str = parts[0] params = parts[1] if len(parts) > 1 else "" batch_items.append((path_str, params)) return batch_items def merge_batch_args(base_args, batch_params_str: str) -> argparse.Namespace: """ Merge batch file row parameters with base CLI parameters. Row parameters override base parameters for that specific path. Args: base_args: Base argparse.Namespace from CLI batch_params_str: Parameter string from batch file row Returns: New argparse.Namespace with merged parameters """ # Create a copy of base args merged = argparse.Namespace(**vars(base_args)) if not batch_params_str: return merged # Parse row parameters as if they were CLI arguments try: param_list = shlex.split(batch_params_str) except ValueError: # If shlex fails, try simple split param_list = batch_params_str.split() # Build a parser just for these arguments (reuse the same one) # We'll manually apply known arguments i = 0 while i < len(param_list): arg = param_list[i] if arg == "--cq": if i + 1 < len(param_list): merged.cq = int(param_list[i + 1]) i += 2 else: i += 1 elif arg in ["--m", "--mode"]: if i + 1 < len(param_list): merged.transcode_mode = param_list[i + 1] i += 2 else: i += 1 elif arg == "--encoder": if i + 1 < len(param_list): merged.encoder = param_list[i + 1] i += 2 else: i += 1 elif arg in ["--r", "--resolution"]: if i + 1 < len(param_list): merged.resolution = param_list[i + 1] i += 2 else: i += 1 elif arg == "--test": merged.test_mode = True i += 1 elif arg == "--language": if i + 1 < len(param_list): merged.audio_language = param_list[i + 1] i += 2 else: i += 1 elif arg == "--filter-audio": merged.filter_audio = True i += 1 elif arg == "--audio-select": if i + 1 < len(param_list): merged.audio_select = param_list[i + 1] i += 2 else: i += 1 elif arg == "--audio-titles": if i + 1 < len(param_list): merged.audio_titles = param_list[i + 1] i += 2 else: i += 1 elif arg == "--audio-channels": if i + 1 < len(param_list): merged.audio_channels = param_list[i + 1] i += 2 else: i += 1 elif arg == "--keep-all-titles": merged.strip_all_titles = False i += 1 elif arg == "--strip-all-titles": merged.strip_all_titles = True i += 1 elif arg == "--unforce-subs": merged.unforce_subs = True i += 1 elif arg == "--no-encode": merged.no_encode = True i += 1 elif arg == "--force-process": merged.force_process = True i += 1 elif arg == "--replace": merged.replace_file = True i += 1 else: i += 1 return merged def parse_audio_dict(audio_str: str, dict_type: str) -> dict: """ Parse audio titles or channels string into dictionary. Args: audio_str: String like "0:English,1:Commentary" or "0:2,1:6" dict_type: "titles" or "channels" Returns: Dictionary mapping stream indices to values, or empty dict on error """ if not audio_str: return {} result = {} try: pairs = audio_str.split(",") for pair in pairs: stream_idx, value = pair.split(":") stream_idx = int(stream_idx.strip()) if dict_type == "channels": # Validate channels: only 2 or 6 allowed channels = int(value.strip()) if channels not in (2, 6): logger.warning(f"Invalid channel count: {channels}. Only 2 or 6 allowed. Skipping.") continue result[stream_idx] = channels else: # titles result[stream_idx] = value.strip() except (ValueError, IndexError) as e: logger.warning(f"Error parsing audio {dict_type}: {e}") 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" # Smart 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" --r 480 # Force 480p for low-res content """ ) parser.add_argument("folder", nargs="?", default=None, help="Input folder containing video files (required unless using --paths-file)") parser.add_argument("--cq", type=int, help="Override default CQ value") parser.add_argument( "--m", "--mode", dest="transcode_mode", default="cq", choices=["cq", "bitrate"], help="Encode mode: CQ (constant quality) or Bitrate mode" ) 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. Auto-selected based on source bit depth if not specified" ) parser.add_argument( "--r", "--resolution", dest="resolution", default=None, choices=["480", "720", "1080"], help="Target resolution (acts as max, downscales if source is larger). If not specified: 4K→1080p, else preserve source" ) 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" ) parser.add_argument( "--audio-titles", dest="audio_titles", default=None, help="Set custom titles for audio streams (e.g., '0:English,1:Commentary'). Format: stream_index:title,stream_index:title" ) parser.add_argument( "--audio-channels", dest="audio_channels", default=None, help="Set channel count for specific audio streams (e.g., '0:2,1:6'). Format: stream_index:channels,stream_index:channels. Only 2 or 6 channels allowed" ) parser.add_argument( "--keep-all-titles", dest="strip_all_titles", default=True, action="store_false", help="Preserve title metadata on audio tracks (default: titles are stripped)" ) parser.add_argument( "--unforce-subs", dest="unforce_subs", default=False, action="store_true", help="Remove forced flag from all subtitle tracks" ) parser.add_argument( "--no-encode", dest="no_encode", default=False, action="store_true", help="Skip encoding: copy video/audio streams as-is. Useful with --unforce-subs to only re-mux subtitles" ) parser.add_argument( "--force-process", dest="force_process", default=False, action="store_true", help="Process files even if they contain ignore tags (e.g., already encoded files with suffix)" ) parser.add_argument( "--replace", dest="replace_file", default=False, action="store_true", help="Replace original file instead of creating suffix version. Requires --no-encode" ) parser.add_argument( "--wait", "-w", dest="wait_seconds", type=int, nargs='?', const=-1, default=None, help="Wait after each file (default: 30s with --no-encode, 0s otherwise). Gives Plex time to detect changes" ) parser.add_argument( "--travel", dest="travel_mode", default=False, action="store_true", help="Travel mode: force 720p resolution and CQ+2, requires --output flag" ) parser.add_argument( "--output", dest="output_folder", default=None, help="Output folder for travel mode (creates subfolder based on input folder name)" ) parser.add_argument( "--paths-file", dest="paths_file", default=None, help="Batch mode: Read paths from file (.txt or .csv). One path per line with optional per-row parameters" ) args = parser.parse_args() # Load configuration config_path = Path(__file__).parent / "config.xml" config = load_config_xml(config_path) # ============================= # BATCH MODE # ============================= if args.paths_file: paths_file = Path(args.paths_file) if not paths_file.exists(): print(f"❌ Paths file not found: {paths_file}") logger.error(f"Paths file not found: {paths_file}") return # Track what we've already processed processed_items = set() batch_queue = [] completed = 0 failed = 0 total_attempted = 0 print("=" * 80) print(f"BATCH MODE: Processing paths from {paths_file.name}") print(f"ℹ️ File will be rechecked after each item for new additions") print("=" * 80) logger.info(f"BATCH MODE: Starting batch processing from {paths_file}") logger.info("File monitoring enabled - will check for new additions after each item") # Initial load batch_items = parse_batch_file(paths_file) if not batch_items: print(f"❌ No valid paths found in {paths_file}") logger.error(f"No valid paths in batch file: {paths_file}") return print(f"📋 Found {len(batch_items)} initial path(s)\n") # Create item signatures for tracking (path + params combo) for path_str, params_str in batch_items: item_sig = f"{path_str}|{params_str}" if item_sig not in processed_items: batch_queue.append((path_str, params_str)) batch_num = 1 # Process batch items with recheck after each while batch_queue: path_str, params_str = batch_queue.pop(0) item_sig = f"{path_str}|{params_str}" total_attempted += 1 print() print("-" * 80) print(f"BATCH [{batch_num}]: {path_str}") if params_str: print(f"Parameters: {params_str}") print("-" * 80) logger.info(f"[BATCH {batch_num}] Processing: {path_str}") try: # Normalize path folder = normalize_input_path(path_str, config.get("path_mappings", {})) # Check if folder exists if not folder.exists(): print(f"❌ [BATCH {batch_num}] Folder not found: {folder}") logger.error(f"[BATCH {batch_num}] Folder not found: {folder}") processed_items.add(item_sig) failed += 1 batch_num += 1 # Recheck file for new items print(f"\n🔄 Rechecking {paths_file.name} for new additions...") new_batch_items = parse_batch_file(paths_file) for path, params in new_batch_items: sig = f"{path}|{params}" if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]: batch_queue.append((path, params)) print(f" ✨ New item found: {path}") continue # Merge batch parameters with base CLI parameters merged_args = merge_batch_args(args, params_str) # Handle travel mode travel_output_folder = None if merged_args.travel_mode: if not merged_args.output_folder: print(f"❌ [BATCH {batch_num}] --travel requires --output folder") logger.error(f"[BATCH {batch_num}] --travel requires --output folder") processed_items.add(item_sig) failed += 1 batch_num += 1 # Recheck file for new items print(f"\n🔄 Rechecking {paths_file.name} for new additions...") new_batch_items = parse_batch_file(paths_file) for path, params in new_batch_items: sig = f"{path}|{params}" if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]: batch_queue.append((path, params)) print(f" ✨ New item found: {path}") continue output_base = Path(merged_args.output_folder) input_folder_name = folder.name travel_output_folder = output_base / input_folder_name travel_output_folder.mkdir(parents=True, exist_ok=True) merged_args.resolution = "720" default_cq = get_default_cq(folder, config, "720", merged_args.encoder) merged_args.cq = default_cq + 2 # Validate --replace requires --no-encode if merged_args.replace_file and not merged_args.no_encode: print(f"❌ [BATCH {batch_num}] --replace requires --no-encode") logger.error(f"[BATCH {batch_num}] --replace requires --no-encode") processed_items.add(item_sig) failed += 1 batch_num += 1 # Recheck file for new items print(f"\n🔄 Rechecking {paths_file.name} for new additions...") new_batch_items = parse_batch_file(paths_file) for path, params in new_batch_items: sig = f"{path}|{params}" if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]: batch_queue.append((path, params)) print(f" ✨ New item found: {path}") continue # Set wait time if merged_args.wait_seconds is None: merged_args.wait_seconds = 0 elif merged_args.wait_seconds == -1: merged_args.wait_seconds = 30 if merged_args.no_encode else 0 # Parse audio dicts from merged args audio_titles_dict = parse_audio_dict(merged_args.audio_titles, "titles") if merged_args.audio_titles else {} audio_channels_dict = parse_audio_dict(merged_args.audio_channels, "channels") if merged_args.audio_channels else {} # Process folder process_folder( folder, merged_args.cq, merged_args.transcode_mode, merged_args.resolution, config, TRACKER_FILE, merged_args.test_mode, merged_args.audio_language, merged_args.filter_audio, merged_args.audio_select, merged_args.encoder, merged_args.strip_all_titles, travel_output_folder, merged_args.unforce_subs, merged_args.no_encode, merged_args.force_process, merged_args.replace_file, merged_args.wait_seconds, audio_titles=audio_titles_dict, audio_channels=audio_channels_dict ) print(f"✓ [BATCH {batch_num}] Completed: {folder.name}") logger.info(f"[BATCH {batch_num}] Completed successfully") processed_items.add(item_sig) completed += 1 batch_num += 1 # Recheck file for new items after successful completion print(f"\n🔄 Rechecking {paths_file.name} for new additions...") new_batch_items = parse_batch_file(paths_file) new_count = 0 for path, params in new_batch_items: sig = f"{path}|{params}" if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]: batch_queue.append((path, params)) print(f" ✨ New item found: {path}") new_count += 1 if new_count == 0: print(f" (no new additions)") except Exception as e: print(f"❌ [BATCH {batch_num}] Error: {e}") logger.error(f"[BATCH {batch_num}] Error: {e}", exc_info=True) processed_items.add(item_sig) failed += 1 batch_num += 1 # Recheck file for new items even on error print(f"\n🔄 Rechecking {paths_file.name} for new additions...") try: new_batch_items = parse_batch_file(paths_file) new_count = 0 for path, params in new_batch_items: sig = f"{path}|{params}" if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]: batch_queue.append((path, params)) print(f" ✨ New item found: {path}") new_count += 1 if new_count == 0: print(f" (no new additions)") except Exception as recheck_error: print(f"⚠️ Error rechecking file: {recheck_error}") logger.warning(f"Error during file recheck: {recheck_error}") # Final check for any items added while processing the last batch print() print("=" * 80) print("✓ BATCH QUEUE COMPLETE - Performing final file check...") print("=" * 80) final_items = parse_batch_file(paths_file) final_new = [] for path, params in final_items: sig = f"{path}|{params}" if sig not in processed_items: final_new.append((path, params)) if final_new: print(f"📋 Found {len(final_new)} new item(s) added during processing!\n") for path_str, params_str in final_new: item_sig = f"{path_str}|{params_str}" total_attempted += 1 print("-" * 80) print(f"BATCH [{batch_num}]: {path_str}") if params_str: print(f"Parameters: {params_str}") print("-" * 80) logger.info(f"[BATCH {batch_num}] Processing: {path_str}") try: folder = normalize_input_path(path_str, config.get("path_mappings", {})) if not folder.exists(): print(f"❌ [BATCH {batch_num}] Folder not found: {folder}") logger.error(f"[BATCH {batch_num}] Folder not found: {folder}") failed += 1 batch_num += 1 continue merged_args = merge_batch_args(args, params_str) travel_output_folder = None if merged_args.travel_mode: if not merged_args.output_folder: print(f"❌ [BATCH {batch_num}] --travel requires --output folder") logger.error(f"[BATCH {batch_num}] --travel requires --output folder") failed += 1 batch_num += 1 continue output_base = Path(merged_args.output_folder) input_folder_name = folder.name travel_output_folder = output_base / input_folder_name travel_output_folder.mkdir(parents=True, exist_ok=True) merged_args.resolution = "720" default_cq = get_default_cq(folder, config, "720", merged_args.encoder) merged_args.cq = default_cq + 2 if merged_args.replace_file and not merged_args.no_encode: print(f"❌ [BATCH {batch_num}] --replace requires --no-encode") logger.error(f"[BATCH {batch_num}] --replace requires --no-encode") failed += 1 batch_num += 1 continue if merged_args.wait_seconds is None: merged_args.wait_seconds = 0 elif merged_args.wait_seconds == -1: merged_args.wait_seconds = 30 if merged_args.no_encode else 0 # Parse audio dicts from merged args audio_titles_dict = parse_audio_dict(merged_args.audio_titles, "titles") if merged_args.audio_titles else {} audio_channels_dict = parse_audio_dict(merged_args.audio_channels, "channels") if merged_args.audio_channels else {} process_folder( folder, merged_args.cq, merged_args.transcode_mode, merged_args.resolution, config, TRACKER_FILE, merged_args.test_mode, merged_args.audio_language, merged_args.filter_audio, merged_args.audio_select, merged_args.encoder, merged_args.strip_all_titles, travel_output_folder, merged_args.unforce_subs, merged_args.no_encode, merged_args.force_process, merged_args.replace_file, merged_args.wait_seconds, audio_titles=audio_titles_dict, audio_channels=audio_channels_dict ) print(f"✓ [BATCH {batch_num}] Completed: {folder.name}") logger.info(f"[BATCH {batch_num}] Completed successfully") completed += 1 batch_num += 1 except Exception as e: print(f"❌ [BATCH {batch_num}] Error: {e}") logger.error(f"[BATCH {batch_num}] Error: {e}", exc_info=True) failed += 1 batch_num += 1 else: print("(no new items found)") # Final summary print() print("=" * 80) print(f"✓ BATCH PROCESSING COMPLETE") print(f" Total items processed: {total_attempted}") print(f" ✓ Succeeded: {completed}") if failed > 0: print(f" ❌ Failed: {failed}") print("=" * 80) logger.info(f"Batch processing finished: {completed} succeeded, {failed} failed out of {total_attempted}") return # ============================= # SINGLE MODE # ============================= if not args.folder: parser.print_help() return # 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 # Handle travel mode travel_output_folder = None if args.travel_mode: if not args.output_folder: print("❌ --travel flag requires --output folder to be specified") logger.error("--travel flag used without --output folder") return # Parse output folder and create subfolder based on input folder name output_base = Path(args.output_folder) input_folder_name = folder.name travel_output_folder = output_base / input_folder_name # Create the output folder structure travel_output_folder.mkdir(parents=True, exist_ok=True) print(f"✅ Travel mode: Output folder set to {travel_output_folder}") logger.info(f"Travel mode enabled: {folder} -> {travel_output_folder}") # Set resolution to 720 in travel mode args.resolution = "720" # Get default CQ for 720p and add 2 default_cq = get_default_cq(folder, config, "720", args.encoder) args.cq = default_cq + 2 print(f"✅ Travel mode: Resolution=720p, CQ={args.cq} (default {default_cq} + 2)") logger.info(f"Travel mode: CQ set to {args.cq}") # Validate --replace flag requires --no-encode if args.replace_file and not args.no_encode: print("❌ --replace requires --no-encode flag") logger.error("--replace flag used without --no-encode") return # Parse audio titles from CLI argument audio_titles_dict = {} if args.audio_titles: try: # Format: "0:English,1:Commentary,2:Descriptive Audio" pairs = args.audio_titles.split(",") for pair in pairs: stream_idx, title = pair.split(":") audio_titles_dict[int(stream_idx.strip())] = title.strip() logger.info(f"Audio titles: {audio_titles_dict}") except (ValueError, IndexError): print("❌ Invalid --audio-titles format. Use: '0:English,1:Commentary'") logger.error(f"Invalid audio titles format: {args.audio_titles}") return # Parse audio channels from CLI argument audio_channels_dict = {} if args.audio_channels: try: # Format: "0:2,1:6" (stream_index:channel_count) pairs = args.audio_channels.split(",") for pair in pairs: stream_idx, channels = pair.split(":") stream_idx = int(stream_idx.strip()) channels = int(channels.strip()) # Validate that only 2 or 6 channels are allowed if channels not in (2, 6): print(f"❌ Invalid channel count: {channels}. Only 2 or 6 channels allowed") logger.error(f"Invalid channel count: {channels}. Only 2 or 6 channels allowed") return audio_channels_dict[stream_idx] = channels logger.info(f"Audio channels: {audio_channels_dict}") except (ValueError, IndexError): print("❌ Invalid --audio-channels format. Use: '0:2,1:6'") logger.error(f"Invalid audio channels format: {args.audio_channels}") return # Set wait time default: 30s if --no-encode and --wait used, 0 otherwise # -1 means --wait was used without a value (use intelligent default) if args.wait_seconds is None: args.wait_seconds = 0 # No --wait flag provided elif args.wait_seconds == -1: args.wait_seconds = 30 if args.no_encode else 0 # --wait used without value # 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, travel_output_folder, args.unforce_subs, args.no_encode, args.force_process, args.replace_file, args.wait_seconds, audio_titles=audio_titles_dict, audio_channels=audio_channels_dict) if __name__ == "__main__": main()