import os import subprocess import argparse def get_video_duration(input_file): """ Get the duration of a video file using ffprobe. ffprobe is a command-line tool used to analyze multimedia streams. In this function: - `-v error`: Limits output to only errors. - `-select_streams v:0`: Selects the first video stream. - `-show_entries format=duration`: Outputs only the video duration. - `-of default=noprint_wrappers=1:nokey=1`: Formats the output to display only the duration value. """ try: result = subprocess.run( [ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", input_file, ], capture_output=True, text=True, check=True, ) return float(result.stdout.strip()) except (subprocess.CalledProcessError, ValueError) as e: raise RuntimeError(f"Failed to get duration for {input_file}: {e}") def process_folder(input_dir, output_dir, trim_start, trim_end, supported_formats): """Process each folder in the input directory and trim video files.""" os.makedirs(output_dir, exist_ok=True) for folder in os.listdir(input_dir): folder_path = os.path.join(input_dir, folder) if not os.path.isdir(folder_path): continue for file in os.listdir(folder_path): if file.endswith(tuple(supported_formats)): input_file = os.path.join(folder_path, file) # Extract episode number or default to folder name # Expected folder naming convention: 'SeriesName #EpisodeNumber - Description' # If '#' is missing, the folder name will be used as the episode identifier if "#" in folder: try: episode = folder.split("#")[1].strip().split(" - ")[0] except IndexError: episode = folder.replace(" ", "_") else: episode = folder.replace(" ", "_") output_ext = os.path.splitext(file)[1] output_file = os.path.join(output_dir, f"ep{episode}{output_ext}") try: duration = get_video_duration(input_file) except RuntimeError as e: print(e) continue if trim_start >= duration: print( f"Skipping {input_file}: trim-start ({trim_start}s) is greater than or equal to video duration ({duration}s)." ) continue if duration - trim_end <= trim_start: print( f"Skipping {input_file}: Invalid trim range (start: {trim_start}s, end: {trim_end}s, duration: {duration}s)." ) continue end_time = duration - trim_end # FFmpeg command to trim the video ffmpeg_command = [ "ffmpeg", # Command to run FFmpeg "-i", input_file, # Input file path "-ss", f"{trim_start:.2f}", # Start trimming from this timestamp (in seconds) "-to", f"{end_time:.2f}", # End trimming at this timestamp (in seconds) "-c", "copy", # Copy codec to avoid re-encoding output_file, # Output file path ] try: subprocess.run(ffmpeg_command, check=True) print(f"Trimmed and saved: {output_file}") except subprocess.CalledProcessError as e: print(f"Failed to process {input_file}: {e}") def main(): """Main function to parse arguments and run the script.""" parser = argparse.ArgumentParser( description="Trim video files in folders and save them with episode numbers." ) parser.add_argument( "--input", type=str, default=".", help="Input directory containing folders with video files", ) parser.add_argument( "--output", type=str, default="output", help="Output directory for trimmed videos", ) parser.add_argument( "--trim-start", type=float, default=46.0, help="Number of seconds to trim from the start", ) parser.add_argument( "--trim-end", type=float, default=16.0, help="Number of seconds to trim from the end", ) parser.add_argument( "--formats", type=str, default=".mp4,.webm,.mkv", help="Comma-separated list of supported video formats eg, '.mp4,.webm,.mkv' ", ) args = parser.parse_args() supported_formats = [fmt.strip() for fmt in args.formats.split(",")] process_folder( args.input, args.output, args.trim_start, args.trim_end, supported_formats ) if __name__ == "__main__": main()