2 |
Lulz.
', 64 | } 65 | ] 66 | }); 67 | 68 | // OR 69 | 70 | const ebook = new EpubPress({ 71 | title: 'Best of HackerNews', 72 | description: 'Favorite articles from HackerNews in May, 2016', 73 | urls: [ 74 | 'http://medium.com/@techBlogger/why-js-is-dead-long-live-php' 75 | ] 76 | }); 77 | ``` 78 | 79 | ##### Publishing 80 | ```js 81 | ebook.publish().then(() => 82 | ebook.download(); // Default epub 83 | // or ebook.email('epubpress@gmail.com') 84 | ).then(() => { 85 | console.log('Success!'); 86 | }).catch((error) => { 87 | console.log(`Error: ${error}`); 88 | }); 89 | ``` 90 | 91 | ##### Checking Status 92 | ```js 93 | ebook.checkStatus().then((status) => { 94 | 95 | }).catch((error) => {}); 96 | ``` 97 | 98 | ##### Event Listening 99 | ```js 100 | const onStatusUpdate = (status) => { console.log(status.message); }; 101 | 102 | // Adding callback 103 | ebook.on('statusUpdate', onStatusUpdate); 104 | 105 | // Removing callback 106 | ebook.removeListener('statusUpdate', onStatusUpdate) 107 | ``` 108 | 109 | ##### Check for updates 110 | 111 | ```js 112 | // epub-press-js updates 113 | EpubPress.checkForUpdates().then((message) => { 114 | console.log(message); // Undefined if no update required 115 | }); 116 | 117 | // epub-press-chrome updates 118 | EpubPress.checkForUpdates('epub-press-chrome', '0.9.0').then((message) => { 119 | console.log(message); 120 | }); 121 | ``` 122 | 123 | ### API 124 | 125 | ##### **`new EpubPress(metadata) => ebook`** 126 | - `metadata.sections`: Object with the url and html for a chapter. 127 | - `metadata.urls`: Array of urls. 128 | - `metadata.title`: Title for the book. 129 | - `metadata.description`: Description for the book. 130 | - `metadata.filetype`: File format to use for downloads. 131 | 132 | ##### **`ebook.publish() => Promise`** 133 | 134 | ##### **`ebook.download(filetype) => Promise`** 135 | - `filetype`: `'mobi'` or `'epub'` (Default `'epub'`) 136 | 137 | ##### **`ebook.email(email, filetype) => Promise`** 138 | - `filetype`: `'mobi'` or `'epub'` (Default `'epub'`) 139 | - `email`: Email address to deliver ebook to. 140 | 141 | ##### **`ebook.checkStatus() => Promise => status`** 142 | - `status.progress`: Percentage complete. (0 -> 100) 143 | - `status.message`: Status message. 144 | 145 | ##### **`ebook.on('statusUpdate', (status) => {}) => callback`** 146 | - `status.progress`: Percentage complete. (0 -> 100) 147 | - `status.message`: Description of current step. 148 | 149 | ##### **`ebook.removeListener(eventName, callback)`** 150 | - `eventName`: Name of the event `callback` exists on. 151 | - `callback`: Listener to be removed. 152 | 153 | ##### **`EpubPress.checkForUpdates(clientName, clientVersion) => Promise => Update Message | undefined`** 154 | - `clientName`: EpubPress client library to check. (Default: `epub-press-js`) 155 | - `clientVersion`: Version of client. (Default: `EpubPress.VERSION`) 156 | 157 | ### Issues 158 | 159 | - Safari downloads the file as `Unknown`. You then must manually add the file extension (eg. `.epub` or `.mobi`) 160 | 161 | Feel free to report any other issues: 162 | 163 | - In the Github repo: https://github.com/haroldtreen/epub-press-clients 164 | - By email: support@epub.press 165 | 166 | ### Related 167 | 168 | - Website: https://epub.press 169 | - Chrome Extension: https://chrome.google.com/webstore/detail/epubpress-create-ebooks-f/pnhdnpnnffpijjbnhnipkehhibchdeok 170 | -------------------------------------------------------------------------------- /packages/epub-press-js/epub-press.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import { saveAs } from 'file-saver'; 3 | 4 | import packageInfo from './package.json'; 5 | 6 | function isBrowser() { 7 | return typeof window !== 'undefined'; 8 | } 9 | 10 | function log(...args) { 11 | if (EpubPress.DEBUG) { 12 | console.log(...args); 13 | } 14 | } 15 | 16 | function isDownloadable(book) { 17 | if (!book.getId()) { 18 | throw new Error('Book has no id. Have you published?'); 19 | } 20 | } 21 | 22 | function saveFile(filename, data) { 23 | if (isBrowser()) { 24 | let file; 25 | if (typeof File === 'function') { 26 | file = new File([data], filename); 27 | } else { 28 | file = new Blob([data], { type: 'application/octet-stream' }); 29 | } 30 | saveAs(file, filename); 31 | } else { 32 | const fs = require('fs'); 33 | fs.writeFileSync(filename, data); 34 | } 35 | } 36 | 37 | function getPublishParams(bookData) { 38 | const body = { 39 | title: bookData.title, 40 | description: bookData.description, 41 | }; 42 | 43 | if (bookData.sections) { 44 | body.sections = bookData.sections; 45 | } else { 46 | body.urls = bookData.urls.slice(); 47 | } 48 | 49 | return { 50 | method: 'POST', 51 | headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, 52 | body: JSON.stringify(body), 53 | }; 54 | } 55 | 56 | function trackPublishStatus(book) { 57 | return new Promise((resolve, reject) => { 58 | const trackingCallback = (checkStatusCounter) => { 59 | book.checkStatus().then((status) => { 60 | book.emit('statusUpdate', status); 61 | if (Number(status.progress) >= 100) { 62 | resolve(book); 63 | } else if (checkStatusCounter >= EpubPress.CHECK_STATUS_LIMIT) { 64 | reject(new Error(EpubPress.ERROR_CODES[503])); 65 | } else { 66 | setTimeout(trackingCallback, EpubPress.POLL_RATE, checkStatusCounter + 1); 67 | } 68 | }).catch(reject); 69 | }; 70 | trackingCallback(1); 71 | }); 72 | } 73 | 74 | function checkResponseStatus(response) { 75 | const defaultErrorMsg = EpubPress.ERROR_CODES[response.status]; 76 | if (response.status >= 200 && response.status < 300) { 77 | return response; 78 | } else if (response.body) { 79 | return response.json().then((body) => { 80 | const hasErrorMsg = body.errors && body.errors.length > 0; 81 | const errorMsg = hasErrorMsg ? body.errors[0].detail : defaultErrorMsg; 82 | return Promise.reject(new Error(errorMsg)); 83 | }); 84 | } 85 | const error = new Error(defaultErrorMsg); 86 | return Promise.reject(error); 87 | } 88 | 89 | function normalizeError(err) { 90 | const knownError = EpubPress.ERROR_CODES[err.message] || EpubPress.ERROR_CODES[err.name]; 91 | if (knownError) { 92 | return new Error(knownError); 93 | } 94 | return err; 95 | } 96 | 97 | function compareVersion(currentVersion, apiVersion) { 98 | const apiVersionNumber = Number(apiVersion.minCompatible.replace('.', '')); 99 | const currentVersionNumber = Number(currentVersion.replace('.', '')); 100 | 101 | if (apiVersionNumber > currentVersionNumber) { 102 | return apiVersion.message; 103 | } 104 | return null; 105 | } 106 | 107 | function buildQuery(params) { 108 | const query = ['email', 'filetype'].map((paramName) => 109 | params[paramName] ? `${paramName}=${encodeURIComponent(params[paramName])}` : '' 110 | ).filter(paramStr => paramStr).join('&'); 111 | return query ? `?${query}` : ''; 112 | } 113 | 114 | class EpubPress { 115 | static checkForUpdates(client = 'epub-press-js', version = EpubPress.getVersion()) { 116 | return new Promise((resolve, reject) => { 117 | fetch(EpubPress.getVersionUrl()) 118 | .then(checkResponseStatus) 119 | .then(response => response.json()) 120 | .then((versionData) => { 121 | const clientVersionData = versionData.clients[client]; 122 | if (clientVersionData) { 123 | resolve(compareVersion(version, clientVersionData)); 124 | } else { 125 | reject(new Error(`Version data for ${client} not found.`)); 126 | } 127 | }) 128 | .catch((e) => { 129 | const error = normalizeError(e); 130 | log('Version check failed', error); 131 | reject(error); 132 | }); 133 | }); 134 | } 135 | 136 | static getPublishUrl() { 137 | return this.prototype.getPublishUrl(); 138 | } 139 | 140 | static getVersionUrl() { 141 | return `${EpubPress.BASE_API}/version`; 142 | } 143 | 144 | static getVersion() { 145 | return EpubPress.VERSION; 146 | } 147 | 148 | constructor(bookData) { 149 | const date = Date().slice(0, Date().match(/\d{4}/).index + 4); 150 | const defaults = { 151 | title: `EpubPress - ${date}`, 152 | description: undefined, 153 | sections: undefined, 154 | urls: undefined, 155 | filetype: 'epub', 156 | }; 157 | 158 | this.bookData = Object.assign({}, defaults, bookData); 159 | this.events = {}; 160 | } 161 | 162 | on(eventName, callback) { 163 | if (!this.events[eventName]) { 164 | this.events[eventName] = []; 165 | } 166 | 167 | this.events[eventName].push(callback); 168 | return callback; 169 | } 170 | 171 | emit(eventName, ...args) { 172 | if (this.events[eventName]) { 173 | this.events[eventName].forEach((cb) => { 174 | cb(...args); 175 | }); 176 | } 177 | } 178 | 179 | removeListener(eventName, callback) { 180 | if (!this.events[eventName]) { 181 | return; 182 | } 183 | 184 | const index = this.events[eventName].indexOf(callback); 185 | if (index >= 0) { 186 | this.events[eventName].splice(index, 1); 187 | } 188 | } 189 | 190 | getUrls() { 191 | let bookUrls = []; 192 | const { urls, sections } = this.bookData; 193 | 194 | if (urls) { 195 | bookUrls = urls.slice(); 196 | } else if (sections) { 197 | bookUrls = sections.map((section) => section.url); 198 | } 199 | return bookUrls; 200 | } 201 | 202 | getFiletype(providedFiletype) { 203 | const filetype = providedFiletype || this.bookData.filetype; 204 | if (!filetype) { 205 | return 'epub'; 206 | } 207 | 208 | return ['mobi', 'epub'].find((type) => filetype.toLowerCase() === type) || 'epub'; 209 | } 210 | 211 | getEmail() { 212 | return this.bookData.email; 213 | } 214 | 215 | getTitle() { 216 | return this.bookData.title; 217 | } 218 | 219 | getDescription() { 220 | return this.bookData.description; 221 | } 222 | 223 | getId() { 224 | return this.bookData.id; 225 | } 226 | 227 | getStatusUrl() { 228 | return `${EpubPress.getPublishUrl()}/${this.getId()}/status`; 229 | } 230 | 231 | getPublishUrl() { 232 | return `${EpubPress.BASE_API}/books`; 233 | } 234 | 235 | getDownloadUrl(filetype = this.getFiletype()) { 236 | const query = buildQuery({ filetype }); 237 | return `${this.getPublishUrl()}/${this.getId()}/download${query}`; 238 | } 239 | 240 | getEmailUrl(email = this.getEmail(), filetype = this.getFiletype()) { 241 | const query = buildQuery({ email, filetype }); 242 | return `${this.getPublishUrl()}/${this.getId()}/email${query}`; 243 | } 244 | 245 | checkStatus() { 246 | return new Promise((resolve, reject) => { 247 | fetch(this.getStatusUrl()) 248 | .then(checkResponseStatus) 249 | .then(response => response.json()) 250 | .then((body) => { 251 | resolve(body); 252 | }) 253 | .catch((e) => { 254 | const error = normalizeError(e); 255 | reject(error); 256 | }); 257 | }); 258 | } 259 | 260 | publish() { 261 | if (this.isPublishing) { 262 | return Promise.reject(new Error('Publishing in progress')); 263 | } else if (this.getId()) { 264 | return Promise.resolve(this.getId()); 265 | } 266 | this.isPublishing = true; 267 | return new Promise((resolve, reject) => { 268 | fetch(this.getPublishUrl(), getPublishParams(this.bookData)) 269 | .then(checkResponseStatus) 270 | .then(response => response.json()) 271 | .then(({ id }) => { 272 | this.bookData.id = id; 273 | return trackPublishStatus(this).then(() => { 274 | resolve(id); 275 | }); 276 | }) 277 | .catch((e) => { 278 | this.isPublishing = false; 279 | const error = normalizeError(e); 280 | log('EbupPress: Publish failed', error); 281 | reject(error); 282 | }); 283 | }); 284 | } 285 | 286 | download(filetype) { 287 | return new Promise((resolve, reject) => { 288 | isDownloadable(this); 289 | 290 | fetch(this.getDownloadUrl(filetype)) 291 | .then(checkResponseStatus) 292 | .then((response) => { 293 | return response.blob ? response.blob() : response.buffer(); 294 | }) 295 | .then((bookFile) => { 296 | if (process.env.NODE_ENV !== 'test') { 297 | const filename = `${this.getTitle()}.${filetype || this.getFiletype()}`; 298 | saveFile(filename, bookFile); 299 | } 300 | resolve(); 301 | }) 302 | .catch((e) => { 303 | const error = normalizeError(e); 304 | log('EpubPress: Download failed', error); 305 | reject(error); 306 | }); 307 | }); 308 | } 309 | 310 | email(email, filetype) { 311 | return new Promise((resolve, reject) => { 312 | if (!email) { 313 | return reject(new Error('EpubPress: No email provided.')); 314 | } 315 | 316 | isDownloadable(this); 317 | 318 | return fetch(this.getEmailUrl(email, filetype)) 319 | .then(checkResponseStatus) 320 | .then(() => { 321 | log('EpubPress: Book delivered.'); 322 | resolve(); 323 | }) 324 | .catch((e) => { 325 | const error = normalizeError(e); 326 | log('EpubPress: Email delivery failed.'); 327 | reject(error); 328 | }); 329 | }); 330 | } 331 | } 332 | 333 | EpubPress.BASE_URL = packageInfo.baseUrl; 334 | EpubPress.BASE_API = `${EpubPress.BASE_URL}/api/v1`; 335 | 336 | EpubPress.VERSION = packageInfo.version; 337 | EpubPress.POLL_RATE = 3000; 338 | EpubPress.CHECK_STATUS_LIMIT = 40; 339 | 340 | EpubPress.ERROR_CODES = { 341 | // Book Create Errors 342 | 0: 'Server is down. Please try again later.', 343 | 'Failed to fetch': 'Server is down. Please try again later.', 344 | 'FetchError': 'Server is down. Please try again later.', 345 | 400: 'There was a problem with the request. Is EpubPress up to date?', 346 | 404: 'Resource not found.', 347 | 422: 'Request contained invalid data.', 348 | 500: 'Unexpected server error.', 349 | 503: 'Server took too long to respond.', 350 | timeout: 'Request took too long to complete.', 351 | error: undefined, 352 | // Download Errors 353 | SERVER_FAILED: 'Server error while downloading.', 354 | SERVER_BAD_CONTENT: 'Book could not be found', 355 | }; 356 | 357 | export default EpubPress; 358 | -------------------------------------------------------------------------------- /packages/epub-press-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epub-press-js", 3 | "version": "0.5.3", 4 | "description": "Javascript client for building books with EpubPress.", 5 | "homepage": "https://github.com/haroldtreen/epub-press-clients#readme", 6 | "baseUrl": "https://epub.press", 7 | "main": "build/index.js", 8 | "directories": { 9 | "tests": "test" 10 | }, 11 | "scripts": { 12 | "test": "export NODE_ENV=test && open http://localhost:5001/tests && webpack-dev-server", 13 | "start": "export NODE_ENV=development && webpack --watch --progress --color", 14 | "build": "export NODE_ENV=development && webpack", 15 | "build-prod": "export NODE_ENV=production && webpack --optimize-minimize --optimize-dedupe", 16 | "preversion": "npm run-script build-prod", 17 | "prepublish": "npm run build-prod" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/haroldtreen/epub-press-clients.git" 22 | }, 23 | "keywords": [ 24 | "epub", 25 | "publishing", 26 | "productivity", 27 | "client", 28 | "epubpress", 29 | "ebooks", 30 | "content", 31 | "extraction" 32 | ], 33 | "author": "EpubPress", 34 | "license": "GPL-3.0+", 35 | "bugs": { 36 | "url": "https://github.com/haroldtreen/epub-press-clients/issues" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.7.4", 40 | "@babel/plugin-transform-runtime": "^7.7.4", 41 | "@babel/preset-env": "^7.7.4", 42 | "@babel/runtime": "^7.7.4", 43 | "babel-loader": "^8.0.6", 44 | "chai": "^3.5.0", 45 | "fetch-mock": "^5.13.1", 46 | "mocha": "^5.2.0", 47 | "mocha-loader": "^2.0.1", 48 | "sinon": "^7.5.0", 49 | "webpack": "^4.41.2", 50 | "webpack-cli": "^3.3.10", 51 | "webpack-dev-server": "^3.9.0" 52 | }, 53 | "dependencies": { 54 | "bluebird": "^3.4.6", 55 | "file-saver": "^1.3.3", 56 | "isomorphic-fetch": "^2.2.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/epub-press-js/tests/browserTest.html: -------------------------------------------------------------------------------- 1 | 2 | 25 | -------------------------------------------------------------------------------- /packages/epub-press-js/tests/epub-press-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import fetchMock from 'fetch-mock'; 3 | 4 | import EpubPress from '../epub-press'; 5 | import packageInfo from '../package.json'; 6 | import Helpers from './helpers'; 7 | 8 | const { isError } = Helpers; 9 | 10 | describe('EpubPress', () => { 11 | describe('.BASE_URLS', () => { 12 | it('has a BASE_URL', () => { 13 | assert.equal(EpubPress.BASE_URL, packageInfo.baseUrl); 14 | }); 15 | 16 | it('has a BASE_API', () => { 17 | assert.include(EpubPress.BASE_API, EpubPress.BASE_URL); 18 | assert.include(EpubPress.BASE_API, 'api'); 19 | }); 20 | }); 21 | 22 | describe('versions', () => { 23 | const VERSION_RESPONSE = { 24 | version: '0.3.0', 25 | minCompatible: '0.8.0', 26 | message: 'An update for EpubPress is available.', 27 | clients: { 28 | 'epub-press-chrome': { 29 | minCompatible: '0.8.0', 30 | message: 'Please update epub-press-chrome', 31 | }, 32 | 'epub-press-js': { 33 | minCompatible: '0.8.0', 34 | message: 'An update for EpubPress is available.', 35 | }, 36 | }, 37 | }; 38 | 39 | describe('.checkForUpdates', () => { 40 | beforeEach(() => { 41 | fetchMock.get(EpubPress.getVersionUrl(), VERSION_RESPONSE); 42 | }); 43 | 44 | it('can detect when an update is needed', () => { 45 | EpubPress.VERSION = '0.7.0'; 46 | 47 | return EpubPress.checkForUpdates().then((result) => { 48 | assert.isTrue(fetchMock.called(EpubPress.getVersionUrl())); 49 | assert.equal(result, VERSION_RESPONSE.message); 50 | }); 51 | }); 52 | 53 | it('can detect when an update is not needed', () => { 54 | EpubPress.VERSION = '0.8.1'; 55 | 56 | return EpubPress.checkForUpdates().then((result) => { 57 | assert.isTrue(fetchMock.called(EpubPress.getVersionUrl())); 58 | assert.isFalse(!!result); 59 | }); 60 | }); 61 | 62 | it('can tell version updates for client libraries', () => 63 | EpubPress.checkForUpdates('epub-press-chrome', '0.7.0') 64 | .then((result) => { 65 | assert.isTrue(fetchMock.called(EpubPress.getVersionUrl())); 66 | assert.include(result, 'epub-press-chrome'); 67 | }) 68 | ); 69 | 70 | it('can check for client library version updates', () => 71 | EpubPress.checkForUpdates('epub-press-chrome', '0.9.0') 72 | .then((result) => { 73 | assert.isTrue(fetchMock.called(EpubPress.getVersionUrl())); 74 | assert.isFalse(!!result); 75 | }) 76 | ); 77 | 78 | it('rejects invalid clients', () => 79 | EpubPress.checkForUpdates('epub-press-iphone').then(() => 80 | Promise.reject('#checkForUpdates should reject invalid clients.') 81 | ) 82 | .catch(isError) 83 | .then((e) => { 84 | assert.include(e.message, 'epub-press-iphone'); 85 | }) 86 | ); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/epub-press-js/tests/helpers.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | 3 | const Helpers = {}; 4 | 5 | Helpers.isError = (e) => { 6 | if (typeof e === 'string') { 7 | return Promise.reject(new Error(e)); 8 | } 9 | return Promise.resolve(e); 10 | }; 11 | 12 | export default Helpers; 13 | -------------------------------------------------------------------------------- /packages/epub-press-js/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |