better-ani-scraped 1.4.0 → 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,38 +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
- - [`AnimeScrapper("crunchyroll")` methods](#animescrappercrunchyroll-methods)
12
- - [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)
13
13
 
14
14
  ---
15
15
 
16
16
  ## Main class
17
17
 
18
18
  ### `AnimeScraper(source)`
19
- Creates a scrapper for the given source (only "animesama", "animepahe" and "crunchyroll" 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
+ ```
20
25
 
21
26
  ---
22
27
 
23
- ## `AnimeScrapper("animesama")` methods
28
+ ## `AnimeScraper("animesama")` methods
24
29
 
25
- - [searchAnime](#animesamasearchanimequery-limit--10)
30
+ - [searchAnime](#animesamasearchanimequery-limit--10-wantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film)
26
31
  - [getSeasons](#animesamagetseasonsanimeurl-language--vostfr)
27
- - [getEmbed](#animesamagetembedanimeurl-hostpriority--sibnet-vidmoly)
32
+ - [getEpisodeTitles](#animesamagetepisodetitlesseasonurl-customchromiumpath)
33
+ - [getEmbed](#animesamagetembedseasonurl-hostpriority--sibnet-vidmoly)
28
34
  - [getAnimeInfo](#animesamagetanimeinfoanimeurl)
29
- - [getAvailableLanguages](#animesamagetavailablelanguagesseasonurl-wantedlanguages--vostfr-vf-va-vkr-vcn-vqc)
30
- - [getAllAnime](#animesamagetallanimeoutput--anime_listjson-get_seasons--false)
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)
31
37
  - [getLatestEpisodes](#animesamagetlatestepisodeslanguagefilter--null)
32
- - [getRandomAnime](#animesamagetrandomanime)
33
- - [getEpisodeTitles](#animesamagetepisodetitlesanimeurl-customChromiumPath)
38
+ - [getRandomAnime](#animesamagetrandomanimewantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film-maxattempts--null-attempt--0)
34
39
 
35
- ### `animesama.searchAnime(query, limit = 10)`
40
+ ### `animesama.searchAnime(query, limit = 10, wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"])`
36
41
  Searches for anime titles that match the given query.
37
42
 
38
43
  - **Parameters:**
39
44
  - `query` *(string)*: The search keyword.
40
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.
41
48
  - **Returns:**
42
49
  An array of anime objects:
43
50
  ```js
@@ -76,19 +83,34 @@ Fetches all available seasons of an anime in the specified language.
76
83
 
77
84
  ---
78
85
 
79
- ### `animesama.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"])`
80
98
  Retrieves embed URLs for episodes, prioritizing by host.
81
99
 
82
100
  - **Parameters:**
83
- - `animeUrl` *(string)*: URL of the anime’s season/episode page.
101
+ - `seasonUrl` *(string)*: URL of the anime’s season page.
84
102
  - `hostPriority` *(string[])*: Array of preferred hostnames.
85
103
  - **Returns:**
86
104
  An array of embed video:
87
105
  ```js
88
- {
89
- title: string,
90
- url: string,
91
- }
106
+ [
107
+ {
108
+ title: string,
109
+ url: string,
110
+ }
111
+ ...
112
+ ]
113
+
92
114
  ```
93
115
  ---
94
116
 
@@ -111,19 +133,20 @@ Extracts basic information from an anime page.
111
133
 
112
134
  ---
113
135
 
114
- ### `animesama.getAvailableLanguages(seasonUrl, wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc"])`
115
- 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).
116
138
 
117
139
  - **Parameters:**
118
140
  - `seasonUrl` *(string)*: The season anime URL.
119
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.
120
143
  - **Returns:**
121
144
  Array of objects containing available languages and their episode count:
122
145
  ```js
123
146
  [
124
147
  {
125
148
  language: string,
126
- episodeCount: int
149
+ episodeCount: number //if numberEpisodes = true
127
150
  }
128
151
  ...
129
152
  ]
@@ -131,14 +154,27 @@ Checks which languages are available for a given anime season (not recommended t
131
154
 
132
155
  ---
133
156
 
134
- ### `animesama.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)`
135
158
  Fetches the full anime catalog, optionally including season information.
136
159
 
137
160
  - **Parameters:**
161
+ - `wantedLanguages` *(string[])*: Language videos to get.
162
+ - `wantedTypes` *(string[])*: Types videos to get.
163
+ - `page` *(number)*: The catalog page number.
138
164
  - `output` *(string)*: File name to save the result as JSON.
139
165
  - `get_seasons` *(boolean)*: If `true`, also fetches seasons for each anime (very slow, ETA is still unknown).
140
166
  - **Returns:**
141
- `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
+ ```
142
178
 
143
179
  ---
144
180
 
@@ -150,20 +186,28 @@ Scrapes the latest released episodes, optionally filtered by language.
150
186
  - **Returns:**
151
187
  Array of episode objects:
152
188
  ```js
153
- {
154
- title: string,
155
- url: string,
156
- cover: string,
157
- language: string,
158
- episode: string
159
- }
189
+ [
190
+ {
191
+ title: string,
192
+ url: string,
193
+ cover: string,
194
+ language: string,
195
+ episode: string
196
+ }
197
+ ...
198
+ ]
160
199
  ```
161
200
 
162
201
  ---
163
202
 
164
- ### `animesama.getRandomAnime()`
203
+ ### `animesama.getRandomAnime(wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"], maxAttempts = null, attempt = 0)`
165
204
  Fetches a random anime from the catalogue.
166
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).
167
211
  - **Returns:**
168
212
  An anime object:
169
213
  ```js
@@ -178,18 +222,8 @@ Fetches a random anime from the catalogue.
178
222
 
179
223
  ---
180
224
 
181
- ### `animesama.getEpisodeTitles(AnimeUrl, customChromiumPath)`
182
- Fetches the names of all episodes in a season
183
-
184
- - **Parameters:**
185
- - `animeUrl` *(string)*: URL of the anime’s season/episode page.
186
- - `animeUrl` *(string)*: Path of the Chromium folder
187
- - **Returns:**
188
- An array of episode titles.
189
-
190
- ---
191
225
 
192
- ## `AnimeScrapper("animepahe")` methods
226
+ ## `AnimeScraper("animepahe")` methods
193
227
 
194
228
  - [searchAnime](#animepahesearchanimequery)
195
229
 
@@ -204,13 +238,13 @@ Searches for anime titles that match the given query.
204
238
  ```js
205
239
  [
206
240
  {
207
- id: int,
241
+ id: number,
208
242
  title: string,
209
243
  type: string,
210
- episodes: int,
244
+ episodes: number,
211
245
  status: string,
212
246
  season: string,
213
- year: int,
247
+ year: number,
214
248
  score: float,
215
249
  session: string,
216
250
  cover: string,
@@ -222,7 +256,7 @@ Searches for anime titles that match the given query.
222
256
 
223
257
  ---
224
258
 
225
- ## `AnimeScrapper("crunchyroll")` methods
259
+ ## `AnimeScraper("crunchyroll")` methods
226
260
 
227
261
  - [searchAnime](#crunchyrollsearchanimequery-limit--10)
228
262
  - [getEpisodeInfo](#crunchyrollgetepisodeinfoanimeurl-seasontitle)
@@ -268,7 +302,7 @@ Extracts information from all episodes of a season of an anime.
268
302
  ```
269
303
  ---
270
304
 
271
- ## Functions
305
+ ## Utility functions
272
306
 
273
307
  - [getVideoUrlFromEmbed](#getvideourlfromembedsource-embedurl)
274
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.4.0",
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": {
@@ -1,10 +1,40 @@
1
1
  import axios from "axios";
2
2
  import * as cheerio from "cheerio";
3
3
  import fs from "fs";
4
+ import path from 'path';
5
+ import { exec as execCallback } from 'child_process';
6
+ import { promisify } from 'util';
7
+ const execAsync = promisify(execCallback);
8
+
4
9
 
5
10
  const BASE_URL = "https://anime-sama.fr";
6
11
  const CATALOGUE_URL = `${BASE_URL}/catalogue`;
7
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
+ }
37
+
8
38
  function getHeaders(referer = BASE_URL) {
9
39
  return {
10
40
  "User-Agent": "Mozilla/5.0",
@@ -13,10 +43,17 @@ function getHeaders(referer = BASE_URL) {
13
43
  };
14
44
  }
15
45
 
16
- export async function searchAnime(query, limit = 10) {
17
- 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(
18
53
  query
19
54
  )}`;
55
+ const isWanted = (text, list) =>
56
+ list.some(item => text.toLowerCase().includes(item.toLowerCase()));
20
57
  const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
21
58
  const $ = cheerio.load(res.data);
22
59
  const results = [];
@@ -34,6 +71,14 @@ export async function searchAnime(query, limit = 10) {
34
71
  .trim();
35
72
  const cover = anchor.find("img").first().attr("src");
36
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
+
37
82
  const altTitles = altRaw
38
83
  ? altRaw
39
84
  .split(",")
@@ -53,7 +98,7 @@ export async function searchAnime(query, limit = 10) {
53
98
  .filter(Boolean)
54
99
  : [];
55
100
 
56
- if (title && link) {
101
+ if (title && link && tagText && languageText) {
57
102
  results.push({
58
103
  title,
59
104
  altTitles,
@@ -123,37 +168,7 @@ export async function getSeasons(animeUrl, language = "vostfr") {
123
168
  return seasons;
124
169
  }
125
170
 
126
-
127
- import path from 'path';
128
- import { exec as execCallback } from 'child_process';
129
- import { promisify } from 'util';
130
- const execAsync = promisify(execCallback);
131
-
132
- async function ensureChromiumInstalled(customPath) {
133
- if (customPath) {
134
- if (fs.existsSync(customPath)) {
135
- console.log("customPath:", customPath);
136
- return customPath;
137
- } else {
138
- console.log(`The custom path to Chromium is invalid : ${customPath}`);
139
- }
140
- }
141
- const basePath = path.join(
142
- process.env.HOME || process.env.USERPROFILE,
143
- '.cache',
144
- 'puppeteer',
145
- 'chrome'
146
- );
147
- const chromiumPath = path.join(basePath, 'win64-135.0.7049.95', 'chrome-win64', 'chrome.exe');
148
-
149
- if (!fs.existsSync(chromiumPath)) {
150
- console.log("📦 Downloading Chromium 135.0.7049.95...");
151
- await execAsync('npx puppeteer browsers install chrome@135.0.7049.95');
152
- }
153
-
154
- return chromiumPath;
155
- }
156
- export async function getEpisodeTitles(animeUrl, customChromiumPath) {
171
+ export async function getEpisodeTitles(seasonUrl, customChromiumPath) {
157
172
  let browser;
158
173
  try {
159
174
  const puppeteer = await import('puppeteer');
@@ -177,7 +192,7 @@ export async function getEpisodeTitles(animeUrl, customChromiumPath) {
177
192
  }
178
193
  });
179
194
 
180
- await page.goto(animeUrl, { waitUntil: 'domcontentloaded' });
195
+ await page.goto(seasonUrl, { waitUntil: 'domcontentloaded' });
181
196
  await page.waitForSelector('#selectEpisodes');
182
197
 
183
198
  const titres = await page.$$eval('#selectEpisodes option', options =>
@@ -194,22 +209,21 @@ export async function getEpisodeTitles(animeUrl, customChromiumPath) {
194
209
  }
195
210
  }
196
211
 
197
-
198
- export async function getEmbed(animeUrl, hostPriority = ["vidmoly"], customChromiumPath) {
199
- const res = await axios.get(animeUrl, {
200
- 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("/")),
201
215
  });
202
216
 
203
217
  const $ = cheerio.load(res.data);
204
218
  const scriptTag = $('script[src*="episodes.js"]').attr("src");
205
219
  if (!scriptTag) throw new Error("No episodes script found");
206
220
 
207
- const scriptUrl = animeUrl.endsWith("/")
208
- ? animeUrl + scriptTag
209
- : animeUrl + "/" + scriptTag;
221
+ const scriptUrl = seasonUrl.endsWith("/")
222
+ ? seasonUrl + scriptTag
223
+ : seasonUrl + "/" + scriptTag;
210
224
 
211
225
  const episodesJs = await axios
212
- .get(scriptUrl, { headers: getHeaders(animeUrl) })
226
+ .get(scriptUrl, { headers: getHeaders(seasonUrl) })
213
227
  .then((r) => r.data);
214
228
 
215
229
  const matches = [
@@ -243,14 +257,13 @@ export async function getEmbed(animeUrl, hostPriority = ["vidmoly"], customChrom
243
257
  finalEmbeds.push(selectedUrl || null);
244
258
  }
245
259
 
246
- const titles = await getEpisodeTitles(animeUrl, customChromiumPath);
260
+ const titles = await getEpisodeTitles(seasonUrl, customChromiumPath);
247
261
  return finalEmbeds.map((url, i) => ({
248
262
  title: titles[i] || "Untitled",
249
263
  url,
250
264
  }));
251
265
  }
252
266
 
253
-
254
267
  export async function getAnimeInfo(animeUrl) {
255
268
  const res = await axios.get(animeUrl, { headers: getHeaders(CATALOGUE_URL) });
256
269
  const $ = cheerio.load(res.data);
@@ -285,7 +298,8 @@ export async function getAnimeInfo(animeUrl) {
285
298
 
286
299
  export async function getAvailableLanguages(
287
300
  seasonUrl,
288
- wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc"]
301
+ wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc", "vf1", "vf2"],
302
+ numberEpisodes = false
289
303
  ) {
290
304
  const languageLinks = [];
291
305
 
@@ -297,87 +311,96 @@ export async function getAvailableLanguages(
297
311
  headers: getHeaders(CATALOGUE_URL),
298
312
  });
299
313
  if (res.status === 200) {
300
- const episodeCount = (await getEmbed(languageUrl)).length;
301
- 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
+
302
321
  }
303
322
  } catch (error) {
304
323
  // If an error occurs (like a 404), we skip that language
305
324
  continue;
306
325
  }
307
326
  }
308
-
309
327
  return languageLinks;
310
328
  }
311
329
 
312
330
  export async function getAllAnime(
331
+ wantedLanguages = ["vostfr", "vf", "vastfr"],
332
+ wantedTypes = ["Anime", "Film"],
333
+ page = null,
313
334
  output = "anime_list.json",
314
335
  get_seasons = false
315
336
  ) {
316
- // BE CAREFUL, GET_SEASONS TAKES A VERY VERY LONG TIME TO FINISH
317
337
  let animeLinks = [];
318
- let page = 1;
319
338
 
320
- try {
321
- while (true) {
322
- const url = page === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${page}`;
323
- const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
324
- const $ = cheerio.load(res.data);
339
+ const isWanted = (text, list) =>
340
+ list.some(item => text.toLowerCase().includes(item.toLowerCase()));
325
341
 
326
- 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);
327
346
 
328
- if (containers.length === 0) {
329
- // console.log("No more anime found, stopping.");
330
- break;
331
- }
347
+ const containers = $("div.shrink-0.m-3.rounded.border-2");
332
348
 
333
- containers.each((_, el) => {
334
- const anchor = $(el).find("a");
335
- const title = anchor.find("h1").text().trim();
336
- 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");
337
353
 
338
- const tagText = anchor
339
- .find("p")
340
- .filter((_, p) => $(p).text().includes("Anime"))
341
- .first()
342
- .text();
354
+ const tagText = anchor.find("p").filter((_, p) =>
355
+ isWanted($(p).text(), wantedTypes)
356
+ ).first().text();
343
357
 
344
- if (title && link && tagText.includes("Anime")) {
345
- const fullUrl = link.startsWith("http") ? link : `${BASE_URL}${link}`;
346
- animeLinks.push({ title: title, url: fullUrl });
347
- }
348
- });
358
+ const languageText = anchor.find("p").filter((_, p) =>
359
+ isWanted($(p).text(), wantedLanguages)
360
+ ).first().text();
349
361
 
350
- page++;
351
- await new Promise((r) => setTimeout(r, 300));
352
- }
353
-
354
- // Deduplicate
355
- const uniqueLinks = animeLinks.filter(
356
- (item, index, self) => index === self.findIndex((i) => i.url === item.url)
357
- );
362
+ if (title && link && tagText && languageText) {
363
+ const fullUrl = link.startsWith("http") ? link : `${BASE_URL}${link}`;
364
+ animeLinks.push({ title, url: fullUrl });
365
+ }
366
+ });
358
367
 
359
- if (get_seasons) {
360
- // console.log("Fetching seasons for each anime...");
361
- for (let anime of uniqueLinks) {
362
- try {
363
- const seasons = await getSeasons(anime.url);
364
- anime.seasons = Array.isArray(seasons) ? seasons : [];
365
- } catch (err) {
366
- console.warn(
367
- `⚠️ Failed to fetch seasons for ${anime.name}: ${err.message}`
368
- );
369
- anime.seasons = [];
370
- }
368
+ return containers.length > 0;
369
+ };
371
370
 
372
- // Optional delay to avoid rate-limiting
373
- 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 = [];
374
379
  }
380
+ await new Promise(r => setTimeout(r, 300));
375
381
  }
382
+ };
376
383
 
377
- fs.writeFileSync(output, JSON.stringify(uniqueLinks, null, 2), "utf-8");
378
- 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
+ }
379
402
  } catch (err) {
380
- console.error("Error occurred:", err.message);
403
+ console.error("🔥 Erreur surpuissante détectée :", err.message);
381
404
  return false;
382
405
  }
383
406
  }
@@ -424,13 +447,21 @@ export async function getLatestEpisodes(languageFilter = null) {
424
447
  }
425
448
  }
426
449
 
427
- 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
+ ) {
428
456
  try {
429
457
  const res = await axios.get(
430
- `${CATALOGUE_URL}/?type[]=Anime&search=&random=1`,
458
+ `${CATALOGUE_URL}/?search=&random=1`,
431
459
  { headers: getHeaders(CATALOGUE_URL) }
432
460
  );
461
+
433
462
  const $ = cheerio.load(res.data);
463
+ const isWanted = (text, list) =>
464
+ list.some(item => text.toLowerCase().includes(item.toLowerCase()));
434
465
 
435
466
  const container = $("div.shrink-0.m-3.rounded.border-2").first();
436
467
  const anchor = container.find("a");
@@ -463,7 +494,15 @@ export async function getRandomAnime() {
463
494
  .filter(Boolean)
464
495
  : [];
465
496
 
466
- 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) {
467
506
  return {
468
507
  title,
469
508
  altTitles,
@@ -472,10 +511,15 @@ export async function getRandomAnime() {
472
511
  cover,
473
512
  };
474
513
  } else {
475
- 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
+ }
476
519
  }
477
520
  } catch (err) {
478
521
  console.error("Failed to fetch random anime:", err.message);
479
522
  return null;
480
523
  }
481
524
  }
525
+
@@ -33,9 +33,18 @@ export class AnimeScraper {
33
33
  }
34
34
  }
35
35
 
36
- async getEmbed(animeUrl, ...rest) {
36
+ async getEpisodeTitles(seasonUrl, ...rest) {
37
37
  try {
38
- 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);
39
48
  } catch (error) {
40
49
  console.error(`This scraper does not have the getEmbed function implemented or an error happened -> ${error}`);
41
50
  return null;
@@ -78,23 +87,15 @@ export class AnimeScraper {
78
87
  }
79
88
  }
80
89
 
81
- async getRandomAnime() {
90
+ async getRandomAnime(...rest) {
82
91
  try {
83
- return await this.source.getRandomAnime();
92
+ return await this.source.getRandomAnime(...rest);
84
93
  } catch (error) {
85
94
  console.error(`This scraper does not have the getRandomAnime function implemented or an error happened -> ${error}`);
86
95
  return null;
87
96
  }
88
97
  }
89
98
 
90
- async getEpisodeTitles(animeUrl, ...rest) {
91
- try {
92
- return await this.source.getEpisodeTitles(animeUrl, ...rest);
93
- } catch (error) {
94
- console.error(`This scraper does not have the getEpisodeTitles function implemented or an error happened -> ${error}`);
95
- return null;
96
- }
97
- }
98
99
  async getEpisodeInfo(animeUrl, ...rest) {
99
100
  try {
100
101
  return await this.source.getEpisodeInfo(animeUrl, ...rest);
@@ -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);
@@ -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('crunchyroll');
5
- const search = await scraper.searchAnime("86");
6
- console.log("Search Results:", search);
7
-
8
- const episodeInfo = await scraper.getEpisodeInfo(search[0].url, "S2")
9
- console.log("Episode Info:", episodeInfo)
10
- };
11
-
12
- main().catch(console.error);
13
-