f2opus (2790B)
1 #!/usr/bin/env python3 2 import argparse 3 import concurrent.futures 4 import os 5 import subprocess 6 from itertools import count 7 from pathlib import Path 8 9 FFMPEG_IN = ( 10 "ffmpeg", 11 "-hide_banner", 12 "-loglevel", 13 "error", 14 "-nostdin", 15 "-threads", 16 "1", 17 ) 18 FFMPEG_OUT = ( 19 "-c:a", 20 "libopus", 21 "-b:a", 22 "160k", 23 "-vbr", 24 "on", 25 "-map_metadata", 26 "0", 27 "-f", 28 "opus", 29 "-y", 30 ) 31 32 33 def convert(in_path, out_path, total, counter): 34 tmp = out_path.with_name(out_path.name + ".part") 35 if out_path.exists() and out_path.stat().st_size > 0: 36 tmp.unlink(missing_ok=True) 37 in_path.unlink() 38 i = next(counter) 39 print(f"[{i}/{total}] SKIP {in_path}") 40 return True 41 42 tmp.unlink(missing_ok=True) 43 result = subprocess.run( 44 [*FFMPEG_IN, "-i", str(in_path), *FFMPEG_OUT, str(tmp)], 45 stdin=subprocess.DEVNULL, 46 stdout=subprocess.DEVNULL, 47 stderr=subprocess.PIPE, 48 ) 49 i = next(counter) 50 if result.returncode == 0 and tmp.exists() and tmp.stat().st_size > 0: 51 os.replace(tmp, out_path) 52 in_path.unlink() 53 print(f"[{i}/{total}] OK {in_path}") 54 return True 55 56 tmp.unlink(missing_ok=True) 57 print(f"[{i}/{total}] FAIL {in_path}") 58 if result.stderr: 59 print(f" {result.stderr.decode().strip().splitlines()[-1]}") 60 return False 61 62 63 def main(): 64 parser = argparse.ArgumentParser( 65 description="Convert FLAC files to Opus in using ffmpeg" 66 ) 67 parser.add_argument( 68 "root", nargs="?", default=".", help="directory to search for FLAC files" 69 ) 70 parser.add_argument( 71 "workers", 72 nargs="?", 73 type=int, 74 default=2 * (os.cpu_count() or 1), 75 help="ffmpeg workers count", 76 ) 77 args = parser.parse_args() 78 79 root = Path(args.root).resolve() 80 jobs = sorted( 81 (p.resolve(), p.with_suffix(".opus").resolve()) for p in root.rglob("*.flac") 82 ) 83 total = len(jobs) 84 if total == 0: 85 print("No FLAC files found") 86 return 87 88 print(f"Found {total} FLAC files in {root}") 89 print(f"Workers: {args.workers}\n") 90 91 counter = count(1) 92 failed = [] 93 94 with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as ex: 95 future_to_in = { 96 ex.submit(convert, in_path, out_path, total, counter): in_path 97 for in_path, out_path in jobs 98 } 99 for future in concurrent.futures.as_completed(future_to_in): 100 if not future.result(): 101 failed.append(future_to_in[future]) 102 103 print(f"\nDone: {total - len(failed)}/{total} converted") 104 if failed: 105 print(f"{len(failed)} failed:") 106 for f in failed: 107 print(f" {f}") 108 109 110 if __name__ == "__main__": 111 main()