198 lines
6.2 KiB
Python
198 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
def format_size(size_bytes):
|
|
"""Formats file size in bytes to a human-readable string."""
|
|
try:
|
|
size_bytes = int(size_bytes)
|
|
except (ValueError, TypeError):
|
|
return "Unknown size"
|
|
|
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
if size_bytes < 1024.0:
|
|
return f"{size_bytes:.2f} {unit}"
|
|
size_bytes /= 1024.0
|
|
return f"{size_bytes:.2f} TB"
|
|
|
|
def parse_fps(fps_str):
|
|
"""Parses fractional frame rates like '30/1' or '24000/1001' into floats."""
|
|
if not fps_str:
|
|
return "Unknown"
|
|
if "/" in fps_str:
|
|
try:
|
|
num, den = map(float, fps_str.split("/"))
|
|
if den != 0:
|
|
val = num / den
|
|
if val.is_integer():
|
|
return f"{int(val)} fps"
|
|
return f"{val:.2f} fps"
|
|
except (ValueError, ZeroDivisionError):
|
|
pass
|
|
try:
|
|
val = float(fps_str)
|
|
if val.is_integer():
|
|
return f"{int(val)} fps"
|
|
return f"{val:.2f} fps"
|
|
except ValueError:
|
|
return fps_str
|
|
|
|
def inspect_video(file_path, raw=False):
|
|
"""Runs ffprobe on the video file and returns parsed metadata dictionary."""
|
|
if not os.path.exists(file_path):
|
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
|
|
# Check if ffprobe is available
|
|
try:
|
|
subprocess.run(["ffprobe", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
raise RuntimeError("ffprobe is not installed or not found in system PATH.")
|
|
|
|
cmd = [
|
|
"ffprobe",
|
|
"-v", "error",
|
|
"-show_format",
|
|
"-show_streams",
|
|
"-of", "json",
|
|
file_path
|
|
]
|
|
|
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
|
|
data = json.loads(result.stdout)
|
|
|
|
if raw:
|
|
return data
|
|
|
|
# Extract format level details
|
|
fmt = data.get("format", {})
|
|
duration = fmt.get("duration")
|
|
size_bytes = fmt.get("size")
|
|
bitrate = fmt.get("bit_rate")
|
|
|
|
# Format files size
|
|
size_str = format_size(size_bytes) if size_bytes else "Unknown"
|
|
|
|
# Parse duration
|
|
try:
|
|
duration_val = float(duration) if duration else 0.0
|
|
duration_str = f"{duration_val:.2f}s"
|
|
except ValueError:
|
|
duration_str = "Unknown"
|
|
duration_val = None
|
|
|
|
# Parse bitrate
|
|
try:
|
|
bitrate_kbps = f"{int(float(bitrate) / 1000)} kbps" if bitrate else "Unknown"
|
|
except ValueError:
|
|
bitrate_kbps = "Unknown"
|
|
|
|
video_streams = [s for s in data.get("streams", []) if s.get("codec_type") == "video"]
|
|
audio_streams = [s for s in data.get("streams", []) if s.get("codec_type") == "audio"]
|
|
|
|
has_video = len(video_streams) > 0
|
|
has_audio = len(audio_streams) > 0
|
|
|
|
video_info = {}
|
|
if has_video:
|
|
v = video_streams[0]
|
|
width = v.get("width")
|
|
height = v.get("height")
|
|
codec = v.get("codec_name", "Unknown").upper()
|
|
r_fps = parse_fps(v.get("r_frame_rate"))
|
|
avg_fps = parse_fps(v.get("avg_frame_rate"))
|
|
|
|
# Prefer r_frame_rate but fallback to avg
|
|
fps = r_fps if r_fps != "0 fps" and r_fps != "Unknown" else avg_fps
|
|
|
|
video_info = {
|
|
"resolution": f"{width}x{height}" if width and height else "Unknown",
|
|
"width": width,
|
|
"height": height,
|
|
"fps": fps,
|
|
"codec": codec,
|
|
"duration": v.get("duration")
|
|
}
|
|
|
|
audio_info = {}
|
|
if has_audio:
|
|
a = audio_streams[0]
|
|
codec = a.get("codec_name", "Unknown").upper()
|
|
channels = a.get("channels", "Unknown")
|
|
sample_rate = a.get("sample_rate")
|
|
sample_rate_khz = f"{float(sample_rate)/1000:.1f} kHz" if sample_rate else "Unknown"
|
|
|
|
audio_info = {
|
|
"codec": codec,
|
|
"channels": channels,
|
|
"sample_rate": sample_rate_khz
|
|
}
|
|
|
|
return {
|
|
"file_name": os.path.basename(file_path),
|
|
"file_size": size_str,
|
|
"size_bytes": size_bytes,
|
|
"duration": duration_str,
|
|
"duration_seconds": duration_val,
|
|
"bitrate": bitrate_kbps,
|
|
"has_video": has_video,
|
|
"video": video_info,
|
|
"has_audio": has_audio,
|
|
"audio": audio_info
|
|
}
|
|
|
|
def print_terminal_report(info):
|
|
"""Prints an aligned terminal report."""
|
|
print(f"\nVideo Inspection Report: {info['file_name']}")
|
|
print("=" * 50)
|
|
print(f"File Size : {info['file_size']}")
|
|
print(f"Duration : {info['duration']}")
|
|
print(f"Bitrate : {info['bitrate']}")
|
|
|
|
print("\nVideo Stream Details:")
|
|
if info["has_video"]:
|
|
v = info["video"]
|
|
print(f" * Resolution : {v['resolution']}")
|
|
print(f" * Frame Rate : {v['fps']}")
|
|
print(f" * Codec : {v['codec']}")
|
|
else:
|
|
print(" * No Video Stream Found.")
|
|
|
|
print("\nAudio Stream Details:")
|
|
if info["has_audio"]:
|
|
a = info["audio"]
|
|
print(" * Status : Audio Present")
|
|
print(f" * Codec : {a['codec']}")
|
|
print(f" * Channels : {a['channels']}")
|
|
print(f" * Sample Rate: {a['sample_rate']}")
|
|
else:
|
|
print(" * Status : No Audio Stream Present")
|
|
print()
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Inspect video details (duration, frame rate, resolution, audio presence) using ffprobe.")
|
|
parser.add_argument("file", help="Path to the video file to inspect")
|
|
parser.add_argument("--json", action="store_true", help="Output parsed summary in JSON format")
|
|
parser.add_argument("--raw", action="store_true", help="Output raw unmodified ffprobe JSON data")
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
if args.raw:
|
|
info = inspect_video(args.file, raw=True)
|
|
print(json.dumps(info, indent=2))
|
|
else:
|
|
info = inspect_video(args.file, raw=False)
|
|
if args.json:
|
|
print(json.dumps(info, indent=2))
|
|
else:
|
|
print_terminal_report(info)
|
|
except Exception as e:
|
|
print(f"Error inspecting video: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|