mirror of
https://github.com/daniel-j/fimfic2epub.git
synced 2024-09-30 00:56:13 +13:00
promisify! wip
This commit is contained in:
parent
d1d78bc777
commit
1bbd6316a5
7 changed files with 349 additions and 330 deletions
|
@ -11,6 +11,7 @@ import fileType from 'file-type'
|
||||||
import { styleCss, coverstyleCss, titlestyleCss } from './styles'
|
import { styleCss, coverstyleCss, titlestyleCss } from './styles'
|
||||||
|
|
||||||
import { cleanMarkup } from './cleanMarkup'
|
import { cleanMarkup } from './cleanMarkup'
|
||||||
|
import fetch from './fetch'
|
||||||
import fetchRemote from './fetchRemote'
|
import fetchRemote from './fetchRemote'
|
||||||
import * as template from './templates'
|
import * as template from './templates'
|
||||||
|
|
||||||
|
@ -40,8 +41,8 @@ module.exports = class FimFic2Epub {
|
||||||
static fetchStoryInfo (storyId, raw = false) {
|
static fetchStoryInfo (storyId, raw = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
storyId = FimFic2Epub.getStoryId(storyId)
|
storyId = FimFic2Epub.getStoryId(storyId)
|
||||||
let url = 'https://www.fimfiction.net/api/story.php?story=' + storyId
|
let url = '/api/story.php?story=' + storyId
|
||||||
fetchRemote(url, (content, type) => {
|
fetch(url).then((content) => {
|
||||||
let data
|
let data
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(content)
|
data = JSON.parse(content)
|
||||||
|
@ -76,32 +77,61 @@ module.exports = class FimFic2Epub {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static parseChapterPage (html) {
|
||||||
|
let trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
|
||||||
|
|
||||||
|
let authorNotesPos = html.indexOf('<div class="authors-note"')
|
||||||
|
let authorNotes = ''
|
||||||
|
if (authorNotesPos !== -1) {
|
||||||
|
authorNotesPos = authorNotesPos + html.substring(authorNotesPos).indexOf('<b>Author\'s Note:</b>')
|
||||||
|
authorNotes = html.substring(authorNotesPos + 22)
|
||||||
|
authorNotes = authorNotes.substring(0, authorNotes.indexOf('\t\n\t</div>'))
|
||||||
|
authorNotes = authorNotes.trim()
|
||||||
|
authorNotes = authorNotes.replace(trimWhitespace, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
let chapterPos = html.indexOf('<div id="chapter_container">')
|
||||||
|
let chapter = html.substring(chapterPos + 29)
|
||||||
|
|
||||||
|
let pos = chapter.indexOf('\t</div>\t\t\n\t')
|
||||||
|
|
||||||
|
chapter = chapter.substring(0, pos).trim()
|
||||||
|
|
||||||
|
// remove leading and trailing <br /> tags and whitespace
|
||||||
|
chapter = chapter.replace(trimWhitespace, '')
|
||||||
|
return {content: chapter, notes: authorNotes, notesFirst: authorNotesPos < chapterPos}
|
||||||
|
}
|
||||||
|
|
||||||
constructor (storyId) {
|
constructor (storyId) {
|
||||||
this.storyId = FimFic2Epub.getStoryId(storyId)
|
this.storyId = FimFic2Epub.getStoryId(storyId)
|
||||||
|
|
||||||
this.hasDownloaded = false
|
this.hasDownloaded = false
|
||||||
this.downloadPromise = null
|
this.fetchPromise = null
|
||||||
|
|
||||||
this.storyInfo = null
|
this.storyInfo = null
|
||||||
this.titlePage = null
|
this.chapters = []
|
||||||
this.chapterContent = []
|
this.chapterContent = []
|
||||||
this.remoteResources = new Map()
|
this.remoteResources = new Map()
|
||||||
|
|
||||||
this.cachedFile = null
|
this.cachedFile = null
|
||||||
this.hasCoverImage = false
|
this.hasCoverImage = false
|
||||||
this.coverImageDimensions = {width: 0, height: 0}
|
this.coverImageDimensions = {width: 0, height: 0}
|
||||||
this.includeTitlePage = true
|
// this.includeTitlePage = true
|
||||||
this.categories = []
|
// this.categories = []
|
||||||
this.tags = []
|
// this.tags = []
|
||||||
|
|
||||||
this.zip = new JSZip()
|
this.zip = new JSZip()
|
||||||
}
|
}
|
||||||
|
|
||||||
download () {
|
fetch () {
|
||||||
if (this.downloadPromise) {
|
if (this.fetchPromise) {
|
||||||
return this.downloadPromise
|
return this.fetchPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.storyInfo = null
|
||||||
|
this.chapters.length = 0
|
||||||
|
this.remoteResources.clear()
|
||||||
|
|
||||||
console.log('Fetching story metadata...')
|
console.log('Fetching story metadata...')
|
||||||
|
|
||||||
let p = FimFic2Epub.fetchStoryInfo(this.storyId)
|
let p = FimFic2Epub.fetchStoryInfo(this.storyId)
|
||||||
|
@ -112,250 +142,108 @@ module.exports = class FimFic2Epub {
|
||||||
this.filename = FimFic2Epub.getFilename(this.storyInfo)
|
this.filename = FimFic2Epub.getFilename(this.storyInfo)
|
||||||
})
|
})
|
||||||
.then(this.fetchTitlePage.bind(this))
|
.then(this.fetchTitlePage.bind(this))
|
||||||
|
.then(this.fetchChapters.bind(this))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.downloadPromise = null
|
console.log('Fetch complete')
|
||||||
|
console.log(this)
|
||||||
|
this.fetchPromise = null
|
||||||
})
|
})
|
||||||
|
|
||||||
this.downloadPromise = p
|
this.fetchPromise = p
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
build () {
|
build () {
|
||||||
return this.extractTitlePageInfo(this.titlePage)
|
this.chapterContent.length = 0
|
||||||
.then(this.checkCoverImage.bind(this))
|
return this.checkCoverImage()
|
||||||
|
.then(this.processChapters.bind(this))
|
||||||
|
.then(this.fetchRemoteFiles.bind(this))
|
||||||
|
.then(this.processStory.bind(this))
|
||||||
|
.then(() => {
|
||||||
|
console.log('Build complete')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTitlePage () {
|
fetchTitlePage () {
|
||||||
console.log('Fetching index page...')
|
console.log('Fetching title page...')
|
||||||
return new Promise((resolve, reject) => {
|
let url = this.storyInfo.url.replace('http://www.fimfiction.net', '')
|
||||||
fetchRemote(this.storyInfo.url, (raw, type) => {
|
return fetch(url).then(this.extractTitlePageInfo.bind(this))
|
||||||
this.titlePage = raw
|
|
||||||
resolve(raw)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extractTitlePageInfo (html) {
|
extractTitlePageInfo (html) {
|
||||||
return new Promise((resolve, reject) => {
|
let descPos = html.indexOf('<div class="description" id="description')
|
||||||
let descPos = html.indexOf('<div class="description" id="description')
|
descPos = descPos + html.substring(descPos).indexOf('">') + 2
|
||||||
descPos = descPos + html.substring(descPos).indexOf('">') + 2
|
html = html.substring(descPos)
|
||||||
html = html.substring(descPos)
|
let ma = html.match(/<a href="(.*?)" class="source">Source<\/a>/)
|
||||||
let ma = html.match(/<a href="(.*?)" class="source">Source<\/a>/)
|
this.storyInfo.source_image = null
|
||||||
this.storyInfo.source_image = null
|
if (ma) {
|
||||||
if (ma) {
|
this.storyInfo.source_image = ma[1]
|
||||||
this.storyInfo.source_image = ma[1]
|
}
|
||||||
}
|
let endCatsPos = html.indexOf('<hr />')
|
||||||
let endCatsPos = html.indexOf('<hr />')
|
let startCatsPos = html.substring(0, endCatsPos).lastIndexOf('</div>')
|
||||||
let startCatsPos = html.substring(0, endCatsPos).lastIndexOf('</div>')
|
let catsHtml = html.substring(startCatsPos, endCatsPos)
|
||||||
let catsHtml = html.substring(startCatsPos, endCatsPos)
|
html = html.substring(endCatsPos + 6)
|
||||||
html = html.substring(endCatsPos + 6)
|
|
||||||
|
|
||||||
let categories = []
|
let categories = []
|
||||||
let matchCategory = /<a href="(.*?)" class="(.*?)">(.*?)<\/a>/g
|
let matchCategory = /<a href="(.*?)" class="(.*?)">(.*?)<\/a>/g
|
||||||
for (let c; (c = matchCategory.exec(catsHtml));) {
|
for (let c; (c = matchCategory.exec(catsHtml));) {
|
||||||
categories.push({
|
categories.push({
|
||||||
url: 'http://www.fimfiction.net' + c[1],
|
url: 'http://www.fimfiction.net' + c[1],
|
||||||
className: c[2],
|
className: c[2],
|
||||||
name: entities.decode(c[3])
|
name: entities.decode(c[3])
|
||||||
})
|
|
||||||
}
|
|
||||||
this.categories = categories
|
|
||||||
|
|
||||||
ma = html.match(/This story is a sequel to <a href="([^"]*)">(.*?)<\/a>/)
|
|
||||||
if (ma) {
|
|
||||||
this.storyInfo.prequel = {
|
|
||||||
url: 'http://www.fimfiction.net' + ma[1],
|
|
||||||
title: entities.decode(ma[2])
|
|
||||||
}
|
|
||||||
html = html.substring(html.indexOf('<hr />') + 6)
|
|
||||||
}
|
|
||||||
let endDescPos = html.indexOf('</div>\n')
|
|
||||||
let description = html.substring(0, endDescPos).trim()
|
|
||||||
|
|
||||||
html = html.substring(endDescPos + 7)
|
|
||||||
let extraPos = html.indexOf('<div class="extra_story_data">')
|
|
||||||
html = html.substring(extraPos + 30)
|
|
||||||
|
|
||||||
ma = html.match(/<span class="published">First Published<\/span><br \/><span>(.*?)<\/span>/)
|
|
||||||
if (ma) {
|
|
||||||
let date = ma[1]
|
|
||||||
date = date.replace(/^(\d+)[a-z]+? ([a-zA-Z]+? \d+)$/, '$1 $2')
|
|
||||||
this.storyInfo.publishDate = (new Date(date).getTime() / 1000) | 0
|
|
||||||
}
|
|
||||||
|
|
||||||
html = html.substring(0, html.indexOf('<div class="button-group"'))
|
|
||||||
|
|
||||||
let tags = []
|
|
||||||
tags.byImage = {}
|
|
||||||
let matchTag = /<a href="\/tag\/(.*?)" class="character_icon" title="(.*?)" style=".*?"><img src="(.*?)" class="character_icon" \/><\/a>/g
|
|
||||||
for (let tag; (tag = matchTag.exec(html));) {
|
|
||||||
let t = {
|
|
||||||
url: 'http://www.fimfiction.net/tag/' + tag[1],
|
|
||||||
name: entities.decode(tag[2]),
|
|
||||||
image: entities.decode(tag[3])
|
|
||||||
}
|
|
||||||
tags.push(t)
|
|
||||||
tags.byImage[t.image] = t
|
|
||||||
if (this.includeTitlePage) {
|
|
||||||
this.remoteResources.set(t.image, {filename: 'tag-' + tag[1], originalUrl: t.image, where: ['tags']})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.tags = tags
|
|
||||||
|
|
||||||
cleanMarkup(description).then((html) => {
|
|
||||||
this.storyInfo.description = html
|
|
||||||
this.findRemoteResources('description', 'description', html)
|
|
||||||
resolve()
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
this.categories = categories
|
||||||
|
|
||||||
checkCoverImage () {
|
ma = html.match(/This story is a sequel to <a href="([^"]*)">(.*?)<\/a>/)
|
||||||
return new Promise((resolve, reject) => {
|
if (ma) {
|
||||||
this.hasCoverImage = !!this.storyInfo.full_image
|
this.storyInfo.prequel = {
|
||||||
|
url: 'http://www.fimfiction.net' + ma[1],
|
||||||
if (this.hasCoverImage) {
|
title: entities.decode(ma[2])
|
||||||
this.remoteResources.set(this.storyInfo.full_image, {filename: 'cover', where: ['cover']})
|
|
||||||
|
|
||||||
if (!isNode) {
|
|
||||||
let coverImage = new Image()
|
|
||||||
coverImage.src = this.storyInfo.full_image
|
|
||||||
|
|
||||||
coverImage.addEventListener('load', () => {
|
|
||||||
this.coverImageDimensions.width = coverImage.width
|
|
||||||
this.coverImageDimensions.height = coverImage.height
|
|
||||||
this.processStory(resolve, reject)
|
|
||||||
}, false)
|
|
||||||
coverImage.addEventListener('error', () => {
|
|
||||||
console.warn('Unable to fetch cover image, skipping...')
|
|
||||||
this.hasCoverImage = false
|
|
||||||
this.processStory(resolve, reject)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.processStory(resolve, reject)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.processStory(resolve, reject)
|
|
||||||
}
|
}
|
||||||
})
|
html = html.substring(html.indexOf('<hr />') + 6)
|
||||||
}
|
}
|
||||||
|
let endDescPos = html.indexOf('</div>\n')
|
||||||
|
let description = html.substring(0, endDescPos).trim()
|
||||||
|
|
||||||
processStory (resolve, reject) {
|
html = html.substring(endDescPos + 7)
|
||||||
console.log('Fetching chapters...')
|
let extraPos = html.indexOf('<div class="extra_story_data">')
|
||||||
|
html = html.substring(extraPos + 30)
|
||||||
|
|
||||||
this.fetchChapters().then(() => {
|
ma = html.match(/<span class="published">First Published<\/span><br \/><span>(.*?)<\/span>/)
|
||||||
console.log('Fetching remote files...')
|
if (ma) {
|
||||||
|
let date = ma[1]
|
||||||
this.fetchRemoteFiles(() => {
|
date = date.replace(/^(\d+)[a-z]+? ([a-zA-Z]+? \d+)$/, '$1 $2')
|
||||||
console.log('Finishing build...')
|
this.storyInfo.publishDate = (new Date(date).getTime() / 1000) | 0
|
||||||
|
|
||||||
this.zip.file('mimetype', 'application/epub+zip')
|
|
||||||
this.zip.file('META-INF/container.xml', containerXml)
|
|
||||||
|
|
||||||
let coverFilename = ''
|
|
||||||
this.remoteResources.forEach((r, url) => {
|
|
||||||
let dest = '../' + r.dest
|
|
||||||
if (r.dest && r.originalUrl && r.where) {
|
|
||||||
let ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g')
|
|
||||||
for (var i = 0; i < r.where.length; i++) {
|
|
||||||
let w = r.where[i]
|
|
||||||
if (typeof w === 'number') {
|
|
||||||
this.chapterContent[w] = this.chapterContent[w].replace(ourl, dest)
|
|
||||||
} else if (w === 'description') {
|
|
||||||
this.storyInfo.description = this.storyInfo.description.replace(ourl, dest)
|
|
||||||
} else if (w === 'tags') {
|
|
||||||
this.tags.byImage[r.originalUrl].image = dest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (r.filename === 'cover' && r.dest) {
|
|
||||||
coverFilename = dest
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (let num = 0; num < this.chapterContent.length; num++) {
|
|
||||||
let html = this.chapterContent[num]
|
|
||||||
let filename = 'OEBPS/Text/chapter_' + zeroFill(3, num + 1) + '.xhtml'
|
|
||||||
this.zip.file(filename, html)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chapterContent.length = 0
|
|
||||||
|
|
||||||
this.zip.file('OEBPS/content.opf', template.createOpf(this))
|
|
||||||
|
|
||||||
if (this.hasCoverImage) {
|
|
||||||
this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(coverFilename, this.coverImageDimensions.width, this.coverImageDimensions.height))
|
|
||||||
} else {
|
|
||||||
this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.includeTitlePage) {
|
|
||||||
this.zip.file('OEBPS/Text/title.xhtml', template.createTitlePage(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zip.file('OEBPS/Text/nav.xhtml', template.createNav(this))
|
|
||||||
this.zip.file('OEBPS/toc.ncx', template.createNcx(this))
|
|
||||||
|
|
||||||
this.zip.file('OEBPS/Styles/style.css', styleCss)
|
|
||||||
this.zip.file('OEBPS/Styles/coverstyle.css', coverstyleCss)
|
|
||||||
if (this.includeTitlePage) {
|
|
||||||
this.zip.file('OEBPS/Styles/titlestyle.css', titlestyleCss)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasDownloaded = true
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchRemoteFiles (cb) {
|
|
||||||
let iter = this.remoteResources.entries()
|
|
||||||
let count = 0
|
|
||||||
let completeCount = 0
|
|
||||||
|
|
||||||
let recursive = () => {
|
|
||||||
let r = iter.next().value
|
|
||||||
if (!r) {
|
|
||||||
if (completeCount === this.remoteResources.size) {
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let url = r[0]
|
|
||||||
r = r[1]
|
|
||||||
|
|
||||||
console.log('Fetching remote file ' + (count + 1) + ' of ' + this.remoteResources.size + ': ' + r.filename, url)
|
|
||||||
count++
|
|
||||||
|
|
||||||
fetchRemote(url, (data, type) => {
|
|
||||||
r.dest = null
|
|
||||||
let info = fileType(isNode ? data : new Uint8Array(data))
|
|
||||||
if (info) {
|
|
||||||
type = info.mime
|
|
||||||
r.type = type
|
|
||||||
let isImage = type.indexOf('image/') === 0
|
|
||||||
let folder = isImage ? 'Images' : 'Misc'
|
|
||||||
let dest = folder + '/*.' + info.ext
|
|
||||||
r.dest = dest.replace('*', r.filename)
|
|
||||||
this.zip.file('OEBPS/' + r.dest, data)
|
|
||||||
if (isNode && r.filename === 'cover') {
|
|
||||||
const sizeOf = require('image-size')
|
|
||||||
this.coverImageDimensions = sizeOf(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completeCount++
|
|
||||||
recursive()
|
|
||||||
}, 'arraybuffer')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// concurrent downloads!
|
html = html.substring(0, html.indexOf('<div class="button-group"'))
|
||||||
recursive()
|
|
||||||
recursive()
|
let tags = []
|
||||||
recursive()
|
tags.byImage = {}
|
||||||
recursive()
|
let matchTag = /<a href="\/tag\/(.*?)" class="character_icon" title="(.*?)" style=".*?"><img src="(.*?)" class="character_icon" \/><\/a>/g
|
||||||
|
for (let tag; (tag = matchTag.exec(html));) {
|
||||||
|
let t = {
|
||||||
|
url: 'http://www.fimfiction.net/tag/' + tag[1],
|
||||||
|
name: entities.decode(tag[2]),
|
||||||
|
image: entities.decode(tag[3])
|
||||||
|
}
|
||||||
|
tags.push(t)
|
||||||
|
tags.byImage[t.image] = t
|
||||||
|
if (this.includeTitlePage) {
|
||||||
|
this.remoteResources.set(t.image, {filename: 'tag-' + tag[1], originalUrl: t.image, where: ['tags']})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.tags = tags
|
||||||
|
|
||||||
|
return cleanMarkup(description).then((html) => {
|
||||||
|
this.storyInfo.description = html
|
||||||
|
this.findRemoteResources('description', 'description', html)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchChapters () {
|
fetchChapters () {
|
||||||
|
console.log('Fetching chapters...')
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let chapters = this.storyInfo.chapters
|
let chapters = this.storyInfo.chapters
|
||||||
let chapterCount = this.storyInfo.chapters.length
|
let chapterCount = this.storyInfo.chapters.length
|
||||||
|
@ -374,18 +262,15 @@ module.exports = class FimFic2Epub {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('Fetching chapter ' + (index + 1) + ' of ' + chapters.length + ': ' + ch.title)
|
console.log('Fetching chapter ' + (index + 1) + ' of ' + chapters.length + ': ' + ch.title)
|
||||||
let url = ch.link.replace('http', 'https')
|
let url = ch.link.replace('http://www.fimfiction.net', '')
|
||||||
fetchRemote(url, (html) => {
|
fetch(url).then((html) => {
|
||||||
template.createChapter(ch, html, (html) => {
|
this.chapters[index] = FimFic2Epub.parseChapterPage(html)
|
||||||
this.findRemoteResources('ch_' + zeroFill(3, index + 1), index, html)
|
completeCount++
|
||||||
this.chapterContent[index] = html
|
if (completeCount < chapterCount) {
|
||||||
completeCount++
|
recursive()
|
||||||
if (completeCount < chapterCount) {
|
} else {
|
||||||
recursive()
|
resolve()
|
||||||
} else {
|
}
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,6 +282,157 @@ module.exports = class FimFic2Epub {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkCoverImage () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.hasCoverImage = !!this.storyInfo.full_image
|
||||||
|
|
||||||
|
if (this.hasCoverImage) {
|
||||||
|
this.remoteResources.set(this.storyInfo.full_image, {filename: 'cover', where: ['cover']})
|
||||||
|
|
||||||
|
if (!isNode) {
|
||||||
|
let coverImage = new Image()
|
||||||
|
coverImage.src = this.storyInfo.full_image
|
||||||
|
|
||||||
|
coverImage.addEventListener('load', () => {
|
||||||
|
this.coverImageDimensions.width = coverImage.width
|
||||||
|
this.coverImageDimensions.height = coverImage.height
|
||||||
|
resolve()
|
||||||
|
}, false)
|
||||||
|
coverImage.addEventListener('error', () => {
|
||||||
|
console.warn('Unable to fetch cover image, skipping...')
|
||||||
|
this.hasCoverImage = false
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
processChapters () {
|
||||||
|
let p = []
|
||||||
|
for (let i = 0; i < this.storyInfo.chapters.length; i++) {
|
||||||
|
let ch = this.storyInfo.chapters[i]
|
||||||
|
p.push(template.createChapter(ch, this.chapters[i]).then((html) => {
|
||||||
|
this.findRemoteResources('ch_' + zeroFill(3, i + 1), i, html)
|
||||||
|
this.chapterContent[i] = html
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return Promise.all(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
processStory () {
|
||||||
|
console.log('Finishing build...')
|
||||||
|
|
||||||
|
this.zip.file('mimetype', 'application/epub+zip')
|
||||||
|
this.zip.file('META-INF/container.xml', containerXml)
|
||||||
|
|
||||||
|
let coverFilename = ''
|
||||||
|
this.remoteResources.forEach((r, url) => {
|
||||||
|
let dest = '../' + r.dest
|
||||||
|
if (r.dest && r.originalUrl && r.where) {
|
||||||
|
let ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g')
|
||||||
|
for (var i = 0; i < r.where.length; i++) {
|
||||||
|
let w = r.where[i]
|
||||||
|
if (typeof w === 'number') {
|
||||||
|
this.chapterContent[w] = this.chapterContent[w].replace(ourl, dest)
|
||||||
|
} else if (w === 'description') {
|
||||||
|
this.storyInfo.description = this.storyInfo.description.replace(ourl, dest)
|
||||||
|
} else if (w === 'tags') {
|
||||||
|
this.tags.byImage[r.originalUrl].image = dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r.filename === 'cover' && r.dest) {
|
||||||
|
coverFilename = dest
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let num = 0; num < this.chapterContent.length; num++) {
|
||||||
|
let html = this.chapterContent[num]
|
||||||
|
let filename = 'OEBPS/Text/chapter_' + zeroFill(3, num + 1) + '.xhtml'
|
||||||
|
this.zip.file(filename, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chapterContent.length = 0
|
||||||
|
|
||||||
|
this.zip.file('OEBPS/content.opf', template.createOpf(this))
|
||||||
|
|
||||||
|
if (this.hasCoverImage) {
|
||||||
|
this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(coverFilename, this.coverImageDimensions.width, this.coverImageDimensions.height))
|
||||||
|
} else {
|
||||||
|
this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.includeTitlePage) {
|
||||||
|
this.zip.file('OEBPS/Text/title.xhtml', template.createTitlePage(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zip.file('OEBPS/Text/nav.xhtml', template.createNav(this))
|
||||||
|
this.zip.file('OEBPS/toc.ncx', template.createNcx(this))
|
||||||
|
|
||||||
|
this.zip.file('OEBPS/Styles/style.css', styleCss)
|
||||||
|
this.zip.file('OEBPS/Styles/coverstyle.css', coverstyleCss)
|
||||||
|
if (this.includeTitlePage) {
|
||||||
|
this.zip.file('OEBPS/Styles/titlestyle.css', titlestyleCss)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasDownloaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRemoteFiles () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('Fetching remote files...')
|
||||||
|
let iter = this.remoteResources.entries()
|
||||||
|
let count = 0
|
||||||
|
let completeCount = 0
|
||||||
|
|
||||||
|
let recursive = () => {
|
||||||
|
let r = iter.next().value
|
||||||
|
if (!r) {
|
||||||
|
if (completeCount === this.remoteResources.size) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let url = r[0]
|
||||||
|
r = r[1]
|
||||||
|
|
||||||
|
console.log('Fetching remote file ' + (count + 1) + ' of ' + this.remoteResources.size + ': ' + r.filename, url)
|
||||||
|
count++
|
||||||
|
|
||||||
|
fetchRemote(url).then((data) => {
|
||||||
|
r.dest = null
|
||||||
|
let info = fileType(isNode ? data : new Uint8Array(data))
|
||||||
|
if (info) {
|
||||||
|
let type = info.mime
|
||||||
|
r.type = type
|
||||||
|
let isImage = type.indexOf('image/') === 0
|
||||||
|
let folder = isImage ? 'Images' : 'Misc'
|
||||||
|
let dest = folder + '/*.' + info.ext
|
||||||
|
r.dest = dest.replace('*', r.filename)
|
||||||
|
this.zip.file('OEBPS/' + r.dest, data)
|
||||||
|
if (isNode && r.filename === 'cover') {
|
||||||
|
const sizeOf = require('image-size')
|
||||||
|
this.coverImageDimensions = sizeOf(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completeCount++
|
||||||
|
recursive()
|
||||||
|
}, 'arraybuffer')
|
||||||
|
}
|
||||||
|
|
||||||
|
// concurrent downloads!
|
||||||
|
recursive()
|
||||||
|
recursive()
|
||||||
|
recursive()
|
||||||
|
recursive()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
findRemoteResources (prefix, where, html) {
|
findRemoteResources (prefix, where, html) {
|
||||||
let remoteCounter = 1
|
let remoteCounter = 1
|
||||||
let matchUrl = /<img.*?src="([^">]*\/([^">]*?))".*?>/g
|
let matchUrl = /<img.*?src="([^">]*\/([^">]*?))".*?>/g
|
||||||
|
|
|
@ -50,7 +50,7 @@ export function cleanMarkup (html) {
|
||||||
let completeCount = 0
|
let completeCount = 0
|
||||||
|
|
||||||
function getYoutubeInfo (ids) {
|
function getYoutubeInfo (ids) {
|
||||||
fetch('https://www.googleapis.com/youtube/v3/videos?id=' + ids + '&part=snippet&maxResults=50&key=' + youtubeKey, (raw, type) => {
|
fetch('https://www.googleapis.com/youtube/v3/videos?id=' + ids + '&part=snippet&maxResults=50&key=' + youtubeKey).then((raw) => {
|
||||||
let data
|
let data
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(raw).items
|
data = JSON.parse(raw).items
|
||||||
|
|
|
@ -5,12 +5,11 @@ import fetch from './fetch'
|
||||||
if (typeof safari !== 'undefined') {
|
if (typeof safari !== 'undefined') {
|
||||||
safari.application.addEventListener('message', function (ev) {
|
safari.application.addEventListener('message', function (ev) {
|
||||||
let url = ev.message
|
let url = ev.message
|
||||||
fetch(url, (buffer, type) => {
|
fetch(url).then((buffer) => {
|
||||||
console.log('Fetched ' + url + ' (' + type + ')')
|
console.log('Fetched ' + url)
|
||||||
ev.target.page.dispatchMessage('remote', {
|
ev.target.page.dispatchMessage('remote', {
|
||||||
input: url,
|
input: url,
|
||||||
output: buffer,
|
output: buffer
|
||||||
type: type
|
|
||||||
})
|
})
|
||||||
}, 'arraybuffer')
|
}, 'arraybuffer')
|
||||||
}, false)
|
}, false)
|
||||||
|
@ -19,9 +18,9 @@ if (typeof safari !== 'undefined') {
|
||||||
|
|
||||||
onMessage.addListener(function (request, sender, sendResponse) {
|
onMessage.addListener(function (request, sender, sendResponse) {
|
||||||
if (typeof request === 'string') {
|
if (typeof request === 'string') {
|
||||||
fetch(request, (blob, type) => {
|
fetch(request, 'blob').then((blob) => {
|
||||||
sendResponse(URL.createObjectURL(blob), type)
|
sendResponse(URL.createObjectURL(blob))
|
||||||
}, 'blob')
|
})
|
||||||
// required for async
|
// required for async
|
||||||
return true
|
return true
|
||||||
} else if (request.showPageAction) {
|
} else if (request.showPageAction) {
|
||||||
|
|
71
src/fetch.js
71
src/fetch.js
|
@ -1,47 +1,52 @@
|
||||||
|
|
||||||
import isNode from 'detect-node'
|
import isNode from 'detect-node'
|
||||||
|
|
||||||
function fetchNode (url, cb, responseType) {
|
function fetchNode (url, responseType) {
|
||||||
const request = require('request')
|
const request = require('request')
|
||||||
request({
|
if (url.indexOf('/') === 0) {
|
||||||
url: url,
|
url = 'http://www.fimfiction.net' + url
|
||||||
encoding: responseType ? null : 'utf8',
|
}
|
||||||
headers: {
|
return new Promise((resolve, reject) => {
|
||||||
referer: 'http://www.fimfiction.net/',
|
request({
|
||||||
cookie: 'view_mature=true'
|
url: url,
|
||||||
}
|
encoding: responseType ? null : 'utf8',
|
||||||
}, (error, response, body) => {
|
headers: {
|
||||||
if (error) {
|
referer: 'http://www.fimfiction.net/',
|
||||||
console.error(error)
|
cookie: 'view_mature=true'
|
||||||
cb()
|
}
|
||||||
return
|
}, (error, response, body) => {
|
||||||
}
|
if (error) {
|
||||||
let type = response.headers['content-type']
|
reject(error)
|
||||||
cb(body, type)
|
return
|
||||||
|
}
|
||||||
|
let type = response.headers['content-type']
|
||||||
|
resolve(body)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function fetch (url, cb, responseType) {
|
export default function fetch (url, responseType) {
|
||||||
if (url.indexOf('//') === 0) {
|
if (url.indexOf('//') === 0) {
|
||||||
url = 'http:' + url
|
url = 'http:' + url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
fetchNode(url, cb, responseType)
|
return fetchNode(url, responseType)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
let x = new XMLHttpRequest()
|
let x = new XMLHttpRequest()
|
||||||
x.open('get', url, true)
|
x.open('get', url, true)
|
||||||
if (responseType) {
|
if (responseType) {
|
||||||
x.responseType = responseType
|
console.log(responseType)
|
||||||
}
|
x.responseType = responseType
|
||||||
x.onload = function () {
|
}
|
||||||
cb(x.response, x.getResponseHeader('content-type'))
|
x.onload = function () {
|
||||||
}
|
// x.getResponseHeader('content-type')
|
||||||
x.onerror = function () {
|
resolve(x.response)
|
||||||
console.error('error')
|
}
|
||||||
cb(null)
|
x.onerror = function () {
|
||||||
}
|
reject('Error fetching ' + url)
|
||||||
x.send()
|
}
|
||||||
|
x.send()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,33 +39,34 @@ if (typeof safari !== 'undefined') {
|
||||||
safari.self.addEventListener('message', safariHandler, false)
|
safari.self.addEventListener('message', safariHandler, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchBackground (url, cb, responseType) {
|
function fetchBackground (url, responseType) {
|
||||||
if (typeof chrome !== 'undefined' && chrome.runtime.sendMessage) {
|
return new Promise((resolve, reject) => {
|
||||||
chrome.runtime.sendMessage(url, function (objurl) {
|
if (typeof chrome !== 'undefined' && chrome.runtime.sendMessage) {
|
||||||
fetch(objurl, cb, responseType)
|
chrome.runtime.sendMessage(url, function (objurl) {
|
||||||
URL.revokeObjectURL(objurl)
|
resolve(fetch(objurl, responseType))
|
||||||
})
|
URL.revokeObjectURL(objurl)
|
||||||
} else if (typeof safari !== 'undefined') {
|
})
|
||||||
safariQueue[url] = {cb: cb, responseType: responseType}
|
} else if (typeof safari !== 'undefined') {
|
||||||
safari.self.tab.dispatchMessage('remote', url)
|
safariQueue[url] = {cb: resolve, responseType: responseType}
|
||||||
} else {
|
safari.self.tab.dispatchMessage('remote', url)
|
||||||
cb(null)
|
} else {
|
||||||
}
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function fetchRemote (url, cb, responseType) {
|
export default function fetchRemote (url, responseType) {
|
||||||
if (url.indexOf('//') === 0) {
|
if (url.indexOf('//') === 0) {
|
||||||
url = 'http:' + url
|
url = 'http:' + url
|
||||||
}
|
}
|
||||||
if (!isNode && document.location.protocol === 'https:' && url.indexOf('http:') === 0) {
|
if (!isNode && document.location.protocol === 'https:' && url.indexOf('http:') === 0) {
|
||||||
fetchBackground(url, cb, responseType)
|
return fetchBackground(url, responseType)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
fetch(url, (data, type) => {
|
return fetch(url, responseType).then((data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
fetchBackground(url, cb, responseType)
|
return fetchBackground(url, responseType)
|
||||||
} else {
|
} else {
|
||||||
cb(data, type)
|
return Promise.resolve(data)
|
||||||
}
|
}
|
||||||
}, responseType)
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ let dialog = {
|
||||||
}
|
}
|
||||||
this.createEpub = (e) => {
|
this.createEpub = (e) => {
|
||||||
e.target.disabled = true
|
e.target.disabled = true
|
||||||
ffc.download()
|
ffc.fetch()
|
||||||
.then(ffc.build.bind(ffc))
|
.then(ffc.build.bind(ffc))
|
||||||
.then(ffc.getFile.bind(ffc)).then((file) => {
|
.then(ffc.getFile.bind(ffc)).then((file) => {
|
||||||
console.log('Saving file...')
|
console.log('Saving file...')
|
||||||
|
|
|
@ -24,43 +24,21 @@ function prettyDate (d) {
|
||||||
return d.getDate() + nth(d) + ' ' + months[d.getMonth()].substring(0, 3) + ' ' + d.getFullYear()
|
return d.getDate() + nth(d) + ' ' + months[d.getMonth()].substring(0, 3) + ' ' + d.getFullYear()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createChapter (ch, html, callback) {
|
export function createChapter (ch, chapter) {
|
||||||
let trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
|
return Promise.all([cleanMarkup(chapter.content), cleanMarkup(chapter.notes)]).then((values) => {
|
||||||
|
|
||||||
let authorNotesPos = html.indexOf('<div class="authors-note"')
|
|
||||||
let authorNotes = ''
|
|
||||||
if (authorNotesPos !== -1) {
|
|
||||||
authorNotesPos = authorNotesPos + html.substring(authorNotesPos).indexOf('<b>Author\'s Note:</b>')
|
|
||||||
authorNotes = html.substring(authorNotesPos + 22)
|
|
||||||
authorNotes = authorNotes.substring(0, authorNotes.indexOf('\t\n\t</div>'))
|
|
||||||
authorNotes = authorNotes.trim()
|
|
||||||
authorNotes = authorNotes.replace(trimWhitespace, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
let chapterPos = html.indexOf('<div id="chapter_container">')
|
|
||||||
let chapter = html.substring(chapterPos + 29)
|
|
||||||
|
|
||||||
let pos = chapter.indexOf('\t</div>\t\t\n\t')
|
|
||||||
|
|
||||||
chapter = chapter.substring(0, pos).trim()
|
|
||||||
|
|
||||||
// remove leading and trailing <br /> tags and whitespace
|
|
||||||
chapter = chapter.replace(trimWhitespace, '')
|
|
||||||
|
|
||||||
Promise.all([cleanMarkup(chapter), cleanMarkup(authorNotes)]).then((values) => {
|
|
||||||
let [cleanChapter, cleanAuthorNotes] = values
|
let [cleanChapter, cleanAuthorNotes] = values
|
||||||
|
|
||||||
ch.realWordCount = htmlWordCount(cleanChapter)
|
ch.realWordCount = htmlWordCount(cleanChapter)
|
||||||
|
|
||||||
let content = [
|
let content = [
|
||||||
m.trust(cleanChapter),
|
m.trust(cleanChapter),
|
||||||
cleanAuthorNotes ? m('div#author_notes', {className: authorNotesPos < chapterPos ? 'top' : 'bottom'}, [
|
cleanAuthorNotes ? m('div#author_notes', {className: chapter.notesFirst ? 'top' : 'bottom'}, [
|
||||||
m('p', m('b', 'Author\'s Note:')),
|
m('p', m('b', 'Author\'s Note:')),
|
||||||
m.trust(cleanAuthorNotes)]) : null
|
m.trust(cleanAuthorNotes)]) : null
|
||||||
]
|
]
|
||||||
|
|
||||||
// if author notes are a the beginning of the chapter
|
// if author notes are a the beginning of the chapter
|
||||||
if (cleanAuthorNotes && authorNotesPos < chapterPos) {
|
if (cleanAuthorNotes && chapter.notesFirst) {
|
||||||
content.reverse()
|
content.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +62,7 @@ export function createChapter (ch, html, callback) {
|
||||||
])
|
])
|
||||||
))
|
))
|
||||||
|
|
||||||
callback(chapterPage)
|
return Promise.resolve(chapterPage)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue