├── .editorconfig ├── .gitignore ├── package.json ├── README.md ├── LICENSE ├── cli.js └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 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 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lixian-115", 3 | "version": "1.2.1", 4 | "description": "Lixian 115 (115 离线下载命令行工具)", 5 | "license": "MIT", 6 | "repository": "coolzilj/lixian-115", 7 | "author": { 8 | "name": "Liu Jin", 9 | "email": "coolziljin@gmail.com", 10 | "url": "liujin.me" 11 | }, 12 | "bin": { 13 | "lx115": "cli.js" 14 | }, 15 | "main": "index.js", 16 | "keywords": [ 17 | "115", 18 | "lixian", 19 | "download" 20 | ], 21 | "dependencies": { 22 | "lodash": "^3.10.0", 23 | "object-assign": "^3.0.0", 24 | "optimist": "^0.6.1", 25 | "parse-torrent": "^5.2.1", 26 | "recursive-readdir": "^1.2.1", 27 | "request": "^2.58.0", 28 | "tough-cookie-filestore": "0.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lixian 115 (115 离线下载命令行工具) 2 | 3 | ``` 4 | 115 离线下载命令行工具 5 | 6 | Options: 7 | -d, --directory 本地种子目录 8 | -t, --torrent 本地种子文件(单个) 9 | -m, --magnet 磁力链(链接后加 \n 最多添加15个) 10 | -v, --version 版本号 11 | ``` 12 | 13 | ## Install 14 | 15 | ``` 16 | $ npm install -g lixian-115 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Login (登录) 22 | 由于 115 的登录策略改变,登录功能已废。 23 | 请在浏览器登录后,推荐使用 [EditThisCookie](http://www.editthiscookie.com/) 插件导出 cookies 到 `{HOMEPATH}/.115.cookies` 即可正常使用。cookies 只支持分号分隔的格式,`a=1;b=2;c=3` [#3](https://github.com/coolzilj/lixian-115/issues/3) 24 | 25 | ### Add multiple torrents (本地种子目录) 26 | ```js 27 | lx115 -d path/of/torrents/folder(存放 .torrent 文件的文件夹路径) 28 | ``` 29 | 30 | ### Add torrent (单个本地种子文件) 31 | ```js 32 | lx115 -t ~/Desktop/完美假妻168.Lock.Me.Up.Tie.Him.Down.2014.HD720P.X264.AAC.chinese.CHS.Mp4Ba.torrent 33 | ``` 34 | 35 | ### Add magnet (磁力链,链接后加 \n 最多添加15个) 36 | ```js 37 | lx115 -m "magnet:?xt=urn:btih:...\nmagnet:?xt=urn:btih:...\nmagnet:?xt=urn:btih:...\nmagnet:?xt=urn:btih:..." 38 | ``` 39 | 40 | ## License 41 | 42 | MIT © [Liu Jin](http://liujin.me) 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Liu Jin (liujin.me) 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 | 23 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Lixian115 = require('./index'); 4 | var pjson = require('./package.json'); 5 | var fs = require('fs'); 6 | var recursive = require('recursive-readdir'); 7 | var parseTorrent = require('parse-torrent'); 8 | var _ = require('lodash'); 9 | var optimist = require('optimist'); 10 | 11 | var lx115 = new Lixian115(); 12 | 13 | var argv = optimist.usage('115 离线下载命令行工具') 14 | .options('d', { 15 | alias: 'directory', 16 | describe: '本地种子目录' 17 | }) 18 | .options('t', { 19 | alias: 'torrent', 20 | describe: '本地种子文件(单个)' 21 | }) 22 | .options('m', { 23 | alias: 'magnet', 24 | describe: '磁力链(链接后加 \\n 最多添加15个)' 25 | }) 26 | .options('v', { 27 | alias: 'version', 28 | describe: '版本号' 29 | }) 30 | .argv 31 | 32 | if (argv.v || argv.version) { 33 | console.log(pjson.version); 34 | 35 | } else if (argv.d || argv.directory) { 36 | recursive(argv.directory, function (err, files) { 37 | if (err) { 38 | console.error(err.message); 39 | } 40 | 41 | // 过滤种子文件 42 | files = _.filter(files, function (file) { 43 | return file.match(/.\.torrent$/); 44 | }); 45 | 46 | // Fix: 超出『离线下载』最大任务运行数量(15个) 47 | var chunks = _.chunk(files, 15); 48 | _.each(chunks, function (chunk, index) { 49 | var urls = ''; 50 | _.each(chunk, function (file) { 51 | // console.log(file); 52 | var torrent = parseTorrent(fs.readFileSync(file)); 53 | var uri = parseTorrent.toMagnetURI(torrent); 54 | urls += uri + '\n'; 55 | }); 56 | 57 | setTimeout(function () { 58 | if (chunk.length === 14) { 59 | console.log("++ Added " + ((index + 1) * 15) + " tasks"); 60 | } else { 61 | console.log("++ Added " + (index * 15 + chunk.length) + " tasks"); 62 | } 63 | lx115.addTasks(urls); 64 | }, index * 20000); 65 | }); 66 | }); 67 | 68 | } else if (argv.t || argv.torrent) { 69 | var torrent = parseTorrent(fs.readFileSync(argv.torrent)); 70 | var url = parseTorrent.toMagnetURI(torrent); 71 | lx115.addTasks(url); 72 | 73 | } else if (argv.m || argv.magnet) { 74 | lx115.addTasks(argv.magnet); 75 | 76 | } else { 77 | optimist.showHelp(); 78 | } 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var request = require('request'); 6 | var objectAssign = require('object-assign'); 7 | 8 | function Lixian115 () { 9 | if (!(this instanceof Lixian115)) { 10 | return new Lixian115(); 11 | } 12 | this.jar = request.jar(); 13 | this.isLogin = false; 14 | this.headers = { 15 | "Accept":"Accept: application/json, text/javascript, */*; q=0.01", 16 | "Accept-Encoding":"text/html", 17 | "Accept-Language":"en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2", 18 | "Content-Type":"application/x-www-form-urlencoded; charset=UTF-8", 19 | "Referer":"http://m.115.com/", 20 | "X-Requested-With": "XMLHttpRequest", 21 | "User-Agent":"Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 22 | }; 23 | }; 24 | 25 | Lixian115.prototype.addTasks = function (urls) { 26 | var self = this; 27 | self.checkLogin(function () { 28 | self.getUid(function (uid) { 29 | self.getSignTime(function (signTime) { 30 | urls = urls.split('\n'); 31 | var obj = {}; 32 | for (var i = 0; i < urls.length; i++) { 33 | obj['url[' + i + ']'] = urls[i]; 34 | }; 35 | var data = { 36 | 'uid': uid, 37 | 'sign': signTime.sign, 38 | 'time': signTime.time 39 | }; 40 | objectAssign(data, obj); 41 | self.setCookie('http://115.com/lixian/?ct=lixian&ac=add_task_urls'); 42 | 43 | request.post({ 44 | url: 'http://115.com/web/lixian/?ct=lixian&ac=add_task_urls', 45 | form: data, 46 | jar: self.jar, 47 | headers: self.headers 48 | }, function (err, res, body) { 49 | if (!err && res.statusCode === 200) { 50 | body = JSON.parse(body); 51 | if (body.state) { 52 | // console.log(body.result); 53 | for (var i = 0; i < body.result.length; i++) { 54 | var result = body.result[i]; 55 | if (result.info_hash) { 56 | console.log("++ Add task " + (i + 1) + " (" + result.name + ")" + " successed."); 57 | } else { 58 | // var name = decodeURI(result.url.split('&tr')[0].match(/&dn=(.+)/)[1]); 59 | // console.log("++ Add task " + (i + 1) + " (" + name + ")" + " failed: " + result.error_msg); 60 | console.log("++ Add task " + (i + 1) + " failed: " + result.error_msg); 61 | } 62 | } 63 | } else { 64 | console.error("Error: " + body.error_msg); 65 | } 66 | } else { 67 | console.error('Error at add task.'); 68 | } 69 | }) 70 | }) 71 | }) 72 | }) 73 | }; 74 | 75 | Lixian115.prototype.getUid = function (cb) { 76 | if (!this.isLogin) { 77 | console.log('Please login first!'); 78 | process.exit(0); 79 | } 80 | var url = 'http://my.115.com/?ct=ajax&ac=get_user_aq'; 81 | this.setCookie(url); 82 | 83 | request.get({url: url, jar: this.jar, headers: this.headers}, function (err, res, body) { 84 | if (!err && res.statusCode === 200) { 85 | cb(JSON.parse(body).data.uid); 86 | } 87 | }) 88 | }; 89 | 90 | Lixian115.prototype.getSignTime = function (cb) { 91 | if (!this.isLogin) { 92 | console.log('Please login first!'); 93 | process.exit(0); 94 | } 95 | var url = 'http://115.com/?ct=offline&ac=space'; 96 | this.setCookie(url); 97 | request.get({url: url, jar: this.jar, headers: this.headers}, function (err, res, body) { 98 | if (!err && res.statusCode === 200) { 99 | var data = { 100 | sign: JSON.parse(body).sign, 101 | time: JSON.parse(body).time 102 | }; 103 | cb(data); 104 | } 105 | }) 106 | }; 107 | 108 | Lixian115.prototype.checkLogin = function (cb) { 109 | var cookieString = this.getCookie(); 110 | if (cookieString) { 111 | this.isLogin = true; 112 | cb(); 113 | } 114 | }; 115 | 116 | Lixian115.prototype.cookiesPath = function () { 117 | var homePath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; 118 | return path.join(homePath, '.115.cookies'); 119 | }; 120 | 121 | Lixian115.prototype.getCookie = function () { 122 | return fs.readFileSync(this.cookiesPath()).toString('utf8'); 123 | }; 124 | 125 | Lixian115.prototype.setCookie = function (url) { 126 | var cookieString = this.getCookie(); 127 | cookieString.split(';').map((c) => { 128 | this.jar.setCookie(request.cookie(c), url); 129 | }); 130 | }; 131 | 132 | module.exports = Lixian115; 133 | --------------------------------------------------------------------------------