├── .env.example ├── .gitignore ├── .nvmrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── __tests__ └── index.test.ts ├── docs └── images │ └── readmoo-api.gif ├── jest.config.js ├── out ├── cli.d.ts ├── cli.js ├── cli.js.map ├── index.d.ts ├── index.js ├── index.js.map ├── types.d.ts ├── types.js └── types.js.map ├── package-lock.json ├── package.json ├── src ├── cli.ts └── index.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Only for testing 2 | CLOUDFRONT_KEY_PAIR_ID= 3 | CLOUDFRONT_POLICY= 4 | CLOUDFRONT_SIGNATURE= 5 | COOKIE= 6 | 7 | EMAIL= 8 | PASSWORD= 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.3 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach by Process ID", 11 | "processId": "${command:PickProcess}" 12 | }, 13 | { 14 | "name": "Debug Jest Tests", 15 | "type": "node", 16 | "request": "launch", 17 | "runtimeArgs": [ 18 | "--inspect-brk", 19 | "${workspaceRoot}/node_modules/.bin/jest", 20 | "--runInBand", 21 | "--coverage", 22 | "false", 23 | "--setupFiles", 24 | "dotenv/config" 25 | ], 26 | "console": "integratedTerminal", 27 | "internalConsoleOptions": "neverOpen", 28 | "port": 9229 29 | }, 30 | { 31 | "type": "node", 32 | "request": "launch", 33 | "name": "Launch Program", 34 | "program": "${workspaceFolder}/index.js", 35 | "preLaunchTask": "tsc: build - tsconfig.json", 36 | "outFiles": [ 37 | "${workspaceFolder}/out/**/*.js" 38 | ] 39 | }, 40 | { 41 | "name": "Current TS File", 42 | "type": "node", 43 | "request": "launch", 44 | "args": ["${relativeFile}"], 45 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 46 | "sourceMaps": true, 47 | "cwd": "${workspaceRoot}", 48 | "protocol": "inspector", 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readmoo API Client 2 | 3 | 僅供技術研究,不負責本工具造成的任何侵權問題。自己的書自己買。 4 | 5 | ## Installation & Usage 6 | 7 | Install with npm global option: 8 | 9 | ```bash 10 | npm i Yukaii/readmoo-api#v1.0.0 -g 11 | ``` 12 | 13 | And type the command `readmoo-dl`: 14 | 15 | ```bash 16 | readmoo-dl 17 | ``` 18 | 19 | The command will ask for your login email & password, enter then choose books to download: 20 | 21 | ![demo](docs/images/readmoo-api.gif) 22 | 23 | ## Related Projects 24 | 25 | 不顧北京反對,一起拖下水 26 | 27 | - [readmoo-dl](https://github.com/msglight4874/readmoo-dl) 讀墨 Epub 下載, written in Ruby by [@msglight4874][1] 28 | - [books-dl](https://github.com/msglight4874/books-dl) 博客來 Epub 下載, written in Ruby by [@msglight4874][1] 29 | 30 | ## LICENSE 31 | 32 | WTFPL 33 | 34 | [1]: https://github.com/msglight4874 35 | -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { checkLogin, login } from '../src' 2 | import { assert } from 'chai' 3 | 4 | test('#login', async function () { 5 | await login(process.env.EMAIL, process.env.PASSWORD) 6 | 7 | assert.isTrue(await checkLogin()) 8 | }) 9 | 10 | -------------------------------------------------------------------------------- /docs/images/readmoo-api.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chusiang/readmoo-api/2335dc28d2e54ca450be0023d1683e9e6bc55d11/docs/images/readmoo-api.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /out/cli.d.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /out/cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | "use strict"; 3 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const inquirer = require("inquirer"); 13 | const BluebirdPromise = require("bluebird"); 14 | const index_1 = require("./index"); 15 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 16 | (() => __awaiter(this, void 0, void 0, function* () { 17 | const isLogin = yield index_1.checkLogin(); 18 | if (!isLogin) { 19 | const { email } = yield inquirer.prompt({ 20 | type: 'input', 21 | name: 'email', 22 | message: '請輸入您的 Email' 23 | }); 24 | const { password } = yield inquirer.prompt({ 25 | type: 'password', 26 | name: 'password', 27 | message: '以及您的密碼' 28 | }); 29 | yield index_1.login(email, password); 30 | } 31 | const booksData = yield index_1.listBooks(); 32 | const { books: selectedBooks } = yield inquirer.prompt({ 33 | type: 'checkbox', 34 | name: 'books', 35 | message: '選擇要下載的書', 36 | choices: booksData.map(({ title, id }) => ({ name: title, value: { id, title }, short: title })) 37 | }); 38 | const outputFiles = yield BluebirdPromise.mapSeries(selectedBooks, ({ id, title }) => __awaiter(this, void 0, void 0, function* () { 39 | try { 40 | const outputDir = yield index_1.downloadBook(id); 41 | const filename = yield index_1.generateEpub(title, outputDir); 42 | yield sleep(200); 43 | return filename; 44 | } 45 | catch (e) { 46 | console.error(`下載 "${title}" 失敗!`); 47 | return; 48 | } 49 | })); 50 | console.log('書籍已下載至:'); 51 | console.log(outputFiles.filter(Boolean).map(f => '- ' + f).join('\n')); 52 | }))(); 53 | //# sourceMappingURL=cli.js.map -------------------------------------------------------------------------------- /out/cli.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;;;;;;AAEA,qCAAoC;AACpC,4CAA4C;AAE5C,mCAMgB;AAEhB,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAEpE;AAAA,CAAC,GAAS,EAAE;IACX,MAAM,OAAO,GAAG,MAAO,kBAAU,EAAE,CAAA;IACnC,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACtC,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,aAAa;SACvB,CAAC,CAAA;QAEF,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACzC,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,QAAQ;SAClB,CAAC,CAAA;QAEF,MAAM,aAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;KAC7B;IAED,MAAM,SAAS,GAAG,MAAM,iBAAS,EAAE,CAAA;IAEnC,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;QACrD,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,SAAS;QAClB,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;KACjG,CAAC,CAAA;IAEF,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,SAAS,CAAC,aAAa,EAAE,CAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QACzF,IAAI;YACF,MAAM,SAAS,GAAG,MAAM,oBAAY,CAAC,EAAE,CAAC,CAAA;YACxC,MAAM,QAAQ,GAAG,MAAM,oBAAY,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;YACrD,MAAM,KAAK,CAAC,GAAG,CAAC,CAAA;YAChB,OAAO,QAAQ,CAAA;SAChB;QAAC,OAAO,CAAC,EAAE;YACV,OAAO,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,CAAA;YAClC,OAAM;SACP;IACH,CAAC,CAAA,CAAC,CAAA;IAEF,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACtB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;AACxE,CAAC,CAAA,CAAC,EAAE,CAAA"} -------------------------------------------------------------------------------- /out/index.d.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | declare class Credential { 3 | private credential; 4 | constructor(); 5 | readmoo: string; 6 | cloudFrontKeyPairId: string; 7 | cloudFrontPolicy: string; 8 | cloudFrontSignature: string; 9 | save(): void; 10 | private setAWSCredential; 11 | saveCredentials(res: AxiosResponse): void; 12 | getHeaders(): { 13 | Cookie: string; 14 | }; 15 | private load; 16 | } 17 | export declare const credential: Credential; 18 | export declare function login(email: string, password: string): Promise; 19 | export declare function checkLogin(): Promise; 20 | export declare function listBooks(): Promise; 21 | export declare function downloadBook(bookId: string): Promise; 22 | export declare function generateEpub(title: string, dir: string, outputFolder?: string): Promise; 23 | export {}; 24 | -------------------------------------------------------------------------------- /out/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | require('dotenv').config(); 12 | const fs = require("fs-extra"); 13 | const path = require("path"); 14 | const os = require("os"); 15 | const axios_1 = require("axios"); 16 | const xmlConvert = require("xml-js"); 17 | const queryString = require("querystring"); 18 | const tempy = require("tempy"); 19 | const archiver = require("archiver"); 20 | const downloadDir = require("downloads-folder"); 21 | const CREDENTIAL_PATH = path.join(os.homedir(), '.readmoo-api', 'credentials.json'); 22 | fs.ensureFileSync(CREDENTIAL_PATH); 23 | class ReadmooAPIError extends TypeError { 24 | } 25 | class LoginError extends ReadmooAPIError { 26 | } 27 | const cookieRegex = (str) => new RegExp(`${str}=[^;]+;`); 28 | const KEY_PAIR_ID_REGEX = cookieRegex('CloudFront-Key-Pair-Id'); 29 | const POLICY_REGEX = cookieRegex('CloudFront-Policy'); 30 | const SINATURE_REGEX = cookieRegex('CloudFront-Signature'); 31 | class Credential { 32 | constructor() { 33 | this.credential = this.load(); 34 | } 35 | get readmoo() { 36 | return this.credential.readmoo; 37 | } 38 | set readmoo(value) { 39 | this.credential.readmoo = value; 40 | } 41 | get cloudFrontKeyPairId() { 42 | return this.credential.cloudFrontKeyPairId; 43 | } 44 | set cloudFrontKeyPairId(value) { 45 | this.credential.cloudFrontKeyPairId = value; 46 | } 47 | get cloudFrontPolicy() { 48 | return this.credential.cloudFrontPolicy; 49 | } 50 | set cloudFrontPolicy(value) { 51 | this.credential.cloudFrontPolicy = value; 52 | } 53 | get cloudFrontSignature() { 54 | return this.credential.cloudFrontSignature; 55 | } 56 | set cloudFrontSignature(value) { 57 | this.credential.cloudFrontSignature = value; 58 | } 59 | save() { 60 | fs.writeFileSync(CREDENTIAL_PATH, JSON.stringify(this.credential, null, 2)); 61 | } 62 | setAWSCredential(cookie) { 63 | let m; 64 | m = cookie.match(KEY_PAIR_ID_REGEX); 65 | if (m && m[0]) { 66 | this.cloudFrontKeyPairId = m[0]; 67 | } 68 | m = cookie.match(POLICY_REGEX); 69 | if (m && m[0]) { 70 | this.cloudFrontPolicy = m[0]; 71 | } 72 | m = cookie.match(SINATURE_REGEX); 73 | if (m && m[0]) { 74 | this.cloudFrontSignature = m[0]; 75 | } 76 | } 77 | saveCredentials(res) { 78 | const cookies = res.headers['set-cookie'] || []; 79 | cookies.map(this.setAWSCredential.bind(this)); 80 | this.save(); 81 | } 82 | getHeaders() { 83 | const cookies = [ 84 | this.readmoo, 85 | this.cloudFrontKeyPairId, 86 | this.cloudFrontPolicy, 87 | this.cloudFrontSignature 88 | ]; 89 | return { 90 | Cookie: cookies.filter(Boolean).join(' ') 91 | }; 92 | } 93 | load() { 94 | try { 95 | return JSON.parse(fs.readFileSync(CREDENTIAL_PATH, 'utf-8')); 96 | } 97 | catch (err) { 98 | return {}; 99 | } 100 | } 101 | } 102 | exports.credential = new Credential(); 103 | function login(email, password) { 104 | return __awaiter(this, void 0, void 0, function* () { 105 | const loginRes = yield axios_1.default.head('https://member.readmoo.com/login/'); 106 | const cookies = loginRes.headers['set-cookie'] || []; 107 | const readmooCookie = cookies.find(c => c.match(/readmoo=([^;]+)/)); 108 | const match = readmooCookie && readmooCookie.match(/readmoo=([^;]+)/); 109 | if (match && match[1]) { 110 | exports.credential.readmoo = `readmoo=${match[1]};`; 111 | exports.credential.save(); 112 | } 113 | else { 114 | throw new LoginError(); 115 | } 116 | const formData = queryString.stringify({ email, password }); 117 | yield axios_1.default.post('https://member.readmoo.com/login/', formData, { 118 | headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded' }, exports.credential.getHeaders()) 119 | }); 120 | }); 121 | } 122 | exports.login = login; 123 | function checkLogin() { 124 | return __awaiter(this, void 0, void 0, function* () { 125 | try { 126 | const res = yield axios_1.default.get('https://new-read.readmoo.com/api/me/readings', { 127 | headers: exports.credential.getHeaders() 128 | }); 129 | if (res.data.status === 'error_login') { 130 | return false; 131 | } 132 | else { 133 | return true; 134 | } 135 | } 136 | catch (error) { 137 | return false; 138 | } 139 | }); 140 | } 141 | exports.checkLogin = checkLogin; 142 | function listBooks() { 143 | return __awaiter(this, void 0, void 0, function* () { 144 | const { data: readingData } = yield axios_1.default.get('https://new-read.readmoo.com/api/me/readings', { 145 | headers: exports.credential.getHeaders() 146 | }); 147 | const bookData = readingData.data[0]; 148 | const readerAPI = bookData.links.reader.match(/[^\?]+/)[0]; 149 | const res = yield axios_1.default.get(`${readerAPI}`, { 150 | headers: exports.credential.getHeaders() 151 | }); 152 | return readingData.included; 153 | }); 154 | } 155 | exports.listBooks = listBooks; 156 | function downloadBook(bookId) { 157 | return __awaiter(this, void 0, void 0, function* () { 158 | const response = yield axios_1.default.get(`https://reader.readmoo.com/api/book/${bookId}/nav`, { 159 | headers: exports.credential.getHeaders() 160 | }); 161 | const { data: bookData } = response; 162 | const { base, nav_dir, opf } = bookData; 163 | exports.credential.saveCredentials(response); 164 | const baseUrl = `https://reader.readmoo.com${base}`; 165 | const navLink = `https://reader.readmoo.com${nav_dir}`; 166 | const opfUrl = `${navLink}${opf}`; 167 | const tmpBookDir = tempy.directory(); 168 | yield downloadEpubContainer(baseUrl, tmpBookDir); 169 | const bookMeta = yield downloadEpubContent(opfUrl, opf, tmpBookDir); 170 | yield downloadEpubAssets(bookMeta, navLink, tmpBookDir); 171 | return tmpBookDir; 172 | }); 173 | } 174 | exports.downloadBook = downloadBook; 175 | function downloadEpubContainer(baseUrl, tmpBookDir) { 176 | return __awaiter(this, void 0, void 0, function* () { 177 | const containerFileName = 'META-INF/container.xml'; 178 | const { data } = yield axios_1.default.get(`${baseUrl}${containerFileName}`, { 179 | headers: exports.credential.getHeaders() 180 | }); 181 | const fn = path.join(tmpBookDir, containerFileName); 182 | fs.ensureFileSync(fn); 183 | fs.writeFileSync(fn, data); 184 | }); 185 | } 186 | function downloadEpubContent(opfUrl, opfPath, tmpBookDir) { 187 | return __awaiter(this, void 0, void 0, function* () { 188 | const { data } = yield axios_1.default.get(opfUrl, { 189 | headers: exports.credential.getHeaders() 190 | }); 191 | const fn = path.join(tmpBookDir, 'OEBPS', opfPath); 192 | fs.ensureFileSync(fn); 193 | fs.writeFileSync(fn, data); 194 | const contentObject = xmlConvert.xml2js(data, { compact: true, ignoreComment: true }); 195 | return contentObject; 196 | }); 197 | } 198 | function downloadEpubAssets(bookMeta, navLink, tmpBookDir) { 199 | return __awaiter(this, void 0, void 0, function* () { 200 | const files = bookMeta.package.manifest.item.map(it => it._attributes['href']) 201 | .map(href => ({ 202 | link: `${navLink}${href}`, 203 | base: href 204 | })); 205 | yield Promise.all(files.map(({ link, base }) => __awaiter(this, void 0, void 0, function* () { 206 | const isImage = base.includes('jpg'); 207 | const responseOpt = isImage ? { responseType: 'stream' } : {}; 208 | const { data } = yield axios_1.default.get(link, Object.assign({ headers: exports.credential.getHeaders() }, responseOpt)); 209 | const filename = path.join(tmpBookDir, 'OEBPS', base); 210 | fs.ensureFileSync(filename); 211 | if (isImage) { 212 | data.pipe(fs.createWriteStream(filename)); 213 | } 214 | else { 215 | fs.writeFileSync(filename, data); 216 | } 217 | }))); 218 | }); 219 | } 220 | function generateEpub(title, dir, outputFolder = downloadDir()) { 221 | return __awaiter(this, void 0, void 0, function* () { 222 | return new Promise((resolve, reject) => { 223 | const outputFile = path.join(outputFolder, `${title}.epub`); 224 | var output = fs.createWriteStream(outputFile); 225 | var archive = archiver('zip', { 226 | zlib: { level: 0 } 227 | }); 228 | archive.on('warning', function (err) { 229 | if (err.code === 'ENOENT') { 230 | } 231 | else { 232 | reject(err); 233 | } 234 | }); 235 | archive.on('error', function (err) { 236 | reject(err); 237 | }); 238 | archive.pipe(output); 239 | archive.directory(dir, false); 240 | archive.finalize(); 241 | return resolve(outputFile); 242 | }); 243 | }); 244 | } 245 | exports.generateEpub = generateEpub; 246 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /out/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /out/types.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chusiang/readmoo-api/2335dc28d2e54ca450be0023d1683e9e6bc55d11/out/types.d.ts -------------------------------------------------------------------------------- /out/types.js: -------------------------------------------------------------------------------- 1 | //# sourceMappingURL=types.js.map -------------------------------------------------------------------------------- /out/types.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readmoo-api", 3 | "version": "1.0.0", 4 | "description": "Readmoo API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --setupFiles dotenv/config", 8 | "build": "tsc", 9 | "build:watch": "tsc -w" 10 | }, 11 | "bin": { 12 | "readmoo-dl": "out/cli.js" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/archiver": "^2.1.2", 18 | "@types/bluebird": "^3.5.26", 19 | "@types/chai": "^4.1.7", 20 | "@types/form-data": "^2.2.1", 21 | "@types/fs-extra": "^5.0.5", 22 | "@types/inquirer": "0.0.44", 23 | "@types/jest": "^24.0.9", 24 | "@types/node": "^11.10.4", 25 | "@types/tempy": "^0.2.0", 26 | "chai": "^4.2.0", 27 | "jest": "^24.1.0", 28 | "ts-jest": "^24.0.0", 29 | "ts-node": "^8.0.2", 30 | "typescript": "^3.3.3333" 31 | }, 32 | "dependencies": { 33 | "archiver": "^3.0.0", 34 | "axios": "^0.18.0", 35 | "bluebird": "^3.5.3", 36 | "dotenv": "^6.2.0", 37 | "downloads-folder": "^1.0.1", 38 | "form-data": "^2.3.3", 39 | "fs-extra": "^7.0.1", 40 | "inquirer": "^6.2.2", 41 | "querystring": "^0.2.0", 42 | "tempy": "^0.2.1", 43 | "xml-js": "^1.6.11" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import * as inquirer from 'inquirer' 4 | import * as BluebirdPromise from 'bluebird'; 5 | 6 | import { 7 | checkLogin, 8 | downloadBook, 9 | generateEpub, 10 | listBooks, 11 | login 12 | } from './index' 13 | 14 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) 15 | 16 | ;(async () => { 17 | const isLogin = await checkLogin() 18 | if (!isLogin) { 19 | const { email } = await inquirer.prompt({ 20 | type: 'input', 21 | name: 'email', 22 | message: '請輸入您的 Email' 23 | }) 24 | 25 | const { password } = await inquirer.prompt({ 26 | type: 'password', 27 | name: 'password', 28 | message: '以及您的密碼' 29 | }) 30 | 31 | await login(email, password) 32 | } 33 | 34 | const booksData = await listBooks() 35 | 36 | const { books: selectedBooks } = await inquirer.prompt({ 37 | type: 'checkbox', 38 | name: 'books', 39 | message: '選擇要下載的書', 40 | choices: booksData.map(({ title, id }) => ({ name: title, value: { id, title }, short: title })) 41 | }) 42 | 43 | const outputFiles = await BluebirdPromise.mapSeries(selectedBooks, async ({ id, title }) => { 44 | try { 45 | const outputDir = await downloadBook(id) 46 | const filename = await generateEpub(title, outputDir) 47 | await sleep(200) 48 | return filename 49 | } catch (e) { 50 | console.error(`下載 "${title}" 失敗!`) 51 | return 52 | } 53 | }) 54 | 55 | console.log('書籍已下載至:') 56 | console.log(outputFiles.filter(Boolean).map(f => '- ' + f).join('\n')) 57 | })() 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 1. Login 3 | * 2. Use that cookies to get S3 guard cookies 4 | * 3. Books API 5 | * 4. ebpub downloader -> can be done independently 6 | */ 7 | 8 | require('dotenv').config() 9 | 10 | import * as fs from 'fs-extra' 11 | import * as path from 'path' 12 | import * as os from 'os' 13 | import * as childProcess from 'child_process' 14 | 15 | import axios, { AxiosResponse } from 'axios' 16 | import * as xmlConvert from 'xml-js' 17 | import * as queryString from 'querystring' 18 | import * as tempy from 'tempy' 19 | import * as archiver from 'archiver' 20 | import * as downloadDir from 'downloads-folder' 21 | 22 | const CREDENTIAL_PATH = path.join(os.homedir(), '.readmoo-api', 'credentials.json') 23 | fs.ensureFileSync(CREDENTIAL_PATH) 24 | 25 | class ReadmooAPIError extends TypeError {} 26 | class LoginError extends ReadmooAPIError {} 27 | 28 | interface ICredential { 29 | readmoo: string, 30 | cloudFrontKeyPairId: string 31 | cloudFrontPolicy: string 32 | cloudFrontSignature: string 33 | } 34 | 35 | 36 | const cookieRegex = (str) => new RegExp(`${str}=[^;]+;`) 37 | 38 | const KEY_PAIR_ID_REGEX = cookieRegex('CloudFront-Key-Pair-Id') 39 | const POLICY_REGEX = cookieRegex('CloudFront-Policy') 40 | const SINATURE_REGEX = cookieRegex('CloudFront-Signature') 41 | 42 | class Credential { 43 | private credential: ICredential 44 | 45 | constructor () { 46 | this.credential = this.load() 47 | } 48 | 49 | get readmoo () { 50 | return this.credential.readmoo 51 | } 52 | 53 | set readmoo (value: string) { 54 | this.credential.readmoo = value 55 | } 56 | 57 | get cloudFrontKeyPairId () { 58 | return this.credential.cloudFrontKeyPairId 59 | } 60 | 61 | set cloudFrontKeyPairId (value: string) { 62 | this.credential.cloudFrontKeyPairId = value 63 | } 64 | 65 | get cloudFrontPolicy () { 66 | return this.credential.cloudFrontPolicy 67 | } 68 | 69 | set cloudFrontPolicy (value: string) { 70 | this.credential.cloudFrontPolicy = value 71 | } 72 | 73 | get cloudFrontSignature () { 74 | return this.credential.cloudFrontSignature 75 | } 76 | 77 | set cloudFrontSignature (value: string) { 78 | this.credential.cloudFrontSignature = value 79 | } 80 | 81 | public save () { 82 | fs.writeFileSync(CREDENTIAL_PATH, JSON.stringify(this.credential, null, 2)) 83 | } 84 | 85 | private setAWSCredential (cookie: string) { 86 | let m 87 | 88 | m = cookie.match(KEY_PAIR_ID_REGEX) 89 | if (m && m[0]) { 90 | this.cloudFrontKeyPairId = m[0] 91 | } 92 | 93 | m = cookie.match(POLICY_REGEX) 94 | if (m && m[0]) { 95 | this.cloudFrontPolicy = m[0] 96 | } 97 | 98 | m = cookie.match(SINATURE_REGEX) 99 | if (m && m[0]) { 100 | this.cloudFrontSignature = m[0] 101 | } 102 | } 103 | 104 | public saveCredentials (res: AxiosResponse) { 105 | const cookies = res.headers['set-cookie'] || [] 106 | cookies.map(this.setAWSCredential.bind(this)) 107 | this.save() 108 | } 109 | 110 | public getHeaders () { 111 | const cookies = [ 112 | this.readmoo, 113 | this.cloudFrontKeyPairId, 114 | this.cloudFrontPolicy, 115 | this.cloudFrontSignature 116 | ] 117 | 118 | return { 119 | Cookie: cookies.filter(Boolean).join(' ') 120 | } 121 | } 122 | 123 | private load () { 124 | try { 125 | return JSON.parse(fs.readFileSync(CREDENTIAL_PATH, 'utf-8')) 126 | } catch (err) { 127 | return {} 128 | } 129 | } 130 | } 131 | 132 | export const credential = new Credential() 133 | 134 | export async function login (email: string, password: string) { 135 | const loginRes = await axios.head('https://member.readmoo.com/login/') 136 | const cookies = loginRes.headers['set-cookie'] || [] 137 | const readmooCookie = cookies.find(c => c.match(/readmoo=([^;]+)/)) 138 | const match = readmooCookie && readmooCookie.match(/readmoo=([^;]+)/) 139 | // match = loginRes.headers.get('set-cookie').match(/readmoo=([^;]+)/) 140 | 141 | if (match && match[1]) { 142 | credential.readmoo = `readmoo=${match[1]};` 143 | credential.save() 144 | } else { 145 | throw new LoginError() 146 | } 147 | 148 | const formData = queryString.stringify({ email, password }) 149 | 150 | await axios.post('https://member.readmoo.com/login/', formData, { 151 | headers: { 152 | 'Content-Type': 'application/x-www-form-urlencoded', 153 | ...credential.getHeaders() 154 | } 155 | }) 156 | } 157 | 158 | export async function checkLogin () { 159 | try { 160 | const res = await axios.get('https://new-read.readmoo.com/api/me/readings', { 161 | headers: credential.getHeaders() 162 | }) 163 | if (res.data.status === 'error_login') { 164 | return false 165 | } else { 166 | return true 167 | } 168 | } catch (error) { 169 | return false 170 | } 171 | } 172 | 173 | export async function listBooks () { 174 | const { data: readingData } = await axios.get('https://new-read.readmoo.com/api/me/readings', { 175 | headers: credential.getHeaders() 176 | }) 177 | 178 | const bookData = readingData.data[0] 179 | const readerAPI = bookData.links.reader.match(/[^\?]+/)[0] 180 | const res = await axios.get(`${readerAPI}`, { 181 | headers: credential.getHeaders() 182 | }) 183 | 184 | return readingData.included 185 | 186 | // const bookId = bookData.relationships.data.find(c => c.type === 'book').id 187 | // const book = readingData.included.find(include => include.id === bookId) 188 | // console.log(book) 189 | } 190 | 191 | // TODO: typed parameter 192 | export async function downloadBook (bookId: string) { 193 | const response = await axios.get(`https://reader.readmoo.com/api/book/${bookId}/nav`, { 194 | headers: credential.getHeaders() 195 | }) 196 | const { data: bookData } = response 197 | const { base, nav_dir, opf } = bookData 198 | credential.saveCredentials(response) 199 | 200 | const baseUrl = `https://reader.readmoo.com${base}` 201 | const navLink = `https://reader.readmoo.com${nav_dir}` 202 | const opfUrl = `${navLink}${opf}` 203 | 204 | // start download all the data 205 | const tmpBookDir = tempy.directory() 206 | 207 | await downloadEpubContainer(baseUrl, tmpBookDir) 208 | const bookMeta = await downloadEpubContent(opfUrl, opf, tmpBookDir) 209 | await downloadEpubAssets(bookMeta, navLink, tmpBookDir) 210 | 211 | return tmpBookDir 212 | } 213 | 214 | async function downloadEpubContainer (baseUrl: string, tmpBookDir: string) { 215 | const containerFileName = 'META-INF/container.xml' 216 | const { data } = await axios.get(`${baseUrl}${containerFileName}`, { 217 | headers: credential.getHeaders() 218 | }) 219 | 220 | const fn = path.join(tmpBookDir, containerFileName) 221 | fs.ensureFileSync(fn) 222 | fs.writeFileSync(fn, data) 223 | } 224 | 225 | async function downloadEpubContent (opfUrl: string, opfPath: string, tmpBookDir: string) { 226 | const { data } = await axios.get(opfUrl, { 227 | headers: credential.getHeaders() 228 | }) 229 | // write content 230 | const fn = path.join(tmpBookDir, 'OEBPS', opfPath) 231 | fs.ensureFileSync(fn) 232 | fs.writeFileSync(fn, data) 233 | 234 | const contentObject: any = xmlConvert.xml2js(data, { compact: true, ignoreComment: true }) 235 | return contentObject 236 | } 237 | 238 | // TODO: typed parameter 239 | async function downloadEpubAssets (bookMeta: any, navLink: string, tmpBookDir: string) { 240 | const files = bookMeta.package.manifest.item.map(it => it._attributes['href']) 241 | .map(href => ({ 242 | link: `${navLink}${href}`, 243 | base: href 244 | })) 245 | await Promise.all(files.map(async ({ link, base }) => { 246 | const isImage = base.includes('jpg') 247 | const responseOpt = isImage ? { responseType: 'stream' } : {} 248 | const { data } = await axios.get(link, { headers: credential.getHeaders() , ...responseOpt}) 249 | const filename = path.join(tmpBookDir, 'OEBPS', base) 250 | fs.ensureFileSync(filename) 251 | if (isImage) { 252 | data.pipe(fs.createWriteStream(filename)) 253 | } else { 254 | fs.writeFileSync(filename, data) 255 | } 256 | })) 257 | } 258 | 259 | export async function generateEpub (title: string, dir: string, outputFolder: string = downloadDir()): Promise { 260 | return new Promise((resolve, reject) => { 261 | 262 | const outputFile = path.join(outputFolder, `${title}.epub`) 263 | // create a file to stream archive data to. 264 | var output = fs.createWriteStream(outputFile) 265 | var archive = archiver('zip', { 266 | zlib: { level: 0 } 267 | }) 268 | 269 | archive.on('warning', function(err) { 270 | if (err.code === 'ENOENT') { 271 | // log warning 272 | } else { 273 | // throw error 274 | reject(err) 275 | } 276 | }) 277 | 278 | archive.on('error', function (err) { 279 | reject(err) 280 | }) 281 | 282 | archive.pipe(output) 283 | archive.directory(dir, false) 284 | archive.finalize() 285 | return resolve(outputFile) 286 | }) 287 | } 288 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "target": "es6", 10 | "sourceMap": true, 11 | "outDir": "./out", 12 | "types": [ 13 | "node", 14 | "jest" 15 | ] 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------