@definisi/vidsrc-scraper 1.1.0 → 2.0.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/README.md +41 -39
- package/dist/scraper.js +65 -62
- package/dist/types.d.ts +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @definisi/vidsrc-scraper
|
|
2
2
|
|
|
3
|
-
Extract HLS streaming URLs from VidSrc using
|
|
3
|
+
Extract HLS streaming URLs from VidSrc using pure HTTP requests.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @definisi/vidsrc-scraper
|
|
@@ -13,42 +13,36 @@ npm install @definisi/vidsrc-scraper
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { scrapeVidsrc } from '@definisi/vidsrc-scraper';
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
const
|
|
16
|
+
// Scrape a movie by TMDB ID
|
|
17
|
+
const result = await scrapeVidsrc('27205', 'movie');
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
if (result.success) {
|
|
20
|
+
console.log('HLS URL:', result.hlsUrl);
|
|
21
|
+
console.log('Subtitles:', result.subtitles);
|
|
22
|
+
}
|
|
21
23
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
timeout: 15000,
|
|
25
|
-
cacheTtl: 600,
|
|
26
|
-
});
|
|
24
|
+
// Scrape a TV episode
|
|
25
|
+
const tvResult = await scrapeVidsrc('1399', 'tv', '1', '1');
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
### scrapeVidsrc(tmdbId, type?, season?, episode?, options?)
|
|
32
|
-
|
|
33
|
-
| Parameter | Type | Default | Description |
|
|
34
|
-
|-----------|------|---------|-------------|
|
|
35
|
-
| tmdbId | string | - | TMDB ID |
|
|
36
|
-
| type | 'movie' \| 'tv' | 'movie' | Content type |
|
|
37
|
-
| season | string \| null | null | Season number (TV only) |
|
|
38
|
-
| episode | string \| null | null | Episode number (TV only) |
|
|
39
|
-
| options | ScrapeOptions | {} | Optional config |
|
|
40
|
-
|
|
41
|
-
### ScrapeOptions
|
|
28
|
+
## Options
|
|
42
29
|
|
|
43
30
|
| Option | Type | Default | Description |
|
|
44
31
|
|--------|------|---------|-------------|
|
|
45
|
-
| timeout | number | 30000 | Request timeout
|
|
46
|
-
| cacheTtl | number | 900 | Cache
|
|
32
|
+
| timeout | number | 30000 | Request timeout in milliseconds |
|
|
33
|
+
| cacheTtl | number | 900 | Cache time-to-live in seconds |
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const result = await scrapeVidsrc('27205', 'movie', null, null, {
|
|
37
|
+
timeout: 60000,
|
|
38
|
+
cacheTtl: 1800,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
## Result Type
|
|
49
43
|
|
|
50
44
|
```typescript
|
|
51
|
-
{
|
|
45
|
+
interface ScrapeResult {
|
|
52
46
|
tmdbId: string;
|
|
53
47
|
type: 'movie' | 'tv';
|
|
54
48
|
season: string | null;
|
|
@@ -60,20 +54,28 @@ const result = await scrapeVidsrc('27205', 'movie', null, null, {
|
|
|
60
54
|
}
|
|
61
55
|
```
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
## Playback
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
The HLS URLs require proper headers. Example with VLC:
|
|
66
60
|
|
|
67
|
-
|
|
61
|
+
```bash
|
|
62
|
+
vlc "HLS_URL" \
|
|
63
|
+
--http-user-agent="Mozilla/5.0" \
|
|
64
|
+
--http-referrer="https://cloudnestra.com/"
|
|
65
|
+
```
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
## How It Works
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
1. Fetches embed page from `vidsrc-embed.ru`
|
|
70
|
+
2. Extracts RCP iframe URL from cloudnestra.com
|
|
71
|
+
3. Extracts prorcp hash from RCP page
|
|
72
|
+
4. Fetches M3U8 URL from prorcp endpoint
|
|
73
|
+
5. Resolves domain placeholders (`{v1}` - `{v5}` -> `cloudnestra.com`)
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
# VLC
|
|
75
|
-
vlc "HLS_URL" --http-user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0"
|
|
75
|
+
## Requirements
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
- Node.js 18+
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/dist/scraper.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0';
|
|
1
4
|
const DEFAULT_OPTIONS = {
|
|
2
5
|
timeout: 30000,
|
|
3
6
|
cacheTtl: 900,
|
|
4
7
|
};
|
|
5
|
-
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0';
|
|
6
|
-
const CDN_DOMAINS = ['cloudnestra.com', 'quibblezoomfable.com'];
|
|
7
8
|
const cache = new Map();
|
|
9
|
+
function createClient(timeout) {
|
|
10
|
+
return axios.create({
|
|
11
|
+
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
|
12
|
+
headers: {
|
|
13
|
+
'User-Agent': UA,
|
|
14
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
15
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
16
|
+
'Accept-Encoding': 'identity',
|
|
17
|
+
},
|
|
18
|
+
timeout,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
8
21
|
function getCacheKey(tmdbId, type, season, episode) {
|
|
9
22
|
return `${type}-${tmdbId}-${season || ''}-${episode || ''}`;
|
|
10
23
|
}
|
|
@@ -20,84 +33,74 @@ function setCache(key, data) {
|
|
|
20
33
|
cache.set(key, { data, timestamp: Date.now() });
|
|
21
34
|
}
|
|
22
35
|
function buildEmbedUrl(tmdbId, type, season, episode) {
|
|
23
|
-
const base = 'https://
|
|
36
|
+
const base = 'https://vidsrc-embed.ru/embed';
|
|
24
37
|
if (type === 'tv') {
|
|
25
|
-
return `${base}/tv
|
|
26
|
-
}
|
|
27
|
-
return `${base}/movie?tmdb=${tmdbId}`;
|
|
28
|
-
}
|
|
29
|
-
async function fetchWithHeaders(url, referer) {
|
|
30
|
-
const headers = {
|
|
31
|
-
'User-Agent': UA,
|
|
32
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
33
|
-
'Accept-Language': 'en-US,en;q=0.5',
|
|
34
|
-
};
|
|
35
|
-
if (referer) {
|
|
36
|
-
headers['Referer'] = referer;
|
|
37
|
-
}
|
|
38
|
-
const res = await fetch(url, { headers });
|
|
39
|
-
if (!res.ok)
|
|
40
|
-
throw new Error(`HTTP ${res.status}`);
|
|
41
|
-
return res.text();
|
|
42
|
-
}
|
|
43
|
-
async function resolveM3u8Domain(templateUrl) {
|
|
44
|
-
for (const domain of CDN_DOMAINS) {
|
|
45
|
-
const testUrl = templateUrl.replace(/\{v\d+\}/, domain);
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(testUrl, {
|
|
48
|
-
method: 'HEAD',
|
|
49
|
-
headers: { 'User-Agent': UA },
|
|
50
|
-
});
|
|
51
|
-
if (res.ok)
|
|
52
|
-
return testUrl;
|
|
53
|
-
}
|
|
54
|
-
catch { }
|
|
38
|
+
return `${base}/tv/${tmdbId}/${season}/${episode}`;
|
|
55
39
|
}
|
|
56
|
-
return
|
|
40
|
+
return `${base}/movie/${tmdbId}`;
|
|
57
41
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return subtitles;
|
|
42
|
+
function resolveM3u8Url(url) {
|
|
43
|
+
// Replace domain placeholders {v1} through {v5} with cloudnestra.com
|
|
44
|
+
return url
|
|
45
|
+
.split(' or ')[0]
|
|
46
|
+
.trim()
|
|
47
|
+
.replace(/\{v[1-5]\}/g, 'cloudnestra.com');
|
|
67
48
|
}
|
|
68
49
|
export async function scrapeVidsrc(tmdbId, type = 'movie', season = null, episode = null, options = {}) {
|
|
69
50
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
70
51
|
const cacheKey = getCacheKey(tmdbId, type, season, episode);
|
|
71
52
|
const cached = getFromCache(cacheKey, opts.cacheTtl);
|
|
72
|
-
if (cached)
|
|
53
|
+
if (cached) {
|
|
73
54
|
return cached;
|
|
55
|
+
}
|
|
56
|
+
const client = createClient(opts.timeout);
|
|
74
57
|
let hlsUrl = null;
|
|
75
|
-
|
|
58
|
+
const subtitles = [];
|
|
76
59
|
try {
|
|
60
|
+
// Step 1: Get embed page
|
|
77
61
|
const embedUrl = buildEmbedUrl(tmdbId, type, season, episode);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
const embedRes = await client.get(embedUrl);
|
|
63
|
+
// Step 2: Extract cloudnestra RCP iframe URL
|
|
64
|
+
const rcpMatch = embedRes.data.match(/src=["']((?:https?:)?\/\/[^"']*cloudnestra\.com\/rcp\/[^"']+)["']/i);
|
|
65
|
+
if (!rcpMatch) {
|
|
66
|
+
throw new Error('No RCP iframe found in embed page');
|
|
67
|
+
}
|
|
82
68
|
let rcpUrl = rcpMatch[1];
|
|
83
69
|
if (rcpUrl.startsWith('//'))
|
|
84
70
|
rcpUrl = 'https:' + rcpUrl;
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
71
|
+
// Step 3: Get RCP page
|
|
72
|
+
const rcpRes = await client.get(rcpUrl, {
|
|
73
|
+
headers: { Referer: embedUrl },
|
|
74
|
+
});
|
|
75
|
+
// Step 4: Extract prorcp hash from loadIframe function
|
|
76
|
+
const prorcpMatch = rcpRes.data.match(/\/prorcp\/([a-zA-Z0-9=+/]+)/);
|
|
77
|
+
if (!prorcpMatch) {
|
|
78
|
+
throw new Error('No prorcp hash found in RCP page');
|
|
79
|
+
}
|
|
80
|
+
const prorcpHash = prorcpMatch[1];
|
|
81
|
+
const rcpHost = new URL(rcpUrl).origin;
|
|
82
|
+
// Step 5: Get prorcp page
|
|
83
|
+
const prorcpUrl = `${rcpHost}/prorcp/${prorcpHash}`;
|
|
84
|
+
const prorcpRes = await client.get(prorcpUrl, {
|
|
85
|
+
headers: { Referer: rcpUrl },
|
|
86
|
+
});
|
|
87
|
+
// Step 6: Extract M3U8 file URL
|
|
88
|
+
const fileMatch = prorcpRes.data.match(/file:\s*["']([^"']+)["']/);
|
|
89
|
+
if (!fileMatch) {
|
|
90
|
+
throw new Error('No file URL found in prorcp page');
|
|
91
|
+
}
|
|
92
|
+
// Step 7: Resolve domain placeholders
|
|
93
|
+
hlsUrl = resolveM3u8Url(fileMatch[1]);
|
|
94
|
+
// Extract subtitles if present
|
|
95
|
+
const subMatches = prorcpRes.data.matchAll(/["'](https?:\/\/[^"']+\.(?:vtt|srt))["']/gi);
|
|
96
|
+
for (const match of subMatches) {
|
|
97
|
+
if (!subtitles.includes(match[1])) {
|
|
98
|
+
subtitles.push(match[1]);
|
|
99
|
+
}
|
|
96
100
|
}
|
|
97
|
-
subtitles = extractSubtitles(prorcpHtml);
|
|
98
101
|
}
|
|
99
102
|
catch (error) {
|
|
100
|
-
console.error(`[
|
|
103
|
+
console.error(`[vidsrc] ${error.message}`);
|
|
101
104
|
}
|
|
102
105
|
const result = {
|
|
103
106
|
tmdbId,
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@definisi/vidsrc-scraper",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Extract HLS streaming URLs from VidSrc
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Extract HLS streaming URLs from VidSrc using axios",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"tmdb"
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"axios": "^1.6.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|