Last active
October 29, 2024 08:50
-
-
Save thepushkarp/4df4a3d6a5221e0604ee42d918aa7a9c to your computer and use it in GitHub Desktop.
Video File Finder and Duration Analyzer
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 python3 | |
""" | |
Video File Finder and Duration Analyzer. | |
This script finds video files in a directory (and its subdirectories) and displays | |
their durations in a formatted table. It can filter videos based on minimum duration. | |
Dependencies: | |
- Python 3.6+ | |
- prettytable (`pip install prettytable`) | |
- ffmpeg/ffprobe: | |
- Windows: Download from https://ffmpeg.org/download.html | |
- macOS: `brew install ffmpeg` | |
- Ubuntu/Debian: `sudo apt-get install ffmpeg` | |
- CentOS/RHEL: `sudo yum install ffmpeg` | |
Basic usage: | |
python video_finder.py | |
Usage with options: | |
python video_finder.py -d /path/to/videos -m 01:30 -v | |
Options: | |
-d, --directory : Directory to search (default: current directory) | |
-m, --min-duration : Minimum duration filter in HH:MM format (e.g., 01:30) | |
-v, --verbose : Enable verbose output | |
Author: Generated by Claude | |
License: MIT | |
""" | |
import os | |
import subprocess | |
import argparse | |
from datetime import timedelta | |
from typing import List, Tuple, Optional | |
from pathlib import Path | |
from prettytable import PrettyTable | |
class VideoFinder: | |
# Common video file extensions | |
VIDEO_EXTENSIONS = { | |
'.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', | |
'.m4v', '.mpeg', '.mpg', '.3gp', '.3g2', '.ts', '.mts' | |
} | |
def __init__(self, min_duration: Optional[str] = None, verbose: bool = False): | |
""" | |
Initialize VideoFinder with optional minimum duration filter. | |
Args: | |
min_duration (str, optional): Minimum duration in HH:MM format | |
verbose (bool): Whether to print detailed logs | |
""" | |
self.min_seconds = self._parse_duration(min_duration) if min_duration else 0 | |
self.verbose = verbose | |
@staticmethod | |
def _parse_duration(duration: str) -> int: | |
"""Convert HH:MM format to seconds.""" | |
try: | |
hours, minutes = map(int, duration.split(':')) | |
return hours * 3600 + minutes * 60 | |
except ValueError: | |
raise ValueError("Duration must be in HH:MM format (e.g., '01:30' for 1 hour 30 minutes)") | |
@staticmethod | |
def _format_duration(seconds: float) -> str: | |
"""Convert seconds to HH:MM:SS format.""" | |
return str(timedelta(seconds=int(seconds))) | |
def _get_video_duration(self, file_path: Path) -> Optional[float]: | |
""" | |
Get video duration using ffprobe. | |
Returns: | |
float or None: Duration in seconds if successful, None if failed | |
""" | |
try: | |
result = subprocess.run( | |
[ | |
"ffprobe", | |
"-v", "error", | |
"-select_streams", "v:0", | |
"-show_entries", "format=duration", | |
"-of", "default=noprint_wrappers=1:nokey=1", | |
str(file_path) | |
], | |
stdout=subprocess.PIPE, | |
stderr=subprocess.STDOUT, | |
timeout=30 # Timeout after 30 seconds | |
) | |
if result.returncode == 0: | |
duration = float(result.stdout.decode().strip()) | |
return duration | |
else: | |
if self.verbose: | |
print(f"Warning: Failed to get duration for {file_path}") | |
return None | |
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: | |
if self.verbose: | |
print(f"Error processing {file_path}: {str(e)}") | |
return None | |
except ValueError as e: | |
if self.verbose: | |
print(f"Error parsing duration for {file_path}: {str(e)}") | |
return None | |
def find_videos(self, directory: str) -> List[Tuple[str, str, str]]: | |
""" | |
Find all video files in the given directory and its subdirectories. | |
Returns: | |
List of tuples: (filename, duration, file_path) | |
""" | |
video_files = [] | |
directory_path = Path(directory) | |
if not directory_path.exists(): | |
raise FileNotFoundError(f"Directory not found: {directory}") | |
if self.verbose: | |
print(f"Searching for videos in: {directory_path.absolute()}") | |
for file_path in directory_path.rglob('*'): | |
if file_path.suffix.lower() in self.VIDEO_EXTENSIONS: | |
if self.verbose: | |
print(f"Processing: {file_path}") | |
duration = self._get_video_duration(file_path) | |
if duration is None: | |
continue | |
if duration >= self.min_seconds: | |
video_files.append(( | |
file_path.name, | |
self._format_duration(duration), | |
str(file_path.absolute()) | |
)) | |
return sorted(video_files, key=lambda x: x[1]) | |
def display_results(self, video_files: List[Tuple[str, str, str]]) -> None: | |
"""Display the results in a formatted table that adapts to terminal width.""" | |
if not video_files: | |
print("\nNo matching video files found.") | |
return | |
print(f"\nFound {len(video_files)} matching video files.") | |
# Get terminal width | |
try: | |
import shutil | |
terminal_width = shutil.get_terminal_size().columns | |
except: | |
# Default if can't get terminal width | |
terminal_width = 100 | |
table = PrettyTable() | |
table.field_names = ["Name", "Duration", "Path"] | |
table.align["Name"] = "l" | |
table.align["Duration"] = "r" | |
table.align["Path"] = "l" | |
# Calculate dynamic widths | |
# Duration is fixed (~8 chars for HH:MM:SS) | |
duration_width = 10 | |
# Name and Path share remaining space (minus margins and separators) | |
available_width = terminal_width - duration_width - 6 # 6 for borders and padding | |
name_width = int(available_width * 0.39) # 39% for name | |
path_width = int(available_width * 0.59) # 59% for path | |
# Set individual max widths | |
table._max_width = { | |
"Name": max(20, name_width), | |
"Duration": duration_width, | |
"Path": max(30, path_width) | |
} | |
# Helper function to truncate text | |
def truncate(text: str, max_length: int) -> str: | |
if len(text) <= max_length: | |
return text | |
return text[:max_length-3] + '...' | |
for file, duration, file_path in video_files: | |
table.add_row([ | |
truncate(file, table.max_width["Name"]), | |
duration, | |
truncate(file_path, table.max_width["Path"]) | |
]) | |
print(table) | |
def main(): | |
parser = argparse.ArgumentParser(description='Find video files and their durations.') | |
parser.add_argument('-d', '--directory', type=str, default='.', | |
help='Directory to search (default: current directory)') | |
parser.add_argument('-m', '--min-duration', type=str, | |
help='Minimum duration filter in HH:MM format (e.g., 01:30)') | |
parser.add_argument('-v', '--verbose', action='store_true', | |
help='Enable verbose output') | |
args = parser.parse_args() | |
try: | |
finder = VideoFinder(min_duration=args.min_duration, verbose=args.verbose) | |
video_files = finder.find_videos(args.directory) | |
finder.display_results(video_files) | |
except FileNotFoundError as e: | |
print(f"Error: {str(e)}") | |
return 1 | |
except ValueError as e: | |
print(f"Error: {str(e)}") | |
return 1 | |
except KeyboardInterrupt: | |
print("\nOperation cancelled by user.") | |
return 1 | |
except Exception as e: | |
print(f"An unexpected error occurred: {str(e)}") | |
return 1 | |
return 0 | |
if __name__ == "__main__": | |
exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment