├── .npmignore ├── .gitignore ├── .eslintrc.js ├── .editorconfig ├── commands ├── helpers.js ├── whoami.js ├── logout.js ├── view-offer.js ├── login.js └── download.js ├── LICENSE ├── lib ├── netrc.js └── auth.js ├── cli.js ├── readme.md ├── package.json └── packtpub.js /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .git* 3 | .jshint* 4 | .eslint* 5 | .npmignore 6 | .travis.yml 7 | renovate.json 8 | credentials.json 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | node_modules 3 | 4 | # logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # private stuff 10 | credentials.json 11 | 12 | # editors 13 | .idea 14 | *.sublime-* 15 | 16 | # general 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'semistandard', 3 | rules: { 4 | 'prefer-const': 'error', 5 | 'space-before-function-paren': ['error', { 6 | anonymous: 'always', 7 | named: 'never' 8 | }] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{js,json,scss}] 12 | indent_size = 2 13 | 14 | [*.{html,hbs}] 15 | indent_size = 4 16 | 17 | [{package.json,lint.yml}] 18 | indent_size = 2 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /commands/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const Promise = require('bluebird'); 5 | 6 | module.exports = { wrap }; 7 | 8 | function wrap(handler) { 9 | return function () { 10 | return Promise.resolve(handler.apply(this, arguments)) 11 | .error(err => { 12 | console.error(chalk`\n{bgRed.white.bold Error} ${err.message}`); 13 | process.exit(1); 14 | }); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2016 Dario Vladovic d.vladimyr@gmail.com 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /commands/whoami.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getCredentials } = require('../lib/auth'); 4 | const { wrap } = require('./helpers.js'); 5 | const chalk = require('chalk'); 6 | 7 | module.exports = { 8 | command: 'whoami', 9 | desc: chalk.whiteBright('Show who are you logged as'), 10 | handler: wrap(handler) 11 | }; 12 | 13 | async function handler() { 14 | const creds = await getCredentials(); 15 | if (!creds) { 16 | console.error('You are not logged in!'); 17 | return; 18 | } 19 | console.log('Logged in as:', creds.username); 20 | } 21 | -------------------------------------------------------------------------------- /commands/logout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { removeCredentials } = require('../lib/auth'); 4 | const { wrap } = require('./helpers.js'); 5 | const chalk = require('chalk'); 6 | 7 | module.exports = { 8 | command: 'logout', 9 | desc: chalk.whiteBright('Log out from packtpub.com'), 10 | handler: wrap(handler) 11 | }; 12 | 13 | async function handler() { 14 | const removed = await removeCredentials(); 15 | if (!removed) { 16 | console.error('You are not logged in!'); 17 | return; 18 | } 19 | console.log('Logged out from packtpub.com'); 20 | } 21 | -------------------------------------------------------------------------------- /lib/netrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | const { promisify } = require('util'); 7 | const netrc = require('netrc'); 8 | 9 | const file = path.join(os.homedir(), '.netrc'); 10 | const readFile = promisify(fs.readFile); 11 | const writeFile = promisify(fs.writeFile); 12 | 13 | module.exports = { read, write }; 14 | 15 | async function read() { 16 | const content = await readFile(file, 'utf8'); 17 | return netrc.parse(content); 18 | } 19 | 20 | async function write(conf) { 21 | const content = netrc.format(conf); 22 | await writeFile(file, content, 'utf8'); 23 | return content; 24 | } 25 | -------------------------------------------------------------------------------- /commands/view-offer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { fetchBook } = require('../packtpub'); 4 | const { getCredentials } = require('../lib/auth'); 5 | const { wrap } = require('./helpers'); 6 | const chalk = require('chalk'); 7 | const opn = require('opn'); 8 | 9 | const options = { 10 | w: { 11 | alias: 'web', 12 | type: 'boolean', 13 | describe: 'Open daily offer in a web browser' 14 | } 15 | }; 16 | 17 | module.exports = { 18 | command: 'view-offer', 19 | desc: chalk.whiteBright('Show daily offer'), 20 | builder: options, 21 | handler: wrap(handler) 22 | }; 23 | 24 | async function handler({ web }) { 25 | const auth = await getCredentials(); 26 | if (!auth) { 27 | console.error('You are not logged in!'); 28 | return; 29 | } 30 | const book = await fetchBook(auth); 31 | if (web) return opn(book.url); 32 | console.log(chalk`\n {underline Daily offer:}`); 33 | console.log(chalk`\n {bold # ${book.title}}\n {green ${book.url}}\n`); 34 | } 35 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const chalk = require('chalk'); 6 | const pkg = require('./package.json'); 7 | const alias = { h: 'help', v: 'version' }; 8 | 9 | const usage = chalk` 10 | {bold $0} v${pkg.version} - {yellow ${pkg.description}} 11 | 12 | Usage: 13 | $0 `; 14 | 15 | const footer = chalk` 16 | Homepage: {green ${pkg.homepage}} 17 | Report issue: {green ${pkg.bugs.url}}`.trim(); 18 | 19 | // eslint-disable-next-line no-unused-expressions 20 | require('yargs') 21 | .strict() 22 | .alias(alias) 23 | .commandDir('commands') 24 | .demandCommand(1, '\nYou need at least one command before moving on') 25 | .recommendCommands() 26 | .fail(onError) 27 | .usage(usage) 28 | .epilogue(footer) 29 | .help() 30 | .argv; 31 | 32 | function onError(msg, err, yargs) { 33 | if (err) { 34 | console.error(err); 35 | process.exit(1); 36 | } 37 | yargs.showHelp(); 38 | if (msg) console.error(chalk.bold(msg)); 39 | process.exit(1); 40 | } 41 | -------------------------------------------------------------------------------- /commands/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { login } = require('../packtpub'); 4 | const { prompt } = require('inquirer'); 5 | const { storeCredentials } = require('../lib/auth'); 6 | const { wrap } = require('./helpers'); 7 | const chalk = require('chalk'); 8 | 9 | const notEmpty = input => input && input.length > 0; 10 | 11 | const questions = [{ 12 | type: 'input', 13 | name: 'username', 14 | message: 'Enter your username:', 15 | validate: notEmpty 16 | }, { 17 | type: 'password', 18 | name: 'password', 19 | message: 'Enter your password:', 20 | validate: notEmpty, 21 | mask: '*' 22 | }]; 23 | 24 | module.exports = { 25 | command: 'login', 26 | desc: chalk.whiteBright('Log into packtpub.com'), 27 | handler: wrap(handler) 28 | }; 29 | 30 | async function handler() { 31 | const { username, password } = await prompt(questions); 32 | await login(username, password); 33 | await storeCredentials(username, password); 34 | console.log('\nSuccessfully logged to packtpub.com.'); 35 | } 36 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const netrc = require('./netrc'); 4 | const aes256 = require('aes256'); 5 | 6 | const machine = 'packtpub.com'; 7 | const key = 'AuZAGfKrwdrh4d2VAC51exVaHDDdZ15M'; 8 | const cipher = aes256.createCipher(key); 9 | 10 | module.exports = { 11 | getCredentials, 12 | removeCredentials, 13 | storeCredentials 14 | }; 15 | 16 | async function getCredentials() { 17 | const conf = await netrc.read(); 18 | const auth = conf[machine]; 19 | if (!auth) return auth; 20 | const password = cipher.decrypt(auth.password); 21 | return { username: auth.login, password }; 22 | } 23 | 24 | async function storeCredentials(username, password) { 25 | password = cipher.encrypt(password); 26 | const conf = await netrc.read(); 27 | conf[machine] = { login: username, password }; 28 | await netrc.write(conf); 29 | return { username, password }; 30 | } 31 | 32 | async function removeCredentials() { 33 | const conf = await netrc.read(); 34 | const auth = conf[machine]; 35 | if (!auth) return false; 36 | delete conf[machine]; 37 | await netrc.write(conf); 38 | return true; 39 | } 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # packtpub-cli [![npm package version](https://img.shields.io/npm/v/packtpub-cli.svg)](https://npm.im/packtpub-cli) [![github license](https://img.shields.io/github/license/vladimyr/packtpub-cli.svg)](https://github.com/vladimyr/packtpub-cli/blob/master/LICENSE) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/Flet/semistandard) 2 | 3 | >Download freely offered books from [packtpub.com](https://www.packtpub.com/packt/offers/free-learning) 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install -g packtpub-cli 9 | ``` 10 | 11 | ## Options 12 | 13 | ``` 14 | packtpub v2.0.0 - Download freely offered books from packtpub.com 15 | 16 | Usage: 17 | packtpub 18 | 19 | Commands: 20 | packtpub download Download book from daily offer 21 | packtpub login Log into packtpub.com 22 | packtpub logout Log out from packtpub.com 23 | packtpub view-offer Show daily offer 24 | packtpub whoami Show who are you logged as 25 | 26 | Options: 27 | -h, --help Show help [boolean] 28 | -v, --version Show version number [boolean] 29 | 30 | Homepage: https://github.com/vladimyr/packtpub-cli 31 | Report issue: https://github.com/vladimyr/packtpub-cli/issues 32 | 33 | ``` 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packtpub-cli", 3 | "version": "2.1.3", 4 | "description": "Download freely offered books from packtpub.com", 5 | "main": "packtpub.js", 6 | "bin": { 7 | "packtpub": "cli.js" 8 | }, 9 | "scripts": { 10 | "lint": "eslint ." 11 | }, 12 | "keywords": [ 13 | "ebook", 14 | "pdf", 15 | "epub", 16 | "download", 17 | "free", 18 | "learning", 19 | "packtpub" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/vladimyr/packtpub-cli.git" 24 | }, 25 | "author": { 26 | "email": "d.vladimyr@gmail.com", 27 | "name": "Dario Vladovic", 28 | "url": "github.com/vladimyr" 29 | }, 30 | "license": "WTFPL", 31 | "bugs": { 32 | "url": "https://github.com/vladimyr/packtpub-cli/issues" 33 | }, 34 | "homepage": "https://github.com/vladimyr/packtpub-cli", 35 | "dependencies": { 36 | "aes256": "^1.0.4", 37 | "bluebird": "^3.5.2", 38 | "chalk": "^2.4.1", 39 | "cheerio": "^0.22.0", 40 | "inquirer": "^6.2.0", 41 | "netrc": "^0.1.4", 42 | "opn": "^5.3.0", 43 | "ora": "^3.0.0", 44 | "request": "^2.88.0", 45 | "url-join": "^4.0.0", 46 | "write": "^1.0.3", 47 | "yargs": "^12.0.2" 48 | }, 49 | "devDependencies": { 50 | "eslint": "^5.6.0", 51 | "eslint-config-semistandard": "^12.0.1", 52 | "eslint-config-standard": "^12.0.0", 53 | "eslint-plugin-import": "^2.14.0", 54 | "eslint-plugin-node": "^7.0.1", 55 | "eslint-plugin-promise": "^4.0.1", 56 | "eslint-plugin-standard": "^4.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /commands/download.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { fetchBook } = require('../packtpub'); 4 | const { getCredentials } = require('../lib/auth'); 5 | const { wrap } = require('./helpers.js'); 6 | const chalk = require('chalk'); 7 | const ora = require('ora'); 8 | const path = require('path'); 9 | const writeFile = require('write').stream; 10 | 11 | const options = { 12 | t: { 13 | alias: 'type', 14 | default: 'pdf', 15 | type: 'string', 16 | choices: ['pdf', 'epub', 'mobi'], 17 | describe: 'Select book format' 18 | }, 19 | d: { 20 | alias: 'dir', 21 | type: 'string', 22 | describe: 'Choose download directory' 23 | } 24 | }; 25 | 26 | module.exports = { 27 | command: 'download', 28 | desc: chalk.whiteBright('Download book from daily offer'), 29 | builder: options, 30 | handler: wrap(handler) 31 | }; 32 | 33 | async function handler({ type, dir = process.cwd() }) { 34 | const auth = await getCredentials(); 35 | if (!auth) { 36 | console.error('\nYou are not logged in!'); 37 | return; 38 | } 39 | 40 | const book = await fetchBook({ ...auth, type }); 41 | console.log(chalk`\n {underline Daily offer:}`); 42 | console.log(chalk`\n {bold # ${book.title}}\n {green ${book.url}}\n`); 43 | const filepath = path.join(dir, book.filename(type)); 44 | console.log(chalk`Download location:\n {green ${filepath}}\n`); 45 | 46 | let spinner; 47 | try { 48 | spinner = ora('Downloading...').start(); 49 | await download(book, filepath); 50 | spinner.text = 'Downloaded'; 51 | spinner.succeed(); 52 | } catch (err) { 53 | if (spinner) spinner.fail(); 54 | throw err; 55 | } 56 | } 57 | 58 | function download(book, filepath) { 59 | return new Promise((resolve, reject) => { 60 | book.byteStream().pipe(writeFile(filepath)) 61 | .once('error', reject) 62 | .once('finish', () => resolve(book)); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /packtpub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let request = require('request'); 4 | const cheerio = require('cheerio'); 5 | const Promise = require('bluebird'); 6 | const qs = require('querystring'); 7 | const Url = require('url'); 8 | const urlJoin = require('url-join'); 9 | 10 | request = request.defaults({ 11 | jar: true, 12 | followRedirect: true, 13 | followAllRedirects: true 14 | }); 15 | request = Promise.promisifyAll(request); 16 | 17 | const baseUrl = 'https://www.packtpub.com/'; 18 | const freeOffersUrl = urlJoin(baseUrl, '/packt/offers/free-learning'); 19 | const parseQuery = uri => qs.parse(uri.query); 20 | 21 | module.exports = { login, fetchBook }; 22 | 23 | async function login(username, password) { 24 | const { body: html } = await request.getAsync(baseUrl); 25 | const form = getFormData(html, username, password); 26 | const resp = await request.postAsync(baseUrl, { form }); 27 | const query = parseQuery(resp.request.uri); 28 | if (query.login) return; 29 | throw new Promise.OperationalError('Using invalid credentials!'); 30 | } 31 | 32 | async function fetchBook({ username, password, type = 'pdf' } = {}) { 33 | await login(username, password); 34 | const { body: html } = await request.getAsync(freeOffersUrl); 35 | const book = getBookData(html); 36 | return Object.assign(book, { 37 | filename(type = 'pdf') { 38 | return `${book.title}.${type}`; 39 | }, 40 | byteStream() { 41 | const downloadUrl = urlJoin(baseUrl, '/ebook_download/', book.id, type); 42 | return request.get(downloadUrl); 43 | } 44 | }); 45 | } 46 | 47 | function getFormData(loginPage, username, password) { 48 | const $ = cheerio.load(loginPage); 49 | const hiddenInputs = 'form#packt-user-login-form input[type="hidden"]'; 50 | 51 | const formData = { 52 | email: username, 53 | password, 54 | op: 'Login' 55 | }; 56 | 57 | $(hiddenInputs).each((i, el) => { 58 | const $el = $(el); 59 | formData[$el.attr('name')] = $el.attr('value'); 60 | }); 61 | 62 | return formData; 63 | } 64 | 65 | function getBookData(offerPage) { 66 | const $ = cheerio.load(offerPage); 67 | 68 | const url = urlJoin(baseUrl, $('.dotd-main-book-image a').attr('href')); 69 | const claimUrl = urlJoin(baseUrl, $('.dotd-main-book-form form').attr('action')); 70 | const id = getBookId(claimUrl); 71 | const title = $('.dotd-title h2').text().trim(); 72 | 73 | return { id, title, url, claimUrl }; 74 | } 75 | 76 | function getBookId(claimUrl) { 77 | const path = Url.parse(claimUrl).path; 78 | const tokens = path.replace(/^\//, '').split('/'); 79 | return tokens[1]; 80 | } 81 | --------------------------------------------------------------------------------