├── .gitignore ├── .travis.yml ├── CONTRIBUTORS.md ├── LICENCE ├── README.md ├── package-lock.json ├── package.json ├── src ├── episodes.js ├── index.d.ts ├── index.js ├── info.js ├── news.js ├── pictures.js ├── recommendations.js ├── reviews.js ├── search │ ├── anime │ │ ├── genresList.json │ │ └── producersList.json │ ├── constants.js │ ├── getLists.js │ ├── index.js │ └── manga │ │ ├── genresList.json │ │ └── producersList.json ├── seasons.js ├── stats.js ├── users.js └── watchList.js └── test ├── episodes.test.js ├── info.test.js ├── news.test.js ├── pictures.test.js ├── recommendations.test.js ├── reviews.test.js ├── search.test.js ├── seasons.test.js ├── stats.test.js ├── users.test.js └── watchList.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "9" 6 | - "10" 7 | - "11" 8 | - "12" 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run test 15 | - npm run cloc 16 | after_success: npm run coverage 17 | 18 | cache: 19 | directories: 20 | - "node_modules" 21 | 22 | deploy: 23 | provider: npm 24 | email: kylart.dev@gmail.com 25 | api_key: $NPM_TOKEN 26 | on: 27 | branch: master 28 | 29 | notifications: 30 | email: false 31 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | ## Many thanks to : 2 | 3 | * [ParadoxOrigins](https://github.com/ParadoxOrigins) : Awesome documentation ✨✨✨. 4 | * [nathanial292](https://github.com/nathanial292) : Improved seasons method 5 | * [Rapougnac](https://github.com/Rapougnac) : [Support for typings](https://github.com/Kylart/MalScraper/pull/74) 6 | * [0wx](https://github.com/0wx) : [Set limit length to the keyword](https://github.com/Kylart/MalScraper/pull/73) 7 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017, Kylart 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MalScraper

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 | At the moment, _MalScraper_ allows one to: 28 | * Gather information about all the anime being released in a season. 29 | * Gather anime-related news (include light-novels, manga, films...). 160 news available. 30 | * Make an anime search (in 2 different ways!). 31 | * Get different information for this anime. 32 | * Get only the best result for an anime search. 33 | * Get a list of an anime's episodes. 34 | * Access the full official MyAnimeList API (includes search, add, update and delete from your user watch lists). 35 | 36 | _MalScraper_ is being developed mainly for [_KawAnime_](https://github.com/Kylart/KawAnime) but anyone can use it for 37 | its own purpose. 38 | 39 | Any contribution is welcomed. 40 | 41 | Tables of content: 42 | * [Installation](https://github.com/Kylart/MalScraper/blob/master/README.md#installation) 43 | * [Use](https://github.com/Kylart/MalScraper/blob/master/README.md#use) 44 | * [Methods](https://github.com/Kylart/MalScraper/blob/master/README.md#methods) 45 | - * [search.search()](https://github.com/Kylart/MalScraper/blob/master/README.md#searchsearch) 46 | - * [getInfoFromName()](https://github.com/Kylart/MalScraper/blob/master/README.md#getinfofromname) 47 | - * [getInfoFromURL()](https://github.com/Kylart/MalScraper/blob/master/README.md#getinfofromurl) 48 | - * [getResultsFromSearch()](https://github.com/Kylart/MalScraper/blob/master/README.md#getresultsfromsearch) 49 | - * [getWatchListFromUser()](https://github.com/Kylart/MalScraper/blob/master/README.md#getwatchlistfromuser) 50 | - * [getSeason()](https://github.com/Kylart/MalScraper/blob/master/README.md#getseason) 51 | - * [getNewsNoDetails()](https://github.com/Kylart/MalScraper/blob/master/README.md#getnewsnodetails) 52 | - * [getEpisodesList()](https://github.com/Kylart/MalScraper/blob/master/README.md#getepisodeslist) 53 | - * [getReviewsList()](https://github.com/Kylart/MalScraper/blob/master/README.md#getreviewslist) 54 | - * [getRecommendationsList()](https://github.com/Kylart/MalScraper/blob/master/README.md#getrecommendationslist) 55 | - * [getStats()](https://github.com/Kylart/MalScraper/blob/master/README.md#getstats) 56 | - * [getPictures()](https://github.com/Kylart/MalScraper/blob/master/README.md#getpictures) 57 | - * [getUser()](https://github.com/Kylart/MalScraper/blob/master/README.md#getuser) 58 | * [Data models](https://github.com/Kylart/MalScraper/blob/master/README.md#data-models) 59 | - * [Anime data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-data-model) 60 | - * [Character data model](https://github.com/Kylart/MalScraper/blob/master/README.md#character-data-model) 61 | - * [Staff data model](https://github.com/Kylart/MalScraper/blob/master/README.md#staff-data-model) 62 | - * [Search result data model](https://github.com/Kylart/MalScraper/blob/master/README.md#search-result-data-model) 63 | - * [Seasonal release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-release-data-model) 64 | - * [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) 65 | - * [News data model](https://github.com/Kylart/MalScraper/blob/master/README.md#news-data-model) 66 | * [Contributing](https://github.com/Kylart/MalScraper/blob/master/README.md#contributing) 67 | * [License](https://github.com/Kylart/MalScraper/blob/master/README.md#license) 68 | 69 | ## Installation 70 | ```npm install --save mal-scraper``` 71 | 72 | ## Use 73 | ```javascript 74 | const malScraper = require('mal-scraper') 75 | ``` 76 | 77 | ## Methods 78 | 79 | ### search.search() 80 | | Parameter | Type | Description | 81 | | --- | --- | --- | 82 | | type | string | type of search (manga or anime) | 83 | | opts | object | options for search (all keys are optional) | 84 | 85 | Usage example: 86 | ```js 87 | const malScraper = require('mal-scraper') 88 | const search = malScraper.search 89 | 90 | const type = 'anime' 91 | 92 | // Helpers for types, genres and list you might need for your research 93 | console.log(search.helpers) 94 | 95 | search.search(type, { 96 | // All optionnals, but all values must be in their relative search.helpers.availableValues. 97 | maxResults: 100, // how many results at most (default: 50) 98 | has: 250, // If you already have results and just want what follows it, you can say it here. Allows pagination! 99 | 100 | term: 'Sakura', // search term 101 | type: 0, // 0-> none, else go check search.helpers.availableValues.type 102 | status: 0, // 0 -> none, else go check https://github.com/Kylart/MalScraper/blob/master/README.md#series-statuses-references or search.helpers.availableValues.status 103 | score: 0, // 0-> none, else go check search.helpers.availableValues.score 104 | producer: 0, // go check search.helpers.availableValue.p..value 105 | rating: 0, // 0-> none, else go check search.helpers.availableValues.r 106 | startDate: { 107 | day: 12, 108 | month: 2, 109 | year: 1990 110 | }, 111 | endDate: { 112 | day: 12, 113 | month: 2, 114 | year: 2015 115 | }, 116 | genreType: 0, // 0 for include genre list, 1 for exclude genre list 117 | genres: [1] // go check search.helpers.availableValues.genres..value 118 | }) 119 | .then(console.log) 120 | .catch(console.error) 121 | ``` 122 | 123 | Returns: A [Anime search model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-search-model) 124 | or [Manga search model](https://github.com/Kylart/MalScraper/blob/master/README.md#manga-search-model) object 125 | 126 | ### getInfoFromName() 127 | 128 | | Parameter | Type | Description | 129 | | --- | --- | --- | 130 | | Name | string | The name of the anime to search, the best match corresponding to that name will be returned | 131 | | getBestMatch | Boolean | Whether you want to use [`match-sorter`](https://github.com/kentcdodds/match-sorter) to find the best result or not (defaults to true) | 132 | | type | string | The type, can be either `manga` or `anime`. Default is `anime` | 133 | 134 | Usage example: 135 | 136 | ```js 137 | const malScraper = require('mal-scraper') 138 | 139 | const name = 'Sakura Trick' 140 | 141 | malScraper.getInfoFromName(name) 142 | .then((data) => console.log(data)) 143 | .catch((err) => console.log(err)) 144 | 145 | // same as 146 | malScraper.getInfoFromName(name, true) 147 | .then((data) => console.log(data)) 148 | .catch((err) => console.log(err)) 149 | 150 | malScraper.getInfoFromName(name, false) 151 | .then((data) => console.log(data)) 152 | .catch((err) => console.log(err)) 153 | 154 | // same as 155 | malScraper.getInfoFromName(name, true, 'anime') 156 | .then((data) => console.log(data)) 157 | .catch((err) => console.log(err)) 158 | 159 | malScraper.getInfoFromName(name, false, 'anime') 160 | .then((data) => console.log(data)) 161 | .catch((err) => console.log(err)) 162 | ``` 163 | 164 | Returns: A [Anime data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-data-model) object 165 | 166 | ### getInfoFromURL() 167 | 168 | This method is faster than `getInfoFromName()` as it only make one HTTP request 169 | 170 | | Parameter | Type | Description | 171 | | --- | --- | --- | 172 | | URL | string | The URL to the anime | 173 | 174 | Usage example: 175 | 176 | ```js 177 | const malScraper = require('mal-scraper') 178 | 179 | const url = 'https://myanimelist.net/anime/20047/Sakura_Trick' 180 | 181 | malScraper.getInfoFromURL(url) 182 | .then((data) => console.log(data)) 183 | .catch((err) => console.log(err)) 184 | ``` 185 | 186 | Returns: A [Anime data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-data-model) object (same as `getInfoFromName()`) 187 | 188 | ### getResultsFromSearch() 189 | 190 | | Parameter | Type | Description | 191 | | --- | --- | --- | 192 | | query | string | The search query | 193 | | type | string | The type, can be either `manga` or `anime`. Default is `anime` | 194 | 195 | Usage example: 196 | 197 | ```js 198 | const malScraper = require('mal-scraper') 199 | 200 | const query = 'sakura' 201 | 202 | malScraper.getResultsFromSearch(query, 'anime') 203 | .then((data) => console.log(data)) 204 | .catch((err) => console.log(err)) 205 | ``` 206 | 207 | Returns: An array of a maximum length of 10 containing [Search result data model](https://github.com/Kylart/MalScraper/blob/master/README.md#search-result-data-model) objects 208 | 209 | ### getSeason() 210 | 211 | This method get the list of anime, OVAs, movies and ONAs released (or planned to be released) during the season of the specified year 212 | 213 | | Parameter | Optional | Type | Description | 214 | | --- | --- |--- | --- | 215 | | year | No | number | The year | 216 | | season | No | string | The season, must be either `spring`, `summer`, `fall` or `winter` | 217 | | type | Yes | string | The type, must be either `TV`, `TVNew`, `TVCon`, `ONAs`, `OVAs`, `Specials` or `Movies` | 218 | 219 | Usage example: 220 | 221 | ```javascript 222 | const malScraper = require('mal-scraper') 223 | 224 | const year = 2017 225 | const season = 'fall' 226 | 227 | malScraper.getSeason(year, season) 228 | // `data` is an object containing the following keys: 'TV', 'TVNew', 'TVCon', 'OVAs', 'ONAs', 'Movies' and 'Specials' 229 | .then((data) => console.log(data)) 230 | .catch((err) => console.log(err)) 231 | ``` 232 | 233 | Returns: A [Seasonal release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-release-data-model) object 234 | 235 | With type parameter: 236 | 237 | Please note: 'TVNew' represents the 'New' anime for this season, whilst 'TVCon' represents the 'Continuing' anime in this season. 'TV' is simply an aggregate for both of these. 238 | ```javascript 239 | const malScraper = require('mal-scraper') 240 | 241 | const year = 2017 242 | const season = 'fall' 243 | const type = 'TV' // Optional type parameter, if not specified will default to returning an object with all of possible type keys 244 | 245 | malScraper.getSeason(year, season, type) 246 | // `data` is an array containing all the 'Seasonal anime release data objects' for the given type 247 | .then((data) => console.log(data)) 248 | .catch((err) => console.log(err)) 249 | ``` 250 | 251 | Returns: A [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) object 252 | 253 | ### getWatchListFromUser() 254 | 255 | #### From v2.11.6 256 | 257 | | Parameter | Type | Description | 258 | | --- | --- | --- | 259 | | username | string | The name of the user | 260 | | after | number | Useful to paginate. Is the number of results you want to start from. By default, MAL returns 300 entries only. | 261 | | type | string | Optional, can be either `anime` or `manga` | 262 | | status | number | Optional, Status in the user's watch list (completed, on-hold...) | 263 | 264 | Usage example: 265 | 266 | ```javascript 267 | const malScraper = require('mal-scraper') 268 | 269 | const username = 'Kylart' 270 | const after = 25 271 | const type = 'anime' // can be either `anime` or `manga` 272 | const status = 7 // All anime 273 | 274 | // Get you an object containing all the entries with status, score... from this user's watch list 275 | malScraper.getWatchListFromUser(username, after, type, status) 276 | .then((data) => console.log(data)) 277 | .catch((err) => console.log(err)) 278 | ``` 279 | 280 | #### From v2.6.0 281 | 282 | | Parameter | Type | Description | 283 | | --- | --- | --- | 284 | | username | string | The name of the user | 285 | | after | number | Useful to paginate. Is the number of results you want to start from. By default, MAL returns 300 entries only. | 286 | | type | string | Optional, can be either `anime` or `manga` | 287 | 288 | Usage example: 289 | 290 | ```javascript 291 | const malScraper = require('mal-scraper') 292 | 293 | const username = 'Kylart' 294 | const after = 25 295 | const type = 'anime' // can be either `anime` or `manga` 296 | 297 | // Get you an object containing all the entries with status, score... from this user's watch list 298 | malScraper.getWatchListFromUser(username, after, type) 299 | .then((data) => console.log(data)) 300 | .catch((err) => console.log(err)) 301 | ``` 302 | 303 | Returns: A [User watch list data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-watch-list-data-model) object 304 | 305 | #### v2.5.2 and before 306 | | Parameter | Type | Description | 307 | | --- | --- | --- | 308 | | username | string | The name of the user | 309 | | type | string | Optional, can be either `anime` or `manga` | 310 | 311 | Usage example: 312 | 313 | ```javascript 314 | const malScraper = require('mal-scraper') 315 | 316 | const username = 'Kylart' 317 | const type = 'anime' // can be either `anime` or `manga` 318 | 319 | // Get you an object containing all the entries with status, score... from this user's watch list 320 | malScraper.getWatchListFromUser(username, type) 321 | .then((data) => console.log(data)) 322 | .catch((err) => console.log(err)) 323 | ``` 324 | 325 | Returns: A [User watch list data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-watch-list-data-model) object 326 | 327 | ### getNewsNoDetails() 328 | 329 | | Parameter | Type | Description | 330 | | --- | --- | --- | 331 | | nbNews | number | The count of news you want to get, default is 160. Note that there is 20 news per page, so if you set it to 60 for example, it will result in 3 requests. You should be aware of that, as MyAnimeList will most likely rate-limit you if more than 35-40~ requests are done in a few seconds | 332 | 333 | Usage example: 334 | 335 | ```javascript 336 | const malScraper = require('mal-scraper') 337 | 338 | const nbNews = 120 339 | 340 | malScraper.getNewsNoDetails(nbNews) 341 | .then((data) => console.log(data)) 342 | .catch((err) => console.log(err)) 343 | ``` 344 | 345 | Returns: An array of [News data model](https://github.com/Kylart/MalScraper/blob/master/README.md#news-data-model) objects 346 | 347 | ### getEpisodesList() 348 | 349 | | Parameter | Type | Description | 350 | | --- | --- | --- | 351 | | anime | object OR string | If an object, it must have the `name` and `id` property. If you only have the name and not the id, you may call the method with the name as a string, this will be slower but the id will be automatically fetched on the way | 352 | | anime.name | string | The name of the anime | 353 | | anime.id | number | The unique identifier of this anime | 354 | 355 | Usage example: 356 | 357 | ```javascript 358 | const malScraper = require('mal-scraper') 359 | 360 | malScraper.getEpisodesList({ 361 | name: 'Sakura Trick', 362 | id: 20047 363 | }) 364 | .then((data) => console.log(data)) 365 | .catch((err) => console.log(err)) 366 | 367 | //Alternatively, if you only have the name and not the id, you can let the method fetch the id on the way at the cost of being slower 368 | 369 | const name = "Sakura Trick" 370 | 371 | malScraper.getEpisodesList(name) 372 | .then((data) => console.log(data)) 373 | .catch((err) => console.log(err)) 374 | ``` 375 | 376 | Returns: An array of [Anime episodes data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-episodes-data-model) objects 377 | 378 | ### getReviewsList() 379 | 380 | | Parameter | Type | Description | 381 | | --- | --- | --- | 382 | | anime | object | An object that must have the `name` and `id` property or just the `name` alone. If you only have the name and not the id, you may call the method with the name as a string, this will be slower but the id will be automatically fetched on the way | 383 | | anime.name | string | The name of the anime | 384 | | anime.id | number | The unique identifier of this anime | 385 | | anime.limit | number | [optionnal] The number max of reviews to fetch - can be really long if omit | 386 | | anime.skip | number | [optionnal] The number of reviews to skip | 387 | 388 | Usage example: 389 | 390 | ```javascript 391 | const malScraper = require('mal-scraper') 392 | 393 | malScraper.getReviewsList({ 394 | name: 'Sakura Trick', 395 | id: 20047, 396 | limit: 1, 397 | skip: 20 398 | }) 399 | .then((data) => console.log(data)) 400 | .catch((err) => console.log(err)) 401 | 402 | //Alternatively, if you only have the name and not the id, you can let the method fetch the id on the way at the cost of being slower 403 | 404 | const name = "Sakura Trick" 405 | 406 | malScraper.getReviewsList({ 407 | name: 'Sakura Trick', 408 | limit: 1, 409 | skip: 20 410 | }) 411 | .then((data) => console.log(data)) 412 | .catch((err) => console.log(err)) 413 | ``` 414 | 415 | Returns: An array of [Anime reviews data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-reviews-data-model) objects 416 | 417 | ### getRecommendationsList() 418 | 419 | | Parameter | Type | Description | 420 | | --- | --- | --- | 421 | | anime | object OR string | If an object, it must have the `name` and `id` property. If you only have the name and not the id, you may call the method with the name as a string, this will be slower but the id will be automatically fetched on the way | 422 | | anime.name | string | The name of the anime | 423 | | anime.id | number | The unique identifier of this anime | 424 | 425 | Usage example: 426 | 427 | ```javascript 428 | const malScraper = require('mal-scraper') 429 | 430 | malScraper.getRecommendationsList({ 431 | name: 'Sakura Trick', 432 | id: 20047 433 | }) 434 | .then((data) => console.log(data)) 435 | .catch((err) => console.log(err)) 436 | 437 | //Alternatively, if you only have the name and not the id, you can let the method fetch the id on the way at the cost of being slower 438 | 439 | const name = "Sakura Trick" 440 | 441 | malScraper.getRecommendationsList(name) 442 | .then((data) => console.log(data)) 443 | .catch((err) => console.log(err)) 444 | ``` 445 | 446 | Returns: An array of [Anime recommendations data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-recommendations-data-model) objects 447 | 448 | ### getStats() 449 | 450 | | Parameter | Type | Description | 451 | | --- | --- | --- | 452 | | anime | object OR string | If an object, it must have the `name` and `id` property. If you only have the name and not the id, you may call the method with the name as a string, this will be slower but the id will be automatically fetched on the way | 453 | | anime.name | string | The name of the anime | 454 | | anime.id | number | The unique identifier of this anime | 455 | 456 | ```javascript 457 | const malScraper = require('mal-scraper') 458 | 459 | malScraper.getStats({ 460 | name: 'Ginga Eiyuu Densetsu', 461 | id: 820 462 | }) 463 | .then((data) => console.log(data)) 464 | .catch((err) => console.log(err)) 465 | 466 | //Alternatively, if you only have the name and not the id, you can let the method fetch the id on the way at the cost of being slower 467 | 468 | const name = "Ginga Eiyuu Densetsu" 469 | 470 | malScraper.getStats(name) 471 | .then((data) => console.log(data)) 472 | .catch((err) => console.log(err)) 473 | ``` 474 | 475 | Returns: An array of [Anime stats data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-stats-data-model) objects 476 | 477 | ### getPictures() 478 | 479 | | Parameter | Type | Description | 480 | | --- | --- | --- | 481 | | anime | object OR string | If an object, it must have the `name` and `id` property. If you only have the name and not the id, you may call the method with the name as a string, this will be slower but the id will be automatically fetched on the way | 482 | | anime.name | string | The name of the anime | 483 | | anime.id | number | The unique identifier of this anime | 484 | 485 | ```javascript 486 | const malScraper = require('mal-scraper') 487 | 488 | malScraper.getPictures({ 489 | name: 'Ginga Eiyuu Densetsu', 490 | id: 820 491 | }) 492 | .then((data) => console.log(data)) 493 | .catch((err) => console.log(err)) 494 | 495 | //Alternatively, if you only have the name and not the id, you can let the method fetch the id on the way at the cost of being slower 496 | 497 | const name = "Ginga Eiyuu Densetsu" 498 | 499 | malScraper.getPictures(name) 500 | .then((data) => console.log(data)) 501 | .catch((err) => console.log(err)) 502 | ``` 503 | 504 | Returns: An array of [Anime pictures data model](https://github.com/Kylart/MalScraper/blob/master/README.md#anime-pictures-data-model) objects 505 | 506 | ### getUser() 507 | | Parameter | Type | Description | 508 | | --- | --- | --- | 509 | | name | string | The username of the user | 510 | 511 | ``` javascript 512 | const malScraper = require('mal-scraper') 513 | 514 | malScraper.getUser('Kame-nos') 515 | .then((data) => console.log(data)) 516 | .catch((err) => console.log(err)) 517 | ``` 518 | Returns: A [User data model]() 519 | 520 | ## Data models 521 | 522 | ### Anime data model 523 | 524 | > You should treat all properties as possibly undefined/empty, the only guaranteed properties are `title`, `type` and `id` 525 | 526 | | Property | Type | Description | 527 | | --- | --- | --- | 528 | | title | string | The title of the anime | 529 | | synopsis | string | The synopsis of the anime | 530 | | picture | string | The URL of the cover picture of the anime | 531 | | characters | array | An array of [Character data model](https://github.com/Kylart/MalScraper/blob/master/README.md#character-data-model) objects | 532 | | staff | array | An array of [Staff data model](https://github.com/Kylart/MalScraper/blob/master/README.md#staff-data-model) objects | 533 | | trailer | string | URL to the embedded video | 534 | | englishTitle | string | The english title of the anime | 535 | | synonyms | string | A list of synonyms of the anime title (other languages names, related ovas/movies/animes) separated by commas, like "Sakura Trick, Sakura Trap" | 536 | | type | string | The type of the anime, can be either `TV`, `OVA`, `Movie` or `Special` | 537 | | episodes | string | The number of aired episodes | 538 | | status | string | The status of the anime (whether it is airing, finished...) | 539 | | aired | string | The date from which the airing started to the one from which it ended, this property will be empty if one of the two dates is unknown | 540 | | premiered | string | The date of when the anime has been premiered | 541 | | broadcast | string | When the anime is broadcasted | 542 | | volumes | string | The number of volumes of the novel | 543 | | chapters | string | The numbers of chapters of the novel | 544 | | published | string | The dates of publications of the novel | 545 | | authors | string | The authors of the novel | 546 | | serialization | string | The serialization of the novel | 547 | | producers | array | An array of the anime producers | 548 | | studios | array | An array of the anime producers | 549 | | source | string | On what the anime is based on (e.g: based on a manga...) | 550 | | genres | array | An array of the anime genres (Action, Slice of Life...) | 551 | | duration | string | Average duration of an episode (or total duration if movie...) | 552 | | rating | string | The rating of the anime (e.g: R18+..), see the [List of possible ratings](https://github.com/Kylart/MalScraper/blob/master/README.md#list-of-possible-ratings) | 553 | | score | string | The average score | 554 | | scoreStats | string | By how many users this anime has been scored, like "scored by 255,693 users" | 555 | | ranked | string | The rank of the anime | 556 | | popularity | string | The popularity of the anime | 557 | | members | string | How many users are members of the anime (have it on their list) | 558 | | favorites | string | Count of how many users have this anime as favorite | 559 | | id | number | The unique identifier of the anime | 560 | | url | string | the URL to the page | 561 | 562 | #### List of possible ratings 563 | 564 | Anime ratings can be either: 565 | 566 | * `G - All Ages` 567 | * `PG - Children` 568 | * `PG-13 - Teens 13 or older` 569 | * `R - 17+ (violence & profanity)` 570 | * `R+ - Mild Nudity` 571 | * `Rx - Hentai` 572 | 573 | #### Anime search model 574 | | Property | Type | Description | 575 | | --- | --- | --- | 576 | | thumbnail | string | Full url for anime thumbnail | 577 | | url | string | Full url for anime page | 578 | | video | string | full url of anime trailer video if any | 579 | | shortDescription | string | Short description of the anime (or manga) | 580 | | title | string | Anime title | 581 | | type | string | Anime type | 582 | | nbEps | string | Anime number of episodes | 583 | | score | string | Anime score | 584 | | startDate | string | Anime start date | 585 | | endDate | string | Anime end date | 586 | | members | string | Anime number of members | 587 | | rating | string | Anime rating | 588 | 589 | #### Manga search model 590 | | Property | Type | Description | 591 | | --- | --- | --- | 592 | | thumbnail | string | Full url for anime thumbnail | 593 | | url | string | Full url for anime page | 594 | | video | string | full url of anime trailer video if any | 595 | | shortDescription | string | Short description of the anime (or manga) | 596 | | title | string | Anime title | 597 | | type | string | Anime type | 598 | | score | string | Anime score | 599 | | nbChapters | string | Number of chapters released so far | 600 | | vols | string | Number of volumes released so far | 601 | | startDate | string | Anime start date | 602 | | endDate | string | Anime end date | 603 | | members | string | Anime number of members | 604 | 605 | #### Staff data model 606 | 607 | | Property | Type | Description | 608 | | --- | --- | --- | 609 | | link | string | Link to the MAL profile of this person | 610 | | picture | string | Link to a picture of the person at the best possible size | 611 | | name | string | Their name and surname, like `Surname, Name` | 612 | | role | string | The role this person has/had in this anime (Director, Sound Director...) | 613 | 614 | #### Character data model 615 | 616 | | Property | Type | Description | 617 | | --- | --- | --- | 618 | | link | string | Link to the MAL profile of this character | 619 | | picture | string | Link to a picture of the character at the best possible size | 620 | | name | string | Their name and surname, like `Surname, Name` | 621 | | role | string | The role this person has/had in this anime (Main, Supporting...) | 622 | | seiyuu | object | An object containing additional data about who dubbed this character | 623 | | seiyuu.link | string | Link to the MAL profile of who dubbed this character | 624 | | seiyuu.picture | string | Link to a picture of the seiyuu at the best possible size | 625 | | seiyuu.name | string | Their name and surname, like `Surname, Name` | 626 | 627 | ### Search result data model 628 | 629 | | Property | Type | Description | 630 | | --- | --- | --- | 631 | | id | number | The unique identifier of this result | 632 | | type | string | The type of the result (e.g: anime...) | 633 | | name | string | The title of the anime | 634 | | url | string | The URL to the anime | 635 | | image_url | string | URL of the image | 636 | | thumbnail_url | string | URL of the thumbnail image | 637 | | es_score | number | A number representing the accuracy of the result, where 1 is a perfect match and 0 a totally irrelevant one | 638 | | payload | object | An object containing additional data about the anime | 639 | | payload.media_type | string | The type of the anime, can be either `TV`, `Movie`, `OVA` or `Special` | 640 | | payload.start_year | number | The year the airing of the anime started | 641 | | payload.aired | string | The date from which the airing started to the one from which it ended | 642 | | payload.score | string | The average score given to this anime | 643 | | payload.status | string | The current status of the anime (whether it is still airing, finished...) | 644 | 645 | ### Seasonal release data model 646 | 647 | **Note: If nothing is found for the given date, the current year/season releases list will be returned** 648 | 649 | | Property | Type | Description | 650 | | --- | --- | --- | 651 | | TV | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 652 | | TVNew | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 653 | | TVCon | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 654 | | OVAs | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 655 | | ONAs | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 656 | | Movies | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 657 | | Specials | array | An array of [Seasonal anime release data model](https://github.com/Kylart/MalScraper/blob/master/README.md#seasonal-anime-release-data-model) objects | 658 | 659 | #### Seasonal anime release data model 660 | 661 | | Property | Type | Description | 662 | | --- | --- | --- | 663 | | picture | string | Link to the picture of the anime | 664 | | synopsis | string | The synopsis of the anime | 665 | | licensor | string | The licensor | 666 | | title | string | The name of the anime | 667 | | link | string | The direct link to the anime page | 668 | | genres | array | An array of strings which are the genres of this anime | 669 | | producers | array | An array of strings which are the producers of this anime | 670 | | fromType | string | From what this anime is based on/an adaptation of (Light novel, manga...) | 671 | | nbEp | string | The number of aired episodes this anime has | 672 | | releaseDate | string | When this anime has been released | 673 | | score | string | The average score users have given to this anime | 674 | 675 | ### User watch list data model 676 | 677 | #### v2.6.0 678 | An array of [User anime entry data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-anime-entry-data-model) objects or [User manga entry data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-manga-entry-data-model) 679 | 680 | #### v2.5.2 and before 681 | | Property | Type | Description | 682 | | --- | --- | --- | 683 | | stats | object | A [User stats data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-stats-data-model) object | 684 | | lists | array | An array of [User anime entry data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-anime-entry-data-model) objects or [User manga entry data model](https://github.com/Kylart/MalScraper/blob/master/README.md#user-manga-entry-data-model)| 685 | 686 | #### User stats data model 687 | 688 | | Property | Type | Description | 689 | | --- | --- | --- | 690 | | TV | string | Number of TV anime this user watched | 691 | | OVA | string | Number of OVA anime this user watched | 692 | | Movies | string | Number of Movies anime this user watched | 693 | | Spcl | string | Number of special anime this user watched | 694 | | ONA | string | Number of ONA anime this user watched | 695 | | Days | string | Number of days spent in front of anime for this user | 696 | | Eps | string | Number of eps watched by this user | 697 | | MeanScore | string | Mean score given by this user | 698 | | ScoreDev | string | Score deviation for this user | 699 | 700 | #### User anime entry data model 701 | 702 | | Property | Type | Description | 703 | | --- | --- | --- | 704 | | status | integer | Status of the anime in the user's watch list (completed, on-hold...), see the [Statuses references](https://github.com/Kylart/MalScraper/blob/master/README.md#statuses-references) | 705 | | score | integer | Score given by the user | 706 | | tags | string | anime tags for this anime. Tags are separated by a comma | 707 | | isRewatching | integer | Whther this user is rewatching this anime | 708 | | numWatchedEpisodes: | integer | Number of episodes this user watched for this anime | 709 | | animeTitle | string | The title of the anime | 710 | | animeNumEpisodes | integer | How many episodes this anime has | 711 | | animeAiringStatus | string | The status of the anime, see the [Series statuses references](https://github.com/Kylart/MalScraper/blob/master/README.md#series-statuses-references) | 712 | | animeId | string | The unique identifier of this anime | 713 | | animeStudios | string | Studios of this anime | 714 | | animeLicensors | string | Who licensed this anime | 715 | | animeSeason | string | ??? | 716 | | hasEpisodeVideo | boolean | Whether episode information are available on MAL | 717 | | hasPromotionVideo | boolean | Whether anime trailer is available on MAL | 718 | | videoUrl | string | path to video url on MAL | 719 | | animeUrl | string | path to anime url on MAL | 720 | | animeImagePath | string | path to anime thumbnail url on MAL | 721 | | isAddedToList | boolean | ??? | 722 | | animeMediaTypeString | string | Type of this anime | 723 | | animeMpaaRatingString | string | Rating of this anime | 724 | | startDateString | string | When did this user start watching it | 725 | | finishDateString | string | When did this user finish it | 726 | | animeStartDateString | string | Start date of the anime following the format (MM-DD-YYYY) | 727 | | animeEndDateString | string | End date of the anime following the format (MM-DD-YYYY) | 728 | | daysString | string | ??? | 729 | | storageString | string | Storage type for this anime (set by the user) | 730 | | priorityString | string | Priority of this anime for the user | 731 | 732 | #### User manga entry data model 733 | 734 | | Property | Type | Description | 735 | | --- | --- | --- | 736 | | myID | string | Deprecated | 737 | | status | string | Status of the manga in the user's watch list (completed, on-hold...), see the [Statuses references](https://github.com/Kylart/MalScraper/blob/master/README.md#statuses-references) | 738 | | score | string | The score the user has given to this manga | 739 | | tags | string | The tags the user has given to this manga | 740 | | isRereading | string | Whether the user is re-reading this manga or not, where `0` means not | 741 | | nbReadChapters | string | Count of how many chapters of this manga the user has read | 742 | | nbReadVolumes | string | Count of how many volumes of this manga the user has read | 743 | | mangaTitle | string | The title of the manga | 744 | | mangaNumChapters | string | Total count of chapters this manga has | 745 | | mangaNumVolumes | string | Count of volumes this manga has | 746 | | mangaPublishingStatus | string | The status of the manga, see the [Series statuses references](https://github.com/Kylart/MalScraper/blob/master/README.md#series-statuses-references) | 747 | | mangaId | string | The unique identifier of this manga | 748 | | mangaMagazines | string | Magazines where this manga airs | 749 | | mangaUrl | string | Path to manga page | 750 | | mangaImagePath | string | path to manga thumbnail | 751 | | isAddedToList | boolean | ??? | 752 | | mangaMediaTypeString | string | The type of the manga, see the [Types references](https://github.com/Kylart/MalScraper/blob/master/README.md#types-references) | 753 | | startDateString | string | A `mm-dd-yyyy` format date of when the user started watching this manga | 754 | | finishDateString | string | A `mm-dd-yyyy` format date of when the user finished watching this manga | 755 | | mangaStartDateString | string | A `mm-dd-yyyy` format date of when the manga started | 756 | | mangaEndDateString | string | A `mm-dd-yyyy` format date of when the manga ended | 757 | | daysString | string | ??? | 758 | | retailString | string | ??? | 759 | | priorityString | string | Priority of this manga for the user | 760 | 761 | The types, statuses and series statuses aren't explicitly given by MyAnimeList, a number is given instead, here's the corresponding statuses/types according to their numbers 762 | 763 | #### Types references 764 | 765 | * `0`: Unknown 766 | * `1`: TV | Manga 767 | * `2`: OVA | Novel 768 | * `3`: Movie | One-shot 769 | * `4`: Special | Doujinshi 770 | * `5`: ONA | Manhwha 771 | * `6`: Music | Manhua 772 | 773 | #### Statuses references 774 | 775 | * `1`: Watching | Reading 776 | * `2`: Completed 777 | * `3`: On-hold 778 | * `4`: Dropped 779 | * `6`: Plan-to-watch | Plan-to-read 780 | 781 | #### Series statuses references 782 | 783 | * `1`: Currently airing | Publishing 784 | * `2`: Finished airing | Finished 785 | * `3`: Not yet aired | Not yet published 786 | 787 | #### News data model 788 | 789 | | Property | Type | Description | 790 | | --- | --- | --- | 791 | | title | string | The title of the news | 792 | | link | string | The link to the article | 793 | | image | string | URL of the cover image of the article | 794 | | text | string | A short preview of the news description | 795 | | newsNumber | string | The unique identifier of the news | 796 | 797 | #### Anime reviews data model 798 | 799 | | Property | Type | Description | 800 | | --- | --- | --- | 801 | | author | string | The name of the author | 802 | | date | date | The date of the comment | 803 | | seen | string | The number of episode seen | 804 | | overall | number | The overall note of the anime | 805 | | story | number | The story note of the anime | 806 | | animation | number | The animation note of the anime| 807 | | sound | number | The sound note of the anime | 808 | | character | number | The character note of the anime | 809 | | enjoyment | number | The enjoyment note of the anime | 810 | | review | string | The complete review | 811 | 812 | #### Anime recommendations data model 813 | 814 | | Property | Type | Description | 815 | | --- | --- | --- | 816 | | pictureImage | date | The link of the picture's anime recommended | 817 | | animeLink | string | The link of the anime recommended | 818 | | anime | number | The name of the anime recommended | 819 | | mainRecommendation | number | The recommendation | 820 | | author | string | The name of the author | 821 | 822 | #### Anime episodes data model 823 | 824 | | Property | Type | Description | 825 | | --- | --- | --- | 826 | | epNumber | number | The episode number | 827 | | aired | string | A "Jan 10, 2014" date like of when the episode has been aired | 828 | | discussionLink | string | - | 829 | | title | string | The title of the episode | 830 | | japaneseTitle | string | The japanese title of the episode | 831 | 832 | #### Anime search results data model 833 | 834 | | Property | Type | Description | 835 | | --- | --- | --- | 836 | | id | string | The unique identifier of this anime | 837 | | title | string | The title of the anime | 838 | | english | string | The english title of the anime | 839 | | synonyms | string | A set of synonyms of this anime | 840 | | episodes | string | The total count of aired episodes this anime has | 841 | | score | string | The average score given by users to this anime | 842 | | type | string | The type of the anime (TV, OVA...) | 843 | | status | string | The status of the anime (Airing, Finished airing...) | 844 | | start_date | string | A yyyy-mm-dd date format of when the anime started to be aired | 845 | | end_date | string | A yyyy-mm-dd date format of when the anime finished | 846 | | synopsis | string | The synopsis of the anime | 847 | | image | string | URL to the cover image of the anime | 848 | 849 | #### Manga search results data model 850 | 851 | | Property | Type | Description | 852 | | --- | --- | --- | 853 | | id | string | The unique identifier of this manga | 854 | | title | string | The title of the manga | 855 | | english | string | The english title of the manga | 856 | | synonyms | string | A set of synonyms of this manga | 857 | | chapters | string | The total count of chapters this manga has | 858 | | volumes | string | The total count of volumes this manga has | 859 | | score | string | The average score given by users to this manga | 860 | | type | string | The type of the manga (Manga, Doujinshi...) | 861 | | status | string | The status of the manga (Publishing, Finished...) | 862 | | start_date | string | A yyyy-mm-dd date format of when the manga started publication | 863 | | end_date | string | A yyyy-mm-dd date format of when the manga finished | 864 | | synopsis | string | The synopsis of the manga | 865 | | image | string | URL to the cover image of the manga | 866 | 867 | #### Anime stats data model 868 | 869 | | Property | Type | Description | 870 | | --- | --- | --- | 871 | | watching | number | The total number of person who are watching the anime | 872 | | completed | number | The total number of person who completed the anime | 873 | | onHold | number | The total number of person who stop watching the anime but will continue later | 874 | | dropped | number | The total number of person who stop watching the anime | 875 | | planToWatch | number | The total number of person who plan to watch the anime | 876 | | total | number | Total of stats | 877 | | score10 | number | The number of person ranking the anime with a 10/10 | 878 | | score9 | number | The number of person ranking the anime with a 9/10 | 879 | | score8 | number | The number of person ranking the anime with a 8/10 | 880 | | score7 | number | The number of person ranking the anime with a 7/10 | 881 | | score6 | number | The number of person ranking the anime with a 6/10 | 882 | | score5 | number | The number of person ranking the anime with a 5/10 | 883 | | score4 | number | The number of person ranking the anime with a 4/10 | 884 | | score3 | number | The number of person ranking the anime with a 3/10 | 885 | | score2 | number | The number of person ranking the anime with a 2/10 | 886 | | score1 | number | The number of person ranking the anime with a 1/10 | 887 | 888 | #### Anime pictures data model 889 | 890 | | Property | Type | Description | 891 | | --- | --- | --- | 892 | | imageLink | number | The link of the image | 893 | 894 | ## Contributing 895 | 1. Fork it! 896 | 2. Create your feature branch: `git checkout -b my-new-feature` 897 | 3. Commit your changes: `git commit -am 'Add some feature'` 898 | 4. Push to the branch: `git push origin my-new-feature` 899 | 5. Submit a pull request. 900 | 901 | ## License 902 | MIT License 903 | 904 | Copyright (c) Kylart 905 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mal-scraper", 3 | "version": "2.13.3", 4 | "description": "Scrap everything you can from MyAnimeList.net", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "scripts": { 8 | "test": "npm run lint && nyc ava --verbose --timeout=1m --serial test/*.test.js", 9 | "test-no": "nyc ava --verbose --timeout=1m --serial test/*.test.js", 10 | "lint": "standard | snazzy", 11 | "lint:fix": "standard --fix | snazzy", 12 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 13 | "cloc": "cloc $(git ls-files)" 14 | }, 15 | "author": "Kylart", 16 | "license": "MIT", 17 | "repository": "https://github.com/Kylart/MalScraper", 18 | "keywords": [ 19 | "MalScraper", 20 | "MyAnimeList", 21 | "Seasonal Anime", 22 | "Information", 23 | "Scraping", 24 | "Anime", 25 | "news" 26 | ], 27 | "engines": { 28 | "node": ">=8" 29 | }, 30 | "dependencies": { 31 | "axios": "^1.4.0", 32 | "cheerio": "^1.0.0-rc.12", 33 | "match-sorter": "^6.3.1" 34 | }, 35 | "devDependencies": { 36 | "ava": "^3.9.0", 37 | "cloc": "^2.5.0", 38 | "codecov": "^3.6.1", 39 | "nock": "^13.0.4", 40 | "nyc": "^14.1.1", 41 | "pre-commit": "^1.2.2", 42 | "snazzy": "^8.0.0", 43 | "standard": "^14.3.1" 44 | }, 45 | "precommit": "lint", 46 | "nyc": { 47 | "exclude": [ 48 | "test", 49 | "src/search", 50 | "src/officialApi" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/episodes.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const { getResultsFromSearch } = require('./info.js') 4 | 5 | const BASE_URI = 'https://myanimelist.net/anime/' 6 | 7 | const parsePage = ($) => { 8 | const allItems = $('tr.episode-list-data') 9 | const result = [] 10 | 11 | allItems.each(function (elem) { 12 | result.push({ 13 | epNumber: +$(this).find('td.episode-number').text().trim(), 14 | aired: $(this).find('td.episode-aired').text().trim(), 15 | discussionLink: $(this).find('td.episode-forum > a').attr('href'), 16 | title: $(this).find('td.episode-title > a').text().trim(), 17 | japaneseTitle: $(this).find('td.episode-title > span').text().trim() 18 | }) 19 | }) 20 | 21 | return result 22 | } 23 | 24 | const searchPage = (url, offset = 0, res = []) => { 25 | return new Promise((resolve, reject) => { 26 | axios.get(url, { 27 | params: { 28 | offset 29 | } 30 | }) 31 | .then(({ data }) => { 32 | const $ = cheerio.load(data) 33 | 34 | const tmpRes = parsePage($) 35 | res = res.concat(tmpRes) 36 | 37 | if (tmpRes.length) { 38 | searchPage(url, offset + 100, res) 39 | .then((data) => resolve(data)) 40 | .catch(/* istanbul ignore next */(err) => reject(err)) 41 | } else { 42 | resolve(res) 43 | } 44 | }) 45 | .catch(/* istanbul ignore next */(err) => reject(err)) 46 | }) 47 | } 48 | 49 | const getEpisodesFromName = (name) => { 50 | return new Promise((resolve, reject) => { 51 | getResultsFromSearch(name) 52 | .then((items) => { 53 | const { url } = items[0] 54 | 55 | searchPage(`${encodeURI(url)}/episode`) 56 | .then((data) => resolve(data)) 57 | .catch(/* istanbul ignore next */(err) => reject(err)) 58 | }) 59 | .catch(/* istanbul ignore next */(err) => reject(err)) 60 | }) 61 | } 62 | 63 | const getEpisodesFromNameAndId = (id, name) => { 64 | return new Promise((resolve, reject) => { 65 | searchPage(`${BASE_URI}${id}/${encodeURI(name)}/episode`) 66 | .then((data) => resolve(data)) 67 | .catch(/* istanbul ignore next */(err) => reject(err)) 68 | }) 69 | } 70 | 71 | const getEpisodesList = (obj) => { 72 | return new Promise((resolve, reject) => { 73 | if (!obj) { 74 | reject(new Error('[Mal-Scraper]: No id nor name received.')) 75 | return 76 | } 77 | 78 | if (typeof obj === 'object' && !obj[0]) { 79 | const { id, name } = obj 80 | 81 | if (!id || !name || isNaN(+id) || typeof name !== 'string') { 82 | reject(new Error('[Mal-Scraper]: Malformed input. ID or name is malformed or missing.')) 83 | return 84 | } 85 | 86 | getEpisodesFromNameAndId(id, name) 87 | .then((data) => resolve(data)) 88 | .catch(/* istanbul ignore next */(err) => reject(err)) 89 | } else { 90 | getEpisodesFromName(obj) 91 | .then((data) => resolve(data)) 92 | .catch(/* istanbul ignore next */(err) => reject(err)) 93 | } 94 | }) 95 | } 96 | 97 | module.exports = { 98 | getEpisodesList 99 | } 100 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mal-scraper' { 2 | //=/ ----- VARIABLES ----- /=// 3 | 4 | let search: Search; 5 | 6 | //=/ ----- FUNCTIONS ----- /=// 7 | 8 | /** 9 | * Get infos about an anime from the given name. 10 | * @param name The name of the anime to search, the best match corresponding to that name will be returned. 11 | * @param getBestMatch Whether you want to use [`match-sorter`](https://github.com/kentcdodds/match-sorter) to find the best result or not. (Default to `true`) 12 | * @returns A promise that resolves to an object containing the infos about the anime. 13 | */ 14 | export function getInfoFromName( 15 | name: string, 16 | getBestMatch?: B, 17 | type?: T, 18 | ): Promise< 19 | T extends 'anime' 20 | ? AnimeDataModel 21 | : T extends 'manga' 22 | ? MangaDataModel 23 | : never 24 | >; 25 | 26 | /** 27 | * Get infos about an anime from the given URL. 28 | * @param url The URL of the anime to search. 29 | * @returns Same as {@link getInfoFromName `getInfoFromName()`}. 30 | */ 31 | export function getInfoFromURL(url: string): Promise; 32 | 33 | /** 34 | * Return an array of a maximum length of 10 containing {@link SearchResultsDataModel Search result data model} objects. 35 | * @param query The query to search. 36 | */ 37 | export function getResultsFromSearch( 38 | query: string, 39 | type?: T 40 | ): Promise; 41 | 42 | /** 43 | * Get the list of animes, OVAs, movies and ONAs released (or planned to be released) during the season of the given year. 44 | * @param year The year of the anime to search. 45 | * @param season The season of the anime to search. 46 | * @param type The type of the anime to search. 47 | */ 48 | export function getSeason( 49 | year: number, 50 | season: Seasons, 51 | type?: Types 52 | ): Promise; 53 | 54 | 55 | /** 56 | * Get the watchlist of the given user. 57 | * @param username The username of the user to search. 58 | * @param after Useful to paginate. Is the number of results you want to start from. By default, MAL returns 300 entries only. 59 | * @param type Optional, can be either `anime` or `manga`. 60 | * @param status Optional, Status in the user's watch list (completed, on-hold...) 61 | * @note From v2.11.6. 62 | */ 63 | export function getWatchListFromUser( 64 | username: string, 65 | after?: number, 66 | type?: T, 67 | status?: number 68 | ): Promise< 69 | T extends 'anime' 70 | ? UserAnimeEntryDataModel[] 71 | : T extends 'manga' 72 | ? UserMangaEntryDataModel[] 73 | : never 74 | >; 75 | 76 | /** 77 | * Get the watchlist of the given user. 78 | * @param username The username of the user to search. 79 | * @param after Useful to paginate. Is the number of results you want to start from. By default, MAL returns 300 entries only. 80 | * @param type Optional, can be either `anime` or `manga`. 81 | * @note From v2.6.0. 82 | */ 83 | export function getWatchListFromUser( 84 | username: string, 85 | after?: number, 86 | type?: T 87 | ): Promise< 88 | T extends 'anime' 89 | ? UserAnimeEntryDataModel[] 90 | : T extends 'manga' 91 | ? UserMangaEntryDataModel[] 92 | : never 93 | >; 94 | 95 | /** 96 | * Get the watchlist of the given user. 97 | * @param username The username of the user to search. 98 | * @param type Optional, can be either `anime` or `manga`. 99 | * @note From v2.5.2 and before. 100 | */ 101 | export function getWatchListFromUser( 102 | username: string, 103 | type?: T 104 | ): Promise< 105 | T extends 'anime' 106 | ? UserAnimeEntryDataModel[] 107 | : T extends 'manga' 108 | ? UserMangaEntryDataModel[] 109 | : never 110 | >; 111 | 112 | /** 113 | * Get news from MyAnimeList. 114 | * @param nbNews The count of news you want to get, default is 160. Note that there is a 20 news per page, so if you set if to 60 for example, it will result in 3 requests. 115 | * You should be aware of that, as MyAnimeList will most likely rate-limit you if more than 35-40~ requests are done in a few seconds. 116 | */ 117 | export function getNewsNoDetails(nbNews?: number): Promise; 118 | 119 | /** 120 | * Get an episode list 121 | * @param anime If an object is passed, it must have the `name`and `id` property. If you only have the name and not the id, you may call the method with the name as string, 122 | * this will be slower but the id will be automatically fetched on the first way. 123 | */ 124 | export function getEpisodesList( 125 | anime: AnimeOptions | string 126 | ): Promise; 127 | 128 | /** 129 | * Returns an array of reviews for the given anime. 130 | * @param anime An object that must have the `name` and `id` property or just the `name` alone. If you only have the name and not the id, 131 | * you may call the method with the name as string, this will be slower but the id will be automatically fetched on the first way. 132 | */ 133 | export function getReviewsList( 134 | anime: ReviewsListAnimeOptions 135 | ): Promise; 136 | 137 | /** 138 | * Get a list of the recommendations for the given anime. 139 | * @param anime If an object is passed, it must have the `name`and `id` property. If you only have the name and not the id, 140 | * you may call the method with the name as string, this will be slower but the id will be automatically fetched on the first way. 141 | */ 142 | export function getRecommendationsList( 143 | anime: AnimeOptions | string 144 | ): Promise; 145 | 146 | /** 147 | * Get the stats of the given anime. 148 | * @param anime If an object is passed, it must have the `name`and `id` property. If you only have the name and not the id, 149 | * you may call the method with the name as string, this will be slower but the id will be automatically fetched on the first way. 150 | */ 151 | export function getStats( 152 | anime: AnimeOptions | string 153 | ): Promise; 154 | 155 | /** 156 | * Get the pictures of the given anime. 157 | * @param anime If an object is passed, it must have the `name`and `id` property. If you only have the name and not the id, 158 | */ 159 | export function getPictures( 160 | anime: AnimeOptions | string 161 | ): Promise; 162 | 163 | //=/ ----- CLASSES ----- /=// 164 | // class officialApi was removed. 165 | 166 | //=/ ---- TYPES ---- /=// 167 | 168 | type AllowedTypes = 'anime' | 'manga'; 169 | 170 | type Search = { 171 | /** 172 | * Search an anime/manga from the given informations 173 | * @param type Type of search (manga or anime) 174 | * @param options Options for the search, all keys are optional 175 | */ 176 | search( 177 | type: A, 178 | options?: SearchOptions 179 | ): Promise< 180 | A extends 'anime' 181 | ? AnimeSearchModel[] 182 | : A extends 'manga' 183 | ? MangaSearchModel[] 184 | : never 185 | >; 186 | 187 | /** 188 | * Helpers for types, genres and list you might need for your research 189 | */ 190 | helpers: Helpers; 191 | }; 192 | 193 | type GenreValues = 194 | | '1' 195 | | '2' 196 | | '3' 197 | | '4' 198 | | '5' 199 | | '6' 200 | | '7' 201 | | '8' 202 | | '9' 203 | | '10' 204 | | '11' 205 | | '12' 206 | | '13' 207 | | '14' 208 | | '15' 209 | | '16' 210 | | '17' 211 | | '18' 212 | | '19' 213 | | '20' 214 | | '21' 215 | | '22' 216 | | '23' 217 | | '24' 218 | | '25' 219 | | '26' 220 | | '27' 221 | | '28' 222 | | '29' 223 | | '30' 224 | | '31' 225 | | '32' 226 | | '33' 227 | | '34' 228 | | '35' 229 | | '36' 230 | | '37' 231 | | '38' 232 | | '39' 233 | | '40' 234 | | '41' 235 | | '42' 236 | | '43'; 237 | 238 | type GenreName = 239 | | 'Action' 240 | | 'Adventure' 241 | | 'Cars' 242 | | 'Comedy' 243 | | 'Dementia' 244 | | 'Demons' 245 | | 'Mystery' 246 | | 'Drama' 247 | | 'Ecchi' 248 | | 'Fantasy' 249 | | 'Game' 250 | | 'Hentai' 251 | | 'Historical' 252 | | 'Horror' 253 | | 'Kids' 254 | | 'Magic' 255 | | 'Martial Arts' 256 | | 'Mecha' 257 | | 'Music' 258 | | 'Parody' 259 | | 'Samurai' 260 | | 'Romance' 261 | | 'School' 262 | | 'Sci-Fi' 263 | | 'Shoujo' 264 | | 'Shoujo Ai' 265 | | 'Shounen' 266 | | 'Shounen Ai' 267 | | 'Space' 268 | | 'Sports' 269 | | 'Super Power' 270 | | 'Vampire' 271 | | 'Yaoi' 272 | | 'Yuri' 273 | | 'Harem' 274 | | 'Slice of Life' 275 | | 'Supernatural' 276 | | 'Military' 277 | | 'Police' 278 | | 'Psychological' 279 | | 'Thriller' 280 | | 'Seinen' 281 | | 'Josei'; 282 | 283 | type RatingValue = 0 | 1 | 2 | 3 | 4 | 5 | 6; 284 | 285 | type RatingName = 'none' | 'G' | 'PG' | 'PG-13' | 'R' | 'R+' | 'Rx'; 286 | 287 | type TypeValue = 0 | 1 | 2 | 3 | 4 | 5 | 6; 288 | 289 | type TypeName = 'none' | 'tv' | 'movie' | 'special' | 'ova' | 'ona' | 'music'; 290 | 291 | type StatusValue = 0 | 1 | 2 | 3; 292 | 293 | type StatusName = 'none' | 'finished' | 'currently' | 'not-aired'; 294 | 295 | type Score = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 296 | 297 | type OrderTypes = [ 298 | 'startDate', 299 | 'score', 300 | 'eps', 301 | 'endDate', 302 | 'type', 303 | 'members', 304 | 'rated' 305 | ]; 306 | 307 | type Types = 308 | | 'TV' 309 | | 'TVNew' 310 | | 'TVCon' 311 | | 'Movies' 312 | | 'OVAs' 313 | | 'ONAs' 314 | | 'Specials'; 315 | 316 | type Seasons = 'spring' | 'summer' | 'fall' | 'winter'; 317 | 318 | type FullRatings = 319 | | 'G - All ages' 320 | | 'PG - Children' 321 | | 'PG-13 - Teens 13 or older' 322 | | 'R - 17+' 323 | | 'R+ - Mild Nudity' 324 | | 'Rx - Hentai'; 325 | 326 | /** 327 | * `0` - Unknown 328 | * `1` - TV | Manga 329 | * `2` - OVA | Novel 330 | * `3` - Movie | One-Shot 331 | * `4` - Special | Doujinshi 332 | * `5` - ONA | Manhwha 333 | * `6` - Music | Manhua 334 | */ 335 | type TypesReferences = 0 | 1 | 2 | 3 | 4 | 5 | 6; 336 | 337 | /** 338 | * `1` - Watching | Reading\ 339 | * `2` - Completed\ 340 | * `3` - On-Hold\ 341 | * `4` - Dropped\ 342 | * `6` - Plan-to-Watch | Plan-to-Read 343 | */ 344 | type StatusReference = 1 | 2 | 3 | 4 | 6; 345 | 346 | /** 347 | * `1` - Watching | Reading\ 348 | * `2` - Completed\ 349 | * `3` - On-Hold\ 350 | * `4` - Dropped\ 351 | * `6` - Plan-to-Watch | Plan-to-Read\ 352 | * `7` - All status above 353 | */ 354 | type SearchStatusReference = StatusReference | 7 355 | 356 | /** 357 | * `1` - Currently airing | Publishing\ 358 | * `2` - Finished airing | Finished\ 359 | * `3` - Not yet aired | Not yet published 360 | */ 361 | type SeriesStatusReference = 1 | 2 | 3; 362 | 363 | //=/ ---- INTERFACES ---- /=// 364 | 365 | /** 366 | * All of this properties are optional 367 | */ 368 | interface SearchOptions { 369 | maxResults?: number; 370 | has?: number; 371 | term: string; 372 | type?: number; 373 | status?: number; 374 | producer?: number; 375 | rating?: number; 376 | startDate?: { 377 | day?: number; 378 | month?: number; 379 | year?: number; 380 | }; 381 | endDate?: { 382 | day?: number; 383 | month?: number; 384 | year?: number; 385 | }; 386 | genreType?: number; 387 | genre?: [number]; 388 | } 389 | 390 | interface Helpers { 391 | availableValues: { 392 | genre: { 393 | anime: Genres[]; 394 | manga: Genres[]; 395 | }; 396 | p: { 397 | anime: { 398 | name: string; 399 | value: string; 400 | }[]; 401 | manga: { 402 | name: string; 403 | value: string; 404 | }[]; 405 | }; 406 | 407 | r: Rating[]; 408 | 409 | score: Score[]; 410 | 411 | status: Status[]; 412 | 413 | type: Type[]; 414 | }; 415 | 416 | genresList: { 417 | anime: Genres[]; 418 | manga: Genres[]; 419 | }; 420 | 421 | orderTypes: OrderTypes; 422 | 423 | producersList: { 424 | anime: { 425 | name: string; 426 | value: string; 427 | }[]; 428 | manga: { 429 | name: string; 430 | value: string; 431 | }[]; 432 | }; 433 | } 434 | 435 | interface AnimeOptions { 436 | /** 437 | * The name of the anime 438 | */ 439 | name: string; 440 | 441 | /** 442 | * The unique id of the anime 443 | */ 444 | id: number; 445 | } 446 | 447 | interface SearchResultsDataModel { 448 | /** 449 | * The unique identifier of the anime/manga 450 | */ 451 | id: string; 452 | 453 | /** 454 | * The type of the anime/manga (e.g: anime, manga, special, ova, ona, music..) 455 | */ 456 | type: string; 457 | 458 | /** 459 | * The title of the anime/manga 460 | */ 461 | name: string; 462 | 463 | /** 464 | * The image URL of the anime/manga 465 | */ 466 | image_url?: string; 467 | 468 | /** 469 | * The thumbnail URL of the anime/manga 470 | */ 471 | thumbnail_url?: string; 472 | 473 | /** 474 | * A number representing the accuracy of the result, 475 | * where 1 is a perfect match and 0 a totally irrelevent result 476 | */ 477 | es_score?: number; 478 | 479 | /** 480 | * An object containing additional data about this anime 481 | */ 482 | payload?: Payload; 483 | } 484 | 485 | /** 486 | * Additionnal data for {@link SearchResultsDataModel} 487 | */ 488 | interface Payload { 489 | /** 490 | * The type of the anime, can be either `TV`, `Movie`, `OVA` or `Special` 491 | */ 492 | media_type?: string; 493 | 494 | /** 495 | * The year the airing of the anime started 496 | */ 497 | start_year?: number; 498 | 499 | /** 500 | * The date from wich the airing started to the one from wich ended 501 | */ 502 | aired?: string; 503 | 504 | /** 505 | * The average score given to this anime 506 | */ 507 | score?: string; 508 | 509 | /** 510 | * The current status of the anime (e.g: currently airing, finished airing, not yet aired) 511 | */ 512 | status?: StatusName; 513 | } 514 | 515 | interface Genres { 516 | value: GenreValues; 517 | name: GenreName; 518 | } 519 | 520 | interface Rating { 521 | value: RatingValue; 522 | name: RatingName; 523 | } 524 | 525 | interface Type { 526 | value: TypeValue; 527 | name: TypeName; 528 | } 529 | 530 | interface Status { 531 | value: StatusValue; 532 | name: StatusName; 533 | } 534 | 535 | interface AnimeSearchModel { 536 | /** 537 | * Full url for anime thumbnail. 538 | */ 539 | thumbnail: string; 540 | 541 | /** 542 | * Full url of the anime page. 543 | */ 544 | url: string; 545 | 546 | /** 547 | * Full url of anime trailer, if any. 548 | */ 549 | video?: string; 550 | 551 | /** 552 | * Short description of the anime. 553 | */ 554 | shortDescription: string; 555 | 556 | /** 557 | * The anime's title. 558 | */ 559 | title: string; 560 | 561 | /** 562 | * The anime's type. 563 | */ 564 | type: string; 565 | 566 | /** 567 | * The number of episodes. 568 | */ 569 | nbEps: string; 570 | 571 | /** 572 | * The anime score. 573 | */ 574 | score: string; 575 | 576 | /** 577 | * The anime start date. 578 | */ 579 | startDate: string; 580 | 581 | /** 582 | * The anime end date. 583 | */ 584 | endDate: string; 585 | 586 | /** 587 | * The anime number of members. 588 | */ 589 | members: string; 590 | 591 | /** 592 | * The anime rating. 593 | */ 594 | rating: string; 595 | } 596 | 597 | interface MangaSearchModel { 598 | /** 599 | * Full url for manga thumbnail. 600 | */ 601 | thumbnail: string; 602 | 603 | /** 604 | * Full url of the manga page. 605 | */ 606 | url: string; 607 | 608 | /** 609 | * Full url of manga trailer, if any. 610 | */ 611 | video?: string; 612 | 613 | /** 614 | * Short description of the manga. 615 | */ 616 | shortDescription: string; 617 | /** 618 | * The manga's title. 619 | */ 620 | title: string; 621 | 622 | /** 623 | * The manga's type. 624 | */ 625 | type: TypeName; 626 | 627 | /** 628 | * The number of chapters released so far. 629 | */ 630 | nbChapters: number; 631 | 632 | /** 633 | * The manga score. 634 | */ 635 | score: Score; 636 | 637 | /** 638 | * The manga start date. 639 | */ 640 | startDate: string; 641 | 642 | /** 643 | * The manga end date. 644 | */ 645 | endDate: string; 646 | 647 | /** 648 | * The manga number of members. 649 | */ 650 | members: string; 651 | 652 | /** 653 | * The number of volumes released so far. 654 | */ 655 | vols: string; 656 | } 657 | 658 | interface AnimeDataModel { 659 | /** 660 | * The title of the anime. 661 | */ 662 | title: string; 663 | 664 | /** 665 | * The synopsis of the anime. 666 | */ 667 | synopsis?: string; 668 | 669 | /** 670 | * The URL to the cover picture of the anime. 671 | */ 672 | picture?: string; 673 | 674 | /** 675 | * An array of {@link CharacterDataModel Character data model} objects. 676 | */ 677 | characters?: CharacterDataModel[]; 678 | 679 | /** 680 | * An array of {@link StaffDataModel Staff data model} objects. 681 | */ 682 | staff?: StaffDataModel[]; 683 | 684 | /** 685 | * A trailer to the embedded video of the anime. 686 | */ 687 | trailer?: string; 688 | 689 | /** 690 | * The english title of the anime. 691 | */ 692 | englishTitle?: string; 693 | 694 | /** 695 | * An array of synonyms of the anime title. (other languages names, related ovas/movies/animes) (e.g: One Piece -> OP) 696 | */ 697 | synonyms: string[]; 698 | 699 | /** 700 | * The type of the anime (e.g: `TV`, `Movie`, `OVA` or `Special`) 701 | */ 702 | type?: 'TV' | 'Movie' | 'OVA' | 'Special'; 703 | 704 | /** 705 | * The number of aired episodes. 706 | */ 707 | episodes?: string; 708 | 709 | /** 710 | * The status of the anime (e.g: `Finished Airing`, `Currently Airing`, `Not yet aired`) 711 | */ 712 | status?: StatusName; 713 | 714 | /** 715 | * The date from which the airing started to the one from which it ended, 716 | * this property will be empty if one of the two dates is unknown 717 | */ 718 | aired?: string; 719 | 720 | /** 721 | * The date when the anime has been premiered. 722 | */ 723 | premiered?: string; 724 | 725 | /** 726 | * When the anime is broadcasted. 727 | */ 728 | broadcast?: string; 729 | 730 | /** 731 | * The number of volumes of the novel 732 | */ 733 | volumes?: string; 734 | 735 | /** 736 | * The number of chapters of the novel 737 | */ 738 | chapters?: string; 739 | 740 | /** 741 | * The dates of publication of the novel 742 | */ 743 | published?: string; 744 | 745 | /** 746 | * The authors of the novel 747 | */ 748 | authors?: string; 749 | 750 | /** 751 | * The serialization of the novel 752 | */ 753 | serialization?: string; 754 | 755 | /** 756 | * An array of producer(s) of the anime 757 | */ 758 | producers?: string[]; 759 | 760 | /** 761 | * An array of the studio(s) of the anime 762 | */ 763 | studios?: string[]; 764 | 765 | /** 766 | * On what the anime is based on (e.g: based on a manga, Light Novel, etc.) 767 | */ 768 | source?: string; 769 | 770 | /** 771 | * An array of genres of the anime (Action, Slice of Life, Drama, etc.) 772 | */ 773 | genres?: GenreName[]; 774 | 775 | /** 776 | * Average duration of an episode of the anime (or total duration if it's a movie) 777 | */ 778 | duration?: string; 779 | 780 | /** 781 | * The rating of the anime (e.g: `Rx`, `R`, `R+`, `PG-13`, `PG`, `G`, `PG-13+`, `Rx+`) 782 | */ 783 | rating?: FullRatings; 784 | 785 | /** 786 | * The average score of the anime 787 | */ 788 | score?: string; 789 | 790 | /** 791 | * By how many users this anime has been rated, like `"scored by 255,693 users"` 792 | */ 793 | scoreStats?: string; 794 | 795 | /** 796 | * The rank of the anime 797 | */ 798 | ranked?: string; 799 | 800 | /** 801 | * The popularity of the anime 802 | */ 803 | popularity?: string; 804 | 805 | /** 806 | * How many users are members of the anime (have it on their list) 807 | */ 808 | members?: string; 809 | 810 | /** 811 | * The count of how many users marked this anime as favorite 812 | */ 813 | favorites?: string; 814 | 815 | /** 816 | * The unique identifier of the anime 817 | */ 818 | id: number; 819 | 820 | /** 821 | * The URL of the anime page 822 | */ 823 | url: string; 824 | } 825 | 826 | interface MangaDataModel { 827 | /** 828 | * The title of the manga 829 | */ 830 | title: string; 831 | 832 | /** 833 | * The synopsis of the manga 834 | */ 835 | synopsis?: string; 836 | 837 | /** 838 | * An URL to the manga's cover image 839 | */ 840 | picture?: string; 841 | 842 | /** 843 | * An array of {@link CharacterDataModel Character data model} objects. 844 | */ 845 | characters?: CharacterDataModel[]; 846 | 847 | /** 848 | * The english title of the manga 849 | */ 850 | englishTitle?: string; 851 | 852 | /** 853 | * A set of synonyms for the manga 854 | */ 855 | synonyms?: string[]; 856 | 857 | /** 858 | * The type of the manga (Manga, Doujinshi...) 859 | */ 860 | type: string; 861 | 862 | /** 863 | * The status of the manga (Publishing, Finished...) 864 | */ 865 | status?: string; 866 | 867 | /** 868 | * A `YYYY-MM-DD` date format of when the manga started publishing 869 | */ 870 | start_date?: string; 871 | 872 | /** 873 | * A `YYYY-MM-DD` date format of when the manga finished publishing 874 | */ 875 | end_date?: string; 876 | 877 | /** 878 | * Total count of volumes this manga has 879 | */ 880 | volumes?: string; 881 | 882 | /** 883 | * Total count of chapters this manga has 884 | */ 885 | chapters?: string; 886 | 887 | /** 888 | * The date from which the publishing started to the one from which it ended, 889 | * this property will be empty if one of the two dates is unknown 890 | */ 891 | published?: string; 892 | 893 | /** 894 | * The authors of the novel 895 | */ 896 | authors?: string; 897 | 898 | /** 899 | * The serialization of the novel 900 | */ 901 | serialization?: string; 902 | 903 | /** 904 | * An array of genres of the manga (Action, Slice of Life, Drama, etc.) 905 | */ 906 | genres?: GenreName[]; 907 | 908 | /** 909 | * The average score given by users to this manga 910 | */ 911 | score?: string; 912 | 913 | /** 914 | * By how many users this manga has been rated, like `"scored by 255,693 users"` 915 | */ 916 | scoreStats?: string; 917 | 918 | /** 919 | * The rank of the manga 920 | */ 921 | ranked?: string; 922 | 923 | /** 924 | * The popularity of the manga 925 | */ 926 | popularity?: string; 927 | 928 | /** 929 | * How many users are members of the manga (have it on their list) 930 | */ 931 | members?: string; 932 | 933 | /** 934 | * The count of how many users marked this manga as favorite 935 | */ 936 | favorites?: string; 937 | 938 | /** 939 | * The unique identifier of this manga 940 | */ 941 | id: string; 942 | 943 | /** 944 | * The URL of the manga page 945 | */ 946 | url: string; 947 | } 948 | 949 | interface CharacterDataModel { 950 | /** 951 | * Link to the character's page on MyAnimeList 952 | */ 953 | link: string; 954 | 955 | /** 956 | * Link to a picture of the character at the best possible resolution 957 | */ 958 | picture: string; 959 | 960 | /** 961 | * Their name and surname, like `"Kazuma Takahashi"` 962 | */ 963 | name: string; 964 | 965 | /** 966 | * The role of this person has/had in this anime (Main, Supporting, etc...) 967 | */ 968 | role: string; 969 | 970 | /** 971 | * An object containing additional data about who dubbed this character 972 | */ 973 | seiyuu: SeiyuuDataModel; 974 | } 975 | 976 | /** 977 | * An object containing additional data about who dubbed this character 978 | */ 979 | interface SeiyuuDataModel { 980 | /** 981 | * Link to the MyAnimeList profile of who dubbed this character 982 | */ 983 | link?: string; 984 | 985 | /** 986 | * Link to a picture of the seiyuu at the best possible resolution 987 | */ 988 | picture?: string; 989 | 990 | /** 991 | * Their name and surname, like `"John Doe"` 992 | */ 993 | name?: string; 994 | } 995 | 996 | /** 997 | * An object containing additional data about the staff of this anime 998 | */ 999 | interface StaffDataModel { 1000 | /** 1001 | * Link to the MAL profile of this person 1002 | */ 1003 | link?: string; 1004 | 1005 | /** 1006 | * A link to a picture of this person at the best possible resolution 1007 | */ 1008 | picture?: string; 1009 | 1010 | /** 1011 | * Their name and surname, like `"John Doe"` 1012 | */ 1013 | name?: string; 1014 | 1015 | /** 1016 | * The role of this person has/had in this anime (Director, Sound Director, etc...) 1017 | */ 1018 | role?: string; 1019 | } 1020 | 1021 | /** 1022 | * An object containing the season data model 1023 | */ 1024 | interface SeasonDataModel { 1025 | /** 1026 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1027 | */ 1028 | TV?: SeasonalDataModel[]; 1029 | 1030 | /** 1031 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1032 | */ 1033 | TVNew?: SeasonalDataModel[]; 1034 | 1035 | /** 1036 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1037 | */ 1038 | TVCon?: SeasonalDataModel[]; 1039 | 1040 | /** 1041 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1042 | */ 1043 | OVAs?: SeasonalDataModel[]; 1044 | 1045 | /** 1046 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1047 | */ 1048 | ONAs?: SeasonalDataModel[]; 1049 | 1050 | /** 1051 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1052 | */ 1053 | Movies?: SeasonalDataModel[]; 1054 | 1055 | /** 1056 | * An array of {@link SeasonDataModel Seasonal anime release data model} objects 1057 | */ 1058 | Specials?: SeasonalDataModel[]; 1059 | } 1060 | 1061 | interface SeasonalDataModel { 1062 | /** 1063 | * Link to the picture of the anime 1064 | */ 1065 | picture?: string; 1066 | 1067 | /** 1068 | * The synopsis of the anime 1069 | */ 1070 | synopsis?: string; 1071 | 1072 | /** 1073 | * The licensor of the anime 1074 | */ 1075 | licensor?: string; 1076 | 1077 | /** 1078 | * The name of the anime 1079 | */ 1080 | title: string; 1081 | 1082 | /** 1083 | * The direct link to the anime page on MyAnimeList 1084 | */ 1085 | link?: string; 1086 | 1087 | /** 1088 | * An array of strings containing the names of the genres of the anime 1089 | */ 1090 | genres?: GenreName[]; 1091 | 1092 | /** 1093 | * An array of strings containing the producers of the anime 1094 | */ 1095 | producers?: string[]; 1096 | 1097 | /** 1098 | * From what this anime is based on/an adaptation of (Light Novel, Manga, etc...) 1099 | */ 1100 | fromType?: string; 1101 | 1102 | /** 1103 | * The number of the aired episodes this anime has 1104 | */ 1105 | nbEps?: string; 1106 | 1107 | /** 1108 | * When this anime has been released 1109 | */ 1110 | releaseDate?: string; 1111 | 1112 | /** 1113 | * The average users have given to this anime 1114 | */ 1115 | score?: string; 1116 | } 1117 | 1118 | interface UserAnimeEntryDataModel { 1119 | /** 1120 | * Status of the anime in the user's watch list (completed, on-hold...), see the {@link StatusReference Status Reference} for more information 1121 | */ 1122 | status?: StatusReference; 1123 | 1124 | /** 1125 | * Score given to the anime by the user 1126 | */ 1127 | score?: number; 1128 | 1129 | /** 1130 | * Anime tags for this anime. Tags are separated by a comma. 1131 | */ 1132 | tags?: string; 1133 | 1134 | /** 1135 | * Whether this user is rewatching this anime 1136 | */ 1137 | isRewatching?: number; 1138 | 1139 | /** 1140 | * Number of episodes this user has watched 1141 | */ 1142 | numWatchedEpisodes?: number; 1143 | 1144 | /** 1145 | * The title of anime 1146 | */ 1147 | animeTitle: string; 1148 | 1149 | /** 1150 | * How many episodes this anime has 1151 | */ 1152 | animeNumEpisodes?: number; 1153 | 1154 | /** 1155 | * The status of the anime, see {@link SeriesStatusReference Series statuses references} for more information 1156 | */ 1157 | animeAiringStatus?: string; 1158 | 1159 | /** 1160 | * The unique identifier of this anime 1161 | */ 1162 | animeId: string; 1163 | 1164 | /** 1165 | * The studios of this anime 1166 | */ 1167 | animeStudios?: string; 1168 | 1169 | /** 1170 | * Who licensed this anime 1171 | */ 1172 | animeLicensors?: string; 1173 | 1174 | /** 1175 | * ??? 1176 | */ 1177 | animeSeason?: string; 1178 | 1179 | /** 1180 | * Whether episode information are available on MyAnimeList 1181 | */ 1182 | hasEpisodeVideo?: boolean; 1183 | 1184 | /** 1185 | * Whether anime trailer is available on MAL 1186 | */ 1187 | hasPromotionVideo?: boolean; 1188 | 1189 | /** 1190 | * The path to the video url on MAL 1191 | */ 1192 | videoURL?: string; 1193 | 1194 | /** 1195 | * The path to the anime URL on MAL 1196 | */ 1197 | animeURL?: string; 1198 | 1199 | /** 1200 | * The path to the anime poster on MAL 1201 | */ 1202 | animeImagePath?: string; 1203 | 1204 | /** 1205 | * ??? 1206 | */ 1207 | isAddedToList?: boolean; 1208 | 1209 | /** 1210 | * Type of this anime 1211 | */ 1212 | animeMediaTypeString?: string; 1213 | 1214 | /** 1215 | * The rating of this anime 1216 | */ 1217 | animeMpaaRatingString?: string; 1218 | 1219 | /** 1220 | * When did this user started watching this anime 1221 | */ 1222 | startDateString?: string; 1223 | 1224 | /** 1225 | * When did this user finished watching this anime 1226 | */ 1227 | finishDateString?: string; 1228 | 1229 | /** 1230 | * The start date of the anime following the format `MM-DD-YYYY` 1231 | */ 1232 | animeStartDateString?: string; 1233 | 1234 | /** 1235 | * The end date of the anime following the format `MM-DD-YYYY` 1236 | */ 1237 | animeEndDateString?: string; 1238 | 1239 | /** 1240 | * ??? 1241 | */ 1242 | daysString?: string; 1243 | 1244 | /** 1245 | * The storage type of this anime (setted by the user) 1246 | */ 1247 | storageString?: string; 1248 | 1249 | /** 1250 | * The priorityof this anime for the user 1251 | */ 1252 | priorityString?: string; 1253 | } 1254 | 1255 | interface UserMangaEntryDataModel { 1256 | /** 1257 | * @deprecated 1258 | */ 1259 | myID: string; 1260 | 1261 | /** 1262 | * Status of the manga in the user's manga list (completed, on-hold...), see the {@link StatusReference Status Reference} for more informations 1263 | */ 1264 | status?: StatusReference; 1265 | 1266 | /** 1267 | * The score the user gave to the manga 1268 | */ 1269 | score?: number; 1270 | 1271 | /** 1272 | * The tags the user gave to the manga 1273 | */ 1274 | tags?: string; 1275 | 1276 | /** 1277 | * Whether the user is re-reading the manga, where `0` means not re-reading and `1` means re-reading 1278 | */ 1279 | isRereading?: number; 1280 | 1281 | /** 1282 | * The number of chapters the user has read 1283 | */ 1284 | nbReadChapters?: number; 1285 | 1286 | /** 1287 | * The number of volumes the user has read 1288 | */ 1289 | nbReadVolumes?: number; 1290 | 1291 | /** 1292 | * The title of the manga 1293 | */ 1294 | mangaTitle: string; 1295 | 1296 | /** 1297 | * Total count of volumes this manga has 1298 | */ 1299 | mangaNumChapters?: number; 1300 | 1301 | /** 1302 | * Count of volumes this manga has 1303 | */ 1304 | mangaNumVolumes?: number; 1305 | 1306 | /** 1307 | * The status of the manga, see {@link SeriesStatusReference Series statuses references} for more information 1308 | */ 1309 | mangaPublishingStatus?: SeriesStatusReference; 1310 | 1311 | /** 1312 | * The unique identifier of this manga 1313 | */ 1314 | mangaId: number; 1315 | 1316 | /** 1317 | * Magazines where this manga airs 1318 | */ 1319 | mangaMagazines?: string; 1320 | 1321 | /** 1322 | * The path to the manga page on MAL 1323 | */ 1324 | mangaURL?: string; 1325 | 1326 | /** 1327 | * The url of the manga poster on MAL 1328 | */ 1329 | mangaImagePath?: string; 1330 | 1331 | /** 1332 | * ??? 1333 | */ 1334 | isAddedToList?: boolean; 1335 | 1336 | /** 1337 | * The type of the manga, see {@link TypesReferences Types References} for more information 1338 | */ 1339 | mangaMediaTypeString?: string; 1340 | 1341 | /** 1342 | * A `MM-DD-YYYY` format date of when the user started reading this manga 1343 | */ 1344 | startDateString?: string; 1345 | 1346 | /** 1347 | * A `MM-DD-YYYY` format date of when the user finished reading this manga 1348 | */ 1349 | finishDateString?: string; 1350 | 1351 | /** 1352 | * A `MM-DD-YYYY` format date of when the manga started publishing 1353 | */ 1354 | mangaStartDateString?: string; 1355 | 1356 | /** 1357 | * A `MM-DD-YYYY` format date of when the manga finished publishing 1358 | */ 1359 | mangaEndDateString?: string; 1360 | 1361 | /** 1362 | * ??? 1363 | */ 1364 | daysString?: string; 1365 | 1366 | /** 1367 | * ??? 1368 | */ 1369 | retailString?: string; 1370 | 1371 | /** 1372 | * Priority of this manga for the user 1373 | */ 1374 | priorityString?: string; 1375 | } 1376 | 1377 | /** 1378 | * News data model 1379 | */ 1380 | interface NewsDataModel { 1381 | /** 1382 | * The title of the news 1383 | */ 1384 | title: string; 1385 | 1386 | /** 1387 | * The link to the article 1388 | */ 1389 | link?: string; 1390 | 1391 | /** 1392 | * The URL of the cover image of the article 1393 | */ 1394 | image?: string; 1395 | 1396 | /** 1397 | * A short preview of the news description 1398 | */ 1399 | text?: string; 1400 | 1401 | /** 1402 | * The unique identifier of the news 1403 | */ 1404 | newsNumber: string; 1405 | } 1406 | 1407 | /** 1408 | * Anime episodes data model 1409 | */ 1410 | interface AnimeEpisodesDataModel { 1411 | /** 1412 | * The episode number 1413 | */ 1414 | epNumber?: number; 1415 | 1416 | /** 1417 | * A "Jan 10, 2014" date like of when the episode has been aired 1418 | */ 1419 | aired?: string; 1420 | 1421 | /** 1422 | * - 1423 | */ 1424 | discussionLink?: string; 1425 | 1426 | /** 1427 | * The episode title 1428 | */ 1429 | title: string; 1430 | 1431 | /** 1432 | * The japanese title of the episode 1433 | */ 1434 | japaneseTitle?: string; 1435 | } 1436 | 1437 | interface ReviewsListAnimeOptions { 1438 | /** 1439 | * The name of the anime 1440 | */ 1441 | name: string; 1442 | 1443 | /** 1444 | * The unique identifier of this anime 1445 | */ 1446 | id?: number; 1447 | 1448 | /** 1449 | * The number max of reviews to fetch - can be really long if omit 1450 | */ 1451 | limit?: number; 1452 | 1453 | /** 1454 | * The number of reviews to skip 1455 | */ 1456 | skip?: number; 1457 | } 1458 | 1459 | interface AnimeReviewsDataModel { 1460 | /** 1461 | * The name of the author 1462 | */ 1463 | author?: string; 1464 | 1465 | /** 1466 | * The date of the comment 1467 | */ 1468 | date?: Date; 1469 | 1470 | /** 1471 | * The number of episode seen 1472 | */ 1473 | seen?: string; 1474 | 1475 | /** 1476 | * The overall note of the anime 1477 | */ 1478 | overall?: number; 1479 | 1480 | /** 1481 | * The story note of the anime 1482 | */ 1483 | story?: number; 1484 | 1485 | /** 1486 | * The animation note of the anime 1487 | */ 1488 | animation?: number; 1489 | 1490 | /** 1491 | * The sound note of the anime 1492 | */ 1493 | sound?: number; 1494 | 1495 | /** 1496 | * The character note of the anime 1497 | */ 1498 | character?: number; 1499 | 1500 | /** 1501 | * The enjoyment note of the anime 1502 | */ 1503 | enjoyment?: number; 1504 | 1505 | /** 1506 | * The complete review 1507 | */ 1508 | review?: string; 1509 | } 1510 | 1511 | interface AnimeRecommendationsDataModel { 1512 | /** 1513 | * The link of the picture's anime recommended 1514 | */ 1515 | pictureImage?: string; 1516 | 1517 | /** 1518 | * The link of the anime recommended 1519 | */ 1520 | animeLink?: string; 1521 | 1522 | /** 1523 | * The name of the anime recommended 1524 | */ 1525 | anime: string; 1526 | 1527 | /** 1528 | * The recommendation 1529 | */ 1530 | mainRecommendation?: string; 1531 | 1532 | /** 1533 | * The name of the author 1534 | */ 1535 | author?: string; 1536 | } 1537 | 1538 | interface AnimeStatsDataModel { 1539 | /** 1540 | * The total number of person who are watching the anime 1541 | */ 1542 | watching?: number; 1543 | 1544 | /** 1545 | * The total number of person who completed the anime 1546 | */ 1547 | completed?: number; 1548 | 1549 | /** 1550 | * The total number of person who stop watching the anime but will continue later 1551 | */ 1552 | onHold?: number; 1553 | 1554 | /** 1555 | * The total number of person who stop watching the anime 1556 | */ 1557 | dropped?: number; 1558 | 1559 | /** 1560 | * The total number of person who are planning to watch the anime 1561 | */ 1562 | planToWatch?: number; 1563 | 1564 | /** 1565 | * The total stats 1566 | */ 1567 | total?: number; 1568 | 1569 | /** 1570 | * The number of person ranking the anime with a 10/10 1571 | */ 1572 | score10?: number; 1573 | 1574 | /** 1575 | * The number of person ranking the anime with a 9/10 1576 | */ 1577 | score9?: number; 1578 | 1579 | /** 1580 | * The number of person ranking the anime with a 8/10 1581 | */ 1582 | score8?: number; 1583 | 1584 | /** 1585 | * The number of person ranking the anime with a 7/10 1586 | */ 1587 | score7?: number; 1588 | 1589 | /** 1590 | * The number of person ranking the anime with a 6/10 1591 | */ 1592 | score6?: number; 1593 | 1594 | /** 1595 | * The number of person ranking the anime with a 5/10 1596 | */ 1597 | score5?: number; 1598 | 1599 | /** 1600 | * The number of person ranking the anime with a 4/10 1601 | */ 1602 | score4?: number; 1603 | 1604 | /** 1605 | * The number of person ranking the anime with a 3/10 1606 | */ 1607 | score3?: number; 1608 | 1609 | /** 1610 | * The number of person ranking the anime with a 2/10 1611 | */ 1612 | score2?: number; 1613 | 1614 | /** 1615 | * The number of person ranking the anime with a 1/10 1616 | */ 1617 | score1?: number; 1618 | } 1619 | 1620 | interface AnimePicturesDataModel { 1621 | /** 1622 | * The link of the image 1623 | */ 1624 | imageLink?: string; 1625 | } 1626 | 1627 | } 1628 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const getSeason = require('./seasons.js') 2 | const getNewsNoDetails = require('./news.js') 3 | const search = require('./search') 4 | const { getInfoFromName, getInfoFromURL, getResultsFromSearch } = require('./info.js') 5 | const { getWatchListFromUser } = require('./watchList.js') 6 | const { getEpisodesList } = require('./episodes.js') 7 | const { getReviewsList } = require('./reviews.js') 8 | const { getRecommendationsList } = require('./recommendations.js') 9 | const { getStats } = require('./stats.js') 10 | const { getPictures } = require('./pictures.js') 11 | const { getUser } = require('./users.js') 12 | 13 | module.exports = { 14 | getSeason, 15 | getNewsNoDetails, 16 | getInfoFromName, 17 | getInfoFromURL, 18 | getResultsFromSearch, 19 | getWatchListFromUser, 20 | getEpisodesList, 21 | getReviewsList, 22 | getRecommendationsList, 23 | getStats, 24 | getPictures, 25 | getUser, 26 | search 27 | } 28 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const { matchSorter } = require('match-sorter') 4 | 5 | const SEARCH_URI = 'https://myanimelist.net/search/prefix.json' 6 | 7 | const getFromBorder = ($, t) => { 8 | return $(`span:contains("${t}")`).parent().text().trim().split(' ').slice(1).join(' ').split('\n')[0].trim() 9 | } 10 | 11 | const getScoreStats = ($) => { 12 | const stats = Number($('span[itemprop="ratingCount"]').first().text()).toLocaleString('en-US') 13 | 14 | return `scored by ${stats} users` 15 | } 16 | 17 | const getPictureUrl = (url) => { 18 | const sizeRegex = /\/r\/\d*x\d*/ 19 | const parts = url.split('.') 20 | 21 | const completeUrl = parts.slice(0, -1).join('.').replace(sizeRegex, '') + '.jpg' 22 | 23 | return completeUrl 24 | } 25 | 26 | const parseCharacterOrStaff = (tr, isStaff = false) => { 27 | const getPicture = (nbChild) => { 28 | const src = tr.find(`td:nth-child(${nbChild})`).find('img').attr('data-srcset') 29 | 30 | if (src && src.includes('1x') && src.includes('2x')) { 31 | return getPictureUrl(src.split('1x, ')[1].replace(' 2x', '')) 32 | } else { 33 | // This most likely means that the seiyuu is not here. 34 | return undefined 35 | } 36 | } 37 | 38 | return JSON.parse(JSON.stringify({ 39 | link: tr.find('td:nth-child(1)').find('a').attr('href'), 40 | picture: getPicture(1), 41 | name: tr.find('td:nth-child(2)').text().trim().split('\n')[0], 42 | role: tr.find('td:nth-child(2)').text().trim().split('\n')[2].trim(), 43 | seiyuu: !isStaff ? { 44 | link: tr.find('td:nth-child(3)').find('a').attr('href'), 45 | picture: getPicture(3), 46 | name: tr.find('td:nth-child(3)').find('a').text().trim() 47 | } : undefined 48 | })) 49 | } 50 | 51 | const getCharactersAndStaff = ($) => { 52 | const results = { 53 | characters: [], 54 | staff: [] 55 | } 56 | 57 | // Characters 58 | const leftC = $('div.detail-characters-list').first().find('div.left-column') 59 | const rightC = $('div.detail-characters-list').first().find('div.left-right') 60 | 61 | const nbLeftC = leftC.children('table').length 62 | const nbRightC = rightC.children('table').length 63 | 64 | // Staff 65 | const leftS = $('div.detail-characters-list').last().find('div.left-column') 66 | const rightS = $('div.detail-characters-list').last().find('div.left-right') 67 | 68 | const nbLeftS = leftS.children('table').length 69 | const nbRightS = rightS.children('table').length 70 | 71 | // Characters 72 | for (let i = 1; i <= nbLeftC; ++i) { 73 | results.characters.push(parseCharacterOrStaff(leftC.find(`table:nth-child(${i}) > tbody > tr`))) 74 | } 75 | 76 | for (let i = 1; i <= nbRightC; ++i) { 77 | results.characters.push(parseCharacterOrStaff(rightC.find(`table:nth-child(${i}) > tbody > tr`))) 78 | } 79 | 80 | // Staff 81 | for (let i = 1; i <= nbLeftS; ++i) { 82 | results.staff.push(parseCharacterOrStaff(leftS.find(`table:nth-child(${i}) > tbody > tr`), true)) 83 | } 84 | 85 | for (let i = 1; i <= nbRightS; ++i) { 86 | results.staff.push(parseCharacterOrStaff(rightS.find(`table:nth-child(${i}) > tbody > tr`), true)) 87 | } 88 | 89 | return results 90 | } 91 | 92 | const parsePage = (data, anime) => { 93 | const $ = cheerio.load(data) 94 | const result = {} 95 | 96 | result.title = anime ? $('.title-name').text() : $('.h1-title span').text() 97 | result.synopsis = $('.js-scrollfix-bottom-rel [itemprop="description"]').text() 98 | result.picture = $('img[itemprop="image"]').attr('data-src') 99 | 100 | const staffAndCharacters = getCharactersAndStaff($) 101 | result.characters = staffAndCharacters.characters 102 | if (anime) { 103 | result.staff = staffAndCharacters.staff 104 | } 105 | 106 | const trailer = $('a.iframe.js-fancybox-video.video-unit.promotion').attr('href') 107 | if (trailer) { 108 | result.trailer = trailer 109 | } 110 | 111 | // Parsing left border. 112 | result.englishTitle = getFromBorder($, 'English:') 113 | result.japaneseTitle = getFromBorder($, 'Japanese:') 114 | result.synonyms = getFromBorder($, 'Synonyms:').split(', ') 115 | result.type = getFromBorder($, 'Type:') 116 | if (anime) { 117 | result.episodes = getFromBorder($, 'Episodes:') 118 | result.aired = getFromBorder($, 'Aired:') 119 | result.premiered = getFromBorder($, 'Premiered:') 120 | result.broadcast = getFromBorder($, 'Broadcast:') 121 | result.producers = getFromBorder($, 'Producers:').split(', ') 122 | result.studios = getFromBorder($, 'Studios:').split(', ') 123 | result.source = getFromBorder($, 'Source:') 124 | result.duration = getFromBorder($, 'Duration:') 125 | result.rating = getFromBorder($, 'Rating:') 126 | result.genres = getFromBorder($, 'Genres:') 127 | ? getFromBorder($, 'Genres:') 128 | .split(', ') 129 | .map((elem) => elem.trim().slice(0, elem.trim().length / 2)) 130 | : getFromBorder($, 'Genre:') 131 | .split(', ') 132 | .map((elem) => elem.trim().slice(0, elem.trim().length / 2)) 133 | } 134 | 135 | if (!anime) { 136 | result.volumes = getFromBorder($, 'Volumes:') 137 | result.chapters = getFromBorder($, 'Chapters:') 138 | result.published = getFromBorder($, 'Published:') 139 | result.serialization = getFromBorder($, 'Serialization:') 140 | result.authors = $('span:contains("Authors")').parent().children('a') 141 | .map(function (a) { return $(this).text().trim() }).get() 142 | result.genres = $('span:contains("Genres:")').parent().children('a') 143 | .map(function (a) { return $(this).attr('title') }).get() 144 | } 145 | 146 | result.status = getFromBorder($, 'Status:') 147 | result.score = getFromBorder($, 'Score:').split(' ')[0].slice(0, -1) 148 | result.scoreStats = getScoreStats($) 149 | result.ranked = getFromBorder($, 'Ranked:').slice(0, -1) 150 | result.popularity = getFromBorder($, 'Popularity:') 151 | result.members = getFromBorder($, 'Members:') 152 | result.favorites = getFromBorder($, 'Favorites:') 153 | 154 | return result 155 | } 156 | 157 | /** 158 | * Check if the url is for an anime or not 159 | * @params string url the url to check 160 | * @return boolean True if the url is for an anime 161 | **/ 162 | const isAnimeFromURL = url => { 163 | const urlSplitted = url.split('/') 164 | return urlSplitted[3] === 'anime' 165 | } 166 | 167 | const getInfoFromURL = (url) => { 168 | return new Promise((resolve, reject) => { 169 | if (!url || typeof url !== 'string' || !url.toLocaleLowerCase().includes('myanimelist')) { 170 | reject(new Error('[Mal-Scraper]: Invalid Url.')) 171 | return 172 | } 173 | 174 | url = encodeURI(url) 175 | const anime = isAnimeFromURL(url) 176 | 177 | axios.get(url) 178 | .then(({ data }) => { 179 | const res = parsePage(data, anime) 180 | res.id = +url.split(/\/+/)[3] 181 | resolve(res) 182 | }) 183 | .catch(/* istanbul ignore next */(err) => reject(err)) 184 | }) 185 | } 186 | 187 | const getResultsFromSearch = (keyword, type = 'anime') => { 188 | return new Promise((resolve, reject) => { 189 | if (!keyword) { 190 | reject(new Error('[Mal-Scraper]: Received no keyword to search.')) 191 | return 192 | } 193 | 194 | axios.get(SEARCH_URI, { 195 | params: { 196 | type: type, 197 | keyword: keyword.slice(0, 100) 198 | } 199 | }).then(({ data }) => { 200 | const items = [] 201 | 202 | data.categories.forEach((elem) => { 203 | elem.items.forEach((item) => { 204 | items.push(item) 205 | }) 206 | }) 207 | 208 | resolve(items) 209 | }).catch(/* istanbul ignore next */(err) => { 210 | reject(err) 211 | }) 212 | }) 213 | } 214 | 215 | const getInfoFromName = (name, getBestMatch = true, type = 'anime') => { 216 | return new Promise((resolve, reject) => { 217 | if (!name || typeof name !== 'string') { 218 | reject(new Error('[Mal-Scraper]: Invalid name.')) 219 | return 220 | } 221 | 222 | getResultsFromSearch(name, type) 223 | .then(async (items) => { 224 | if (!items.length) { 225 | resolve(null) 226 | return 227 | } 228 | try { 229 | const bestMatch = matchSorter(items, name, { keys: ['name'] }) 230 | const itemMatch = getBestMatch && bestMatch && bestMatch.length ? bestMatch[0] : items[0] 231 | const url = itemMatch.url 232 | const data = await getInfoFromURL(url) 233 | 234 | data.url = url 235 | 236 | resolve(data) 237 | } catch (e) { 238 | /* istanbul ignore next */ 239 | reject(e) 240 | } 241 | }) 242 | .catch(/* istanbul ignore next */(err) => reject(err)) 243 | }) 244 | } 245 | 246 | module.exports = { 247 | getInfoFromURL, 248 | getResultsFromSearch, 249 | getInfoFromName 250 | } 251 | -------------------------------------------------------------------------------- /src/news.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | 4 | const NEWS_URL_URI = 'https://myanimelist.net/news?p=' 5 | 6 | /* istanbul ignore next */ 7 | const byProperty = (prop) => { 8 | return (a, b) => { 9 | return typeof a[prop] === 'number' 10 | ? (a[prop] - b[prop]) 11 | : (a[prop] < b[prop]) 12 | ? -1 13 | : (a[prop] > b[prop]) 14 | ? 1 15 | : 0 16 | } 17 | } 18 | 19 | // 160 news. This is already expensive enough 20 | module.exports = (nbNews = 160) => { 21 | return new Promise((resolve, reject) => { 22 | const maxPage = Math.ceil(nbNews / 20) + 1 23 | 24 | const promises = [] 25 | 26 | for (let i = 1; i < maxPage; ++i) { 27 | promises.push(axios.get(`${NEWS_URL_URI}${i}`)) 28 | } 29 | 30 | axios.all(promises) 31 | .then(axios.spread(function () { 32 | const result = [] 33 | 34 | for (let i = 0; i < maxPage - 1; ++i) { 35 | const { data } = arguments[`${i}`] 36 | const $ = cheerio.load(data) 37 | 38 | const pageElements = $('.news-unit-right') // 20 elements 39 | 40 | // Pictures for each element 41 | const images = [] 42 | $('.image').each(function () { 43 | images.push($(this).attr('src')) 44 | }) 45 | 46 | // Get links for info 47 | const links = [] 48 | $('.image-link').each(function () { 49 | links.push($(this).attr('href')) 50 | }) 51 | 52 | // Gathering news' Titles 53 | const titles = pageElements.find('p.title').text().split('\n ') 54 | titles.shift() 55 | const texts = pageElements.find('div.text').text().split('\n ') 56 | texts.shift() 57 | 58 | for (let i = 0, l = titles.length; i < l; ++i) { 59 | titles[i] = titles[i].slice(0, -5) 60 | texts[i] = texts[i].slice(0, -5) 61 | } 62 | 63 | for (let i = 0, l = titles.length; i < l; ++i) { 64 | const tmp = links[i].split('/') 65 | result.push({ 66 | title: titles[i], 67 | link: links[i], 68 | image: images[i], 69 | text: texts[i], 70 | newsNumber: tmp[tmp.length - 1] 71 | }) 72 | } 73 | } 74 | 75 | result.sort(byProperty('newsNumber')) 76 | result.reverse() 77 | resolve(result.slice(0, nbNews)) 78 | })) 79 | .catch(/* istanbul ignore next */(err) => reject(err)) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /src/pictures.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const { getResultsFromSearch } = require('./info.js') 4 | 5 | const BASE_URI = 'https://myanimelist.net/anime/' 6 | 7 | const parsePage = ($) => { 8 | const items = $('#content .js-picture-gallery img') 9 | const result = [] 10 | 11 | items.each(function () { 12 | result.push({ 13 | imageLink: $(this).attr('data-src').trim() 14 | }) 15 | }) 16 | 17 | return result 18 | } 19 | 20 | const searchPage = (url) => { 21 | return new Promise((resolve, reject) => { 22 | axios.get(url) 23 | .then(({ data }) => { 24 | const $ = cheerio.load(data) 25 | const res = parsePage($) 26 | resolve(res) 27 | }) 28 | .catch(/* istanbul ignore next */(err) => reject(err)) 29 | }) 30 | } 31 | 32 | const getPicturesFromName = (name) => { 33 | return new Promise((resolve, reject) => { 34 | getResultsFromSearch(name) 35 | .then((items) => { 36 | const { url } = items[0] 37 | 38 | searchPage(`${encodeURI(url)}/pics`) 39 | .then((data) => resolve(data)) 40 | .catch(/* istanbul ignore next */(err) => reject(err)) 41 | }) 42 | .catch(/* istanbul ignore next */(err) => reject(err)) 43 | }) 44 | } 45 | 46 | const getPicturesFromNameAndId = (id, name) => { 47 | return new Promise((resolve, reject) => { 48 | searchPage(`${BASE_URI}${id}/${encodeURI(name)}/pics`) 49 | .then((data) => resolve(data)) 50 | .catch(/* istanbul ignore next */(err) => reject(err)) 51 | }) 52 | } 53 | 54 | const getPictures = (obj) => { 55 | return new Promise((resolve, reject) => { 56 | if (!obj) { 57 | reject(new Error('[Mal-Scraper]: No id nor name received.')) 58 | return 59 | } 60 | 61 | if (typeof obj === 'object' && !obj[0]) { 62 | const { id, name } = obj 63 | 64 | if (!id || !name || isNaN(+id) || typeof name !== 'string') { 65 | reject(new Error('[Mal-Scraper]: Malformed input. ID or name is malformed or missing.')) 66 | return 67 | } 68 | 69 | getPicturesFromNameAndId(id, name) 70 | .then((data) => resolve(data)) 71 | .catch(/* istanbul ignore next */(err) => reject(err)) 72 | } else { 73 | getPicturesFromName(obj) 74 | .then((data) => resolve(data)) 75 | .catch(/* istanbul ignore next */(err) => reject(err)) 76 | } 77 | }) 78 | } 79 | 80 | module.exports = { 81 | getPictures 82 | } 83 | -------------------------------------------------------------------------------- /src/recommendations.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const { getResultsFromSearch } = require('./info.js') 4 | 5 | const BASE_URI = 'https://myanimelist.net/anime/' 6 | 7 | const parsePage = ($) => { 8 | const recommendations = $('#content td:nth-child(2) .borderClass table') 9 | const results = [] 10 | recommendations.each(function () { 11 | const recommendation = {} 12 | recommendation.pictureImage = $(this).find('tr td:nth-child(1) .picSurround img').attr('data-src') 13 | recommendation.animeLink = $(this).find('tr td:nth-child(1) .picSurround a').attr('href') 14 | recommendation.anime = $(this).find('tr td:nth-child(2) div:nth-child(2) a strong').text().trim() 15 | recommendation.mainRecommendation = $(this).find('tr td:nth-child(2) .detail-user-recs-text').text().trim() 16 | recommendation.author = $(this).find('tr td:nth-child(2) > .borderClass .spaceit_pad:nth-child(2) > a').text().trim() 17 | results.push(recommendation) 18 | }) 19 | 20 | return results 21 | } 22 | 23 | const searchPage = (url) => { 24 | return new Promise((resolve, reject) => { 25 | axios.get(url) 26 | .then(({ data }) => { 27 | const $ = cheerio.load(data) 28 | const res = parsePage($) 29 | resolve(res) 30 | }) 31 | .catch(/* istanbul ignore next */(err) => reject(err)) 32 | }) 33 | } 34 | 35 | const getRecommendationsFromName = (name) => { 36 | return new Promise((resolve, reject) => { 37 | getResultsFromSearch(name) 38 | .then((items) => { 39 | const { url } = items[0] 40 | 41 | searchPage(`${encodeURI(url)}/userrecs`) 42 | .then((data) => resolve(data)) 43 | .catch(/* istanbul ignore next */(err) => reject(err)) 44 | }) 45 | .catch(/* istanbul ignore next */(err) => reject(err)) 46 | }) 47 | } 48 | 49 | const getRecommendationsFromNameAndId = (id, name = 'anything') => { 50 | return new Promise((resolve, reject) => { 51 | searchPage(`${BASE_URI}${id}/${encodeURI(name)}/userrecs`) 52 | .then((data) => resolve(data)) 53 | .catch(/* istanbul ignore next */(err) => reject(err)) 54 | }) 55 | } 56 | 57 | const getRecommendationsList = (obj) => { 58 | return new Promise((resolve, reject) => { 59 | if (!obj) { 60 | reject(new Error('[Mal-Scraper]: No id nor name received.')) 61 | return 62 | } 63 | 64 | if (typeof obj === 'object' && !obj[0]) { 65 | const { id, name } = obj 66 | 67 | if (!id || isNaN(+id) || (name && typeof name !== 'string')) { 68 | reject(new Error('[Mal-Scraper]: Malformed input. ID or name is malformed or missing.')) 69 | return 70 | } 71 | 72 | getRecommendationsFromNameAndId(id, name) 73 | .then((data) => resolve(data)) 74 | .catch(/* istanbul ignore next */(err) => reject(err)) 75 | } else { 76 | getRecommendationsFromName(obj) 77 | .then((data) => resolve(data)) 78 | .catch(/* istanbul ignore next */(err) => reject(err)) 79 | } 80 | }) 81 | } 82 | 83 | module.exports = { 84 | getRecommendationsList 85 | } 86 | -------------------------------------------------------------------------------- /src/reviews.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const { getResultsFromSearch } = require('./info.js') 4 | 5 | const BASE_URI = 'https://myanimelist.net/anime/' 6 | const NUMBER_REVIEWS_BY_PAGE = 20 7 | const INITIAL_FIRST_PAGE_REVIEW = 1 8 | 9 | /** 10 | * Return a formatter javascript date 11 | * @params malDate string a date in a string object 12 | * @return date The string parse to a date 13 | **/ 14 | const malDateToJsDate = (malDate) => { 15 | return new Date(malDate) 16 | } 17 | 18 | /** 19 | * Return a formatted javascript number 20 | * @params malNumber string a number in a string object 21 | * @return number The string parse to a number 22 | **/ 23 | const malNumberToJsNumber = (malNumber) => { 24 | return malNumber ? Number(malNumber) : 0 25 | } 26 | 27 | const parsePage = ($) => { 28 | const items = $('.borderDark') 29 | const result = [] 30 | 31 | items.each(function (elem) { 32 | const notes = $(this).find('.spaceit.pt8 div') 33 | const reviewMore = $(this).find('.spaceit.pt8 span') 34 | // For presenting the review only without the notes 35 | $(this).find('.spaceit.pt8 div').remove() 36 | $(this).find('.spaceit.pt8 span').remove() 37 | $(this).find('.spaceit.pt8 a.js-toggle-review-button').remove() 38 | 39 | result.push({ 40 | author: $($(this).find('.spaceit td:nth-child(2) a')['0']).text().trim(), 41 | date: malDateToJsDate($($(this).find('.spaceit .mb8 div')['0']).text().trim()), 42 | seen: $(this).find('.spaceit .mb8 .lightLink').text().trim(), 43 | overall: malNumberToJsNumber($(notes).find('tr:nth-child(1) td:nth-child(2)').text().trim()), 44 | story: malNumberToJsNumber($(notes).find('tr:nth-child(2) td:nth-child(2)').text().trim()), 45 | animation: malNumberToJsNumber($(notes).find('tr:nth-child(3) td:nth-child(2)').text().trim()), 46 | sound: malNumberToJsNumber($(notes).find('tr:nth-child(4) td:nth-child(2)').text().trim()), 47 | character: malNumberToJsNumber($(notes).find('tr:nth-child(5) td:nth-child(2)').text().trim()), 48 | enjoyment: malNumberToJsNumber($(notes).find('tr:nth-child(6) td:nth-child(2)').text().trim()), 49 | review: $(this).find('.spaceit.pt8').text().trim() + $(reviewMore).text().trim() 50 | }) 51 | }) 52 | 53 | return result 54 | } 55 | 56 | const searchPage = (url, limit, skip, p, res = []) => { 57 | return new Promise((resolve, reject) => { 58 | axios.get(url, { 59 | params: { 60 | p 61 | } 62 | }).then(({ data }) => { 63 | const $ = cheerio.load(data) 64 | 65 | const tmpRes = parsePage($) 66 | res = res.concat(tmpRes) 67 | 68 | // If there is some skip to do, we splice the first result of the first page 69 | if (skip !== 0) { 70 | res.splice(0, skip) 71 | skip = 0 72 | } 73 | 74 | if (res.length <= limit) { 75 | p++ 76 | searchPage(url, limit, skip, p, res) 77 | .then((data) => resolve(data)) 78 | .catch(/* istanbul ignore next */(err) => reject(err)) 79 | } else { 80 | // If our limit is under the number of result in the page, we remove the excess 81 | if (res.length !== limit) { 82 | const nbrElementToRemove = res.length - limit 83 | res.splice(-nbrElementToRemove, nbrElementToRemove) 84 | } 85 | resolve(res) 86 | } 87 | }).catch(/* istanbul ignore next */(err) => reject(err)) 88 | }) 89 | } 90 | 91 | const getReviewsFromName = (name, limit, skip, p) => { 92 | return new Promise((resolve, reject) => { 93 | getResultsFromSearch(name).then((items) => { 94 | const { url } = items[0] 95 | 96 | searchPage(`${encodeURI(url)}/reviews`, limit, skip, p) 97 | .then((data) => resolve(data)) 98 | .catch(/* istanbul ignore next */(err) => reject(err)) 99 | }).catch(/* istanbul ignore next */(err) => reject(err)) 100 | }) 101 | } 102 | 103 | const getReviewsFromNameAndId = (id, name, limit, skip, p) => { 104 | return new Promise((resolve, reject) => { 105 | searchPage(`${BASE_URI}${id}/${encodeURI(name)}/reviews`, limit, skip, p) 106 | .then((data) => resolve(data)) 107 | .catch(/* istanbul ignore next */(err) => reject(err)) 108 | }) 109 | } 110 | 111 | /** 112 | * Return the starting page of the query depending of the number of element to skip 113 | * @params skip number The number of element to skip 114 | * @return number page to start the query 115 | **/ 116 | const startingPage = (skip) => { 117 | return skip !== 0 ? Math.floor(skip / NUMBER_REVIEWS_BY_PAGE) + 1 : INITIAL_FIRST_PAGE_REVIEW 118 | } 119 | 120 | /** 121 | * Return the number of skip remaining after skipping x page 122 | * @params skip number Total number of skip of the call 123 | * @params p number Number of page to skip 124 | * @return number Number of skip remaining in the first page 125 | **/ 126 | const skipByPage = (skip, p) => { 127 | return skip !== 0 ? Math.max(0, skip - ((p - 1) * NUMBER_REVIEWS_BY_PAGE)) : 0 128 | } 129 | 130 | const getReviewsList = (obj) => { 131 | return new Promise((resolve, reject) => { 132 | if (!obj || typeof obj !== 'object') { 133 | reject(new Error('[Mal-Scraper]: No id nor name received.')) 134 | return 135 | } 136 | const { id, name, limit } = obj 137 | let skip = obj.skip ? obj.skip : 0 138 | 139 | if ((obj.id && (!name || isNaN(+id))) || typeof name !== 'string') { 140 | reject(new Error('[Mal-Scraper]: Malformed input. ID or name is malformed or missing.')) 141 | return 142 | } 143 | 144 | const p = startingPage(skip) 145 | skip = skipByPage(skip, p) 146 | 147 | if (obj.id) { 148 | getReviewsFromNameAndId(id, name, limit, skip, p) 149 | .then((data) => resolve(data)) 150 | .catch(/* istanbul ignore next */(err) => reject(err)) 151 | } else { 152 | getReviewsFromName(name, limit, skip, p) 153 | .then((data) => resolve(data)) 154 | .catch(/* istanbul ignore next */(err) => reject(err)) 155 | } 156 | }) 157 | } 158 | 159 | module.exports = { 160 | getReviewsList 161 | } 162 | -------------------------------------------------------------------------------- /src/search/anime/genresList.json: -------------------------------------------------------------------------------- 1 | [{"value":"1","name":"Action"},{"value":"2","name":"Adventure"},{"value":"3","name":"Cars"},{"value":"4","name":"Comedy"},{"value":"5","name":"Dementia"},{"value":"6","name":"Demons"},{"value":"7","name":"Mystery"},{"value":"8","name":"Drama"},{"value":"9","name":"Ecchi"},{"value":"10","name":"Fantasy"},{"value":"11","name":"Game"},{"value":"12","name":"Hentai"},{"value":"13","name":"Historical"},{"value":"14","name":"Horror"},{"value":"15","name":"Kids"},{"value":"16","name":"Magic"},{"value":"17","name":"Martial Arts"},{"value":"18","name":"Mecha"},{"value":"19","name":"Music"},{"value":"20","name":"Parody"},{"value":"21","name":"Samurai"},{"value":"22","name":"Romance"},{"value":"23","name":"School"},{"value":"24","name":"Sci-Fi"},{"value":"25","name":"Shoujo"},{"value":"26","name":"Shoujo Ai"},{"value":"27","name":"Shounen"},{"value":"28","name":"Shounen Ai"},{"value":"29","name":"Space"},{"value":"30","name":"Sports"},{"value":"31","name":"Super Power"},{"value":"32","name":"Vampire"},{"value":"33","name":"Yaoi"},{"value":"34","name":"Yuri"},{"value":"35","name":"Harem"},{"value":"36","name":"Slice of Life"},{"value":"37","name":"Supernatural"},{"value":"38","name":"Military"},{"value":"39","name":"Police"},{"value":"40","name":"Psychological"},{"value":"41","name":"Thriller"},{"value":"42","name":"Seinen"},{"value":"43","name":"Josei"}] -------------------------------------------------------------------------------- /src/search/constants.js: -------------------------------------------------------------------------------- 1 | 2 | const lists = require('./getLists.js') 3 | 4 | const trace = '####' 5 | const ROOT_URL = 'https://myanimelist.net' 6 | const BASE_URL = `${ROOT_URL}/${trace}.php` 7 | 8 | const availableValues = { 9 | type: [ 10 | { name: 'none', value: 0 }, 11 | { name: 'tv', value: 1 }, 12 | { name: 'ova', value: 2 }, 13 | { name: 'movie', value: 3 }, 14 | { name: 'special', value: 4 }, 15 | { name: 'ona', value: 5 }, 16 | { name: 'music', value: 6 } 17 | ], 18 | status: [ 19 | { name: 'none', value: 0 }, 20 | { name: 'finished', value: 1 }, 21 | { name: 'currently', value: 2 }, 22 | { name: 'not-aired', value: 3 } 23 | ], 24 | r: [ 25 | { name: 'none', value: 0 }, 26 | { name: 'G', value: 1 }, 27 | { name: 'PG', value: 2 }, 28 | { name: 'PG-13', value: 3 }, 29 | { name: 'R', value: 4 }, 30 | { name: 'R+', value: 5 }, 31 | { name: 'Rx', value: 6 } 32 | ], 33 | score: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 34 | p: lists.producers, 35 | genre: lists.genres 36 | } 37 | 38 | const orderMap = { 39 | keys: { 40 | startDate: 2, 41 | score: 3, 42 | eps: 4, 43 | endDate: 5, 44 | type: 6, 45 | members: 7, 46 | rated: 8 47 | }, 48 | order: { 49 | DESC: 2, 50 | ASC: 1 51 | } 52 | } 53 | 54 | const columns = { 55 | anime: [ 56 | 'thumbnail', 57 | 'title', 58 | 'type', 59 | 'nbEps', 60 | 'score', 61 | 'startDate', 62 | 'endDate', 63 | 'members', 64 | 'rating' 65 | ], 66 | manga: [ 67 | 'thumbnail', 68 | 'title', 69 | 'type', 70 | 'vols', 71 | 'nbChapters', 72 | 'score', 73 | 'startDate', 74 | 'endDate', 75 | 'members' 76 | ] 77 | } 78 | 79 | module.exports = { 80 | trace, 81 | ROOT_URL, 82 | BASE_URL, 83 | lists, 84 | availableValues, 85 | orderMap, 86 | columns 87 | } 88 | -------------------------------------------------------------------------------- /src/search/getLists.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | 4 | const trace = '####' 5 | const BASE_URL = `https://myanimelist.net/${trace}.php` 6 | 7 | const getProducers = (type) => { 8 | return new Promise((resolve, reject) => { 9 | axios.get(BASE_URL.replace(trace, type)) 10 | .then(({ data }) => { 11 | const $ = cheerio.load(data) 12 | const result = [] 13 | 14 | $(`select[name="${type === 'anime' ? 'p' : 'mid'}"] option`).each(function () { 15 | result.push({ value: $(this).val(), name: $(this).text().trim() }) 16 | }) 17 | 18 | resolve(result) 19 | }) 20 | .catch((err) => reject(err)) 21 | }) 22 | } 23 | 24 | const getGenres = (type) => { 25 | return new Promise((resolve, reject) => { 26 | axios.get(BASE_URL.replace(trace, type)) 27 | .then(({ data }) => { 28 | const $ = cheerio.load(data) 29 | const result = [] 30 | 31 | $('#advancedSearch > .space_table > tbody > tr').each(function () { 32 | $(this).find('td').each(function () { 33 | const value = $(this).find('input').val() 34 | const name = $(this).find('label').text().trim() 35 | 36 | result.push({ value, name }) 37 | }) 38 | }) 39 | 40 | resolve(result) 41 | }) 42 | .catch((err) => reject(err)) 43 | }) 44 | } 45 | 46 | module.exports = { 47 | getProducers, 48 | getGenres, 49 | producers: { 50 | anime: require('./anime/producersList.json'), 51 | manga: require('./manga/producersList.json') 52 | }, 53 | genres: { 54 | anime: require('./anime/genresList.json'), 55 | manga: require('./manga/genresList.json') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/search/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | 4 | const { 5 | trace, 6 | ROOT_URL, 7 | BASE_URL, 8 | availableValues, 9 | columns, 10 | lists, 11 | orderMap 12 | } = require('./constants.js') 13 | 14 | /** 15 | * @typedef {{value: String, name: String}} Genre 16 | * 17 | * @typedef {{ 18 | * sd: Number, 19 | * sm: Number, 20 | * sy: Number, 21 | * }} StartDate 22 | * 23 | * @typedef {{ 24 | * ed: Number, 25 | * em: Number, 26 | * ey: Number, 27 | * }} EndDate 28 | * 29 | * @typedef {{ 30 | * term: String, 31 | * type: Number, 32 | * status: Number, 33 | * score: Number, 34 | * producer: Number, 35 | * rating: Number, 36 | * startDate: StartDate, 37 | * endDate: EndDate, 38 | * genreType: Number, 39 | * genres: Genre[], 40 | * has: Number 41 | * }} SearchOpts 42 | */ 43 | 44 | const getOrderParams = (opts) => { 45 | const { keys, order = ['DESC', 'DESC'] } = opts 46 | 47 | if (!Array.isArray(keys) || !Array.isArray(order)) throw new Error('Invalid order parameters.') 48 | if (!keys.length) throw new Error('Invalid order keys.') 49 | if (order && order.length !== keys.length) throw new Error('Invalid order.') 50 | 51 | return keys.reduce((acc, key, index) => { 52 | const _order = order[index] 53 | 54 | acc += `o=${encodeURIComponent(orderMap.keys[key])}&w=${encodeURIComponent(orderMap.order[_order])}&` 55 | 56 | return acc 57 | }, '?') 58 | } 59 | 60 | const getParams = (_type, opts) => { 61 | const { 62 | term = '', 63 | type = 0, 64 | status = 0, 65 | score = 0, 66 | producer = 0, 67 | rating = 0, 68 | startDate = {}, 69 | endDate = {}, 70 | genreType = 0, 71 | genres = [], 72 | has: after 73 | } = opts 74 | 75 | if (!availableValues.type.map(({ value }) => +value).includes(type)) throw new Error('Invalid Type.') 76 | if (!availableValues.status.map(({ value }) => +value).includes(status)) throw new Error('Invalid status.') 77 | if (_type === 'anime' && !availableValues.r.map(({ value }) => +value).includes(rating)) throw new Error('Invalid rating.') 78 | 79 | if (!availableValues.score.includes(score)) throw new Error('Invalid score.') 80 | 81 | if (!availableValues.p[_type].map(({ value }) => +value).includes(producer)) throw new Error('Invalid producer.') 82 | 83 | genres.forEach((genre) => { 84 | if (genre && !availableValues.genre[_type].map(({ value }) => +value).includes(genre)) throw new Error('Invalid genre.') 85 | }) 86 | 87 | return JSON.parse(JSON.stringify({ 88 | sd: startDate.day || 0, 89 | sm: startDate.month || 0, 90 | sy: startDate.year || 0, 91 | ed: endDate.day || 0, 92 | em: endDate.month || 0, 93 | ey: endDate.year || 0, 94 | 95 | c: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 96 | 97 | gx: genreType === 'exclude' ? 1 : 0, 98 | 99 | q: term, 100 | p: producer, 101 | r: _type === 'anime' ? rating : undefined, 102 | 103 | genre: genres, 104 | type, 105 | status, 106 | score, 107 | 108 | show: typeof after === 'number' ? after : undefined 109 | })) 110 | } 111 | 112 | const parsePage = (type, $) => { 113 | const result = [] 114 | const table = $('#content div.list table tbody tr') 115 | 116 | table.each(function (index) { 117 | if (index === 0) return 118 | 119 | const entry = {} 120 | 121 | $(this).find('td').each(function (subIndex) { 122 | if (subIndex === 0) { 123 | entry.thumbnail = $(this).find('.picSurround > a > img').attr('data-srcset').split(', ')[1].split(' ')[0] 124 | return 125 | } 126 | 127 | if (subIndex === 1) { 128 | $(this).find('a').each(function (_i) { 129 | if (_i > 1) return 130 | 131 | if (_i === 0) entry.url = $(this).attr('href') 132 | if (_i === 1) entry.video = $(this).text().trim() !== 'add' ? $(this).attr('href') : null 133 | }) 134 | 135 | entry.shortDescription = $(this).children().last().text() 136 | 137 | entry.title = $(this).find('a strong').text().trim() 138 | 139 | return 140 | } 141 | 142 | entry[columns[type][subIndex]] = $(this).text().trim() 143 | }) 144 | 145 | result.push(entry) 146 | }) 147 | 148 | return result 149 | } 150 | 151 | const hasNext = ($) => { 152 | // This should be like 153 | // [1] 2 3 ... 20 154 | const anchor = $('#content > div.normal_header > div').find('span') 155 | 156 | // If last character is a closing bracket, it means that the current page is at the end. 157 | const hasNext = anchor.text().slice(-1) !== ']' 158 | let nextUrl = null 159 | 160 | if (hasNext) { 161 | // Looking for the current page which is between brackets 162 | const currentPageNumber = anchor.text().match(/\[\d+\]/) 163 | 164 | if (currentPageNumber.length) { 165 | // Removing brackets and adding one to find next page 166 | const nextPageNumber = +currentPageNumber[0].slice(1, -1) + 1 167 | 168 | // href is a patial URI missing the website URL. 169 | nextUrl = ROOT_URL + anchor.find(`a:contains(${nextPageNumber})`).attr('href') 170 | } 171 | } 172 | 173 | return { hasNext, nextUrl } 174 | } 175 | 176 | const getResults = (type, url, params = {}, maxResult = 50, result = []) => { 177 | return new Promise((resolve, reject) => { 178 | axios.get(url, { params }) 179 | .then((res) => { 180 | const { data } = res 181 | const $ = cheerio.load(data) 182 | const next = hasNext($) 183 | const _result = [...result, ...parsePage(type, $)] 184 | 185 | resolve( 186 | _result.length < maxResult && next.hasNext 187 | ? getResults(type, next.nextUrl, {}, maxResult, _result) 188 | : _result 189 | ) 190 | }) 191 | .catch(reject) 192 | }) 193 | } 194 | 195 | /** 196 | * Makes a search request based on: 197 | * -- https://myanimelist.net/anime.php 198 | * -- https://myanimelist.net/manga.php 199 | * 200 | * @param {String} type anime | manga 201 | * @param {SearchOpts} opts 202 | */ 203 | const search = (type, opts) => { 204 | const params = getParams(type, opts) 205 | const order = opts.order && getOrderParams(opts.order) 206 | 207 | return getResults( 208 | type, 209 | BASE_URL.replace(trace, type) + (order || ''), 210 | params, opts.maxResults 211 | ) 212 | } 213 | 214 | module.exports = { 215 | search, 216 | helpers: { 217 | availableValues, 218 | producersList: lists.producers, 219 | genresList: lists.genres, 220 | orderTypes: Object.keys(orderMap.keys) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/search/manga/genresList.json: -------------------------------------------------------------------------------- 1 | [{"value":"1","name":"Action"},{"value":"2","name":"Adventure"},{"value":"3","name":"Cars"},{"value":"4","name":"Comedy"},{"value":"5","name":"Dementia"},{"value":"6","name":"Demons"},{"value":"7","name":"Mystery"},{"value":"8","name":"Drama"},{"value":"9","name":"Ecchi"},{"value":"10","name":"Fantasy"},{"value":"11","name":"Game"},{"value":"12","name":"Hentai"},{"value":"13","name":"Historical"},{"value":"14","name":"Horror"},{"value":"15","name":"Kids"},{"value":"16","name":"Magic"},{"value":"17","name":"Martial Arts"},{"value":"18","name":"Mecha"},{"value":"19","name":"Music"},{"value":"20","name":"Parody"},{"value":"21","name":"Samurai"},{"value":"22","name":"Romance"},{"value":"23","name":"School"},{"value":"24","name":"Sci-Fi"},{"value":"25","name":"Shoujo"},{"value":"26","name":"Shoujo Ai"},{"value":"27","name":"Shounen"},{"value":"28","name":"Shounen Ai"},{"value":"29","name":"Space"},{"value":"30","name":"Sports"},{"value":"31","name":"Super Power"},{"value":"32","name":"Vampire"},{"value":"33","name":"Yaoi"},{"value":"34","name":"Yuri"},{"value":"35","name":"Harem"},{"value":"36","name":"Slice of Life"},{"value":"37","name":"Supernatural"},{"value":"38","name":"Military"},{"value":"39","name":"Police"},{"value":"40","name":"Psychological"},{"value":"41","name":"Seinen"},{"value":"42","name":"Josei"},{"value":"43","name":"Doujinshi"},{"value":"44","name":"Gender Bender"},{"value":"45","name":"Thriller"}] -------------------------------------------------------------------------------- /src/search/manga/producersList.json: -------------------------------------------------------------------------------- 1 | [{"value":"0","name":"Select magazine"},{"value":"37","name":".hack//G.U. The World"},{"value":"773","name":"2D Dream Magazine"},{"value":"556","name":"4-koma Nano Ace"},{"value":"210","name":"@vitamin"},{"value":"1400","name":"Abumix"},{"value":"503","name":"Ace Assault"},{"value":"519","name":"Ace Momogumi"},{"value":"105","name":"Ace Next"},{"value":"280","name":"Ace Tokunou"},{"value":"1291","name":"Action Deluxe"},{"value":"514","name":"Action Pizazz"},{"value":"537","name":"Action Pizazz DX"},{"value":"1019","name":"Action Pizazz HB"},{"value":"679","name":"Action Pizazz Special"},{"value":"1037","name":"Action Young"},{"value":"4","name":"Afternoon"},{"value":"739","name":"Age Premium"},{"value":"347","name":"Ai no Taiken Special Deluxe"},{"value":"863","name":"Aka Lala"},{"value":"249","name":"Akamaru Jump"},{"value":"217","name":"Alice"},{"value":"150","name":"AlphaPolis Web Manga"},{"value":"1403","name":"Alterna pixiv"},{"value":"741","name":"Altima Ace"},{"value":"39","name":"Amie Magazine"},{"value":"1153","name":"Ane LaLa"},{"value":"486","name":"Ane-kei Petit Comic"},{"value":"641","name":"Angel Club"},{"value":"1205","name":"Animage"},{"value":"1251","name":"Animal House"},{"value":"449","name":"Anise Magazine"},{"value":"491","name":"AniSen"},{"value":"805","name":"Ao Lala"},{"value":"1175","name":"Aoharu"},{"value":"551","name":"Apple Collection"},{"value":"1276","name":"Apple Mystery"},{"value":"1368","name":"aQtto!"},{"value":"1197","name":"Aqua BL Kingdom"},{"value":"125","name":"Aqua Comics"},{"value":"189","name":"Aqua Deep"},{"value":"541","name":"Aqua PiPi"},{"value":"499","name":"ARIA"},{"value":"1163","name":"Asahi Shinbun"},{"value":"501","name":"Asahi Shougakusei Shinbun"},{"value":"14","name":"Asuka (Monthly)"},{"value":"149","name":"Asuka Ciel"},{"value":"97","name":"Asuka Fantasy DX"},{"value":"151","name":"Asuka Mystery DX"},{"value":"283","name":"AX"},{"value":"1143","name":"AYLA"},{"value":"905","name":"B'S Anima"},{"value":"1433","name":"B's-LOG Cheek"},{"value":"1109","name":"B's-LOVEY Katsubou"},{"value":"1025","name":"B's-LOVEY recottia"},{"value":"1457","name":"b-boy Cube"},{"value":"529","name":"b-Boy Honey"},{"value":"226","name":"b-Boy Luv"},{"value":"1416","name":"b-boy Omegaverse"},{"value":"1422","name":"B-BOY P!"},{"value":"400","name":"b-Boy Phoenix"},{"value":"274","name":"b-Boy Zips"},{"value":"418","name":"b-Rash"},{"value":"865","name":"Baby"},{"value":"1359","name":"Badi"},{"value":"195","name":"Bamboo Comics"},{"value":"1418","name":"Barazoku"},{"value":"51","name":"Be x Boy GOLD"},{"value":"306","name":"Be-Love"},{"value":"127","name":"Beans Ace"},{"value":"831","name":"Bessatsu CoroCoro Comic"},{"value":"111","name":"Bessatsu Friend"},{"value":"793","name":"Bessatsu Friend Zoukan BetsuFure"},{"value":"176","name":"Bessatsu Hana to Yume"},{"value":"1306","name":"Bessatsu Harmony Romance"},{"value":"910","name":"Bessatsu Manga Goraku"},{"value":"53","name":"Bessatsu Margaret"},{"value":"813","name":"Bessatsu Shounen Champion"},{"value":"450","name":"Bessatsu Shounen Magazine"},{"value":"561","name":"Bessatsu Young Champion"},{"value":"240","name":"Bessatsu Young Magazine"},{"value":"54","name":"Betsucomi"},{"value":"815","name":"Betsuma Sister"},{"value":"467","name":"BGM"},{"value":"1009","name":"bianca"},{"value":"179","name":"Biblos"},{"value":"55","name":"Big Comic"},{"value":"294","name":"Big Comic for Ladies"},{"value":"1","name":"Big Comic Original"},{"value":"1169","name":"Big Comic Original Zoukan"},{"value":"3","name":"Big Comic Spirits"},{"value":"264","name":"Big Comic Superior"},{"value":"1155","name":"Big Comic Zoukan"},{"value":"705","name":"Big Gangan"},{"value":"1404","name":"Big Gangan Okawari"},{"value":"299","name":"Big Gold"},{"value":"683","name":"Bishoujo Kakumei KIWAME"},{"value":"979","name":"Bishoujo Kakumei KIWAME Road"},{"value":"657","name":"Bishoujo-teki Kaikatsuryoku"},{"value":"916","name":"Blade Online"},{"value":"553","name":"BLink"},{"value":"468","name":"Bokura"},{"value":"231","name":"Bonita"},{"value":"504","name":"Booking"},{"value":"1405","name":"Boukenou"},{"value":"302","name":"Bouquet"},{"value":"1296","name":"Bouquet DX"},{"value":"1065","name":"BOX-AiR"},{"value":"627","name":"Boy's LOVE"},{"value":"629","name":"BOY'S Pierce"},{"value":"1317","name":"BOY'S Pierce Kindan"},{"value":"665","name":"Boys Capi!"},{"value":"1415","name":"Boys Fan"},{"value":"721","name":"Boys Jam!"},{"value":"191","name":"Boys L"},{"value":"1263","name":"Bstreet"},{"value":"1267","name":"Bushiroad (Monthly)"},{"value":"414","name":"Bushiroad TCG Magazine"},{"value":"57","name":"Business Jump"},{"value":"577","name":"Buster Comic"},{"value":"460","name":"Cab"},{"value":"1049","name":"Cabaret-Club Comic"},{"value":"1432","name":"cakes"},{"value":"154","name":"Candy Time"},{"value":"550","name":"Canna"},{"value":"687","name":"Canopri Comic"},{"value":"1303","name":"Capbon!"},{"value":"472","name":"Catalogue Series"},{"value":"1298","name":"Champion Cross"},{"value":"186","name":"Champion RED"},{"value":"434","name":"Champion RED Ichigo"},{"value":"1015","name":"Champion Tap!"},{"value":"426","name":"Chance"},{"value":"1305","name":"Chance+"},{"value":"1377","name":"Char@"},{"value":"133","name":"Chara"},{"value":"392","name":"Chara Mel"},{"value":"496","name":"Chara Mel Febri"},{"value":"333","name":"Chara Selection"},{"value":"388","name":"Charade"},{"value":"124","name":"Cheese!"},{"value":"266","name":"Cheese! Zoukan"},{"value":"1001","name":"Cheri+"},{"value":"1073","name":"ChobeComi!"},{"value":"58","name":"Chorus"},{"value":"170","name":"ChuChu"},{"value":"1384","name":"Chuuou Kouron"},{"value":"59","name":"Ciao"},{"value":"611","name":"Ciao DX"},{"value":"997","name":"Ciao DX Horror & Mystery"},{"value":"482","name":"CIEL"},{"value":"152","name":"CIEL TrèsTrès"},{"value":"1409","name":"Cigarillo"},{"value":"337","name":"Cita Cita"},{"value":"907","name":"Cita-NIUM"},{"value":"466","name":"Citron"},{"value":"593","name":"Club Sunday"},{"value":"330","name":"Cobalt"},{"value":"817","name":"Cocohana"},{"value":"991","name":"Colorful Drops"},{"value":"215","name":"Comi Digi"},{"value":"603","name":"Comic 0EX"},{"value":"137","name":"Comic Alive"},{"value":"1449","name":"Comic Alpha"},{"value":"983","name":"Comic Anthurium"},{"value":"315","name":"Comic Aqua"},{"value":"571","name":"Comic Aun"},{"value":"269","name":"Comic Avarus"},{"value":"162","name":"Comic B's-LOG"},{"value":"559","name":"Comic B's-LOG Air Raid"},{"value":"528","name":"Comic B's-LOG Kyun!"},{"value":"1288","name":"Comic Bavel"},{"value":"647","name":"Comic Bazooka"},{"value":"677","name":"Comic Be"},{"value":"114","name":"Comic Beam"},{"value":"1376","name":"Comic BEAT"},{"value":"554","name":"Comic Bee"},{"value":"1358","name":"Comic Bingo"},{"value":"91","name":"Comic Birz"},{"value":"28","name":"Comic Blade"},{"value":"30","name":"Comic Blade Masamune"},{"value":"119","name":"Comic Blade Zebel"},{"value":"109","name":"Comic Bonbon"},{"value":"1460","name":"Comic Boost"},{"value":"1137","name":"Comic Break"},{"value":"867","name":"Comic BugBug"},{"value":"475","name":"Comic BunBun"},{"value":"92","name":"Comic Bunch"},{"value":"196","name":"Comic Burger"},{"value":"509","name":"Comic Candoll"},{"value":"562","name":"Comic Champ"},{"value":"252","name":"Comic Chara"},{"value":"517","name":"Comic Charge"},{"value":"1043","name":"Comic ChoiS!"},{"value":"969","name":"Comic Comomo"},{"value":"502","name":"Comic Comp"},{"value":"102","name":"COMIC Crimson"},{"value":"193","name":"Comic Cue"},{"value":"1309","name":"Comic Cune"},{"value":"903","name":"Comic Cyutt"},{"value":"801","name":"Comic Dangan"},{"value":"1459","name":"Comic Days"},{"value":"1213","name":"Comic Dengeki Daioh \"g\""},{"value":"63","name":"Comic Dengeki Teioh"},{"value":"685","name":"Comic Dolphin"},{"value":"1111","name":"Comic Dolphin Jr."},{"value":"267","name":"Comic Dragon"},{"value":"707","name":"Comic Earth☆Star"},{"value":"1177","name":"Comic Ero-tama"},{"value":"1171","name":"Comic Europa"},{"value":"1374","name":"Comic ExE"},{"value":"443","name":"Comic Fans"},{"value":"743","name":"Comic Fantasy"},{"value":"1378","name":"Comic Fire"},{"value":"1364","name":"Comic Flamingo"},{"value":"446","name":"Comic Flamingo R"},{"value":"174","name":"Comic Flapper"},{"value":"1277","name":"Comic fleur"},{"value":"492","name":"Comic Gaia"},{"value":"281","name":"Comic Gamest"},{"value":"1099","name":"Comic Gamma"},{"value":"1233","name":"Comic Gang"},{"value":"1217","name":"Comic Garden"},{"value":"340","name":"Comic Gardo"},{"value":"769","name":"Comic GEKI-YABA"},{"value":"1085","name":"Comic Gekiman"},{"value":"573","name":"Comic Gene"},{"value":"1322","name":"Comic GENKi"},{"value":"1271","name":"Comic Genra"},{"value":"1446","name":"Comic Gotta"},{"value":"1113","name":"Comic Grape"},{"value":"1360","name":"Comic GT"},{"value":"33","name":"Comic Gum"},{"value":"558","name":"Comic Hanaman"},{"value":"941","name":"Comic Heaven"},{"value":"122","name":"Comic High!"},{"value":"681","name":"Comic Hime-Sakura"},{"value":"643","name":"Comic Himedorobow"},{"value":"1011","name":"Comic Himekuri"},{"value":"1337","name":"Comic HJ Bunko"},{"value":"1147","name":"Comic Holic"},{"value":"258","name":"Comic Hotmilk"},{"value":"963","name":"Comic ino."},{"value":"1279","name":"Comic it"},{"value":"1350","name":"Comic Jidai Katsugeki"},{"value":"1331","name":"Comic JSCK"},{"value":"995","name":"Comic Jumbo"},{"value":"631","name":"Comic June"},{"value":"533","name":"Comic Kairaku-ten"},{"value":"581","name":"Comic Kairaku-ten Beast"},{"value":"875","name":"Comic Kairaku-ten XTC"},{"value":"1382","name":"Comic Kairakuten Hoshi-gumi"},{"value":"671","name":"Comic Karyou Gakuen"},{"value":"1083","name":"Comic KOH"},{"value":"1357","name":"Comic KURiBERON"},{"value":"523","name":"Comic Kwai"},{"value":"1055","name":"Comic Lemon Club"},{"value":"348","name":"Comic LO"},{"value":"396","name":"Comic Magazine LYNX"},{"value":"993","name":"Comic Magnum"},{"value":"1249","name":"Comic Magnum X"},{"value":"398","name":"Comic Mahou no iLand"},{"value":"981","name":"Comic Maihime Musou"},{"value":"1211","name":"Comic Man-Ten"},{"value":"1321","name":"Comic Mana"},{"value":"552","name":"Comic Manga Ou"},{"value":"350","name":"Comic Marble"},{"value":"406","name":"Comic Master"},{"value":"452","name":"Comic Masyo"},{"value":"973","name":"Comic Mate"},{"value":"1318","name":"Comic Mate Legend"},{"value":"1462","name":"Comic MeDu"},{"value":"1319","name":"Comic Mega GOLD"},{"value":"639","name":"Comic MegaCube"},{"value":"585","name":"Comic MegaMilk"},{"value":"619","name":"Comic Megaplus"},{"value":"404","name":"Comic Megastore"},{"value":"1021","name":"Comic Megastore Alpha"},{"value":"582","name":"Comic Megastore H"},{"value":"827","name":"Comic Meteor"},{"value":"617","name":"Comic MILF"},{"value":"957","name":"Comic Milk Purin"},{"value":"1081","name":"Comic Minimon"},{"value":"977","name":"Comic Moe Max"},{"value":"428","name":"Comic Momohime"},{"value":"1029","name":"Comic Moog"},{"value":"1051","name":"Comic Muga"},{"value":"1053","name":"Comic Mugen Tensei"},{"value":"555","name":"Comic Mujin"},{"value":"1371","name":"Comic Newtype"},{"value":"508","name":"Comic NORA"},{"value":"1394","name":"Comic Orange Club"},{"value":"1283","name":"Comic Orca"},{"value":"965","name":"Comic Orecano!"},{"value":"633","name":"Comic P Flirt"},{"value":"661","name":"Comic Papipo"},{"value":"1453","name":"Comic PASH!"},{"value":"717","name":"Comic Penguin Celeb"},{"value":"464","name":"Comic Penguin Club"},{"value":"595","name":"Comic Penguin Club Sanzokuban"},{"value":"623","name":"Comic Plum"},{"value":"943","name":"Comic Polaris"},{"value":"1383","name":"Comic POOL"},{"value":"649","name":"Comic Pot"},{"value":"487","name":"Comic Potpourri Club"},{"value":"959","name":"Comic Prism"},{"value":"601","name":"Comic Purumelo"},{"value":"1117","name":"Comic Pururun Max"},{"value":"1300","name":"Comic Ran"},{"value":"1123","name":"Comic Ran Twins"},{"value":"1203","name":"Comic Ran-Oh!"},{"value":"987","name":"Comic Rats"},{"value":"1017","name":"Comic Revolution"},{"value":"93","name":"Comic REX"},{"value":"1373","name":"Comic Ride"},{"value":"579","name":"Comic RiN"},{"value":"1355","name":"Comic Rise"},{"value":"1297","name":"Comic Ruelle"},{"value":"175","name":"Comic Rush"},{"value":"410","name":"Comic Ryu"},{"value":"1391","name":"Comic Ryu Web"},{"value":"270","name":"Comic Sai"},{"value":"1320","name":"Comic Saija"},{"value":"525","name":"Comic Sangokushi"},{"value":"1379","name":"Comic saseco"},{"value":"268","name":"Comic Seed!"},{"value":"495","name":"Comic Shingeki"},{"value":"589","name":"Comic Shitsuraku-ten"},{"value":"1027","name":"Comic Shoujo Tengoku"},{"value":"659","name":"Comic Sigma"},{"value":"961","name":"Comic Situation Play"},{"value":"399","name":"Comic Spica"},{"value":"543","name":"Comic Sumomo"},{"value":"328","name":"Comic Tenma"},{"value":"1149","name":"Comic Tokumori"},{"value":"520","name":"Comic Tom (Monthly)"},{"value":"521","name":"Comic Tom Plus"},{"value":"1274","name":"Comic Toutetsu"},{"value":"569","name":"Comic Unreal"},{"value":"234","name":"Comic Valkyrie"},{"value":"955","name":"Comic X-Eros"},{"value":"609","name":"Comic XO"},{"value":"518","name":"Comic YA!"},{"value":"584","name":"Comic Yell!"},{"value":"423","name":"Comic Yuri Hime"},{"value":"417","name":"Comic Yuri Hime S"},{"value":"719","name":"Comic Zenon"},{"value":"110","name":"Comic ZERO-SUM"},{"value":"1330","name":"Comic Zeroshiki"},{"value":"930","name":"Comic Zip"},{"value":"605","name":"Comic@Bunch"},{"value":"271","name":"Comic@loid"},{"value":"1445","name":"Comicawa"},{"value":"1389","name":"Comicloud"},{"value":"1278","name":"comico"},{"value":"1280","name":"Comicomi"},{"value":"1159","name":"ComicWalker"},{"value":"104","name":"Comique Hug"},{"value":"112","name":"Comp Ace"},{"value":"38","name":"Comptiq"},{"value":"15","name":"Cookie"},{"value":"314","name":"Cookie BOX"},{"value":"1372","name":"CoroCoro Aniki"},{"value":"213","name":"CoroCoro Comic"},{"value":"197","name":"Cotton Comic"},{"value":"188","name":"Craft"},{"value":"304","name":"Cutie Comic"},{"value":"1257","name":"Cyber Comics"},{"value":"1185","name":"Cyberia ManiaEX"},{"value":"1417","name":"Cycomics"},{"value":"1461","name":"D Morning"},{"value":"1411","name":"Da Vinci"},{"value":"203","name":"Daito Comics Boys Love"},{"value":"180","name":"Daiwon"},{"value":"192","name":"Daria"},{"value":"845","name":"Daum Webtoon"},{"value":"99","name":"Dear+"},{"value":"232","name":"Deluxe Betsucomi"},{"value":"255","name":"Deluxe Margaret"},{"value":"1325","name":"Dengeki Bazooka!!"},{"value":"140","name":"Dengeki Bunko Magazine"},{"value":"18","name":"Dengeki Comic Gao!"},{"value":"799","name":"Dengeki Comic Japan"},{"value":"342","name":"Dengeki D-manga Online"},{"value":"23","name":"Dengeki Daioh"},{"value":"494","name":"Dengeki Daioh Genesis"},{"value":"273","name":"Dengeki Daioh Web Comic"},{"value":"1157","name":"Dengeki G's Comic"},{"value":"300","name":"Dengeki G's Festival! Comic"},{"value":"198","name":"Dengeki G's magazine"},{"value":"1167","name":"Dengeki Girl's Style"},{"value":"522","name":"Dengeki Hobby Magazine"},{"value":"1304","name":"Dengeki hp"},{"value":"318","name":"Dengeki Kuro Maoh"},{"value":"163","name":"Dengeki Maoh"},{"value":"737","name":"Dengeki Moeoh"},{"value":"476","name":"Dengeki Nintendo DS"},{"value":"402","name":"Dengeki PlayStation"},{"value":"453","name":"Dengeki Teioh"},{"value":"202","name":"Denshi Birz"},{"value":"95","name":"Dessert"},{"value":"351","name":"Doki~!"},{"value":"1115","name":"Doki~! Special"},{"value":"1401","name":"Doku Ringo Comic"},{"value":"1390","name":"Dra-Dra-Dragon Age"},{"value":"98","name":"Dragon Age"},{"value":"1426","name":"Dragon Age Extra"},{"value":"260","name":"Dragon Age Pure"},{"value":"64","name":"Dragon Junior"},{"value":"312","name":"Dragon Magazine"},{"value":"465","name":"Dragon Youth"},{"value":"156","name":"drap"},{"value":"1199","name":"drap Milk"},{"value":"1245","name":"DVD Majiyaba"},{"value":"1268","name":"e Young Magazine"},{"value":"513","name":"E★2"},{"value":"214","name":"E★Everystar"},{"value":"1362","name":"eBigComic4"},{"value":"725","name":"Edith"},{"value":"389","name":"Egoist"},{"value":"749","name":"Elegance Eve"},{"value":"1315","name":"Emerald"},{"value":"1406","name":"enigma"},{"value":"272","name":"Ergo"},{"value":"171","name":"Evening"},{"value":"26","name":"Eyes"},{"value":"1341","name":"Falcom Magazine"},{"value":"115","name":"Famitsu"},{"value":"839","name":"Famitsu Bros"},{"value":"532","name":"Famitsu Comic Clear"},{"value":"511","name":"Famitsu Playstation+"},{"value":"108","name":"FBonline"},{"value":"103","name":"Feel Young"},{"value":"324","name":"Fellows!"},{"value":"96","name":"Fig"},{"value":"276","name":"FlexComix Blood"},{"value":"715","name":"FlexComix Flare"},{"value":"440","name":"FlexComix Next"},{"value":"139","name":"Flower Comics Special"},{"value":"121","name":"Flowers (Monthly)"},{"value":"621","name":"For Mrs."},{"value":"1129","name":"Fresh Jump"},{"value":"190","name":"Friend"},{"value":"19","name":"From Gamers"},{"value":"1045","name":"Futabasha Web Magazine"},{"value":"1119","name":"G'sister"},{"value":"613","name":"G-men"},{"value":"932","name":"GA Bunko Magazine"},{"value":"1430","name":"Gachicomi"},{"value":"1398","name":"Galette"},{"value":"412","name":"Gangan Joker"},{"value":"419","name":"Gangan Online"},{"value":"1419","name":"Gangan pixiv"},{"value":"245","name":"Gangan Powered"},{"value":"939","name":"Gangan Wing"},{"value":"1339","name":"Ganma!"},{"value":"1324","name":"Garaku no Mori"},{"value":"250","name":"Garo"},{"value":"516","name":"gateau"},{"value":"353","name":"Gekiman Special"},{"value":"279","name":"Gekkan! Spirits"},{"value":"1436","name":"Genbun Magazine"},{"value":"1301","name":"Gene pixiv"},{"value":"435","name":"Generous Kiss"},{"value":"1105","name":"Gensou Fantasy"},{"value":"107","name":"Genzo"},{"value":"591","name":"Gessan"},{"value":"35","name":"GFantasy"},{"value":"791","name":"Girls forM"},{"value":"76","name":"Go Go Bunch"},{"value":"357","name":"GoKinjo Scandal"},{"value":"316","name":"good! Afternoon"},{"value":"1329","name":"Goraku Egg"},{"value":"237","name":"Gothic & Lolita Bible"},{"value":"1313","name":"Gougai onBLUE"},{"value":"1151","name":"Graman Geki!"},{"value":"947","name":"Grand Jump"},{"value":"889","name":"Grand Jump Premium"},{"value":"68","name":"Gundam Ace"},{"value":"131","name":"Gush"},{"value":"924","name":"GUSH pêche"},{"value":"1223","name":"GUSH Pochi."},{"value":"178","name":"Gust"},{"value":"1323","name":"Hana LaLa online"},{"value":"21","name":"Hana to Yume"},{"value":"729","name":"Hana to Yume Online"},{"value":"1191","name":"Hana to Yume Planet"},{"value":"1071","name":"Hana to Yume Plus"},{"value":"436","name":"Hanamaru"},{"value":"1247","name":"Hanamaru Manga"},{"value":"138","name":"Hanaoto"},{"value":"480","name":"Hanaoto DX"},{"value":"1189","name":"Handy Comic"},{"value":"248","name":"Harlequin"},{"value":"1314","name":"Harmony Romance"},{"value":"1095","name":"Harmony Romance Zoukangou"},{"value":"953","name":"Harta"},{"value":"1023","name":"haruca"},{"value":"1141","name":"Hatsu Kiss"},{"value":"881","name":"Heros"},{"value":"219","name":"HertZ"},{"value":"1295","name":"HiBaNa"},{"value":"1041","name":"Hinakan Hi!"},{"value":"811","name":"Hirari,"},{"value":"507","name":"Hitomi"},{"value":"928","name":"Hobby Japan"},{"value":"1414","name":"Honey Milk"},{"value":"901","name":"Hontou ni Atta Kowai Hanashi"},{"value":"362","name":"Hontou ni Atta Yukai na Hanashi"},{"value":"1075","name":"Hontou ni Kowai Douwa"},{"value":"106","name":"Horror Comics Special"},{"value":"534","name":"Horror M"},{"value":"1423","name":"Hug pixiv"},{"value":"261","name":"Ichiban Suki"},{"value":"424","name":"Ichibansuki"},{"value":"471","name":"Ichijinsha Mobile"},{"value":"286","name":"Ichiraci"},{"value":"1261","name":"iHertZ"},{"value":"1039","name":"ihr hertZ"},{"value":"136","name":"Ikki"},{"value":"327","name":"Infernal Boys"},{"value":"257","name":"IQ Jump"},{"value":"322","name":"Issue"},{"value":"564","name":"ITAN"},{"value":"524","name":"JC.COM"},{"value":"488","name":"Jidaigeki Manga JIN"},{"value":"1237","name":"Josei Seven"},{"value":"599","name":"Jour Suteki na Shufu-tachi"},{"value":"212","name":"Judy"},{"value":"1225","name":"Juicy"},{"value":"303","name":"Juliet"},{"value":"1393","name":"Jump Cross"},{"value":"1366","name":"Jump GIGA"},{"value":"1013","name":"Jump LIVE"},{"value":"1351","name":"Jump Original"},{"value":"161","name":"Jump SQ."},{"value":"675","name":"Jump SQ. LaB"},{"value":"484","name":"Jump SQ.19"},{"value":"1316","name":"Jump SQ.Crown"},{"value":"967","name":"Jump VS"},{"value":"567","name":"Jump X"},{"value":"645","name":"Jun-ai Kajitsu"},{"value":"695","name":"JUNE"},{"value":"130","name":"June Comics"},{"value":"145","name":"June Comics Piace Series"},{"value":"94","name":"JuniorChamp"},{"value":"230","name":"Junk! Boy"},{"value":"1007","name":"Kadokawa Niconico Ace"},{"value":"1439","name":"Kakao"},{"value":"295","name":"Karen"},{"value":"651","name":"Karyou Gakuen Shotoubu"},{"value":"1310","name":"Karyou Sakuragumi Etsu"},{"value":"1326","name":"Keetai ShuuPlay"},{"value":"454","name":"KERA"},{"value":"201","name":"Kero-kero Ace"},{"value":"346","name":"KG"},{"value":"233","name":"Kibou no Tomo"},{"value":"123","name":"Kimi to Boku"},{"value":"367","name":"Kindai Mahjong"},{"value":"391","name":"Kindai Mahjong Gold"},{"value":"326","name":"Kindai Mahjong Original"},{"value":"1421","name":"Kindai Manga"},{"value":"462","name":"Kinniku Otoko"},{"value":"493","name":"Kirara 16"},{"value":"113","name":"Kiss"},{"value":"540","name":"Kiss PLUS"},{"value":"1087","name":"KiSSCA"},{"value":"1057","name":"Kissui"},{"value":"625","name":"Koi June"},{"value":"1285","name":"Kono Manga ga Sugoi! Web"},{"value":"781","name":"Koushoku Shounen"},{"value":"1420","name":"Ktoon"},{"value":"1443","name":"Kuaikan Manhua"},{"value":"1063","name":"Kurage Bunch"},{"value":"667","name":"Kuro LaLa"},{"value":"1221","name":"Kurofune Momo"},{"value":"447","name":"Kurofune Zero"},{"value":"11","name":"LaLa"},{"value":"135","name":"LaLa DX"},{"value":"1059","name":"LaLa Melody Online"},{"value":"393","name":"LaLa Special"},{"value":"425","name":"LC Mystery Comic"},{"value":"914","name":"Leed Comic"},{"value":"1294","name":"Lemon People"},{"value":"1273","name":"Lezhin Comics Webtoon"},{"value":"1431","name":"LiQulle"},{"value":"1440","name":"Love Coffre"},{"value":"1363","name":"Love Jossie"},{"value":"227","name":"Love Mission"},{"value":"1441","name":"Love Silky"},{"value":"1183","name":"Magalabo"},{"value":"1353","name":"Magazine Bang"},{"value":"126","name":"Magazine Be x Boy"},{"value":"1107","name":"Magazine Cyberia"},{"value":"116","name":"Magazine E-no"},{"value":"1272","name":"Magazine Fresh!"},{"value":"1335","name":"Magazine pocket"},{"value":"128","name":"Magazine Special"},{"value":"989","name":"Magazine WOoooo!"},{"value":"1069","name":"Magazine WOoooo! B-gumi"},{"value":"70","name":"Magazine-Z"},{"value":"1349","name":"MAGCOMI"},{"value":"445","name":"Magi-Cu"},{"value":"263","name":"Mandala"},{"value":"1033","name":"Manga 4-koma Palette"},{"value":"847","name":"Manga Action"},{"value":"1133","name":"Manga Ai! Hime"},{"value":"1047","name":"Manga Aiki"},{"value":"371","name":"Manga Allman"},{"value":"1293","name":"Manga Animec"},{"value":"655","name":"Manga Bangaichi"},{"value":"1131","name":"Manga Bon"},{"value":"1061","name":"Manga Box"},{"value":"375","name":"Manga Club"},{"value":"376","name":"Manga Club Original"},{"value":"1456","name":"Manga Cross"},{"value":"408","name":"Manga Erotics F"},{"value":"1265","name":"Manga Erotopia"},{"value":"920","name":"Manga Goccha"},{"value":"607","name":"Manga Goraku"},{"value":"689","name":"Manga Goraku dokuhon"},{"value":"1286","name":"Manga Goraku Nexter"},{"value":"1302","name":"Manga Goraku Special"},{"value":"1207","name":"Manga Grimm Douwa"},{"value":"377","name":"Manga Home"},{"value":"1101","name":"Manga Hot Milk"},{"value":"1231","name":"Manga Kisoutengai"},{"value":"1429","name":"Manga Kocchi"},{"value":"379","name":"Manga Life"},{"value":"1219","name":"Manga Life MOMO"},{"value":"380","name":"Manga Life Original"},{"value":"1235","name":"Manga Life STORIA"},{"value":"1413","name":"Manga Life STORIA Dash"},{"value":"823","name":"Manga Life Win"},{"value":"382","name":"Manga Pachinko Land"},{"value":"912","name":"Manga Palette Lite"},{"value":"1451","name":"Manga Park"},{"value":"301","name":"Manga Shounen"},{"value":"500","name":"Manga Sunday"},{"value":"355","name":"Manga Time"},{"value":"293","name":"Manga Time Dash!"},{"value":"358","name":"Manga Time Family"},{"value":"359","name":"Manga Time Jumbo"},{"value":"182","name":"Manga Time Kirara"},{"value":"361","name":"Manga Time Kirara Carat"},{"value":"365","name":"Manga Time Kirara Forward"},{"value":"363","name":"Manga Time Kirara MAX"},{"value":"935","name":"Manga Time Kirara Miracle!"},{"value":"1089","name":"Manga Time Kirara☆Magica"},{"value":"169","name":"Manga Time Lovely"},{"value":"292","name":"Manga Time Natural"},{"value":"356","name":"Manga Time Original"},{"value":"360","name":"Manga Time Special"},{"value":"893","name":"Manga Town"},{"value":"444","name":"Manga Twister"},{"value":"1399","name":"Manga UP!"},{"value":"1179","name":"Manga Zettai Manzoku"},{"value":"1361","name":"MangaONE"},{"value":"473","name":"MARBLE"},{"value":"17","name":"Margaret"},{"value":"1201","name":"Margaret BOOK Store!"},{"value":"218","name":"Marukatsu Famicom"},{"value":"1392","name":"Matogrosso"},{"value":"1290","name":"MC☆Axis"},{"value":"308","name":"Me"},{"value":"536","name":"Megami Magazine"},{"value":"1215","name":"Megastore"},{"value":"1127","name":"Mei"},{"value":"321","name":"Mellow Mellow"},{"value":"71","name":"Melody"},{"value":"385","name":"Melon Comic"},{"value":"1343","name":"Men's Action"},{"value":"971","name":"Men's GOLD"},{"value":"339","name":"Men's Young"},{"value":"1035","name":"Men's Young Special Ikazuchi"},{"value":"310","name":"Meng Meng Guan"},{"value":"498","name":"Mephisto"},{"value":"531","name":"MiChao"},{"value":"1282","name":"Mikosuri Hangekijou Bekkan"},{"value":"1356","name":"Mikosuri Hangekijou Kyonyuu-chan"},{"value":"1342","name":"MIKU-Pack"},{"value":"251","name":"Millefeui"},{"value":"1352","name":"mimi"},{"value":"1253","name":"Mink"},{"value":"691","name":"Miracle Jump"},{"value":"297","name":"Mist Magazine"},{"value":"433","name":"Mobile Flower"},{"value":"530","name":"Mobile Man"},{"value":"755","name":"moca"},{"value":"1093","name":"Model Graphix"},{"value":"437","name":"Mon Mon"},{"value":"1424","name":"Monster Comics"},{"value":"117","name":"Monthly Action"},{"value":"1243","name":"Monthly QooPA!"},{"value":"1270","name":"Monthly Shounen Magazine+"},{"value":"72","name":"Morning"},{"value":"415","name":"Morning KC"},{"value":"1311","name":"Morning Party Zoukan"},{"value":"345","name":"Morning Two"},{"value":"1125","name":"motto!"},{"value":"429","name":"Mr. Magazine"},{"value":"753","name":"Mugen Anthology Series"},{"value":"239","name":"Mugenkan"},{"value":"510","name":"Muteki Renai S*girl"},{"value":"167","name":"Mystery Bonita"},{"value":"20","name":"Nakayoshi"},{"value":"1239","name":"Nakayoshi Deluxe"},{"value":"547","name":"Nakayoshi Lovely"},{"value":"383","name":"Nakeru!! Onna no Kandou Jitsuwa"},{"value":"384","name":"Namaiki!"},{"value":"1077","name":"Nate Manhwa"},{"value":"673","name":"Naver Webtoon"},{"value":"381","name":"Neko Para"},{"value":"386","name":"Neko Para Plus"},{"value":"635","name":"Nekopanchi"},{"value":"505","name":"Nemesis"},{"value":"187","name":"Nemuki"},{"value":"597","name":"New Youth"},{"value":"31","name":"Newtype"},{"value":"653","name":"Newtype Ace"},{"value":"723","name":"Next Comic First"},{"value":"803","name":"Nico Nico Seiga"},{"value":"549","name":"Niconico Yuri Hime"},{"value":"458","name":"Nikutaiha"},{"value":"1338","name":"Nintendo Dream"},{"value":"246","name":"Novel Japan"},{"value":"439","name":"NyanType"},{"value":"390","name":"Office YOU"},{"value":"485","name":"Oh Super Jump"},{"value":"1458","name":"Ohta Web Comic"},{"value":"285","name":"Ohzora Shuppan"},{"value":"1387","name":"OK!COMIC"},{"value":"1344","name":"Omegaverse Project"},{"value":"821","name":"onBLUE"},{"value":"1412","name":"One More Kiss"},{"value":"1437","name":"One Pack Comic"},{"value":"394","name":"Opera"},{"value":"544","name":"Oto☆Nyan"},{"value":"1031","name":"Otoko no Ko Jidai"},{"value":"783","name":"Otome High!"},{"value":"899","name":"Oyajism"},{"value":"378","name":"Pachinko 777"},{"value":"416","name":"Party"},{"value":"1375","name":"Pasocom Magazine"},{"value":"949","name":"Persona Magazine"},{"value":"73","name":"Petit Comic"},{"value":"1345","name":"Petit Comic Zoukan"},{"value":"289","name":"Petit Flower"},{"value":"413","name":"Petit Princess"},{"value":"331","name":"Phryné"},{"value":"153","name":"Piace"},{"value":"1438","name":"Pianissimo"},{"value":"1386","name":"pixiv Comic"},{"value":"1442","name":"pixiv Essay"},{"value":"1428","name":"pixiv Heros"},{"value":"1425","name":"pixiv Sylph"},{"value":"546","name":"Play Comic"},{"value":"557","name":"pocopoco"},{"value":"1181","name":"Ponimaga"},{"value":"320","name":"Pre-Comic Bunbun"},{"value":"1091","name":"Prince (Quarterly)"},{"value":"74","name":"Princess"},{"value":"1450","name":"Princess (Gong Zhu Zhi)"},{"value":"166","name":"Princess Gold"},{"value":"427","name":"Pua Pua"},{"value":"1435","name":"Pucchigumi"},{"value":"469","name":"Pureri"},{"value":"1307","name":"Qpa"},{"value":"1333","name":"Qpano"},{"value":"325","name":"Quick Japan"},{"value":"332","name":"Racish"},{"value":"545","name":"Rakuen Le Paradis"},{"value":"148","name":"Rakuen Web Zoukan"},{"value":"184","name":"Reijin"},{"value":"951","name":"Reijin Bravo!"},{"value":"1402","name":"Reijin Uno!"},{"value":"1328","name":"Renai Hakusho Pastel"},{"value":"829","name":"Renai Love MAX"},{"value":"370","name":"Renai Paradise"},{"value":"372","name":"Renai Paradise Mini"},{"value":"975","name":"Renai Revolution"},{"value":"199","name":"Ribon Bikkuri"},{"value":"438","name":"Ribon Deluxe"},{"value":"9","name":"Ribon Magazine"},{"value":"254","name":"Ribon Mascot"},{"value":"165","name":"Ribon Original"},{"value":"703","name":"Ribon Special"},{"value":"409","name":"Rinka"},{"value":"1145","name":"Run Run"},{"value":"159","name":"RuTile"},{"value":"474","name":"RuTiLe Sweet"},{"value":"697","name":"Saikyou Jump"},{"value":"999","name":"Saizensen"},{"value":"711","name":"Sakura Hearts"},{"value":"172","name":"Samurai Ace"},{"value":"387","name":"Sapio"},{"value":"1193","name":"Sengoku Bushou Retsuden"},{"value":"341","name":"Serie Mystery"},{"value":"288","name":"Seventeen (Monthly)"},{"value":"256","name":"SF Adventure"},{"value":"291","name":"SF Magazine"},{"value":"1284","name":"Shincho 45"},{"value":"669","name":"Shiro LaLa"},{"value":"22","name":"Sho-Comi"},{"value":"211","name":"Sho-Comi Zoukan"},{"value":"143","name":"Shogakukan Books"},{"value":"79","name":"Shojo Beat"},{"value":"1336","name":"Shokuman"},{"value":"526","name":"Shonen Shojo Bokeno"},{"value":"397","name":"Shougaku Gonensei"},{"value":"918","name":"Shougaku Ninensei"},{"value":"284","name":"Shougaku Sannensei"},{"value":"855","name":"Shougaku Yonensei"},{"value":"663","name":"Shoujo Club"},{"value":"309","name":"Shoujo Friend"},{"value":"319","name":"Shoujo Teikoku"},{"value":"937","name":"Shounen"},{"value":"27","name":"Shounen Ace"},{"value":"463","name":"Shounen Big Comic"},{"value":"158","name":"Shounen Captain"},{"value":"206","name":"Shounen Champion"},{"value":"282","name":"Shounen Champion (Monthly)"},{"value":"527","name":"Shounen Club"},{"value":"1367","name":"Shounen Edgestar"},{"value":"1173","name":"Shounen Fang"},{"value":"1347","name":"Shounen Gaho"},{"value":"13","name":"Shounen Gangan"},{"value":"515","name":"Shounen Jets (Monthly)"},{"value":"129","name":"Shounen Jump (Monthly)"},{"value":"83","name":"Shounen Jump (Weekly)"},{"value":"548","name":"Shounen Jump NEXT!"},{"value":"1209","name":"Shounen Jump+"},{"value":"483","name":"Shounen King"},{"value":"48","name":"Shounen Magazine (Monthly)"},{"value":"8","name":"Shounen Magazine (Weekly)"},{"value":"1312","name":"Shounen Magazine Edge"},{"value":"1308","name":"Shounen Magazine R"},{"value":"244","name":"Shounen Rival"},{"value":"100","name":"Shounen Sirius"},{"value":"229","name":"Shounen Sunday"},{"value":"411","name":"Shounen Sunday Super"},{"value":"448","name":"Shounen Takarajima (Weekly)"},{"value":"985","name":"Shousetsu B-Boy"},{"value":"1292","name":"Shousetsu Chara"},{"value":"132","name":"Shousetsu Chocolat"},{"value":"1327","name":"Shousetsu Dear+"},{"value":"141","name":"Shousetsu June"},{"value":"1388","name":"Shousetsu Shinchou"},{"value":"1289","name":"Shuukan Bunshun"},{"value":"1334","name":"Shuukan Gendai"},{"value":"1380","name":"Shuukan Taishuu"},{"value":"1299","name":"Shuunin ga Yuku! Special"},{"value":"1396","name":"ShuuPlay News"},{"value":"157","name":"Shy"},{"value":"225","name":"Silky"},{"value":"1370","name":"South"},{"value":"1287","name":"SPA!"},{"value":"1395","name":"Spinel"},{"value":"1447","name":"Sports Today Webtoon"},{"value":"205","name":"Star☆Girls"},{"value":"42","name":"Stencil"},{"value":"431","name":"Studio Voice"},{"value":"420","name":"Sugar"},{"value":"1135","name":"Suiyoubi no Sirius"},{"value":"1444","name":"Suiyoubi wa Mattari Dash X Comic"},{"value":"374","name":"Suku Suku Paradise"},{"value":"40","name":"Sunday Gene-X"},{"value":"1410","name":"Sunday Webry"},{"value":"147","name":"Super Beboy Comics"},{"value":"693","name":"Super Dash & Go!"},{"value":"85","name":"Super Jump"},{"value":"86","name":"Super Manga Blast"},{"value":"373","name":"Super Pachisuro 777"},{"value":"512","name":"super Robot Magazine"},{"value":"366","name":"Suspiria"},{"value":"478","name":"Swan Magazine"},{"value":"421","name":"Sylph"},{"value":"311","name":"Tales of Magazine"},{"value":"727","name":"Tech Gian"},{"value":"343","name":"Televi Magazine"},{"value":"344","name":"Televi Magazine Zoukan Tele Manga Heros"},{"value":"1434","name":"Tencent Animation"},{"value":"922","name":"The Dessert"},{"value":"395","name":"The Hana to Yume"},{"value":"313","name":"The Margaret"},{"value":"329","name":"The Sneaker"},{"value":"1346","name":"The Sneaker Web"},{"value":"1165","name":"Tokusatsu Ace"},{"value":"290","name":"Tonari no Young Jump"},{"value":"470","name":"Tsubomi"},{"value":"29","name":"twi4"},{"value":"479","name":"TYPE-MOON Ace"},{"value":"933","name":"u17"},{"value":"25","name":"Ultra Jump"},{"value":"506","name":"Ultra Jump Egg"},{"value":"432","name":"Un Poco"},{"value":"1005","name":"Ura Sunday"},{"value":"1385","name":"uvu"},{"value":"168","name":"V-Jump"},{"value":"477","name":"Vanilla"},{"value":"369","name":"Vitaman"},{"value":"699","name":"Viva☆Tales of Magazine"},{"value":"538","name":"WAaI!"},{"value":"879","name":"WAaI! Mahalo"},{"value":"1397","name":"Web Bazooka"},{"value":"305","name":"Web Comic Action"},{"value":"565","name":"Web Comic Beat's"},{"value":"587","name":"Web Comic EDEN"},{"value":"1103","name":"Web Comic Gamma"},{"value":"1452","name":"Web Comic Gamma+"},{"value":"615","name":"Web Comic Gekkin"},{"value":"5","name":"Web Comic Gum"},{"value":"401","name":"Web Comic High!"},{"value":"1139","name":"Web Comic Zenyon"},{"value":"1079","name":"Web Ikipara Comic"},{"value":"1369","name":"Web Magazine f3"},{"value":"489","name":"Web Magazine Wings"},{"value":"637","name":"Web Spica"},{"value":"1381","name":"WEBgateau"},{"value":"1332","name":"WEBLink"},{"value":"945","name":"Weekly ASCII"},{"value":"354","name":"Weekly Manga Times"},{"value":"173","name":"Weekly Playboy"},{"value":"45","name":"Wings"},{"value":"134","name":"Wink"},{"value":"352","name":"Woman Gekijou"},{"value":"277","name":"Wonderland"},{"value":"1097","name":"Yawaraka Spirits"},{"value":"459","name":"Yomban"},{"value":"539","name":"Yomiuri Shimbun"},{"value":"307","name":"YOU"},{"value":"403","name":"Young Ace"},{"value":"1340","name":"Young Ace UP"},{"value":"2","name":"Young Animal"},{"value":"118","name":"Young Animal Arashi"},{"value":"1121","name":"Young Animal Densi"},{"value":"560","name":"Young Animal Island"},{"value":"926","name":"Young Champ"},{"value":"47","name":"Young Champion Magazine"},{"value":"216","name":"Young Champion Retsu"},{"value":"451","name":"Young Comic"},{"value":"24","name":"Young Gangan"},{"value":"1067","name":"Young Hip"},{"value":"87","name":"Young Jump"},{"value":"535","name":"Young Jump (Monthly)"},{"value":"575","name":"Young King"},{"value":"88","name":"Young King OURs"},{"value":"46","name":"Young King OURs GH"},{"value":"1187","name":"Young King OURs Lite"},{"value":"265","name":"Young King OURs+"},{"value":"6","name":"Young Magazine (Monthly)"},{"value":"10","name":"Young Magazine (Weekly)"},{"value":"1195","name":"Young Magazine the 3rd"},{"value":"120","name":"Young Magazine Uppers"},{"value":"1427","name":"Young Magazine Zoukan Aka Buta"},{"value":"455","name":"Young Manga"},{"value":"89","name":"Young Sunday (Weekly)"},{"value":"90","name":"Young You"},{"value":"1454","name":"Yuri Hime@pixiv"},{"value":"247","name":"Yuri Shimai"},{"value":"335","name":"Yuri☆Koi"},{"value":"1448","name":"Yuruyon"},{"value":"490","name":"ZERO"},{"value":"701","name":"Zero-Sum Online"},{"value":"177","name":"Zero-Sum WARD"},{"value":"1241","name":"Zettai Renai Sweet"},{"value":"16","name":"Zipper"},{"value":"1281","name":"Zoukan flowers"},{"value":"1354","name":"Zoukan Taishuu"},{"value":"1161","name":"Zoukan Young Gangan"}] -------------------------------------------------------------------------------- /src/seasons.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | 4 | const SEASON_URI = 'https://myanimelist.net/anime/season/' 5 | const maxYear = 1901 + (new Date()).getYear() 6 | const possibleSeasons = { 7 | winter: 1, 8 | spring: 1, 9 | summer: 1, 10 | fall: 1 11 | } 12 | 13 | const type2Class = { 14 | TV: 1, 15 | TVNew: 1, 16 | TVCon: 1, 17 | OVAs: 2, 18 | Movies: 3, 19 | Specials: 4, 20 | ONAs: 5 21 | } 22 | 23 | const possibleTypes = Object.keys(type2Class) 24 | const possibleTV = { 25 | TVNew: 'TV (New)', 26 | TVCon: 'TV (Continuing)' 27 | } 28 | 29 | const getType = (type, $) => { 30 | const result = [] 31 | const typeString = possibleTypes.find((_type) => type === _type) 32 | 33 | let classToSearch = `.js-seasonal-anime-list-key-${type2Class[typeString]} .seasonal-anime.js-seasonal-anime` 34 | const typeClass = `.js-seasonal-anime-list-key-${type2Class[typeString]}` 35 | 36 | // If TVNew or TVCon are selected, filter them out to the specific elements on page 37 | if (Object.keys(possibleTV).includes(typeString)) { 38 | const tvType = possibleTV[typeString] 39 | 40 | $(typeClass).children('.anime-header').each(function () { 41 | if ($(this).text() === tvType) { 42 | classToSearch = $(this) 43 | .parent() 44 | .children() 45 | .filter(function () { return $(this).hasClass('seasonal-anime') }) 46 | } 47 | }) 48 | } 49 | 50 | $(classToSearch).each(function () { 51 | // For obvious reasons (or not) 52 | if ($(this).hasClass('kids') || $(this).hasClass('r18')) return 53 | 54 | const general = $(this).find('div:nth-child(1)') 55 | const picture = $(this).find('.image').find('img') 56 | const prod = $(this).find('.prodsrc') 57 | const info = $(this).find('.information') 58 | const synopsis = $(this).find('.synopsis') 59 | 60 | result.push({ 61 | picture: picture.attr(picture.hasClass('lazyload') ? 'data-src' : 'src'), 62 | synopsis: synopsis.find('span').text().trim(), 63 | licensor: synopsis.find('p').attr('data-licensors') ? synopsis.find('p').attr('data-licensors').slice(0, -1) : '', 64 | title: general.find('.title').find('h2 a').text().trim(), 65 | link: general.find('.title').find('a').attr('href') ? general.find('.title').find('a').attr('href').replace('/video', '') : '', 66 | genres: general.find('.genres').find('.genres-inner').text().trim().split('\n \n '), 67 | producers: prod.find('.producer').text().trim().split(', '), 68 | fromType: prod.find('.source').text().trim(), 69 | nbEp: prod.find('.eps').find('a').text().trim().replace(' eps', ''), 70 | releaseDate: info.find('.info').find('span').text().trim(), 71 | score: info.find('.scormem').find('.score').text().trim(), 72 | members: info.find('.scormem').find('.member.fl-r').text().trim().replace(/,/g, '') 73 | }) 74 | }) 75 | 76 | return result 77 | } 78 | 79 | /** 80 | * Allows to gather seasonal information from myanimelist.net 81 | * @param {number|string} year - The year of the season you want to look up for. 82 | * @param {string} season - Can be either "winter", "spring", "summer" or "fall". 83 | * @param {string} type - The type of show you can search for, can be "TV", "TVNew", "TVCon", "ONAs", "OVAs", "Specials" or "Movies". 84 | */ 85 | const getSeasons = (year, season, type) => { 86 | return new Promise((resolve, reject) => { 87 | if (!possibleSeasons[season]) { 88 | reject(new Error('[Mal-Scraper]: Entered season does not match any existing season.')) 89 | return 90 | } 91 | if (!(year <= maxYear) || !(year >= 1917)) { 92 | reject(new Error(`[Mal-Scraper]: Year must be between 1917 and ${maxYear}.`)) 93 | return 94 | } 95 | 96 | const uri = `${SEASON_URI}${year}/${season}` 97 | 98 | axios.get(uri) 99 | .then(({ data }) => { 100 | const $ = cheerio.load(data) 101 | 102 | if (typeof type === 'undefined') { 103 | resolve({ 104 | TV: getType('TV', $), 105 | TVNew: getType('TVNew', $), 106 | TVCon: getType('TVCon', $), 107 | OVAs: getType('OVAs', $), 108 | ONAs: getType('ONAs', $), 109 | Movies: getType('Movies', $), 110 | Specials: getType('Specials', $) 111 | }) 112 | } else { 113 | if (!possibleTypes.includes(type)) { 114 | reject(new Error(`[Mal-Scraper]: Invalid type provided, Possible options are ${possibleTypes.join(', ')}`)) 115 | return 116 | } 117 | resolve(getType(type, $)) 118 | } 119 | }) 120 | .catch(/* istanbul ignore next */ (err) => { 121 | reject(err) 122 | }) 123 | }) 124 | } 125 | 126 | module.exports = getSeasons 127 | -------------------------------------------------------------------------------- /src/stats.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const { getResultsFromSearch } = require('./info.js') 4 | 5 | const BASE_URI = 'https://myanimelist.net/anime/' 6 | 7 | const malNumberToJsNumber = (malNumber) => { 8 | if (malNumber) { 9 | malNumber = malNumber.replace(/\D/gi, '') 10 | return Number(malNumber) 11 | } 12 | 13 | return 0 14 | } 15 | 16 | const parsePage = ($) => { 17 | const stats = $('#content table td:nth-child(2) .spaceit_pad') 18 | 19 | const summaryStats = stats.slice(0, 6) 20 | const scoreStats = stats.slice(6) 21 | const scoreStatsLength = scoreStats.length 22 | const result = {} 23 | 24 | summaryStats.each(function (elem) { 25 | $(this).find('span').remove() 26 | }) 27 | 28 | result.watching = malNumberToJsNumber($(summaryStats[0]).text()) 29 | result.completed = malNumberToJsNumber($(summaryStats[1]).text()) 30 | result.onHold = malNumberToJsNumber($(summaryStats[2]).text()) 31 | result.dropped = malNumberToJsNumber($(summaryStats[3]).text()) 32 | result.planToWatch = malNumberToJsNumber($(summaryStats[4]).text()) 33 | result.total = malNumberToJsNumber($(summaryStats[5]).text()) 34 | 35 | scoreStats.each(function (index) { 36 | result['score' + (scoreStatsLength - index)] = malNumberToJsNumber($(this).find('small').text()) 37 | }) 38 | 39 | return result 40 | } 41 | 42 | const searchPage = (url) => { 43 | return new Promise((resolve, reject) => { 44 | axios.get(url) 45 | .then(({ data }) => { 46 | const $ = cheerio.load(data) 47 | const res = parsePage($) 48 | resolve(res) 49 | }) 50 | .catch(/* istanbul ignore next */(err) => reject(err)) 51 | }) 52 | } 53 | 54 | const getStatsFromName = (name) => { 55 | return new Promise((resolve, reject) => { 56 | getResultsFromSearch(name) 57 | .then((items) => { 58 | const { url } = items[0] 59 | 60 | searchPage(`${encodeURI(url)}/stats`) 61 | .then((data) => resolve(data)) 62 | .catch(/* istanbul ignore next */(err) => reject(err)) 63 | }) 64 | .catch(/* istanbul ignore next */(err) => reject(err)) 65 | }) 66 | } 67 | 68 | const getStatsFromNameAndId = (id, name) => { 69 | return new Promise((resolve, reject) => { 70 | searchPage(`${BASE_URI}${id}/${encodeURI(name)}/stats`) 71 | .then((data) => resolve(data)) 72 | .catch(/* istanbul ignore next */(err) => reject(err)) 73 | }) 74 | } 75 | 76 | const getStats = (obj) => { 77 | return new Promise((resolve, reject) => { 78 | if (!obj) { 79 | reject(new Error('[Mal-Scraper]: No id nor name received.')) 80 | return 81 | } 82 | 83 | if (typeof obj === 'object' && !obj[0]) { 84 | const { id, name } = obj 85 | 86 | if (!id || !name || isNaN(+id) || typeof name !== 'string') { 87 | reject(new Error('[Mal-Scraper]: Malformed input. ID or name is malformed or missing.')) 88 | return 89 | } 90 | 91 | getStatsFromNameAndId(id, name) 92 | .then((data) => resolve(data)) 93 | .catch(/* istanbul ignore next */(err) => reject(err)) 94 | } else { 95 | getStatsFromName(obj) 96 | .then((data) => resolve(data)) 97 | .catch(/* istanbul ignore next */(err) => reject(err)) 98 | } 99 | }) 100 | } 101 | 102 | module.exports = { 103 | getStats 104 | } 105 | -------------------------------------------------------------------------------- /src/users.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | 4 | const BASE_URI = 'https://myanimelist.net/profile/' 5 | 6 | /** 7 | * funtction that makes a string in camelCase 8 | * @param str a string 9 | * @returns camelCase string 10 | */ 11 | function camelize (str) { 12 | return str.replace(/^\w|[A-Z]|\b\w/g, function (word, index) { 13 | return index === 0 ? word.toLowerCase() : word.toUpperCase() 14 | }).replace(/\s+/g, '') 15 | } 16 | 17 | /** 18 | * Method that it's used to add user's favorites 19 | * @param $ 20 | * @param res the result object 21 | * @param fav list of favourites 22 | * @param i integer for specific favorite 23 | */ 24 | const addFavorites = ($, res, fav, i) => { 25 | if ($(fav).text() !== '') { // check if there are no favorites 26 | const favs = [] 27 | fav.each(function () { 28 | favs.push($(this).text()) 29 | }) 30 | if (i === 1) { 31 | Object.assign(res, { favoriteAnime: favs }) 32 | } else if (i === 2) { 33 | Object.assign(res, { favoriteManga: favs }) 34 | } else if (i === 3) { 35 | Object.assign(res, { favoriteCharacters: favs }) 36 | } else { 37 | Object.assign(res, { favoritePeople: favs }) 38 | } 39 | } 40 | } 41 | 42 | /* the method that it's used in order to use to parse the page 43 | and get all the info we want 44 | */ 45 | const parsePage = ($, name) => { 46 | const pfp = $('#content .user-image img') // getting the profile picture page section 47 | const statusTitles = $('#content .user-profile .user-status-title') // getting the status titles page section 48 | const statusData = $('#content .user-profile .user-status-data') // getting the status data page section 49 | const result = [] // we will put here all the properties of the final object 50 | // pushing some basic properties and values 51 | Object.assign(result, { username: name }) 52 | Object.assign(result, { profilePictureLink: $(pfp).attr('data-src').trim() }) 53 | Object.assign(result, { lastOnline: $(statusData[0]).text() }) 54 | statusTitles.each(function (index, status) { 55 | if ($(status).text() === 'Gender' || $(status).text() === 'Birthday' || $(status).text() === 'Joined' || $(status).text() === 'Location') { 56 | Object.assign(result, { [camelize($(status).text())]: $(statusData[index]).text() }) 57 | } 58 | }) 59 | const bio = $('#content .profile-about-user .word-break') // getting the bio page section 60 | if ($(bio).text() !== '') { // check if there is no bio 61 | Object.assign(result, { Bio: $(bio).text().replace(/\n\n/g, '').trim().replace(/\n/g, ' ').trim() }) // trim the whitespaces and remove extra newlines 62 | } 63 | // getting the text of the stats page section 64 | const stats = $('#statistics .stat-score').text().replace(/\n\n/g, '').trim().replace(/\n/g, ' ').replace(/\s+/g, ' ').trim() 65 | // getting the words of the text 66 | const words = stats.split(' ') 67 | // pushing the right values 68 | Object.assign(result, { animeDays: words[1] }) 69 | Object.assign(result, { animeMeanScore: words[4] }) 70 | Object.assign(result, { mangaDays: words[6] }) 71 | Object.assign(result, { mangaMeanScore: words[9] }) 72 | /* 73 | getting and adding the user's favorites 74 | anime, manga, characters and people 75 | */ 76 | const FavoriteAnime = $('#anime_favorites .fs10') 77 | addFavorites($, result, FavoriteAnime, 1) 78 | const favMangas = $('#manga_favorites .fs10') 79 | addFavorites($, result, favMangas, 2) 80 | const favChars = $('#character_favorites .fs10') 81 | addFavorites($, result, favChars, 3) 82 | const favActors = $('.favmore .fs10') 83 | addFavorites($, result, favActors, 4) 84 | return result 85 | } 86 | 87 | const searchPage = (url, name) => { 88 | return new Promise((resolve, reject) => { 89 | axios.get(url) 90 | .then(({ data }) => { 91 | const $ = cheerio.load(data) 92 | const res = parsePage($, name) 93 | resolve(res) 94 | }) 95 | .catch(/* istanbul ignore next */(err) => reject(err)) 96 | }) 97 | } 98 | 99 | const getUserFromName = (name) => { 100 | return new Promise((resolve, reject) => { 101 | searchPage(`${BASE_URI}${name}`, name) 102 | .then((data) => resolve(data)) 103 | .catch(/* istanbul ignore next */(err) => reject(err)) 104 | }) 105 | } 106 | 107 | // wrapper method to check if @name is actually string 108 | const getUser = (name) => { 109 | return new Promise((resolve, reject) => { 110 | if (!name || typeof name !== 'string') { 111 | reject(new Error('[Mal-Scraper]: Malformed input. Name is malformed or missing.')) 112 | return 113 | } 114 | 115 | getUserFromName(name) 116 | .then((data) => resolve(data)) 117 | .catch(/* istanbul ignore next */(err) => reject(err)) 118 | }) 119 | } 120 | 121 | module.exports = { 122 | getUser 123 | } 124 | -------------------------------------------------------------------------------- /src/watchList.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const toCamelCase = (string) => { 4 | return string 5 | .split('_') 6 | .map((chunk, index) => index ? chunk.charAt(0).toUpperCase() + chunk.slice(1) : chunk) 7 | .join('') 8 | } 9 | 10 | const format = (obj) => { 11 | return Object.keys(obj) 12 | .reduce((acc, key) => { 13 | acc[toCamelCase(key)] = obj[key] 14 | 15 | return acc 16 | }, {}) 17 | } 18 | 19 | /** 20 | * Allows to retrieve a user's watch lists and stuff. 21 | * @param {string} user The name of the user. 22 | * @param {number} after How many results you already have. 23 | * @param {string} type Can be either 'anime' or 'manga' 24 | * @param {number} status Status in the user's watch list (completed, on-hold...) 25 | * 26 | * @returns {promise} 27 | */ 28 | 29 | const getWatchListFromUser = (user, after = 0, type = 'anime', status = 7) => { 30 | return new Promise((resolve, reject) => { 31 | if (!user) { 32 | reject(new Error('[Mal-Scraper]: No user received.')) 33 | return 34 | } 35 | 36 | axios.get(`https://myanimelist.net/${type}list/${user}/load.json`, { 37 | params: { 38 | offset: after, 39 | status 40 | } 41 | }) 42 | .then(({ data }) => { 43 | resolve(data.map(format)) 44 | }) 45 | .catch((err) => reject(err)) 46 | }) 47 | } 48 | 49 | module.exports = { 50 | getWatchListFromUser 51 | } 52 | -------------------------------------------------------------------------------- /test/episodes.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getEpisodesList } = require('../src') 3 | 4 | const drifers = { 5 | name: 'Drifters', 6 | id: 31339 7 | } 8 | 9 | const NS = { 10 | name: 'Naruto Shippuuden', 11 | id: 1735 12 | } 13 | 14 | test.beforeEach(async t => { 15 | await new Promise(resolve => setTimeout(resolve, 5000)) 16 | }) 17 | 18 | test('getEpisodesList returns the right number of episode for Naruto Shippuuden with ID and name', async t => { 19 | try { 20 | const data = await getEpisodesList({ 21 | name: NS.name, 22 | id: NS.id 23 | }) 24 | 25 | t.is(typeof data, 'object') 26 | t.is(data.length, 250) 27 | t.is(data[0].title, 'Homecoming') 28 | t.is(data[100].title, 'Painful Decision') 29 | } catch (e) { 30 | t.fail() 31 | } 32 | }) 33 | 34 | test('getEpisodesList returns an error if called with no arguments', async t => { 35 | try { 36 | await getEpisodesList() 37 | } catch (e) { 38 | t.true(e.message === '[Mal-Scraper]: No id nor name received.') 39 | } 40 | }) 41 | 42 | test('getEpisodesList returns the right number of episode for Drifters with ID and name', async t => { 43 | try { 44 | const data = await getEpisodesList({ 45 | name: drifers.name, 46 | id: drifers.id 47 | }) 48 | 49 | t.is(typeof data, 'object') 50 | t.is(data.length, 6) 51 | t.is(data[0].title, 'Fight Song') 52 | } catch (e) { 53 | t.fail() 54 | } 55 | }) 56 | 57 | test('getEpisodesList returns an error if called with malformed object', async t => { 58 | try { 59 | await getEpisodesList({ name: drifers }) 60 | } catch (e) { 61 | t.true(e.message === '[Mal-Scraper]: Malformed input. ID or name is malformed or missing.') 62 | } 63 | }) 64 | 65 | test('getEpisodesList returns the right number of episode for Drifters with name only', async t => { 66 | try { 67 | const data = await getEpisodesList(drifers.name) 68 | 69 | t.is(typeof data, 'object') 70 | t.is(data.length, 6) 71 | t.is(data[0].title, 'Fight Song') 72 | } catch (e) { 73 | t.fail() 74 | } 75 | }) 76 | -------------------------------------------------------------------------------- /test/info.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const nock = require('nock') 3 | const { getInfoFromName, getInfoFromURL, getResultsFromSearch } = require('../src') 4 | 5 | const name = 'Sakura Trick' 6 | const url = 'https://myanimelist.net/anime/20047/Sakura_Trick' 7 | 8 | test.beforeEach(async t => { 9 | await new Promise(resolve => setTimeout(resolve, 1500)) 10 | }) 11 | 12 | test('getInfoFromName returns null if no result - static tests', async t => { 13 | // Static tests 14 | nock('https://myanimelist.net') 15 | .get('/search/prefix.json?type=anime&keyword=l') 16 | .reply(200, { categories: [{ type: 'anime', items: [] }] }) 17 | 18 | try { 19 | const data = await getInfoFromName('l') 20 | t.is(data, null) 21 | 22 | nock.cleanAll() 23 | } catch (e) { 24 | t.fail() 25 | } 26 | }) 27 | 28 | test('getInfoFromURL returns valid information for a novel', async t => { 29 | try { 30 | const data = await getInfoFromURL('https://myanimelist.net/manga/21479/Sword_Art_Online') 31 | 32 | t.is(typeof data, 'object') 33 | t.is(data.id, 21479) 34 | t.is(data.title, 'Sword Art Online') 35 | t.is(data.englishTitle, 'Sword Art Online') 36 | t.is(data.japaneseTitle, 'ソードアート・オンライン') 37 | t.is(data.status, 'Publishing') 38 | t.is(data.authors[0], 'BUNBUN') 39 | t.is(data.authors[1], 'Kawahara, Reki') 40 | t.is(data.type, 'Light Novel') 41 | t.is(data.synonyms[0], 'S.A.O') 42 | t.is(data.synonyms[1], 'SAO') 43 | t.is(data.genres[0], 'Action') 44 | t.is(data.genres[1], 'Adventure') 45 | t.is(data.genres[2], 'Fantasy') 46 | t.is(data.genres[3], 'Romance') 47 | t.is(data.characters.length, 10) 48 | t.not(data.characters[0].link, undefined) 49 | t.not(data.characters[0].picture, undefined) 50 | t.is(data.characters[0].name, 'Kirigaya, Kazuto') 51 | t.is(data.characters[0].role, 'Main') 52 | t.is(data.staff, undefined) 53 | t.is(data.serialization, 'None') 54 | t.not(data.synopsis, undefined) 55 | t.not(data.picture, undefined) 56 | t.not(data.score, undefined) 57 | t.not(data.scoreStats, undefined) 58 | t.not(data.ranked, undefined) 59 | t.not(data.popularity, undefined) 60 | t.not(data.members, undefined) 61 | t.not(data.favorites, undefined) 62 | } catch (e) { 63 | t.fail() 64 | } 65 | }) 66 | 67 | test('getInfoFromURL returns valid information for an anime that has mix names', async t => { 68 | try { 69 | const data = await getInfoFromURL('https://myanimelist.net/anime/30654/Ansatsu_Kyoushitsu_2nd_Season') 70 | 71 | t.is(typeof data, 'object') 72 | t.is(data.id, 30654) 73 | t.is(data.title, 'Ansatsu Kyoushitsu 2nd Season') 74 | t.not(data.synopsis, undefined) 75 | t.not(data.picture, undefined) 76 | t.is(data.characters.length, 10) 77 | t.not(data.characters[0].link, undefined) 78 | t.not(data.characters[0].picture, undefined) 79 | t.is(data.characters[0].name, 'Koro-sensei') 80 | t.is(data.characters[0].role, 'Main') 81 | t.is(data.staff.length, 4) 82 | t.not(data.staff[0].link, undefined) 83 | t.not(data.staff[0].picture, undefined) 84 | t.is(data.staff[0].name, 'Cook, Justin') 85 | t.is(data.staff[0].role, 'Producer') 86 | t.not(data.trailer, undefined) 87 | t.is(data.englishTitle, 'Assassination Classroom Second Season') 88 | t.is(data.japaneseTitle, '暗殺教室 第2期') 89 | t.is(data.synonyms[0], 'Ansatsu Kyoushitsu Season 2') 90 | t.is(data.synonyms[1], 'Ansatsu Kyoushitsu Final Season') 91 | t.is(data.type, 'TV') 92 | t.is(data.episodes, '25') 93 | t.is(data.aired, 'Jan 8, 2016 to Jul 1, 2016') 94 | t.is(data.premiered, 'Winter 2016') 95 | t.is(data.broadcast, 'Fridays at 01:25 (JST)') 96 | t.is(data.producers.length, 7) 97 | t.is(data.producers[0], 'Dentsu') 98 | t.is(data.producers[1], 'Studio Hibari') 99 | t.is(data.producers[2], 'Fuji TV') 100 | t.is(data.studios[0], 'Lerche') 101 | t.is(data.source, 'Manga') 102 | t.is(data.duration, '23 min. per ep.') 103 | t.is(data.status, 'Finished Airing') 104 | t.is(data.rating, 'PG-13 - Teens 13 or older') 105 | t.is(data.genres.length, 2) 106 | t.is(data.genres[0], 'Action') 107 | t.is(data.genres[1], 'Comedy') 108 | t.not(data.score, undefined) 109 | t.not(data.scoreStats, undefined) 110 | t.not(data.ranked, undefined) 111 | t.not(data.popularity, undefined) 112 | t.not(data.members, undefined) 113 | t.not(data.favorites, undefined) 114 | } catch (e) { 115 | t.fail() 116 | } 117 | }) 118 | 119 | test('getInfoFromURL returns an error if invalid url', async t => { 120 | try { 121 | await getInfoFromURL('hello') 122 | } catch (e) { 123 | e.message.includes('Invalid Url') 124 | ? t.pass() 125 | : t.fail() 126 | } 127 | }) 128 | 129 | test('getInfoFromURL returns valid information', async t => { 130 | try { 131 | const data = await getInfoFromURL(url) 132 | 133 | t.is(typeof data, 'object') 134 | t.is(data.title, name) 135 | t.is(data.characters.length, 10) 136 | t.is(data.staff.length, 4) 137 | t.is(data.status, 'Finished Airing') 138 | t.is(data.studios[0], 'Studio Deen') 139 | t.not(data.picture, undefined) 140 | } catch (e) { 141 | t.fail() 142 | } 143 | }) 144 | 145 | test('getInfoFromName returns an error if invalid name', async t => { 146 | try { 147 | await getInfoFromName() 148 | } catch (e) { 149 | e.message.includes('Invalid name') 150 | ? t.pass() 151 | : t.fail() 152 | } 153 | }) 154 | 155 | test('getInfoFromName returns valid information', async t => { 156 | try { 157 | const data = await getInfoFromName(name) 158 | 159 | t.is(typeof data, 'object') 160 | t.is(data.title, name) 161 | t.is(data.characters.length, 10) 162 | t.is(data.staff.length, 4) 163 | t.is(data.status, 'Finished Airing') 164 | t.is(data.studios[0], 'Studio Deen') 165 | } catch (e) { 166 | t.fail() 167 | } 168 | }) 169 | 170 | test('getInfoFromName returns valid with not the best match', async t => { 171 | try { 172 | const data = await getInfoFromName(name, false) 173 | 174 | t.is(typeof data, 'object') 175 | t.is(data.title, name) 176 | t.is(data.characters.length, 10) 177 | t.is(data.staff.length, 4) 178 | t.is(data.status, 'Finished Airing') 179 | t.is(data.studios[0], 'Studio Deen') 180 | } catch (e) { 181 | t.fail() 182 | } 183 | }) 184 | 185 | test('getInfoFromName returns valid when best match gives no answer', async t => { 186 | try { 187 | const data = await getInfoFromName('gakkou gurashi', false) 188 | 189 | t.is(typeof data, 'object') 190 | t.is(data.title, 'Gakkougurashi!') 191 | t.is(data.url, 'https://myanimelist.net/anime/24765/Gakkougurashi') 192 | } catch (e) { 193 | t.fail() 194 | } 195 | }) 196 | 197 | test('getResultsFromSearch returns an error if invalid keyword', async t => { 198 | try { 199 | await getResultsFromSearch() 200 | } catch (e) { 201 | e.message.includes('[Mal-Scraper]') 202 | ? t.pass() 203 | : t.fail() 204 | } 205 | }) 206 | 207 | test('getResultsFromSearch returns a valid array', async t => { 208 | try { 209 | const data = await getResultsFromSearch(name) 210 | 211 | t.is(typeof data, 'object') 212 | t.is(data.length, 10) 213 | t.is(data[0].name, name) 214 | t.is(data[0].id, 20047) 215 | } catch (e) { 216 | t.fail() 217 | } 218 | }) 219 | -------------------------------------------------------------------------------- /test/news.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getNewsNoDetails } = require('../src') 3 | 4 | test.beforeEach(async t => { 5 | await new Promise(resolve => setTimeout(resolve, 1500)) 6 | }) 7 | 8 | test('getNewsNoDetails returns 160 news entries', async t => { 9 | try { 10 | const data = await getNewsNoDetails() 11 | 12 | t.is(data.length, 160) 13 | } catch (e) { 14 | console.log(e.message) 15 | t.fail() 16 | } 17 | }) 18 | 19 | test('getNewsNoDetails returns 24 news entries', async t => { 20 | try { 21 | const data = await getNewsNoDetails(24) 22 | 23 | t.is(data.length, 24) 24 | } catch (e) { 25 | console.log(e.message) 26 | t.fail() 27 | } 28 | }) 29 | 30 | test('getNewsNoDetails returns 18 news entries', async t => { 31 | try { 32 | const data = await getNewsNoDetails(18) 33 | 34 | t.is(data.length, 18) 35 | } catch (e) { 36 | console.log(e.message) 37 | t.fail() 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /test/pictures.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getPictures } = require('../src') 3 | 4 | const NS = { 5 | name: 'Ginga Eiyuu Densetsu', 6 | id: 820 7 | } 8 | 9 | test.beforeEach(async t => { 10 | await new Promise(resolve => setTimeout(resolve, 5000)) 11 | }) 12 | 13 | test('getPictures returns the pictures for Ginga Eiyuu Densetsu with ID and name', async t => { 14 | try { 15 | const data = await getPictures({ 16 | name: NS.name, 17 | id: NS.id 18 | }) 19 | 20 | t.is(typeof data, 'object') 21 | t.is(data[0].imageLink, 'https://cdn.myanimelist.net/images/anime/8/9568.jpg') 22 | t.is(data[1].imageLink, 'https://cdn.myanimelist.net/images/anime/13/13225.jpg') 23 | } catch (e) { 24 | t.fail() 25 | } 26 | }) 27 | 28 | test('getPictures returns the pictures for Ginga Eiyuu Densetsu with name only', async t => { 29 | try { 30 | const data = await getPictures(NS.name) 31 | 32 | t.is(typeof data, 'object') 33 | t.is(data[0].imageLink, 'https://cdn.myanimelist.net/images/anime/8/9568.jpg') 34 | t.is(data[1].imageLink, 'https://cdn.myanimelist.net/images/anime/13/13225.jpg') 35 | } catch (e) { 36 | t.fail() 37 | } 38 | }) 39 | 40 | test('getPictures returns an error if called with no arguments', async t => { 41 | try { 42 | await getPictures() 43 | } catch (e) { 44 | t.true(e.message === '[Mal-Scraper]: No id nor name received.') 45 | } 46 | }) 47 | 48 | test('getPictures returns an error if called with malformed object', async t => { 49 | try { 50 | await getPictures({ name: NS.name }) 51 | } catch (e) { 52 | t.true(e.message === '[Mal-Scraper]: Malformed input. ID or name is malformed or missing.') 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /test/recommendations.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getRecommendationsList } = require('../src') 3 | 4 | const NS = { 5 | name: 'Bleach', 6 | id: 269 7 | } 8 | 9 | test.beforeEach(async t => { 10 | await new Promise(resolve => setTimeout(resolve, 5000)) 11 | }) 12 | 13 | test('getRecommendationsList returns the recommendation for Bleach with ID and name', async t => { 14 | try { 15 | const data = await getRecommendationsList({ 16 | name: NS.name, 17 | id: NS.id 18 | }) 19 | 20 | t.is(typeof data, 'object') 21 | t.is(data[0].author, 'banglaCM') 22 | t.is(data[0].animeLink, 'https://myanimelist.net/anime/392/Yuu☆Yuu☆Hakusho') 23 | t.truthy(data[0].pictureImage) 24 | t.truthy(data[0].mainRecommendation) 25 | // t.is(data[1].author, 'nate23nate23') 26 | // t.is(data[1].anime, 'Naruto: Shippuuden') 27 | // t.is(data[2].author, 'xaynie') 28 | // t.is(data[2].anime, 'Naruto') 29 | // t.is(data[3].anime, 'Jujutsu Kaisen') 30 | } catch (e) { 31 | t.fail() 32 | } 33 | }) 34 | 35 | test('getRecommendationsList returns the stats for Bleach with name only', async t => { 36 | try { 37 | const data = await getRecommendationsList(NS.name) 38 | 39 | t.is(typeof data, 'object') 40 | t.is(data[0].author, 'Wolf48') 41 | t.is(data[0].animeLink, 'https://myanimelist.net/anime/4155/One_Piece_Film__Strong_World') 42 | t.truthy(data[0].pictureImage) 43 | t.truthy(data[0].mainRecommendation) 44 | t.is(data[1].author, 'supermegasonic') 45 | t.is(data[1].anime, 'Naruto: Shippuuden Movie 4 - The Lost Tower') 46 | } catch (e) { 47 | t.fail(e) 48 | } 49 | }) 50 | 51 | test('getRecommendationsList returns an error if called with no arguments', async t => { 52 | try { 53 | await getRecommendationsList() 54 | } catch (e) { 55 | t.true(e.message === '[Mal-Scraper]: No id nor name received.') 56 | } 57 | }) 58 | 59 | test('getRecommendationsList returns an error if called with malformed object', async t => { 60 | try { 61 | await getRecommendationsList({ name: NS.name }) 62 | } catch (e) { 63 | t.true(e.message === '[Mal-Scraper]: Malformed input. ID or name is malformed or missing.') 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /test/reviews.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getReviewsList } = require('../src') 3 | 4 | const NS = { 5 | name: 'Naruto Shippuuden', 6 | id: 1735 7 | } 8 | 9 | test.beforeEach(async t => { 10 | await new Promise(resolve => setTimeout(resolve, 5000)) 11 | }) 12 | 13 | test('getReviewsList returns the first review for Naruto Shippuuden with ID and name', async t => { 14 | try { 15 | const data = await getReviewsList({ 16 | name: NS.name, 17 | id: NS.id, 18 | limit: 40 19 | }) 20 | 21 | t.is(typeof data, 'object') 22 | t.is(data.length, 40) 23 | t.is(data[0].author, 'Xyik') 24 | t.is(data[0].overall, 6) 25 | t.is(data[0].story, 8) 26 | t.is(data[0].animation, 7) 27 | t.is(data[0].sound, 6) 28 | t.is(data[0].character, 8) 29 | t.is(data[0].enjoyment, 7) 30 | t.is(data[20].author, 'husa2004') 31 | t.is(data[20].overall, 10) 32 | t.is(data[20].story, 10) 33 | t.is(data[20].animation, 9) 34 | t.is(data[20].sound, 9) 35 | t.is(data[20].character, 9) 36 | t.is(data[20].enjoyment, 10) 37 | } catch (e) { 38 | t.fail() 39 | } 40 | }) 41 | 42 | test('getReviewsList returns the second reviews after the first page for Naruto Shippuuden with ID and name', async t => { 43 | try { 44 | const data = await getReviewsList({ 45 | name: NS.name, 46 | id: NS.id, 47 | limit: 1, 48 | skip: 21 49 | }) 50 | 51 | t.is(typeof data, 'object') 52 | t.is(data.length, 1) 53 | t.is(data[0].author, 'Bluthut') 54 | t.is(data[0].overall, 9) 55 | t.is(data[0].story, 9) 56 | t.is(data[0].animation, 8) 57 | t.is(data[0].sound, 9) 58 | t.is(data[0].character, 9) 59 | t.is(data[0].enjoyment, 10) 60 | } catch (e) { 61 | t.fail() 62 | } 63 | }) 64 | 65 | test('getReviewsList returns the 20 firsts review base on the name of the anime', async t => { 66 | try { 67 | const data = await getReviewsList({ 68 | name: NS.name, 69 | limit: 40 70 | }) 71 | 72 | t.is(typeof data, 'object') 73 | t.is(data.length, 40) 74 | t.is(data[0].author, 'Xyik') 75 | t.is(data[0].overall, 6) 76 | t.is(data[0].story, 8) 77 | t.is(data[0].animation, 7) 78 | t.is(data[0].sound, 6) 79 | t.is(data[0].character, 8) 80 | t.is(data[0].enjoyment, 7) 81 | t.is(data[20].author, 'husa2004') 82 | t.is(data[20].overall, 10) 83 | t.is(data[20].story, 10) 84 | t.is(data[20].animation, 9) 85 | t.is(data[20].sound, 9) 86 | t.is(data[20].character, 9) 87 | t.is(data[20].enjoyment, 10) 88 | } catch (e) { 89 | t.fail() 90 | } 91 | }) 92 | -------------------------------------------------------------------------------- /test/search.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { search } = require('../src') 3 | 4 | test.beforeEach(async t => { 5 | await new Promise(resolve => setTimeout(resolve, 5000)) 6 | }) 7 | 8 | test('Search.search Checking that the search return the correct number of element', async t => { 9 | try { 10 | const data = await search.search('anime', { 11 | term: 'Sakura', 12 | maxResults: 100 13 | }) 14 | 15 | t.is(typeof data, 'object') 16 | t.is(data.length, 100) 17 | } catch (e) { 18 | t.fail() 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /test/seasons.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getSeason } = require('../src') 3 | 4 | test.beforeEach(async t => { 5 | await new Promise(resolve => setTimeout(resolve, 1500)) 6 | }) 7 | 8 | test('getSeasons returns an error if not valid season', async t => { 9 | try { 10 | await getSeason(2017, 'bla') 11 | 12 | t.fail() 13 | } catch (e) { 14 | e.message.includes('existing season') 15 | ? t.pass() 16 | : t.fail() 17 | } 18 | }) 19 | 20 | test('getSeasons returns an error if not valid year', async t => { 21 | try { 22 | await getSeason((new Date()).getFullYear() + 3, 'fall') 23 | 24 | t.fail() 25 | } catch (e) { 26 | e.message.includes('Year must') 27 | ? t.pass() 28 | : t.fail() 29 | } 30 | }) 31 | 32 | test('getSeasons returns an error if not valid type', async t => { 33 | try { 34 | await getSeason(2017, 'fall', 'TVc') 35 | 36 | t.fail() 37 | } catch (e) { 38 | e.message.includes('[Mal-Scraper]: Invalid type provided') 39 | ? t.pass() 40 | : t.fail() 41 | } 42 | }) 43 | 44 | test('getSeasons with type TV returns the correct season', async t => { 45 | try { 46 | const data = await getSeason(2017, 'fall', 'TV') 47 | 48 | t.is(data.length, 93) 49 | t.is(data[0].title, 'Black Clover') 50 | } catch (e) { 51 | console.log(e.message) 52 | t.fail() 53 | } 54 | }) 55 | 56 | test('getSeasons with type TVNew returns the correct season', async t => { 57 | try { 58 | const data = await getSeason(2017, 'fall', 'TVNew') 59 | 60 | t.is(data.length, 57) 61 | t.is(data[0].title, 'Black Clover') 62 | } catch (e) { 63 | console.log(e.message) 64 | t.fail() 65 | } 66 | }) 67 | 68 | test('getSeasons with type TVCon returns the correct season', async t => { 69 | try { 70 | const data = await getSeason(2017, 'fall', 'TVCon') 71 | 72 | t.is(data.length, 36) 73 | t.is(data[0].title, 'One Piece') 74 | } catch (e) { 75 | console.log(e.message) 76 | t.fail() 77 | } 78 | }) 79 | 80 | test('getSeasons with type ONAs returns the correct season', async t => { 81 | try { 82 | const data = await getSeason(2017, 'fall', 'ONAs') 83 | 84 | t.is(data.length, 71) 85 | t.is(data[0].title, 'Yi Ren Zhi Xia 2') 86 | } catch (e) { 87 | console.log(e.message) 88 | t.fail() 89 | } 90 | }) 91 | 92 | test('getSeasons with type OVAs returns the correct season', async t => { 93 | try { 94 | const data = await getSeason(2017, 'fall', 'OVAs') 95 | 96 | t.is(data.length, 11) 97 | t.is(data[0].title, 'Shingeki no Kyojin: Lost Girls') 98 | } catch (e) { 99 | console.log(e.message) 100 | t.fail() 101 | } 102 | }) 103 | 104 | test('getSeasons with type returns the correct season', async t => { 105 | try { 106 | const data = await getSeason(2017, 'fall', 'Specials') 107 | 108 | t.is(data.length, 30) 109 | t.is(data[0].title, 'Net-juu no Susume Special') 110 | } catch (e) { 111 | console.log(e.message) 112 | t.fail() 113 | } 114 | }) 115 | 116 | test('getSeasons with type Movies returns the correct season', async t => { 117 | try { 118 | const data = await getSeason(2017, 'fall', 'Movies') 119 | 120 | t.is(data.length, 27) 121 | t.is(data[0].title, 'Fate/stay night Movie: Heaven\'s Feel - I. Presage Flower') 122 | } catch (e) { 123 | console.log(e.message) 124 | t.fail() 125 | } 126 | }) 127 | 128 | test('getSeasons returns the right season', async t => { 129 | try { 130 | const data = await getSeason(2017, 'fall') 131 | 132 | t.is(typeof data.TV, 'object') 133 | t.is(typeof data.TVNew, 'object') 134 | t.is(typeof data.TVCon, 'object') 135 | t.is(typeof data.OVAs, 'object') 136 | t.is(typeof data.Movies, 'object') 137 | t.is(data.TV.length, 93) 138 | t.is(data.TVNew.length, 57) 139 | t.is(data.TVCon.length, 36) 140 | t.is(data.OVAs.length, 11) 141 | t.is(data.Movies.length, 27) 142 | t.is(data.TV[0].title, 'Black Clover') 143 | t.is(data.TVNew[0].title, 'Black Clover') 144 | t.is(data.TVCon[0].title, 'One Piece') 145 | } catch (e) { 146 | t.fail(e) 147 | } 148 | }) 149 | -------------------------------------------------------------------------------- /test/stats.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getStats } = require('../src') 3 | 4 | const NS = { 5 | name: 'Ginga Eiyuu Densetsu', 6 | id: 820 7 | } 8 | 9 | test.beforeEach(async t => { 10 | await new Promise(resolve => setTimeout(resolve, 5000)) 11 | }) 12 | 13 | test('getStats returns the stats for Ginga Eiyuu Densetsu with ID and name', async t => { 14 | try { 15 | const data = await getStats({ 16 | name: NS.name, 17 | id: NS.id 18 | }) 19 | 20 | t.is(typeof data, 'object') 21 | t.is(typeof data.watching, 'number') 22 | t.is(typeof data.completed, 'number') 23 | t.is(typeof data.onHold, 'number') 24 | t.is(typeof data.dropped, 'number') 25 | t.is(typeof data.planToWatch, 'number') 26 | t.is(typeof data.total, 'number') 27 | t.is(typeof data.score10, 'number') 28 | t.is(typeof data.score1, 'number') 29 | t.assert(data.watching > 23000) 30 | t.assert(data.completed > 47000) 31 | t.assert(data.onHold > 13000) 32 | t.assert(data.dropped > 5000) 33 | t.assert(data.planToWatch > 114000) 34 | t.assert(data.total > 204000) 35 | t.assert(data.score10 > 27000) 36 | t.assert(data.score1 > 1700) 37 | } catch (e) { 38 | t.fail() 39 | } 40 | }) 41 | 42 | test('getStats returns the stats for Ginga Eiyuu Densetsu with name only', async t => { 43 | try { 44 | const data = await getStats(NS.name) 45 | 46 | t.is(typeof data, 'object') 47 | t.is(typeof data.watching, 'number') 48 | t.is(typeof data.completed, 'number') 49 | t.is(typeof data.onHold, 'number') 50 | t.is(typeof data.dropped, 'number') 51 | t.is(typeof data.planToWatch, 'number') 52 | t.is(typeof data.total, 'number') 53 | t.is(typeof data.score10, 'number') 54 | t.is(typeof data.score1, 'number') 55 | t.assert(data.watching > 23000) 56 | t.assert(data.completed > 47000) 57 | t.assert(data.onHold > 13000) 58 | t.assert(data.dropped > 5000) 59 | t.assert(data.planToWatch > 114000) 60 | t.assert(data.total > 204000) 61 | t.assert(data.score10 > 27000) 62 | t.assert(data.score1 > 1700) 63 | } catch (e) { 64 | t.fail() 65 | } 66 | }) 67 | 68 | test('getStats returns an error if called with no arguments', async t => { 69 | try { 70 | await getStats() 71 | } catch (e) { 72 | t.true(e.message === '[Mal-Scraper]: No id nor name received.') 73 | } 74 | }) 75 | 76 | test('getStats returns an error if called with malformed object', async t => { 77 | try { 78 | await getStats({ name: NS.name }) 79 | } catch (e) { 80 | t.true(e.message === '[Mal-Scraper]: Malformed input. ID or name is malformed or missing.') 81 | } 82 | }) 83 | -------------------------------------------------------------------------------- /test/users.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getUser } = require('../src') 3 | 4 | const NS = { 5 | name: 'Sebelius' 6 | } 7 | 8 | test.beforeEach(async t => { 9 | await new Promise(resolve => setTimeout(resolve, 5000)) 10 | }) 11 | 12 | test('getUser() returns the user for Sebelius with name only', async t => { 13 | try { 14 | const data = await getUser(NS.name) 15 | t.is(typeof data, 'object') 16 | t.is(data.username, 'Sebelius') 17 | t.is(data.gender, 'Male') 18 | t.is(data.birthday, 'Jun 16, 1964') 19 | t.is(data.location, 'Sri Lanka') 20 | t.is(data.joined, 'Feb 11, 2023') 21 | t.is(data.animeDays, '0.8') 22 | t.is(data.animeMeanScore, '10.00') 23 | t.is(data.mangaDays, '0.3') 24 | t.is(data.favoriteAnime[0], 'Toradora!') 25 | t.is(data.favoriteAnime[1], 'Eromanga-sensei') 26 | t.is(data.favoriteManga[0], 'Boruto: Naruto Next Generations') 27 | t.is(data.favoriteCharacters[0], 'All Might') 28 | t.is(data.favoriteCharacters[1], 'Ichinose, Takumi') 29 | t.is(data.favoritePeople[0], 'Aoyama, Yutaka') 30 | t.is(data.favoritePeople[1], 'Yamaguchi, Kappei') 31 | } catch (e) { 32 | t.fail() 33 | } 34 | }) 35 | 36 | // errors 37 | 38 | test('getUser returns an error if called with no arguments', async t => { 39 | try { 40 | await getUser() 41 | } catch (e) { 42 | t.true(e.message === '[Mal-Scraper]: Malformed input. Name is malformed or missing.') 43 | } 44 | }) 45 | 46 | test('getUser returns an error if called with malformed object', async t => { 47 | try { 48 | await getUser({ name: NS.name }) 49 | } catch (e) { 50 | t.true(e.message === '[Mal-Scraper]: Malformed input. Name is malformed or missing.') 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /test/watchList.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getWatchListFromUser } = require('../src') 3 | 4 | test.beforeEach(async t => { 5 | await new Promise(resolve => setTimeout(resolve, 1500)) 6 | }) 7 | 8 | test('getWachListFromUser returns an error if no user given', async t => { 9 | try { 10 | await getWatchListFromUser() 11 | 12 | t.fail() 13 | } catch (e) { 14 | e.message.includes('[Mal-Scraper]') 15 | ? t.pass() 16 | : t.fail() 17 | } 18 | }) 19 | 20 | test('getWatchListFromUser returns a valid array with entries', async t => { 21 | try { 22 | const data = await getWatchListFromUser('Kylart') 23 | 24 | t.is(typeof data, 'object') 25 | t.not(data.length, 0) 26 | } catch (e) { 27 | console.log(e.message) 28 | t.fail() 29 | } 30 | }) 31 | 32 | test('getWatchListFromUser returns an error if invalid user', async t => { 33 | try { 34 | await getWatchListFromUser('thisuserprollyDoesNotExist') 35 | 36 | t.fail() 37 | } catch (e) { 38 | t.is(e.response.status, 400) 39 | } 40 | }) 41 | --------------------------------------------------------------------------------