import JSZip from 'jszip' import escapeStringRegexp from 'escape-string-regexp' import zeroFill from 'zero-fill' import { XmlEntities } from 'html-entities' import sanitize from 'sanitize-filename' import URL from 'url' import isNode from 'detect-node' import { styleCss, coverstyleCss, titlestyleCss } from './styles' import { cleanMarkup } from './cleanMarkup' import fetchRemote from './fetchRemote' import * as template from './templates' import { mimeMap, containerXml } from './constants' const entities = new XmlEntities() module.exports = class FimFic2Epub { static getStoryId (id) { if (isNaN(id)) { let url = URL.parse(id, false, true) if (url.hostname === 'www.fimfiction.net' || url.hostname === 'fimfiction.net') { let m = url.pathname.match(/^\/story\/(\d+)/) if (m) { id = m[1] } } } return id } static getFilename (storyInfo) { return sanitize(storyInfo.title + ' by ' + storyInfo.author.name + '.epub') } static fetchStoryInfo (storyId, raw = false) { return new Promise((resolve, reject) => { storyId = FimFic2Epub.getStoryId(storyId) let url = 'https://www.fimfiction.net/api/story.php?story=' + storyId fetchRemote(url, (content, type) => { let data try { data = JSON.parse(content) } catch (e) {} if (!data) { reject('Unable to fetch story info') return } if (data.error) { reject(data.error) return } let story = data.story if (raw) { resolve(story) return } // this is so the metadata can be cached. if (!story.chapters) story.chapters = [] delete story.likes delete story.dislikes delete story.views delete story.total_views delete story.comments story.chapters.forEach((ch) => { delete ch.views }) // Add version number story.FIMFIC2EPUB_VERSION = FIMFIC2EPUB_VERSION resolve(story) }) }) } constructor (storyId) { this.storyId = FimFic2Epub.getStoryId(storyId) this.hasDownloaded = false this.isDownloading = false this.zip = null this.chapterContent = [] this.remoteResources = new Map() this.storyInfo = null this.isDownloading = false this.cachedFile = null this.hasCoverImage = false this.coverImageDimensions = {width: 0, height: 0} this.includeTitlePage = true this.categories = [] this.tags = [] } download () { return new Promise((resolve, reject) => { if (this.isDownloading) { reject('Already downloading') return } if (this.hasDownloaded) { resolve() return } this.build().then(resolve).catch(reject) }) } build () { return new Promise((resolve, reject) => { this.isDownloading = true this.zip = new JSZip() this.zip.file('mimetype', 'application/epub+zip') this.zip.file('META-INF/container.xml', containerXml) console.log('Fetching story metadata...') FimFic2Epub.fetchStoryInfo(this.storyId).then((storyInfo) => { this.storyInfo = storyInfo this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id this.filename = FimFic2Epub.getFilename(this.storyInfo) 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.zip.file('OEBPS/toc.ncx', template.createNcx(this)) this.zip.file('OEBPS/Text/nav.xhtml', template.createNav(this)) this.fetchTitlePage(resolve, reject) }).catch(reject) }) } 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('