fetch-fic
Version:
Package up delicious, delicious fanfic from various sources into epub ebooks ready for reading in your ereader of choice.
169 lines (150 loc) • 5.95 kB
JavaScript
'use strict'
const Output = use('output')
const moment = require('moment')
function letterCount (nn) {
let base = Math.floor((nn-1) / 26)
let num = (nn-1) % 26
return (base > 0 ? String.fromCharCode(96 + base) : '') +
String.fromCharCode(97 + num)
}
function upperLetterCount (nn) {
let base = Math.floor((nn-1) / 26)
let num = (nn-1) % 26
return (base > 0 ? String.fromCharCode(64 + base) : '') +
String.fromCharCode(65 + num)
}
class OutputEpub extends Output {
from (fic) {
const filenameize = use('filenameize')
return super.from(fic).to(filenameize(fic.title) + '.epub')
}
chapterExt () {
return '.xhtml'
}
async write () {
const Streampub = require('streampub')
const statusRe = /^status:(stalled|abandoned|complete|one-shot)$/
const fandomRe = /^fandom:/
let tags = this.fic.tags && this.fic.tags.filter(tag => !fandomRe.test(tag) && !statusRe.test(tag))
let modified = moment(this.fic.modified || this.fic.started || this.fic.created)
let status = this.fic.tags && this.fic.tags.filter(tag => statusRe.test(tag))[0]
if (status) status = status.replace(/^status:/, '')
if (!status && modified) {
const oneMonthAgo = moment().subtract(1, 'month')
const sixMonthsAgo = moment().subtract(6, 'months')
if (modified.isBefore(sixMonthsAgo)) {
status = 'abandoned'
} else if (modified.isBefore(oneMonthAgo)) {
status = 'stalled'
} else {
status = 'in-progress'
}
}
let fandom = this.fic.tags && this.fic.tags.filter(tag => fandomRe.test(tag)).map(tag => tag.replace(fandomRe, ''))[0]
const epub = new Streampub({
id: this.fic.id,
title: this.fic.title,
author: this.fic.author,
authorUrl: this.fic.authorUrl,
description: this.fic.notes || this.fic.description,
source: this.fic.link,
subject: tags && tags.length && tags.join(','),
publisher: this.fic.publisher,
published: this.fic.started || this.fic.created || this.fic.modified,
modified: modified,
calibre: {
'updated': modified && {'#value#': modified.toISOString().slice(0,10), 'datatype': 'text'},
'words': {'#value#': this.fic.words, 'datatype': 'int'},
'authorurl': {'#value#': this.fic.authorUrl, 'datatype': 'text'},
'status': status && {'#value#': status, 'datatype': 'enumeration'},
'fandom': fandom && {'#value#': fandom, 'datatype': 'text'},
'rec': this.fic.description && {'#value#': this.fic.description, 'datatype': 'text'}
}
})
const WriteStreamAtomic = require('fs-write-stream-atomic')
const output = new WriteStreamAtomic(this.outname)
const fun = require('funstream')
await fun(this.fic)
.pipe(this.transform())
.pipe(epub)
.pipe(output)
return this.outname
}
transformChapter (chapter, stream) {
const Streampub = require('streampub')
const result = []
if (!this.seenAny) {
this.seenAny = true
let title = 'Title Page'
if (this.fic.numberTOC) title = 'ⅰ. ' + title
result.push(Streampub.newChapter(title, this.titlePageHTML(), 0, 'top.xhtml'))
if (this.fic.includeTOC) {
let toctitle = 'Table of Contents'
if (this.fic.numberTOC) toctitle = 'ⅱ. ' + toctitle
result.push(Streampub.newChapter(toctitle, this.tableOfContentsHTML(), 1, 'toc.xhtml'))
}
}
if (chapter.outputType === 'image') {
return result.concat(Streampub.newFile(chapter.filename, chapter.content))
}
if (chapter.outputType === 'cover') {
return result.concat(Streampub.newCoverImage(chapter.content))
}
const index = chapter.order != null && (1 + chapter.order)
let name = chapter.name
if (name != null && this.fic.numberTOC) {
if (index >= 9000) {
name = upperLetterCount(index - 9000) + '. ' + name
} else if (index >= 8000) {
name = letterCount(index - 8000) + '. ' + name
} else {
name = String(index) + '. ' + name
if (chapter.author && chapter.author != this.fic.author) {
name += ` (${chapter.author})`
}
}
}
const filename = this.chapterFilename(chapter)
const html = use('html-template-tag')
const toSanitize = '<html xmlns:epub="http://www.idpf.org/2007/ops">\n' +
(name ? html`<head><title>${name}</title></head>` : '') +
'<section epub:type="chapter">' + chapter.content + '</section>\n' +
'</html>\n'
return result.concat(Streampub.newChapter(name, this.prepareHtml(toSanitize), 100 + index, filename))
}
prepareHtml (html) {
// remove any doc-wide stylesheet so we can do our styling.
return super.prepareHtml(html.replace(/<style>[^>]+<[/]style>/, ''))
}
html (content) {
return `<html xmlns:epub="http://www.idpf.org/2007/ops">\n${content}</html>\n`
}
htmlBody (body) {
return `<body epub:type="cover titlepage">\n${body}</body>\n`
}
htmlTitle () {
const html = use('html-template-tag')
return html`<section epub:type="title"><h1 style="text-align: center;">${this.fic.title}</h1></section>` + '\n'
}
htmlByline () {
if (!this.fic.author) return ''
return `<h3 style="text-align: center;">by <span epub:type="credits">${this.htmlAuthor(this.fic.author, this.fic.authorUrl)}</span></h3>\n`
}
htmlCoverImage () {
if (!this.coverName) return ''
return `<p><img style="display: block; margin-left: auto; margin-right: auto;" src="images/cover.jpg"></p>\n`
}
htmlSummaryRow (key, value) {
if (key === 'Tags') {
return super.htmlSummaryRow(key, `<section epub:type="keywords">${value}`)
} else {
return super.htmlSummaryRow(key, value)
}
}
htmlDescription () {
if (!this.fic.description) return ''
return `<section epub:type="abstract">${super.htmlDescription()}</section>`
}
}
OutputEpub.aliases = []
module.exports = OutputEpub