feat: improve logging

This commit is contained in:
2026-01-23 12:27:01 +01:00
parent d6bccdc088
commit 9fd6f281ef
2 changed files with 215 additions and 44 deletions

View File

@@ -11,16 +11,16 @@ Converts AAC 5.1/7.1 audio tracks to E-AC-3 while preserving all other streams (
```bash
# Preview conversions (recommended first step)
python aac2eac.py /path/to/media --dry-run
python aac2eac3.py /path/to/media --dry-run
# Convert to new files (original.eac.mkv)
python aac2eac.py /path/to/media
python aac2eac3.py /path/to/media
# Overwrite original files (creates temporary backups)
python aac2eac.py /path/to/media --overwrite
python aac2eac3.py /path/to/media --overwrite
# Custom FFmpeg path
python aac2eac.py /path/to/media --ffmpeg /usr/local/bin/ffmpeg
# Custom FFmpeg path (e.g., Jellyfin's FFmpeg)
python aac2eac3.py /path/to/media --ffmpeg /usr/lib/jellyfin-ffmpeg/ffmpeg
```
## Options
@@ -32,10 +32,46 @@ python aac2eac.py /path/to/media --ffmpeg /usr/local/bin/ffmpeg
## What it does
- Scans directory for .mkv and .mp4 files
- Scans directory for .mkv and .mp4 files recursively
- Identifies AAC audio tracks with 6 channels (5.1) or 8 channels (7.1)
- Converts those tracks to E-AC-3:
- 5.1 → 640 kbps
- 7.1 → 1024 kbps
- Converts those tracks to E-AC-3 @ 640 kbps
- Note: 7.1 AAC is downmixed to 5.1 E-AC-3 (FFmpeg limitation)
- Copies all other streams unchanged (video, subtitles, other audio)
- Preserves all metadata
- Preserves all metadata (language tags, track titles, etc.)
## Features
- **Detailed progress tracking** - Shows file-by-file progress with conversion times
- **Smart skipping** - Automatically skips files without AAC 5.1/7.1 tracks
- **Comprehensive summary** - Displays statistics at the end (converted, skipped, failed, time)
- **Color-coded output** - Easy-to-read terminal output with visual indicators
- **Safe operation** - Creates temporary backups when using `--overwrite`
## Example output
```
════════════════════════════════════════════════════════════════════
AAC to E-AC-3 Converter
════════════════════════════════════════════════════════════════════
Scanning: /media/movies
Found 5 media files
▶ [1/5] movie.mkv
Track 1: AAC 6ch (eng) → E-AC-3 @ 640k
Converting → movie.eac.mkv
✓ Completed in 45s (4.2 GB)
▶ [2/5] another_movie.mkv
No AAC 5.1/7.1 tracks found - skipping
════════════════════════════════════════════════════════════════════
Conversion Summary
════════════════════════════════════════════════════════════════════
Total files scanned: 5
Successfully converted: 3
Skipped (no AAC): 2
Total time: 3m 25s
Average per file: 1m 8s
```

View File

@@ -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__":