├── src ├── si │ ├── url.json │ ├── helpers │ │ ├── index.js │ │ └── config.js │ ├── index.js │ ├── info.js │ ├── upload.js │ ├── scrap.js │ └── search.js ├── pantsu │ ├── url.json │ ├── helpers │ │ ├── index.js │ │ └── config.js │ ├── header.js │ ├── info.js │ ├── checkUser.js │ ├── index.js │ ├── update.js │ ├── login.js │ ├── upload.js │ └── search.js └── index.js ├── .gitignore ├── .travis.yml ├── LICENCE ├── package.json ├── README.md └── test ├── pantsu.test.js └── si.test.js /src/si/url.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://nyaa.si" 3 | } -------------------------------------------------------------------------------- /src/pantsu/url.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://nyaa.net/api" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | 4 | .idea 5 | 6 | node_modules 7 | 8 | coverage 9 | .nyc_output -------------------------------------------------------------------------------- /src/si/helpers/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.js') 2 | 3 | module.exports = { 4 | config 5 | } 6 | -------------------------------------------------------------------------------- /src/pantsu/helpers/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.js') 2 | 3 | module.exports = { 4 | config 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const pantsu = require('./pantsu') 2 | const si = require('./si') 3 | 4 | module.exports = { 5 | pantsu, 6 | si 7 | } 8 | -------------------------------------------------------------------------------- /src/si/index.js: -------------------------------------------------------------------------------- 1 | const helpers = require('./helpers') 2 | 3 | const search = require('./search.js') 4 | const info = require('./info.js') 5 | const upload = require('./upload.js') 6 | 7 | module.exports = { 8 | ...helpers, 9 | cli: helpers.config.cli, 10 | 11 | ...search, 12 | ...info, 13 | ...upload 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "11" 6 | - "12" 7 | install: 8 | - npm install 9 | script: 10 | - npm test 11 | - npm run cloc 12 | after_success: npm run coverage 13 | 14 | deploy: 15 | provider: npm 16 | email: kylart.dev@gmail.com 17 | api_key: $NPM_TOKEN 18 | on: 19 | branch: master 20 | 21 | notifications: 22 | email: false -------------------------------------------------------------------------------- /src/pantsu/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows to request torrent's head its ID. 3 | * 4 | * @param {number|string} id The ID of the torrent. 5 | * 6 | * @returns {promise} 7 | */ 8 | async function checkHeader (id) { 9 | if (!id) throw new Error('[Nyaapi]: No ID was given on torrent head request.') 10 | 11 | const { data } = await this.cli.get(`/view/${id}`) 12 | 13 | return data 14 | } 15 | 16 | module.exports = { 17 | checkHeader 18 | } 19 | -------------------------------------------------------------------------------- /src/pantsu/info.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Request torrent information according to its ID. 3 | * 4 | * @param {number} id The ID of the torrent you want information of. 5 | * 6 | * @returns {promise} 7 | */ 8 | 9 | async function infoRequest (id) { 10 | if (!id) throw new Error('[Nyaapi]: No ID given on request demand.') 11 | 12 | const { data } = await this.cli.get(`/view/${id}`) 13 | 14 | return data 15 | } 16 | 17 | module.exports = { 18 | infoRequest 19 | } 20 | -------------------------------------------------------------------------------- /src/pantsu/checkUser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows to check a user profile from its ID. 3 | * 4 | * @param {number|string} id The ID of the user you want to check the profile. 5 | */ 6 | 7 | async function checkUser (id) { 8 | if (!id) throw new Error('[Nyaapi]: No ID was given on user check demand.') 9 | 10 | const { data } = await this.cli.get('/profile', { 11 | params: { id } 12 | }) 13 | 14 | return data 15 | } 16 | 17 | module.exports = { 18 | checkUser 19 | } 20 | -------------------------------------------------------------------------------- /src/pantsu/helpers/config.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const BASE_URL = 'https://nyaa.net/api' 4 | 5 | module.exports = { 6 | url: BASE_URL, 7 | 8 | cli: axios.create({ 9 | baseURL: BASE_URL 10 | }), 11 | 12 | /** 13 | * Allows to specify a specific URL to access nyaa 14 | * 15 | * @param {String} url New URL to use 16 | * @returns {void} 17 | */ 18 | updateBaseUrl (url) { 19 | this.url = url 20 | this.cli = axios.create({ baseURL: url }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pantsu/index.js: -------------------------------------------------------------------------------- 1 | const helpers = require('./helpers') 2 | 3 | const search = require('./search.js') 4 | const info = require('./info.js') 5 | const upload = require('./upload.js') 6 | const update = require('./update.js') 7 | const login = require('./login') 8 | const checkUser = require('./checkUser.js') 9 | const checkHeader = require('./header.js') 10 | 11 | module.exports = { 12 | ...helpers, 13 | cli: helpers.config.cli, 14 | 15 | ...search, 16 | ...info, 17 | ...upload, 18 | ...update, 19 | ...login, 20 | ...checkUser, 21 | ...checkHeader 22 | } 23 | -------------------------------------------------------------------------------- /src/si/helpers/config.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const https = require('https') 3 | 4 | const BASE_URL = 'https://nyaa.si' 5 | 6 | module.exports = { 7 | url: BASE_URL, 8 | 9 | cli: axios.create({ 10 | baseURL: BASE_URL, 11 | httpsAgent: new https.Agent({ 12 | rejectUnauthorized: false 13 | }) 14 | }), 15 | 16 | /** 17 | * Allows to specify a specific URL to access nyaa 18 | * 19 | * @param {String} url New URL to use 20 | * @returns {void} 21 | */ 22 | updateBaseUrl (url) { 23 | this.url = url 24 | this.cli = axios.create({ baseURL: url }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pantsu/update.js: -------------------------------------------------------------------------------- 1 | const omit = require('lodash.omit') 2 | 3 | /** 4 | * Allows the updating of a torrent. 5 | * 6 | * @param {Object} opts All the paramters to update (id goes here) 7 | * 8 | * @returns {promise} 9 | */ 10 | async function update (opts = {}) { 11 | if (!opts.id || !opts.token) { 12 | throw new Error('[Nyaapi]: No ID or Token given on update demand.') 13 | } 14 | 15 | const { data } = await this.cli.put( 16 | '/update', 17 | omit(opts, 'token'), { 18 | headers: { 19 | Authorization: opts.token 20 | } 21 | }) 22 | 23 | return data 24 | } 25 | 26 | module.exports = { 27 | update 28 | } 29 | -------------------------------------------------------------------------------- /src/pantsu/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows to log into nyaa.pantsu.cat 3 | * 4 | * @param {Object} credentials Object containing a username aong with its password. 5 | * @param {String} credentials.username Your username 6 | * @param {String} credentials.password Your password 7 | * 8 | * @returns {promise} 9 | */ 10 | async function login (credentials = {}) { 11 | if (!credentials.username || !credentials.password) { 12 | throw new Error('[Nyaapi]: No username or password were given on login demand.') 13 | } 14 | 15 | const { data } = await this.cli.post('/login', credentials) 16 | 17 | return data 18 | } 19 | 20 | module.exports = { 21 | login 22 | } 23 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) Kylart 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /src/pantsu/upload.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const FormData = require('form-data') 3 | 4 | /** 5 | * Allows the uploading of torrent file to nyaa.pantsu.cat 6 | * 7 | * @param {Object} opts Options. 8 | * 9 | * @returns {Promise} 10 | */ 11 | async function upload (opts = {}) { 12 | if ((!opts.magnet && !opts.torrent) || !opts.token || !opts.username) { 13 | throw new Error('[Nyaapi]: No file/torrent, token or username were given.') 14 | } 15 | 16 | if (opts.torrent) { 17 | opts.torrent = fs.createReadStream(opts.torrent) 18 | } 19 | 20 | const form = new FormData() 21 | 22 | Object.entries(opts) 23 | .forEach(([key, value]) => { 24 | if (key === 'torrent') return form.append('torrent', fs.createReadStream(opts.torrent)) 25 | if (key === 'token') return 26 | 27 | form.append(key, value) 28 | }) 29 | 30 | return this.cli.post( 31 | '/upload', 32 | form, { 33 | headers: { 34 | Authorization: opts.token, 35 | ...form.getHeaders() 36 | } 37 | } 38 | ) 39 | } 40 | 41 | module.exports = { 42 | upload 43 | } 44 | -------------------------------------------------------------------------------- /src/si/info.js: -------------------------------------------------------------------------------- 1 | const { extractPageFromHTML } = require('./scrap.js') 2 | 3 | /** 4 | * @typedef {Object} TorrentInfo 5 | * @property {Number} id 6 | * @property {Object} info 7 | * @property {String} info.category 8 | * @property {String} info.completed 9 | * @property {String} info.date 10 | * @property {String} info.description 11 | * @property {String} info.filesize 12 | * @property {String} info.hash 13 | * @property {String} info.leechers 14 | * @property {String} info.magnet 15 | * @property {String} info.name 16 | * @property {String} info.seeders 17 | * @property {String} info.sub_category 18 | * @property {String} info.torrent 19 | * @property {String} info.uploader_name 20 | */ 21 | 22 | /** 23 | * Request torrent information according to its ID. 24 | * 25 | * @param {Number} id The ID of the torrent you want information of. 26 | * 27 | * @returns {Promise} 28 | */ 29 | async function infoRequest (id) { 30 | if (!id || isNaN(+id)) throw new Error('[Nyaapi]: No ID given on request demand.') 31 | 32 | const { data } = await this.cli.get(`/view/${id}`) 33 | 34 | return { 35 | id, 36 | info: extractPageFromHTML(data) 37 | } 38 | } 39 | 40 | module.exports = { 41 | infoRequest 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nyaapi", 3 | "version": "2.4.4", 4 | "description": "Non-official api for getting torrent links from Nyaa.si and Nyaa.pantsu.cat", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "standard --verbose --fix | snazzy", 8 | "test:si": "nyc ava --verbose --serial test/si.test.js", 9 | "test:pantsu": "nyc ava --verbose --serial test/pantsu.test.js", 10 | "test": "npm run lint && nyc ava --verbose --serial", 11 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 12 | "cloc": "cloc $(git ls-files)" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Kylart/Nyaapi.git" 17 | }, 18 | "author": "Kylart", 19 | "license": "MIT", 20 | "keywords": [ 21 | "kawanime", 22 | "nyaa-si", 23 | "nyaa-pantsu", 24 | "torrent", 25 | "magnet-links" 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/Kylart/Nyaapi/issues" 29 | }, 30 | "homepage": "https://github.com/Kylart/Nyaapi#readme", 31 | "dependencies": { 32 | "axios": "^0.23.0", 33 | "cheerio": "^1.0.0-rc.3", 34 | "form-data": "^3.0.0", 35 | "lodash.omit": "^4.5.0" 36 | }, 37 | "devDependencies": { 38 | "ava": "^2.4.0", 39 | "cloc": "^2.5.0", 40 | "codecov": "^3.6.1", 41 | "colors": "^1.4.0", 42 | "nyc": "^14.1.1", 43 | "pre-commit": "^1.2.2", 44 | "snazzy": "^8.0.0", 45 | "standard": "^14.3.1" 46 | }, 47 | "standard": { 48 | "ignore": [ 49 | "test/*.js" 50 | ] 51 | }, 52 | "nyc": { 53 | "exclude": [ 54 | "**/*.test.js", 55 | "**/node_modules/**", 56 | "src/si/upload.js", 57 | "src/si/helpers/**", 58 | "src/pantsu/upload.js", 59 | "src/pantsu/update.js", 60 | "src/pantsu/login.js", 61 | "src/pantsu/header.js", 62 | "src/pantsu/helpers/**" 63 | ] 64 | }, 65 | "pre-commit": [ 66 | "lint" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/si/upload.js: -------------------------------------------------------------------------------- 1 | const { createReadStream } = require('fs') 2 | 3 | const FormData = require('form-data') 4 | 5 | /** 6 | * 7 | * @typedef {Object} UploadOpts 8 | * @property {Object} credentials 9 | * @property {Stirng} username 10 | * @property {Stirng} password 11 | * @property {String} torrent Path to the torrent file 12 | * @property {String} category Valid nyaa.si category 13 | * @property {String} name 14 | * @property {String} [information] 15 | * @property {String} [description] 16 | * @property {Boolean} [anonymous] 17 | * @property {Boolean} [hidden] 18 | * @property {Boolean} [complete] 19 | * @property {Boolean} [remake] 20 | * @property {Boolean} [trusted] 21 | * 22 | * @typedef {Object} NyaaUploadError 23 | * @property {Object} errors 24 | * @property {String[]} errors.torrent 25 | */ 26 | 27 | /** 28 | * Allows to upload file or magnet to nyaa.si 29 | * 30 | * @param {UploadOpts} opts Object description all the information to upload 31 | * 32 | * @returns {Promise} 33 | */ 34 | async function upload (opts) { 35 | if (!opts.credentials) { 36 | throw new Error('[Nyaapi]: No credentials given on upload demand.') 37 | } 38 | 39 | if (!opts.torrent) { 40 | throw new Error('[Nyaapi]: No torrent file given on upload demand.') 41 | } 42 | 43 | if (!opts.name) { 44 | throw new Error('[Nyaapi]: No name given on upload demand.') 45 | } 46 | 47 | if (!opts.category) { 48 | throw new Error('[Nyaapi]: No category given on upload demand.') 49 | } 50 | 51 | const form = new FormData() 52 | form.append('torrent', createReadStream(opts.torrent)) 53 | form.append('torrent_data', JSON.stringify({ 54 | name: opts.name, 55 | category: opts.category, 56 | information: opts.information, 57 | description: opts.description, 58 | anonymous: opts.anonymous, 59 | hidden: opts.hidden, 60 | complete: opts.complete, 61 | remake: opts.remake, 62 | trusted: opts.trusted 63 | })) 64 | 65 | const { data } = await this.cli.post( 66 | '/api/upload', 67 | form, { 68 | auth: opts.credentials, 69 | headers: { 70 | ...form.getHeaders() 71 | } 72 | } 73 | ) 74 | 75 | return data 76 | } 77 | 78 | module.exports = { 79 | upload 80 | } 81 | -------------------------------------------------------------------------------- /src/pantsu/search.js: -------------------------------------------------------------------------------- 1 | const omit = require('lodash.omit') 2 | 3 | /** 4 | * 5 | * Research anything you desire on nyaa.pantsu.cat 6 | * 7 | * @param {string} term Keywords describing the research. 8 | * @param {number} n Number of results wanted (Defaults to null). 9 | * @param {Object} opts Research options as described on the official documentation (optional). 10 | * 11 | * @returns {promise} 12 | */ 13 | async function search (term, n = null, opts = {}) { 14 | if (!term || (typeof term === 'object' && !term.term)) { 15 | throw new Error('[Nyaapi]: No term given on search demand.') 16 | } 17 | 18 | if (typeof term === 'object') { 19 | opts = term 20 | term = opts.term 21 | n = opts.n 22 | } 23 | 24 | opts.c = opts.c || [] 25 | opts.q = term 26 | opts.limit = n || 99999 27 | opts = omit(opts, 'n') 28 | 29 | const { data } = await this.cli.get('/search', { 30 | params: opts 31 | }) 32 | 33 | return data.torrents 34 | } 35 | 36 | /** 37 | * 38 | * Research anything you desire on nyaa.pantsu.cat every single result. 39 | * 40 | * @param {string} term Keywords describing the research. 41 | * @param {Object} opts Research options as described on the official documentation (optional). 42 | * 43 | * @returns {promise} 44 | */ 45 | async function searchAll (term, opts = {}) { 46 | if (!term || (typeof term === 'object' && !term.term)) { 47 | throw new Error('[Nyaapi]: No term given on search demand.') 48 | } 49 | 50 | if (typeof term === 'object') { 51 | opts = term 52 | term = opts.term 53 | } 54 | 55 | let results = [] 56 | let torrents = [] 57 | opts.page = 1 58 | let _continue = true 59 | 60 | while (_continue && opts.page < 4) { 61 | // We stop at 900 results, that should be enough 62 | torrents = await this.search(term, null, opts) 63 | ++opts.page 64 | results = results.concat(torrents) 65 | 66 | _continue = torrents.length 67 | } 68 | 69 | return results 70 | } 71 | 72 | /** 73 | * 74 | * List specific category on nyaa.pantsu.cat 75 | * 76 | * @param {string} c Category to list. 77 | * @param {number} p Page of the category. 78 | * @param {Object} opts Research options as described on the official documentation (optional). 79 | * 80 | * @returns {promise} 81 | */ 82 | async function list (c, p, opts = {}) { 83 | if (typeof c === 'object') { 84 | opts = c 85 | c = opts.c 86 | p = p || opts.p 87 | } 88 | 89 | opts.c = c || [] 90 | opts.page = p || 1 91 | opts.limit = opts.n || 100 92 | opts = omit(opts, 'n') 93 | 94 | const { data } = await this.cli.get('/search', { params: opts }) 95 | 96 | return data.torrents 97 | } 98 | 99 | module.exports = { 100 | list, 101 | search, 102 | searchAll 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Nyaapi 2.0

2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 |

10 | 11 | 12 | 13 |

14 | 15 |

16 | 17 | Build Status 18 | 19 | 20 | Codecov 21 | 22 | 23 | License 24 | 25 |

26 | 27 | This is an api allowing one to: 28 | * gather torrents directly from [nyaa.si](https://nyaa.si) and [nyaa.pantsu.cat](https://nyaa.pantsu.cat) in about a second or less. 29 | * upload a torrent to any nyaa. 30 | * check a user's profile and torrents. 31 | * So many things you should check the wiki to understand better. 32 | 33 | __All the documentation there is to know about how to use Nyaapi is located in the [wiki](https://github.com/Kylart/Nyaapi/wiki).__ 34 | 35 | Any contribution is welcomed. 36 | 37 | # Install 38 | ``` 39 | npm install --save nyaapi 40 | ``` 41 | 42 | # Use 43 | Nyaapi is organised with `si` methods and `pantsu` methods. 44 | You can access either of them like so: 45 | ```javascript 46 | const {si, pantsu} = require('nyaapi') 47 | 48 | console.log(si) 49 | /** 50 | * [Si] methods: 51 | * > list 52 | * > search 53 | * > searchAll 54 | * > searchPage 55 | * > searchByUser 56 | * > searchAllByUser 57 | * > searchByUserAndByPage 58 | * > infoRequest 59 | * > upload 60 | * 61 | */ 62 | console.log(pantsu) 63 | /** 64 | * [Pantsu] methods: 65 | * > list 66 | * > search 67 | * > searchAll 68 | * > infoRequest 69 | * > upload 70 | * > update 71 | * > login 72 | * > checkUser 73 | * > checkHeader 74 | * 75 | */ 76 | ``` 77 | 78 | # Configuration 79 | For both `si` and `pantsu` you can update the base URL for the calls this way: 80 | ```javascript 81 | const { si, pantsu } = require('nyaapi') 82 | 83 | si.config.updateBaseUrl('https://nyaa.whatever') 84 | pantsu.config.updateBaseUrl('https://nyaa.whatever') 85 | ``` 86 | 87 | It is important to know that all the pantsu methods are fully based on [the offcial api of nyaa.pantsu.cat](https://nyaa.pantsu.cat/apidoc). 88 | 89 | > For a complete documentation, please check out the [wiki](https://github.com/Kylart/Nyaapi/wiki) for a tour of all the methods and how to use them. -------------------------------------------------------------------------------- /src/si/scrap.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio') 2 | 3 | const { url } = require('./helpers/config') 4 | 5 | function extractFromHTML (data, includeMaxPage = false) { 6 | const $ = cheerio.load(data) 7 | const baseUrl = url 8 | const results = [] 9 | 10 | const _getChild = (ctx, nb) => { 11 | return $(ctx).find(`td:nth-child(${nb})`) 12 | } 13 | 14 | $('tr').slice(1).each(function () { 15 | // speical handling for hash, as very rarely the download option will be unavailable and missing. 16 | let hash = "" 17 | let magnetElement = _getChild(this, 3).find('a:nth-child(2)').attr('href') 18 | if (!magnetElement) 19 | magnetElement = _getChild(this, 3).find('a:nth-child(1)').attr('href') 20 | // magnetElement is assumed to be valid, if the magnet option is unavailable the torrent wouldn't be present. 21 | hash = magnetElement.match(/btih:(\w+)/)[1] 22 | 23 | const result = { 24 | id: _getChild(this, 2).find('a:not(.comments)').attr('href').replace('/view/', ''), 25 | name: _getChild(this, 2).find('a:not(.comments)').text().trim(), 26 | hash: hash, 27 | date: new Date(_getChild(this, 5).attr('data-timestamp') * 1000).toISOString(), 28 | filesize: _getChild(this, 4).text(), 29 | category: _getChild(this, 1).find('a').attr('href').replace('/?c=', '').replace(/\d{1,2}$/, '0'), 30 | sub_category: _getChild(this, 1).find('a').attr('href').replace('/?c=', ''), 31 | magnet: magnetElement, 32 | torrent: baseUrl + _getChild(this, 3).find('a:nth-child(1)').attr('href'), 33 | seeders: _getChild(this, 6).text(), 34 | leechers: _getChild(this, 7).text(), 35 | completed: _getChild(this, 8).text(), 36 | status: $(this).attr('class') 37 | } 38 | 39 | results.push(result) 40 | }) 41 | 42 | if (includeMaxPage) { 43 | /* istanbul ignore next */ 44 | return includeMaxPage 45 | ? { results, maxPage: +$('ul.pagination li:nth-last-child(2) a').text() } 46 | : results 47 | } else { 48 | return results 49 | } 50 | } 51 | 52 | function extractPageFromHTML (data) { 53 | const $ = cheerio.load(data) 54 | const baseUrl = url 55 | 56 | return { 57 | name: $('.panel-heading > .panel-title').eq(0).text().trim(), 58 | hash: $('div > kbd').text().trim().toLowerCase(), 59 | date: new Date($('div[class="col-md-1"]:contains("Date:")').next().attr('data-timestamp') * 1000).toISOString(), 60 | filesize: $('div[class="col-md-1"]:contains("File size:")').next().text(), 61 | description: $('#torrent-description').text(), 62 | category: $('div[class="col-md-5"] > a').eq(0).attr('href').replace('/?c=', ''), 63 | sub_category: $('div[class="col-md-5"] > a').eq(1).attr('href').replace('/?c=', ''), 64 | uploader_name: $('div[class="col-md-1"]:contains("Submitter:")').next().text().trim(), 65 | magnet: $('.panel-footer > a').eq(1).attr('href'), 66 | torrent: baseUrl + $('.panel-footer > a').eq(0).attr('href'), 67 | seeders: $('div[class="col-md-1"]:contains("Seeders:")').next().children().first().text(), 68 | leechers: $('div[class="col-md-1"]:contains("Leechers:")').next().children().first().text(), 69 | completed: $('div[class="col-md-1"]:contains("Completed:")').next().text() 70 | } 71 | } 72 | 73 | module.exports = { 74 | extractFromHTML, 75 | extractPageFromHTML 76 | } 77 | -------------------------------------------------------------------------------- /test/pantsu.test.js: -------------------------------------------------------------------------------- 1 | require('colors') 2 | const test = require('ava') 3 | 4 | const {pantsu} = require('../src/index.js') 5 | 6 | const fansub = '[HorribleSubs]' 7 | const quality = '720p' 8 | const anime = 'Youjo senki' 9 | const number = 18 10 | 11 | test.before('[Pantsu] methods', () => { 12 | console.log('[Pantsu] methods:'.green) 13 | Object.keys(pantsu).forEach((key) => { 14 | console.log(` > ${key}`.yellow) 15 | }) 16 | }) 17 | 18 | test('Search method gives 18 results with 3 arguments', async t => { 19 | try { 20 | const data = await pantsu.search(fansub, number, {order: true}) 21 | 22 | t.is(data.length, number) 23 | } catch (e) { 24 | t.fail(e.message) 25 | } 26 | }) 27 | 28 | test('Search method gives results with 1 object argument', async t => { 29 | try { 30 | const data = await pantsu.search({ 31 | term: fansub, 32 | n: 52, 33 | order: true 34 | }) 35 | 36 | t.is(data.length, 52) 37 | } catch (e) { 38 | t.fail(e.message) 39 | } 40 | }) 41 | 42 | test('Search method return an error if no term is given', async t => { 43 | try { 44 | const data = await pantsu.search() 45 | 46 | t.fail() 47 | } catch (e) { 48 | t.true(e.message.includes('[Nyaapi]')) 49 | } 50 | }) 51 | 52 | test('Search method return an error if no term is given in object arg', async t => { 53 | try { 54 | const data = await pantsu.search({}) 55 | 56 | t.fail() 57 | } catch (e) { 58 | t.true(e.message.includes('[Nyaapi]')) 59 | } 60 | }) 61 | 62 | 63 | test('SearchAll method gives 900 results with 3 arguments', async t => { 64 | try { 65 | const data = await pantsu.searchAll(fansub, {sort: true}) 66 | 67 | t.is(data.length, 900) 68 | } catch (e) { 69 | t.fail(e.message) 70 | } 71 | }) 72 | 73 | test('SearchAll method gives 900 results with 2 arguments', async t => { 74 | try { 75 | const data = await pantsu.searchAll({ 76 | term: fansub, 77 | sort: true 78 | }) 79 | 80 | t.is(data.length, 900) 81 | } catch (e) { 82 | t.fail(e.message) 83 | } 84 | }) 85 | 86 | test('SearchAll method return an error if no term is given', async t => { 87 | try { 88 | const data = await pantsu.searchAll() 89 | 90 | t.fail() 91 | } catch (e) { 92 | t.true(e.message.includes('[Nyaapi]')) 93 | } 94 | }) 95 | 96 | test('SearchAll method return an error if no term is given in object arg', async t => { 97 | try { 98 | const data = await pantsu.searchAll({}) 99 | 100 | t.fail() 101 | } catch (e) { 102 | t.true(e.message.includes('[Nyaapi]')) 103 | } 104 | }) 105 | 106 | test('InfoRequest method gives right info', async t => { 107 | try { 108 | const data = await pantsu.infoRequest(510619) 109 | 110 | t.is(data.name, '[HorribleSubs] Sakura Trick - 01 [1080p].mkv') 111 | t.is(data.hash, 'DAD8DB31EB1DAD06E651621818AF427C4F1D04FE') 112 | } catch (e) { 113 | t.fail(e.message) 114 | } 115 | }) 116 | 117 | test('InfoRequest method returns an error if no ID is given', async t => { 118 | try { 119 | const data = await pantsu.infoRequest() 120 | 121 | t.fail() 122 | } catch (e) { 123 | t.true(e.message.includes('[Nyaapi]')) 124 | } 125 | }) 126 | 127 | test('CheckUser method gives right info', async t => { 128 | try { 129 | const data = await pantsu.checkUser(12035) 130 | 131 | t.is(data.ok, true) 132 | t.is(data.data.username, 'HorribleSubs') 133 | t.is(data.data.created_at, '2017-07-02T14:52:25Z') 134 | } catch (e) { 135 | t.fail(e.message) 136 | } 137 | }) 138 | 139 | test('Checkuser method returns an error if no ID is given', async t => { 140 | try { 141 | const data = await pantsu.checkUser() 142 | 143 | t.fail() 144 | } catch (e) { 145 | t.true(e.message.includes('[Nyaapi]')) 146 | } 147 | }) -------------------------------------------------------------------------------- /src/si/search.js: -------------------------------------------------------------------------------- 1 | const { extractFromHTML } = require('./scrap.js') 2 | 3 | const timeout = (time) => new Promise(resolve => setTimeout(resolve, time)) 4 | 5 | /** 6 | * Allows to scrap only one specific page of a research. 7 | * 8 | * @param {string} term Keywords describing the research. 9 | * @param {number} p The page you want to look for. 10 | * @param {object} opts Research options as described on the documentation. 11 | */ 12 | async function searchPage (term = '', p, opts = {}, includeMaxPage) { 13 | if (!term) throw new Error('[Nyaapi]: No term was given on search demand.') 14 | 15 | if (typeof term === 'object') { 16 | opts = term 17 | term = opts.term 18 | p = p || opts.p 19 | } 20 | 21 | if (!p) throw new Error('[Nyaapi]: No page number was given on search page demand.') 22 | 23 | const { data } = await this.cli.get('/', { 24 | params: { 25 | f: opts.filter || 0, 26 | c: opts.category || '1_0', 27 | q: term, 28 | p: p, 29 | s: opts.sort || 'id', 30 | o: opts.direction || 'desc' 31 | } 32 | }) 33 | 34 | return extractFromHTML(data, includeMaxPage) 35 | } 36 | 37 | /** 38 | * Research anything you desire on nyaa.si and get all the results. 39 | * 40 | * @param {string} term Keywords describing the research. 41 | * @param {Object} opts Research options as described on the documentation. 42 | */ 43 | async function searchAll (term = '', opts = {}) { 44 | if (!term || (typeof term === 'object' && !term.term)) { 45 | throw new Error('[Nyaapi]: No search term was given.') 46 | } 47 | 48 | if (typeof term === 'object') { 49 | opts = term 50 | term = opts.term 51 | } 52 | 53 | const { results: tempResults, maxPage } = await this.searchPage(term, 1, opts, true) 54 | 55 | const searchs = [] 56 | for (let page = 2; page <= maxPage; ++page) { 57 | const makeSearch = () => 58 | this.searchPage(term, page, opts) 59 | .catch(e => timeout(1000).then(makeSearch)) 60 | 61 | searchs.push(makeSearch()) 62 | } 63 | 64 | const results = await Promise.all(searchs) 65 | 66 | return results.reduce((acc, result) => acc.concat(result), tempResults) 67 | } 68 | 69 | /** 70 | * Research anything you desire on nyaa.si. 71 | * 72 | * @param {string} term Keywords describing the research. 73 | * @param {number} n Number of results wanted (Defaults to null). 74 | * @param {Object} opts Research options as described on the documentation. 75 | */ 76 | async function search (term = '', n = null, opts = {}) { 77 | if (!term || (typeof term === 'object' && !term.term)) { 78 | throw new Error('[Nyaapi]: No term given on search demand.') 79 | } 80 | 81 | if (typeof term === 'object') { 82 | opts = term 83 | term = opts.term 84 | n = n || opts.n 85 | } 86 | 87 | // If there is no n, then the user's asking for all the results, right? 88 | if (!n) { 89 | return this.searchAll(term, opts) 90 | } else { 91 | let results = [] 92 | let tmpData = [] 93 | let _continue = true 94 | let page = 1 95 | const maxPage = Math.ceil(n / 75) 96 | 97 | while (_continue && page <= maxPage) { 98 | tmpData = await this.searchPage(term, page, opts) 99 | results = results.concat(tmpData) 100 | ++page 101 | _continue = tmpData.length 102 | } 103 | 104 | return results.slice(0, n) 105 | } 106 | } 107 | 108 | /** 109 | * Research anything you desire according to a certain user and a specific page on nyaa.si. 110 | * 111 | * @param {string} user The user you want to spy on. 112 | * @param {string} term Keywords describing the research. 113 | * @param {number} p The page you want to look for. 114 | * @param {number} n Number of results wanted on this page (Defaults to null). 115 | * @param {Object} opts Research options as described on the documentation. 116 | * 117 | * @returns {promise} 118 | */ 119 | async function searchByUserAndByPage (user = null, term = '', p = null, n = null, opts = {}) { 120 | if (!user) throw new Error('[Nyaapi]: No user given on search demand.') 121 | 122 | if (typeof user === 'object') { 123 | opts = user 124 | user = opts.user 125 | p = opts.p 126 | term = term || opts.term 127 | n = n || opts.n || 75 128 | } 129 | 130 | if (!p) throw new Error('[Nyaapi]: No page given on search by page demand.') 131 | 132 | const { data } = await this.cli.get(`/user/${user}`, { 133 | params: { 134 | f: opts.filter || 0, 135 | c: opts.category || '1_0', 136 | q: term || '', 137 | p 138 | } 139 | }) 140 | 141 | const results = extractFromHTML(data) 142 | 143 | return results.slice(0, n || results.length) 144 | } 145 | 146 | /** 147 | * Research anything you desire according to a certain user on nyaa.si and get all the results. 148 | * 149 | * @param {string} user The user you want to spy on. 150 | * @param {string} term Keywords describing the research. 151 | * @param {Object} opts Research options as described on the documentation. 152 | */ 153 | async function searchAllByUser (user = null, term = '', opts = {}) { 154 | if (!user || (typeof user === 'object' && user && !user.user)) { 155 | throw new Error('[Nyaapi]: No user was given.') 156 | } 157 | 158 | if (typeof user === 'object') { 159 | opts = user 160 | term = opts.term 161 | user = opts.user 162 | } 163 | 164 | let page = 1 165 | let results = [] 166 | let tmpData = [] 167 | 168 | while (page <= 15) { 169 | // We stop at page === 15 because nyaa.si offers a maximum of 1000 results on standard research 170 | results = results.concat(tmpData) 171 | 172 | opts.user = user 173 | opts.term = term 174 | opts.p = page 175 | 176 | try { 177 | tmpData = await this.searchByUserAndByPage(opts) 178 | ++page 179 | } catch (e) { 180 | if (e.statusCode !== 404) { throw e } 181 | 182 | break 183 | } 184 | } 185 | 186 | return results 187 | } 188 | 189 | /** 190 | * Research anything you desire according to a certain user on nyaa.si 191 | * 192 | * @param {string} user The user you want to spy on. 193 | * @param {string} term Keywords describing the research. 194 | * @param {number} n Number of results wanted on this page (Defaults to null). 195 | * @param {Object} opts Research options as described on the documentation. 196 | */ 197 | async function searchByUser (user = null, term = '', n = null, opts = {}) { 198 | if (!user || (typeof user === 'object' && user && !user.user)) { 199 | throw new Error('[Nyaapi]: No user given on search demand.') 200 | } 201 | 202 | if (typeof user === 'object') { 203 | opts = user 204 | user = opts.user 205 | term = term || opts.term || '' 206 | n = n || opts.n 207 | } 208 | 209 | // If there is no n, then the user's asking for all the results, right? 210 | if (!n) { 211 | return this.searchAllByUser(user, term, opts) 212 | } else { 213 | let results = [] 214 | let tmpData = [] 215 | let page = 1 216 | let _continue = true 217 | const maxPage = Math.ceil(n / 75) 218 | 219 | while (_continue && page <= maxPage) { 220 | opts.user = user 221 | opts.term = term 222 | opts.p = page 223 | 224 | tmpData = await this.searchByUserAndByPage(opts) 225 | results = results.concat(tmpData) 226 | ++page 227 | _continue = tmpData.length 228 | } 229 | 230 | return results.slice(0, n) 231 | } 232 | } 233 | 234 | /** 235 | * List specific category on nyaa.si. 236 | * 237 | * @param {string} c Category to list. 238 | * @param {number} p Page of the category. 239 | * @param {object} opts Research options as described on the documentation. 240 | */ 241 | async function list (c, p, opts = {}) { 242 | if (typeof c === 'object') { 243 | opts = c 244 | c = opts.c 245 | p = p || opts.p 246 | } 247 | 248 | const { data } = await this.cli.get('/', { 249 | params: { 250 | f: opts.filter || 0, 251 | c: c || '1_0', 252 | p: p || 1, 253 | s: opts.sort || 'id', 254 | o: opts.direction || 'desc' 255 | } 256 | }) 257 | 258 | return extractFromHTML(data, true) 259 | } 260 | 261 | module.exports = { 262 | list, 263 | search, 264 | searchAll, 265 | searchPage, 266 | searchByUser, 267 | searchAllByUser, 268 | searchByUserAndByPage 269 | } 270 | -------------------------------------------------------------------------------- /test/si.test.js: -------------------------------------------------------------------------------- 1 | require('colors') 2 | const test = require('ava') 3 | 4 | const {si} = require('../src/index.js') 5 | 6 | const fansub = 'horriblesubs' 7 | const quality = '720p' 8 | const anime = 'Youjo senki' 9 | const number = 18 10 | 11 | test.before('[Si] methods', () => { 12 | console.log('[Si] methods:'.green) 13 | Object.keys(si).forEach((key) => { 14 | console.log(` > ${key}`.yellow) 15 | }) 16 | }) 17 | 18 | test.beforeEach(async t => { 19 | await new Promise(resolve => setTimeout(resolve, 1500)) 20 | }) 21 | 22 | test(`Search method gives ${number} results with 3 arguments`, async t => { 23 | try { 24 | const data = await si.search(anime, number, {filter: 2}) 25 | 26 | t.is(data.length, number) 27 | } catch (e) { 28 | console.log(e) 29 | t.fail(e.message) 30 | } 31 | }) 32 | 33 | test(`Search method gives ${number} results with 1 object argument`, async t => { 34 | try { 35 | const data = await si.search({ 36 | term: anime, 37 | n: number, 38 | filter: 2 39 | }) 40 | 41 | t.is(data.length, number) 42 | } catch (e) { 43 | t.fail(e.message) 44 | } 45 | }) 46 | 47 | test(`Search method gives 1000 results with no number`, async t => { 48 | try { 49 | const data = await si.search({ 50 | term: fansub, 51 | filter: 2 52 | }) 53 | 54 | t.is(data.length, 1000) 55 | } catch (e) { 56 | t.fail(e.message) 57 | } 58 | }) 59 | 60 | test('Search method fails if no term is given', async t => { 61 | try { 62 | const data = await si.search() 63 | t.fail() 64 | } catch (e) { 65 | t.true(e.message.includes('[Nyaapi]')) 66 | } 67 | }) 68 | 69 | test('SearchAll method returns a 1000 results with 1 object argument', async t => { 70 | try { 71 | const data = await si.searchAll({ 72 | term: fansub, 73 | filter: '2' 74 | }) 75 | 76 | t.is(data.length, 1000) 77 | } catch (e) { 78 | t.fail(e.message) 79 | } 80 | }) 81 | 82 | test('SearchAll method returns a 1000 results with 2 arguments', async t => { 83 | try { 84 | const data = await si.searchAll(fansub, { 85 | filter: '2' 86 | }) 87 | 88 | t.is(data.length, 1000) 89 | } catch (e) { 90 | t.fail(e.message) 91 | } 92 | }) 93 | 94 | test('SearchAll method returns an error if no term is given', async t => { 95 | try { 96 | const data = await si.searchAll() 97 | 98 | t.fail() 99 | } catch (e) { 100 | t.true(e.message.includes('[Nyaapi]')) 101 | } 102 | }) 103 | 104 | test('SearchAll method returns an error if no term in object is given', async t => { 105 | try { 106 | const data = await si.searchAll({}) 107 | 108 | t.fail() 109 | } catch (e) { 110 | t.true(e.message.includes('[Nyaapi]')) 111 | } 112 | }) 113 | 114 | test('SearchPage method returns 75 results with 1 object argument', async t => { 115 | try { 116 | const data = await si.searchPage({ 117 | term: fansub, 118 | p: 2, 119 | filter: '2' 120 | }) 121 | 122 | t.is(data.length, 75) 123 | } catch (e) { 124 | t.fail(e.message) 125 | } 126 | }) 127 | 128 | test('SearchPage method returns a 75 results with 3 arguments', async t => { 129 | try { 130 | const data = await si.searchPage(fansub, 3, { 131 | filter: '2' 132 | }) 133 | 134 | t.is(data.length, 75) 135 | } catch (e) { 136 | t.fail(e.message) 137 | } 138 | }) 139 | 140 | test('SearchPage method returns an error if no term is given', async t => { 141 | try { 142 | const data = await si.searchPage() 143 | 144 | t.fail() 145 | } catch (e) { 146 | t.true(e.message.includes('[Nyaapi]')) 147 | } 148 | }) 149 | 150 | test('SearchPage method returns an error if no page is given', async t => { 151 | try { 152 | const data = await si.searchPage(fansub) 153 | 154 | t.fail() 155 | } catch (e) { 156 | t.true(e.message.includes('[Nyaapi]')) 157 | } 158 | }) 159 | 160 | test(`SearchByUser method returns ${number} with 4 arguments`, async t => { 161 | try { 162 | const data = await si.searchByUser(fansub, anime, number, {filter: 2}) 163 | 164 | t.is(data.length, number) 165 | } catch (e) { 166 | t.fail(e.message) 167 | } 168 | }) 169 | 170 | test(`SearchByUser method returns ${number} with 1 object argument`, async t => { 171 | try { 172 | const data = await si.searchByUser({ 173 | user: fansub, 174 | term: anime, 175 | n: number, 176 | filter: 2 177 | }) 178 | 179 | t.is(data.length, number) 180 | } catch (e) { 181 | t.fail(e.message) 182 | } 183 | }) 184 | 185 | test('SearchByUser method returns a 1000 results if no n is given', async t => { 186 | try { 187 | const data = await si.searchByUser({ 188 | user: fansub, 189 | filter: 2 190 | }) 191 | 192 | t.is(data.length, 1050) 193 | } catch (e) { 194 | t.fail(e.message) 195 | } 196 | }) 197 | 198 | test('SearchByUser method returns an error is no user is given', async t => { 199 | try { 200 | const data = await si.searchByUser() 201 | 202 | t.fail() 203 | } catch (e) { 204 | t.true(e.message.includes('[Nyaapi]')) 205 | } 206 | }) 207 | 208 | test(`SearchAllByUser method returns 41 results with 4 arguments`, async t => { 209 | try { 210 | const data = await si.searchAllByUser(fansub, anime, {filter: 2}) 211 | 212 | t.is(data.length, 41) 213 | } catch (e) { 214 | t.fail(e.message) 215 | } 216 | }) 217 | 218 | test(`SearchAllByUser method returns 41 results with 1 object argument`, async t => { 219 | try { 220 | const data = await si.searchAllByUser({ 221 | user: fansub, 222 | term: anime, 223 | filter: 2 224 | }) 225 | 226 | t.is(data.length, 41) 227 | } catch (e) { 228 | t.fail(e.message) 229 | } 230 | }) 231 | 232 | test(`SearchAllByUser method returns 1050 results with 1 object argument and no term`, async t => { 233 | try { 234 | const data = await si.searchAllByUser(fansub) 235 | 236 | t.is(data.length, 1050) 237 | } catch (e) { 238 | t.fail(e.message) 239 | } 240 | }) 241 | 242 | test(`SearchAllByUser method returns an error if no user is given`, async t => { 243 | try { 244 | const data = await si.searchAllByUser({ 245 | term: anime, 246 | filter: 2 247 | }) 248 | 249 | t.fail() 250 | } catch (e) { 251 | t.true(e.message.includes('[Nyaapi]')) 252 | } 253 | }) 254 | 255 | test(`SearchAllByUser method returns an error if no argument is given`, async t => { 256 | try { 257 | const data = await si.searchAllByUser() 258 | 259 | t.fail() 260 | } catch (e) { 261 | t.true(e.message.includes('[Nyaapi]')) 262 | } 263 | }) 264 | 265 | test('SearchByUserAndByPage method returns 75 results with 1 object argument', async t => { 266 | try { 267 | const data = await si.searchByUserAndByPage({ 268 | user: fansub, 269 | p: 2, 270 | filter: '2' 271 | }) 272 | 273 | t.is(data.length, 75) 274 | } catch (e) { 275 | t.fail(e.message) 276 | } 277 | }) 278 | 279 | test('SearchByUserAndByPage method returns a 75 results with 4 arguments', async t => { 280 | try { 281 | const data = await si.searchByUserAndByPage(fansub, '', 3, null,{ 282 | filter: '2' 283 | }) 284 | 285 | t.is(data.length, 75) 286 | } catch (e) { 287 | t.fail(e.message) 288 | } 289 | }) 290 | 291 | test('SearchByUserAndByPage method returns an error if no term is given', async t => { 292 | try { 293 | const data = await si.searchByUserAndByPage() 294 | 295 | t.fail() 296 | } catch (e) { 297 | t.true(e.message.includes('[Nyaapi]')) 298 | } 299 | }) 300 | 301 | test('SearchByUserAndByPage method returns an error if no page is given', async t => { 302 | try { 303 | const data = await si.searchByUserAndByPage(fansub) 304 | 305 | t.fail() 306 | } catch (e) { 307 | t.true(e.message.includes('[Nyaapi]')) 308 | } 309 | }) 310 | 311 | test('infoRequest method returns an error if invalid id is given', async t => { 312 | try { 313 | const data = await si.infoRequest('blabla') 314 | 315 | t.fail() 316 | } catch (e) { 317 | t.true(e.message.includes('[Nyaapi]')) 318 | } 319 | }) 320 | 321 | test('infoRequest method returns an error if no id is given', async t => { 322 | try { 323 | const data = await si.infoRequest() 324 | 325 | t.fail() 326 | } catch (e) { 327 | t.true(e.message.includes('[Nyaapi]')) 328 | } 329 | }) 330 | 331 | test('infoRequest method returns a valid result', async t => { 332 | try { 333 | const id = 537126 334 | const data = await si.infoRequest(id) 335 | 336 | t.is(typeof data, 'object') 337 | t.is(data.id, id) 338 | t.is(typeof data.info, 'object') 339 | } catch (e) { 340 | t.fail(e.message) 341 | } 342 | }) --------------------------------------------------------------------------------