mirror of
https://github.com/daniel-j/fimfic2epub.git
synced 2024-06-18 18:34:57 +12:00
It'll be fine, I Promise
This commit is contained in:
parent
edb92e364b
commit
9423c22733
|
@ -16,4 +16,6 @@ const STORY_ID = process.argv[2]
|
||||||
|
|
||||||
const ffc = new FimFic2Epub(STORY_ID)
|
const ffc = new FimFic2Epub(STORY_ID)
|
||||||
|
|
||||||
ffc.download()
|
ffc.download().then(() => {
|
||||||
|
ffc.saveStory()
|
||||||
|
})
|
||||||
|
|
|
@ -41,135 +41,142 @@ export default class FimFic2Epub {
|
||||||
}
|
}
|
||||||
|
|
||||||
download () {
|
download () {
|
||||||
if (this.isDownloading) {
|
return new Promise((resolve, reject) => {
|
||||||
alert("Calm down, I'm working on it (it's processing)")
|
if (this.isDownloading) {
|
||||||
return
|
reject()
|
||||||
}
|
return
|
||||||
if (this.cachedBlob) {
|
}
|
||||||
this.saveStory()
|
if (this.cachedBlob) {
|
||||||
return
|
resolve()
|
||||||
}
|
return
|
||||||
this.build()
|
}
|
||||||
|
this.build().then(resolve).catch(reject)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
build () {
|
build () {
|
||||||
this.isDownloading = true
|
return new Promise((resolve, reject) => {
|
||||||
|
this.isDownloading = true
|
||||||
|
|
||||||
this.zip = new JSZip()
|
this.zip = new JSZip()
|
||||||
this.zip.file('mimetype', 'application/epub+zip')
|
this.zip.file('mimetype', 'application/epub+zip')
|
||||||
this.zip.file('META-INF/container.xml', containerXml)
|
this.zip.file('META-INF/container.xml', containerXml)
|
||||||
|
|
||||||
console.log('Fetching story metadata...')
|
console.log('Fetching story metadata...')
|
||||||
|
|
||||||
fetchRemote('https://www.fimfiction.net/api/story.php?story=' + this.storyId, (raw, type) => {
|
fetchRemote('https://www.fimfiction.net/api/story.php?story=' + this.storyId, (raw, type) => {
|
||||||
let data
|
let data
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(raw)
|
data = JSON.parse(raw)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Unable to fetch story json')
|
console.log('Unable to fetch story json')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error(data.error)
|
console.error(data.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.storyInfo = data.story
|
this.storyInfo = data.story
|
||||||
this.storyInfo.chapters = this.storyInfo.chapters || []
|
this.storyInfo.chapters = this.storyInfo.chapters || []
|
||||||
this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id
|
this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id
|
||||||
|
|
||||||
this.zip.file('Styles/style.css', styleCss)
|
this.zip.file('Styles/style.css', styleCss)
|
||||||
this.zip.file('Styles/coverstyle.css', coverstyleCss)
|
this.zip.file('Styles/coverstyle.css', coverstyleCss)
|
||||||
if (this.includeTitlePage) {
|
if (this.includeTitlePage) {
|
||||||
this.zip.file('Styles/titlestyle.css', titlestyleCss)
|
this.zip.file('Styles/titlestyle.css', titlestyleCss)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.zip.file('toc.ncx', template.createNcx(this))
|
this.zip.file('toc.ncx', template.createNcx(this))
|
||||||
this.zip.file('Text/nav.xhtml', template.createNav(this))
|
this.zip.file('Text/nav.xhtml', template.createNav(this))
|
||||||
|
|
||||||
this.fetchTitlePage()
|
this.fetchTitlePage(resolve, reject)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTitlePage () {
|
|
||||||
fetchRemote(this.storyInfo.url, (raw, type) => {
|
|
||||||
this.extractTitlePageInfo(raw, () => this.checkCoverImage())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
extractTitlePageInfo (html, cb) {
|
|
||||||
let descPos = html.indexOf('<div class="description" id="description')
|
|
||||||
descPos = descPos + html.substring(descPos).indexOf('">') + 2
|
|
||||||
html = html.substring(descPos)
|
|
||||||
let ma = html.match(/<a href="(.*?)" class="source">Source<\/a>/)
|
|
||||||
this.storyInfo.source_image = null
|
|
||||||
if (ma) {
|
|
||||||
this.storyInfo.source_image = ma[1]
|
|
||||||
}
|
|
||||||
let endCatsPos = html.indexOf('<hr />')
|
|
||||||
let startCatsPos = html.substring(0, endCatsPos).lastIndexOf('</div>')
|
|
||||||
let catsHtml = html.substring(startCatsPos, endCatsPos)
|
|
||||||
html = html.substring(endCatsPos + 6)
|
|
||||||
|
|
||||||
let categories = []
|
|
||||||
let matchCategory = /<a href="(.*?)" class="(.*?)">(.*?)<\/a>/g
|
|
||||||
for (let c; (c = matchCategory.exec(catsHtml));) {
|
|
||||||
categories.push({
|
|
||||||
url: 'http://www.fimfiction.net' + c[1],
|
|
||||||
className: c[2],
|
|
||||||
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, (html) => {
|
|
||||||
this.storyInfo.description = html
|
|
||||||
this.findRemoteResources('description', 'description', html)
|
|
||||||
cb()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCoverImage () {
|
fetchTitlePage (resolve, reject) {
|
||||||
|
console.log('Fetching index page...')
|
||||||
|
fetchRemote(this.storyInfo.url, (raw, type) => {
|
||||||
|
this.extractTitlePageInfo(raw).then(() => this.checkCoverImage(resolve, reject))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
extractTitlePageInfo (html) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let descPos = html.indexOf('<div class="description" id="description')
|
||||||
|
descPos = descPos + html.substring(descPos).indexOf('">') + 2
|
||||||
|
html = html.substring(descPos)
|
||||||
|
let ma = html.match(/<a href="(.*?)" class="source">Source<\/a>/)
|
||||||
|
this.storyInfo.source_image = null
|
||||||
|
if (ma) {
|
||||||
|
this.storyInfo.source_image = ma[1]
|
||||||
|
}
|
||||||
|
let endCatsPos = html.indexOf('<hr />')
|
||||||
|
let startCatsPos = html.substring(0, endCatsPos).lastIndexOf('</div>')
|
||||||
|
let catsHtml = html.substring(startCatsPos, endCatsPos)
|
||||||
|
html = html.substring(endCatsPos + 6)
|
||||||
|
|
||||||
|
let categories = []
|
||||||
|
let matchCategory = /<a href="(.*?)" class="(.*?)">(.*?)<\/a>/g
|
||||||
|
for (let c; (c = matchCategory.exec(catsHtml));) {
|
||||||
|
categories.push({
|
||||||
|
url: 'http://www.fimfiction.net' + c[1],
|
||||||
|
className: c[2],
|
||||||
|
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, (html) => {
|
||||||
|
this.storyInfo.description = html
|
||||||
|
this.findRemoteResources('description', 'description', html)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCoverImage (resolve, reject) {
|
||||||
this.hasCoverImage = !!this.storyInfo.full_image
|
this.hasCoverImage = !!this.storyInfo.full_image
|
||||||
|
|
||||||
if (this.hasCoverImage) {
|
if (this.hasCoverImage) {
|
||||||
|
@ -180,19 +187,19 @@ export default class FimFic2Epub {
|
||||||
coverImage.src = this.storyInfo.full_image
|
coverImage.src = this.storyInfo.full_image
|
||||||
|
|
||||||
coverImage.addEventListener('load', () => {
|
coverImage.addEventListener('load', () => {
|
||||||
this.processStory(coverImage)
|
this.processStory(resolve, reject, coverImage)
|
||||||
}, false)
|
}, false)
|
||||||
} else {
|
} else {
|
||||||
fetchRemote(this.storyInfo.full_image, (data, type) => {
|
fetchRemote(this.storyInfo.full_image, (data, type) => {
|
||||||
this.processStory(process.sizeOf(data))
|
this.processStory(resolve, reject, process.sizeOf(data))
|
||||||
}, 'buffer')
|
}, 'buffer')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.processStory()
|
this.processStory(resolve, reject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processStory (coverImage) {
|
processStory (resolve, reject, coverImage) {
|
||||||
console.log('Fetching chapters...')
|
console.log('Fetching chapters...')
|
||||||
|
|
||||||
this.fetchChapters(() => {
|
this.fetchChapters(() => {
|
||||||
|
@ -251,21 +258,12 @@ export default class FimFic2Epub {
|
||||||
.then((blob) => {
|
.then((blob) => {
|
||||||
this.cachedBlob = blob
|
this.cachedBlob = blob
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
this.saveStory()
|
resolve()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.zip
|
this.cachedBlob = true
|
||||||
.generateNodeStream({
|
this.isDownloading = false
|
||||||
type: 'nodebuffer',
|
resolve()
|
||||||
streamFiles: true,
|
|
||||||
mimeType: 'application/epub+zip',
|
|
||||||
compression: 'DEFLATE',
|
|
||||||
compressionOptions: {level: 9}
|
|
||||||
})
|
|
||||||
.pipe(process.fs.createWriteStream(this.storyInfo.title + ' by ' + this.storyInfo.author.name + '.epub'))
|
|
||||||
.on('finish', () => {
|
|
||||||
console.log('Saved epub')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -377,13 +375,31 @@ export default class FimFic2Epub {
|
||||||
|
|
||||||
saveStory () {
|
saveStory () {
|
||||||
console.log('Saving epub...')
|
console.log('Saving epub...')
|
||||||
|
|
||||||
|
let filename = this.storyInfo.title + ' by ' + this.storyInfo.author.name + '.epub'
|
||||||
|
|
||||||
|
if (isNode) {
|
||||||
|
this.zip
|
||||||
|
.generateNodeStream({
|
||||||
|
type: 'nodebuffer',
|
||||||
|
streamFiles: true,
|
||||||
|
mimeType: 'application/epub+zip',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: {level: 9}
|
||||||
|
})
|
||||||
|
.pipe(process.fs.createWriteStream(filename))
|
||||||
|
.on('finish', () => {
|
||||||
|
console.log('Saved epub as', filename)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (typeof safari !== 'undefined') {
|
if (typeof safari !== 'undefined') {
|
||||||
blobToDataURL(this.cachedBlob, (dataurl) => {
|
blobToDataURL(this.cachedBlob, (dataurl) => {
|
||||||
document.location.href = dataurl
|
document.location.href = dataurl
|
||||||
alert('Rename downloaded file to .epub')
|
alert('Rename downloaded file to .epub')
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
saveAs(this.cachedBlob, this.storyInfo.title + ' by ' + this.storyInfo.author.name + '.epub')
|
saveAs(this.cachedBlob, filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
src/fetch.js
32
src/fetch.js
|
@ -1,12 +1,38 @@
|
||||||
|
|
||||||
export default function fetch (url, cb, type) {
|
import isNode from 'detect-node'
|
||||||
|
|
||||||
|
function fetchNode (url, cb, responseType) {
|
||||||
|
process.request({
|
||||||
|
url: url,
|
||||||
|
encoding: responseType ? null : 'utf8',
|
||||||
|
headers: {
|
||||||
|
referer: 'http://www.fimfiction.net/'
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
cb()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let type = response.headers['content-type']
|
||||||
|
cb(body, type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function fetch (url, cb, responseType) {
|
||||||
if (url.indexOf('//') === 0) {
|
if (url.indexOf('//') === 0) {
|
||||||
url = 'http:' + url
|
url = 'http:' + url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNode) {
|
||||||
|
fetchNode(url, cb, responseType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let x = new XMLHttpRequest()
|
let x = new XMLHttpRequest()
|
||||||
x.open('get', url, true)
|
x.open('get', url, true)
|
||||||
if (type) {
|
if (responseType) {
|
||||||
x.responseType = type
|
x.responseType = responseType
|
||||||
}
|
}
|
||||||
x.onload = function () {
|
x.onload = function () {
|
||||||
cb(x.response, x.getResponseHeader('content-type'))
|
cb(x.response, x.getResponseHeader('content-type'))
|
||||||
|
|
|
@ -55,11 +55,7 @@ export default function fetchRemote (url, cb, responseType) {
|
||||||
if (url.indexOf('//') === 0) {
|
if (url.indexOf('//') === 0) {
|
||||||
url = 'http:' + url
|
url = 'http:' + url
|
||||||
}
|
}
|
||||||
if (isNode) {
|
if (!isNode && document.location.protocol === 'https:' && url.indexOf('http:') === 0) {
|
||||||
fetchNode(url, cb, responseType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (document.location.protocol === 'https:' && url.indexOf('http:') === 0) {
|
|
||||||
fetchBackground(url, cb, responseType)
|
fetchBackground(url, cb, responseType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -72,17 +68,3 @@ export default function fetchRemote (url, cb, responseType) {
|
||||||
}, responseType)
|
}, responseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchNode (url, cb, responseType) {
|
|
||||||
process.request({
|
|
||||||
url: url,
|
|
||||||
encoding: responseType ? null : 'utf8'
|
|
||||||
}, (error, response, body) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error)
|
|
||||||
cb()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let type = response.headers['content-type']
|
|
||||||
cb(body, type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ const epubButton = document.querySelector('.story_container ul.chapters li.botto
|
||||||
if (epubButton) {
|
if (epubButton) {
|
||||||
epubButton.addEventListener('click', function (e) {
|
epubButton.addEventListener('click', function (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ffc.download()
|
ffc.download().then(() => {
|
||||||
|
ffc.saveStory()
|
||||||
|
})
|
||||||
}, false)
|
}, false)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue