2025-10-28 22:19:22 -06:00
|
|
|
import os
|
|
|
|
|
import subprocess
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
# ANSI color codes
|
|
|
|
|
class Colors:
|
|
|
|
|
PURPLE = '\033[95m'
|
|
|
|
|
BLUE = '\033[94m'
|
|
|
|
|
GREEN = '\033[92m'
|
|
|
|
|
YELLOW = '\033[93m'
|
|
|
|
|
RED = '\033[91m'
|
|
|
|
|
ENDC = '\033[0m'
|
|
|
|
|
|
|
|
|
|
# Set up logging
|
|
|
|
|
log_dir = "logs"
|
|
|
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
|
log_file = os.path.join(log_dir, f"repair_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
|
|
|
|
|
logging.basicConfig(filename=log_file, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
|
|
|
|
def check_file_health(input_file):
|
|
|
|
|
"""Check if MP4 file has issues using ffmpeg"""
|
|
|
|
|
print(f"\n{Colors.BLUE}Analyzing: {input_file}{Colors.ENDC}")
|
|
|
|
|
logging.info(f"Analyzing file: {input_file}")
|
|
|
|
|
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg',
|
|
|
|
|
'-v', 'error',
|
|
|
|
|
'-i', str(input_file),
|
|
|
|
|
'-f', 'null',
|
|
|
|
|
'-'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
|
|
|
|
|
|
if result.stderr:
|
|
|
|
|
print(f"{Colors.RED}Issues found:{Colors.ENDC}")
|
|
|
|
|
for line in result.stderr.split('\n')[:10]: # Show first 10 errors
|
|
|
|
|
if line.strip():
|
|
|
|
|
print(f" {Colors.YELLOW}{line}{Colors.ENDC}")
|
|
|
|
|
logging.warning(f"File has errors: {result.stderr[:500]}")
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
print(f"{Colors.GREEN}No errors detected{Colors.ENDC}")
|
|
|
|
|
logging.info("File appears healthy")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def get_file_info(input_file):
|
|
|
|
|
"""Get basic file info"""
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffprobe',
|
|
|
|
|
'-v', 'error',
|
|
|
|
|
'-show_entries', 'format=duration,size:stream=codec_name,width,height',
|
|
|
|
|
'-of', 'json',
|
|
|
|
|
input_file
|
|
|
|
|
]
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(result.stdout)
|
|
|
|
|
except:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def repair_method_1_remux(input_file, output_file):
|
|
|
|
|
"""Method 1: Simple remux (copy streams to new container)"""
|
|
|
|
|
print(f"\n{Colors.BLUE}Trying Method 1: Remux (copy streams){Colors.ENDC}")
|
|
|
|
|
logging.info("Attempting repair method 1: Remux")
|
|
|
|
|
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg',
|
|
|
|
|
'-v', 'warning',
|
|
|
|
|
'-i', str(input_file),
|
|
|
|
|
'-c', 'copy',
|
|
|
|
|
'-map', '0',
|
|
|
|
|
'-y',
|
|
|
|
|
str(output_file)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
|
if result.returncode == 0 and Path(output_file).exists():
|
|
|
|
|
print(f"{Colors.GREEN}Method 1 successful!{Colors.ENDC}")
|
|
|
|
|
logging.info("Method 1 successful")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
print(f"{Colors.RED}Method 1 failed{Colors.ENDC}")
|
|
|
|
|
logging.warning(f"Method 1 failed: {result.stderr[:200]}")
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"{Colors.RED}Method 1 error: {e}{Colors.ENDC}")
|
|
|
|
|
logging.error(f"Method 1 error: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def repair_method_2_error_concealment(input_file, output_file):
|
|
|
|
|
"""Method 2: Remux with error concealment"""
|
|
|
|
|
print(f"\n{Colors.BLUE}Trying Method 2: Error concealment{Colors.ENDC}")
|
|
|
|
|
logging.info("Attempting repair method 2: Error concealment")
|
|
|
|
|
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg',
|
|
|
|
|
'-v', 'warning',
|
|
|
|
|
'-err_detect', 'ignore_err',
|
|
|
|
|
'-i', str(input_file),
|
|
|
|
|
'-c', 'copy',
|
|
|
|
|
'-map', '0',
|
|
|
|
|
'-fflags', '+genpts+igndts',
|
|
|
|
|
'-y',
|
|
|
|
|
str(output_file)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
|
if result.returncode == 0 and Path(output_file).exists():
|
|
|
|
|
print(f"{Colors.GREEN}Method 2 successful!{Colors.ENDC}")
|
|
|
|
|
logging.info("Method 2 successful")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
print(f"{Colors.RED}Method 2 failed{Colors.ENDC}")
|
|
|
|
|
logging.warning(f"Method 2 failed: {result.stderr[:200]}")
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"{Colors.RED}Method 2 error: {e}{Colors.ENDC}")
|
|
|
|
|
logging.error(f"Method 2 error: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def repair_method_3_force_keyframes(input_file, output_file):
|
|
|
|
|
"""Method 3: Force keyframe processing"""
|
|
|
|
|
print(f"\n{Colors.BLUE}Trying Method 3: Force keyframe processing{Colors.ENDC}")
|
|
|
|
|
logging.info("Attempting repair method 3: Force keyframe processing")
|
|
|
|
|
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg',
|
|
|
|
|
'-v', 'warning',
|
|
|
|
|
'-fflags', '+discardcorrupt+genpts',
|
|
|
|
|
'-i', str(input_file),
|
|
|
|
|
'-c', 'copy',
|
|
|
|
|
'-map', '0',
|
|
|
|
|
'-avoid_negative_ts', 'make_zero',
|
|
|
|
|
'-y',
|
|
|
|
|
str(output_file)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
|
if result.returncode == 0 and Path(output_file).exists():
|
|
|
|
|
print(f"{Colors.GREEN}Method 3 successful!{Colors.ENDC}")
|
|
|
|
|
logging.info("Method 3 successful")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
print(f"{Colors.RED}Method 3 failed{Colors.ENDC}")
|
|
|
|
|
logging.warning(f"Method 3 failed: {result.stderr[:200]}")
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"{Colors.RED}Method 3 error: {e}{Colors.ENDC}")
|
|
|
|
|
logging.error(f"Method 3 error: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def repair_file(input_file, output_dir):
|
|
|
|
|
"""Attempt to repair a damaged MP4 file"""
|
|
|
|
|
input_path = Path(input_file)
|
|
|
|
|
output_path = Path(output_dir) / f"{input_path.stem}_repaired{input_path.suffix}"
|
|
|
|
|
|
|
|
|
|
print(f"\n{Colors.PURPLE}{'='*60}{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.PURPLE}Processing: {input_path.name}{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.PURPLE}{'='*60}{Colors.ENDC}")
|
|
|
|
|
|
|
|
|
|
# Skip if already repaired
|
|
|
|
|
if output_path.exists():
|
|
|
|
|
print(f"{Colors.YELLOW}Already repaired: {output_path.name}{Colors.ENDC}")
|
|
|
|
|
logging.info(f"Skipping {input_path} - already repaired")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Check file health first
|
|
|
|
|
is_healthy = check_file_health(str(input_path))
|
|
|
|
|
|
|
|
|
|
if is_healthy:
|
|
|
|
|
print(f"{Colors.GREEN}File appears healthy, no repair needed{Colors.ENDC}")
|
|
|
|
|
logging.info("File is healthy, skipping")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Try repair methods in sequence
|
|
|
|
|
methods = [
|
|
|
|
|
repair_method_1_remux,
|
|
|
|
|
repair_method_2_error_concealment,
|
|
|
|
|
repair_method_3_force_keyframes
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for method in methods:
|
|
|
|
|
if method(str(input_path), str(output_path)):
|
|
|
|
|
# Verify the repaired file
|
|
|
|
|
print(f"\n{Colors.BLUE}Verifying repaired file...{Colors.ENDC}")
|
|
|
|
|
if check_file_health(str(output_path)):
|
|
|
|
|
print(f"{Colors.GREEN}✓ Repair successful and verified!{Colors.ENDC}")
|
|
|
|
|
logging.info(f"Successfully repaired: {output_path}")
|
|
|
|
|
|
|
|
|
|
# Show size comparison
|
|
|
|
|
original_size = input_path.stat().st_size / (1024*1024)
|
|
|
|
|
repaired_size = output_path.stat().st_size / (1024*1024)
|
|
|
|
|
print(f"Original: {original_size:.2f} MB → Repaired: {repaired_size:.2f} MB")
|
|
|
|
|
logging.info(f"Size: {original_size:.2f} MB → {repaired_size:.2f} MB")
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
print(f"{Colors.YELLOW}Repair completed but file still has issues{Colors.ENDC}")
|
|
|
|
|
logging.warning("Repaired file still has issues")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"{Colors.RED}All repair methods failed{Colors.ENDC}")
|
|
|
|
|
logging.error(f"Failed to repair: {input_path}")
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
input_dir = "input"
|
|
|
|
|
output_dir = "output"
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# Get list of MP4 files
|
|
|
|
|
files = [f for f in os.listdir(input_dir) if f.endswith(('.mp4', '.DVR.mp4'))]
|
|
|
|
|
total_files = len(files)
|
|
|
|
|
|
|
|
|
|
if total_files == 0:
|
|
|
|
|
print(f"{Colors.YELLOW}No MP4 files found in input directory{Colors.ENDC}")
|
|
|
|
|
logging.info("No files to process")
|
|
|
|
|
else:
|
|
|
|
|
print(f"\n{Colors.BLUE}Found {total_files} MP4 files to check{Colors.ENDC}")
|
|
|
|
|
logging.info(f"Found {total_files} files to process")
|
|
|
|
|
|
|
|
|
|
for i, file in enumerate(files, 1):
|
|
|
|
|
input_file = os.path.join(input_dir, file)
|
|
|
|
|
print(f"\n{Colors.BLUE}[{i}/{total_files}]{Colors.ENDC}")
|
|
|
|
|
repair_file(input_file, output_dir)
|
|
|
|
|
|
|
|
|
|
print(f"\n{Colors.GREEN}{'='*60}{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.GREEN}Repair process complete!{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.GREEN}Check the output directory for repaired files{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.GREEN}Log file: {log_file}{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.GREEN}{'='*60}{Colors.ENDC}")
|
|
|
|
|
|
|
|
|
|
|
2025-12-01 22:25:51 -07:00
|
|
|
|