Compress .MOV files
Category: Development
Language: python
Created by: lvcaspacifico Created At: Mar 10, 2024
Updated: Mar 10, 2024
Observations
Please pay attention to the comment section in the code, it contains instructions on how to run the script.
Script
"""
Video compressor
----------------
Finds all .mov files recursively under a folder and compresses any that
are over a size threshold (default 90 MB) to bring them safely under
GitHub's 100 MB hard limit.
Output goes to a "compressed/" subfolder mirroring the original structure.
Originals are never touched.
Encoding: H.264 video + AAC audio inside a .mov container.
Uses CRF (Constant Rate Factor) — quality-based encoding, not a fixed
bitrate. CRF 23 is FFmpeg's default and looks great for social/web video.
Lower = better quality + bigger file. Raise to 26-28 if files are still too big.
Requirements:
FFmpeg installed and available on PATH
https://ffmpeg.org/download.php (Windows: get the build from gyan.dev)
Usage:
python compress_videos.py # processes current folder
python compress_videos.py "C:/path/to/folder" # processes specific folder
"""
import os
import sys
import shutil
import subprocess
SIZE_THRESHOLD_MB = 19 # only compress files larger than this
TARGET_MAX_MB = 19 # aim to stay under this (used for fallback)
CRF = 24 # quality: 18 (great) → 28 (smaller/lower)
OUTPUT_DIR_NAME = "compressed"
# ── Helpers ───────────────────────────────────────────────────────────────────
def check_ffmpeg():
if not shutil.which('ffmpeg'):
print("""
Error: FFmpeg not found on PATH.
Install it from: https://ffmpeg.org/download.php
Windows: download the 'full' build from https://www.gyan.dev/ffmpeg/builds/
Extract it, then add the bin/ folder to your system PATH.
Restart your terminal after doing this.
""")
sys.exit(1)
def find_mov_files(folder: str):
results = []
for root, _, files in os.walk(folder):
# Don't recurse into our own output folder
if OUTPUT_DIR_NAME in root.split(os.sep):
continue
for f in files:
if f.lower().endswith('.mov'):
results.append(os.path.join(root, f))
return results
def file_mb(path: str) -> float:
return os.path.getsize(path) / (1024 * 1024)
def compress(src: str, dst: str, crf: int) -> bool:
os.makedirs(os.path.dirname(dst), exist_ok=True)
cmd = [
'ffmpeg',
'-i', src,
'-c:v', 'libx264', # H.264 video
'-crf', str(crf), # quality factor
'-preset', 'slow', # slower = better compression at same quality
'-c:a', 'aac', # AAC audio
'-b:a', '128k', # audio bitrate
'-movflags', '+faststart', # web-friendly: moov atom at front
'-y', # overwrite output without asking
dst
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f" [ffmpeg error]\n{result.stderr[-800:]}")
return False
return True
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
folder = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
folder = os.path.abspath(folder)
if not os.path.isdir(folder):
print(f"Error: '{folder}' is not a valid directory.")
sys.exit(1)
check_ffmpeg()
mov_files = find_mov_files(folder)
if not mov_files:
print(f"No .mov files found under: {folder}")
sys.exit(0)
out_root = os.path.join(folder, OUTPUT_DIR_NAME)
# Split into needs-compression vs small-enough
to_compress = [f for f in mov_files if file_mb(f) > SIZE_THRESHOLD_MB]
small = [f for f in mov_files if file_mb(f) <= SIZE_THRESHOLD_MB]
print(f"Source: {folder}")
print(f"Output: {out_root}")
print(f"Found {len(mov_files)} .mov file(s) — {len(to_compress)} need compression, {len(small)} already small enough\n")
ok = failed = skipped = 0
# ── Files that need compressing ───────────────────────────────────────────
if to_compress:
print("── Compressing ──────────────────────────────────────")
for src in to_compress:
rel = os.path.relpath(src, folder)
dst = os.path.join(out_root, rel)
name = os.path.basename(src)
size_in = file_mb(src)
if os.path.exists(dst):
print(f" SKIP {name} (already in output folder)")
skipped += 1
continue
print(f" ... {name} ({size_in:.1f} MB) CRF {CRF}")
if compress(src, dst, CRF):
size_out = file_mb(dst)
saved = size_in - size_out
flag = " ⚠ still over 100MB — raise CRF" if size_out > 100 else ""
print(f" OK {name} → {size_out:.1f} MB (saved {saved:.1f} MB){flag}")
ok += 1
else:
print(f" FAIL {name}")
failed += 1
# ── Files that are already small — just copy ──────────────────────────────
if small:
print("\n── Already small enough (copying as-is) ─────────────")
for src in small:
rel = os.path.relpath(src, folder)
dst = os.path.join(out_root, rel)
name = os.path.basename(src)
if os.path.exists(dst):
print(f" SKIP {name} (already in output folder)")
skipped += 1
continue
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
print(f" OK {name} ({file_mb(src):.1f} MB) copied")
ok += 1
print(f"""
Done. {ok} file(s) processed
{skipped} skipped
{failed} failed
""")
if failed:
print("Tip: re-run the script — failed files will be retried.")
if __name__ == '__main__':
main()