├── .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 | [](https://travis-ci.org/nemanjan00/uniread)
4 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fnemanjan00%2Funiread?ref=badge_shield)
5 | [](#backers) [](#sponsors) [](https://snyk.io/test/github/nemanjan00/uniread)
6 | [](https://www.npmjs.com/package/uniread)
7 | [](https://opensource.org/licenses/MIT)
8 |
9 | Uniread is [Spritz](http://spritzinc.com/) like CLI fast reading software.
10 |
11 | 
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 |
--------------------------------------------------------------------------------