├── .eslintrc ├── .gitignore ├── README.md ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "func-names": ["error", "never"] 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node Jdownloader API 2 | ====== 3 | **A NodeJS wrapper for My.jdownloader.org API** 4 | 5 | https://my.jdownloader.org/developers/ 6 | 7 | This is a rewritten version of this PHP wrapper 8 | https://github.com/tofika/my.jdownloader.org-api-php-class 9 | 10 | Features 11 | -------- 12 | - Connect to the My.JDownloader service 13 | - Reconnect 14 | - Disconnect from the My.JDownloader service 15 | - List Devices 16 | - Add Links and start download 17 | - List actual links from the download are 18 | - List all packages and get current download status 19 | 20 | Usage 21 | -------- 22 | 23 | To install `jdownloader-api` in your node.js project: 24 | 25 | ``` 26 | npm install jdownloader-api 27 | ``` 28 | 29 | And to access it from within node, simply add: 30 | 31 | ```javascript 32 | const jdownloaderAPI = require('jdownloader-api'); 33 | ``` 34 | API 35 | -------- 36 | ## Connect 37 | 38 | ```javascript 39 | jdownloaderAPI.connect(_USERNAME_, _PASSWORD_) 40 | ``` 41 | 42 | ## Disconnect 43 | 44 | ```javascript 45 | jdownloaderAPI.disconnect() 46 | ``` 47 | ## Reconnect 48 | 49 | ```javascript 50 | jdownloaderAPI.reconnect() 51 | ``` 52 | 53 | ## listDevices 54 | 55 | ```javascript 56 | // List all active devices from the connected account 57 | // deviceName and deviceId 58 | jdownloaderAPI.listDevices() 59 | ``` 60 | 61 | ## addLinks 62 | 63 | ```javascript 64 | // This will add links to the device and autostart downloads if 65 | // autostart parameter is true otherwise it will leave the package in the linkGrabber 66 | // nb : links must be comma separated 67 | jdownloaderAPI.addLinks(LINKS, DEVICEID, true(autostart)) 68 | ``` 69 | 70 | ## queryLinks 71 | 72 | ```javascript 73 | // List all links from the download area of the specified device 74 | jdownloaderAPI.queryLinks(DEVICEID) 75 | ``` 76 | 77 | ## queryPackages 78 | 79 | ```javascript 80 | // List all packages from the download area of the specified device 81 | // current status, total bytes loaded, etc ... 82 | // nb : packagesUUIS must be comma separated you can get them from the queryLinks method 83 | jdownloaderAPI.queryPackages(DEVICEID, PACKAGESUUIDS) 84 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase,no-param-reassign,no-underscore-dangle */ 2 | const crypto = require('ezcrypto').Crypto; 3 | const aesjs = require('aes-js'); 4 | const requestPromise = require('request-promise'); 5 | const textEncoding = require('text-encoding'); 6 | const pkcs7 = require('pkcs7'); 7 | 8 | const __ENPOINT = 'https://api.jdownloader.org'; 9 | const __APPKEY = 'my_jd_nodeJS_webinterface'; 10 | const __SERVER_DOMAIN = 'server'; 11 | const __DEVICE_DOMAIN = 'device'; 12 | 13 | let __rid_counter; 14 | let __loginSecret; 15 | let __deviceSecret; 16 | let __sessionToken; 17 | let __regainToken; 18 | let __serverEncryptionToken; 19 | let __deviceEncryptionToken; 20 | const __apiVer = 1; 21 | 22 | const uniqueRid = () => { 23 | __rid_counter = Math.floor(Date.now()); 24 | return __rid_counter; 25 | }; 26 | 27 | const createSecret = (username, password, domain) => crypto.SHA256(username + password + domain, { asBytes: true }); 28 | 29 | const sign = (key, data) => crypto.HMAC(crypto.SHA256, data, key, { asBytes: false }); 30 | 31 | const encrypt = (data, iv_key) => { 32 | const string_iv_key = crypto.charenc.Binary.bytesToString(iv_key); 33 | const string_iv = string_iv_key.substring(0, string_iv_key.length / 2); 34 | const string_key = string_iv_key.substring(string_iv_key.length / 2, string_iv_key.length); 35 | const iv = crypto.charenc.Binary.stringToBytes(string_iv); 36 | const key = crypto.charenc.Binary.stringToBytes(string_key); 37 | const aesCbc = new aesjs.ModeOfOperation.cbc(key, iv); 38 | const dataBytes = aesjs.utils.utf8.toBytes(data); 39 | const paddedData = aesjs.padding.pkcs7.pad(dataBytes); 40 | const encryptedBytes = aesCbc.encrypt(paddedData); 41 | const buff = Buffer.from(encryptedBytes, 'base64'); 42 | const base64data = buff.toString('base64'); 43 | return base64data; 44 | }; 45 | 46 | const decrypt = (data, iv_key) => { 47 | const string_iv_key = crypto.charenc.Binary.bytesToString(iv_key); 48 | const string_iv = string_iv_key.substring(0, string_iv_key.length / 2); 49 | const string_key = string_iv_key.substring(string_iv_key.length / 2, string_iv_key.length); 50 | const iv = crypto.charenc.Binary.stringToBytes(string_iv); 51 | const key = crypto.charenc.Binary.stringToBytes(string_key); 52 | const aesCbc = new aesjs.ModeOfOperation.cbc(key, iv); 53 | const test = aesCbc.decrypt(Buffer.from(data, 'base64')); 54 | const textDecoder = new textEncoding.TextDecoder('utf-8'); 55 | return textDecoder.decode(pkcs7.unpad(test)); 56 | }; 57 | 58 | const unescapeJson = json => json.replace(/[^\x30-\x39\x41-\x5A\x61-\x7A\x7B\x7D\x20\x26\x28\x29\x2C\x27\x22\x2E\x2F\x26\x40\x5C\x3A\x2D\x5C\x5B\x5D]/g, '').replace(/\s+/g, ' '); 59 | 60 | const postQuery = (url, params) => { 61 | let options = { 62 | method: 'POST', 63 | uri: url, 64 | headers: { 65 | 'Content-Type': 'application/aesjson-jd; charset=utf-8', 66 | }, 67 | }; 68 | if (params) { 69 | options = { 70 | method: 'POST', 71 | uri: url, 72 | body: params, 73 | }; 74 | } 75 | return requestPromise.post(options); 76 | }; 77 | 78 | 79 | const callServer = (query, key, params) => { 80 | let rid = uniqueRid(); 81 | if (params) { 82 | if (key) { 83 | params = encrypt(params, key); 84 | } 85 | rid = __rid_counter; 86 | } 87 | 88 | if (query.includes('?')) { 89 | query += '&'; 90 | } else { 91 | query += '?'; 92 | } 93 | query = `${query}rid=${rid}`; 94 | const signature = sign(key, query); 95 | query += `&signature=${signature}`; 96 | const url = __ENPOINT + query; 97 | 98 | return new Promise((resolve, rejected) => { 99 | postQuery(url, params) 100 | .then((parsedBody) => { 101 | const result = decrypt(parsedBody, key); 102 | resolve(JSON.parse(unescapeJson(result))); 103 | }).catch((err) => { 104 | rejected(err); 105 | }); 106 | }); 107 | }; 108 | 109 | const callAction = (action, deviceId, params) => { 110 | if (__sessionToken === undefined) { 111 | return Promise.reject(new Error('Not connected')); 112 | } 113 | const query = `/t_${encodeURI(__sessionToken)}_${encodeURI(deviceId)}${action}`; 114 | let json; 115 | if (params) { 116 | json = { 117 | url: action, 118 | params, 119 | rid: uniqueRid(), 120 | apiVer: __apiVer, 121 | }; 122 | } else { 123 | json = { 124 | url: action, 125 | rid: uniqueRid(), 126 | apiVer: __apiVer, 127 | }; 128 | } 129 | const jsonData = encrypt(JSON.stringify(json), __deviceEncryptionToken); 130 | const url = __ENPOINT + query; 131 | return new Promise((resolve, rejected) => { 132 | postQuery(url, jsonData) 133 | .then((parsedBody) => { 134 | const result = decrypt(parsedBody, __deviceEncryptionToken); 135 | resolve(JSON.parse(unescapeJson(result))); 136 | }).catch((err) => { 137 | rejected(decrypt(err.error, __deviceEncryptionToken)); 138 | }); 139 | }); 140 | }; 141 | 142 | const updateEncryptionToken = (oldTokenBytes, updateToken) => { 143 | const updateTokenBytes = crypto.util.hexToBytes(updateToken); 144 | const buffer = Buffer.from(oldTokenBytes); 145 | const secondbuffer = Buffer.from(updateTokenBytes); 146 | const thirdbuffer = Buffer.concat([buffer, secondbuffer], buffer.length + secondbuffer.length); 147 | return crypto.SHA256(thirdbuffer, { asBytes: true }); 148 | }; 149 | 150 | exports.connect = (username, password) => { 151 | __loginSecret = createSecret(username, password, __SERVER_DOMAIN); 152 | __deviceSecret = createSecret(username, password, __DEVICE_DOMAIN); 153 | 154 | const query = `/my/connect?email=${encodeURI(username)}&appkey=${__APPKEY}`; 155 | 156 | return new Promise((resolve, rejected) => { 157 | callServer(query, __loginSecret, null).then((val) => { 158 | __sessionToken = val.sessiontoken; 159 | __regainToken = val.regaintoken; 160 | __serverEncryptionToken = updateEncryptionToken(__loginSecret, __sessionToken); 161 | __deviceEncryptionToken = updateEncryptionToken(__deviceSecret, __sessionToken); 162 | resolve(true); 163 | }).catch((error) => { 164 | rejected(error); 165 | }); 166 | }); 167 | }; 168 | 169 | exports.reconnect = function () { 170 | const query = `/my/reconnect?appkey=${encodeURI(__APPKEY)}&sessiontoken=${encodeURI(__sessionToken)}®aintoken=${encodeURI(__regainToken)}`; 171 | return new Promise((resolve, rejected) => { 172 | callServer(query, __serverEncryptionToken).then((val) => { 173 | __sessionToken = val.sessiontoken; 174 | __regainToken = val.regaintoken; 175 | __serverEncryptionToken = updateEncryptionToken(__serverEncryptionToken, __sessionToken); 176 | __deviceEncryptionToken = updateEncryptionToken(__deviceSecret, __sessionToken); 177 | resolve(true); 178 | }).catch((error) => { 179 | rejected(error); 180 | }); 181 | }); 182 | }; 183 | 184 | 185 | exports.disconnect = function () { 186 | const query = `/my/disconnect?sessiontoken=${encodeURI(__sessionToken)}`; 187 | return new Promise((resolve, rejected) => { 188 | callServer(query, __serverEncryptionToken).then(() => { 189 | __sessionToken = ''; 190 | __regainToken = ''; 191 | __serverEncryptionToken = ''; 192 | __deviceEncryptionToken = ''; 193 | resolve(true); 194 | }).catch((error) => { 195 | rejected(error); 196 | }); 197 | }); 198 | }; 199 | 200 | exports.listDevices = () => { 201 | const query = `/my/listdevices?sessiontoken=${encodeURI(__sessionToken)}`; 202 | return new Promise((resolve, rejected) => { 203 | callServer(query, __serverEncryptionToken).then((val) => { 204 | resolve(val.list); 205 | }).catch((error) => { 206 | rejected(error); 207 | }); 208 | }); 209 | }; 210 | 211 | exports.getDirectConnectionInfos = deviceId => new Promise((resolve, rejected) => { 212 | callAction('/device/getDirectConnectionInfos', deviceId, null) 213 | .then((val) => { 214 | resolve(val); 215 | }).catch((error) => { 216 | rejected(error); 217 | }); 218 | }); 219 | 220 | /** 221 | * 222 | * @param {string[]} links - Array of links 223 | * @param {string} deviceId - Device ID 224 | * @param {boolean} autostart - Autostart 225 | * @param {string} [packageName] - name of the package 226 | * @param {string} [destinationFolder] - destination folder 227 | * @returns {Promise} 228 | */ 229 | exports.addLinks = ( 230 | links, 231 | deviceId, 232 | autostart, 233 | packageName = null, 234 | destinationFolder = null 235 | ) => { 236 | const params = JSON.stringify({ 237 | links: links.join(","), 238 | autostart, 239 | priority: "DEFAULT", 240 | ...(packageName !== null && { packageName }), 241 | ...(destinationFolder !== null && { 242 | destinationFolder, 243 | overwritePackagizerRules: true, 244 | }), 245 | }); 246 | return new Promise((resolve, rejected) => { 247 | callAction("/linkgrabberv2/addLinks", deviceId, [params]) 248 | .then((val) => { 249 | resolve(val); 250 | }) 251 | .catch((error) => { 252 | rejected(error); 253 | }); 254 | }); 255 | }; 256 | 257 | 258 | exports.queryLinks = (deviceId) => { 259 | const params = `{"addedDate" : true, 260 | "bytesLoaded": true, 261 | "bytesTotal": true, 262 | "enabled": true, 263 | "finished": true, 264 | "url": true, 265 | "status": true, 266 | "speed": true, 267 | "finishedDate": true, 268 | "priority" : true, 269 | "extractionStatus": true, 270 | "host": true, 271 | "running" : true}`; 272 | return new Promise((resolve, rejected) => { 273 | callAction('/downloadsV2/queryLinks', deviceId, [params]) 274 | .then((val) => { 275 | resolve(val); 276 | }).catch((error) => { 277 | rejected(error); 278 | }); 279 | }); 280 | }; 281 | 282 | exports.queryPackages = (deviceId, packagesIds) => { 283 | //params see https://my.jdownloader.org/developers/#tag_144 284 | const params = `{ 285 | "bytesLoaded" : true, 286 | "bytesTotal" : true, 287 | "childCount" : true, 288 | "comment" : true, 289 | "enabled" : true, 290 | "eta" : true, 291 | "finished" : true, 292 | "hosts" : true, 293 | "priority" : true, 294 | "running" : true, 295 | "saveTo" : true, 296 | "speed" : true, 297 | "status" : true, 298 | "packageUUIDs" : [${packagesIds}]}`; 299 | return new Promise((resolve, rejected) => { 300 | callAction('/downloadsV2/queryPackages', deviceId, [params]) 301 | .then((val) => { 302 | resolve(val); 303 | }).catch((error) => { 304 | rejected(error); 305 | }); 306 | }); 307 | }; 308 | 309 | exports.cleanUpFinishedLinks = (deviceId) => { 310 | const params = ["[]", "[]", "DELETE_FINISHED", "REMOVE_LINKS_ONLY", "ALL"]; 311 | return new Promise((resolve, rejected) => { 312 | callAction('/downloadsV2/cleanup', deviceId, params) 313 | .then((val) => { 314 | resolve(val); 315 | }).catch((error) => { 316 | rejected(error); 317 | }); 318 | }); 319 | }; 320 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jdownloader-api", 3 | "version": "1.1.3", 4 | "description": "A node wrapper module for My.Jdownloader.org API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Mallé Guissé", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "eslint": "^4.9.0", 13 | "eslint-config-airbnb": "^16.1.0", 14 | "eslint-plugin-import": "^2.7.0", 15 | "eslint-plugin-jsx-a11y": "^6.0.2", 16 | "eslint-plugin-react": "^7.4.0" 17 | }, 18 | "dependencies": { 19 | "aes-js": "^3.1.0", 20 | "ezcrypto": "0.0.3", 21 | "pkcs7": "^1.0.2", 22 | "request": "^2.88.0", 23 | "request-promise": "^4.2.2", 24 | "text-encoding": "^0.6.4" 25 | } 26 | } 27 | --------------------------------------------------------------------------------