mirror of
https://github.com/tiennm99/miti99.git
synced 2026-09-17 22:21:24 +00:00
Extract newsletter scripts to a neutral scripts/newsletter/ engine and add side-by-side support for all three AI coding tools off one source of truth. - Move 9 scripts + substack config from .claude/skills/**/scripts to scripts/newsletter/ (history preserved); fix PROJECT_ROOT depth and cross-require/config paths; repoint all SKILL.md invocations - Add canonical AGENTS.md; reduce CLAUDE.md to an @AGENTS.md import - Add opencode.json (permissions, no MCP); skills auto-discovered in place - Add codex/prompts/*.md (6 prompts) + copy installers (install.sh/.ps1) - Add docs/multi-tool-usage.md (setup, per-tool invocation, teardown) + README pointer
113 lines
3.6 KiB
JavaScript
113 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Meta URL router for the mt-add-url skill — the single entry per URL.
|
|
// Usage: node add-url.js "<url>"
|
|
// Outputs: JSON { original_url, clean_url, http_status, accessible,
|
|
// duplicate, route, title?, author? }
|
|
// route ∈ youtube | image | video | document | article
|
|
|
|
const path = require("path");
|
|
const {
|
|
cleanUrl,
|
|
isSubstackImage,
|
|
fetchWithTimeout,
|
|
checkAccessibility,
|
|
checkDuplicate,
|
|
classifyType,
|
|
} = require("./url-utils");
|
|
|
|
const url = process.argv[2];
|
|
if (!url) {
|
|
console.error("Usage: node add-url.js <url>");
|
|
process.exit(1);
|
|
}
|
|
|
|
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
|
const CONTENT_DIR = path.join(PROJECT_ROOT, "content", "post");
|
|
|
|
const YT_HOSTS = new Set(["youtube.com", "www.youtube.com", "m.youtube.com"]);
|
|
|
|
// Detect a YouTube video and extract its id from the supported URL shapes:
|
|
// youtube.com/watch?v=ID, youtu.be/ID, youtube.com/shorts/ID
|
|
// Playlists/channels are intentionally NOT YouTube routes (fall through to type).
|
|
function detectYouTube(targetUrl) {
|
|
try {
|
|
const p = new URL(targetUrl);
|
|
const host = p.host.toLowerCase();
|
|
if (host === "youtu.be") {
|
|
const id = p.pathname.slice(1).split("/")[0];
|
|
return id ? { isYouTube: true, videoId: id } : { isYouTube: false };
|
|
}
|
|
if (YT_HOSTS.has(host)) {
|
|
if (p.pathname === "/watch") {
|
|
const id = p.searchParams.get("v");
|
|
return id ? { isYouTube: true, videoId: id } : { isYouTube: false };
|
|
}
|
|
if (p.pathname.startsWith("/shorts/")) {
|
|
const id = p.pathname.split("/")[2];
|
|
return id ? { isYouTube: true, videoId: id } : { isYouTube: false };
|
|
}
|
|
}
|
|
return { isYouTube: false };
|
|
} catch {
|
|
return { isYouTube: false };
|
|
}
|
|
}
|
|
|
|
// Canonical watch URL — oEmbed accepts watch URLs reliably for all shapes.
|
|
function canonicalWatchUrl(videoId) {
|
|
return `https://www.youtube.com/watch?v=${videoId}`;
|
|
}
|
|
|
|
// Fetch title/author via YouTube oEmbed (no API key). Best-effort: any failure
|
|
// returns {} so the route stays `youtube` and the skill can fall back.
|
|
async function fetchYouTubeMeta(watchUrl) {
|
|
const endpoint = `https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json`;
|
|
const res = await fetchWithTimeout(endpoint);
|
|
if (!res || !res.ok) return {};
|
|
try {
|
|
const data = await res.json();
|
|
return { title: data.title, author: data.author_name };
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const cleanedUrl = cleanUrl(url);
|
|
const yt = detectYouTube(cleanedUrl);
|
|
|
|
// For YouTube, dedup/store against the canonical watch URL so youtu.be and
|
|
// shorts links collapse onto the same identity-param key as watch URLs.
|
|
const effectiveUrl = yt.isYouTube ? canonicalWatchUrl(yt.videoId) : cleanedUrl;
|
|
// Route order: YouTube → Substack image (by host, not extension, so f_auto /
|
|
// .avif / .heic / extensionless CDN URLs still route to the image handler) →
|
|
// file-extension classification.
|
|
let route;
|
|
if (yt.isYouTube) route = "youtube";
|
|
else if (isSubstackImage(cleanedUrl)) route = "image";
|
|
else route = classifyType(cleanedUrl);
|
|
|
|
const httpStatus = await checkAccessibility(cleanedUrl);
|
|
const accessible = httpStatus === "200";
|
|
const duplicate = checkDuplicate(effectiveUrl, CONTENT_DIR);
|
|
|
|
const out = {
|
|
original_url: url,
|
|
clean_url: effectiveUrl,
|
|
http_status: httpStatus,
|
|
accessible,
|
|
duplicate,
|
|
route,
|
|
};
|
|
|
|
if (route === "youtube") {
|
|
const meta = await fetchYouTubeMeta(effectiveUrl);
|
|
if (meta.title) out.title = meta.title;
|
|
if (meta.author) out.author = meta.author;
|
|
}
|
|
|
|
console.log(JSON.stringify(out, null, 2));
|
|
}
|
|
|
|
main();
|