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 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.fr) and [animepahe](https://animepahe.ru) available at the moment). This tool allows you to search for anime, retrieve information, get episodes, and more.
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.fr/catalogue/drcl-midnight-children/scan/vf";
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.fr/catalogue/86-eighty-six/";
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.fr/catalogue/86-eighty-six/saison1/vostfr/";
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.fr/catalogue/one-piece/saison11/vostfr";
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.fr/catalogue/86-eighty-six/saison1/vostfr/";
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.fr/catalogue/drcl-midnight-children/scan/vf";
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.fr/catalogue/drcl-midnight-children";
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.1",
4
- "description": "Scrape anime data from different sources (only anime-sama.fr, animepahe and crunchyroll for the moment)",
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"
@@ -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.fr";
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) ? wantedLanguages : ["vostfr", "vf", "vastfr"];
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 || list.some(item => text.toLowerCase().includes(item.toLowerCase()));
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(query)}&page=${pageNum}`;
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("h1").first().text().trim();
79
- const altRaw = anchor.find("p.text-xs.opacity-40.italic").first().text().trim();
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
- const tagText = anchor.find("p").filter((_, p) =>
83
- isWanted($(p).text(), types)
84
- ).first().text();
85
-
86
- const languageText = anchor.find("p").filter((_, p) =>
87
- isWanted($(p).text(), languages)
88
- ).first().text();
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.split(",").map((t) => t.trim()).filter(Boolean)
115
+ ? altRaw
116
+ .split(",")
117
+ .map((t) => t.trim())
118
+ .filter(Boolean)
92
119
  : [];
93
120
 
94
- const genreRaw = anchor.find("p.text-xs.font-medium.text-gray-300").first().text().trim();
121
+ const genreRaw = anchor.find("p.info-value").first().text().trim();
95
122
  const genres = genreRaw
96
- ? genreRaw.split(",").map((g) => g.trim()).filter(Boolean)
123
+ ? genreRaw
124
+ .split(",")
125
+ .map((g) => g.trim())
126
+ .filter(Boolean)
97
127
  : [];
98
128
 
99
- const hasValidType = types.length === 0 || tagText;
100
- const hasValidLanguage = languages.length === 0 || languageText;
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 = ["vostfr", "vf", "vastfr"],
434
- wantedTypes = ["Anime", "Film"],
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 = pageNum === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${pageNum}`;
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.shrink-0.m-3.rounded.border-2");
485
+ const containers = $("div.catalog-card > a");
450
486
 
451
487
  containers.each((_, el) => {
452
- const anchor = $(el).find("a");
453
- const title = anchor.find("h1").text().trim();
488
+ const anchor = $(el);
454
489
  const link = anchor.attr("href");
455
- const img = anchor.find("img").attr("src");
456
-
457
- const paragraphs = anchor.find("p").toArray().map(p => $(p).text().trim());
458
-
459
- const altTitles = paragraphs[0] ? paragraphs[0].split(',').map(name => name.trim()) : [];
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: img,
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(`⚠️ Failed to fetch seasons for ${anime.title}: ${err.message}`);
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 = [...new Map(animeLinks.map(item => [item.url, item])).values()];
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("h1").text().trim();
609
+ const title = $(el).find("h2").text().trim();
543
610
  const cover = $(el).find("img").attr("src");
544
-
545
- const buttons = $(el).find("button");
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 || languageFilter.map(l => l.toLowerCase()).includes(language.toLowerCase()))
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
- const title = $(el).find("h1").text().trim();
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
- const buttons = $(el).find("button");
588
- const type = $(buttons[0]).text().trim().toLowerCase();
589
- const language = url.split("/").slice(6, 7)[0];
590
- const chapter = $(buttons[2]).text().trim();
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
- `${CATALOGUE_URL}/?search=&random=1`,
631
- { headers: getHeaders(CATALOGUE_URL) }
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 container = $("div.shrink-0.m-3.rounded.border-2").first();
639
- const anchor = container.find("a");
710
+ const anchor = containers;
640
711
  const link = anchor.attr("href");
641
- const title = anchor.find("h1").first().text().trim();
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.find("p").filter((_, p) =>
670
- isWanted($(p).text(), wantedTypes)
671
- ).first().text();
733
+ const tagText = anchor
734
+ .find("p")
735
+ .filter((_, p) => isWanted($(p).text(), wantedTypes))
736
+ .first()
737
+ .text();
672
738
 
673
- const languageText = anchor.find("p").filter((_, p) =>
674
- isWanted($(p).text(), wantedLanguages)
675
- ).first().text();
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(wantedLanguages, wantedTypes, maxAttempts, attempt + 1);
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.fr/s2/scans/get_nb_chap_et_img.php?oeuvre=${title}`
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.fr/s2/scans/${mangaTitle}/${wantedChapter}/${i}.jpg`);
795
+ imgUrls.push(`https://anime-sama.org/s2/scans/${mangaTitle}/${wantedChapter}/${i}.jpg`);
722
796
  }
723
797
 
724
798
  return imgUrls
@@ -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" || source === "oneupload" ) {
11
- return await extractor.getVidmolyOrOneuploadVideo(embedUrl);
10
+ if (source === "vidmoly") {
11
+ return await extractor.getVidmolyVideo(embedUrl, ...rest);
12
12
  }
13
- if (source === "movearnpre" || source === "smoothpre" ) {
14
- return await extractor.getMovearnpreOrSmoothpreVideo(embedUrl);
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}`);
@@ -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
- export async function getVidmolyOrOneuploadVideo(embedUrl) {
55
- try {
56
- if (embedUrl.includes("vidmoly.to/")) {
57
- embedUrl = embedUrl.replace("vidmoly.to/", "vidmoly.net/");
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
- console.log(embedUrl)
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 getVidmolyVideo:", err.message);
166
+ console.error("Erreur getOneuploadVideo:", err.message);
76
167
  return null;
77
168
  }
78
169
  }
79
170
 
80
- import puppeteer from 'puppeteer';
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: 'networkidle2' });
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
  }