Variable Frame-Rate Directory Scan [Python]

Not sure how useful this will be, in the same way I’m not sure how useful the Resync script will be, but I figured since I made it for myself I’d upload here as well. I created a full directory scanner that can tell you which video files are VFR/Variable Frame-Rate and which are CFR. If you don’t know what that is read about it here:

There are two versions.

Lightweight: Scans just the metadata for information that indicates VFR. Can run on thousands of files relatively quickly. NOT super accurate, and will definitely miss some VFR files because it doesn’t actually scan any of the video frames.

Hardcore: Scans each frame of every video in the directory to determine if the video is VFR. Pretty much 100% accurate, but super slow and resource intensive. Good for pointing at a small folder of videos (maybe videos you just downloaded?) that you want to be certain are/are-not VFR.

You need python installed and ffmpeg on your path… blah blah blah

Here they are:

Lightweight

import subprocess
from pathlib import Path
import tkinter as tk
from tkinter import filedialog


def get_directory_path():
  root = tk.Tk()
  root.withdraw()
  root.attributes("-topmost", True)
  print("Opening file explorer... Select a directory containing video files.")
  path = filedialog.askdirectory(title="Select Directory to Scan")
  return Path(path) if path else None


def check_vfr(video_path: Path):
  cmd = [
      "ffprobe",
      "-v",
      "error",
      "-select_streams",
      "v:0",
      "-show_entries",
      "stream=avg_frame_rate,r_frame_rate",
      "-of",
      "default=noprint_wrappers=1:nokey=1",
      str(video_path),
  ]

  try:
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
    lines = [line.strip() for line in result.stdout.strip().splitlines()]

    if len(lines) < 2:
      return None

    avg_str, r_str = lines[0], lines[1]

    def parse_frac(f):
      num, den = map(int, f.split("/")) if "/" in f else (int(float(f)), 1)
      return num, den

    avg_num, avg_den = parse_frac(avg_str)
    r_num, r_den = parse_frac(r_str)

    is_vfr = (avg_str != r_str) or any(val > 100000 for val in (avg_num, avg_den, r_num, r_den))
    return is_vfr

  except Exception:
    return None


def scan_directory(dir_path: Path):
  if not dir_path or not dir_path.exists():
    print("You didn't fucking pick anything. Exiting.")
    return

  video_extensions = {".mp4", ".mkv", ".avi", ".mov", ".flv", ".webm", ".m4v"}
  video_files = [f for f in dir_path.rglob("*") if f.suffix.lower() in video_extensions]

  if not video_files:
    print(f"\nNo supported video files found in: {dir_path}")
    return

  print(f"\nScanning directory: {dir_path}")
  print(f"Found {len(video_files)} video file(s). Processing...\n")

  vfr_results = []
  cfr_count = 0
  error_count = 0

  for idx, video_path in enumerate(video_files, 1):
    relative_path = video_path.relative_to(dir_path)
    print(f"[{idx}/{len(video_files)}] Checking: {relative_path}")

    vfr_status = check_vfr(video_path)

    if vfr_status is True:
      vfr_results.append(relative_path)
    elif vfr_status is False:
      cfr_count += 1
    else:
      error_count += 1

  print("\n" + "=" * 50)
  print("--- Beep Bopp, SCAN RESULTS ---")
  print("=" * 50)

  if vfr_results:
    print(f"\nThese are FUCKING VFR Videos ({len(vfr_results)} found):")
    for vfr_file in vfr_results:
      print(f"  - {vfr_file}")
  else:
    print("\nResult:Congratulations! No VFR videos found. All checked files are CFR (Constant Frame Rate) or unreadable. Yipeeee!")

  print(f"\nSummary:")
  print(f"  - Total Scanned: {len(video_files)}")
  print(f"  - VFR Flagged:   {len(vfr_results)}")
  print(f"  - CFR (Wahoo and shit):   {cfr_count}")
  print(f"  - Errors/Failed: {error_count}")


if __name__ == "__main__":
  scan_directory(get_directory_path())
  input("\nPress Enter to exit...")

:flexed_biceps:Hardcore :hammer_and_wrench:

Reminder, don’t run this version on 3000 videos because it will take till the heat death of the universe to scan them.

import re
import subprocess
from pathlib import Path
import tkinter as tk
from tkinter import filedialog


def get_directory_path():
  root = tk.Tk()
  root.withdraw()
  root.attributes("-topmost", True)
  print("Opening file explorer... Select a directory containing video files.")
  path = filedialog.askdirectory(title="Pick a fucking directory")
  return Path(path) if path else None


def analyze_video(video_path: Path):
  try:
    probe_cmd = [
        "ffprobe",
        "-v",
        "error",
        "-select_streams",
        "v:0",
        "-show_entries",
        "stream=avg_frame_rate,r_frame_rate",
        "-of",
        "default=noprint_wrappers=1:nokey=1",
        str(video_path),
    ]
    probe_res = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
    lines = [line.strip() for line in probe_res.stdout.strip().splitlines()]

    avg_str = lines[0] if len(lines) > 0 else "30/1"

    def parse_frac(f):
      return tuple(map(int, f.split("/"))) if "/" in f else (int(float(f)), 1)

    avg_num, avg_den = parse_frac(avg_str)
    avg_fps = avg_num / avg_den if avg_den != 0 else 0.0

    cmd = ["ffmpeg", "-i", str(video_path), "-vf", "vfrdet", "-f", "null", "-"]

    result = subprocess.run(cmd, capture_output=True, text=True)
    stderr_output = result.stderr

    match = re.search(r"VFR:\s*([0-9.]+)", stderr_output)

    if not match:
      return None, avg_fps

    vfr_ratio = float(match.group(1))
    is_vfr = vfr_ratio > 0.0
    return is_vfr, avg_fps

  except Exception:
    return None, 0.0


def scan_directory(dir_path: Path):
  if not dir_path or not dir_path.exists():
    print("You didn't fucking pick anything. Exiting.")
    return

  video_extensions = {".mp4", ".mkv", ".avi", ".mov", ".flv", ".webm", ".m4v"}
  video_files = [f for f in dir_path.rglob("*") if f.suffix.lower() in video_extensions]

  if not video_files:
    print(f"\nNo supported video files found in: {dir_path}")
    return

  print(f"\nScanning directory: {dir_path}")
  print(f"Found {len(video_files)} video file(s). Processing with deep vfrdet analysis...")
  print("This is gonna take a while. Place your thumb up your ass while you wait :)\n")

  vfr_results = []
  cfr_count = 0
  error_count = 0

  for idx, video_path in enumerate(video_files, 1):
    relative_path = video_path.relative_to(dir_path)
    print(f"[{idx}/{len(video_files)}] Analyzing: {relative_path}")

    vfr_status, avg_fps = analyze_video(video_path)

    if vfr_status is True:
      vfr_results.append((relative_path, avg_fps))
    elif vfr_status is False:
      cfr_count += 1
    else:
      error_count += 1

  print("\n" + "=" * 50)
  print("--- HARDCORE SCAN RESULTS ---")
  print("=" * 50)

  if vfr_results:
    print(f"\nFlagged VFR Videos ({len(vfr_results)} found):")
    for vfr_file, fps in vfr_results:
      print(f"  - {vfr_file} (Avg FPS: {fps:.2f})")
  else:
    print("\nResult: Congrats. No VFR videos found. All checked files are CFR (Constant Frame Rate). That's neat.")

  print(f"\nSummary:")
  print(f"  - Total Scanned: {len(video_files)}")
  print(f"  - VFR Flagged:   {len(vfr_results)}")
  print(f"  - CFR (Clean):   {cfr_count}")
  print(f"  - Errors/Failed: {error_count}")


if __name__ == "__main__":
  scan_directory(get_directory_path())
  input("\nPress Enter to exit...")

Download the two versions here:

1 Like