better-ani-scraped 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCUMENTATION.md +136 -54
- package/examples/animepahe/example_usage_searchAnime.js +10 -0
- package/examples/animesama/example_usage_getAllAnime.js +10 -0
- package/examples/animesama/example_usage_getAnimeInfo.js +11 -0
- package/examples/animesama/example_usage_getAvailableLanguages.js +11 -0
- package/examples/animesama/example_usage_getEmbed.js +11 -0
- package/examples/animesama/example_usage_getEpisodeTitles.js +11 -0
- package/examples/animesama/example_usage_getLatestEpisodes.js +10 -0
- package/examples/animesama/example_usage_getRandomAnime.js +10 -0
- package/examples/animesama/example_usage_getSeasons.js +11 -0
- package/examples/animesama/example_usage_getVideoUrlFromEmbed.js +11 -0
- package/examples/animesama/example_usage_searchAnime.js +10 -0
- package/examples/crunchyroll/example_usage_getEpisodeInfo.js +12 -0
- package/examples/crunchyroll/example_usage_searchAnime.js +11 -0
- package/package.json +6 -3
- package/scrapers/animesama.js +149 -106
- package/scrapers/crunchyroll.js +89 -0
- package/scrapers/scrapers.js +21 -9
- package/examples/example_usage_01.js +0 -23
- package/examples/example_usage_02.js +0 -14
- package/examples/example_usage_03.js +0 -9
- package/examples/example_usage_04.js +0 -13
package/DOCUMENTATION.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Ani-Scraped Documentation
|
|
1
|
+
# Better-Ani-Scraped Documentation
|
|
2
2
|
|
|
3
3
|
A set of utility functions for scraping anime data from multiple sources (only [anime-sama](https://anime-sama.fr) and [animepahe](https://animepahe.ru) available at the moment). This tool allows you to search for anime, retrieve information, get episodes, and more.
|
|
4
4
|
|
|
@@ -6,37 +6,45 @@ A set of utility functions for scraping anime data from multiple sources (only [
|
|
|
6
6
|
|
|
7
7
|
## Summary
|
|
8
8
|
- [Main class](#main-class)
|
|
9
|
-
- [`
|
|
10
|
-
- [`
|
|
11
|
-
- [
|
|
9
|
+
- [`AnimeScraper("animesama")` methods](#animescraperanimesama-methods)
|
|
10
|
+
- [`AnimeScraper("animepahe")` methods](#animescraperanimepahe-methods)
|
|
11
|
+
- [`AnimeScraper("crunchyroll")` methods](#animescrapercrunchyroll-methods)
|
|
12
|
+
- [Utility functions](#utility-functions)
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
## Main class
|
|
16
17
|
|
|
17
18
|
### `AnimeScraper(source)`
|
|
18
|
-
Creates a
|
|
19
|
+
Creates a scraper for the given source (only "animesama", "animepahe" and "crunchyroll" available at the moment).
|
|
20
|
+
```js
|
|
21
|
+
const animesama = new AnimeScraper('animesama') //for Anime Sama
|
|
22
|
+
const animepahe = new AnimeScraper('animepahe') //for Anime Pahe
|
|
23
|
+
const crunchyroll = new AnimeScraper('crunchyroll') //for Crunchyroll
|
|
24
|
+
```
|
|
19
25
|
|
|
20
26
|
---
|
|
21
27
|
|
|
22
|
-
## `
|
|
28
|
+
## `AnimeScraper("animesama")` methods
|
|
23
29
|
|
|
24
|
-
- [searchAnime](#
|
|
25
|
-
- [getSeasons](#
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
30
|
+
- [searchAnime](#animesamasearchanimequery-limit--10-wantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film)
|
|
31
|
+
- [getSeasons](#animesamagetseasonsanimeurl-language--vostfr)
|
|
32
|
+
- [getEpisodeTitles](#animesamagetepisodetitlesseasonurl-customchromiumpath)
|
|
33
|
+
- [getEmbed](#animesamagetembedseasonurl-hostpriority--sibnet-vidmoly)
|
|
34
|
+
- [getAnimeInfo](#animesamagetanimeinfoanimeurl)
|
|
35
|
+
- [getAvailableLanguages](#animesamagetavailablelanguagesseasonurl-wantedlanguages--vostfr-vf-va-vkr-vcn-vqc-vf1-vf2-numberepisodes--false)
|
|
36
|
+
- [getAllAnime](#animesamagetallanimewantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film-page--null-output--anime_listjson-get_seasons--false)
|
|
37
|
+
- [getLatestEpisodes](#animesamagetlatestepisodeslanguagefilter--null)
|
|
38
|
+
- [getRandomAnime](#animesamagetrandomanimewantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film-maxattempts--null-attempt--0)
|
|
33
39
|
|
|
34
|
-
### `searchAnime(query, limit = 10)`
|
|
40
|
+
### `animesama.searchAnime(query, limit = 10, wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"])`
|
|
35
41
|
Searches for anime titles that match the given query.
|
|
36
42
|
|
|
37
43
|
- **Parameters:**
|
|
38
44
|
- `query` *(string)*: The search keyword.
|
|
39
45
|
- `limit` *(number)*: Maximum number of results to return (default: 10).
|
|
46
|
+
- `wantedLanguages` *(string[])*: Array of wanted languages.
|
|
47
|
+
- `wantedTypes` *(string[])*: Array of wanted types.
|
|
40
48
|
- **Returns:**
|
|
41
49
|
An array of anime objects:
|
|
42
50
|
```js
|
|
@@ -54,7 +62,7 @@ Searches for anime titles that match the given query.
|
|
|
54
62
|
|
|
55
63
|
---
|
|
56
64
|
|
|
57
|
-
### `getSeasons(animeUrl, language = "vostfr")`
|
|
65
|
+
### `animesama.getSeasons(animeUrl, language = "vostfr")`
|
|
58
66
|
Fetches all available seasons of an anime in the specified language.
|
|
59
67
|
|
|
60
68
|
- **Parameters:**
|
|
@@ -75,23 +83,38 @@ Fetches all available seasons of an anime in the specified language.
|
|
|
75
83
|
|
|
76
84
|
---
|
|
77
85
|
|
|
78
|
-
### `
|
|
86
|
+
### `animesama.getEpisodeTitles(seasonUrl, customChromiumPath)`
|
|
87
|
+
Fetches the names of all episodes in a season
|
|
88
|
+
|
|
89
|
+
- **Parameters:**
|
|
90
|
+
- `seasonUrl` *(string)*: URL of the anime’s season page.
|
|
91
|
+
- `customChromiumPath` *(string)*: Path of the Chromium folder
|
|
92
|
+
- **Returns:**
|
|
93
|
+
An array of episode titles.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### `animesama.getEmbed(seasonUrl, hostPriority = ["sibnet", "vidmoly"])`
|
|
79
98
|
Retrieves embed URLs for episodes, prioritizing by host.
|
|
80
99
|
|
|
81
100
|
- **Parameters:**
|
|
82
|
-
- `
|
|
101
|
+
- `seasonUrl` *(string)*: URL of the anime’s season page.
|
|
83
102
|
- `hostPriority` *(string[])*: Array of preferred hostnames.
|
|
84
103
|
- **Returns:**
|
|
85
104
|
An array of embed video:
|
|
86
105
|
```js
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
[
|
|
107
|
+
{
|
|
108
|
+
title: string,
|
|
109
|
+
url: string,
|
|
110
|
+
}
|
|
111
|
+
...
|
|
112
|
+
]
|
|
113
|
+
|
|
91
114
|
```
|
|
92
115
|
---
|
|
93
116
|
|
|
94
|
-
### `getAnimeInfo(animeUrl)`
|
|
117
|
+
### `animesama.getAnimeInfo(animeUrl)`
|
|
95
118
|
Extracts basic information from an anime page.
|
|
96
119
|
|
|
97
120
|
- **Parameters:**
|
|
@@ -110,19 +133,20 @@ Extracts basic information from an anime page.
|
|
|
110
133
|
|
|
111
134
|
---
|
|
112
135
|
|
|
113
|
-
### `getAvailableLanguages(seasonUrl, wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc"])`
|
|
114
|
-
Checks which languages are available for a given anime season (
|
|
136
|
+
### `animesama.getAvailableLanguages(seasonUrl, wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc", "vf1", "vf2"], numberEpisodes = false)`
|
|
137
|
+
Checks which languages are available for a given anime season (Avoid using `numberEpisodes = true`, as checking many languages significantly increases execution time).
|
|
115
138
|
|
|
116
139
|
- **Parameters:**
|
|
117
140
|
- `seasonUrl` *(string)*: The season anime URL.
|
|
118
141
|
- `wantedLanguages` *(string[])*: Language codes to check (e.g., ["vostfr", "vf", "va", ...]).
|
|
142
|
+
- `numberEpisodes` *(boolean)*: If `true`, also fetches the number of episodes in each language.
|
|
119
143
|
- **Returns:**
|
|
120
144
|
Array of objects containing available languages and their episode count:
|
|
121
145
|
```js
|
|
122
146
|
[
|
|
123
147
|
{
|
|
124
148
|
language: string,
|
|
125
|
-
episodeCount:
|
|
149
|
+
episodeCount: number //if numberEpisodes = true
|
|
126
150
|
}
|
|
127
151
|
...
|
|
128
152
|
]
|
|
@@ -130,18 +154,31 @@ Checks which languages are available for a given anime season (not recommended t
|
|
|
130
154
|
|
|
131
155
|
---
|
|
132
156
|
|
|
133
|
-
### `getAllAnime(output = "anime_list.json", get_seasons = false)`
|
|
157
|
+
### `animesama.getAllAnime(wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"], page = null, output = "anime_list.json", get_seasons = false)`
|
|
134
158
|
Fetches the full anime catalog, optionally including season information.
|
|
135
159
|
|
|
136
160
|
- **Parameters:**
|
|
161
|
+
- `wantedLanguages` *(string[])*: Language videos to get.
|
|
162
|
+
- `wantedTypes` *(string[])*: Types videos to get.
|
|
163
|
+
- `page` *(number)*: The catalog page number.
|
|
137
164
|
- `output` *(string)*: File name to save the result as JSON.
|
|
138
165
|
- `get_seasons` *(boolean)*: If `true`, also fetches seasons for each anime (very slow, ETA is still unknown).
|
|
139
166
|
- **Returns:**
|
|
140
|
-
`true` if successful, `false` otherwise.
|
|
167
|
+
if `page = null`, `true` if successful, `false` otherwise.
|
|
168
|
+
else, an array of anime objects :
|
|
169
|
+
```js
|
|
170
|
+
[
|
|
171
|
+
{
|
|
172
|
+
title: string,
|
|
173
|
+
url: string,
|
|
174
|
+
}
|
|
175
|
+
...
|
|
176
|
+
]
|
|
177
|
+
```
|
|
141
178
|
|
|
142
179
|
---
|
|
143
180
|
|
|
144
|
-
### `getLatestEpisodes(languageFilter = null)`
|
|
181
|
+
### `animesama.getLatestEpisodes(languageFilter = null)`
|
|
145
182
|
Scrapes the latest released episodes, optionally filtered by language.
|
|
146
183
|
|
|
147
184
|
- **Parameters:**
|
|
@@ -149,20 +186,28 @@ Scrapes the latest released episodes, optionally filtered by language.
|
|
|
149
186
|
- **Returns:**
|
|
150
187
|
Array of episode objects:
|
|
151
188
|
```js
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
[
|
|
190
|
+
{
|
|
191
|
+
title: string,
|
|
192
|
+
url: string,
|
|
193
|
+
cover: string,
|
|
194
|
+
language: string,
|
|
195
|
+
episode: string
|
|
196
|
+
}
|
|
197
|
+
...
|
|
198
|
+
]
|
|
159
199
|
```
|
|
160
200
|
|
|
161
201
|
---
|
|
162
202
|
|
|
163
|
-
### `getRandomAnime()`
|
|
203
|
+
### `animesama.getRandomAnime(wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"], maxAttempts = null, attempt = 0)`
|
|
164
204
|
Fetches a random anime from the catalogue.
|
|
165
205
|
|
|
206
|
+
- **Parameters:**
|
|
207
|
+
- `wantedLanguages` *(string[])*: Language videos to get.
|
|
208
|
+
- `wantedTypes` *(string[])*: Types videos to get.
|
|
209
|
+
- `maxAttempts` *(number|null)* The number of attempts of the function. If null, retry until a result is obtained.
|
|
210
|
+
- `attempt` *(number)* Current number of attempts (leave empty).
|
|
166
211
|
- **Returns:**
|
|
167
212
|
An anime object:
|
|
168
213
|
```js
|
|
@@ -177,22 +222,13 @@ Fetches a random anime from the catalogue.
|
|
|
177
222
|
|
|
178
223
|
---
|
|
179
224
|
|
|
180
|
-
### `getEpisodeTitles(AnimeUrl)`
|
|
181
|
-
Fetches the names of all episodes in a season
|
|
182
|
-
|
|
183
|
-
- **Parameters:**
|
|
184
|
-
- `animeUrl` *(string)*: URL of the anime’s season/episode page.
|
|
185
|
-
- **Returns:**
|
|
186
|
-
An array of episode titles.
|
|
187
225
|
|
|
188
|
-
|
|
226
|
+
## `AnimeScraper("animepahe")` methods
|
|
189
227
|
|
|
190
|
-
|
|
228
|
+
- [searchAnime](#animepahesearchanimequery)
|
|
191
229
|
|
|
192
|
-
- [searchAnime](#searchanimequery)
|
|
193
230
|
|
|
194
|
-
|
|
195
|
-
### `searchAnime(query)`
|
|
231
|
+
### `animepahe.searchAnime(query)`
|
|
196
232
|
Searches for anime titles that match the given query.
|
|
197
233
|
|
|
198
234
|
- **Parameters:**
|
|
@@ -202,13 +238,13 @@ Searches for anime titles that match the given query.
|
|
|
202
238
|
```js
|
|
203
239
|
[
|
|
204
240
|
{
|
|
205
|
-
id:
|
|
241
|
+
id: number,
|
|
206
242
|
title: string,
|
|
207
243
|
type: string,
|
|
208
|
-
episodes:
|
|
244
|
+
episodes: number,
|
|
209
245
|
status: string,
|
|
210
246
|
season: string,
|
|
211
|
-
year:
|
|
247
|
+
year: number,
|
|
212
248
|
score: float,
|
|
213
249
|
session: string,
|
|
214
250
|
cover: string,
|
|
@@ -220,7 +256,53 @@ Searches for anime titles that match the given query.
|
|
|
220
256
|
|
|
221
257
|
---
|
|
222
258
|
|
|
223
|
-
##
|
|
259
|
+
## `AnimeScraper("crunchyroll")` methods
|
|
260
|
+
|
|
261
|
+
- [searchAnime](#crunchyrollsearchanimequery-limit--10)
|
|
262
|
+
- [getEpisodeInfo](#crunchyrollgetepisodeinfoanimeurl-seasontitle)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
### `crunchyroll.searchAnime(query, limit = 10)`
|
|
266
|
+
Searches for anime titles that match the given query.
|
|
267
|
+
|
|
268
|
+
- **Parameters:**
|
|
269
|
+
- `query` *(string)*: The search keyword.
|
|
270
|
+
- `limit` *(number)*: Maximum number of results to return (default: 10).
|
|
271
|
+
- **Returns:**
|
|
272
|
+
An array of anime objects:
|
|
273
|
+
```js
|
|
274
|
+
[
|
|
275
|
+
{
|
|
276
|
+
title: string,
|
|
277
|
+
url: string,
|
|
278
|
+
cover: string
|
|
279
|
+
},
|
|
280
|
+
...
|
|
281
|
+
]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `crunchyroll.getEpisodeInfo(animeUrl, seasonTitle)`
|
|
285
|
+
Extracts information from all episodes of a season of an anime.
|
|
286
|
+
|
|
287
|
+
- **Parameters:**
|
|
288
|
+
- `animeUrl` *(string)*: Anime page URL.
|
|
289
|
+
- `seasonTitle` *(string)*: Name of the season for which you want episode information. If null, returns episodes from season 1.
|
|
290
|
+
- **Returns:**
|
|
291
|
+
An array of episode objects:
|
|
292
|
+
```js
|
|
293
|
+
[
|
|
294
|
+
{
|
|
295
|
+
title: string,
|
|
296
|
+
synopsis: string,
|
|
297
|
+
releaseDate: string,
|
|
298
|
+
cover: string
|
|
299
|
+
},
|
|
300
|
+
...
|
|
301
|
+
]
|
|
302
|
+
```
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Utility functions
|
|
224
306
|
|
|
225
307
|
- [getVideoUrlFromEmbed](#getvideourlfromembedsource-embedurl)
|
|
226
308
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animepahe = new AnimeScraper('animepahe');
|
|
5
|
+
|
|
6
|
+
const search = await animepahe.searchAnime("86");
|
|
7
|
+
console.log("Search Results:", search);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
|
|
6
|
+
const catalogue = await animesama.getAllAnime(["vostfr", "vf", "vastfr"], ["Anime", "Film"], 2);
|
|
7
|
+
console.log(catalogue)
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
const animeUrl = "https://anime-sama.fr/catalogue/86-eighty-six/";
|
|
6
|
+
|
|
7
|
+
const animeInfo = await animesama.getAnimeInfo(animeUrl);
|
|
8
|
+
console.log(animeInfo);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
const seasonUrl = "https://anime-sama.fr/catalogue/86-eighty-six/saison1/vostfr/";
|
|
6
|
+
|
|
7
|
+
const animeLanguages = await animesama.getAvailableLanguages(seasonUrl, ["vostfr", "vf", "va", "vkr","vcn", "vqc", "vf1", "vf2"], false);
|
|
8
|
+
console.log(animeLanguages);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
const seasonUrl = "https://anime-sama.fr/catalogue/86-eighty-six/saison1/vostfr/";
|
|
6
|
+
|
|
7
|
+
const embeds = await animesama.getEmbed(seasonUrl, ["sibnet", "vidmoly"]);
|
|
8
|
+
console.log("Embed Links:", embeds);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
const seasonUrl = "https://anime-sama.fr/catalogue/86-eighty-six/saison1/vostfr/";
|
|
6
|
+
|
|
7
|
+
const episodeTitles = await animesama.getEpisodeTitles(seasonUrl);
|
|
8
|
+
console.log("Episode Titles:", episodeTitles);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
|
|
6
|
+
const new_episodes = await animesama.getLatestEpisodes(["vostfr", "vf"]);
|
|
7
|
+
console.log(new_episodes);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
|
|
6
|
+
const random_episode = await animesama.getRandomAnime(["vostfr", "vf", "vastfr"], ["Anime", "Film"], 10);
|
|
7
|
+
console.log(random_episode);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnimeScraper, getVideoUrlFromEmbed } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
const animeUrl = "https://anime-sama.fr/catalogue/86-eighty-six/";
|
|
6
|
+
|
|
7
|
+
const seasons = await animesama.getSeasons(animeUrl, "vostfr");
|
|
8
|
+
console.log("Seasons:", seasons);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getVideoUrlFromEmbed } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const embedUrl = "https://video.sibnet.ru/shell.php?videoid=4291083";
|
|
5
|
+
|
|
6
|
+
const videoUrl = await getVideoUrlFromEmbed("sibnet", embedUrl)
|
|
7
|
+
console.log("Video URL:", videoUrl);
|
|
8
|
+
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const animesama = new AnimeScraper('animesama');
|
|
5
|
+
|
|
6
|
+
const search = await animesama.searchAnime("86", 3, ["vostfr", "vf", "vastfr"], ["Anime", "Film"]);
|
|
7
|
+
console.log("Search Results:", search);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const crunchyroll = new AnimeScraper('crunchyroll');
|
|
5
|
+
const animeUrl = "https://www.crunchyroll.com/fr/series/GVDHX8DM5/86-eighty-six/";
|
|
6
|
+
|
|
7
|
+
const episodeInfo = await crunchyroll.getEpisodeInfo(animeUrl, "S2")
|
|
8
|
+
console.log("Episode Info:", episodeInfo)
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
main().catch(console.error);
|
|
12
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
+
|
|
3
|
+
const main = async () => {
|
|
4
|
+
const crunchyroll = new AnimeScraper('crunchyroll');
|
|
5
|
+
|
|
6
|
+
const search = await crunchyroll.searchAnime("86", 3);
|
|
7
|
+
console.log("Search Results:", search);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
11
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-ani-scraped",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Scrape anime data from different sources (only anime-sama.fr for the moment)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"type": "module",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"
|
|
20
|
+
"@ablanc/crunchyroll": "^2.4.0",
|
|
21
21
|
"axios": "^1.8.4",
|
|
22
22
|
"cheerio": "^1.0.0",
|
|
23
|
-
"playwright": "^1.52.0"
|
|
23
|
+
"playwright": "^1.52.0",
|
|
24
|
+
"puppeteer": "^24.7.2",
|
|
25
|
+
"puppeteer-extra": "^3.3.6",
|
|
26
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
|
24
27
|
},
|
|
25
28
|
"repository": {
|
|
26
29
|
"type": "git",
|
package/scrapers/animesama.js
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import * as cheerio from "cheerio";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { exec as execCallback } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
const execAsync = promisify(execCallback);
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
const BASE_URL = "https://anime-sama.fr";
|
|
7
|
-
const CATALOGUE_URL = `${BASE_URL}/catalogue`;
|
|
11
|
+
const CATALOGUE_URL = `${BASE_URL}/catalogue`;
|
|
12
|
+
|
|
13
|
+
async function ensureChromiumInstalled(customPath) {
|
|
14
|
+
if (customPath) {
|
|
15
|
+
if (fs.existsSync(customPath)) {
|
|
16
|
+
console.log("customPath:", customPath);
|
|
17
|
+
return customPath;
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`The custom path to Chromium is invalid : ${customPath}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const basePath = path.join(
|
|
23
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
24
|
+
'.cache',
|
|
25
|
+
'puppeteer',
|
|
26
|
+
'chrome'
|
|
27
|
+
);
|
|
28
|
+
const chromiumPath = path.join(basePath, 'win64-135.0.7049.95', 'chrome-win64', 'chrome.exe');
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(chromiumPath)) {
|
|
31
|
+
console.log("📦 Downloading Chromium 135.0.7049.95...");
|
|
32
|
+
await execAsync('npx puppeteer browsers install chrome@135.0.7049.95');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return chromiumPath;
|
|
36
|
+
}
|
|
8
37
|
|
|
9
38
|
function getHeaders(referer = BASE_URL) {
|
|
10
39
|
return {
|
|
@@ -14,10 +43,17 @@ function getHeaders(referer = BASE_URL) {
|
|
|
14
43
|
};
|
|
15
44
|
}
|
|
16
45
|
|
|
17
|
-
export async function searchAnime(
|
|
18
|
-
|
|
46
|
+
export async function searchAnime(
|
|
47
|
+
query,
|
|
48
|
+
limit = 10,
|
|
49
|
+
wantedLanguages = ["vostfr", "vf", "vastfr"],
|
|
50
|
+
wantedTypes = ["Anime", "Film"]
|
|
51
|
+
) {
|
|
52
|
+
const url = `${CATALOGUE_URL}/?search=${encodeURIComponent(
|
|
19
53
|
query
|
|
20
54
|
)}`;
|
|
55
|
+
const isWanted = (text, list) =>
|
|
56
|
+
list.some(item => text.toLowerCase().includes(item.toLowerCase()));
|
|
21
57
|
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
22
58
|
const $ = cheerio.load(res.data);
|
|
23
59
|
const results = [];
|
|
@@ -35,6 +71,14 @@ export async function searchAnime(query, limit = 10) {
|
|
|
35
71
|
.trim();
|
|
36
72
|
const cover = anchor.find("img").first().attr("src");
|
|
37
73
|
|
|
74
|
+
const tagText = anchor.find("p").filter((_, p) =>
|
|
75
|
+
isWanted($(p).text(), wantedTypes)
|
|
76
|
+
).first().text();
|
|
77
|
+
|
|
78
|
+
const languageText = anchor.find("p").filter((_, p) =>
|
|
79
|
+
isWanted($(p).text(), wantedLanguages)
|
|
80
|
+
).first().text();
|
|
81
|
+
|
|
38
82
|
const altTitles = altRaw
|
|
39
83
|
? altRaw
|
|
40
84
|
.split(",")
|
|
@@ -54,7 +98,7 @@ export async function searchAnime(query, limit = 10) {
|
|
|
54
98
|
.filter(Boolean)
|
|
55
99
|
: [];
|
|
56
100
|
|
|
57
|
-
if (title && link) {
|
|
101
|
+
if (title && link && tagText && languageText) {
|
|
58
102
|
results.push({
|
|
59
103
|
title,
|
|
60
104
|
altTitles,
|
|
@@ -124,37 +168,7 @@ export async function getSeasons(animeUrl, language = "vostfr") {
|
|
|
124
168
|
return seasons;
|
|
125
169
|
}
|
|
126
170
|
|
|
127
|
-
|
|
128
|
-
import path from 'path';
|
|
129
|
-
import { exec as execCallback } from 'child_process';
|
|
130
|
-
import { promisify } from 'util';
|
|
131
|
-
const execAsync = promisify(execCallback);
|
|
132
|
-
|
|
133
|
-
async function ensureChromiumInstalled(customPath) {
|
|
134
|
-
if (customPath) {
|
|
135
|
-
if (fs.existsSync(customPath)) {
|
|
136
|
-
console.log("customPath:", customPath);
|
|
137
|
-
return customPath;
|
|
138
|
-
} else {
|
|
139
|
-
console.log(`The custom path to Chromium is invalid : ${customPath}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const basePath = path.join(
|
|
143
|
-
process.env.HOME || process.env.USERPROFILE,
|
|
144
|
-
'.cache',
|
|
145
|
-
'puppeteer',
|
|
146
|
-
'chrome'
|
|
147
|
-
);
|
|
148
|
-
const chromiumPath = path.join(basePath, 'win64-135.0.7049.95', 'chrome-win64', 'chrome.exe');
|
|
149
|
-
|
|
150
|
-
if (!fs.existsSync(chromiumPath)) {
|
|
151
|
-
console.log("📦 Downloading Chromium 135.0.7049.95...");
|
|
152
|
-
await execAsync('npx puppeteer browsers install chrome@135.0.7049.95');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return chromiumPath;
|
|
156
|
-
}
|
|
157
|
-
export async function getEpisodeTitles(animeUrl, customChromiumPath) {
|
|
171
|
+
export async function getEpisodeTitles(seasonUrl, customChromiumPath) {
|
|
158
172
|
let browser;
|
|
159
173
|
try {
|
|
160
174
|
const puppeteer = await import('puppeteer');
|
|
@@ -178,7 +192,7 @@ export async function getEpisodeTitles(animeUrl, customChromiumPath) {
|
|
|
178
192
|
}
|
|
179
193
|
});
|
|
180
194
|
|
|
181
|
-
await page.goto(
|
|
195
|
+
await page.goto(seasonUrl, { waitUntil: 'domcontentloaded' });
|
|
182
196
|
await page.waitForSelector('#selectEpisodes');
|
|
183
197
|
|
|
184
198
|
const titres = await page.$$eval('#selectEpisodes option', options =>
|
|
@@ -195,22 +209,21 @@ export async function getEpisodeTitles(animeUrl, customChromiumPath) {
|
|
|
195
209
|
}
|
|
196
210
|
}
|
|
197
211
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
headers: getHeaders(animeUrl.split("/").slice(0, 5).join("/")),
|
|
212
|
+
export async function getEmbed(seasonUrl, hostPriority = ["sibnet", "vidmoly"], customChromiumPath) {
|
|
213
|
+
const res = await axios.get(seasonUrl, {
|
|
214
|
+
headers: getHeaders(seasonUrl.split("/").slice(0, 5).join("/")),
|
|
202
215
|
});
|
|
203
216
|
|
|
204
217
|
const $ = cheerio.load(res.data);
|
|
205
218
|
const scriptTag = $('script[src*="episodes.js"]').attr("src");
|
|
206
219
|
if (!scriptTag) throw new Error("No episodes script found");
|
|
207
220
|
|
|
208
|
-
const scriptUrl =
|
|
209
|
-
?
|
|
210
|
-
:
|
|
221
|
+
const scriptUrl = seasonUrl.endsWith("/")
|
|
222
|
+
? seasonUrl + scriptTag
|
|
223
|
+
: seasonUrl + "/" + scriptTag;
|
|
211
224
|
|
|
212
225
|
const episodesJs = await axios
|
|
213
|
-
.get(scriptUrl, { headers: getHeaders(
|
|
226
|
+
.get(scriptUrl, { headers: getHeaders(seasonUrl) })
|
|
214
227
|
.then((r) => r.data);
|
|
215
228
|
|
|
216
229
|
const matches = [
|
|
@@ -244,14 +257,13 @@ export async function getEmbed(animeUrl, hostPriority = ["vidmoly"], customChrom
|
|
|
244
257
|
finalEmbeds.push(selectedUrl || null);
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
const titles = await getEpisodeTitles(
|
|
260
|
+
const titles = await getEpisodeTitles(seasonUrl, customChromiumPath);
|
|
248
261
|
return finalEmbeds.map((url, i) => ({
|
|
249
262
|
title: titles[i] || "Untitled",
|
|
250
263
|
url,
|
|
251
264
|
}));
|
|
252
265
|
}
|
|
253
266
|
|
|
254
|
-
|
|
255
267
|
export async function getAnimeInfo(animeUrl) {
|
|
256
268
|
const res = await axios.get(animeUrl, { headers: getHeaders(CATALOGUE_URL) });
|
|
257
269
|
const $ = cheerio.load(res.data);
|
|
@@ -286,7 +298,8 @@ export async function getAnimeInfo(animeUrl) {
|
|
|
286
298
|
|
|
287
299
|
export async function getAvailableLanguages(
|
|
288
300
|
seasonUrl,
|
|
289
|
-
wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc"]
|
|
301
|
+
wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc", "vf1", "vf2"],
|
|
302
|
+
numberEpisodes = false
|
|
290
303
|
) {
|
|
291
304
|
const languageLinks = [];
|
|
292
305
|
|
|
@@ -298,87 +311,96 @@ export async function getAvailableLanguages(
|
|
|
298
311
|
headers: getHeaders(CATALOGUE_URL),
|
|
299
312
|
});
|
|
300
313
|
if (res.status === 200) {
|
|
301
|
-
|
|
302
|
-
|
|
314
|
+
if (numberEpisodes){
|
|
315
|
+
const episodeCount = (await getEmbed(languageUrl)).length;
|
|
316
|
+
languageLinks.push({ language: language.toUpperCase(), episodeCount: episodeCount });
|
|
317
|
+
} else {
|
|
318
|
+
languageLinks.push({ language: language.toUpperCase()});
|
|
319
|
+
}
|
|
320
|
+
|
|
303
321
|
}
|
|
304
322
|
} catch (error) {
|
|
305
323
|
// If an error occurs (like a 404), we skip that language
|
|
306
324
|
continue;
|
|
307
325
|
}
|
|
308
326
|
}
|
|
309
|
-
|
|
310
327
|
return languageLinks;
|
|
311
328
|
}
|
|
312
329
|
|
|
313
330
|
export async function getAllAnime(
|
|
331
|
+
wantedLanguages = ["vostfr", "vf", "vastfr"],
|
|
332
|
+
wantedTypes = ["Anime", "Film"],
|
|
333
|
+
page = null,
|
|
314
334
|
output = "anime_list.json",
|
|
315
335
|
get_seasons = false
|
|
316
336
|
) {
|
|
317
|
-
// BE CAREFUL, GET_SEASONS TAKES A VERY VERY LONG TIME TO FINISH
|
|
318
337
|
let animeLinks = [];
|
|
319
|
-
let page = 1;
|
|
320
338
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const url = page === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${page}`;
|
|
324
|
-
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
325
|
-
const $ = cheerio.load(res.data);
|
|
339
|
+
const isWanted = (text, list) =>
|
|
340
|
+
list.some(item => text.toLowerCase().includes(item.toLowerCase()));
|
|
326
341
|
|
|
327
|
-
|
|
342
|
+
const fetchPage = async (pageNum) => {
|
|
343
|
+
const url = pageNum === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${pageNum}`;
|
|
344
|
+
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
345
|
+
const $ = cheerio.load(res.data);
|
|
328
346
|
|
|
329
|
-
|
|
330
|
-
// console.log("No more anime found, stopping.");
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
347
|
+
const containers = $("div.shrink-0.m-3.rounded.border-2");
|
|
333
348
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
containers.each((_, el) => {
|
|
350
|
+
const anchor = $(el).find("a");
|
|
351
|
+
const title = anchor.find("h1").text().trim();
|
|
352
|
+
const link = anchor.attr("href");
|
|
338
353
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
.first()
|
|
343
|
-
.text();
|
|
354
|
+
const tagText = anchor.find("p").filter((_, p) =>
|
|
355
|
+
isWanted($(p).text(), wantedTypes)
|
|
356
|
+
).first().text();
|
|
344
357
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
});
|
|
358
|
+
const languageText = anchor.find("p").filter((_, p) =>
|
|
359
|
+
isWanted($(p).text(), wantedLanguages)
|
|
360
|
+
).first().text();
|
|
350
361
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const uniqueLinks = animeLinks.filter(
|
|
357
|
-
(item, index, self) => index === self.findIndex((i) => i.url === item.url)
|
|
358
|
-
);
|
|
362
|
+
if (title && link && tagText && languageText) {
|
|
363
|
+
const fullUrl = link.startsWith("http") ? link : `${BASE_URL}${link}`;
|
|
364
|
+
animeLinks.push({ title, url: fullUrl });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
359
367
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
for (let anime of uniqueLinks) {
|
|
363
|
-
try {
|
|
364
|
-
const seasons = await getSeasons(anime.url);
|
|
365
|
-
anime.seasons = Array.isArray(seasons) ? seasons : [];
|
|
366
|
-
} catch (err) {
|
|
367
|
-
console.warn(
|
|
368
|
-
`⚠️ Failed to fetch seasons for ${anime.name}: ${err.message}`
|
|
369
|
-
);
|
|
370
|
-
anime.seasons = [];
|
|
371
|
-
}
|
|
368
|
+
return containers.length > 0;
|
|
369
|
+
};
|
|
372
370
|
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
const enrichWithSeasons = async (list) => {
|
|
372
|
+
for (const anime of list) {
|
|
373
|
+
try {
|
|
374
|
+
const seasons = await getSeasons(anime.url);
|
|
375
|
+
anime.seasons = Array.isArray(seasons) ? seasons : [];
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.warn(`⚠️ Failed to fetch seasons for ${anime.title}: ${err.message}`);
|
|
378
|
+
anime.seasons = [];
|
|
375
379
|
}
|
|
380
|
+
await new Promise(r => setTimeout(r, 300));
|
|
376
381
|
}
|
|
382
|
+
};
|
|
377
383
|
|
|
378
|
-
|
|
379
|
-
|
|
384
|
+
try {
|
|
385
|
+
if (page) {
|
|
386
|
+
await fetchPage(page);
|
|
387
|
+
if (get_seasons) await enrichWithSeasons(animeLinks);
|
|
388
|
+
return animeLinks;
|
|
389
|
+
} else {
|
|
390
|
+
let currentPage = 1;
|
|
391
|
+
while (await fetchPage(currentPage++)) {
|
|
392
|
+
await new Promise(r => setTimeout(r, 300));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Dédupliquer les URLs
|
|
396
|
+
const uniqueLinks = [...new Map(animeLinks.map(item => [item.url, item])).values()];
|
|
397
|
+
if (get_seasons) await enrichWithSeasons(uniqueLinks);
|
|
398
|
+
|
|
399
|
+
fs.writeFileSync(output, JSON.stringify(uniqueLinks, null, 2), "utf-8");
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
380
402
|
} catch (err) {
|
|
381
|
-
console.error("
|
|
403
|
+
console.error("🔥 Erreur surpuissante détectée :", err.message);
|
|
382
404
|
return false;
|
|
383
405
|
}
|
|
384
406
|
}
|
|
@@ -425,13 +447,21 @@ export async function getLatestEpisodes(languageFilter = null) {
|
|
|
425
447
|
}
|
|
426
448
|
}
|
|
427
449
|
|
|
428
|
-
export async function getRandomAnime(
|
|
450
|
+
export async function getRandomAnime(
|
|
451
|
+
wantedLanguages = ["vostfr", "vf", "vastfr"],
|
|
452
|
+
wantedTypes = ["Anime", "Film"],
|
|
453
|
+
maxAttempts = null,
|
|
454
|
+
attempt = 0
|
|
455
|
+
) {
|
|
429
456
|
try {
|
|
430
457
|
const res = await axios.get(
|
|
431
|
-
`${CATALOGUE_URL}/?
|
|
458
|
+
`${CATALOGUE_URL}/?search=&random=1`,
|
|
432
459
|
{ headers: getHeaders(CATALOGUE_URL) }
|
|
433
460
|
);
|
|
461
|
+
|
|
434
462
|
const $ = cheerio.load(res.data);
|
|
463
|
+
const isWanted = (text, list) =>
|
|
464
|
+
list.some(item => text.toLowerCase().includes(item.toLowerCase()));
|
|
435
465
|
|
|
436
466
|
const container = $("div.shrink-0.m-3.rounded.border-2").first();
|
|
437
467
|
const anchor = container.find("a");
|
|
@@ -464,7 +494,15 @@ export async function getRandomAnime() {
|
|
|
464
494
|
.filter(Boolean)
|
|
465
495
|
: [];
|
|
466
496
|
|
|
467
|
-
|
|
497
|
+
const tagText = anchor.find("p").filter((_, p) =>
|
|
498
|
+
isWanted($(p).text(), wantedTypes)
|
|
499
|
+
).first().text();
|
|
500
|
+
|
|
501
|
+
const languageText = anchor.find("p").filter((_, p) =>
|
|
502
|
+
isWanted($(p).text(), wantedLanguages)
|
|
503
|
+
).first().text();
|
|
504
|
+
|
|
505
|
+
if (title && link && tagText && languageText) {
|
|
468
506
|
return {
|
|
469
507
|
title,
|
|
470
508
|
altTitles,
|
|
@@ -473,10 +511,15 @@ export async function getRandomAnime() {
|
|
|
473
511
|
cover,
|
|
474
512
|
};
|
|
475
513
|
} else {
|
|
476
|
-
|
|
514
|
+
if (maxAttempts === null || attempt < maxAttempts) {
|
|
515
|
+
return await getRandomAnime(wantedLanguages, wantedTypes, maxAttempts, attempt + 1);
|
|
516
|
+
} else {
|
|
517
|
+
throw new Error("Max attempts reached without finding a valid anime.");
|
|
518
|
+
}
|
|
477
519
|
}
|
|
478
520
|
} catch (err) {
|
|
479
521
|
console.error("Failed to fetch random anime:", err.message);
|
|
480
522
|
return null;
|
|
481
523
|
}
|
|
482
524
|
}
|
|
525
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer-extra';
|
|
2
|
+
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
3
|
+
puppeteer.use(StealthPlugin());
|
|
4
|
+
|
|
5
|
+
const LANGUAGE = "fr";
|
|
6
|
+
const CATALOGUE_URL = `https://www.crunchyroll.com/${LANGUAGE}`;
|
|
7
|
+
|
|
8
|
+
export async function searchAnime(query, limit = 10) {
|
|
9
|
+
const url = `${CATALOGUE_URL}/search?q=${encodeURIComponent(query)}`;
|
|
10
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
11
|
+
const page = await browser.newPage();
|
|
12
|
+
|
|
13
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
14
|
+
await page.waitForSelector('.series-results-cards-wrapper [data-t="search-series-card"]');
|
|
15
|
+
const results = await page.evaluate((limit) => {
|
|
16
|
+
const cards = document.querySelectorAll('.series-results-cards-wrapper [data-t="search-series-card"]');
|
|
17
|
+
const results = [];
|
|
18
|
+
|
|
19
|
+
cards.forEach(card => {
|
|
20
|
+
if (results.length < limit) {
|
|
21
|
+
const title = card.querySelector('.search-show-card__title-link--7ilnY')?.innerText;
|
|
22
|
+
const url = card.querySelector('.search-show-card__title-link--7ilnY')?.href;
|
|
23
|
+
const cover = card.querySelector('.content-image__image--7tGlg').src?.replace(/cdn-cgi\/image\/[^\/]+(\/catalog\/.*)/, 'cdn-cgi/image/$1') || null;
|
|
24
|
+
|
|
25
|
+
if (title && url && !results.some(result => result.url === url)) {
|
|
26
|
+
results.push({ title, url, cover });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return results;
|
|
31
|
+
}, limit);
|
|
32
|
+
|
|
33
|
+
await browser.close();
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export async function getEpisodeInfo(animeUrl, seasonTitle) {
|
|
40
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
41
|
+
const page = await browser.newPage();
|
|
42
|
+
await page.goto(animeUrl, { waitUntil: 'domcontentloaded' });
|
|
43
|
+
try {
|
|
44
|
+
await page.waitForSelector('.erc-seasons-select .dropdown-trigger--P--FX', { timeout: 5000 });
|
|
45
|
+
await page.click('.erc-seasons-select .dropdown-trigger--P--FX');
|
|
46
|
+
await page.evaluate((seasonTitle) => {
|
|
47
|
+
const options = Array.from(document.querySelectorAll('.extended-option--Wk-jL'));
|
|
48
|
+
const target = options.find(opt => {
|
|
49
|
+
const label = opt.querySelector('.extended-option__text--MQWp1');
|
|
50
|
+
return label && label.textContent.includes(seasonTitle);
|
|
51
|
+
});
|
|
52
|
+
if (target) {
|
|
53
|
+
target.click();
|
|
54
|
+
} else {
|
|
55
|
+
console.warn('Saison non trouvée:', seasonTitle);
|
|
56
|
+
}
|
|
57
|
+
}, seasonTitle);
|
|
58
|
+
} catch { }
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await page.waitForSelector('.show-more-button-boxed button', { timeout: 1000 });
|
|
63
|
+
await page.click('.show-more-button-boxed button');
|
|
64
|
+
} catch { }
|
|
65
|
+
|
|
66
|
+
await page.waitForSelector('div.card:not(.placeholder-card)', { timeout: 10000 });
|
|
67
|
+
const allCardInfo = await page.evaluate(() => {
|
|
68
|
+
const cards = document.querySelectorAll('div.card:not(.placeholder-card)');
|
|
69
|
+
const episodeInfo = [];
|
|
70
|
+
|
|
71
|
+
cards.forEach(card => {
|
|
72
|
+
const title = card?.querySelector('.playable-card__title-link--96psl')?.textContent || null;
|
|
73
|
+
const synopsis = card?.querySelector('.playable-card-hover__description--4Lpe4')?.textContent || null;
|
|
74
|
+
const releaseDate = card?.querySelector('.playable-card-hover__release--3Xg35 .text--gq6o-')?.textContent || null;
|
|
75
|
+
const cover = card?.querySelector('img.progressive-image-loading__original--k-k-7')?.src?.replace(/cdn-cgi\/image\/[^\/]+(\/catalog\/.*)/, 'cdn-cgi/image/$1') || null;
|
|
76
|
+
episodeInfo.push({
|
|
77
|
+
title,
|
|
78
|
+
synopsis,
|
|
79
|
+
releaseDate,
|
|
80
|
+
cover,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return episodeInfo;
|
|
85
|
+
});
|
|
86
|
+
await browser.close();
|
|
87
|
+
return allCardInfo;
|
|
88
|
+
}
|
|
89
|
+
|
package/scrapers/scrapers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as animesama from "./animesama.js";
|
|
2
2
|
import * as animepahe from "./animepahe.js";
|
|
3
|
+
import * as crunchyroll from "./crunchyroll.js";
|
|
3
4
|
|
|
4
5
|
export class AnimeScraper {
|
|
5
6
|
constructor(source) {
|
|
@@ -7,8 +8,10 @@ export class AnimeScraper {
|
|
|
7
8
|
this.source = animepahe;
|
|
8
9
|
} else if (source === 'animesama') {
|
|
9
10
|
this.source = animesama;
|
|
10
|
-
} else {
|
|
11
|
-
|
|
11
|
+
} else if (source === 'crunchyroll') {
|
|
12
|
+
this.source = crunchyroll;
|
|
13
|
+
} else {
|
|
14
|
+
throw new Error('Invalid source. Choose either "animepahe", "crunchyroll" or "animesama".');
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -30,9 +33,18 @@ export class AnimeScraper {
|
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
async
|
|
36
|
+
async getEpisodeTitles(seasonUrl, ...rest) {
|
|
34
37
|
try {
|
|
35
|
-
return await this.source.
|
|
38
|
+
return await this.source.getEpisodeTitles(seasonUrl, ...rest);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`This scraper does not have the getEpisodeTitles function implemented or an error happened -> ${error}`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getEmbed(seasonUrl, ...rest) {
|
|
46
|
+
try {
|
|
47
|
+
return await this.source.getEmbed(seasonUrl, ...rest);
|
|
36
48
|
} catch (error) {
|
|
37
49
|
console.error(`This scraper does not have the getEmbed function implemented or an error happened -> ${error}`);
|
|
38
50
|
return null;
|
|
@@ -75,20 +87,20 @@ export class AnimeScraper {
|
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
async getRandomAnime() {
|
|
90
|
+
async getRandomAnime(...rest) {
|
|
79
91
|
try {
|
|
80
|
-
return await this.source.getRandomAnime();
|
|
92
|
+
return await this.source.getRandomAnime(...rest);
|
|
81
93
|
} catch (error) {
|
|
82
94
|
console.error(`This scraper does not have the getRandomAnime function implemented or an error happened -> ${error}`);
|
|
83
95
|
return null;
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
async
|
|
99
|
+
async getEpisodeInfo(animeUrl, ...rest) {
|
|
88
100
|
try {
|
|
89
|
-
return await this.source.
|
|
101
|
+
return await this.source.getEpisodeInfo(animeUrl, ...rest);
|
|
90
102
|
} catch (error) {
|
|
91
|
-
console.error(`This scraper does not have the
|
|
103
|
+
console.error(`This scraper does not have the getEpisodeInfo function implemented or an error happened -> ${error}`);
|
|
92
104
|
return null;
|
|
93
105
|
}
|
|
94
106
|
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { AnimeScraper, getVideoUrlFromEmbed } from "../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
-
|
|
3
|
-
const main = async () => {
|
|
4
|
-
const scraper = new AnimeScraper('animesama');
|
|
5
|
-
|
|
6
|
-
const search = await scraper.searchAnime("86");
|
|
7
|
-
console.log("Search Results:", search);
|
|
8
|
-
|
|
9
|
-
const animeUrl = Object.values(search)[0].url;
|
|
10
|
-
const seasons = await scraper.getSeasons(animeUrl, "vostfr");
|
|
11
|
-
console.log("Seasons:", seasons);
|
|
12
|
-
|
|
13
|
-
const embeds = await scraper.getEmbed(seasons[0].url, [
|
|
14
|
-
"sibnet",
|
|
15
|
-
"vidmoly",
|
|
16
|
-
]);
|
|
17
|
-
console.log("Embed Links:", embeds);
|
|
18
|
-
|
|
19
|
-
const videoUrl = await getVideoUrlFromEmbed("sibnet", embeds[0].url)
|
|
20
|
-
console.log("Video URL:", videoUrl);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
main().catch(console.error);
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { AnimeScraper } from "../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
-
|
|
3
|
-
const main = async () => {
|
|
4
|
-
const scraper = new AnimeScraper('animesama');
|
|
5
|
-
|
|
6
|
-
const animeUrl = "https://anime-sama.fr/catalogue/sword-art-online";
|
|
7
|
-
const animeInfo = await scraper.getAnimeInfo(animeUrl);
|
|
8
|
-
console.log(animeInfo);
|
|
9
|
-
|
|
10
|
-
const animeLanguages = await scraper.getAvailableLanguages(`${animeUrl}/saison1/vostfr`, ["vostfr", "vf"]);
|
|
11
|
-
console.log(animeLanguages);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
main().catch(console.error);
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { AnimeScraper } from "../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
-
|
|
3
|
-
const main = async () => {
|
|
4
|
-
const scraper = new AnimeScraper('animesama');
|
|
5
|
-
|
|
6
|
-
await scraper.getAllAnime("output_anime_list.json", false);
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
main().catch(console.error);
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { AnimeScraper } from "../index.js"; // REPLACE BY "from 'better-ani-scraped';"
|
|
2
|
-
|
|
3
|
-
const main = async () => {
|
|
4
|
-
const scraper = new AnimeScraper('animesama');
|
|
5
|
-
|
|
6
|
-
const new_episodes = await scraper.getLatestEpisodes(["vostfr", "vf"]);
|
|
7
|
-
console.log(new_episodes);
|
|
8
|
-
|
|
9
|
-
const random_episode = await scraper.getRandomAnime();
|
|
10
|
-
console.log(random_episode);
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
main().catch(console.error);
|