← back to Tiktok Architect
scripts/tt-oembed.js
88 lines
#!/usr/bin/env node
/**
* tt-oembed.js — resolve ONE TikTok URL to clean metadata WITHOUT auth or JS.
*
* TikTok video pages are JS-rendered, so WebFetch/curl on the HTML returns only
* the generic "Make Your Day" shell. The public oEmbed endpoint
* (tiktok.com/oembed?url=...) returns the caption (as `title`), author, and
* thumbnail as plain JSON — no login, no browser, $0. This is the reliable way
* to turn a dropped link (including a /t/<short> redirect) into a candidate.
*
* Usage:
* node tt-oembed.js <tiktok-url> # canonical or /t/<short> link
*
* Examples:
* node tt-oembed.js https://www.tiktok.com/t/ZP8GY4Rgo/
* node tt-oembed.js https://www.tiktok.com/@tiktok_architect/video/7643451753400241421
*
* Exit codes: 0 ok, 1 error (bad input / not found / network).
* Output: a JSON object on stdout:
* { id, url, author, caption, hashtags[], claude_relevant, thumbnail }
* `claude_relevant` is a cheap keyword flag so the scout can skip the creator's
* off-topic posts (motorcycles, hobbies) without a second call.
*/
'use strict';
const RELEVANCE_RE =
/\b(claude|claudecode|claude-code|anthropic|vibecod|cursor|codex|copilot|chatgpt|openai|\bllm\b|\bai\b|aitools|aiagent|agentic|mcp|prompt|coding|developer|softwareengineer|programming)\b/i;
function hashtagsFrom(caption) {
const tags = new Set();
const re = /#([\p{L}\p{N}_]+)/gu;
let m;
while ((m = re.exec(caption || ''))) tags.add(m[1].toLowerCase());
return [...tags];
}
// Follow redirects (the /t/<short> links are 301s) to the canonical URL, then
// pull the numeric video id out of .../video/<id>.
async function resolve(url) {
let res;
try {
res = await fetch(url, { redirect: 'follow', headers: { 'User-Agent': 'Mozilla/5.0' } });
} catch (e) {
// network hiccup on the redirect hop — fall back to the input URL
return { canonical: url, id: (url.match(/video\/(\d{5,25})/) || [])[1] || null };
}
const canonical = res.url || url;
const id = (canonical.match(/video\/(\d{5,25})/) || [])[1] || null;
return { canonical, id };
}
async function main() {
const input = process.argv[2];
if (!input || !/tiktok\.com/i.test(input)) {
console.error('usage: node tt-oembed.js <tiktok-url>');
process.exit(1);
}
const { canonical, id } = await resolve(input);
const oe = `https://www.tiktok.com/oembed?url=${encodeURIComponent(canonical)}`;
const res = await fetch(oe, { headers: { 'User-Agent': 'Mozilla/5.0' } });
if (!res.ok) {
console.error(`oembed http ${res.status} for ${canonical}`);
process.exit(1);
}
const j = await res.json();
const caption = j.title || '';
const hashtags = hashtagsFrom(caption);
const out = {
id: id || j.embed_product_id || null,
url: canonical,
author: j.author_name || j.author_unique_id || null,
caption,
hashtags,
claude_relevant: RELEVANCE_RE.test(caption),
thumbnail: j.thumbnail_url || null,
};
console.log(JSON.stringify(out, null, 2));
}
main().catch((e) => {
console.error(String((e && e.message) || e));
process.exit(1);
});