Skip to content

Instantly share code, notes, and snippets.

@thepushkarp
Last active October 29, 2024 08:50
Show Gist options
  • Save thepushkarp/4df4a3d6a5221e0604ee42d918aa7a9c to your computer and use it in GitHub Desktop.
Save thepushkarp/4df4a3d6a5221e0604ee42d918aa7a9c to your computer and use it in GitHub Desktop.
Video File Finder and Duration Analyzer
#!/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