Add export-story script and draft for Tiny Accident
This commit is contained in:
parent
7f7a62a391
commit
808f565e59
16 changed files with 678 additions and 15 deletions
127
scripts/export-story.ts
Normal file
127
scripts/export-story.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { type ChildProcess, exec, execSync } from "node:child_process";
|
||||
import { readdir, mkdir, mkdtemp, writeFile, readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join as pathJoin, normalize } from "node:path";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { program } from "commander";
|
||||
import fetchRetryWrapper from "fetch-retry";
|
||||
|
||||
function getRTFStyles(rtfSource: string) {
|
||||
const matches = rtfSource.matchAll(
|
||||
/\\s(\d+)(?:\\sbasedon\d+)?\\snext\d+((?:\\[a-z0-9]+ ?)+)(?: ([A-Z][a-zA-Z ]*));/g,
|
||||
);
|
||||
let hasMatches = false;
|
||||
const rtfStyles: Record<number | string, string> = {};
|
||||
for (const [_, styleNumber, partialRTFStyle, styleName] of matches) {
|
||||
hasMatches = true;
|
||||
const rtfStyle = `\\s${styleNumber}${partialRTFStyle}`;
|
||||
rtfStyles[Number.parseInt(styleNumber)] = rtfStyle;
|
||||
rtfStyles[styleName] = rtfStyle;
|
||||
}
|
||||
if (!hasMatches) {
|
||||
throw new Error("Couldn't find valid RTF styles!");
|
||||
}
|
||||
return rtfStyles;
|
||||
}
|
||||
|
||||
const fetchRetry = fetchRetryWrapper(global.fetch);
|
||||
|
||||
async function exportStory(slug: string, options: { outputDir: string }) {
|
||||
/* Check that outputDir is valid */
|
||||
const outputDir = normalize(options.outputDir);
|
||||
let files: string[];
|
||||
try {
|
||||
files = await readdir(outputDir);
|
||||
} catch {
|
||||
files = [];
|
||||
console.log(`Created directory at ${await mkdir(outputDir, { recursive: true })}`);
|
||||
}
|
||||
if (files.length > 1) {
|
||||
console.error(`Directory ${outputDir} is not empty!`);
|
||||
process.exit(1);
|
||||
}
|
||||
/* Check if Astro development server needs to be spawned */
|
||||
const healthcheckURL = `http://localhost:4321/healthcheck`;
|
||||
let devServerProcess: ChildProcess | null = null;
|
||||
try {
|
||||
await fetchRetry(healthcheckURL, { retries: 3, retryDelay: 1000 });
|
||||
} catch {
|
||||
/* Spawn Astro dev server */
|
||||
console.log("Starting Astro development server...");
|
||||
devServerProcess = exec("npm run dev");
|
||||
await setTimeout(2000);
|
||||
try {
|
||||
await fetchRetry(healthcheckURL, { retries: 5, retryDelay: 2000 });
|
||||
} catch {
|
||||
console.error("Astro dev server didn't respond in time!");
|
||||
devServerProcess && devServerProcess.kill();
|
||||
devServerProcess = null;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
/* Get data (story, thumbnail, descriptions) from Astro development server */
|
||||
let storyText = "";
|
||||
try {
|
||||
console.log("Getting data from Astro...");
|
||||
|
||||
const exportStoryURL = `http://localhost:4321/stories/export/story/${slug}`;
|
||||
const exportThumbnailURL = `http://localhost:4321/stories/export/thumbnail/${slug}`;
|
||||
const exportDescriptionURLs = (website: string) =>
|
||||
`http://localhost:4321/stories/export/description/${website}/${slug}`;
|
||||
|
||||
await Promise.all(
|
||||
["eka", "furaffinity", "inkbunny", "sofurry", "weasyl"].map(async (website) => {
|
||||
const description = await fetch(exportDescriptionURLs(website));
|
||||
if (!description.ok) {
|
||||
throw new Error(`Failed to get description for "${website}"`);
|
||||
}
|
||||
const descriptionExt = description.headers.get("Content-Type")?.startsWith("text/markdown") ? "md" : "txt";
|
||||
return await writeFile(
|
||||
pathJoin(outputDir, `description_${website}.${descriptionExt}`),
|
||||
await description.text(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
const thumbnail = await fetch(exportThumbnailURL);
|
||||
if (!thumbnail.ok) {
|
||||
throw new Error("Failed to get thumbnail");
|
||||
}
|
||||
const thumbnailExt = thumbnail.headers.get("Content-Type")?.startsWith("image/png") ? "png" : "jpg";
|
||||
writeFile(pathJoin(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
||||
const story = await fetch(exportStoryURL);
|
||||
if (!story.ok) {
|
||||
throw new Error("Failed to get story");
|
||||
}
|
||||
storyText = await story.text();
|
||||
writeFile(pathJoin(outputDir, `${slug}.txt`), storyText);
|
||||
} finally {
|
||||
if (devServerProcess) {
|
||||
console.log("Shutting down the Astro development server...");
|
||||
devServerProcess.kill();
|
||||
devServerProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse story into output formats */
|
||||
console.log("Parsing story into output formats...");
|
||||
await writeFile(pathJoin(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
||||
const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
|
||||
await writeFile(pathJoin(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
||||
execSync(`libreoffice --convert-to "rtf:Rich Text Format" --outdir ${tempDir} ${pathJoin(tempDir, "temp.txt")}`);
|
||||
const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
|
||||
const rtfStyles = getRTFStyles(rtfText);
|
||||
await writeFile(
|
||||
pathJoin(outputDir, `${slug}.rtf`),
|
||||
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
||||
);
|
||||
console.log("Success!");
|
||||
return;
|
||||
}
|
||||
|
||||
await program
|
||||
.name("export-story")
|
||||
.description("Generate and export formatted upload files for a story")
|
||||
.argument("<story-slug>", `Slug portion of the story's URL (eg. "the-lost-of-the-marshes/chapter-1")`)
|
||||
.option("-o, --output-dir <directory>", `Empty or inexistent directory path to export files to`)
|
||||
.action(exportStory)
|
||||
.parseAsync();
|
||||
Loading…
Add table
Add a link
Reference in a new issue