better-ani-scraped 1.6.81 → 1.7.1
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 +62 -0
- package/examples/animesama/example_usage_getAllAnime.js +1 -1
- package/examples/animesama/example_usage_getAllTitleScans.js +10 -0
- package/examples/animesama/example_usage_getEmbed.js +2 -2
- package/examples/animesama/example_usage_getImgScans.js +10 -0
- package/examples/animesama/example_usage_getLatestScans.js +10 -0
- package/examples/animesama/example_usage_getSeasons.js +2 -2
- package/examples/utility-functions/example_usage_getVideoUrlFromEmbed.js +9 -0
- package/package.json +1 -1
- package/scrapers/animesama.js +174 -78
- package/scrapers/scrapers.js +79 -16
- package/utils/dispatcher.js +3 -0
- package/utils/extractVideoUrl.js +25 -1
package/DOCUMENTATION.md
CHANGED
|
@@ -35,7 +35,11 @@ const crunchyroll = new AnimeScraper('crunchyroll') //for Crunchyroll
|
|
|
35
35
|
- [getAvailableLanguages](#animesamagetavailablelanguagesseasonurl-wantedlanguages--vostfr-vf-va-vkr-vcn-vqc-vf1-vf2-numberepisodes--false)
|
|
36
36
|
- [getAllAnime](#animesamagetallanimewantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film-page--null-output--anime_listjson-get_seasons--false)
|
|
37
37
|
- [getLatestEpisodes](#animesamagetlatestepisodeslanguagefilter--null)
|
|
38
|
+
- [getLatestScans](#animesamagetlatestscanslanguagefilter--null)
|
|
38
39
|
- [getRandomAnime](#animesamagetrandomanimewantedlanguages--vostfr-vf-vastfr-wantedtypes--anime-film-maxattempts--null-attempt--0)
|
|
40
|
+
- [getAllTitleScans](#animesamagetalltitlescansmangaurl-numberimg--false)
|
|
41
|
+
- [getImgScans](#animesamagetimgscansmangaurl-wantedchapter)
|
|
42
|
+
|
|
39
43
|
|
|
40
44
|
### `animesama.searchAnime(query, limit = 10, wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"], page = null)`
|
|
41
45
|
Searches for anime titles that match the given query.
|
|
@@ -232,6 +236,30 @@ Scrapes the latest released episodes, optionally filtered by language.
|
|
|
232
236
|
|
|
233
237
|
---
|
|
234
238
|
|
|
239
|
+
### `animesama.getLatestScans(languageFilter = null)`
|
|
240
|
+
Scrapes the latest released episodes, optionally filtered by language.
|
|
241
|
+
|
|
242
|
+
- **Parameters:**
|
|
243
|
+
- `languageFilter` *(string[]|null)*: If set, filters episodes by language in the array. If null, returns all scans.
|
|
244
|
+
- **Returns:**
|
|
245
|
+
Array of scans objects:
|
|
246
|
+
```js
|
|
247
|
+
[
|
|
248
|
+
{
|
|
249
|
+
title: string,
|
|
250
|
+
url: string,
|
|
251
|
+
cover: string,
|
|
252
|
+
type: string,
|
|
253
|
+
language: string,
|
|
254
|
+
chapter: string
|
|
255
|
+
}
|
|
256
|
+
...
|
|
257
|
+
]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
|
|
235
263
|
### `animesama.getRandomAnime(wantedLanguages = ["vostfr", "vf", "vastfr"], wantedTypes = ["Anime", "Film"], maxAttempts = null, attempt = 0)`
|
|
236
264
|
Fetches a random anime from the catalogue.
|
|
237
265
|
|
|
@@ -254,6 +282,40 @@ Fetches a random anime from the catalogue.
|
|
|
254
282
|
|
|
255
283
|
---
|
|
256
284
|
|
|
285
|
+
### `animesama.getAllTitleScans(mangaUrl, numberImg = false)`
|
|
286
|
+
Scrapes all the scans of a chapter.
|
|
287
|
+
|
|
288
|
+
- **Parameters:**
|
|
289
|
+
- `mangaUrl` *(string)*: The manga URL.
|
|
290
|
+
- `numberImg` *(boolean)*: If `true`, indicates the number of images in each chapter.
|
|
291
|
+
- **Returns:**
|
|
292
|
+
An array of chapter titles if *numberImg = false*
|
|
293
|
+
Else :
|
|
294
|
+
```js
|
|
295
|
+
{
|
|
296
|
+
scans :
|
|
297
|
+
[
|
|
298
|
+
{
|
|
299
|
+
title: string,
|
|
300
|
+
url: string,
|
|
301
|
+
},
|
|
302
|
+
...
|
|
303
|
+
]
|
|
304
|
+
mangaTitle: string,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### `animesama.getImgScans(mangaUrl, wantedChapter)`
|
|
310
|
+
Scrapes all the scans of a chapter.
|
|
311
|
+
|
|
312
|
+
- **Parameters:**
|
|
313
|
+
- `mangaUrl` *(string)*: The manga URL.
|
|
314
|
+
- `wantedChapter` *(int)*: The number of the chapter you want.
|
|
315
|
+
- **Returns:**
|
|
316
|
+
An array of image URL.
|
|
317
|
+
|
|
318
|
+
---
|
|
257
319
|
|
|
258
320
|
## `AnimeScraper("animepahe")` methods
|
|
259
321
|
|
|
@@ -3,7 +3,7 @@ import { AnimeScraper } from "../../index.js"; // REPLACE BY "from 'better-ani-s
|
|
|
3
3
|
const main = async () => {
|
|
4
4
|
const animesama = new AnimeScraper('animesama');
|
|
5
5
|
|
|
6
|
-
const catalogue = await animesama.getAllAnime(["vostfr", "vf", "vastfr"], ["Anime", "Film"],
|
|
6
|
+
const catalogue = await animesama.getAllAnime(["vostfr", "vf", "vastfr"], ["Anime", "Film", "Scans"], 1);
|
|
7
7
|
console.log(catalogue)
|
|
8
8
|
};
|
|
9
9
|
|
|
@@ -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
|
+
const mangaUrl = "https://anime-sama.fr/catalogue/drcl-midnight-children/scan/vf";
|
|
6
|
+
const chapterTitles = await animesama.getAllTitleScans(mangaUrl, true);
|
|
7
|
+
console.log("Titles:", chapterTitles);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -2,9 +2,9 @@ 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/
|
|
5
|
+
const seasonUrl = "https://anime-sama.fr/catalogue/one-piece/saison11/vostfr";
|
|
6
6
|
|
|
7
|
-
const embeds = await animesama.getEmbed(seasonUrl, ["sibnet", "vidmoly", "sendvid"], true, true);
|
|
7
|
+
const embeds = await animesama.getEmbed(seasonUrl, ["smoothpre", "movearnpre", "sibnet", "vidmoly", "sendvid"], true, true);
|
|
8
8
|
|
|
9
9
|
console.log("Embed Links:", JSON.stringify(embeds, null, 2));
|
|
10
10
|
|
|
@@ -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
|
+
const scansUrl = "https://anime-sama.fr/catalogue/drcl-midnight-children/scan/vf";
|
|
6
|
+
const scansImgUrl = await animesama.getImgScans(scansUrl, 9);
|
|
7
|
+
console.log("Image scans:", scansImgUrl);
|
|
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 new_scans = await animesama.getLatestScans(["vostfr", "vf"]);
|
|
7
|
+
console.log(new_scans);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
main().catch(console.error);
|
|
@@ -2,9 +2,9 @@ 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/
|
|
5
|
+
const animeUrl = "https://anime-sama.fr/catalogue/drcl-midnight-children";
|
|
6
6
|
|
|
7
|
-
const seasons = await animesama.getSeasons(animeUrl, ["vostfr", "vf"]);
|
|
7
|
+
const seasons = await animesama.getSeasons(animeUrl, ["vostfr", "vf"], ["Anime", "Scans"]);
|
|
8
8
|
console.log("Seasons:", seasons);
|
|
9
9
|
};
|
|
10
10
|
|
|
@@ -5,6 +5,8 @@ const main = async () => {
|
|
|
5
5
|
const embedUrlSendvid = "https://sendvid.com/embed/4vzpcb0q";
|
|
6
6
|
const embedUrlVidmoly = "https://vidmoly.to/embed-rvqrwg5zk37w.html";
|
|
7
7
|
const embedUrlOneupload = "https://oneupload.net/embed-axdrxh1y3p37.html";
|
|
8
|
+
const embedUrlSmoothpre = "https://smoothpre.com/embed/8294jcf1q8jf";
|
|
9
|
+
const embedUrlMovearnpre = "https://movearnpre.com/embed/e3xbkin87yt3";
|
|
8
10
|
|
|
9
11
|
const videoUrlSibnet = await getVideoUrlFromEmbed("sibnet", embedUrlSibnet)
|
|
10
12
|
console.log("Video URL Sibnet:", videoUrlSibnet);
|
|
@@ -17,6 +19,13 @@ const main = async () => {
|
|
|
17
19
|
|
|
18
20
|
const videoUrlOneupload = await getVideoUrlFromEmbed("oneupload", embedUrlOneupload)
|
|
19
21
|
console.log("Video URL Oneupload:", videoUrlOneupload);
|
|
22
|
+
|
|
23
|
+
const videoUrlSmoothpre = await getVideoUrlFromEmbed("smoothpre", embedUrlSmoothpre)
|
|
24
|
+
console.log("Video URL Smoothpre:", videoUrlSmoothpre);
|
|
25
|
+
|
|
26
|
+
const videoUrlMovearnpre = await getVideoUrlFromEmbed("movearnpre", embedUrlMovearnpre)
|
|
27
|
+
console.log("Video URL Movearnpre:", videoUrlMovearnpre);
|
|
28
|
+
|
|
20
29
|
};
|
|
21
30
|
|
|
22
31
|
main().catch(console.error);
|
package/package.json
CHANGED
package/scrapers/animesama.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { exec as execCallback } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
+
import { title } from "process";
|
|
7
8
|
const execAsync = promisify(execCallback);
|
|
8
9
|
|
|
9
10
|
|
|
@@ -36,19 +37,13 @@ async function ensureChromiumInstalled(customPath) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function getHeaders(referer = BASE_URL) {
|
|
39
|
-
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
40
40
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"Referer": referer,
|
|
45
|
-
},
|
|
46
|
-
userAgent
|
|
41
|
+
"User-Agent": "Mozilla/5.0",
|
|
42
|
+
"Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
|
|
43
|
+
Referer: referer,
|
|
47
44
|
};
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
47
|
export async function searchAnime(
|
|
53
48
|
query,
|
|
54
49
|
limit = 10,
|
|
@@ -59,7 +54,6 @@ export async function searchAnime(
|
|
|
59
54
|
const languages = Array.isArray(wantedLanguages) ? wantedLanguages : ["vostfr", "vf", "vastfr"];
|
|
60
55
|
const types = Array.isArray(wantedTypes) ? wantedTypes : ["Anime", "Film"];
|
|
61
56
|
|
|
62
|
-
const { headers } = getHeaders(CATALOGUE_URL);
|
|
63
57
|
const isWanted = (text, list) =>
|
|
64
58
|
list.length === 0 || list.some(item => text.toLowerCase().includes(item.toLowerCase()));
|
|
65
59
|
|
|
@@ -71,7 +65,7 @@ export async function searchAnime(
|
|
|
71
65
|
? `${CATALOGUE_URL}/?search=${encodeURIComponent(query)}`
|
|
72
66
|
: `${CATALOGUE_URL}/?search=${encodeURIComponent(query)}&page=${pageNum}`;
|
|
73
67
|
|
|
74
|
-
const res = await axios.get(url, { headers:
|
|
68
|
+
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
75
69
|
const $ = cheerio.load(res.data);
|
|
76
70
|
|
|
77
71
|
const containers = $("a.flex.divide-x");
|
|
@@ -131,17 +125,26 @@ export async function searchAnime(
|
|
|
131
125
|
return results;
|
|
132
126
|
}
|
|
133
127
|
|
|
134
|
-
export async function getSeasons(animeUrl, languagePriority = ["vostfr", "vf", "va", "vkr", "vcn", "vqc", "vf1", "vf2"]) {
|
|
135
|
-
const
|
|
136
|
-
const { headersAnime } = getHeaders(animeUrl);
|
|
137
|
-
const res = await axios.get(animeUrl, { headers: headersCatalog });
|
|
128
|
+
export async function getSeasons(animeUrl, languagePriority = ["vostfr", "vf", "va", "vkr", "vcn", "vqc", "vf1", "vf2"], wantedTypes=["Anime", "Kai", "Scans"]) {
|
|
129
|
+
const res = await axios.get(animeUrl, { headers: getHeaders(CATALOGUE_URL) });
|
|
138
130
|
const html = res.data;
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
let mainAnimeOnly = html;
|
|
132
|
+
if (wantedTypes.length !== 0) {
|
|
133
|
+
if (!wantedTypes.includes("Anime")) {
|
|
134
|
+
mainAnimeOnly = mainAnimeOnly.replace(/<h2.*?>Anime<\/h2>[\s\S]*?(?=<h2|$)/g, '');
|
|
135
|
+
}
|
|
136
|
+
if (!wantedTypes.includes("Kai")) {
|
|
137
|
+
mainAnimeOnly = mainAnimeOnly.replace(/<h2.*?>Anime Version Kai<\/h2>[\s\S]*?(?=<h2|$)/g, '');
|
|
138
|
+
}
|
|
139
|
+
if (!wantedTypes.includes("Scans")) {
|
|
140
|
+
mainAnimeOnly = mainAnimeOnly.replace(/<h2.*?>Manga<\/h2>[\s\S]*?(?=<h2|$)/g, '');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
141
144
|
const $ = cheerio.load(mainAnimeOnly);
|
|
142
145
|
const scriptTags = $("script")
|
|
143
146
|
.toArray()
|
|
144
|
-
.filter(script => $(script).html().includes(
|
|
147
|
+
.filter(script => ["panneauAnime", "panneauScan"].some(str => $(script).html().includes(str)));
|
|
145
148
|
|
|
146
149
|
const animeName = animeUrl.split("/")[4];
|
|
147
150
|
let seasons = [];
|
|
@@ -149,7 +152,7 @@ export async function getSeasons(animeUrl, languagePriority = ["vostfr", "vf", "
|
|
|
149
152
|
for (const language of languagePriority) {
|
|
150
153
|
seasons = [];
|
|
151
154
|
let languageAvailable = false;
|
|
152
|
-
|
|
155
|
+
let scansLanguage = "";
|
|
153
156
|
for (let script of scriptTags) {
|
|
154
157
|
const content = $(script).html();
|
|
155
158
|
|
|
@@ -157,38 +160,53 @@ export async function getSeasons(animeUrl, languagePriority = ["vostfr", "vf", "
|
|
|
157
160
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
158
161
|
.replace(/\/\/.*$/gm, "");
|
|
159
162
|
|
|
160
|
-
const matches = [...uncommentedContent.matchAll(/panneauAnime\("([^"]+)", "([^"]+)"\);/g)];
|
|
163
|
+
const matches = [...uncommentedContent.matchAll(/(panneauAnime|panneauScan)\("([^"]+)", "([^"]+)"\);/g)];
|
|
161
164
|
|
|
162
165
|
for (let match of matches) {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
const type = match[1];
|
|
167
|
+
const title = match[2];
|
|
168
|
+
const href = match[3].split("/")[0];
|
|
169
|
+
|
|
170
|
+
if (type === "panneauScan") {
|
|
171
|
+
let found = false;
|
|
172
|
+
for (const lang of languagePriority) {
|
|
173
|
+
const fullUrl = `${CATALOGUE_URL}/${animeName}/${href}/${lang}`;
|
|
174
|
+
try {
|
|
175
|
+
const check = await axios.head(fullUrl, { headers: getHeaders(animeUrl) });
|
|
176
|
+
if (check.status === 200) {
|
|
177
|
+
seasons.push({ title, url: fullUrl });
|
|
178
|
+
scansLanguage = lang
|
|
179
|
+
found = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
if (found) languageAvailable = true;
|
|
185
|
+
} else {
|
|
186
|
+
const fullUrl = `${CATALOGUE_URL}/${animeName}/${href}/${language}`;
|
|
187
|
+
try {
|
|
188
|
+
const check = await axios.head(fullUrl, { headers: getHeaders(animeUrl) });
|
|
189
|
+
if (check.status === 200) {
|
|
190
|
+
seasons.push({ title, url: fullUrl });
|
|
191
|
+
languageAvailable = true;
|
|
192
|
+
}
|
|
193
|
+
} catch {}
|
|
174
194
|
}
|
|
175
|
-
} catch (err) {
|
|
176
|
-
// Ignore missing URLs
|
|
177
195
|
}
|
|
178
196
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
if (wantedTypes.includes("Scans") && wantedTypes.length==1 && scansLanguage) {
|
|
198
|
+
return { scansLanguage, seasons };
|
|
199
|
+
}
|
|
200
|
+
else if (languageAvailable) {
|
|
182
201
|
return { language, seasons };
|
|
183
202
|
}
|
|
184
203
|
}
|
|
185
204
|
|
|
186
|
-
return
|
|
205
|
+
return [];
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
|
|
190
209
|
export async function getEpisodeTitles(seasonUrl, customChromiumPath) {
|
|
191
|
-
const { headers, userAgent } = getHeaders(seasonUrl);
|
|
192
210
|
let browser;
|
|
193
211
|
try {
|
|
194
212
|
const puppeteer = await import('puppeteer');
|
|
@@ -205,7 +223,7 @@ export async function getEpisodeTitles(seasonUrl, customChromiumPath) {
|
|
|
205
223
|
});
|
|
206
224
|
|
|
207
225
|
const page = await browser.newPage();
|
|
208
|
-
await page.setExtraHTTPHeaders(
|
|
226
|
+
await page.setExtraHTTPHeaders(getHeaders(seasonUrl));
|
|
209
227
|
await page.setRequestInterception(true);
|
|
210
228
|
page.on('request', (req) => {
|
|
211
229
|
const blocked = ['image', 'stylesheet', 'font', 'media'];
|
|
@@ -215,7 +233,7 @@ export async function getEpisodeTitles(seasonUrl, customChromiumPath) {
|
|
|
215
233
|
req.continue();
|
|
216
234
|
}
|
|
217
235
|
});
|
|
218
|
-
await page.setUserAgent(
|
|
236
|
+
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36');
|
|
219
237
|
await page.evaluateOnNewDocument(() => {
|
|
220
238
|
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
221
239
|
});
|
|
@@ -244,10 +262,8 @@ export async function getEmbed(
|
|
|
244
262
|
includeInfo = false,
|
|
245
263
|
customChromiumPath
|
|
246
264
|
) {
|
|
247
|
-
const { headersSplit } = getHeaders(seasonUrl.split("/").slice(0, 5).join("/"));
|
|
248
|
-
const { headers } = getHeaders(seasonUrl);
|
|
249
265
|
const res = await axios.get(seasonUrl, {
|
|
250
|
-
headers:
|
|
266
|
+
headers: getHeaders(seasonUrl.split("/").slice(0, 5).join("/")),
|
|
251
267
|
});
|
|
252
268
|
|
|
253
269
|
const $ = cheerio.load(res.data);
|
|
@@ -265,10 +281,10 @@ export async function getEmbed(
|
|
|
265
281
|
: seasonUrl + "/" + scriptTag;
|
|
266
282
|
|
|
267
283
|
const episodesJs = await axios
|
|
268
|
-
.get(scriptUrl, { headers:
|
|
284
|
+
.get(scriptUrl, { headers: getHeaders(seasonUrl) })
|
|
269
285
|
.then((r) => r.data);
|
|
270
286
|
|
|
271
|
-
const matches = [...episodesJs.matchAll(/var\s+(eps\d+)\s*=\s*(\[[^\]]+\])/g)];
|
|
287
|
+
const matches = [...episodesJs.toLowerCase().matchAll(/var\s+(eps\d+)\s*=\s*(\[[^\]]+\])/g)];
|
|
272
288
|
if (!matches.length) throw new Error("No episode arrays found");
|
|
273
289
|
|
|
274
290
|
let episodeMatrix = [];
|
|
@@ -291,18 +307,18 @@ export async function getEmbed(
|
|
|
291
307
|
|
|
292
308
|
for (const host of hostPriority) {
|
|
293
309
|
for (const arr of episodeMatrix) {
|
|
294
|
-
if (i < arr.length && arr[i].includes(host)) {
|
|
295
|
-
if (!hosts.includes(host)) {
|
|
310
|
+
if (i < arr.length && arr[i].includes(host.toLowerCase())) {
|
|
311
|
+
if (!hosts.includes(host.toLowerCase())) {
|
|
296
312
|
urls.push(arr[i]);
|
|
297
|
-
hosts.push(host);
|
|
313
|
+
hosts.push(host.toLowerCase());
|
|
298
314
|
}
|
|
299
|
-
break;
|
|
315
|
+
break;
|
|
300
316
|
}
|
|
301
317
|
}
|
|
302
318
|
}
|
|
303
319
|
|
|
304
320
|
finalEmbeds.push({
|
|
305
|
-
title: null,
|
|
321
|
+
title: null,
|
|
306
322
|
url: urls.length ? urls : null,
|
|
307
323
|
host: hosts.length ? hosts : null,
|
|
308
324
|
});
|
|
@@ -313,22 +329,22 @@ export async function getEmbed(
|
|
|
313
329
|
|
|
314
330
|
for (const host of hostPriority) {
|
|
315
331
|
for (const arr of episodeMatrix) {
|
|
316
|
-
if (i < arr.length && arr[i].includes(host)) {
|
|
332
|
+
if (i < arr.length && arr[i].includes(host.toLowerCase())) {
|
|
317
333
|
selectedUrl = arr[i];
|
|
318
|
-
selectedHost = host;
|
|
334
|
+
selectedHost = host.toLowerCase();
|
|
319
335
|
break;
|
|
320
336
|
}
|
|
321
337
|
}
|
|
322
338
|
if (selectedUrl) break;
|
|
323
339
|
}
|
|
324
340
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
341
|
+
finalEmbeds.push({
|
|
342
|
+
title: null,
|
|
343
|
+
url: selectedUrl || null,
|
|
344
|
+
host: selectedHost || null,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
330
347
|
}
|
|
331
|
-
}
|
|
332
348
|
|
|
333
349
|
const titles = await getEpisodeTitles(seasonUrl, customChromiumPath);
|
|
334
350
|
finalEmbeds.forEach((embed, i) => {
|
|
@@ -351,8 +367,7 @@ export async function getEmbed(
|
|
|
351
367
|
|
|
352
368
|
|
|
353
369
|
export async function getAnimeInfo(animeUrl) {
|
|
354
|
-
const { headers
|
|
355
|
-
const res = await axios.get(animeUrl, { headers: headers });
|
|
370
|
+
const res = await axios.get(animeUrl, { headers: getHeaders(CATALOGUE_URL) });
|
|
356
371
|
const $ = cheerio.load(res.data);
|
|
357
372
|
|
|
358
373
|
const cover = $("#coverOeuvre").attr("src");
|
|
@@ -388,7 +403,6 @@ export async function getAvailableLanguages(
|
|
|
388
403
|
wantedLanguages = ["vostfr", "vf", "va", "vkr", "vcn", "vqc", "vf1", "vf2"],
|
|
389
404
|
numberEpisodes = false
|
|
390
405
|
) {
|
|
391
|
-
const { headers } = getHeaders(CATALOGUE_URL);
|
|
392
406
|
const languageLinks = [];
|
|
393
407
|
|
|
394
408
|
// Iterate over each possible language and check if the page exists
|
|
@@ -396,7 +410,7 @@ export async function getAvailableLanguages(
|
|
|
396
410
|
const languageUrl = seasonUrl.split('/').map((s, i) => i === 6 ? language : s).join('/');
|
|
397
411
|
try {
|
|
398
412
|
const res = await axios.get(languageUrl, {
|
|
399
|
-
headers:
|
|
413
|
+
headers: getHeaders(CATALOGUE_URL),
|
|
400
414
|
});
|
|
401
415
|
if (res.status === 200) {
|
|
402
416
|
if (numberEpisodes){
|
|
@@ -422,7 +436,6 @@ export async function getAllAnime(
|
|
|
422
436
|
output = "anime_list.json",
|
|
423
437
|
get_seasons = false
|
|
424
438
|
) {
|
|
425
|
-
const { headers } = getHeaders(CATALOGUE_URL);
|
|
426
439
|
let animeLinks = [];
|
|
427
440
|
|
|
428
441
|
const isWanted = (text, list) =>
|
|
@@ -430,30 +443,39 @@ export async function getAllAnime(
|
|
|
430
443
|
|
|
431
444
|
const fetchPage = async (pageNum) => {
|
|
432
445
|
const url = pageNum === 1 ? CATALOGUE_URL : `${CATALOGUE_URL}?page=${pageNum}`;
|
|
433
|
-
const res = await axios.get(url, { headers:
|
|
446
|
+
const res = await axios.get(url, { headers: getHeaders(CATALOGUE_URL) });
|
|
434
447
|
const $ = cheerio.load(res.data);
|
|
435
|
-
|
|
448
|
+
|
|
436
449
|
const containers = $("div.shrink-0.m-3.rounded.border-2");
|
|
437
|
-
|
|
450
|
+
|
|
438
451
|
containers.each((_, el) => {
|
|
439
452
|
const anchor = $(el).find("a");
|
|
440
453
|
const title = anchor.find("h1").text().trim();
|
|
441
454
|
const link = anchor.attr("href");
|
|
442
455
|
const img = anchor.find("img").attr("src");
|
|
443
|
-
|
|
456
|
+
|
|
444
457
|
const paragraphs = anchor.find("p").toArray().map(p => $(p).text().trim());
|
|
445
|
-
|
|
458
|
+
|
|
446
459
|
const altTitles = paragraphs[0] ? paragraphs[0].split(',').map(name => name.trim()) : [];
|
|
447
460
|
const genres = paragraphs[1] ? paragraphs[1].split(',').map(genre => genre.trim()) : [];
|
|
448
461
|
const type = paragraphs[2] ? paragraphs[2].split(',').map(t => t.trim()) : [];
|
|
449
462
|
const language = paragraphs[3] ? paragraphs[3].split(',').map(lang => lang.trim()) : [];
|
|
450
|
-
|
|
451
|
-
const
|
|
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));
|
|
473
|
+
|
|
452
474
|
if (
|
|
453
475
|
title &&
|
|
454
476
|
link &&
|
|
455
477
|
filteredTypes.length > 0 &&
|
|
456
|
-
filteredLanguages.length > 0
|
|
478
|
+
(filteredLanguages.length > 0 || hasScans)
|
|
457
479
|
) {
|
|
458
480
|
const fullUrl = link.startsWith("http") ? link : `${BASE_URL}${link}`;
|
|
459
481
|
animeLinks.push({
|
|
@@ -467,10 +489,10 @@ export async function getAllAnime(
|
|
|
467
489
|
});
|
|
468
490
|
}
|
|
469
491
|
});
|
|
470
|
-
|
|
492
|
+
|
|
471
493
|
return containers.length > 0;
|
|
472
494
|
};
|
|
473
|
-
|
|
495
|
+
|
|
474
496
|
const enrichWithSeasons = async (list) => {
|
|
475
497
|
for (const anime of list) {
|
|
476
498
|
try {
|
|
@@ -509,8 +531,7 @@ export async function getAllAnime(
|
|
|
509
531
|
|
|
510
532
|
export async function getLatestEpisodes(languageFilter = null) {
|
|
511
533
|
try {
|
|
512
|
-
const { headers
|
|
513
|
-
const res = await axios.get(BASE_URL, { headers: headers });
|
|
534
|
+
const res = await axios.get(BASE_URL, { headers: getHeaders() });
|
|
514
535
|
const $ = cheerio.load(res.data);
|
|
515
536
|
|
|
516
537
|
const container = $("#containerAjoutsAnimes");
|
|
@@ -522,7 +543,7 @@ export async function getLatestEpisodes(languageFilter = null) {
|
|
|
522
543
|
const cover = $(el).find("img").attr("src");
|
|
523
544
|
|
|
524
545
|
const buttons = $(el).find("button");
|
|
525
|
-
const language = $(buttons[0]).text().trim().toLowerCase();
|
|
546
|
+
const language = $(buttons[0]).text().trim().toLowerCase(); // Normalisation
|
|
526
547
|
const episode = $(buttons[1]).text().trim();
|
|
527
548
|
|
|
528
549
|
if (
|
|
@@ -550,17 +571,64 @@ export async function getLatestEpisodes(languageFilter = null) {
|
|
|
550
571
|
}
|
|
551
572
|
}
|
|
552
573
|
|
|
574
|
+
export async function getLatestScans(languageFilter = null) {
|
|
575
|
+
try {
|
|
576
|
+
const res = await axios.get(BASE_URL, { headers: getHeaders() });
|
|
577
|
+
const $ = cheerio.load(res.data);
|
|
578
|
+
|
|
579
|
+
const container = $("#containerAjoutsScans");
|
|
580
|
+
const scans = [];
|
|
581
|
+
|
|
582
|
+
container.find("a").each((_, el) => {
|
|
583
|
+
const url = $(el).attr("href");
|
|
584
|
+
const title = $(el).find("h1").text().trim();
|
|
585
|
+
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
|
+
|
|
592
|
+
if (
|
|
593
|
+
title &&
|
|
594
|
+
url &&
|
|
595
|
+
cover &&
|
|
596
|
+
type &&
|
|
597
|
+
language &&
|
|
598
|
+
chapter &&
|
|
599
|
+
(languageFilter === null ||
|
|
600
|
+
languageFilter
|
|
601
|
+
.map((l) => l.toLowerCase())
|
|
602
|
+
.includes(language.toLowerCase()))
|
|
603
|
+
) {
|
|
604
|
+
scans.push({
|
|
605
|
+
title,
|
|
606
|
+
url,
|
|
607
|
+
cover,
|
|
608
|
+
type,
|
|
609
|
+
language,
|
|
610
|
+
chapter,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
return scans;
|
|
616
|
+
} catch (err) {
|
|
617
|
+
console.error("Failed to fetch today scans:", err.message);
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
553
622
|
export async function getRandomAnime(
|
|
554
623
|
wantedLanguages = ["vostfr", "vf", "vastfr"],
|
|
555
624
|
wantedTypes = ["Anime", "Film"],
|
|
556
625
|
maxAttempts = null,
|
|
557
626
|
attempt = 0
|
|
558
627
|
) {
|
|
559
|
-
const { headers } = getHeaders(CATALOGUE_URL);
|
|
560
628
|
try {
|
|
561
629
|
const res = await axios.get(
|
|
562
630
|
`${CATALOGUE_URL}/?search=&random=1`,
|
|
563
|
-
{ headers:
|
|
631
|
+
{ headers: getHeaders(CATALOGUE_URL) }
|
|
564
632
|
);
|
|
565
633
|
|
|
566
634
|
const $ = cheerio.load(res.data);
|
|
@@ -627,3 +695,31 @@ export async function getRandomAnime(
|
|
|
627
695
|
}
|
|
628
696
|
}
|
|
629
697
|
|
|
698
|
+
|
|
699
|
+
export async function getAllTitleScans(mangaUrl, numberImg = false) {
|
|
700
|
+
const res = await axios.get(mangaUrl, { headers: getHeaders() });
|
|
701
|
+
const $ = cheerio.load(res.data);
|
|
702
|
+
|
|
703
|
+
const title = encodeURIComponent($("#titreOeuvre").text().trim());
|
|
704
|
+
const urlInfo = `https://anime-sama.fr/s2/scans/get_nb_chap_et_img.php?oeuvre=${title}`
|
|
705
|
+
const infoPage = await axios.get(urlInfo, { headers: getHeaders() });
|
|
706
|
+
const titleChapter = Object.keys(infoPage.data)
|
|
707
|
+
|
|
708
|
+
if (numberImg) {
|
|
709
|
+
return {scans :infoPage.data, mangaTitle :title}
|
|
710
|
+
} else {
|
|
711
|
+
return titleChapter
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export async function getImgScans(mangaUrl, wantedChapter) {
|
|
716
|
+
const infoScan = await getAllTitleScans(mangaUrl, true)
|
|
717
|
+
const numberImg = infoScan.scans[wantedChapter.toString()];
|
|
718
|
+
const mangaTitle = infoScan.mangaTitle
|
|
719
|
+
const imgUrls = [];
|
|
720
|
+
for (let i = 1; i <= numberImg; i++) {
|
|
721
|
+
imgUrls.push(`https://anime-sama.fr/s2/scans/${mangaTitle}/${wantedChapter}/${i}.jpg`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return imgUrls
|
|
725
|
+
}
|
package/scrapers/scrapers.js
CHANGED
|
@@ -4,14 +4,16 @@ import * as crunchyroll from "./crunchyroll.js";
|
|
|
4
4
|
|
|
5
5
|
export class AnimeScraper {
|
|
6
6
|
constructor(source) {
|
|
7
|
-
if (source ===
|
|
7
|
+
if (source === "animepahe") {
|
|
8
8
|
this.source = animepahe;
|
|
9
|
-
} else if (source ===
|
|
9
|
+
} else if (source === "animesama") {
|
|
10
10
|
this.source = animesama;
|
|
11
|
-
} else if (source ===
|
|
11
|
+
} else if (source === "crunchyroll") {
|
|
12
12
|
this.source = crunchyroll;
|
|
13
|
-
}
|
|
14
|
-
throw new Error(
|
|
13
|
+
} else {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'Invalid source. Choose either "animepahe", "crunchyroll" or "animesama".'
|
|
16
|
+
);
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -19,7 +21,9 @@ export class AnimeScraper {
|
|
|
19
21
|
try {
|
|
20
22
|
return await this.source.searchAnime(query, ...rest);
|
|
21
23
|
} catch (error) {
|
|
22
|
-
console.error(
|
|
24
|
+
console.error(
|
|
25
|
+
`This scraper does not have the searchAnime function implemented or an error happened -> ${error}`
|
|
26
|
+
);
|
|
23
27
|
return null;
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -28,7 +32,9 @@ export class AnimeScraper {
|
|
|
28
32
|
try {
|
|
29
33
|
return await this.source.getSeasons(animeUrl, ...rest);
|
|
30
34
|
} catch (error) {
|
|
31
|
-
console.error(
|
|
35
|
+
console.error(
|
|
36
|
+
`This scraper does not have the getSeasons function implemented or an error happened -> ${error}`
|
|
37
|
+
);
|
|
32
38
|
return null;
|
|
33
39
|
}
|
|
34
40
|
}
|
|
@@ -37,16 +43,30 @@ export class AnimeScraper {
|
|
|
37
43
|
try {
|
|
38
44
|
return await this.source.getEpisodeTitles(seasonUrl, ...rest);
|
|
39
45
|
} catch (error) {
|
|
40
|
-
console.error(
|
|
46
|
+
console.error(
|
|
47
|
+
`This scraper does not have the getEpisodeTitles function implemented or an error happened -> ${error}`
|
|
48
|
+
);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getEmbed(seasonUrl, ...rest) {
|
|
54
|
+
try {
|
|
55
|
+
return await this.source.getEmbed(seasonUrl, ...rest);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(
|
|
58
|
+
`This scraper does not have the getEmbed function implemented or an error happened -> ${error}`
|
|
59
|
+
);
|
|
41
60
|
return null;
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
|
-
|
|
45
63
|
async getEmbed(seasonUrl, ...rest) {
|
|
46
64
|
try {
|
|
47
65
|
return await this.source.getEmbed(seasonUrl, ...rest);
|
|
48
66
|
} catch (error) {
|
|
49
|
-
console.error(
|
|
67
|
+
console.error(
|
|
68
|
+
`This scraper does not have the getEmbed function implemented or an error happened -> ${error}`
|
|
69
|
+
);
|
|
50
70
|
return null;
|
|
51
71
|
}
|
|
52
72
|
}
|
|
@@ -55,7 +75,9 @@ export class AnimeScraper {
|
|
|
55
75
|
try {
|
|
56
76
|
return await this.source.getAnimeInfo(animeUrl);
|
|
57
77
|
} catch (error) {
|
|
58
|
-
console.error(
|
|
78
|
+
console.error(
|
|
79
|
+
`This scraper does not have the getAnimeInfo function implemented or an error happened -> ${error}`
|
|
80
|
+
);
|
|
59
81
|
return null;
|
|
60
82
|
}
|
|
61
83
|
}
|
|
@@ -64,7 +86,9 @@ export class AnimeScraper {
|
|
|
64
86
|
try {
|
|
65
87
|
return await this.source.getAvailableLanguages(animeUrl, ...rest);
|
|
66
88
|
} catch (error) {
|
|
67
|
-
console.error(
|
|
89
|
+
console.error(
|
|
90
|
+
`This scraper does not have the getAvailableLanguages function implemented or an error happened -> ${error}`
|
|
91
|
+
);
|
|
68
92
|
return null;
|
|
69
93
|
}
|
|
70
94
|
}
|
|
@@ -73,7 +97,9 @@ export class AnimeScraper {
|
|
|
73
97
|
try {
|
|
74
98
|
return await this.source.getAllAnime(...rest);
|
|
75
99
|
} catch (error) {
|
|
76
|
-
console.error(
|
|
100
|
+
console.error(
|
|
101
|
+
`This scraper does not have the getAllAnime function implemented or an error happened -> ${error}`
|
|
102
|
+
);
|
|
77
103
|
return null;
|
|
78
104
|
}
|
|
79
105
|
}
|
|
@@ -82,16 +108,30 @@ export class AnimeScraper {
|
|
|
82
108
|
try {
|
|
83
109
|
return await this.source.getLatestEpisodes(...rest);
|
|
84
110
|
} catch (error) {
|
|
85
|
-
console.error(
|
|
111
|
+
console.error(
|
|
112
|
+
`This scraper does not have the getLatestEpisodes function implemented or an error happened -> ${error}`
|
|
113
|
+
);
|
|
86
114
|
return null;
|
|
87
115
|
}
|
|
88
116
|
}
|
|
89
117
|
|
|
118
|
+
async getLatestScans(...rest) {
|
|
119
|
+
try {
|
|
120
|
+
return await this.source.getLatestScans(...rest);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(
|
|
123
|
+
`This scraper does not have the getLatestScans function implemented or an error happened -> ${error}`
|
|
124
|
+
);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
90
128
|
async getRandomAnime(...rest) {
|
|
91
129
|
try {
|
|
92
130
|
return await this.source.getRandomAnime(...rest);
|
|
93
131
|
} catch (error) {
|
|
94
|
-
console.error(
|
|
132
|
+
console.error(
|
|
133
|
+
`This scraper does not have the getRandomAnime function implemented or an error happened -> ${error}`
|
|
134
|
+
);
|
|
95
135
|
return null;
|
|
96
136
|
}
|
|
97
137
|
}
|
|
@@ -100,7 +140,30 @@ export class AnimeScraper {
|
|
|
100
140
|
try {
|
|
101
141
|
return await this.source.getEpisodeInfo(animeUrl, ...rest);
|
|
102
142
|
} catch (error) {
|
|
103
|
-
console.error(
|
|
143
|
+
console.error(
|
|
144
|
+
`This scraper does not have the getEpisodeInfo function implemented or an error happened -> ${error}`
|
|
145
|
+
);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getAllTitleScans(mangaUrl, ...rest) {
|
|
151
|
+
try {
|
|
152
|
+
return await this.source.getAllTitleScans(mangaUrl, ...rest);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(
|
|
155
|
+
`This scraper does not have the getAllTitleScans function implemented or an error happened -> ${error}`
|
|
156
|
+
);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async getImgScans(mangaUrl, ...rest) {
|
|
161
|
+
try {
|
|
162
|
+
return await this.source.getImgScans(mangaUrl, ...rest);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(
|
|
165
|
+
`This scraper does not have the getImgScans function implemented or an error happened -> ${error}`
|
|
166
|
+
);
|
|
104
167
|
return null;
|
|
105
168
|
}
|
|
106
169
|
}
|
package/utils/dispatcher.js
CHANGED
|
@@ -10,6 +10,9 @@ export async function getVideoUrlFromEmbed(source, embedUrl) {
|
|
|
10
10
|
if (source === "vidmoly" || source === "oneupload" ) {
|
|
11
11
|
return await extractor.getVidmolyOrOneuploadVideo(embedUrl);
|
|
12
12
|
}
|
|
13
|
+
if (source === "movearnpre" || source === "smoothpre" ) {
|
|
14
|
+
return await extractor.getMovearnpreOrSmoothpreVideo(embedUrl);
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
throw new Error(`Unsupported embed source: ${source}`);
|
|
15
18
|
}
|
package/utils/extractVideoUrl.js
CHANGED
|
@@ -60,7 +60,6 @@ export async function getVidmolyOrOneuploadVideo(embedUrl) {
|
|
|
60
60
|
const { data } = await axios.get(embedUrl, {
|
|
61
61
|
headers: getHeaders(embedUrl),
|
|
62
62
|
});
|
|
63
|
-
console.log(data)
|
|
64
63
|
const $ = cheerio.load(data);
|
|
65
64
|
const scripts = $("script");
|
|
66
65
|
|
|
@@ -77,3 +76,28 @@ export async function getVidmolyOrOneuploadVideo(embedUrl) {
|
|
|
77
76
|
return null;
|
|
78
77
|
}
|
|
79
78
|
}
|
|
79
|
+
|
|
80
|
+
import puppeteer from 'puppeteer';
|
|
81
|
+
|
|
82
|
+
export async function getMovearnpreOrSmoothpreVideo(embedUrl) {
|
|
83
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
84
|
+
const page = await browser.newPage();
|
|
85
|
+
|
|
86
|
+
await page.setUserAgent(
|
|
87
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await page.goto(embedUrl, { waitUntil: 'networkidle2' });
|
|
91
|
+
|
|
92
|
+
await page.waitForFunction('typeof jwplayer !== "undefined"');
|
|
93
|
+
|
|
94
|
+
const videoUrl = await page.evaluate(() => {
|
|
95
|
+
const player = jwplayer();
|
|
96
|
+
const sources = player?.getPlaylist()?.[0]?.sources;
|
|
97
|
+
return sources?.[0]?.file || null;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await browser.close();
|
|
101
|
+
const finalUrl = embedUrl.split("/").slice(0, 3).join("/") + videoUrl
|
|
102
|
+
return finalUrl;
|
|
103
|
+
}
|