├── .editorconfig ├── .eslintrc.json ├── .fossa.yml ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .snyk ├── .tern-project ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── uniread.js ├── devScripts ├── getBooks.sh ├── install.sh └── update.sh ├── index.js ├── package.json ├── screencast └── spritz.gif ├── src ├── index.js ├── interfaces │ ├── cli │ │ └── index.js │ └── index.js ├── methods │ ├── index.js │ └── spritz │ │ └── index.js └── sources │ ├── epub │ └── index.js │ ├── index.js │ ├── pdf │ └── index.js │ └── text │ └── index.js ├── tests ├── books │ └── index.js ├── devTools │ └── index.js ├── index.js └── uniread │ └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | [*.{json,yml,md}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | "tab" 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "double" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ], 24 | "no-console": 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.fossa.yml: -------------------------------------------------------------------------------- 1 | # Generated by FOSSA CLI (https://github.com/fossas/fossa-cli) 2 | # Visit https://fossa.io to learn more 3 | version: 1 4 | cli: 5 | server: https://app.fossa.io 6 | project: github.com/nemanjan00/uniread 7 | fetcher: git 8 | analyze: 9 | modules: 10 | - name: uniread 11 | path: package.json 12 | type: nodejs 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn install and test 21 | run: | 22 | yarn 23 | npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | 3 | /node_modules 4 | /yarn-error.log 5 | /package-lock.json 6 | 7 | # Tern 8 | 9 | /.tern-port 10 | 11 | # Some books for testing 12 | 13 | /books 14 | 15 | # Coverage 16 | 17 | /.nyc_output 18 | 19 | # VScode 20 | 21 | /.vscode 22 | 23 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | 'npm:request:20160119': 7 | - blessed-contrib > picture-tube > request: 8 | patched: '2018-06-07T13:00:00.547Z' 9 | SNYK-JS-LODASH-450202: 10 | - snyk > snyk-nuget-plugin > lodash: 11 | patched: '2019-07-04T07:14:34.395Z' 12 | - blessed-contrib > lodash: 13 | patched: '2019-07-04T07:14:34.395Z' 14 | - html-to-text > lodash: 15 | patched: '2019-07-04T07:14:34.395Z' 16 | - snyk > @snyk/dep-graph > lodash: 17 | patched: '2019-07-04T07:14:34.395Z' 18 | - snyk > snyk-config > lodash: 19 | patched: '2019-07-04T07:14:34.395Z' 20 | - snyk > snyk-nodejs-lockfile-parser > lodash: 21 | patched: '2019-07-04T07:14:34.395Z' 22 | - snyk > lodash: 23 | patched: '2019-07-04T07:14:34.395Z' 24 | - snyk > snyk-php-plugin > lodash: 25 | patched: '2019-07-04T07:14:34.395Z' 26 | - snyk > inquirer > lodash: 27 | patched: '2019-07-04T07:14:34.395Z' 28 | - snyk > snyk-go-plugin > graphlib > lodash: 29 | patched: '2019-07-04T07:14:34.395Z' 30 | - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: 31 | patched: '2019-07-04T07:14:34.395Z' 32 | - snyk > @snyk/dep-graph > graphlib > lodash: 33 | patched: '2019-07-04T07:14:34.395Z' 34 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 6, 3 | "libs": [ 4 | ], 5 | "plugins": { 6 | "node": {}, 7 | "commonjs": {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | sudo: required 4 | node_js: 5 | - 12.20.1 6 | - 14.15.4 7 | - 15.8.0 8 | env: 9 | secure: SuSmgXeOCp+xP6WcrmV0hrh6nInHt2MbZDKde8Q07M2EWdxeYCArofK5tiIZTwgfzsc8yTIFQmLNKpZlRnPsotrYjqpqvOM4xjAgbxWa1n+U9fRqKGLKPfVxucO0QhT9FaBJf2GOZF9PcCDBbHYIO+jbnk1YsBY+nwY24YePrAnkoHBbmaAqIwxhWa9oHul81Pcj7OaOQQWJL65oaDmzqGQd5ZUGSnHvq6axpJfKD2WZWOba47or+rV3144SNUfBqdR6YLJRPB2U9fQMCtnKxi4cnR4BarwVwvNo0CL3sNIcdIYrcnNZM0qZKaznL9ZE1L/C2+b4R+7mBeO0uXJqTUgq8WXxpn0bgYEkk4H7s01yEP6xTf9BL+cUrgTliWrPHhkz8GsS/wa4nKj5uEoeNjlfjOsUgmU0v1qAvmMTAZTS/DymyxHA8b7Tip37sZdRb5Eb91gByLyIHTr0R8cbYTaKQyShJD7n1C10ZlQgW8pvzK831/Hu38Jni5JZifdrbrOvJ5VPxAN9k4msJ/CfhPwdbJtdXdKx8eQPCS8pSaio6poJLLlvjJdhnLuNdOhXTciUXZ2vc3T+YEQ+YeAN9sVU002Kts3UbKX/4JmLY7FVLATjKHpH1LYfWWZeMWWIVQAFz/lF8t4cQJT0gM8uSfp5SpWTZeq4EiQajaNpqnk= 10 | install: 11 | - yarn global add snyk 12 | #- bash ./devScripts/install.sh 13 | - yarn 14 | jobs: 15 | include: 16 | - stage: test 17 | script: 18 | - yarn test 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nemanja Nedeljković 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uniread 2 | 3 | [![Build Status](https://travis-ci.org/nemanjan00/uniread.svg?branch=master)](https://travis-ci.org/nemanjan00/uniread) 4 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fnemanjan00%2Funiread.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fnemanjan00%2Funiread?ref=badge_shield) 5 | [![Backers on Open Collective](https://opencollective.com/uniread/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/uniread/sponsors/badge.svg)](#sponsors) [![Known Vulnerabilities](https://snyk.io/test/github/nemanjan00/uniread/badge.svg)](https://snyk.io/test/github/nemanjan00/uniread) 6 | [![npm](https://img.shields.io/npm/dt/uniread.svg)](https://www.npmjs.com/package/uniread) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | Uniread is [Spritz](http://spritzinc.com/) like CLI fast reading software. 10 | 11 | ![Screencast](https://github.com/nemanjan00/uniread/blob/master/screencast/spritz.gif?raw=true) 12 | 13 | ## Content 14 | 15 | 16 | 17 | * [Supported ebook types](#supported-ebook-types) 18 | * [Installation / update](#installation--update) 19 | * [Usage](#usage) 20 | * [Developers guide](#developers-guide) 21 | * [Yarn package manager](#yarn-package-manager) 22 | * [Getting sample books for testing](#getting-sample-books-for-testing) 23 | * [Coding Style](#coding-style) 24 | * [Linting](#linting) 25 | * [Testing](#testing) 26 | * [Authors](#authors) 27 | * [Contributors](#contributors) 28 | * [Backers](#backers) 29 | * [Sponsors](#sponsors) 30 | 31 | 32 | 33 | ## Supported ebook types 34 | 35 | We try to support as much as possible of ebook formats. If you have any kind of requests, feel free to create feature request in [issues](https://github.com/nemanjan00/uniread/issues). 36 | 37 | * epub (thanks to [julien-c/epub](https://github.com/julien-c/epub) library) 38 | * text (thanks to [pzmarzly](https://github.com/pzmarzly) and PR [#25](https://github.com/nemanjan00/uniread/pull/25)) 39 | * pdf (thanks to [Mozilla pdf.js](https://github.com/mozilla/pdf.js) library) 40 | 41 | ## Installation / update 42 | 43 | ```bash 44 | sudo npm install -g uniread 45 | ``` 46 | 47 | ## Usage 48 | 49 | ```bash 50 | uniread ~/Books/somebook.epub 51 | ``` 52 | 53 | ## Developers guide 54 | 55 | ### Yarn package manager 56 | 57 | For this project development, we are using faster, [yarn](https://yarnpkg.com/lang/en/) package manager. 58 | 59 | To install it, run: 60 | 61 | ```bash 62 | sudo npm install -g yarn 63 | ``` 64 | 65 | After that, you need to install dependencies, using: 66 | 67 | ```bash 68 | yarn 69 | ``` 70 | 71 | ### Getting sample books for testing 72 | 73 | Books source: https://pressbooks.com/sample-books/ 74 | 75 | To download books, run 76 | 77 | ```bash 78 | yarn get-books 79 | ``` 80 | 81 | ### Coding Style 82 | 83 | Coding style of this project is defined inside `.editorconfig` and to use it, download [Editor Config plugin](https://editorconfig.org/) for your text editor. 84 | 85 | #### Linting 86 | 87 | For linting, we are using eslinter and to run it, you can just use: 88 | 89 | ```bash 90 | yarn lint 91 | ``` 92 | 93 | ### Testing 94 | 95 | To run tests, we use mocha. 96 | 97 | To run it, simply: 98 | 99 | ``` 100 | yarn test 101 | ``` 102 | 103 | ## Authors 104 | 105 | * [Nemanja Nedeljković](https://github.com/nemanjan00) 106 | 107 | Also, huge thanks to [these](https://github.com/nemanjan00/uniread/graphs/contributors) people. 108 | 109 | ## Contributors 110 | 111 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 112 | 113 | 114 | 115 | ## Backers 116 | 117 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/uniread#backer)] 118 | 119 | 120 | 121 | ## Sponsors 122 | 123 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uniread#sponsor)] 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /bin/uniread.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const updateNotifier = require("update-notifier"); 4 | const pkg = require("../package.json"); 5 | 6 | const fs = require("fs"); 7 | 8 | const cli = require("../").interfaces.cli; 9 | const spritz = require("../").methods.spritz; 10 | 11 | let timeout = false; 12 | 13 | const run = () => { 14 | let file = process.argv[process.argv.length - 1]; 15 | 16 | if(!fs.existsSync(file)){ 17 | console.log("File does not exist"); 18 | process.exit(1); 19 | } 20 | 21 | spritz.getBook(file).then((book) => { 22 | cli(book); 23 | }).catch(() => { 24 | console.log("Book format not supported"); 25 | process.exit(1); 26 | }); 27 | }; 28 | 29 | setTimeout(() => { 30 | if(!timeout){ 31 | timeout = true; 32 | run(); 33 | } 34 | }, 2000); 35 | 36 | const notifier = updateNotifier({ 37 | pkg: pkg, 38 | callback: (error, response) => { 39 | if(!timeout){ 40 | timeout = true; 41 | if(error){ 42 | run(); 43 | } 44 | 45 | if(response.type == "latest"){ 46 | run(); 47 | } else { 48 | notifier.update = response; 49 | 50 | notifier.notify({defer: false, isGlobal: true}); 51 | 52 | setTimeout(run, 2000); 53 | } 54 | } 55 | } 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /devScripts/getBooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd $(git rev-parse --show-toplevel) 4 | 5 | echo "[*] Creating books folder in the root of project" 6 | 7 | mkdir -p ./books 8 | 9 | echo "[*] Downloading sample books for testing" 10 | 11 | if [ -f ./books/Metamorphosis-jackson.pdf ]; then 12 | echo "[-] ./books/Metamorphosis-jackson.pdf already exists" 13 | else 14 | wget --directory-prefix=./books https://s3-us-west-2.amazonaws.com/pressbooks-samplefiles/MetamorphosisJacksonTheme/Metamorphosis-jackson.pdf 15 | fi 16 | 17 | if [ -f ./books/Metamorphosis-jackson.mobi ]; then 18 | echo "[-] ./books/Metamorphosis-jackson.mobi already exists" 19 | else 20 | wget --directory-prefix=./books https://s3-us-west-2.amazonaws.com/pressbooks-samplefiles/MetamorphosisJacksonTheme/Metamorphosis-jackson.mobi 21 | fi 22 | 23 | if [ -f ./books/Metamorphosis-jackson.epub ]; then 24 | echo "[-] ./books/Metamorphosis-jackson.epub already exists" 25 | else 26 | wget --directory-prefix=./books https://s3-us-west-2.amazonaws.com/pressbooks-samplefiles/MetamorphosisJacksonTheme/Metamorphosis-jackson.epub 27 | fi 28 | 29 | popd 30 | 31 | -------------------------------------------------------------------------------- /devScripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is derived from https://github.com/jpillora/installer 3 | # Original license notice: 4 | # --- 5 | # Copyright © 2016 Jaime Pillora 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | # --- 10 | 11 | TMP_DIR="/tmp/install-fossa-cli" 12 | 13 | function cleanup { 14 | rm -rf $TMP_DIR > /dev/null 15 | } 16 | trap cleanup EXIT 17 | 18 | function fail { 19 | msg=$1 20 | echo "============" 21 | echo "Error: $msg" 1>&2 22 | exit 1 23 | } 24 | 25 | # This function will ask for root privileges before executing a command 26 | # The goal is to allow the user to run this script as a normal user and 27 | # to be asked for authorizations as needed 28 | function askRoot { 29 | if [ $(id -u) -eq 0 ]; then 30 | "$@" 31 | else 32 | echo "The following command needs administrator privileges:" 33 | echo 34 | echo -e "\\t$*" 35 | echo 36 | # The -k flag forces sudo to re-ask the user for their authorization 37 | if command -v sudo > /dev/null; then 38 | sudo -k "$@" 39 | elif command -v su > /dev/null; then 40 | su root -c "/bin/bash $@" 41 | else 42 | fail "neither sudo nor su are installed" 43 | fi 44 | fi 45 | } 46 | 47 | function install { 48 | # Settings 49 | USER="fossas" 50 | REPO="fossa-cli" 51 | BIN="fossa" 52 | INSECURE="false" 53 | OUT_DIR="/usr/local/bin" 54 | GH="https://github.com" 55 | GH_API="https://api.github.com" 56 | 57 | # `bash` check 58 | [ ! "$BASH_VERSION" ] && fail "Please use bash instead" 59 | [ ! -d $OUT_DIR ] && fail "output directory missing: $OUT_DIR" 60 | 61 | # Check for non-POSIX dependencies 62 | GET="" 63 | if command -v curl > /dev/null; then 64 | GET="curl" 65 | if [[ $INSECURE = "true" ]]; then GET="$GET --insecure"; fi 66 | GET="$GET --fail -# -L" 67 | elif command -v wget > /dev/null; then 68 | GET="wget" 69 | if [[ $INSECURE = "true" ]]; then GET="$GET --no-check-certificate"; fi 70 | GET="$GET -qO-" 71 | else 72 | fail "neither wget nor curl are installed" 73 | fi 74 | command -v tar > /dev/null || fail "tar is not installed" 75 | command -v gzip > /dev/null || fail "gzip is not installed" 76 | 77 | # Detect OS 78 | case $(uname -s) in 79 | Darwin) OS="darwin";; 80 | Linux) OS="linux";; 81 | *) fail "unknown os: $(uname -s)";; 82 | esac 83 | 84 | # Detect architecture 85 | if uname -m | grep 64 > /dev/null; then 86 | ARCH="amd64" 87 | elif uname -m | grep arm > /dev/null; then 88 | ARCH="arm" 89 | elif uname -m | grep 386 > /dev/null; then 90 | ARCH="386" 91 | else 92 | fail "unknown arch: $(uname -m)" 93 | fi 94 | 95 | # Fail for unsupported OS/architecture combinations 96 | case "${OS}_${ARCH}" in 97 | "darwin_amd64") ;; 98 | "linux_amd64") ;; 99 | "windows_amd64") ;; 100 | *) fail "No asset for platform ${OS}-${ARCH}";; 101 | esac 102 | 103 | # Enter temporary directory 104 | mkdir -p $TMP_DIR 105 | cd $TMP_DIR || fail "changing directory to $TMP_DIR failed" 106 | 107 | # Download and validate release 108 | bash -c "$GET $GH_API/repos/$USER/$REPO/releases/latest" > latest || fail "downloading latest release metadata failed" 109 | RELEASE=$(grep tag_name latest | cut -d'"' -f4) 110 | VERSION=${RELEASE#v} # remove prefix "v" 111 | 112 | echo "Installing $USER/$REPO $RELEASE..." 113 | RELEASE_URL="$GH/$USER/$REPO/releases/download/$RELEASE" 114 | bash -c "$GET $RELEASE_URL/${REPO}_${VERSION}_${OS}_${ARCH}.tar.gz" > release.tar.gz || fail "downloading release failed" 115 | bash -c "$GET $RELEASE_URL/${REPO}_${VERSION}_checksums.txt" > checksums.txt || fail "downloading checksums failed" 116 | # TODO: checksums are not actually validated. We need to check with `sha256sum` on Linux and `shasum -a 256` on MacOS. 117 | 118 | # Extract release 119 | tar zxf release.tar.gz || fail "tar failed" 120 | rm release.tar.gz 121 | 122 | # Move binary into output directory 123 | chmod +x $BIN || fail "chmod +x failed" 124 | 125 | # Admin privileges are required to run this command 126 | askRoot mv $BIN $OUT_DIR/$BIN || fail "mv failed" 127 | echo "Installed at $OUT_DIR/$BIN" 128 | } 129 | 130 | install 131 | -------------------------------------------------------------------------------- /devScripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wget -O devScripts/install.sh https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh 4 | yarn upgrade 5 | 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // For a better app detection 2 | 3 | module.exports = require("./src"); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniread", 3 | "version": "0.0.28", 4 | "description": "Uniread is Spritz like CLI fast reading software. ", 5 | "main": "index.js", 6 | "repository": "https://github.com/nemanjan00/uniread", 7 | "author": "Nemanja Nedeljkovic ", 8 | "license": "MIT", 9 | "private": false, 10 | "dependencies": { 11 | "blessed": "^0.1.81", 12 | "blessed-contrib": "^4.8.17", 13 | "dateformat": "^3.0.3", 14 | "epub": "https://github.com/nemanjan00/epub.git#67e7822d0be55f9cb0594f23dd034e2358439f63", 15 | "file-type": "^8.0.0", 16 | "html-to-text": "^5.1.1", 17 | "minimist": "^1.2.0", 18 | "opencollective-postinstall": "^2.0.0", 19 | "pdfjs-dist": "^2.8.335", 20 | "snyk": "^1.189.0", 21 | "textversionjs": "^1.1.1", 22 | "update-notifier": "^2.5.0" 23 | }, 24 | "scripts": { 25 | "get-books": "./devScripts/getBooks.sh", 26 | "lint": "./node_modules/.bin/eslint . --fix", 27 | "test": "./node_modules/.bin/mocha --reporter spec --timeout 60000 tests/index.js", 28 | "coverage": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --reporter spec --timeout 60000 tests/index.js", 29 | "watch-cli": "nodemon -I ./bin/uniread.js", 30 | "snyk-protect": "snyk protect", 31 | "prepare": "yarn snyk-protect", 32 | "depUpdate": "./devScripts/update.sh", 33 | "postinstall": "opencollective-postinstall", 34 | "prepublish": "npm run snyk-protect" 35 | }, 36 | "bin": { 37 | "uniread": "./bin/uniread.js" 38 | }, 39 | "devDependencies": { 40 | "chai": "^4.1.2", 41 | "chai-as-promised": "^7.1.1", 42 | "eslint": "^4.19.1", 43 | "husky": "^3.0.1", 44 | "mocha": "^5.2.0", 45 | "nodemon": "^1.18.3", 46 | "nyc": "^12.0.2" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "eslint . ; yarn test" 51 | } 52 | }, 53 | "snyk": true, 54 | "collective": { 55 | "type": "opencollective", 56 | "url": "https://opencollective.com/uniread" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /screencast/spritz.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemanjan00/uniread/a083d010763828f70f964f6f40c6839bbeef05e2/screencast/spritz.gif -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | interfaces: require("./interfaces"), 3 | methods: require("./methods"), 4 | sources: require("./sources") 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /src/interfaces/cli/index.js: -------------------------------------------------------------------------------- 1 | const dateformat = require("dateformat"); 2 | 3 | const blessed = require("blessed"); 4 | const contrib = require("blessed-contrib"); 5 | 6 | module.exports = (book) => { 7 | const player = { 8 | _speed: 250, 9 | _book: undefined, 10 | _current: 0, 11 | 12 | _screen: undefined, 13 | _text: undefined, 14 | _grid: undefined, 15 | _chapterList: undefined, 16 | 17 | _tick: undefined, 18 | 19 | _chapter: -1, 20 | 21 | _report: () => { 22 | return "Speed: " + player._speed + "ms / " + (Math.round(60 * 1000 / player._speed)) + " WPM\nProgress: " + player._current + "/" + player._book.text.length + "\nTime left: " + player._niceTime(); 23 | }, 24 | 25 | _niceTime: () => { 26 | let wordsPerSeconds = (1 / player._speed * 1000); 27 | 28 | let timeLeft = Math.round((player._book.text.length - player._current)/wordsPerSeconds); 29 | 30 | timeLeft = new Date(timeLeft *1000); 31 | 32 | let response = dateformat(timeLeft, "UTC:hh:MM:ss"); 33 | 34 | if(timeLeft < 3600 * 1000){ 35 | response = response.replace("12", "00"); 36 | } 37 | 38 | return response; 39 | }, 40 | 41 | _init: (book) => { 42 | player._screen = blessed.screen({debug: true}); 43 | 44 | var grid = new contrib.grid({rows: 12, cols: 12, screen: player._screen}); 45 | 46 | player._textBox = grid.set(0, 6, 2, 6, blessed.box, { 47 | label: "Book" 48 | }); 49 | 50 | book.links = book.links.filter((chapter) => chapter.name !== undefined); 51 | 52 | let chapters = book.links.map(link => link.name); 53 | 54 | player._book = book; 55 | 56 | player._reportBox = grid.set(2, 6, 2, 6, blessed.box, { 57 | label: "Info" 58 | }); 59 | 60 | player._reportText = blessed.text({ 61 | label: player._report() 62 | }); 63 | 64 | player._reportBox.append(player._reportText); 65 | 66 | player._chapterList = grid.set(0, 0, 11, 6, blessed.list, { 67 | style: { 68 | selected: { 69 | bg: "red" 70 | } 71 | }, 72 | label: "Chapters", 73 | items: chapters, 74 | mouse: true 75 | }); 76 | 77 | let help = grid.set(11, 0, 1, 12, blessed.text, { 78 | style: { 79 | selected: { 80 | bg: "red" 81 | } 82 | }, 83 | label: "help", 84 | }); 85 | 86 | help.append(blessed.text({label: "space pause | j/k Next/prev chapter | -/+ speed up/down | h/l rewind back/forward | q escape "})); 87 | 88 | player._text = blessed.text({ 89 | label: "Book" 90 | }); 91 | 92 | player._textBox.append(player._text); 93 | 94 | player._screen.key(["escape", "q", "C-c"], function() { 95 | return process.exit(0); 96 | }); 97 | 98 | player._screen.key(["space"], function() { 99 | player.togglePlay(); 100 | 101 | player._draw(); 102 | }); 103 | 104 | player._screen.key(["j", "down"], function() { 105 | player._chapterList.down(); 106 | 107 | player._draw(); 108 | }); 109 | 110 | player._screen.key(["k", "up"], function() { 111 | player._chapterList.up(); 112 | 113 | player._draw(); 114 | }); 115 | 116 | player._screen.key(["-"], function() { 117 | player._speed += 10; 118 | 119 | player._draw(); 120 | }); 121 | 122 | player._screen.key(["+", "="], function() { 123 | if(player._speed > 10){ 124 | player._speed -= 10; 125 | } 126 | 127 | player._draw(); 128 | }); 129 | 130 | player._screen.key(["h", "left"], function() { 131 | if(player._current > 0){ 132 | player._current--; 133 | } 134 | 135 | player._draw(); 136 | }); 137 | 138 | player._screen.key(["l", "right"], function() { 139 | player._current++; 140 | 141 | player._draw(); 142 | }); 143 | 144 | player._screen.render(); 145 | 146 | player._chapterList.on("select item", (element, key) => { 147 | player._current = player._book.links[key].word; 148 | }); 149 | 150 | player.togglePlay(); 151 | }, 152 | 153 | _draw: () => { 154 | player._reportText.setLabel(player._report()); 155 | 156 | player._text.setLabel(player._focusText(player._book.text[player._current])); 157 | player._screen.render(); 158 | }, 159 | 160 | _tickFunction: () => { 161 | let next = player._book.text[player._current - 1] || ""; 162 | 163 | player._screen.debug(next); 164 | 165 | player._tick = setTimeout(() => { 166 | let currentChapter = -1; 167 | 168 | player._book.links.some((link, key) => { 169 | currentChapter = key - 1; 170 | 171 | return link.word > player._current + 1; 172 | }); 173 | 174 | if(currentChapter !== player._chapter){ 175 | player._chapterList.select(currentChapter); 176 | } 177 | 178 | player._draw(); 179 | 180 | player._current++; 181 | 182 | player._tickFunction(); 183 | }, ((next.indexOf(",") !== -1 184 | || next.indexOf(".") !== -1 185 | || next.indexOf("?") !== -1 186 | || next.indexOf("!") !== -1 187 | || next.indexOf(";") !== -1)?2:1) * player._speed); 188 | }, 189 | 190 | _focusText: (text) => { 191 | let length = Math.ceil((7 - text.length) / 2); 192 | 193 | for(let i = length; i > 0; i--){ 194 | text = " " + text; 195 | } 196 | 197 | return text+"\n ^"; 198 | }, 199 | 200 | togglePlay: () => { 201 | if(player._tick !== undefined){ 202 | clearTimeout(player._tick); 203 | player._tick = undefined; 204 | } else { 205 | player._tickFunction(); 206 | } 207 | } 208 | }; 209 | 210 | return player._init(book); 211 | }; 212 | 213 | -------------------------------------------------------------------------------- /src/interfaces/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cli: require("./cli") 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /src/methods/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | spritz: require("./spritz") 3 | }; 4 | -------------------------------------------------------------------------------- /src/methods/spritz/index.js: -------------------------------------------------------------------------------- 1 | // TODO: Do tests 2 | 3 | const sources = require("../../sources/"); 4 | const textVersion = require("textversionjs"); 5 | 6 | String.prototype.replaceAll = function(search, replacement) { 7 | var target = this; 8 | return target.replace(new RegExp(search, "g"), replacement); 9 | }; 10 | 11 | const textVersionConfig = { 12 | linkProcess: (_, linkText) => linkText, 13 | imgProcess: (_, alt) => alt, 14 | headingStyle: "hashify" 15 | }; 16 | 17 | module.exports = { 18 | getBook: (file) => { 19 | return new Promise((resolve, reject) => { 20 | sources.detectEngine(file).then((book) => { 21 | module.exports.transformBook(book).then((book) => { 22 | resolve(book); 23 | }); 24 | }).catch((error) => { 25 | reject(error); 26 | }); 27 | }); 28 | }, 29 | transformBook: (book) => { 30 | return new Promise((resolve) => { 31 | let title = book.getTitle(); 32 | 33 | book.getChapters().then((chapters) => { 34 | chapters = chapters.map((chapter) => { 35 | chapter.content = textVersion(chapter.content, textVersionConfig); 36 | 37 | return chapter; 38 | }); 39 | 40 | chapters = module.exports.transformChapters(chapters); 41 | 42 | chapters.title = title; 43 | 44 | resolve(chapters); 45 | }); 46 | }); 47 | }, 48 | transformChapters: (chapters) => { 49 | const text = []; 50 | const links = []; 51 | 52 | chapters.forEach((chapter) => { 53 | links.push({ 54 | name: chapter.title, 55 | word: text.length 56 | }); 57 | 58 | chapter.content = chapter.content 59 | .replaceAll("\r\n", "\n") 60 | .replaceAll("\t", " ") 61 | .replaceAll("\n", " "); 62 | 63 | chapter.content = chapter.content.split(" "); 64 | 65 | chapter.content.forEach((word) => { 66 | text.push(word); 67 | }); 68 | }); 69 | 70 | let book = { 71 | text: text, 72 | links: links 73 | }; 74 | 75 | return book; 76 | } 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /src/sources/epub/index.js: -------------------------------------------------------------------------------- 1 | const EPub = require("epub"); 2 | const htmlToText = require("html-to-text"); 3 | 4 | module.exports = (filename) => { 5 | let book = { 6 | _epub: undefined, 7 | 8 | _init: (filename) => { 9 | return new Promise((resolve) => { 10 | book._epub = new EPub(filename); 11 | 12 | book._epub.on("end", function(){ 13 | resolve(book); 14 | }); 15 | 16 | book._epub.parse(); 17 | }); 18 | }, 19 | _getChapter: (id) => { 20 | return new Promise((resolve) => { 21 | book._epub.getChapter(id, (error, content) => { 22 | resolve(content); 23 | }); 24 | }); 25 | }, 26 | 27 | getTitle: () => { 28 | return book._epub.metadata.title; 29 | }, 30 | getChapters: () => { 31 | return new Promise((resolve) => { 32 | let chaptersContent = []; 33 | 34 | let chapters = book._epub.flow.map(function(chapter){ 35 | let chapterResult = { 36 | id: chapter.id, 37 | title: chapter.title 38 | }; 39 | 40 | chaptersContent.push(book._getChapter(chapter.id)); 41 | 42 | return chapterResult; 43 | }); 44 | 45 | Promise.all(chaptersContent).then((content) => { 46 | content.forEach((content, key) => { 47 | chapters[key].content = htmlToText.fromString(content, { 48 | ignoreHref: true, 49 | ignoreImage: true 50 | 51 | }); 52 | }); 53 | 54 | resolve(chapters); 55 | }); 56 | }); 57 | } 58 | }; 59 | 60 | return book._init(filename); 61 | }; 62 | 63 | -------------------------------------------------------------------------------- /src/sources/index.js: -------------------------------------------------------------------------------- 1 | const fileType = require("file-type"); 2 | const fs = require("fs"); 3 | 4 | const engines = { 5 | epub: require("./epub"), 6 | pdf: require("./pdf"), 7 | text: require("./text") 8 | }; 9 | 10 | 11 | module.exports = { 12 | engines: engines, 13 | _detectEngine: (filename) => { 14 | const data = fs.readFileSync(filename); 15 | 16 | const type = fileType(data); 17 | 18 | if(type !== null && engines[type.ext] !== undefined){ 19 | return engines[type.ext]; 20 | } 21 | 22 | // `file-type` does not detect plaintext files 23 | if(filename.endsWith(".txt")) { 24 | return engines.text; 25 | } 26 | 27 | return false; 28 | }, 29 | detectEngine: (filename) => { 30 | let engine = module.exports._detectEngine(filename); 31 | 32 | if(engine){ 33 | return engine(filename); 34 | } 35 | 36 | return Promise.reject("Engine not found"); 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /src/sources/pdf/index.js: -------------------------------------------------------------------------------- 1 | const pdfJs = require("pdfjs-dist/legacy/build/pdf.js"); 2 | pdfJs.disableWorker = true; 3 | 4 | const fs = require("fs"); 5 | 6 | module.exports = (filename) => { 7 | const book = { 8 | _book: undefined, 9 | 10 | _init: () => { 11 | return new Promise((resolve) => { 12 | fs.readFile(filename, function (err, data) { 13 | var data_array = new Uint8Array(data); 14 | pdfJs.getDocument(data_array).promise.then(function (pdf) { 15 | book._book = pdf; 16 | 17 | resolve(book); 18 | }); 19 | }); 20 | }); 21 | }, 22 | 23 | getTitle: () => { 24 | return filename; 25 | }, 26 | getChapters: () => { 27 | return new Promise((resolve) => { 28 | book._readAllPages().then((content) => { 29 | let chapters = [{ 30 | id: 1, 31 | title: "Content not supported in pdf files yet.", 32 | content: content.join(" ") 33 | }]; 34 | 35 | resolve(chapters); 36 | }); 37 | }); 38 | }, 39 | _readPage: (id) => { 40 | var promise = new Promise(function(resolve){ 41 | book._book.getPage(id).then(function(page){ 42 | page.getTextContent().then(function(page){ 43 | page = page.items; 44 | 45 | page = page.map(function(item){ 46 | return item.str; 47 | }); 48 | 49 | page = page.join(" "); 50 | 51 | resolve(page); 52 | }); 53 | }); 54 | }); 55 | 56 | return promise; 57 | }, 58 | _readAllPages: () => { 59 | var promise = new Promise(function(resolve){ 60 | var pages = []; 61 | 62 | for(var i = 0; i < book._book._pdfInfo.numPages; i++){ 63 | pages.push(book._readPage(i + 1)); 64 | } 65 | 66 | Promise.all(pages).then(function(pages){ 67 | resolve(pages); 68 | }); 69 | }); 70 | 71 | return promise; 72 | } 73 | }; 74 | 75 | return book._init(filename); 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /src/sources/text/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | module.exports = (filename) => { 4 | const book = { 5 | _book: undefined, 6 | _init: () => { 7 | return new Promise((resolve) => { 8 | fs.readFile(filename, function (err, data) { 9 | let text = data.toString(); 10 | book._book = text; 11 | resolve(book); 12 | }); 13 | }); 14 | }, 15 | getTitle: () => { 16 | return filename; 17 | }, 18 | getChapters: () => { 19 | return new Promise((resolve) => { 20 | let chapters = [{ 21 | id: 1, 22 | title: "Content not supported in text files.", 23 | content: book._book 24 | }]; 25 | 26 | resolve(chapters); 27 | }); 28 | } 29 | }; 30 | 31 | return book._init(filename); 32 | }; 33 | -------------------------------------------------------------------------------- /tests/books/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | const chai = require("chai"); 4 | 5 | const chaiAsPromised = require("chai-as-promised"); 6 | 7 | chai.use(chaiAsPromised); 8 | 9 | const expect = chai.expect; 10 | 11 | const epub = require("../../src/sources/epub"); 12 | const pdf = require("../../src/sources/pdf"); 13 | 14 | const sources = require("../../src/sources"); 15 | 16 | const validateBookFormat = (engine, file, done) => { 17 | let promise; 18 | if(engine instanceof Promise){ 19 | promise = engine; 20 | } else { 21 | promise = engine(file); 22 | } 23 | 24 | expect(promise).to.be.a("promise"); 25 | 26 | promise.then((book) => { 27 | try { 28 | expect(book.getTitle).to.be.a("function"); 29 | 30 | expect(book.getTitle()).to.be.a("string"); 31 | 32 | expect(book.getChapters).to.be.a("function"); 33 | 34 | promise = book.getChapters(); 35 | 36 | expect(promise).to.be.a("promise"); 37 | 38 | promise.then((chapters) => { 39 | try { 40 | expect(chapters).to.be.a("array"); 41 | 42 | chapters.forEach((chapter) => { 43 | expect(chapter).to.be.a("object"); 44 | 45 | expect(chapter.title).to.be.a("string"); 46 | expect(chapter.content).to.be.a("string"); 47 | }); 48 | 49 | done(); 50 | } catch (e){ 51 | done(e); 52 | } 53 | }); 54 | } catch (e){ 55 | done(e); 56 | } 57 | }); 58 | }; 59 | 60 | let files = [ 61 | "./books/Metamorphosis-jackson.epub", 62 | "./books/Metamorphosis-jackson.pdf" 63 | ]; 64 | 65 | describe("Book engines", function() { 66 | describe("ePub book engine", function() { 67 | it("Decodes ePub book into uniread format", function(done) { 68 | validateBookFormat(epub, "./books/Metamorphosis-jackson.epub", done); 69 | }); 70 | }); 71 | 72 | 73 | describe("pdf book engine", function() { 74 | it("Decodes pdf book into uniread format", function(done) { 75 | validateBookFormat(pdf, "./books/Metamorphosis-jackson.pdf", done); 76 | }); 77 | }); 78 | 79 | describe("Auto detection book engine", function() { 80 | it("Detects engine", function() { 81 | expect(sources._detectEngine("./books/Metamorphosis-jackson.pdf")).to.equal(sources.engines.pdf); 82 | expect(sources._detectEngine("./books/Metamorphosis-jackson.epub")).to.equal(sources.engines.epub); 83 | expect(sources._detectEngine("./index.js")).to.equal(false); 84 | expect(sources._detectEngine("./books/Metamorphosis-jackson.mobi")).to.equal(false); 85 | }); 86 | }); 87 | 88 | describe("Auto detected engine testing", function() { 89 | files.forEach((file) => { 90 | it("Detects engine for" + file, function(done) { 91 | let engine = sources.detectEngine(file); 92 | 93 | expect(engine).to.be.a("promise"); 94 | 95 | validateBookFormat(engine, file, done); 96 | }); 97 | }); 98 | 99 | it("Detects engine for invalid formats", function() { 100 | expect(sources.detectEngine("./index.js")).to.be.rejected; 101 | expect(sources.detectEngine("./books/Metamorphosis-jackson.mobi")).to.be.rejected; 102 | }); 103 | }); 104 | }); 105 | 106 | -------------------------------------------------------------------------------- /tests/devTools/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | const expect = require("chai").expect; 4 | 5 | const child_process = require("child_process"); 6 | const fs = require("fs"); 7 | 8 | describe("Development tools", function() { 9 | describe("Sample book downloader", function() { 10 | it("Downloads sample books", function() { 11 | child_process.execFileSync("./devScripts/getBooks.sh"); 12 | 13 | expect(fs.existsSync("./books/Metamorphosis-jackson.pdf")).to.equal(true); 14 | expect(fs.existsSync("./books/Metamorphosis-jackson.epub")).to.equal(true); 15 | expect(fs.existsSync("./books/Metamorphosis-jackson.mobi")).to.equal(true); 16 | }); 17 | }); 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* global describe */ 2 | 3 | describe("Unichat", function() { 4 | require("./devTools"); 5 | require("./books"); 6 | require("./uniread"); 7 | }); 8 | 9 | -------------------------------------------------------------------------------- /tests/uniread/index.js: -------------------------------------------------------------------------------- 1 | /* global describe */ 2 | 3 | require("../../src/methods/spritz"); 4 | 5 | describe("Uniread book engine", function() { 6 | }); 7 | 8 | --------------------------------------------------------------------------------