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 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
- - [`AnimeScrapper("animesama")` methods](#animescrapperanimesama-methods)
10
- - [`AnimeScrapper("animepahe")` methods](#animescrapperanimepahe-methods)
11
- - [Functions](#functions)
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 scrapper for the given source (only "animesama" and "animepahe" available at the moment).
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
- ## `AnimeScrapper("animesama")` methods
28
+ ## `AnimeScraper("animesama")` methods
23
29
 
24
- - [searchAnime](#searchanimequery-limit--10)
25
- - [getSeasons](#getseasonsanimeurl-language--vostfr)
26
- - [getEmbed](#getembedanimeurl-hostpriority--sibnet-vidmoly)
27
- - [getAnimeInfo](#getanimeinfoanimeurl)
28
- - [getAvailableLanguages](#getavailablelanguagesseasonurl-wantedlanguages--vostfr-vf-va-vkr-vcn-vqc)
29
- - [getAllAnime](#getallanimeoutput--anime_listjson-get_seasons--false)
30
- - [getLatestEpisodes](#getlatestepisodeslanguagefilter--null)
31
- - [getRandomAnime](#getrandomanime)
32
- - [getEpisodeTitles](#getepisodetitlesanimeurl)
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
- ### `getEmbed(animeUrl, hostPriority = ["sibnet", "vidmoly"])`
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
- - `animeUrl` *(string)*: URL of the anime’s season/episode page.
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
- title: string,
89
- url: string,
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 (not recommended to use the default value of wantedLanguages, the more languages there is the more the function is long to run, only checks for languages you want).
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: int
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
- title: string,
154
- url: string,
155
- cover: string,
156
- language: string,
157
- episode: string
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
- ## `AnimeScrapper("animepahe")` methods
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: int,
241
+ id: number,
206
242
  title: string,
207
243
  type: string,
208
- episodes: int,
244
+ episodes: number,
209
245
  status: string,
210
246
  season: string,
211
- year: int,
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
- ## Functions
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.2",
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
- "ani-scraped": "^1.2.8",
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",
@@ -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 puppeteer from'puppeteer';
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(query, limit = 10) {
18
- const url = `${CATALOGUE_URL}/?type%5B%5D=Anime&search=${encodeURIComponent(
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(animeUrl, { waitUntil: 'domcontentloaded' });
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
- export async function getEmbed(animeUrl, hostPriority = ["vidmoly"], customChromiumPath) {
200
- const res = await axios.get(animeUrl, {
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 = animeUrl.endsWith("/")
209
- ? animeUrl + scriptTag
210
- : animeUrl + "/" + scriptTag;
221
+ const scriptUrl = seasonUrl.endsWith("/")
222
+ ? seasonUrl + scriptTag
223
+ : seasonUrl + "/" + scriptTag;
211
224
 
212
225
  const episodesJs = await axios
213
- .get(scriptUrl, { headers: getHeaders(animeUrl) })
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(animeUrl, customChromiumPath);
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
- const episodeCount = (await getEmbed(languageUrl)).length;
302
- languageLinks.push({ language: language.toUpperCase(), episodeCount: episodeCount });
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
- try {
322
- while (true) {
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
- const containers = $("div.shrink-0.m-3.rounded.border-2");
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
- if (containers.length === 0) {
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
- containers.each((_, el) => {
335
- const anchor = $(el).find("a");
336
- const title = anchor.find("h1").text().trim();
337
- const link = anchor.attr("href");
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
- const tagText = anchor
340
- .find("p")
341
- .filter((_, p) => $(p).text().includes("Anime"))
342
- .first()
343
- .text();
354
+ const tagText = anchor.find("p").filter((_, p) =>
355
+ isWanted($(p).text(), wantedTypes)
356
+ ).first().text();
344
357
 
345
- if (title && link && tagText.includes("Anime")) {
346
- const fullUrl = link.startsWith("http") ? link : `${BASE_URL}${link}`;
347
- animeLinks.push({ title: title, url: fullUrl });
348
- }
349
- });
358
+ const languageText = anchor.find("p").filter((_, p) =>
359
+ isWanted($(p).text(), wantedLanguages)
360
+ ).first().text();
350
361
 
351
- page++;
352
- await new Promise((r) => setTimeout(r, 300));
353
- }
354
-
355
- // Deduplicate
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
- if (get_seasons) {
361
- // console.log("Fetching seasons for each anime...");
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
- // Optional delay to avoid rate-limiting
374
- await new Promise((r) => setTimeout(r, 300));
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
- fs.writeFileSync(output, JSON.stringify(uniqueLinks, null, 2), "utf-8");
379
- return true;
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("Error occurred:", err.message);
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}/?type[]=Anime&search=&random=1`,
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
- if (title && link) {
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
- throw new Error("No anime found in random response.");
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
+
@@ -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
- throw new Error('Invalid source. Choose either "animepahe" or "animesama".');
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 getEmbed(animeUrl, ...rest) {
36
+ async getEpisodeTitles(seasonUrl, ...rest) {
34
37
  try {
35
- return await this.source.getEmbed(animeUrl, ...rest);
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 getEpisodeTitles(animeUrl, ...rest) {
99
+ async getEpisodeInfo(animeUrl, ...rest) {
88
100
  try {
89
- return await this.source.getEpisodeTitles(animeUrl, ...rest);
101
+ return await this.source.getEpisodeInfo(animeUrl, ...rest);
90
102
  } catch (error) {
91
- console.error(`This scraper does not have the getRandomAnime function implemented or an error happened -> ${error}`);
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);