From 6424dd516bf11daaa4622480eb06bd85fa4128df Mon Sep 17 00:00:00 2001 From: Thyfwx <131562370+Thyfwx@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:38:01 -0500 Subject: [PATCH] Add TikTok account creation date lookup (no API key) and terminal UI Reads the createTime embedded in TikTok's public profile page, so you can find when an account was created from just a username, with no API key to set up and no signup. Adds: - tiktok_created.py: CLI lookup (username, @handle, profile URL, video URL, or numeric id) - tiktok_ui.py: Rich terminal UI - start.sh, start.bat, TokIntel.command, TokIntel.app: launchers - a README section documenting it Purely additive: the original RapidAPI tool is unchanged. Credit to Victor Bancayan / Hack Underway for TokIntel. --- .gitignore | 3 + README.md | 25 ++ TokIntel.app/Contents/Info.plist | 22 ++ TokIntel.app/Contents/MacOS/TokIntel | 8 + TokIntel.command | 5 + start.bat | 14 ++ start.sh | 24 ++ tiktok_created.py | 332 +++++++++++++++++++++++++++ tiktok_ui.py | 147 ++++++++++++ 9 files changed, 580 insertions(+) create mode 100644 TokIntel.app/Contents/Info.plist create mode 100755 TokIntel.app/Contents/MacOS/TokIntel create mode 100755 TokIntel.command create mode 100644 start.bat create mode 100755 start.sh create mode 100644 tiktok_created.py create mode 100644 tiktok_ui.py diff --git a/.gitignore b/.gitignore index 68597ea..76cf9aa 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ __pycache__/ # Research Reports (DO NOT UPLOAD) reports/ + +# macOS Finder metadata +.DS_Store diff --git a/README.md b/README.md index 6c6c9ba..646930c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,31 @@ - πŸ“‚ **Batch Processing:** Scan lists of targets via TXT file. - πŸ’‘ **Automatic API Setup:** Interactive onboarding on the first run. +--- + +## πŸ†“ Account creation date, no API key needed + +Want to know when a TikTok account was created without setting up an API key? `tiktok_created.py` reads the `createTime` that TikTok embeds in the public profile page, so a username is all you need. + +```bash +python3 tiktok_created.py charlidamelio +python3 tiktok_created.py @nasa https://www.tiktok.com/@zachking +``` + +You get the account creation date plus the basics (followers, likes, bio, verified, private). It also decodes a video URL or ID to its upload time, using the snowflake timestamp inside the id (`id >> 32`). Reports save to `reports/` as JSON and TXT, the same as the main tool. + +### Start the app (interactive UI) + +The UI shows a TikTok style banner and a prompt where you type usernames one after another. + +- **macOS:** double click `TokIntel.app` (or `TokIntel.command`) +- **Windows:** double click `start.bat` +- **Any terminal (macOS or Linux):** run `./start.sh` + +The launcher creates its own virtual environment and installs `requests`, `colorama`, and `rich` on first run, so there is nothing to set up by hand. + +_This no-key lookup mode is an addition to TokIntel by Victor Bancayan (Hack Underway), contributed by [@Thyfwx](https://github.com/Thyfwx)._ + ## πŸ“Œ Prerequisites - Python 3.8+ diff --git a/TokIntel.app/Contents/Info.plist b/TokIntel.app/Contents/Info.plist new file mode 100644 index 0000000..add072a --- /dev/null +++ b/TokIntel.app/Contents/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleName + TokIntel + CFBundleDisplayName + TokIntel + CFBundleIdentifier + com.tokintel.launcher + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + CFBundleExecutable + TokIntel + CFBundlePackageType + APPL + LSMinimumSystemVersion + 10.13 + + diff --git a/TokIntel.app/Contents/MacOS/TokIntel b/TokIntel.app/Contents/MacOS/TokIntel new file mode 100755 index 0000000..ae2226e --- /dev/null +++ b/TokIntel.app/Contents/MacOS/TokIntel @@ -0,0 +1,8 @@ +#!/bin/sh +# Double-clicking TokIntel.app runs this. It finds the repo (the app bundle +# lives at /TokIntel.app/Contents/MacOS/TokIntel) and opens start.sh in +# Terminal, which launches the UI. App bundles always launch, never open in an +# editor, so this is the reliable double-click path on macOS. +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../../.." && pwd)" +open -a Terminal "$REPO/start.sh" diff --git a/TokIntel.command b/TokIntel.command new file mode 100755 index 0000000..632f088 --- /dev/null +++ b/TokIntel.command @@ -0,0 +1,5 @@ +#!/bin/sh +# Double-click this file in Finder (macOS) to launch TokIntel in Terminal. +# It just hands off to start.sh next to it. +DIR="$(cd "$(dirname "$0")" && pwd)" +exec "$DIR/start.sh" diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..9f833f9 --- /dev/null +++ b/start.bat @@ -0,0 +1,14 @@ +@echo off +REM Windows double-click launcher for TokIntel. +REM Sets up its own virtual environment and dependencies on first run. +cd /d "%~dp0" + +if not exist "venv\Scripts\python.exe" ( + echo First run - setting up a local Python environment... + python -m venv venv +) + +venv\Scripts\python.exe -c "import importlib.util as u,sys;sys.exit(0 if all(u.find_spec(m) for m in ('requests','colorama','rich')) else 1)" 2>nul || venv\Scripts\python.exe -m pip install -q requests colorama rich + +venv\Scripts\python.exe tiktok_ui.py %* +pause diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..76bdf04 --- /dev/null +++ b/start.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# TokIntel β€” TikTok account-creation lookup UI. +# ./start.sh run from a terminal +# TokIntel.command double-click in Finder (macOS) +# Self-locating (works wherever the repo lives) and self-healing (sets up its +# own venv + dependencies on first run). + +DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$DIR" || exit 1 +PY="$DIR/venv/bin/python" + +if [ ! -x "$PY" ]; then + echo "First run β€” setting up a local Python environment…" + python3 -m venv "$DIR/venv" || { echo "python3 is required."; exit 1; } + PY="$DIR/venv/bin/python" +fi + +# Install dependencies only if any are missing. +"$PY" - <<'CHECK' 2>/dev/null || "$PY" -m pip install -q requests colorama rich +import importlib.util as u, sys +sys.exit(0 if all(u.find_spec(m) for m in ("requests", "colorama", "rich")) else 1) +CHECK + +exec "$PY" "$DIR/tiktok_ui.py" "$@" diff --git a/tiktok_created.py b/tiktok_created.py new file mode 100644 index 0000000..431aff9 --- /dev/null +++ b/tiktok_created.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +""" +tiktok_created.py: when was a TikTok account created? (no API key needed) + +TikTok's public profile page embeds the account's `createTime` (and a lot of +other profile data) in a JSON blob. We read that directly, so there is no API +key to set up. Works from a username, an @handle, or a profile URL. + +Bonus: a TikTok *video* URL/ID decodes to its upload time via the Snowflake +timestamp baked into video IDs (id >> 32). (Note: many *user* IDs are NOT +snowflakes, so we trust the embedded createTime for accounts, not id>>32.) + +Usage: + ./venv/bin/python tiktok_created.py --input charlidamelio + ./venv/bin/python tiktok_created.py --input https://www.tiktok.com/@nasa + ./venv/bin/python tiktok_created.py --input https://www.tiktok.com/@x/video/7076288989640055298 + ./venv/bin/python tiktok_created.py --file usernames.txt + +Part of TokIntel (https://github.com/HackUnderway/TokIntel) by Victor Bancayan / +Hack Underway. This account-lookup addition (no API key needed) was contributed +by @Thyfwx. +""" +import argparse +import json +import os +import re +import random +import string +import time +from datetime import datetime, UTC +from urllib.parse import quote + +import requests + +try: + from colorama import Fore, init + init(autoreset=True) +except Exception: # colorama is optional + class _Nope: + def __getattr__(self, _): return "" + Fore = _Nope() + +UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") +TIMEOUT = 20 +DELAY = 1.5 # be polite between requests + +REHYDRATION_RE = re.compile( + r'id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>(.*?)', re.S) + + +# ------------------------------------------------------------------ helpers +def fmt(ts): + try: + return datetime.fromtimestamp(int(ts), UTC).strftime('%Y-%m-%d %H:%M:%S UTC') + except Exception: + return None + + +def classify(value): + """Return ('video', video_id) | ('user', username) | ('id', number).""" + v = value.strip() + + vid = re.search(r'/video/(\d+)', v) + if vid: + return "video", vid.group(1) + + handle = re.search(r'tiktok\.com/@([\w.\-]+)', v) + if handle: + return "user", handle.group(1) + + if v.lstrip('@').replace('.', '').replace('_', '').replace('-', '').isalnum() and not v.isdigit(): + return "user", v.lstrip('@') + + if v.isdigit(): + return "id", v + + return "user", v.lstrip('@') + + +def decode_snowflake(num): + """id >> 32 -> upload/creation unix seconds (only valid for 64-bit snowflakes).""" + n = int(num) + if n < (1 << 40): # too small to be a snowflake β€” would decode to ~1970 + return None + return n >> 32 + + +# ------------------------------------------------------------------ fetchers +def fetch_user(username, session): + # quote() leaves valid username characters (letters, digits, _ . - ~) as-is + # but percent-encodes anything unusual, so a crafted target can't alter the + # request structure. The host and scheme are already a fixed literal prefix. + url = f"https://www.tiktok.com/@{quote(username, safe='')}" + r = session.get(url, timeout=TIMEOUT) + m = REHYDRATION_RE.search(r.text) + if not m: + return {"error": f"no_data (http_{r.status_code}, {len(r.text)}B) β€” rate-limited/blocked or layout changed"} + try: + scope = json.loads(m.group(1)).get("__DEFAULT_SCOPE__", {}) + except ValueError: + return {"error": "json_parse_failed"} + + detail = scope.get("webapp.user-detail") + if not detail: + return {"error": "user-detail missing β€” profile not returned (blocked, or no such page)"} + + status = detail.get("statusCode") + info = detail.get("userInfo", {}) + user = info.get("user", {}) + # statsV2 carries correct string counts; the legacy `stats` object stores + # 32-bit ints that overflow (negative likes) for accounts past ~2.1B, so + # prefer statsV2 and fall back to stats only if it's absent. + stats = info.get("statsV2") or info.get("stats") or {} + if not user: + return {"error": f"user not found (statusCode={status})"} + + def as_int(v): + try: + return int(v) + except (TypeError, ValueError): + return v + + ct = user.get("createTime") + uid = user.get("id") + id_est = decode_snowflake(uid) if uid and str(uid).isdigit() else None + + return { + "type": "account", + "username": user.get("uniqueId") or username, + "nickname": user.get("nickname"), + "account_created": fmt(ct) if ct else None, + "account_created_unix": ct, + "created_estimate_from_id": fmt(id_est) if id_est else None, + "user_id": uid, + "sec_uid": user.get("secUid"), + "verified": user.get("verified"), + "private": user.get("privateAccount"), + "bio": user.get("signature"), + "region": user.get("region"), + "followers": as_int(stats.get("followerCount")), + "following": as_int(stats.get("followingCount")), + "likes": as_int(stats.get("heartCount")), + "videos": as_int(stats.get("videoCount")), + } + + +def decode_video(video_id): + ts = decode_snowflake(video_id) + return { + "type": "video", + "video_id": video_id, + "uploaded": fmt(ts) if ts else None, + "note": "This is the VIDEO upload time, not the account creation date.", + } + + +def decode_id(num): + ts = decode_snowflake(num) + return { + "type": "raw_id", + "id": num, + "decoded": fmt(ts) if ts else None, + "note": ("Decoded from snowflake id>>32. Works for video IDs and " + "snowflake user IDs; small/legacy user IDs can't be decoded " + "this way β€” fetch the @username instead for createTime."), + } + + +# ------------------------------------------------------------------ reports +def reports_dir(): + if not os.path.exists("reports"): + os.makedirs("reports") + return "reports" + + +def save_reports(results, prefix): + rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + base = os.path.join(reports_dir(), f"created_{prefix}_{stamp}_{rand}") + + with open(base + ".json", "w", encoding="utf-8") as f: + json.dump(results, f, indent=4, ensure_ascii=False) + + with open(base + ".txt", "w", encoding="utf-8") as f: + f.write("TikTok account creation report\n" + "=" * 44 + "\n\n") + for r in results: + f.write(f"Target : {r['target']}\n") + d = r["data"] + if "error" in d: + f.write(f" ERROR: {d['error']}\n\n") + continue + if d.get("type") == "account": + f.write(f" Username : @{d.get('username')}\n") + f.write(f" Account created: {d.get('account_created')}\n") + f.write(f" Nickname : {d.get('nickname')}\n") + f.write(f" Verified : {d.get('verified')}\n") + f.write(f" Private : {d.get('private')}\n") + f.write(f" Followers : {d.get('followers')}\n") + f.write(f" Likes : {d.get('likes')}\n") + f.write(f" Videos : {d.get('videos')}\n") + f.write(f" Bio : {d.get('bio')}\n") + f.write(f" User ID : {d.get('user_id')}\n") + elif d.get("type") == "video": + f.write(f" Video uploaded : {d.get('uploaded')}\n") + f.write(f" (video id {d.get('video_id')})\n") + else: + f.write(f" Decoded id : {d.get('decoded')}\n") + f.write("\n") + return base + ".json", base + ".txt" + + +# ------------------------------------------------------------------ main +def new_session(): + s = requests.Session() + s.headers.update({"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"}) + return s + + +def lookup(target, session): + """Resolve one target to its result dict. Never raises.""" + kind, val = classify(target) + try: + if kind == "video": + return kind, decode_video(val) + if kind == "id": + return kind, decode_id(val) + return kind, fetch_user(val, session) + except Exception as e: + return kind, {"error": f"{type(e).__name__}: {e}"} + + +def show(data, indent=" "): + """Print a one-line human-readable result.""" + if "error" in data: + print(Fore.RED + f"{indent}⚠️ {data['error']}") + elif data.get("type") == "account": + if data.get("account_created"): + print(Fore.GREEN + f"{indent}πŸ“… created: {data['account_created']} " + + Fore.WHITE + f"(@{data['username']}, {data.get('followers')} followers)") + else: + print(Fore.YELLOW + f"{indent}🟑 profile found but no createTime; " + f"id-estimate: {data.get('created_estimate_from_id')}") + elif data.get("type") == "video": + print(Fore.GREEN + f"{indent}πŸ“… uploaded: {data['uploaded']}") + else: + print(Fore.GREEN + f"{indent}πŸ“… decoded: {data['decoded']}") + + +def run_batch(targets, session): + print(Fore.CYAN + f"\n[+] Targets: {len(targets)} (free mode β€” no API key)\n") + results = [] + for i, target in enumerate(targets, 1): + print(Fore.WHITE + f"[{i}/{len(targets)}] {target}") + kind, data = lookup(target, session) + show(data) + results.append({"target": target, "data": data}) + if kind == "user" and i < len(targets): + time.sleep(DELAY) + return results + + +def run_interactive(session): + print(Fore.CYAN + "\nπŸ”Ž TikTok creation-date lookup (free β€” no API key)") + print(Fore.WHITE + " Type a username, @handle, or profile/video URL.") + print(Fore.WHITE + " Press Enter on an empty line (or type 'q') to quit.\n") + results = [] + while True: + try: + entry = input("πŸ”Ž username> ").strip() + except (EOFError, KeyboardInterrupt): + print() + break + if not entry or entry.lower() in {"q", "quit", "exit"}: + break + _, data = lookup(entry, session) + show(data, indent=" ") + results.append({"target": entry, "data": data}) + return results + + +def main(): + p = argparse.ArgumentParser( + description="Get a TikTok account's creation date β€” free, no API key. " + "Run with no arguments for interactive mode.") + p.add_argument("targets", nargs="*", + help="one or more usernames / @handles / profile or video URLs") + p.add_argument("--input", help="a single target (same as a positional arg)") + p.add_argument("--file", help="text file with one target per line") + args = p.parse_args() + + # Command-line targets: zsh (unlike bash) does NOT strip inline '# comments', + # so a '#' token arrives as a literal arg. Treat it as start-of-comment and + # ignore it plus everything after β€” a pasted "user # note" stays clean. + cli = [] + for t in [*args.targets, *( [args.input] if args.input else [] )]: + if t.strip().startswith("#"): + break + cli.append(t) + + # File targets: skip blank lines and whole-line '#' comments. + file_lines = [] + if args.file: + with open(args.file, encoding="utf-8") as f: + file_lines = [ln.strip() for ln in f + if ln.strip() and not ln.strip().startswith("#")] + + # de-dupe in order, drop blanks + seen, targets = set(), [] + for t in [*cli, *file_lines]: + t = t.strip() + if not t or t in seen: + continue + seen.add(t) + targets.append(t) + + session = new_session() + + if targets: + results = run_batch(targets, session) + prefix = "single" if len(targets) == 1 else "batch" + else: + results = run_interactive(session) + prefix = "interactive" + + if results: + jp, tp = save_reports(results, prefix) + print(Fore.CYAN + f"\nπŸ“ Reports:\n {jp}\n {tp}\n") + + +if __name__ == "__main__": + main() diff --git a/tiktok_ui.py b/tiktok_ui.py new file mode 100644 index 0000000..e6251d2 --- /dev/null +++ b/tiktok_ui.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +tiktok_ui.py β€” a pretty terminal UI for TikTok account creation-date lookups. + +Wraps the free, no-API logic from tiktok_created.py in a Rich interface: +type a username (or @handle / profile URL / video URL) and get a clean card +showing when the account was created, plus profile stats. No API key or signup. + +Launch it with ./start.sh (or the `tokintel` alias). + +Part of TokIntel (https://github.com/HackUnderway/TokIntel) by Victor Bancayan / +Hack Underway. This lookup UI (no API key needed) was contributed by @Thyfwx. +""" +import os +import sys + +from rich.console import Console, Group +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align +from rich.prompt import Prompt +from rich import box + +# Reuse the validated lookup engine living next to this file. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from tiktok_created import lookup, new_session, save_reports # noqa: E402 + +console = Console() + +TIKTOK_CYAN = "#25F4EE" +TIKTOK_RED = "#FE2C55" +TT_LOGO = [ + "β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—", + "β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•", + " β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• ", + " β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•— ", + " β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•—", + " β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β•", +] + + +def num(x): + return f"{x:,}" if isinstance(x, int) else ("β€”" if x in (None, "") else str(x)) + + +def header(): + # TikTok-style wordmark: cyan on the left, white core, red on the right. + w = max(len(line) for line in TT_LOGO) + a, b = w // 3, 2 * w // 3 + logo = Text(justify="center") + for line in TT_LOGO: + line = line.ljust(w) + logo.append(line[:a], style=f"bold {TIKTOK_CYAN}") + logo.append(line[a:b], style="bold white") + logo.append(line[b:] + "\n", style=f"bold {TIKTOK_RED}") + + tag = Text(justify="center") + tag.append("β™ͺ ", style=TIKTOK_CYAN) + tag.append("Account Lookup", style="bold white") + tag.append(" β™ͺ", style=TIKTOK_RED) + sub = Text("when was this account created? Β· no API key needed", + style="dim italic", justify="center") + credit = Text("part of TokIntel Β· by Hack Underway", style="dim", justify="center") + + body = Group(logo, tag, Text(""), sub, credit) + console.print(Panel(body, box=box.DOUBLE, border_style=TIKTOK_CYAN, padding=(1, 3))) + + +def render_account(d): + created = d.get("account_created") or "unknown" + tbl = Table.grid(padding=(0, 2)) + tbl.add_column(justify="right", style="cyan", no_wrap=True) + tbl.add_column(style="white") + if d.get("nickname"): + tbl.add_row("Name", str(d["nickname"])) + tbl.add_row("Verified", "βœ… yes" if d.get("verified") else "no") + tbl.add_row("Private", "πŸ”’ yes" if d.get("private") else "no") + tbl.add_row("Followers", num(d.get("followers"))) + tbl.add_row("Following", num(d.get("following"))) + tbl.add_row("Likes", num(d.get("likes"))) + tbl.add_row("Videos", num(d.get("videos"))) + if d.get("region"): + tbl.add_row("Region", str(d["region"])) + if d.get("bio"): + tbl.add_row("Bio", str(d["bio"])) + tbl.add_row("User ID", str(d.get("user_id"))) + + body = Group( + Align.center(Text(f"πŸ“… {created}", style="bold green")), + Align.center(Text("account created", style="dim")), + Text(""), + tbl, + ) + console.print(Panel(body, title=f"[bold magenta]@{d.get('username')}[/]", + border_style="green", box=box.ROUNDED, padding=(1, 2))) + + +def render_simple(title, line, note=None, color="green"): + body = [Align.center(Text(line, style=f"bold {color}"))] + if note: + body.append(Align.center(Text(note, style="dim"))) + console.print(Panel(Group(*body), title=title, border_style=color, + box=box.ROUNDED, padding=(1, 2))) + + +def render(data): + if "error" in data: + render_simple("not found", data["error"], color="red") + elif data.get("type") == "account": + render_account(data) + elif data.get("type") == "video": + render_simple("video", f"πŸ“… uploaded {data.get('uploaded')}", + note="video upload time, not account creation") + else: + render_simple("id decode", f"πŸ“… {data.get('decoded')}", note=data.get("note")) + + +def main(): + console.clear() + header() + console.print( + " Type a [bold cyan]username[/], @handle, or profile/video URL. " + "Empty line or [bold]q[/] to quit.\n") + + session = new_session() + results = [] + while True: + try: + entry = Prompt.ask("[bold magenta]πŸ”Ž lookup[/]", default="", show_default=False).strip() + except (EOFError, KeyboardInterrupt): + break + if not entry or entry.lower() in {"q", "quit", "exit"}: + break + with console.status(f"[cyan]Fetching {entry}…[/]", spinner="dots"): + _, data = lookup(entry, session) + render(data) + results.append({"target": entry, "data": data}) + + if results: + _, tp = save_reports(results, "ui") + console.print(f"\n[dim]Looked up {len(results)} Β· report saved β†’[/] {tp}") + console.print("\n[magenta]bye πŸ‘[/]\n") + + +if __name__ == "__main__": + main()