#!/usr/bin/env python3
# inDEXdown (power user edition) by @dexdigi - whatupdex.com
# Downloads videos with source attribution logging. Run with --help for usage.
import subprocess
import sys
import os
import csv
import re
import json
from datetime import datetime
from pathlib import Path
def get_output_dir():
home = Path.home()
if sys.platform == "darwin":
d = home / "Movies" / "inDEXdown"
else:
d = home / "Videos" / "inDEXdown"
d.mkdir(parents=True, exist_ok=True)
return d
def detect_platform(url):
if "tiktok.com" in url:
return "tiktok"
if "instagram.com" in url:
return "instagram"
if "twitter.com" in url or "x.com" in url:
return "twitter"
if "youtube.com" in url or "youtu.be" in url:
return "youtube"
return "other"
def extract_instagram_handle(url):
try:
from urllib.parse import urlparse
parsed = urlparse(url)
parts = [p for p in parsed.path.split("/") if p]
if len(parts) >= 2 and parts[1] in ("p", "reel", "tv"):
return parts[0]
except Exception:
pass
return None
def sanitize_handle(handle):
return re.sub(r"[^a-zA-Z0-9._-]", "", handle)
def sanitize_title(title, max_len=30):
safe = re.sub(r"[^a-zA-Z0-9 .,!?'()-]", "", title[:max_len])
safe = re.sub(r" +", " ", safe).strip()
return safe if safe else "video"
def extract_names(platform, url, meta):
uploader_id = meta.get("uploader_id") or ""
channel = meta.get("channel") or ""
uploader = meta.get("uploader") or ""
video_id = meta.get("id", "unknown")
title = meta.get("title", "video")
if platform == "tiktok":
handle = uploader or video_id
display_name = channel or handle
elif platform == "instagram":
ig_handle = extract_instagram_handle(url)
handle = ig_handle or channel or video_id
display_name = uploader or handle
elif platform == "twitter":
handle = uploader_id or video_id
if handle.startswith("@"):
handle = handle[1:]
display_name = uploader or handle
prefix = f"{display_name} - "
if title.startswith(prefix):
title = title[len(prefix):]
else: # youtube, other
if uploader_id and uploader_id.startswith("@"):
handle = uploader_id[1:]
elif channel and channel != "NA":
handle = channel
elif uploader and uploader != "NA":
handle = uploader
else:
handle = video_id
display_name = uploader or handle
if not handle or handle == "NA":
handle = video_id
if not display_name or display_name == "NA":
display_name = handle
handle = sanitize_handle(handle)
if not handle:
handle = video_id
meta["title"] = title
return handle, display_name
def build_filename(handle, title, video_id, upload_date, ext="mp4"):
safe_title = sanitize_title(title)
return f"@{handle} - {safe_title} [{video_id}] ({upload_date}).{ext}"
def neutralize_csv(value):
if value and value[0] in "=+-@":
return "'" + value
return value
def log_to_csv(output_dir, handle, display_name, title, url, upload_date, video_id, filename):
csv_path = output_dir / "inDEXdown_log.csv"
header = ["download_date", "username", "display_name", "title", "url", "upload_date", "video_id", "filename"]
if csv_path.exists():
content = csv_path.read_text(encoding="utf-8")
if f",{video_id}," in content:
print(f" Already logged: {video_id}")
return
write_header = not csv_path.exists()
with open(csv_path, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
if write_header:
writer.writerow(header)
writer.writerow([
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
f"@{handle}",
neutralize_csv(display_name),
neutralize_csv(title),
url,
upload_date,
video_id,
filename,
])
def check_actual_codec(filepath):
try:
result = subprocess.run(
["ffmpeg", "-i", filepath, "-hide_banner"],
capture_output=True, text=True, timeout=10
)
return "Video: h264" in result.stderr
except Exception:
return False
def fetch_metadata(url):
print("Fetching video info...")
result = subprocess.run(
["yt-dlp", "--no-playlist", "--no-warnings", "--dump-single-json", url],
capture_output=True, text=True, timeout=120
)
if result.returncode != 0:
print(f"Error: couldn't get video info. Check the URL and try again.")
sys.exit(1)
return json.loads(result.stdout)
def download(url, quality="default", output_dir=None):
if output_dir is None:
output_dir = get_output_dir()
else:
output_dir = Path(output_dir).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
meta = fetch_metadata(url)
video_id = meta.get("id", "unknown")
title = meta.get("title", "video")
upload_date_raw = meta.get("upload_date", "")
upload_date = f"{upload_date_raw[:4]}-{upload_date_raw[4:6]}-{upload_date_raw[6:8]}" if len(upload_date_raw) == 8 else "unknown"
duration = meta.get("duration", 0)
vcodec = meta.get("vcodec", "unknown")
platform = detect_platform(url)
handle, display_name = extract_names(platform, url, meta)
title = meta["title"] # may have been modified by extract_names
generic_prefixes = ["Video by ", "Photo by ", "Reel by ", "TikTok video #"]
is_generic = any(title.startswith(p) for p in generic_prefixes) or (title.startswith("Video ") and len(title) < 12)
if is_generic:
desc = meta.get("description", "")
if desc and desc != "NA":
title = desc.replace("\n", " ")[:100]
else:
title = f"{handle} post"
filename = build_filename(handle, title, video_id, upload_date)
output_path = output_dir / filename
is_audio_only = vcodec == "none"
if is_audio_only:
fmt = "bestaudio[ext=mp3]/bestaudio/best"
elif quality == "highest":
fmt = "bestvideo+bestaudio/best"
else:
fmt = "bestvideo[vcodec^=avc1]+bestaudio/bestvideo+bestaudio/best"
if is_audio_only:
filename = build_filename(handle, title, video_id, upload_date, ext="mp3")
output_path = output_dir / filename
output_template = str(output_path).replace(".mp4", ".%(ext)s").replace(".mp3", ".%(ext)s")
print(f"Downloading: {title[:50]}")
dl_cmd = [
"yt-dlp",
"--no-playlist", "--no-mtime", "--restrict-filenames",
"--add-metadata", "--newline",
"-f", fmt,
"-o", output_template,
url,
]
if not is_audio_only:
dl_cmd.insert(-1, "--merge-output-format")
dl_cmd.insert(-1, "mp4")
dl_cmd.insert(-1, "--postprocessor-args")
dl_cmd.insert(-1, "ffmpeg:-c:v copy -c:a aac -b:a 192k -ac 2 -ar 48000")
dl_result = subprocess.run(dl_cmd)
if dl_result.returncode != 0:
print("Download failed.")
sys.exit(1)
downloaded = None
for f in output_dir.iterdir():
if video_id in f.name and not f.name.endswith(".part"):
downloaded = f
break
if not downloaded:
print("Error: download seemed to succeed but can't find the file.")
sys.exit(1)
filepath = str(downloaded)
if quality == "default" and downloaded.suffix in (".mp4", ".mkv", ".webm"):
is_h264 = check_actual_codec(filepath)
if not is_h264:
print("Converting to h264...")
tmp_output = filepath.replace(".mp4", "_h264.mp4")
encode_result = subprocess.run([
"ffmpeg",
"-i", filepath,
"-c:v", "libx264", "-preset", "fast", "-crf", "18",
"-c:a", "aac", "-b:a", "192k", "-ac", "2", "-ar", "48000",
"-y", tmp_output,
])
if encode_result.returncode == 0:
os.remove(filepath)
os.rename(tmp_output, filepath)
else:
print("Warning: h264 conversion failed, keeping original.")
try:
os.remove(tmp_output)
except OSError:
pass
log_to_csv(output_dir, handle, display_name, title, url, upload_date, video_id, downloaded.name)
print(f"Done! Saved to {output_dir}")
def main():
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
print("inDEXdown (power user edition) by @dexdigi")
print()
print("Usage:")
print(" python3 inDEXdown.py \"<url>\" Default quality (h264)")
print(" python3 inDEXdown.py \"<url>\" --highest Highest quality")
print(" python3 inDEXdown.py \"<url>\" --output ~/Downloads Custom output directory")
print()
print("Requirements: yt-dlp and ffmpeg must be installed and in your PATH.")
print(" pip install yt-dlp")
print(" ffmpeg: https://ffmpeg.org/download.html")
sys.exit(0)
url = sys.argv[1]
quality = "highest" if "--highest" in sys.argv else "default"
output_dir = None
if "--output" in sys.argv:
idx = sys.argv.index("--output")
if idx + 1 < len(sys.argv):
output_dir = sys.argv[idx + 1]
else:
print("Error: --output requires a directory path.")
sys.exit(1)
for dep in ("yt-dlp", "ffmpeg"):
try:
subprocess.run([dep, "--version"], capture_output=True, timeout=10)
except FileNotFoundError:
print(f"Error: {dep} not found. Install it first."); sys.exit(1)
download(url, quality, output_dir)
if __name__ == "__main__":
main()