Files
miti99/scripts/newsletter/detect-image-source.js
T
tiennm99 fb292d444d feat(newsletter): port the engine to JavaScript
Translates the seven-subcommand engine to Node ESM, one module per former Go
file, invoked as `node scripts/newsletter <command>` from the repo root. The Go
implementation stays in place for now so parity can be measured against it.

Hand-rolled HTML and XML regexes give way to cheerio, which removes the manual
string surgery in caption extraction and covers RSS and sitemap XML through
xmlMode without a second parser. fetch-via-defuddle gains a local extraction
stage ahead of the defuddle.md proxy; the proxy stays, because fetching from a
third IP is the whole point when this machine's IP is the blocked one. Both
stages now emit the same YAML frontmatter plus body, and an extraction that
looks like a bot challenge counts as a local failure so the proxy still runs.

Behaviour is preserved where it is load-bearing rather than where it is merely
idiomatic: query strings are rebuilt by string surgery so surviving parameters
keep their original order and encoding, duplicate detection stays an index loop
with byte-offset boundary checks so a prefix of a stored URL is not a false
match, empty optional fields are omitted rather than emitted as "", a deep-crawl
miss reports cutoff as null rather than dropping the key, bullet filtering counts
code points, tag counts keep their six-column alignment, and a malformed percent
sequence falls back to the raw substring.

The printer lives in its own module so a command module never imports the
dispatcher: index.js runs main() at module scope, so that cycle would execute
the CLI as a side effect of any import. Missing dependencies report one
actionable line instead of a module-resolution stack trace, and a reader that
closes early exits quietly instead of raising EPIPE.
2026-09-18 16:23:58 +07:00

67 lines
2.1 KiB
JavaScript

// Detect whether an image URL is Substack-hosted and extract its S3 image UUID.
// Usage: node scripts/newsletter detect-image-source "<image-url>"
// Output: JSON { original_url, clean_url, isSubstack, uuid?, innerUrl? }
//
// Substack images are usually served via a CDN wrapper:
//
// https://substackcdn.com/image/fetch/$s_!x!,.../https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F<uuid>_WxH.png
//
// The publication is NOT encoded in the URL — only the image identity (uuid) is.
import { printJson } from "./json-out.js";
import { cleanUrl, isSubstackImage, substackImageUuid } from "./url-utils.js";
/**
* extractInnerUrl pulls the inner S3 URL out of a substackcdn /image/fetch/
* wrapper (if present). A malformed percent sequence falls back to the raw
* substring rather than throwing.
* @param {string} target
* @returns {string}
*/
export function extractInnerUrl(target) {
const marker = target.indexOf("/https%3A%2F%2F");
if (marker !== -1) {
const raw = target.slice(marker + 1);
try {
return decodeURIComponent(raw);
} catch {
return raw;
}
}
// Some forms embed a plain (already-decoded) inner https URL.
if (target.length > 8) {
const plain = target.slice(8).indexOf("/https://");
if (plain !== -1) return target.slice(8 + plain + 1);
}
return target;
}
/**
* @param {string[]} args
* @returns {Promise<void>}
*/
export async function runDetectImageSource(args) {
if (args.length < 1 || args[0] === "") {
process.stderr.write("Usage: node scripts/newsletter detect-image-source <image-url>\n");
process.exit(1);
}
const target = args[0];
const isSubstack = isSubstackImage(target);
// Empty optional fields are omitted, not emitted as "": the skills branch on
// the key being present.
/** @type {Record<string, unknown>} */
const out = {
original_url: target,
clean_url: cleanUrl(target),
isSubstack,
};
if (isSubstack) {
const uuid = substackImageUuid(target);
const innerUrl = extractInnerUrl(target);
if (uuid !== "") out.uuid = uuid;
if (innerUrl !== "") out.innerUrl = innerUrl;
}
printJson(out);
}