better-ani-scraped 1.0.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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,241 @@
1
+ # Ani-Scraped Documentation
2
+
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
+
5
+ ---
6
+
7
+ ## Summary
8
+ - [Main class](#main-class)
9
+ - [`AnimeScrapper("animesama")` methods](#animescrapperanimesama-methods)
10
+ - [`AnimeScrapper("animepahe")` methods](#animescrapperanimepahe-methods)
11
+ - [Functions](#functions)
12
+
13
+ ---
14
+
15
+ ## Main class
16
+
17
+ ### `AnimeScraper(source)`
18
+ Creates a scrapper for the given source (only "animesama" and "animepahe" available at the moment).
19
+
20
+ ---
21
+
22
+ ## `AnimeScrapper("animesama")` methods
23
+
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)
33
+
34
+ ### `searchAnime(query, limit = 10)`
35
+ Searches for anime titles that match the given query.
36
+
37
+ - **Parameters:**
38
+ - `query` *(string)*: The search keyword.
39
+ - `limit` *(number)*: Maximum number of results to return (default: 10).
40
+ - **Returns:**
41
+ An array of anime objects:
42
+ ```js
43
+ [
44
+ {
45
+ title: string,
46
+ altTitles: string[],
47
+ genres: string[],
48
+ url: string,
49
+ cover: string
50
+ },
51
+ ...
52
+ ]
53
+ ```
54
+
55
+ ---
56
+
57
+ ### `getSeasons(animeUrl, language = "vostfr")`
58
+ Fetches all available seasons of an anime in the specified language.
59
+
60
+ - **Parameters:**
61
+ - `animeUrl` *(string)*: The full URL of the anime.
62
+ - `language` *(string)*: Language to filter by (default: "vostfr").
63
+ - **Returns:**
64
+ Either an array of season objects:
65
+ ```js
66
+ [
67
+ {
68
+ title: string,
69
+ url: string
70
+ },
71
+ ...
72
+ ]
73
+ ```
74
+ Or an error object if the language is not available.
75
+
76
+ ---
77
+
78
+ ### `getEmbed(animeUrl, hostPriority = ["sibnet", "vidmoly"])`
79
+ Retrieves embed URLs for episodes, prioritizing by host.
80
+
81
+ - **Parameters:**
82
+ - `animeUrl` *(string)*: URL of the anime’s season/episode page.
83
+ - `hostPriority` *(string[])*: Array of preferred hostnames.
84
+ - **Returns:**
85
+ An array of embed video:
86
+ ```js
87
+ {
88
+ title: string,
89
+ url: string,
90
+ }
91
+ ```
92
+ ---
93
+
94
+ ### `getAnimeInfo(animeUrl)`
95
+ Extracts basic information from an anime page.
96
+
97
+ - **Parameters:**
98
+ - `animeUrl` *(string)*: The URL of the anime.
99
+ - **Returns:**
100
+ An object containing:
101
+ ```js
102
+ {
103
+ title: string,
104
+ altTitles: string[],
105
+ cover: string,
106
+ genres: string[],
107
+ synopsis: string
108
+ }
109
+ ```
110
+
111
+ ---
112
+
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).
115
+
116
+ - **Parameters:**
117
+ - `seasonUrl` *(string)*: The season anime URL.
118
+ - `wantedLanguages` *(string[])*: Language codes to check (e.g., ["vostfr", "vf", "va", ...]).
119
+ - **Returns:**
120
+ Array of objects containing available languages and their episode count:
121
+ ```js
122
+ [
123
+ {
124
+ language: string,
125
+ episodeCount: int
126
+ }
127
+ ...
128
+ ]
129
+ ```
130
+
131
+ ---
132
+
133
+ ### `getAllAnime(output = "anime_list.json", get_seasons = false)`
134
+ Fetches the full anime catalog, optionally including season information.
135
+
136
+ - **Parameters:**
137
+ - `output` *(string)*: File name to save the result as JSON.
138
+ - `get_seasons` *(boolean)*: If `true`, also fetches seasons for each anime (very slow, ETA is still unknown).
139
+ - **Returns:**
140
+ `true` if successful, `false` otherwise.
141
+
142
+ ---
143
+
144
+ ### `getLatestEpisodes(languageFilter = null)`
145
+ Scrapes the latest released episodes, optionally filtered by language.
146
+
147
+ - **Parameters:**
148
+ - `languageFilter` *(string[]|null)*: If set, filters episodes by language in the array. If null, returns all episodes.
149
+ - **Returns:**
150
+ Array of episode objects:
151
+ ```js
152
+ {
153
+ title: string,
154
+ url: string,
155
+ cover: string,
156
+ language: string,
157
+ episode: string
158
+ }
159
+ ```
160
+
161
+ ---
162
+
163
+ ### `getRandomAnime()`
164
+ Fetches a random anime from the catalogue.
165
+
166
+ - **Returns:**
167
+ An anime object:
168
+ ```js
169
+ {
170
+ title: string,
171
+ altTitles: string[],
172
+ genres: string[],
173
+ url: string,
174
+ cover: string
175
+ }
176
+ ```
177
+
178
+ ---
179
+
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
+
188
+ ---
189
+
190
+ ## `AnimeScrapper("animepahe")` methods
191
+
192
+ - [searchAnime](#searchanimequery)
193
+
194
+
195
+ ### `searchAnime(query)`
196
+ Searches for anime titles that match the given query.
197
+
198
+ - **Parameters:**
199
+ - `query` *(string)*: The search keyword.
200
+ - **Returns:**
201
+ An array of anime objects:
202
+ ```js
203
+ [
204
+ {
205
+ id: int,
206
+ title: string,
207
+ type: string,
208
+ episodes: int,
209
+ status: string,
210
+ season: string,
211
+ year: int,
212
+ score: float,
213
+ session: string,
214
+ cover: string,
215
+ url: string
216
+ },
217
+ ...
218
+ ]
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Functions
224
+
225
+ - [getVideoUrlFromEmbed](#getvideourlfromembedsource-embedurl)
226
+
227
+ ### `getVideoUrlFromEmbed(source, embedUrl)`
228
+ Retrieves the video URL of the source's embed.
229
+
230
+ - **Parameters:**
231
+ - `source` *(string)*: The embed source (only "sibnet" available at the moment)
232
+ - `embedUrl` *(string)*: The embed url of the given source.
233
+ - **Returns:**
234
+ A video URL as a string.
235
+
236
+ ---
237
+ ---
238
+ ---
239
+ ---
240
+
241
+ > ⚠️ This project scrapes data from online sources. Use at your own risk.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 rgpegasus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ <h1 align="center">
2
+ Better-Ani-Scraped
3
+ </h1>
4
+ <p align="center">
5
+ A set of utility functions for scraping anime data from multiple sources. This tool allows you to search for anime, retrieve information, get episodes, and more.
6
+ <p>
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/ani-scraped"><img src="https://img.shields.io/npm/v/ani-scraped"></a>
9
+ <a href="https://www.npmjs.com/package/ani-scraped"><img src="https://img.shields.io/npm/dw/ani-scraped"></a>
10
+ <p>
11
+
12
+ <p align="center">
13
+ <a href="#installation">Installation</a> | <a href="#example-usage">Example Usage</a> | <a href="#documentation">Documentation</a> | <a href="#license">License</a> | <a href="#legal-disclaimer">Legal Disclaimer</a>
14
+ </p>
15
+
16
+
17
+ ## Installation
18
+ See [Legal Disclaimer](#legal-disclaimer).
19
+ ```
20
+ npm install better-ani-scraped@latest
21
+ ```
22
+
23
+ ## Example Usage
24
+ View files in the [examples](https://github.com/rgpegasus/better-ani-scraped/tree/main/examples) folder.
25
+
26
+ ## Documentation
27
+ Full API reference available in the [documentation](DOCUMENTATION.md) file.
28
+
29
+ ## License
30
+ This package is under the [MIT license](LICENSE).
31
+
32
+ ## Legal Disclaimer
33
+ This package is intended for **educational purposes only**. Scraping content from websites such as Animesama and Animepahe may be a violation of their terms of service, intellectual property rights, or other applicable laws. By using this package, you acknowledge that you are solely responsible for ensuring that your use complies with all relevant laws and regulations, including but not limited to:
34
+ - The Terms of Service of the websites being scraped
35
+ - Copyright laws regarding the content on the websites
36
+ - Any other applicable laws or regulations in your country or jurisdiction
37
+
38
+ The author of this package assumes **no responsibility for any legal issues**, damages, or consequences arising from the use of this package.
39
+
40
+ Please make sure you review the websites' terms of service and obtain permission from the website owners if necessary before using this package. If in doubt, consult with a legal professional regarding the legality of web scraping in your jurisdiction.
41
+
42
+ ## TODO
43
+ - Implement more animepahe methods
44
+
45
+
46
+
47
+ ### Special thanks to
48
+ - [Hxpe Dev](https://github.com/hxpe_dev)
@@ -0,0 +1,23 @@
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("frieren");
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[11])
20
+ console.log("Video URL:", videoUrl);
21
+ };
22
+
23
+ main().catch(console.error);
@@ -0,0 +1,14 @@
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);
@@ -0,0 +1,9 @@
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);
@@ -0,0 +1,13 @@
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);
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { getVideoUrlFromEmbed } from "./utils/dispatcher.js";
2
+ import { AnimeScraper } from "./scrapers/scrapers.js";
3
+
4
+ export {
5
+ AnimeScraper,
6
+ getVideoUrlFromEmbed,
7
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "better-ani-scraped",
3
+ "version": "1.0.0",
4
+ "description": "Scrape anime data from different sources (only anime-sama.fr for the moment)",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "anime",
11
+ "scraper",
12
+ "anime-sama",
13
+ "ani-scraped",
14
+ "better-ani-scraped"
15
+ ],
16
+ "author": "rgpegasus",
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "dependencies": {
20
+ "axios": "^1.8.4",
21
+ "cheerio": "^1.0.0",
22
+ "puppeteer": "^24.6.1"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/rgpegasus/better-ani-scraped"
27
+ }
28
+ }
@@ -0,0 +1,31 @@
1
+ import puppeteer from "puppeteer";
2
+
3
+ const BASE_URL = "https://animepahe.ru";
4
+
5
+ export async function searchAnime(query) {
6
+ const browser = await puppeteer.launch({ headless: true });
7
+ const page = await browser.newPage();
8
+
9
+ // Go to any page, we need to load a page to enable network interception
10
+ await page.goto(BASE_URL);
11
+
12
+ // Intercept the API request using Puppeteer
13
+ const searchResults = await page.evaluate(async (query) => {
14
+ const response = await fetch(`https://animepahe.ru/api?m=search&q=${query}`);
15
+ const data = await response.json();
16
+
17
+ // Map over the data, rename 'poster' to 'cover', and remove 'poster' field
18
+ return data.data.map(anime => {
19
+ const { poster, ...rest } = anime; // Destructure 'poster' and keep the rest of the properties
20
+ return {
21
+ ...rest,
22
+ cover: poster,
23
+ url: `https://animepahe.ru/anime/${anime.session}`
24
+ };
25
+ });
26
+ }, query);
27
+
28
+ await browser.close();
29
+
30
+ return searchResults;
31
+ }
@@ -0,0 +1,431 @@
1
+ import axios from "axios";
2
+ import * as cheerio from "cheerio";
3
+ import fs from "fs";
4
+ import puppeteer from'puppeteer';
5
+
6
+ const BASE_URL = "https://anime-sama.fr";
7
+ const CATALOGUE_URL = `${BASE_URL}/catalogue`;
8
+
9
+ function getHeaders(referer = BASE_URL) {
10
+ return {
11
+ "User-Agent": "Mozilla/5.0",
12
+ "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
13
+ Referer: referer,
14
+ };
15
+ }
16
+
17
+ export async function searchAnime(query, limit = 10) {
18
+ const url = `${CATALOGUE_URL}/?type%5B%5D=Anime&search=${encodeURIComponent(
19
+ query
20
+ )}`;
21
+ const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
22
+ const $ = cheerio.load(res.data);
23
+ const results = [];
24
+
25
+ $("a.flex.divide-x").each((i, el) => {
26
+ if (i >= limit) return false;
27
+
28
+ const anchor = $(el);
29
+ const link = anchor.attr("href");
30
+ const title = anchor.find("h1").first().text().trim();
31
+ const altRaw = anchor
32
+ .find("p.text-xs.opacity-40.italic")
33
+ .first()
34
+ .text()
35
+ .trim();
36
+ const cover = anchor.find("img").first().attr("src");
37
+
38
+ const altTitles = altRaw
39
+ ? altRaw
40
+ .split(",")
41
+ .map((t) => t.trim())
42
+ .filter(Boolean)
43
+ : [];
44
+
45
+ const genreRaw = anchor
46
+ .find("p.text-xs.font-medium.text-gray-300")
47
+ .first()
48
+ .text()
49
+ .trim();
50
+ const genres = genreRaw
51
+ ? genreRaw
52
+ .split(",")
53
+ .map((g) => g.trim())
54
+ .filter(Boolean)
55
+ : [];
56
+
57
+ if (title && link) {
58
+ results.push({
59
+ title,
60
+ altTitles,
61
+ genres,
62
+ url: link.startsWith("http") ? link : `${CATALOGUE_URL}${link}`,
63
+ cover,
64
+ });
65
+ }
66
+ });
67
+
68
+ return results;
69
+ }
70
+
71
+ export async function getSeasons(animeUrl, language = "vostfr") {
72
+ const res = await axios.get(animeUrl, { headers: getHeaders(CATALOGUE_URL) });
73
+ const html = res.data;
74
+
75
+ // Only keep the part before the Kai section
76
+ const mainAnimeOnly = html.split("Anime Version Kai")[0];
77
+
78
+ const $ = cheerio.load(mainAnimeOnly);
79
+ const scriptTags = $("script")
80
+ .toArray()
81
+ .filter((script) => {
82
+ return $(script).html().includes("panneauAnime");
83
+ });
84
+
85
+ const animeName = animeUrl.split("/")[4];
86
+ const seasons = [];
87
+ let languageAvailable = false;
88
+
89
+ for (let script of scriptTags) {
90
+ const content = $(script).html();
91
+
92
+ // Remove anything inside comments either ("/* */" or "//")
93
+ const uncommentedContent = content
94
+ .replace(/\/\*[\s\S]*?\*\//g, "")
95
+ .replace(/\/\/.*$/gm, "");
96
+
97
+ const matches = [
98
+ ...uncommentedContent.matchAll(/panneauAnime\("([^"]+)", "([^"]+)"\);/g),
99
+ ];
100
+
101
+ for (let match of matches) {
102
+ const title = match[1];
103
+ const href = match[2].split("/")[0];
104
+ const fullUrl = `${CATALOGUE_URL}/${animeName}/${href}/${language}`;
105
+
106
+ try {
107
+ const check = await axios.head(fullUrl, {
108
+ headers: getHeaders(animeUrl),
109
+ });
110
+ if (check.status === 200) {
111
+ languageAvailable = true;
112
+ seasons.push({ title, url: fullUrl });
113
+ }
114
+ } catch (err) {
115
+ // Ignore missing URLs
116
+ }
117
+ }
118
+ }
119
+
120
+ if (!languageAvailable) {
121
+ return { error: `Language "${language}" is not available for this anime.` };
122
+ }
123
+
124
+ return seasons;
125
+ }
126
+
127
+ async function getEpisodeTitles(animeUrl) {
128
+ let browser;
129
+ try {
130
+ browser = await puppeteer.launch({
131
+ headless: true,
132
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
133
+ });
134
+ const page = await browser.newPage();
135
+ await page.setRequestInterception(true);
136
+ page.on('request', (req) => {
137
+ const blocked = ['image', 'stylesheet', 'font', 'media'];
138
+ if (blocked.includes(req.resourceType())) {
139
+ req.abort();
140
+ } else {
141
+ req.continue();
142
+ }
143
+ });
144
+ await page.goto(animeUrl, { waitUntil: 'domcontentloaded' });
145
+ await page.waitForSelector('#selectEpisodes');
146
+ // Récupération des titres d'épisodes
147
+ const titres = await page.$$eval('#selectEpisodes option', options =>
148
+ options.map(o => o.textContent.trim())
149
+ );
150
+ return titres;
151
+ } catch (error) {
152
+ console.error('Erreur dans la récupération des titres:', error);
153
+ return [];
154
+ } finally {
155
+ if (browser) await browser.close();
156
+ }
157
+ }
158
+
159
+ export async function getEmbed(animeUrl, hostPriority = ["sibnet", "vidmoly"]) {
160
+ let res, episodesJs;
161
+ try {
162
+ res = await axios.get(animeUrl, {
163
+ headers: getHeaders(animeUrl.split("/").slice(0, 5).join("/")),
164
+ });
165
+
166
+ const $ = cheerio.load(res.data);
167
+ const scriptTag = $('script[src*="episodes.js"]').attr("src");
168
+
169
+ if (!scriptTag) throw new Error("No episodes script found");
170
+ const scriptUrl = animeUrl.endsWith("/") ? animeUrl + scriptTag : animeUrl + "/" + scriptTag;
171
+
172
+ episodesJs = await axios.get(scriptUrl, { headers: getHeaders(animeUrl) }).then(r => r.data);
173
+
174
+ const match = episodesJs.match(/var\s+eps\d+\s*=\s*(\[[^\]]+\])/);
175
+ if (!match) throw new Error("No episode array found");
176
+
177
+ const arrayString = match[1];
178
+ let links = [];
179
+
180
+ try {
181
+ links = eval(arrayString);
182
+ } catch (e) {
183
+ console.warn("Could not parse episode links array:", e);
184
+ }
185
+ const titles = await getEpisodeTitles(animeUrl);
186
+
187
+ const results = titles.slice(0, links.length).map((title, i) => ({
188
+ title,
189
+ url: [links[i]]
190
+ }));
191
+ for (const host of hostPriority) {
192
+ const filtered = results.filter(ep =>
193
+ ep.url.some(link => link.includes(host))
194
+ );
195
+ if (filtered.length) return filtered;
196
+ }
197
+ return results;
198
+ } catch (error) {
199
+ console.error('Erreur lors de la récupération des données d\'épisodes:', error);
200
+ return [];
201
+ }
202
+ }
203
+
204
+ export async function getAnimeInfo(animeUrl) {
205
+ const res = await axios.get(animeUrl, { headers: getHeaders(CATALOGUE_URL) });
206
+ const $ = cheerio.load(res.data);
207
+
208
+ const cover = $("#coverOeuvre").attr("src");
209
+ const title = $("#titreOeuvre").text();
210
+ const altRaw = $("#titreAlter").text();
211
+ const altTitles = altRaw
212
+ ? altRaw
213
+ .split(",")
214
+ .map((t) => t.trim())
215
+ .filter(Boolean)
216
+ : [];
217
+
218
+ const genres = $("h2:contains('Genres')")
219
+ .next("a")
220
+ .text()
221
+ .trim()
222
+ .split(",")
223
+ .map((genre) => genre.trim());
224
+
225
+ const synopsis = $("h2:contains('Synopsis')").next("p").text().trim();
226
+
227
+ return {
228
+ title,
229
+ altTitles,
230
+ cover,
231
+ genres,
232
+ synopsis,
233
+ };
234
+ }
235
+
236
+ export async function getAvailableLanguages(
237
+ seasonUrl,
238
+ wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc"]
239
+ ) {
240
+ const languageLinks = [];
241
+
242
+ // Iterate over each possible language and check if the page exists
243
+ for (let language of wantedLanguages) {
244
+ const languageUrl = seasonUrl.replace("vostfr", `${language}`);
245
+ try {
246
+ const res = await axios.get(languageUrl, {
247
+ headers: getHeaders(CATALOGUE_URL),
248
+ });
249
+ if (res.status === 200) {
250
+ const episodeCount = (await getEmbed(languageUrl)).length;
251
+ languageLinks.push({ language: language.toUpperCase(), episodeCount: episodeCount });
252
+ }
253
+ } catch (error) {
254
+ // If an error occurs (like a 404), we skip that language
255
+ continue;
256
+ }
257
+ }
258
+
259
+ return languageLinks;
260
+ }
261
+
262
+ export async function getAllAnime(
263
+ output = "anime_list.json",
264
+ get_seasons = false
265
+ ) {
266
+ // BE CAREFUL, GET_SEASONS TAKES A VERY VERY LONG TIME TO FINISH
267
+ let animeLinks = [];
268
+ let page = 1;
269
+
270
+ try {
271
+ while (true) {
272
+ const url = page === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${page}`;
273
+ const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
274
+ const $ = cheerio.load(res.data);
275
+
276
+ const containers = $("div.shrink-0.m-3.rounded.border-2");
277
+
278
+ if (containers.length === 0) {
279
+ // console.log("No more anime found, stopping.");
280
+ break;
281
+ }
282
+
283
+ containers.each((_, el) => {
284
+ const anchor = $(el).find("a");
285
+ const title = anchor.find("h1").text().trim();
286
+ const link = anchor.attr("href");
287
+
288
+ const tagText = anchor
289
+ .find("p")
290
+ .filter((_, p) => $(p).text().includes("Anime"))
291
+ .first()
292
+ .text();
293
+
294
+ if (title && link && tagText.includes("Anime")) {
295
+ const fullUrl = link.startsWith("http") ? link : `${BASE_URL}${link}`;
296
+ animeLinks.push({ title: title, url: fullUrl });
297
+ }
298
+ });
299
+
300
+ page++;
301
+ await new Promise((r) => setTimeout(r, 300));
302
+ }
303
+
304
+ // Deduplicate
305
+ const uniqueLinks = animeLinks.filter(
306
+ (item, index, self) => index === self.findIndex((i) => i.url === item.url)
307
+ );
308
+
309
+ if (get_seasons) {
310
+ // console.log("Fetching seasons for each anime...");
311
+ for (let anime of uniqueLinks) {
312
+ try {
313
+ const seasons = await getSeasons(anime.url);
314
+ anime.seasons = Array.isArray(seasons) ? seasons : [];
315
+ } catch (err) {
316
+ console.warn(
317
+ `⚠️ Failed to fetch seasons for ${anime.name}: ${err.message}`
318
+ );
319
+ anime.seasons = [];
320
+ }
321
+
322
+ // Optional delay to avoid rate-limiting
323
+ await new Promise((r) => setTimeout(r, 300));
324
+ }
325
+ }
326
+
327
+ fs.writeFileSync(output, JSON.stringify(uniqueLinks, null, 2), "utf-8");
328
+ return true;
329
+ } catch (err) {
330
+ console.error("Error occurred:", err.message);
331
+ return false;
332
+ }
333
+ }
334
+
335
+ export async function getLatestEpisodes(languageFilter = null) {
336
+ try {
337
+ const res = await axios.get(BASE_URL, { headers: getHeaders() });
338
+ const $ = cheerio.load(res.data);
339
+
340
+ const container = $("#containerAjoutsAnimes");
341
+ const episodes = [];
342
+
343
+ container.find("a").each((_, el) => {
344
+ const link = $(el).attr("href");
345
+ const title = $(el).find("h1").text().trim();
346
+ const cover = $(el).find("img").attr("src");
347
+
348
+ const buttons = $(el).find("button");
349
+ const language = $(buttons[0]).text().trim().toLowerCase(); // Normalisation
350
+ const episode = $(buttons[1]).text().trim();
351
+
352
+ if (
353
+ title &&
354
+ link &&
355
+ cover &&
356
+ language &&
357
+ episode &&
358
+ (languageFilter === null || languageFilter.map(l => l.toLowerCase()).includes(language.toLowerCase()))
359
+ ) {
360
+ episodes.push({
361
+ title: title,
362
+ url: link,
363
+ cover,
364
+ language,
365
+ episode,
366
+ });
367
+ }
368
+ });
369
+
370
+ return episodes;
371
+ } catch (err) {
372
+ console.error("Failed to fetch today episodes:", err.message);
373
+ return [];
374
+ }
375
+ }
376
+
377
+ export async function getRandomAnime() {
378
+ try {
379
+ const res = await axios.get(
380
+ `${CATALOGUE_URL}/?type[]=Anime&search=&random=1`,
381
+ { headers: getHeaders(CATALOGUE_URL) }
382
+ );
383
+ const $ = cheerio.load(res.data);
384
+
385
+ const container = $("div.shrink-0.m-3.rounded.border-2").first();
386
+ const anchor = container.find("a");
387
+ const link = anchor.attr("href");
388
+ const title = anchor.find("h1").first().text().trim();
389
+ const altRaw = anchor
390
+ .find("p.text-xs.opacity-40.italic")
391
+ .first()
392
+ .text()
393
+ .trim();
394
+ const cover = anchor.find("img").first().attr("src");
395
+
396
+ const altTitles = altRaw
397
+ ? altRaw
398
+ .split(",")
399
+ .map((t) => t.trim())
400
+ .filter(Boolean)
401
+ : [];
402
+
403
+ const genreRaw = anchor
404
+ .find("p.text-xs.font-medium.text-gray-300")
405
+ .first()
406
+ .text()
407
+ .trim();
408
+
409
+ const genres = genreRaw
410
+ ? genreRaw
411
+ .split(",")
412
+ .map((g) => g.trim())
413
+ .filter(Boolean)
414
+ : [];
415
+
416
+ if (title && link) {
417
+ return {
418
+ title,
419
+ altTitles,
420
+ genres,
421
+ url: link.startsWith("http") ? link : `${CATALOGUE_URL}${link}`,
422
+ cover,
423
+ };
424
+ } else {
425
+ throw new Error("No anime found in random response.");
426
+ }
427
+ } catch (err) {
428
+ console.error("Failed to fetch random anime:", err.message);
429
+ return null;
430
+ }
431
+ }
@@ -0,0 +1,86 @@
1
+ import * as animesama from "./animesama.js";
2
+ import * as animepahe from "./animepahe.js";
3
+
4
+ export class AnimeScraper {
5
+ constructor(source) {
6
+ if (source === 'animepahe') {
7
+ this.source = animepahe;
8
+ } else if (source === 'animesama') {
9
+ this.source = animesama;
10
+ } else {
11
+ throw new Error('Invalid source. Choose either "animepahe" or "animesama".');
12
+ }
13
+ }
14
+
15
+ async searchAnime(query, ...rest) {
16
+ try {
17
+ return await this.source.searchAnime(query, ...rest);
18
+ } catch (error) {
19
+ console.error(`This scraper does not have the searchAnime function implemented or an error happened -> ${error}`);
20
+ return null;
21
+ }
22
+ }
23
+
24
+ async getSeasons(animeUrl, ...rest) {
25
+ try {
26
+ return await this.source.getSeasons(animeUrl, ...rest);
27
+ } catch (error) {
28
+ console.error(`This scraper does not have the getSeasons function implemented or an error happened -> ${error}`);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async getEmbed(animeUrl, ...rest) {
34
+ try {
35
+ return await this.source.getEmbed(animeUrl, ...rest);
36
+ } catch (error) {
37
+ console.error(`This scraper does not have the getEmbed function implemented or an error happened -> ${error}`);
38
+ return null;
39
+ }
40
+ }
41
+
42
+ async getAnimeInfo(animeUrl) {
43
+ try {
44
+ return await this.source.getAnimeInfo(animeUrl);
45
+ } catch (error) {
46
+ console.error(`This scraper does not have the getAnimeInfo function implemented or an error happened -> ${error}`);
47
+ return null;
48
+ }
49
+ }
50
+
51
+ async getAvailableLanguages(animeUrl, ...rest) {
52
+ try {
53
+ return await this.source.getAvailableLanguages(animeUrl, ...rest);
54
+ } catch (error) {
55
+ console.error(`This scraper does not have the getAvailableLanguages function implemented or an error happened -> ${error}`);
56
+ return null;
57
+ }
58
+ }
59
+
60
+ async getAllAnime(...rest) {
61
+ try {
62
+ return await this.source.getAllAnime(...rest);
63
+ } catch (error) {
64
+ console.error(`This scraper does not have the getAllAnime function implemented or an error happened -> ${error}`);
65
+ return null;
66
+ }
67
+ }
68
+
69
+ async getLatestEpisodes(...rest) {
70
+ try {
71
+ return await this.source.getLatestEpisodes(...rest);
72
+ } catch (error) {
73
+ console.error(`This scraper does not have the getLatestEpisodes function implemented or an error happened -> ${error}`);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async getRandomAnime() {
79
+ try {
80
+ return await this.source.getRandomAnime();
81
+ } catch (error) {
82
+ console.error(`This scraper does not have the getRandomAnime function implemented or an error happened -> ${error}`);
83
+ return null;
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,9 @@
1
+ import * as extractor from "./extractVideoUrl.js";
2
+
3
+ export async function getVideoUrlFromEmbed(source, embedUrl) {
4
+ if (source === "sibnet") {
5
+ return await extractor.getSibnetVideo(embedUrl);
6
+ }
7
+
8
+ throw new Error(`Unsupported embed source: ${source}`);
9
+ }
@@ -0,0 +1,67 @@
1
+ import axios from "axios";
2
+ import * as cheerio from "cheerio";
3
+
4
+ export async function getSibnetVideo(embedUrl) {
5
+ let intermediaries = [];
6
+ let realUrl = "";
7
+
8
+ const getIntermediary = async () => {
9
+ try {
10
+ const { data } = await axios.get(embedUrl, { headers: getHeaders(embedUrl) });
11
+ const $ = cheerio.load(data);
12
+ const script = $("script")
13
+ .toArray()
14
+ .map((s) => $(s).html())
15
+ .find((s) => s.includes("player.src"));
16
+ const match = script?.match(/player\.src\(\[{src:\s*["']([^"']+)["']/);
17
+ if (match) intermediaries.push(`https://video.sibnet.ru${match[1]}`);
18
+ return !!match;
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+
24
+ const followRedirection = async () => {
25
+ if (!intermediaries.length) return false;
26
+ try {
27
+ const first = await axios.get(intermediaries[0], {
28
+ headers: getHeaders(embedUrl),
29
+ maxRedirects: 0,
30
+ validateStatus: (s) => s >= 200 && s < 303,
31
+ });
32
+
33
+ const redirect1 = correct(first.headers.location);
34
+ intermediaries.push(redirect1);
35
+
36
+ const second = await axios.get(redirect1, {
37
+ headers: getHeaders(intermediaries[0]),
38
+ maxRedirects: 0,
39
+ validateStatus: (s) => s >= 200 && s < 303,
40
+ });
41
+
42
+ realUrl =
43
+ second.status === 302
44
+ ? correct(second.headers.location)
45
+ : second.status === 200
46
+ ? intermediaries.pop()
47
+ : "";
48
+ return !!realUrl;
49
+ } catch {
50
+ return false;
51
+ }
52
+ };
53
+
54
+ const correct = (url) => (url.startsWith("https:") ? url : `https:${url}`);
55
+
56
+ const getHeaders = (referer) => ({
57
+ Accept: "*/*",
58
+ Referer: referer,
59
+ Range: "bytes=0-",
60
+ "User-Agent":
61
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
62
+ });
63
+
64
+ return (await getIntermediary()) && (await followRedirection())
65
+ ? realUrl
66
+ : null;
67
+ }