Skip to content

Instantly share code, notes, and snippets.

@wesleyel
Created May 16, 2025 08:17
Show Gist options
  • Save wesleyel/0f2d64cf2cece5a89e639a316ee22ad6 to your computer and use it in GitHub Desktop.
Save wesleyel/0f2d64cf2cece5a89e639a316ee22ad6 to your computer and use it in GitHub Desktop.
Obtain media duratin like tree
#!/usr/bin/env -S uv --quiet run --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "mutagen>=1.47.0",
# "opencv-python>=4.11.0.86",
# ]
# ///
from pathlib import Path
from mutagen import File as MutagenFile
import cv2
import csv
import argparse
MEDIA_EXTS = {'.mp3', '.mp4', '.wav', '.mkv', '.flac', '.aac', '.mov', '.avi', '.wmv', '.ogg'}
AUDIO_EXTS = {'.mp3', '.wav', '.flac', '.aac', '.ogg'}
VIDEO_EXTS = {'.mp4', '.mkv', '.mov', '.avi', '.wmv'}
def get_media_duration(filepath):
"""Return duration in seconds for a media file using Python libs. Return 0 if not a media file or error."""
ext = Path(filepath).suffix.lower()
try:
if ext in AUDIO_EXTS:
audio = MutagenFile(filepath)
if audio is not None and audio.info is not None:
return audio.info.length
elif ext in VIDEO_EXTS:
# Use OpenCV to get video duration
cap = cv2.VideoCapture(str(filepath))
if not cap.isOpened():
return 0.0
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
cap.release()
if fps > 0:
return frame_count / fps
else:
return 0.0
except Exception:
return 0.0
return 0.0
def scan_dir(path: Path, indent: str = "", is_last: bool = True, csv_rows=None, parent_path=""):
"""Recursively scan directory, print tree and duration, return total duration. Optionally collect csv_rows."""
total_duration = 0.0
entries = sorted(list(path.iterdir()), key=lambda x: (x.is_file(), x.name.lower()))
for idx, entry in enumerate(entries):
last = (idx == len(entries) - 1)
prefix = indent + ("└── " if last else "├── ")
rel_path = str(entry.relative_to(parent_path)) if parent_path else str(entry)
if entry.is_dir():
sub_duration = scan_dir(entry, indent + (" " if last else "│ "), last, csv_rows, parent_path or str(path.parent))
print(f"{prefix}{entry.name}/ [total: {format_duration(sub_duration)}]")
if csv_rows is not None:
csv_rows.append([rel_path + '/', 'dir', f"{sub_duration:.2f}"])
total_duration += sub_duration
elif entry.suffix.lower() in MEDIA_EXTS:
duration = get_media_duration(entry)
print(f"{prefix}{entry.name} [{format_duration(duration)}]")
if csv_rows is not None:
csv_rows.append([rel_path, 'file', f"{duration:.2f}"])
total_duration += duration
else:
# Not a media file, skip
continue
return total_duration
def format_duration(seconds):
if seconds < 1e-2:
return "0s"
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
if h:
return f"{h}h{m}m{s}s"
elif m:
return f"{m}m{s}s"
else:
return f"{s}s"
def main():
parser = argparse.ArgumentParser(description="Scan media files and print tree with durations.")
parser.add_argument('directory', nargs='?', help='Directory to scan')
parser.add_argument('--csv', dest='csv', help='Export result to CSV file')
args = parser.parse_args()
if not args.directory:
print("Usage: python media_tree.py <directory> [--csv output.csv]")
return
root = Path(args.directory)
if not root.is_dir():
print(f"{root} is not a directory.")
return
print(f"{root.name}/")
csv_rows = [] if args.csv else None
total = scan_dir(root, csv_rows=csv_rows, parent_path=str(root.parent))
print(f"\nTotal duration: {format_duration(total)}")
if args.csv:
with open(args.csv, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['path', 'type', 'duration_seconds'])
writer.writerows(csv_rows)
print(f"CSV exported to {args.csv}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment