fix multichapter epub generation

This commit is contained in:
Daniel Bulant 2023-07-24 21:50:52 +02:00
parent 56562f88ca
commit b79647df02
3 changed files with 57 additions and 52 deletions

View file

@ -1,38 +1,37 @@
/**
* @typedef Chapter
* @property {string} id
* @property {number} number
* @property {number?} volume
* @property {string[]} links
* @property {string} hash
* @property {string} baseUrl
* @property {string} title
*/
import request, { proxy, imageproxy } from "./request"; import request, { proxy, imageproxy } from "./request";
const RETRY_LIMIT = 10; const RETRY_LIMIT = 10;
interface Chapter {
id: string,
number: string,
volume?: string,
links: string[],
hash: string,
hashes: string[],
baseUrl: string,
title: string
}
interface Opts {
quality: string,
file: WritableStream,
title: string,
id: string,
language: string,
author: string,
updatedAt: string | Date,
chapters: Chapter[],
callback?: (chapter: string | number, link: number, finished: boolean) => void,
onerror?: (error: Error) => void
}
/** /**
* Base generator, to be extended * Base generator, to be extended
*/ */
export class BaseGenerator { export class BaseGenerator {
/** opts: Opts;
* constructor(opts: Opts) {
* @param {object} opts
* @param {string} opts.quality
* @param {WritableStream} opts.file
* @param {string} opts.title
* @param {string} opts.id
* @param {string} opts.language
* @param {string} opts.author
* @param {string|Date} opts.updatedAt
* @param {Chapter[]} opts.chapters
* @param {(chapter: number, link: number, finished: boolean) => void} [opts.callback]
* @param {(error: Error) => void} [opts.onerror]
*/
constructor(opts) {
this.opts = opts; this.opts = opts;
this.opts.quality = "data"; this.opts.quality = "data";
} }
@ -83,9 +82,9 @@ export class BaseGenerator {
* @param {number} link * @param {number} link
* @param {boolean} finished * @param {boolean} finished
*/ */
callback(chapter = -1, link = -1, finished = false) { callback(chapter: string | number = -1, link: number = -1, finished = false) {
if(this.opts.callback) { if(this.opts.callback) {
this.opts.callback(chapter, parseInt(link), finished); this.opts.callback(chapter, Number(link), finished);
} }
} }
} }

View file

@ -8,6 +8,10 @@ const enc = new TextEncoder();
* Handles epub generation * Handles epub generation
*/ */
export class EpubGenerator extends BaseGenerator { export class EpubGenerator extends BaseGenerator {
writer: WritableStreamDefaultWriter<Uint8Array>;
zip: Zip;
hashes: { hash: string, chapter: string }[];
async generate() { async generate() {
this.writer = this.opts.file.getWriter(); this.writer = this.opts.file.getWriter();
this.zip = new Zip(); this.zip = new Zip();
@ -34,7 +38,7 @@ export class EpubGenerator extends BaseGenerator {
chapter.links = data.urls; chapter.links = data.urls;
chapter.hashes = data.hashes; chapter.hashes = data.hashes;
chapter.hash = data.hash; chapter.hash = data.hash;
this.hashes.push(...chapter.hashes); this.hashes.push(...(chapter.hashes.map(t => ({ hash: t, chapter: chapterI }))));
} }
} }
@ -52,7 +56,7 @@ export class EpubGenerator extends BaseGenerator {
for(const i in chapter.links) { for(const i in chapter.links) {
let url = chapter.links[i]; let url = chapter.links[i];
let hash = chapter.hashes[i]; let hash = chapter.hashes[i];
this.callback(chapterI, i, false); this.callback(chapterI, Number(i), false);
const start = performance.now(); const start = performance.now();
const res = await this.fetchImage(url, chapter); const res = await this.fetchImage(url, chapter);
const image = new ZipPassThrough("OEBPS/" + hash); const image = new ZipPassThrough("OEBPS/" + hash);
@ -67,12 +71,13 @@ export class EpubGenerator extends BaseGenerator {
url: url url: url
}); });
image.push(data, true); image.push(data, true);
const textContent = new ZipPassThrough("OEBPS/" + i + ".xhtml"); let fileI = this.hashes.findIndex(t => t.hash === hash);
const textContent = new ZipPassThrough(`OEBPS/${fileI}.xhtml`);
this.zip.add(textContent); this.zip.add(textContent);
textContent.push(enc.encode(`<?xml version="1.0" encoding="UTF-8"?> textContent.push(enc.encode(`<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head> <head>
<title>Page ${i + 1}</title> <title>Chapter ${chapterI} Page ${Number(i) + 1}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="EPB-UUID" content=""/> <meta name="EPB-UUID" content=""/>
</head> </head>
@ -80,7 +85,7 @@ export class EpubGenerator extends BaseGenerator {
<img style="margin:auto;height:100%;" src="${hash}" /> <img style="margin:auto;height:100%;" src="${hash}" />
</body> </body>
</html>`), true); </html>`), true);
this.callback(chapterI, i, true); this.callback(chapterI, Number(i), true);
} }
} }
@ -113,28 +118,28 @@ export class EpubGenerator extends BaseGenerator {
<metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/" <metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"> xmlns:dcterms="http://purl.org/dc/terms/">
<dc:title>${this.opts.title}</dc:title> <dc:title>${this.opts.title}</dc:title>
<dc:language>${this.opts.language || "en"}</dc:language> <dc:language>${this.opts.language || "en"}</dc:language>
<dc:creator>${this.opts.author}</dc:creator> <dc:creator>${this.opts.author}</dc:creator>
<dc:identifier id="bookid">${this.opts.id}</dc:identifier> <dc:identifier id="bookid">${this.opts.id}</dc:identifier>
<dc:type>Image</dc:type> <dc:type>Image</dc:type>
<meta property="dcterms:modified">${this.opts.updatedAt.toString().split("+")[0]}Z</meta> <meta property="dcterms:modified">${this.opts.updatedAt.toString().split("+")[0]}Z</meta>
<meta property="rendition:layout">pre-paginated</meta> <meta property="rendition:layout">pre-paginated</meta>
<meta property="rendition:orientation">portrait</meta> <meta property="rendition:orientation">portrait</meta>
<meta property="rendition:spread">landscape</meta> <meta property="rendition:spread">landscape</meta>
</metadata> </metadata>
<manifest> <manifest>
${this.hashes.map((t, i) => ` <item id="i${i}" href="${t}" fallback="fallback" media-type="image/${t.substr(t.lastIndexOf(".") + 1) === "jpg" ? "jpeg" : t.substr(t.lastIndexOf(".") + 1)}"/>`).join("\n")} ${this.hashes.map((t, i) => ` <item id="i${i}" href="${t.hash}" fallback="fallback" media-type="image/${t.hash.substr(t.hash.lastIndexOf(".") + 1) === "jpg" ? "jpeg" : t.hash.substr(t.hash.lastIndexOf(".") + 1)}"/>`).join("\n")}
${this.hashes.map((t, i) => ` <item id="p${i}" href="${i}.xhtml" media-type="application/xhtml+xml" />`).join("\n")} ${this.hashes.map((t, i) => ` <item id="p${i}" href="${i}.xhtml" media-type="application/xhtml+xml" />`).join("\n")}
<item id="ncxtoc" href="toc.ncx" media-type="application/x-dtbncx+xml"/> <item id="ncxtoc" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
</manifest> </manifest>
<spine toc="ncxtoc"> <spine toc="ncxtoc">
${this.hashes.map((t, i) => ` <itemref idref="p${i}" linear="yes" />`).join("\n")} ${this.hashes.map((t, i) => ` <itemref idref="p${i}" linear="yes" />`).join("\n")}
<itemref idref="fallback" linear="no" /> <itemref idref="fallback" linear="no" />
</spine> </spine>
</package>`), true); </package>`), true);
@ -142,6 +147,7 @@ export class EpubGenerator extends BaseGenerator {
toc() { toc() {
const ncx = new ZipPassThrough("OEBPS/toc.ncx"); const ncx = new ZipPassThrough("OEBPS/toc.ncx");
this.zip.add(ncx); this.zip.add(ncx);
ncx.push(enc.encode(`<?xml version="1.0" encoding="utf-8"?> ncx.push(enc.encode(`<?xml version="1.0" encoding="utf-8"?>
<!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">
@ -159,14 +165,14 @@ export class EpubGenerator extends BaseGenerator {
</docAuthor> </docAuthor>
<navMap> <navMap>
${this.opts.chapters.map((t, i) => t.links.map((link, i) => ` ${this.opts.chapters.map((t, i) => `
<navPoint id="p${i}" playOrder="${i + 1}"> <navPoint id="p${this.hashes.findIndex(h => h.hash === t.hashes[0])}" playOrder="${i + 1}">
<navLabel> <navLabel>
<text>${this.opts.title} Chapter ${t.number} Page ${i + 1}</text> <text>${this.opts.title} Chapter ${t.number}</text>
</navLabel> </navLabel>
<content src="${i}.xhtml"/> <content src="${this.hashes.findIndex(h => h.hash === t.hashes[0])}.xhtml"/>
</navPoint> </navPoint>
`)).flat().join("\n")} `).flat().join("\n")}
</navMap> </navMap>
</ncx>`), true); </ncx>`), true);
} }

View file

@ -6,6 +6,6 @@
"$lib/*": ["src/lib/*"] "$lib/*": ["src/lib/*"]
} }
}, },
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"], "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte", "src/**/*.ts"],
"extends": "./.svelte-kit/tsconfig.json" "extends": "./.svelte-kit/tsconfig.json"
} }