Created
May 16, 2025 08:17
-
-
Save wesleyel/0f2d64cf2cece5a89e639a316ee22ad6 to your computer and use it in GitHub Desktop.
Obtain media duratin like tree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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