diff --git a/AV1 Master.bat b/AV1 Master.bat index 76bf8cb..6a42388 100644 --- a/AV1 Master.bat +++ b/AV1 Master.bat @@ -57,7 +57,7 @@ if not errorlevel 1 ( if "%RES_CHOICE%"=="1080" ( set "CQ_DEFAULT=28" ) else ( - set "CQ_DEFAULT=30" + set "CQ_DEFAULT=32" ) ) else ( REM Defaults for anime @@ -119,17 +119,29 @@ for /R "%ORIGINATION_FOLDER%" %%F in (*.mkv *.mp4) do ( REM Wait briefly to ensure copy finishes timeout /t 1 /nobreak >nul - REM ========================= - REM Determine audio bitrate based on channel count - REM ========================= - set "AUDIO_BITRATE=" - set "AUDIO_CHANNELS=" - for /f "tokens=1,2 delims=," %%A in ('ffprobe -v error -select_streams a -show_entries stream=index,channels -of csv=p=0 "%INPUT_FILE%"') do ( + rem ========================= + rem Detect audio bitrate & bucket to safe values + rem ========================= + for /f "tokens=1,2,3 delims=," %%A in ('ffprobe -v error -select_streams a:0 -show_entries stream=index,channels,bit_rate -of csv=p=0 "%INPUT_FILE%"') do ( + set "STREAM_INDEX=%%A" + set "STREAM_CHANNELS=%%B" + set /a "STREAM_BR=%%C/1000" rem kbps approx + if %%B==2 ( - set "AUDIO_BITRATE=128k" + if !STREAM_BR! lss 80 ( + set "AUDIO_BITRATE=64000" + ) else if !STREAM_BR! lss 112 ( + set "AUDIO_BITRATE=96000" + ) else ( + set "AUDIO_BITRATE=128000" + ) set "AUDIO_CHANNELS=2" ) else ( - set "AUDIO_BITRATE=192k" + if !STREAM_BR! lss 176 ( + set "AUDIO_BITRATE=160000" + ) else ( + set "AUDIO_BITRATE=192000" + ) set "AUDIO_CHANNELS=6" ) ) @@ -143,9 +155,20 @@ for /R "%ORIGINATION_FOLDER%" %%F in (*.mkv *.mp4) do ( ) REM Default to 128k / 2 channels if detection failed - if "!AUDIO_BITRATE!"=="" set "AUDIO_BITRATE=128k" + if "!AUDIO_BITRATE!"=="" set "AUDIO_BITRATE=128000" if "!AUDIO_CHANNELS!"=="" set "AUDIO_CHANNELS=2" + set "LANGUAGE_TAG=" + for /f "tokens=1,* delims==" %%A in ('ffprobe -v error -select_streams a:0 -show_entries stream_tags=language -of default=nokey=1:noprint_wrappers=1 "!INPUT_FILE!"') do ( + set "LANGUAGE_TAG=%%A" + ) + + if "!LANGUAGE_TAG!"=="" ( + set "LANGUAGE_METADATA=-metadata:s:a:0 language=eng" + ) else ( + set "LANGUAGE_METADATA=" + ) + REM ========================= REM Run conversion with AV1 NVENC, 10-bit, auto aspect ratio REM ========================= @@ -153,7 +176,7 @@ for /R "%ORIGINATION_FOLDER%" %%F in (*.mkv *.mp4) do ( -vf "scale=!SCALE_WIDTH!:!SCALE_HEIGHT!:flags=!FILTER_FLAGS!:force_original_aspect_ratio=decrease" ^ -map 0:v -map 0:a -map 0:s? ^ -c:v av1_nvenc -preset p1 -cq %CQ% -pix_fmt p010le ^ - -c:a aac -b:a !AUDIO_BITRATE! -ac !AUDIO_CHANNELS! ^ + -c:a aac -b:a !AUDIO_BITRATE! -ac !AUDIO_CHANNELS! !LANGUAGE_METADATA! -metadata:s:a:0 bit_rate=!AUDIO_BITRATE! ^ -c:s copy ^ "%PROCESSING_FOLDER%\%%~nF%SUFFIX%.mkv" @@ -177,7 +200,6 @@ for /R "%ORIGINATION_FOLDER%" %%F in (*.mkv *.mp4) do ( ) else ( echo ERROR: Converted file not found. Skipping deletion of originals. ) - echo ===================================================== ) else ( echo Skipping: %%~nxF (contains 'EHX' or 'MeGusta') ) diff --git a/main.py b/main.py new file mode 100644 index 0000000..f0ca4bc --- /dev/null +++ b/main.py @@ -0,0 +1,145 @@ +import os +import shutil +import subprocess +from pathlib import Path +import argparse + +def get_audio_info(filepath): + """Return (stream_index, channels, bitrate_in_bits).""" + cmd = [ + "ffprobe", "-v", "error", "-select_streams", "a:0", + "-show_entries", "stream=index,channels,bit_rate", + "-of", "csv=p=0", filepath + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if not result.stdout: + return None, 2, 128000 + parts = result.stdout.strip().split(',') + if len(parts) < 3: + return None, 2, 128000 + _, channels, bitrate = parts + channels = int(channels) + bitrate = int(bitrate) // 1000 # kbps approx + + # Bucket logic + if channels == 2: + if bitrate < 80: + br = 64000 + elif bitrate < 112: + br = 96000 + else: + br = 128000 + else: + if bitrate < 176: + br = 160000 + else: + br = 192000 + return 0, channels, br + +def get_language_tag(filepath): + cmd = [ + "ffprobe", "-v", "error", "-select_streams", "a:0", + "-show_entries", "stream_tags=language", + "-of", "default=nokey=1:noprint_wrappers=1", filepath + ] + result = subprocess.run(cmd, capture_output=True, text=True) + return result.stdout.strip() or None + + +def main(): + parser = argparse.ArgumentParser( + description="Batch AV1 transcoder (FFmpeg-based) for anime/TV folders." + ) + parser.add_argument("origination", help="Path to origination folder (e.g., P:\\Anime\\Show)") + parser.add_argument("-p", "--processing", default=r"C:\Users\Tyler\Videos\Video Conversion\temp", + help="Temporary processing folder (default: %(default)s)") + parser.add_argument("-c", "--completed", default=None, + help="Completed output folder (default: back to original)") + parser.add_argument("-r", "--resolution", type=int, choices=[720, 1080], default=1080, + help="Output resolution height (default: %(default)s)") + parser.add_argument("-cq", type=int, default=None, + help="Constant quality value for AV1_NVENC (auto if not specified)") + + args = parser.parse_args() + + orig = args.origination + processing = args.processing + completed = args.completed + res_choice = str(args.resolution) + + # Auto-detect filter & default CQ + filter_flags = "lanczos" + if "\\tv\\" in orig.lower() or "/tv/" in orig.lower(): + filter_flags = "bicubic" + cq_default = 28 if res_choice == "1080" else 32 + else: + cq_default = 32 if res_choice == "1080" else 34 + + cq = args.cq if args.cq is not None else cq_default + + print("\n=== Using These Settings ===") + print(f"Origination: {orig}") + print(f"Processing: {processing}") + print(f"Completed: {completed if completed else '[Return to original folder]'}") + print(f"Resolution: {res_choice}") + print(f"Filter: {filter_flags}") + print(f"CQ: {cq}") + print("=============================\n") + + os.makedirs(processing, exist_ok=True) + if completed: + os.makedirs(completed, exist_ok=True) + + suffix = " -EHX" + + for root, dirs, files in os.walk(orig): + for f in files: + if not f.lower().endswith((".mkv", ".mp4")): + continue + if "ehx" in f.lower() or "megusta" in f.lower(): + print(f"Skipping {f} (contains 'EHX' or 'MeGusta')") + continue + + print("=" * 60) + print(f"Processing: {f}") + src = Path(root) / f + tmp = Path(processing) / f + shutil.copy2(src, tmp) + + # Detect audio info + _, channels, abr = get_audio_info(str(tmp)) + lang = get_language_tag(str(tmp)) + lang_metadata = [] + if not lang: + lang_metadata = ["-metadata:s:a:0", "language=eng"] + + width, height = ("1920", "1080") if res_choice == "1080" else ("1280", "720") + out_file = Path(processing) / f"{Path(f).stem}{suffix}.mkv" + + ffmpeg_cmd = [ + "ffmpeg", "-y", "-i", str(tmp), + "-vf", f"scale={width}:{height}:flags={filter_flags}:force_original_aspect_ratio=decrease", + "-map", "0:v", "-map", "0:a", "-map", "0:s?", + "-c:v", "av1_nvenc", "-preset", "p1", "-cq", str(cq), "-pix_fmt", "p010le", + "-c:a", "aac", "-b:a", str(abr), "-ac", str(channels), + *lang_metadata, + "-metadata:s:a:0", f"bit_rate={abr}", + "-c:s", "copy", + str(out_file) + ] + + subprocess.run(ffmpeg_cmd) + + target = Path(completed) / out_file.name if completed else Path(root) / out_file.name + shutil.move(out_file, target) + print(f"Moved file to {target}") + + if target.exists(): + print("Conversion confirmed. Deleting originals...") + os.remove(src) + os.remove(tmp) + else: + print("ERROR: Converted file not found. Skipping deletion of originals.") + +if __name__ == "__main__": + main()