Start generalizing to work for Ward too

* Updates the command-line options in various ways, notably changing --start-url to --book
* Renames the "book" directory to "staging"
* Abstracts out most book-specific data into books.js (but not yet cover or substitutions.json)
* Makes per-book subfolders under cache and staging directories
This commit is contained in:
Domenic Denicola 2020-07-01 15:10:12 -04:00
commit fd914b43f5
5 changed files with 87 additions and 49 deletions

4
.gitignore vendored
View file

@ -2,5 +2,5 @@
/npm-debug.log /npm-debug.log
cache/ cache/
book/ staging/
Worm.epub *.epub

41
lib/books.js Normal file
View file

@ -0,0 +1,41 @@
"use strict";
// TODO cover
exports.worm = {
startURL: "https://parahumans.wordpress.com/2011/06/11/1-1/",
title: "Worm",
author: "wildbow",
id: "e7f3532d-8db6-4888-be80-1976166b7059",
// First paragraph of https://parahumans.wordpress.com/about/
description: `
An introverted teenage girl with an unconventional superpower, Taylor goes out in costume to find escape from a deeply
unhappy and frustrated civilian life. Her first attempt at taking down a supervillain sees her mistaken for one,
thrusting her into the midst of the local ‘cape’ scene’s politics, unwritten rules, and ambiguous morals. As she risks
life and limb, Taylor faces the dilemma of having to do the wrong things for the right reasons.
`.trim()
};
exports.ward = {
startURL: "https://www.parahumans.net/2017/09/11/daybreak-1-1/",
title: "Ward",
author: "wildbow",
id: "a6b6b156-2f17-43c0-8bb1-bfa91f3ef62a",
// Synposis from https://www.parahumans.net/
description: `
The unwritten rules that govern the fights and outright wars between ‘capes’ have been amended: everyone gets their
second chance. It’s an uneasy thing to come to terms with when notorious supervillains and even monsters are playing at
being hero. The world ended two years ago, and as humanity straddles the old world and the new, there aren’t records,
witnesses, or facilities to answer the villains’ past actions in the present. One of many compromises, uneasy truces and
deceptions that are starting to splinter as humanity rebuilds.
None feel the injustice of this new status quo or the lack of established footing more than the past residents of the
parahuman asylums. The facilities hosted parahumans and their victims, but the facilities are ruined or gone; one of
many fragile ex-patients is left to find a place in a fractured world. She’s perhaps the person least suited to have
anything to do with this tenuous peace or to stand alongside these false heroes. She’s put in a position to make the
decision: will she compromise to help forge what they call, with dark sentiment, a second golden age? Or will she stand
tall as a gilded dark age dawns?
`.trim()
};

View file

@ -3,17 +3,7 @@ const fs = require("fs").promises;
const path = require("path"); const path = require("path");
const cpr = require("util").promisify(require("cpr")); const cpr = require("util").promisify(require("cpr"));
const BOOK_TITLE = "Worm";
const BOOK_AUTHOR = "wildbow";
const BOOK_PUBLISHER = "Domenic Denicola"; const BOOK_PUBLISHER = "Domenic Denicola";
const BOOK_ID = "urn:uuid:e7f3532d-8db6-4888-be80-1976166b7059";
// First paragraph of https://parahumans.wordpress.com/about/
const BOOK_DESCRIPTION = `
An introverted teenage girl with an unconventional superpower, Taylor goes out in costume to find escape from a deeply
unhappy and frustrated civilian life. Her first attempt at taking down a supervillain sees her mistaken for one,
thrusting her into the midst of the local ‘cape’ scene’s politics, unwritten rules, and ambiguous morals. As she risks
life and limb, Taylor faces the dilemma of having to do the wrong things for the right reasons.`;
const NCX_FILENAME = "toc.ncx"; const NCX_FILENAME = "toc.ncx";
@ -21,13 +11,13 @@ const COVER_IMG_FILENAME = "cover.png";
const COVER_XHTML_FILENAME = "cover.xhtml"; const COVER_XHTML_FILENAME = "cover.xhtml";
const COVER_MIMETYPE = "image/png"; const COVER_MIMETYPE = "image/png";
module.exports = async (scaffoldingPath, bookPath, contentPath, chaptersPath, manifestPath) => { module.exports = async (scaffoldingPath, bookPath, contentPath, chaptersPath, manifestPath, bookInfo) => {
await Promise.all([ await Promise.all([
cpr(scaffoldingPath, bookPath, { overwrite: true, confirm: true, filter: noThumbs }), cpr(scaffoldingPath, bookPath, { overwrite: true, confirm: true, filter: noThumbs }),
getChapters(contentPath, chaptersPath, manifestPath).then(chapters => { getChapters(contentPath, chaptersPath, manifestPath).then(chapters => {
return Promise.all([ return Promise.all([
writeOPF(chapters, contentPath), writeOPF(chapters, contentPath, bookInfo),
writeNcx(chapters, contentPath) writeNcx(chapters, contentPath, bookInfo)
]); ]);
}) })
]); ]);
@ -38,7 +28,7 @@ function noThumbs(filePath) {
return path.basename(filePath) !== "Thumbs.db"; return path.basename(filePath) !== "Thumbs.db";
} }
function writeOPF(chapters, contentPath) { function writeOPF(chapters, contentPath, bookInfo) {
const manifestChapters = chapters.map(c => { const manifestChapters = chapters.map(c => {
return `<item id="${c.id}" href="${c.href}" media-type="application/xhtml+xml"/>`; return `<item id="${c.id}" href="${c.href}" media-type="application/xhtml+xml"/>`;
}).join("\n"); }).join("\n");
@ -51,12 +41,12 @@ function writeOPF(chapters, contentPath) {
<package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId"> <package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:title>${BOOK_TITLE}</dc:title> <dc:title>${bookInfo.title}</dc:title>
<dc:language>en</dc:language> <dc:language>en</dc:language>
<dc:identifier id="BookId" opf:scheme="UUID">${BOOK_ID}</dc:identifier> <dc:identifier id="BookId" opf:scheme="UUID">urn:uuid:${bookInfo.id}</dc:identifier>
<dc:creator opf:file-as="${BOOK_AUTHOR}" opf:role="aut">${BOOK_AUTHOR}</dc:creator> <dc:creator opf:file-as="${bookInfo.author}" opf:role="aut">${bookInfo.author}</dc:creator>
<dc:publisher>${BOOK_PUBLISHER}</dc:publisher> <dc:publisher>${BOOK_PUBLISHER}</dc:publisher>
<dc:description>${BOOK_DESCRIPTION}</dc:description> <dc:description>${bookInfo.description}</dc:description>
<meta name="cover" content="cover-image"/> <meta name="cover" content="cover-image"/>
</metadata> </metadata>
@ -80,7 +70,7 @@ ${spineChapters}
return fs.writeFile(path.resolve(contentPath, "content.opf"), contents); return fs.writeFile(path.resolve(contentPath, "content.opf"), contents);
} }
function writeNcx(chapters, contentPath) { function writeNcx(chapters, contentPath, bookInfo) {
const navPoints = chapters.map((c, i) => { const navPoints = chapters.map((c, i) => {
return `<navPoint class="chapter" id="${c.id}" playOrder="${i + 1}"> return `<navPoint class="chapter" id="${c.id}" playOrder="${i + 1}">
<navLabel><text>${c.title}</text></navLabel> <navLabel><text>${c.title}</text></navLabel>
@ -92,18 +82,18 @@ function writeNcx(chapters, contentPath) {
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd"> <!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
<ncx version="2005-1" xml:lang="en" xmlns="http://www.daisy.org/z3986/2005/ncx/"> <ncx version="2005-1" xml:lang="en" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<head> <head>
<meta name="dtb:uid" content="${BOOK_ID}"/> <meta name="dtb:uid" content="urn:uuid:${bookInfo.id}"/>
<meta name="dtb:depth" content="1"/> <meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/> <meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/> <meta name="dtb:maxPageNumber" content="0"/>
</head> </head>
<docTitle> <docTitle>
<text>${BOOK_TITLE}</text> <text>${bookInfo.title}</text>
</docTitle> </docTitle>
<docAuthor> <docAuthor>
<text>${BOOK_AUTHOR}</text> <text>${bookInfo.author}</text>
</docAuthor> </docAuthor>
<navMap> <navMap>

View file

@ -6,51 +6,54 @@ const fs = require("fs").promises;
const yargs = require("yargs"); const yargs = require("yargs");
const packageJson = require("../package.json"); const packageJson = require("../package.json");
const books = require("./books.js");
const download = require("./download.js"); const download = require("./download.js");
const convert = require("./convert.js"); const convert = require("./convert.js");
const scaffold = require("./scaffold.js"); const scaffold = require("./scaffold.js");
const zip = require("./zip.js"); const zip = require("./zip.js");
const OUTPUT_DEFAULT = "(Book name).epub";
const argv = yargs const argv = yargs
.usage(`${packageJson.description}\n\n${packageJson.name} [<command1> [<command2> [<command3> ...]]]\n\n` + .usage(`${packageJson.description}\n\n${packageJson.name} [<command1> [<command2> [<command3> ...]]]\n\n` +
"Each command will fail if the previously-listed one has not yet been run (with matching options).") "Each command will fail if the previously-listed one has not yet been run (with matching options).")
.command("download", "download all chapters by crawling parahumans.wordpress.com") .command("download", "download all chapters into the cache")
.command("convert", "convert the raw chapter HTML files into cleaned-up ebook chapters") .command("convert", "convert the raw HTML into cleaned-up ebook chapters")
.command("scaffold", "assemble the table of contents, etc. to complete the EPUB") .command("scaffold", "assemble the table of contents, etc.")
.command("zip", "zip up the EPUB files into a .epub output") .command("zip", "zip up the created files into a .epub output")
.option("s", { .option("b", {
alias: "start-url", alias: "book",
default: "https://parahumans.wordpress.com/2011/06/11/1-1/", default: Object.keys(books)[0],
describe: "the URL from which to start crawling, for the download command", describe: "the book to operate on",
choices: Object.keys(books),
requiresArg: true, requiresArg: true,
global: true global: true
}) })
.option("c", { .option("c", {
alias: "cache-directory", alias: "cache",
default: "cache", default: "cache",
describe: "cache directory, for the download and convert commands", describe: "cache directory for downloaded raw chapters",
requiresArg: true, requiresArg: true,
global: true global: true
}) })
.option("b", { .option("s", {
alias: "book-directory", alias: "staging",
default: "book", default: "staging",
describe: "directory in which to assemble the EPUB files before zipping, for the convert, scaffold, and zip " + describe: "directory in which to assemble the EPUB files",
"commands",
requiresArg: true, requiresArg: true,
global: true global: true
}) })
.option("o", { .option("o", {
alias: "out", alias: "out",
default: "Worm.epub", default: OUTPUT_DEFAULT,
describe: "output file destination, for the zip command", describe: "output file destination",
requiresArg: true, requiresArg: true,
global: true global: true
}) })
.option("j", { .option("j", {
alias: "jobs", alias: "jobs",
default: 10, default: 10,
describe: "Number of concurrent read/write jobs to run, for the convert command", describe: "Number of concurrent read/write conversion jobs",
requiresArg: true, requiresArg: true,
global: true global: true
}) })
@ -60,18 +63,21 @@ const argv = yargs
.version() .version()
.argv; .argv;
const cachePath = path.resolve(argv.cacheDirectory); const outputFilename = argv.out === OUTPUT_DEFAULT ? `${books[argv.book].title}.epub` : argv.out;
const cachePath = path.resolve(argv.cache, argv.book);
const manifestPath = path.resolve(cachePath, "manifest.json"); const manifestPath = path.resolve(cachePath, "manifest.json");
const scaffoldingPath = path.resolve(__dirname, "../scaffolding"); const scaffoldingPath = path.resolve(__dirname, "../scaffolding");
const bookPath = path.resolve(argv.bookDirectory); const stagingPath = path.resolve(argv.staging, argv.book);
const contentPath = path.resolve(bookPath, "OEBPS"); const contentPath = path.resolve(stagingPath, "OEBPS");
const chaptersPath = path.resolve(contentPath, "chapters"); const chaptersPath = path.resolve(contentPath, "chapters");
const commands = []; const commands = [];
if (argv._.includes("download")) { if (argv._.includes("download")) {
commands.push(() => download(argv.startUrl, cachePath, manifestPath)); const startURL = books[argv.book].startURL;
commands.push(() => download(startURL, cachePath, manifestPath));
} }
if (argv._.includes("convert")) { if (argv._.includes("convert")) {
@ -83,11 +89,12 @@ if (argv._.includes("convert")) {
} }
if (argv._.includes("scaffold")) { if (argv._.includes("scaffold")) {
commands.push(() => scaffold(scaffoldingPath, bookPath, contentPath, chaptersPath, manifestPath)); const bookInfo = books[argv.book];
commands.push(() => scaffold(scaffoldingPath, stagingPath, contentPath, chaptersPath, manifestPath, bookInfo));
} }
if (argv._.includes("zip")) { if (argv._.includes("zip")) {
commands.push(() => zip(bookPath, contentPath, path.resolve(argv.out))); commands.push(() => zip(stagingPath, contentPath, path.resolve(outputFilename)));
} }
(async () => { (async () => {

View file

@ -15,6 +15,6 @@
</style> </style>
</head> </head>
<body> <body>
<img src="cover.png" alt="Worm"/> <img src="cover.png" alt=""/>
</body> </body>
</html> </html>