feat: improve logging
This commit is contained in:
203
aac2eac.py
203
aac2eac.py
@@ -4,6 +4,7 @@ import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
EXTENSIONS = {".mkv", ".mp4"}
|
||||
@@ -25,12 +26,106 @@ args = parser.parse_args()
|
||||
# ----------------------------
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s | %(levelname)-7s | %(message)s",
|
||||
format="%(message)s",
|
||||
datefmt="%H:%M:%S"
|
||||
)
|
||||
log = logging.getLogger("aac2eac3")
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Colors & Formatting
|
||||
# ----------------------------
|
||||
class Colors:
|
||||
HEADER = '\033[95m'
|
||||
BLUE = '\033[94m'
|
||||
CYAN = '\033[96m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BOLD = '\033[1m'
|
||||
DIM = '\033[2m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
|
||||
def header(text):
|
||||
log.info(f"\n{Colors.BOLD}{Colors.CYAN}{'═' * 70}{Colors.RESET}")
|
||||
log.info(f"{Colors.BOLD}{Colors.CYAN} {text}{Colors.RESET}")
|
||||
log.info(f"{Colors.BOLD}{Colors.CYAN}{'═' * 70}{Colors.RESET}\n")
|
||||
|
||||
|
||||
def section(text):
|
||||
log.info(f"\n{Colors.BOLD}{Colors.BLUE}▶ {text}{Colors.RESET}")
|
||||
|
||||
|
||||
def success(text):
|
||||
log.info(f"{Colors.GREEN}✓ {text}{Colors.RESET}")
|
||||
|
||||
|
||||
def info(text, indent=0):
|
||||
prefix = " " * indent
|
||||
log.info(f"{prefix}{Colors.DIM}{text}{Colors.RESET}")
|
||||
|
||||
|
||||
def warning(text):
|
||||
log.info(f"{Colors.YELLOW}⚠ {text}{Colors.RESET}")
|
||||
|
||||
|
||||
def error(text):
|
||||
log.info(f"{Colors.RED}✗ {text}{Colors.RESET}")
|
||||
|
||||
|
||||
def format_size(bytes):
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if bytes < 1024.0:
|
||||
return f"{bytes:.1f} {unit}"
|
||||
bytes /= 1024.0
|
||||
return f"{bytes:.1f} TB"
|
||||
|
||||
|
||||
def format_time(seconds):
|
||||
if seconds < 60:
|
||||
return f"{seconds:.0f}s"
|
||||
elif seconds < 3600:
|
||||
return f"{seconds // 60:.0f}m {seconds % 60:.0f}s"
|
||||
else:
|
||||
return f"{seconds // 3600:.0f}h {(seconds % 3600) // 60:.0f}m"
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Stats Tracking
|
||||
# ----------------------------
|
||||
class Stats:
|
||||
def __init__(self):
|
||||
self.total_files = 0
|
||||
self.processed = 0
|
||||
self.skipped = 0
|
||||
self.converted = 0
|
||||
self.failed = 0
|
||||
self.start_time = time.time()
|
||||
self.conversion_times = []
|
||||
|
||||
def print_summary(self):
|
||||
elapsed = time.time() - self.start_time
|
||||
header("Conversion Summary")
|
||||
|
||||
log.info(f" {Colors.BOLD}Total files scanned:{Colors.RESET} {self.total_files}")
|
||||
log.info(f" {Colors.GREEN}{Colors.BOLD}Successfully converted:{Colors.RESET} {self.converted}")
|
||||
log.info(f" {Colors.YELLOW}{Colors.BOLD}Skipped (no AAC):{Colors.RESET} {self.skipped}")
|
||||
if self.failed > 0:
|
||||
log.info(f" {Colors.RED}{Colors.BOLD}Failed:{Colors.RESET} {self.failed}")
|
||||
|
||||
log.info(f"\n {Colors.BOLD}Total time:{Colors.RESET} {format_time(elapsed)}")
|
||||
|
||||
if self.conversion_times:
|
||||
avg_time = sum(self.conversion_times) / len(self.conversion_times)
|
||||
log.info(f" {Colors.BOLD}Average per file:{Colors.RESET} {format_time(avg_time)}")
|
||||
|
||||
log.info("")
|
||||
|
||||
|
||||
stats = Stats()
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Helpers
|
||||
# ----------------------------
|
||||
@@ -65,15 +160,20 @@ def build_output_name(file: Path):
|
||||
# ----------------------------
|
||||
|
||||
def convert(file: Path):
|
||||
stats.processed += 1
|
||||
|
||||
section(f"[{stats.processed}/{stats.total_files}] {file.name}")
|
||||
|
||||
streams = probe_streams(file)
|
||||
if not streams:
|
||||
log.warning("No streams found: %s", file.name)
|
||||
warning("No streams found")
|
||||
stats.skipped += 1
|
||||
return
|
||||
|
||||
# Build FFmpeg codec maps
|
||||
codec_map = {}
|
||||
target_bitrates = {}
|
||||
has_aac_multichannel = False
|
||||
aac_tracks = []
|
||||
|
||||
for s in streams:
|
||||
if s["codec_type"] == "audio":
|
||||
@@ -81,7 +181,7 @@ def convert(file: Path):
|
||||
if s["codec_name"] == "aac" and s.get("channels") in (6, 8):
|
||||
codec_map[s["index"]] = "eac3"
|
||||
target_bitrates[s["index"]] = "640k"
|
||||
has_aac_multichannel = True
|
||||
aac_tracks.append(s)
|
||||
else:
|
||||
codec_map[s["index"]] = "copy"
|
||||
elif s["codec_type"] == "video":
|
||||
@@ -89,59 +189,79 @@ def convert(file: Path):
|
||||
elif s["codec_type"] == "subtitle":
|
||||
codec_map[s["index"]] = "copy"
|
||||
|
||||
if not has_aac_multichannel:
|
||||
log.info("Skipping %s (no AAC 5.1/7.1 tracks)", file.name)
|
||||
if not aac_tracks:
|
||||
info("No AAC 5.1/7.1 tracks found - skipping", 1)
|
||||
stats.skipped += 1
|
||||
return
|
||||
|
||||
# Show what will be converted
|
||||
for s in aac_tracks:
|
||||
channels = s.get("channels", 0)
|
||||
lang = s.get("tags", {}).get("language", "und")
|
||||
title = s.get("tags", {}).get("title", "")
|
||||
track_info = f"Track {s['index']}: AAC {channels}ch"
|
||||
if lang != "und":
|
||||
track_info += f" ({lang})"
|
||||
if title:
|
||||
track_info += f" - {title}"
|
||||
info(f"{track_info} → E-AC-3 @ 640k", 1)
|
||||
|
||||
output = build_output_name(file)
|
||||
|
||||
if args.dry_run:
|
||||
log.info("[DRY RUN] Would convert %s → %s", file.name, output.name)
|
||||
for s in streams:
|
||||
if s["codec_type"] == "audio" and s["index"] in codec_map:
|
||||
action = codec_map[s["index"]]
|
||||
if action == "eac3":
|
||||
log.info(" [DRY RUN] Stream %d: %s %dch → E-AC-3 @ %s",
|
||||
s["index"], s["codec_name"], s.get("channels", 0),
|
||||
target_bitrates[s["index"]])
|
||||
else:
|
||||
log.info(" [DRY RUN] Stream %d: %s (copy)",
|
||||
s["index"], s["codec_name"])
|
||||
if args.overwrite:
|
||||
info(f"Would replace: {Colors.CYAN}{file.name}{Colors.RESET}", 1)
|
||||
else:
|
||||
info(f"Would create: {Colors.CYAN}{output.name}{Colors.RESET}", 1)
|
||||
stats.converted += 1
|
||||
return
|
||||
|
||||
cmd = [args.ffmpeg, "-y", "-hide_banner", "-loglevel", "info", "-stats", "-i", str(file)]
|
||||
|
||||
# Map all streams
|
||||
# Build FFmpeg command
|
||||
cmd = [args.ffmpeg, "-y", "-hide_banner", "-loglevel", "error", "-stats", "-i", str(file)]
|
||||
cmd += ["-map", "0"]
|
||||
|
||||
# Set codecs
|
||||
for idx, c in codec_map.items():
|
||||
cmd += [f"-c:{idx}", c]
|
||||
if c == "eac3":
|
||||
cmd += [f"-b:a:{idx}", target_bitrates[idx]]
|
||||
|
||||
# Preserve metadata
|
||||
cmd += ["-map_metadata", "0", str(output)]
|
||||
|
||||
log.info("Converting %s → %s", file.name, output.name)
|
||||
info(f"Converting → {Colors.CYAN}{output.name}{Colors.RESET}", 1)
|
||||
|
||||
start_time = time.time()
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
|
||||
# Show FFmpeg progress
|
||||
for line in process.stdout:
|
||||
if line.strip():
|
||||
print(f" {Colors.DIM}{line.rstrip()}{Colors.RESET}")
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
|
||||
process.wait()
|
||||
elapsed = time.time() - start_time
|
||||
stats.conversion_times.append(elapsed)
|
||||
|
||||
if process.returncode != 0:
|
||||
log.error("FFmpeg failed: %s", file.name)
|
||||
error(f"FFmpeg failed (exit code {process.returncode})")
|
||||
stats.failed += 1
|
||||
if output.exists():
|
||||
output.unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
if args.overwrite:
|
||||
backup = file.with_suffix(file.suffix + ".bak")
|
||||
log.info("Replacing original file")
|
||||
shutil.move(file, backup)
|
||||
shutil.move(output, file)
|
||||
backup.unlink(missing_ok=True)
|
||||
info(f"Original file replaced", 1)
|
||||
|
||||
log.info("Completed: %s", file.name)
|
||||
# Show file sizes
|
||||
if output.exists() or (args.overwrite and file.exists()):
|
||||
final_file = file if args.overwrite else output
|
||||
size = format_size(final_file.stat().st_size)
|
||||
success(f"Completed in {format_time(elapsed)} ({size})")
|
||||
|
||||
stats.converted += 1
|
||||
|
||||
|
||||
# ----------------------------
|
||||
@@ -149,27 +269,42 @@ def convert(file: Path):
|
||||
# ----------------------------
|
||||
|
||||
def main():
|
||||
header("AAC to E-AC-3 Converter")
|
||||
|
||||
root = Path(args.path).resolve()
|
||||
if not root.is_dir():
|
||||
raise SystemExit(f"Invalid directory: {root}")
|
||||
error(f"Invalid directory: {root}")
|
||||
raise SystemExit(1)
|
||||
|
||||
info(f"Scanning: {Colors.CYAN}{root}{Colors.RESET}")
|
||||
files = sorted(f for f in root.rglob("*") if f.suffix.lower() in EXTENSIONS)
|
||||
|
||||
if not files:
|
||||
log.warning("No media files found")
|
||||
warning("No media files found")
|
||||
return
|
||||
|
||||
log.info("Found %d files", len(files))
|
||||
stats.total_files = len(files)
|
||||
|
||||
log.info(f"Found {Colors.BOLD}{len(files)}{Colors.RESET} media files")
|
||||
|
||||
if args.dry_run:
|
||||
log.info("=== DRY RUN MODE - No files will be modified ===")
|
||||
log.info(f"\n{Colors.YELLOW}{Colors.BOLD}DRY RUN MODE{Colors.RESET} - No files will be modified\n")
|
||||
|
||||
if args.overwrite:
|
||||
log.info(f"{Colors.YELLOW}Overwrite mode enabled{Colors.RESET} - Original files will be replaced\n")
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
convert(file)
|
||||
except KeyboardInterrupt:
|
||||
log.warning("Interrupted by user")
|
||||
log.info(f"\n\n{Colors.YELLOW}Interrupted by user{Colors.RESET}")
|
||||
stats.print_summary()
|
||||
sys.exit(130)
|
||||
except Exception:
|
||||
log.exception("Unexpected error while processing %s", file.name)
|
||||
except Exception as e:
|
||||
error(f"Unexpected error: {e}")
|
||||
stats.failed += 1
|
||||
|
||||
stats.print_summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user