better-ani-scraped 1.7.1 → 1.7.3
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 +1 -1
- package/examples/animesama/example_usage_getAllTitleScans.js +1 -1
- package/examples/animesama/example_usage_getAnimeInfo.js +1 -1
- package/examples/animesama/example_usage_getAvailableLanguages.js +1 -1
- package/examples/animesama/example_usage_getEmbed.js +1 -1
- package/examples/animesama/example_usage_getEpisodeTitles.js +1 -1
- package/examples/animesama/example_usage_getImgScans.js +1 -1
- package/examples/animesama/example_usage_getSeasons.js +1 -1
- package/package.json +2 -2
- package/scrapers/animesama.js +169 -95
- package/utils/dispatcher.js +8 -5
- package/utils/extractVideoUrl.js +102 -13
package/DOCUMENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
,,,,# Better-Ani-Scraped Documentation
|
|
2
2
|
|
|
3
|
-
A set of utility functions for scraping anime data from multiple sources (only [anime-sama](https://anime-sama.
|
|
3
|
+
A set of utility functions for scraping anime data from multiple sources (only [anime-sama](https://anime-sama.org) 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
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const mangaUrl = "https://anime-sama.
|
|
5
|
+
const mangaUrl = "https://anime-sama.org/catalogue/one-piece/scan_noir-et-blanc/vf";
|
|
6
6
|
const chapterTitles = await animesama.getAllTitleScans(mangaUrl, true);
|
|
7
7
|
console.log("Titles:", chapterTitles);
|
|
8
8
|
};
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const animeUrl = "https://anime-sama.
|
|
5
|
+
const animeUrl = "https://anime-sama.org/catalogue/86-eighty-six/";
|
|
6
6
|
|
|
7
7
|
const animeInfo = await animesama.getAnimeInfo(animeUrl);
|
|
8
8
|
console.log(animeInfo);
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const seasonUrl = "https://anime-sama.
|
|
5
|
+
const seasonUrl = "https://anime-sama.org/catalogue/86-eighty-six/saison1/vostfr/";
|
|
6
6
|
|
|
7
7
|
const animeLanguages = await animesama.getAvailableLanguages(seasonUrl, ["vostfr", "vf", "va", "vkr","vcn", "vqc", "vf1", "vf2"], false);
|
|
8
8
|
console.log(animeLanguages);
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const seasonUrl = "https://anime-sama.
|
|
5
|
+
const seasonUrl = "https://anime-sama.org/catalogue/one-piece/saison11/vostfr";
|
|
6
6
|
|
|
7
7
|
const embeds = await animesama.getEmbed(seasonUrl, ["smoothpre", "movearnpre", "sibnet", "vidmoly", "sendvid"], true, true);
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const seasonUrl = "https://anime-sama.
|
|
5
|
+
const seasonUrl = "https://anime-sama.org/catalogue/86-eighty-six/saison1/vostfr/";
|
|
6
6
|
|
|
7
7
|
const episodeTitles = await animesama.getEpisodeTitles(seasonUrl);
|
|
8
8
|
console.log("Episode Titles:", episodeTitles);
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const scansUrl = "https://anime-sama.
|
|
5
|
+
const scansUrl = "https://anime-sama.org/catalogue/drcl-midnight-children/scan/vf";
|
|
6
6
|
const scansImgUrl = await animesama.getImgScans(scansUrl, 9);
|
|
7
7
|
console.log("Image scans:", scansImgUrl);
|
|
8
8
|
};
|
|
@@ -2,7 +2,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
2
2
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
|
-
const animeUrl = "https://anime-sama.
|
|
5
|
+
const animeUrl = "https://anime-sama.org/catalogue/drcl-midnight-children";
|
|
6
6
|
|
|
7
7
|
const seasons = await animesama.getSeasons(animeUrl, ["vostfr", "vf"], ["Anime", "Scans"]);
|
|
8
8
|
console.log("Seasons:", seasons);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-ani-scraped",
|
|
3
|
-
"version": "1.7.
|
|
4
|
-
"description": "Scrape anime data from different sources (only anime-sama.
|
|
3
|
+
"version": "1.7.3",
|
|
4
|
+
"description": "Scrape anime data from different sources (only anime-sama.org, animepahe and crunchyroll for the moment)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
package/scrapers/animesama.js
CHANGED
|
@@ -8,7 +8,7 @@ import { title } from "process";
|
|
|
8
8
|
const execAsync = promisify(execCallback);
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
const BASE_URL = "https://anime-sama.
|
|
11
|
+
const BASE_URL = "https://anime-sama.org";
|
|
12
12
|
const CATALOGUE_URL = `${BASE_URL}/catalogue`;
|
|
13
13
|
|
|
14
14
|
async function ensureChromiumInstalled(customPath) {
|
|
@@ -51,11 +51,14 @@ export async function searchAnime(
|
|
|
51
51
|
wantedTypes = null,
|
|
52
52
|
page = null
|
|
53
53
|
) {
|
|
54
|
-
const languages = Array.isArray(wantedLanguages)
|
|
54
|
+
const languages = Array.isArray(wantedLanguages)
|
|
55
|
+
? wantedLanguages
|
|
56
|
+
: ["vostfr", "vf", "vastfr"];
|
|
55
57
|
const types = Array.isArray(wantedTypes) ? wantedTypes : ["Anime", "Film"];
|
|
56
58
|
|
|
57
59
|
const isWanted = (text, list) =>
|
|
58
|
-
list.length === 0 ||
|
|
60
|
+
list.length === 0 ||
|
|
61
|
+
list.some((item) => text.toLowerCase().includes(item.toLowerCase()));
|
|
59
62
|
|
|
60
63
|
const results = [];
|
|
61
64
|
|
|
@@ -63,47 +66,75 @@ export async function searchAnime(
|
|
|
63
66
|
const url =
|
|
64
67
|
pageNum === 1
|
|
65
68
|
? `${CATALOGUE_URL}/?search=${encodeURIComponent(query)}`
|
|
66
|
-
: `${CATALOGUE_URL}/?search=${encodeURIComponent(
|
|
69
|
+
: `${CATALOGUE_URL}/?search=${encodeURIComponent(
|
|
70
|
+
query
|
|
71
|
+
)}&page=${pageNum}`;
|
|
67
72
|
|
|
68
73
|
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
69
74
|
const $ = cheerio.load(res.data);
|
|
70
|
-
|
|
71
|
-
const containers = $("a.flex.divide-x");
|
|
72
|
-
|
|
75
|
+
const containers = $("div.catalog-card > a");
|
|
73
76
|
containers.each((_, el) => {
|
|
74
77
|
if (results.length >= limit) return false;
|
|
75
78
|
|
|
76
79
|
const anchor = $(el);
|
|
77
80
|
const link = anchor.attr("href");
|
|
78
|
-
const title = anchor.find("
|
|
79
|
-
const altRaw = anchor.find("p
|
|
81
|
+
const title = anchor.find("h2").first().text().trim();
|
|
82
|
+
const altRaw = anchor.find("p").first().text().trim();
|
|
80
83
|
const cover = anchor.find("img").first().attr("src");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const synopsis = anchor.find("div.synopsis-content").first().text();
|
|
85
|
+
|
|
86
|
+
const typesRaw = anchor
|
|
87
|
+
.find(".info-row")
|
|
88
|
+
.filter(
|
|
89
|
+
(_, row) => $(row).find(".info-label").text().trim() === "Types"
|
|
90
|
+
)
|
|
91
|
+
.find(".info-value")
|
|
92
|
+
.toArray()
|
|
93
|
+
.map((el) => $(el).text().trim())
|
|
94
|
+
.filter((t) => isWanted(t, types));
|
|
95
|
+
const filteredTypes = wantedTypes.length === 0 ? typesRaw : typesRaw;
|
|
96
|
+
const hasScans = filteredTypes.some(
|
|
97
|
+
(t) => t.toLowerCase() === "scans".toLowerCase()
|
|
98
|
+
);
|
|
99
|
+
const languagesRaw = anchor
|
|
100
|
+
.find(".info-row")
|
|
101
|
+
.filter(
|
|
102
|
+
(_, row) => $(row).find(".info-label").text().trim() === "Langues"
|
|
103
|
+
)
|
|
104
|
+
.find(".info-value")
|
|
105
|
+
.toArray()
|
|
106
|
+
.map((el) => $(el).text().trim())
|
|
107
|
+
.filter((l) => isWanted(l, languages));
|
|
108
|
+
|
|
109
|
+
const filteredLanguages =
|
|
110
|
+
wantedLanguages.length === 0 || hasScans
|
|
111
|
+
? languagesRaw
|
|
112
|
+
: languagesRaw.filter((lang) => isWanted(lang, wantedLanguages));
|
|
89
113
|
|
|
90
114
|
const altTitles = altRaw
|
|
91
|
-
? altRaw
|
|
115
|
+
? altRaw
|
|
116
|
+
.split(",")
|
|
117
|
+
.map((t) => t.trim())
|
|
118
|
+
.filter(Boolean)
|
|
92
119
|
: [];
|
|
93
120
|
|
|
94
|
-
const genreRaw = anchor.find("p.
|
|
121
|
+
const genreRaw = anchor.find("p.info-value").first().text().trim();
|
|
95
122
|
const genres = genreRaw
|
|
96
|
-
? genreRaw
|
|
123
|
+
? genreRaw
|
|
124
|
+
.split(",")
|
|
125
|
+
.map((g) => g.trim())
|
|
126
|
+
.filter(Boolean)
|
|
97
127
|
: [];
|
|
98
128
|
|
|
99
|
-
const hasValidType = types.length === 0 ||
|
|
100
|
-
const hasValidLanguage = languages.length === 0 ||
|
|
129
|
+
const hasValidType = types.length === 0 || filteredTypes;
|
|
130
|
+
const hasValidLanguage = languages.length === 0 || filteredLanguages;
|
|
101
131
|
|
|
102
132
|
if (title && link && hasValidType && hasValidLanguage) {
|
|
103
133
|
results.push({
|
|
104
134
|
title,
|
|
105
135
|
altTitles,
|
|
106
136
|
genres,
|
|
137
|
+
synopsis,
|
|
107
138
|
url: link.startsWith("http") ? link : `${CATALOGUE_URL}${link}`,
|
|
108
139
|
cover,
|
|
109
140
|
});
|
|
@@ -117,7 +148,7 @@ export async function searchAnime(
|
|
|
117
148
|
await fetchPage(page);
|
|
118
149
|
} else {
|
|
119
150
|
let currentPage = 1;
|
|
120
|
-
while (await fetchPage(currentPage++) && results.length < limit) {
|
|
151
|
+
while ((await fetchPage(currentPage++)) && results.length < limit) {
|
|
121
152
|
await new Promise((res) => setTimeout(res, 300));
|
|
122
153
|
}
|
|
123
154
|
}
|
|
@@ -430,47 +461,79 @@ export async function getAvailableLanguages(
|
|
|
430
461
|
}
|
|
431
462
|
|
|
432
463
|
export async function getAllAnime(
|
|
433
|
-
wantedLanguages =
|
|
434
|
-
wantedTypes =
|
|
464
|
+
wantedLanguages = null,
|
|
465
|
+
wantedTypes = null,
|
|
435
466
|
page = null,
|
|
436
467
|
output = "anime_list.json",
|
|
437
468
|
get_seasons = false
|
|
438
469
|
) {
|
|
470
|
+
const languages = Array.isArray(wantedLanguages)
|
|
471
|
+
? wantedLanguages
|
|
472
|
+
: ["vostfr", "vf", "vastfr"];
|
|
473
|
+
const types = Array.isArray(wantedTypes) ? wantedTypes : ["Anime", "Film"];
|
|
439
474
|
let animeLinks = [];
|
|
440
475
|
|
|
441
476
|
const isWanted = (text, list) =>
|
|
442
|
-
list.some(item => text.toLowerCase().includes(item.toLowerCase()));
|
|
477
|
+
list.some((item) => text.toLowerCase().includes(item.toLowerCase()));
|
|
443
478
|
|
|
444
479
|
const fetchPage = async (pageNum) => {
|
|
445
|
-
const url =
|
|
480
|
+
const url =
|
|
481
|
+
pageNum === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${pageNum}`;
|
|
446
482
|
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
447
483
|
const $ = cheerio.load(res.data);
|
|
448
484
|
|
|
449
|
-
const containers = $("div.
|
|
485
|
+
const containers = $("div.catalog-card > a");
|
|
450
486
|
|
|
451
487
|
containers.each((_, el) => {
|
|
452
|
-
const anchor = $(el)
|
|
453
|
-
const title = anchor.find("h1").text().trim();
|
|
488
|
+
const anchor = $(el);
|
|
454
489
|
const link = anchor.attr("href");
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
const genres = paragraphs[1] ? paragraphs[1].split(',').map(genre => genre.trim()) : [];
|
|
461
|
-
const type = paragraphs[2] ? paragraphs[2].split(',').map(t => t.trim()) : [];
|
|
462
|
-
const language = paragraphs[3] ? paragraphs[3].split(',').map(lang => lang.trim()) : [];
|
|
463
|
-
|
|
464
|
-
const filteredTypes = wantedTypes.length === 0
|
|
465
|
-
? type
|
|
466
|
-
: type.filter(t => isWanted(t, wantedTypes));
|
|
467
|
-
|
|
468
|
-
const hasScans = filteredTypes.some(t => t.toLowerCase() === "scans".toLowerCase());
|
|
469
|
-
|
|
470
|
-
const filteredLanguages = (wantedLanguages.length === 0 || hasScans)
|
|
471
|
-
? language
|
|
472
|
-
: language.filter(lang => isWanted(lang, wantedLanguages));
|
|
490
|
+
const title = anchor.find("h2").first().text().trim();
|
|
491
|
+
const altRaw = anchor.find("p").first().text().trim();
|
|
492
|
+
const genreRaw = anchor.find("p.info-value").first().text().trim();
|
|
493
|
+
const cover = anchor.find("img").first().attr("src");
|
|
494
|
+
const synopsis = anchor.find("div.synopsis-content").first().text();
|
|
473
495
|
|
|
496
|
+
const altTitles = altRaw
|
|
497
|
+
? altRaw
|
|
498
|
+
.split(",")
|
|
499
|
+
.map((t) => t.trim())
|
|
500
|
+
.filter(Boolean)
|
|
501
|
+
: [];
|
|
502
|
+
const genres = genreRaw
|
|
503
|
+
? genreRaw
|
|
504
|
+
.split(",")
|
|
505
|
+
.map((g) => g.trim())
|
|
506
|
+
.filter(Boolean)
|
|
507
|
+
: [];
|
|
508
|
+
const typesRaw = anchor
|
|
509
|
+
.find(".info-row")
|
|
510
|
+
.filter(
|
|
511
|
+
(_, row) => $(row).find(".info-label").text().trim() === "Types"
|
|
512
|
+
)
|
|
513
|
+
.find(".info-value")
|
|
514
|
+
.toArray()
|
|
515
|
+
.map((el) => $(el).text().trim())
|
|
516
|
+
.filter((t) => isWanted(t, types));
|
|
517
|
+
|
|
518
|
+
const filteredTypes = wantedTypes.length === 0 ? typesRaw : typesRaw;
|
|
519
|
+
|
|
520
|
+
const hasScans = filteredTypes.some(
|
|
521
|
+
(t) => t.toLowerCase() === "scans".toLowerCase()
|
|
522
|
+
);
|
|
523
|
+
const languagesRaw = anchor
|
|
524
|
+
.find(".info-row")
|
|
525
|
+
.filter(
|
|
526
|
+
(_, row) => $(row).find(".info-label").text().trim() === "Langues"
|
|
527
|
+
)
|
|
528
|
+
.find(".info-value")
|
|
529
|
+
.toArray()
|
|
530
|
+
.map((el) => $(el).text().trim())
|
|
531
|
+
.filter((l) => isWanted(l, languages));
|
|
532
|
+
|
|
533
|
+
const filteredLanguages =
|
|
534
|
+
wantedLanguages.length === 0 || hasScans
|
|
535
|
+
? languagesRaw
|
|
536
|
+
: languagesRaw.filter((lang) => isWanted(lang, wantedLanguages));
|
|
474
537
|
if (
|
|
475
538
|
title &&
|
|
476
539
|
link &&
|
|
@@ -482,7 +545,7 @@ export async function getAllAnime(
|
|
|
482
545
|
url: fullUrl,
|
|
483
546
|
title,
|
|
484
547
|
altTitles,
|
|
485
|
-
cover
|
|
548
|
+
cover,
|
|
486
549
|
genres,
|
|
487
550
|
types: filteredTypes,
|
|
488
551
|
languages: filteredLanguages,
|
|
@@ -499,10 +562,12 @@ export async function getAllAnime(
|
|
|
499
562
|
const seasons = await getSeasons(anime.url);
|
|
500
563
|
anime.seasons = Array.isArray(seasons) ? seasons : [];
|
|
501
564
|
} catch (err) {
|
|
502
|
-
console.warn(
|
|
565
|
+
console.warn(
|
|
566
|
+
`⚠️ Failed to fetch seasons for ${anime.title}: ${err.message}`
|
|
567
|
+
);
|
|
503
568
|
anime.seasons = [];
|
|
504
569
|
}
|
|
505
|
-
await new Promise(r => setTimeout(r, 300));
|
|
570
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
506
571
|
}
|
|
507
572
|
};
|
|
508
573
|
|
|
@@ -514,10 +579,12 @@ export async function getAllAnime(
|
|
|
514
579
|
} else {
|
|
515
580
|
let currentPage = 1;
|
|
516
581
|
while (await fetchPage(currentPage++)) {
|
|
517
|
-
await new Promise(r => setTimeout(r, 300));
|
|
582
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
518
583
|
}
|
|
519
584
|
|
|
520
|
-
const uniqueLinks = [
|
|
585
|
+
const uniqueLinks = [
|
|
586
|
+
...new Map(animeLinks.map((item) => [item.url, item])).values(),
|
|
587
|
+
];
|
|
521
588
|
if (get_seasons) await enrichWithSeasons(uniqueLinks);
|
|
522
589
|
|
|
523
590
|
fs.writeFileSync(output, JSON.stringify(uniqueLinks, null, 2), "utf-8");
|
|
@@ -539,20 +606,20 @@ export async function getLatestEpisodes(languageFilter = null) {
|
|
|
539
606
|
|
|
540
607
|
container.find("a").each((_, el) => {
|
|
541
608
|
const link = $(el).attr("href");
|
|
542
|
-
const title = $(el).find("
|
|
609
|
+
const title = $(el).find("h2").text().trim();
|
|
543
610
|
const cover = $(el).find("img").attr("src");
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
const language = $(buttons[0]).text().trim().toLowerCase(); // Normalisation
|
|
547
|
-
const episode = $(buttons[1]).text().trim();
|
|
548
|
-
|
|
611
|
+
const language = link.split("/").slice(6, 7).join("");
|
|
612
|
+
const episode = $(el).find(".info-text").text().trim();
|
|
549
613
|
if (
|
|
550
614
|
title &&
|
|
551
615
|
link &&
|
|
552
616
|
cover &&
|
|
553
617
|
language &&
|
|
554
618
|
episode &&
|
|
555
|
-
(languageFilter === null ||
|
|
619
|
+
(languageFilter === null ||
|
|
620
|
+
languageFilter
|
|
621
|
+
.map((l) => l.toLowerCase())
|
|
622
|
+
.includes(language.toLowerCase()))
|
|
556
623
|
) {
|
|
557
624
|
episodes.push({
|
|
558
625
|
title: title,
|
|
@@ -580,15 +647,19 @@ export async function getLatestScans(languageFilter = null) {
|
|
|
580
647
|
const scans = [];
|
|
581
648
|
|
|
582
649
|
container.find("a").each((_, el) => {
|
|
583
|
-
const url = $(el).attr("href")
|
|
584
|
-
|
|
650
|
+
const url = $(el).attr("href").startsWith("http")
|
|
651
|
+
? $(el).attr("href")
|
|
652
|
+
: `${CATALOGUE_URL}${$(el).attr("href")}`;
|
|
653
|
+
const title = $(el).find("h2").text().trim();
|
|
585
654
|
const cover = $(el).find("img").attr("src");
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
655
|
+
const type = $(el)
|
|
656
|
+
.find(".badge-text")
|
|
657
|
+
.first()
|
|
658
|
+
.text()
|
|
659
|
+
.trim()
|
|
660
|
+
.toLowerCase();
|
|
661
|
+
const language = url.split("/").slice(6, 7).join("");
|
|
662
|
+
const chapter = $(el).find(".info-text").text().trim();
|
|
592
663
|
if (
|
|
593
664
|
title &&
|
|
594
665
|
url &&
|
|
@@ -626,25 +697,22 @@ export async function getRandomAnime(
|
|
|
626
697
|
attempt = 0
|
|
627
698
|
) {
|
|
628
699
|
try {
|
|
629
|
-
const res = await axios.get(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
);
|
|
700
|
+
const res = await axios.get(`${CATALOGUE_URL}/?search=&random=1`, {
|
|
701
|
+
headers: getHeaders(CATALOGUE_URL),
|
|
702
|
+
});
|
|
633
703
|
|
|
634
704
|
const $ = cheerio.load(res.data);
|
|
635
705
|
const isWanted = (text, list) =>
|
|
636
|
-
list.some(item => text.toLowerCase().includes(item.toLowerCase()));
|
|
706
|
+
list.some((item) => text.toLowerCase().includes(item.toLowerCase()));
|
|
707
|
+
|
|
708
|
+
const containers = $("div.catalog-card > a");
|
|
637
709
|
|
|
638
|
-
const
|
|
639
|
-
const anchor = container.find("a");
|
|
710
|
+
const anchor = containers;
|
|
640
711
|
const link = anchor.attr("href");
|
|
641
|
-
const title = anchor.find("
|
|
642
|
-
const altRaw = anchor
|
|
643
|
-
.find("p.text-xs.opacity-40.italic")
|
|
644
|
-
.first()
|
|
645
|
-
.text()
|
|
646
|
-
.trim();
|
|
712
|
+
const title = anchor.find("h2").first().text().trim();
|
|
713
|
+
const altRaw = anchor.find("p").first().text().trim();
|
|
647
714
|
const cover = anchor.find("img").first().attr("src");
|
|
715
|
+
const synopsis = anchor.find("div.synopsis-content").first().text();
|
|
648
716
|
|
|
649
717
|
const altTitles = altRaw
|
|
650
718
|
? altRaw
|
|
@@ -653,11 +721,7 @@ export async function getRandomAnime(
|
|
|
653
721
|
.filter(Boolean)
|
|
654
722
|
: [];
|
|
655
723
|
|
|
656
|
-
const genreRaw = anchor
|
|
657
|
-
.find("p.text-xs.font-medium.text-gray-300")
|
|
658
|
-
.first()
|
|
659
|
-
.text()
|
|
660
|
-
.trim();
|
|
724
|
+
const genreRaw = anchor.find("p.info-value").first().text().trim();
|
|
661
725
|
|
|
662
726
|
const genres = genreRaw
|
|
663
727
|
? genreRaw
|
|
@@ -666,25 +730,35 @@ export async function getRandomAnime(
|
|
|
666
730
|
.filter(Boolean)
|
|
667
731
|
: [];
|
|
668
732
|
|
|
669
|
-
const tagText = anchor
|
|
670
|
-
|
|
671
|
-
|
|
733
|
+
const tagText = anchor
|
|
734
|
+
.find("p")
|
|
735
|
+
.filter((_, p) => isWanted($(p).text(), wantedTypes))
|
|
736
|
+
.first()
|
|
737
|
+
.text();
|
|
672
738
|
|
|
673
|
-
const languageText = anchor
|
|
674
|
-
|
|
675
|
-
|
|
739
|
+
const languageText = anchor
|
|
740
|
+
.find("p")
|
|
741
|
+
.filter((_, p) => isWanted($(p).text(), wantedLanguages))
|
|
742
|
+
.first()
|
|
743
|
+
.text();
|
|
676
744
|
|
|
677
745
|
if (title && link && tagText && languageText) {
|
|
678
746
|
return {
|
|
679
747
|
title,
|
|
680
748
|
altTitles,
|
|
681
749
|
genres,
|
|
750
|
+
synopsis,
|
|
682
751
|
url: link.startsWith("http") ? link : `${CATALOGUE_URL}${link}`,
|
|
683
752
|
cover,
|
|
684
753
|
};
|
|
685
754
|
} else {
|
|
686
755
|
if (maxAttempts === null || attempt < maxAttempts) {
|
|
687
|
-
return await getRandomAnime(
|
|
756
|
+
return await getRandomAnime(
|
|
757
|
+
wantedLanguages,
|
|
758
|
+
wantedTypes,
|
|
759
|
+
maxAttempts,
|
|
760
|
+
attempt + 1
|
|
761
|
+
);
|
|
688
762
|
} else {
|
|
689
763
|
throw new Error("Max attempts reached without finding a valid anime.");
|
|
690
764
|
}
|
|
@@ -701,7 +775,7 @@ export async function getAllTitleScans(mangaUrl, numberImg = false) {
|
|
|
701
775
|
const $ = cheerio.load(res.data);
|
|
702
776
|
|
|
703
777
|
const title = encodeURIComponent($("#titreOeuvre").text().trim());
|
|
704
|
-
const urlInfo = `https://anime-sama.
|
|
778
|
+
const urlInfo = `https://anime-sama.org/s2/scans/get_nb_chap_et_img.php?oeuvre=${title}`
|
|
705
779
|
const infoPage = await axios.get(urlInfo, { headers: getHeaders() });
|
|
706
780
|
const titleChapter = Object.keys(infoPage.data)
|
|
707
781
|
|
|
@@ -718,7 +792,7 @@ export async function getImgScans(mangaUrl, wantedChapter) {
|
|
|
718
792
|
const mangaTitle = infoScan.mangaTitle
|
|
719
793
|
const imgUrls = [];
|
|
720
794
|
for (let i = 1; i <= numberImg; i++) {
|
|
721
|
-
imgUrls.push(`https://anime-sama.
|
|
795
|
+
imgUrls.push(`https://anime-sama.org/s2/scans/${mangaTitle}/${wantedChapter}/${i}.jpg`);
|
|
722
796
|
}
|
|
723
797
|
|
|
724
798
|
return imgUrls
|
package/utils/dispatcher.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import * as extractor from "./extractVideoUrl.js";
|
|
2
2
|
|
|
3
|
-
export async function getVideoUrlFromEmbed(source, embedUrl) {
|
|
3
|
+
export async function getVideoUrlFromEmbed(source, embedUrl, ...rest) {
|
|
4
4
|
if (source === "sibnet") {
|
|
5
5
|
return await extractor.getSibnetVideo(embedUrl);
|
|
6
6
|
}
|
|
7
7
|
if (source === "sendvid") {
|
|
8
8
|
return await extractor.getSendvidVideo(embedUrl);
|
|
9
9
|
}
|
|
10
|
-
if (source === "vidmoly"
|
|
11
|
-
return await extractor.
|
|
10
|
+
if (source === "vidmoly") {
|
|
11
|
+
return await extractor.getVidmolyVideo(embedUrl, ...rest);
|
|
12
12
|
}
|
|
13
|
-
if (source === "
|
|
14
|
-
return await extractor.
|
|
13
|
+
if (source === "oneupload") {
|
|
14
|
+
return await extractor.getOneuploadVideo(embedUrl);
|
|
15
|
+
}
|
|
16
|
+
if (source === "movearnpre" || source === "smoothpre") {
|
|
17
|
+
return await extractor.getMovearnpreOrSmoothpreVideo(embedUrl, ...rest);
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
throw new Error(`Unsupported embed source: ${source}`);
|
package/utils/extractVideoUrl.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import * as cheerio from "cheerio";
|
|
3
|
-
|
|
3
|
+
import puppeteer from "puppeteer-core";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
4
6
|
|
|
5
7
|
const getHeaders = (referer) => ({
|
|
6
8
|
Accept: "*/*",
|
|
@@ -8,6 +10,39 @@ const getHeaders = (referer) => ({
|
|
|
8
10
|
"User-Agent":
|
|
9
11
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
10
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
|
+
|
|
38
|
+
async function launchBrowser (customChromiumPath) {
|
|
39
|
+
const executablePath = await ensureChromiumInstalled(customChromiumPath);
|
|
40
|
+
return puppeteer.launch({
|
|
41
|
+
executablePath: executablePath,
|
|
42
|
+
headless: true,
|
|
43
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
44
|
+
});
|
|
45
|
+
};
|
|
11
46
|
export async function getSibnetVideo(embedUrl) {
|
|
12
47
|
try {
|
|
13
48
|
const { data } = await axios.get(embedUrl, {
|
|
@@ -50,13 +85,69 @@ export async function getSendvidVideo(embedUrl) {
|
|
|
50
85
|
return null;
|
|
51
86
|
}
|
|
52
87
|
}
|
|
88
|
+
export async function getVidmolyVideo(embedUrl, customChromiumPath) {
|
|
89
|
+
if (embedUrl.includes("vidmoly.to/"))
|
|
90
|
+
embedUrl = embedUrl.replace("vidmoly.to/", "vidmoly.net/");
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
const browser = await launchBrowser(customChromiumPath);
|
|
93
|
+
|
|
94
|
+
const page = await browser.newPage();
|
|
95
|
+
await page.setUserAgent(
|
|
96
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await page.goto(embedUrl, { waitUntil: "domcontentloaded" });
|
|
100
|
+
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
const maxWait = 10000;
|
|
103
|
+
const maxReloads = 50;
|
|
104
|
+
let reloadCount = 0;
|
|
105
|
+
let videoUrl = null;
|
|
106
|
+
|
|
107
|
+
while (Date.now() - start < maxWait) {
|
|
108
|
+
videoUrl = await page.evaluate(() => {
|
|
109
|
+
const scripts = Array.from(document.querySelectorAll("script")).map(
|
|
110
|
+
(s) => s.textContent || ""
|
|
111
|
+
);
|
|
112
|
+
for (const s of scripts) {
|
|
113
|
+
const match = s.match(/file\s*:\s*"(https?:\/\/[^"]+\.m3u8[^"]*)"/);
|
|
114
|
+
if (match && match[1]) return match[1];
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (videoUrl) break;
|
|
120
|
+
|
|
121
|
+
const pleaseWait = await page.evaluate(() => {
|
|
122
|
+
const t = (document.title || "").toLowerCase();
|
|
123
|
+
const b = document.body?.innerText?.toLowerCase() || "";
|
|
124
|
+
return t.includes("please wait") || b.includes("please wait");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (pleaseWait && reloadCount < maxReloads) {
|
|
128
|
+
reloadCount++;
|
|
129
|
+
console.log(
|
|
130
|
+
`[getVidmolyVideo] detected 'Please Wait' — reload #${reloadCount}`
|
|
131
|
+
);
|
|
132
|
+
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => null);
|
|
58
133
|
}
|
|
59
|
-
|
|
134
|
+
|
|
135
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
136
|
+
}
|
|
137
|
+
const openingTime = await page.evaluate(() => {
|
|
138
|
+
const st = typeof skipTime !== "undefined" ? skipTime : null;
|
|
139
|
+
const rs = typeof result !== "undefined" ? result : null;
|
|
140
|
+
return [rs, st];
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await browser.close();
|
|
144
|
+
|
|
145
|
+
if (!videoUrl) console.warn("[getVidmolyVideo] timeout: video not found");
|
|
146
|
+
return { videoUrl, openingTime };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function getOneuploadVideo(embedUrl) {
|
|
150
|
+
try {
|
|
60
151
|
const { data } = await axios.get(embedUrl, {
|
|
61
152
|
headers: getHeaders(embedUrl),
|
|
62
153
|
});
|
|
@@ -72,22 +163,20 @@ export async function getVidmolyOrOneuploadVideo(embedUrl) {
|
|
|
72
163
|
}
|
|
73
164
|
return null;
|
|
74
165
|
} catch (err) {
|
|
75
|
-
console.error("Erreur
|
|
166
|
+
console.error("Erreur getOneuploadVideo:", err.message);
|
|
76
167
|
return null;
|
|
77
168
|
}
|
|
78
169
|
}
|
|
79
170
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
export async function getMovearnpreOrSmoothpreVideo(embedUrl) {
|
|
83
|
-
const browser = await puppeteer.launch({ headless: true });
|
|
171
|
+
export async function getMovearnpreOrSmoothpreVideo(embedUrl, customChromiumPath) {
|
|
172
|
+
const browser = await launchBrowser(customChromiumPath);
|
|
84
173
|
const page = await browser.newPage();
|
|
85
174
|
|
|
86
175
|
await page.setUserAgent(
|
|
87
176
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
88
177
|
);
|
|
89
178
|
|
|
90
|
-
await page.goto(embedUrl, { waitUntil:
|
|
179
|
+
await page.goto(embedUrl, { waitUntil: "networkidle2" });
|
|
91
180
|
|
|
92
181
|
await page.waitForFunction('typeof jwplayer !== "undefined"');
|
|
93
182
|
|
|
@@ -98,6 +187,6 @@ export async function getMovearnpreOrSmoothpreVideo(embedUrl) {
|
|
|
98
187
|
});
|
|
99
188
|
|
|
100
189
|
await browser.close();
|
|
101
|
-
const finalUrl = embedUrl.split("/").slice(0, 3).join("/") + videoUrl
|
|
190
|
+
const finalUrl = embedUrl.split("/").slice(0, 3).join("/") + videoUrl;
|
|
102
191
|
return finalUrl;
|
|
103
192
|
}
|