├── .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 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------