@vyla-entertainment/sdk 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/package.json +1 -1
- package/src/config.js +26 -47
- package/src/sdk.d.ts +1 -1
- package/src/sources/1embed.js +34 -11
- package/src/sources/anihq.js +14 -2
- package/src/sources/anineko.js +122 -15
- package/src/sources/flaxmovies.js +21 -9
- package/src/sources/fsonic.js +2 -2
- package/src/sources/fsonline.js +2 -2
- package/src/sources/lmscript.js +43 -0
- package/src/sources/videasy.js +6 -12
- package/src/sources/vidvault.js +114 -0
- package/src/sources/vidzee.js +73 -87
- package/src/sources/vixsrc.js +28 -14
- package/src/utils/helpers.js +56 -20
- package/src/sources/goated.js +0 -262
- package/src/sources/nebula.js +0 -62
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { USER_AGENT } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://vidvault.ru";
|
|
4
|
+
const HEADERS = {
|
|
5
|
+
"Referer": `${BASE_URL}/`,
|
|
6
|
+
"Origin": BASE_URL,
|
|
7
|
+
"User-Agent": USER_AGENT,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
async function getToken() {
|
|
11
|
+
const res = await fetch(`${BASE_URL}/api/get-token`, {
|
|
12
|
+
method: "GET",
|
|
13
|
+
headers: HEADERS
|
|
14
|
+
});
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
return data.t;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getCaptionType(url) {
|
|
20
|
+
return url.toLowerCase().includes(".vtt") ? "vtt" : "srt";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getStream(args) {
|
|
24
|
+
const { id, s, e } = args;
|
|
25
|
+
const type = (s != null && e != null) ? "tv" : "movie";
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const token = await getToken();
|
|
29
|
+
|
|
30
|
+
const payload = { type, tmdbId: String(id) };
|
|
31
|
+
if (type === "tv") {
|
|
32
|
+
payload.season = String(s);
|
|
33
|
+
payload.episode = String(e);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const res = await fetch(`${BASE_URL}/api/download-proxy`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
...HEADERS,
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"x-request-token": token
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(payload)
|
|
44
|
+
});
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
|
|
47
|
+
const allUrls = [];
|
|
48
|
+
const subtitles = [];
|
|
49
|
+
|
|
50
|
+
const mp4 = data?.mp4Data;
|
|
51
|
+
if (mp4) {
|
|
52
|
+
const country = mp4.country || "";
|
|
53
|
+
const lanName = mp4.lanName || "";
|
|
54
|
+
const detailPath = mp4.detailPath || "";
|
|
55
|
+
|
|
56
|
+
const downloads = mp4.downloadInfo?.data?.downloads || [];
|
|
57
|
+
for (const d of downloads) {
|
|
58
|
+
if (!d?.url) continue;
|
|
59
|
+
allUrls.push({
|
|
60
|
+
quality: `${d.resolution}p ${detailPath} ${country} ${lanName}`.trim(),
|
|
61
|
+
url: d.url,
|
|
62
|
+
type: "mp4",
|
|
63
|
+
headers: HEADERS
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const srtCaptions = mp4.downloadInfo?.data?.captions || [];
|
|
68
|
+
for (const c of srtCaptions) {
|
|
69
|
+
if (!c?.url) continue;
|
|
70
|
+
subtitles.push({
|
|
71
|
+
label: c.lanName || c.lan || "Unknown",
|
|
72
|
+
type: getCaptionType(c.url),
|
|
73
|
+
language: c.lan || "",
|
|
74
|
+
url: c.url
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const mkvFiles = data?.mkvData?.files || [];
|
|
80
|
+
for (const f of mkvFiles) {
|
|
81
|
+
if (!f?.url) continue;
|
|
82
|
+
allUrls.push({
|
|
83
|
+
quality: `MKV ${f.size || ""}`.trim(),
|
|
84
|
+
url: f.url,
|
|
85
|
+
type: "mp4",
|
|
86
|
+
headers: HEADERS
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const mkvV2 = data?.mkvV2Data;
|
|
91
|
+
if (mkvV2?.url) {
|
|
92
|
+
allUrls.push({
|
|
93
|
+
quality: `${mkvV2.quality || ""} ${mkvV2.language || ""} ${mkvV2.country || ""} (${mkvV2.size || ""})`.trim(),
|
|
94
|
+
url: mkvV2.url,
|
|
95
|
+
type: "mp4",
|
|
96
|
+
headers: HEADERS
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const mkvV3 = data?.mkvV3Data;
|
|
101
|
+
if (mkvV3?.url) {
|
|
102
|
+
allUrls.push({
|
|
103
|
+
quality: `${mkvV3.quality || ""} ${mkvV3.language || ""} ${mkvV3.country || ""} (${mkvV3.size || ""})`.trim(),
|
|
104
|
+
url: mkvV3.url,
|
|
105
|
+
type: "mp4",
|
|
106
|
+
headers: HEADERS
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return allUrls.length ? { allUrls, subtitles } : null;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/sources/vidzee.js
CHANGED
|
@@ -1,70 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
// i can't fix this, i'll wait for someone else to fix this lol
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
2
4
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
|
3
5
|
|
|
4
|
-
const PLAYER_URL = 'https://player.vidzee.wtf';
|
|
5
6
|
const CORE_URL = 'https://core.vidzee.wtf';
|
|
7
|
+
const PLAYER_URL = 'https://player.vidzee.wtf';
|
|
8
|
+
const KEY = 'c4a8f1d7e2b9a6c3d0f5e8a1b7c4d9e2';
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const now = Date.now();
|
|
11
|
-
if (cachedKey && now - cachedKeyTs < 300000) return cachedKey;
|
|
12
|
-
|
|
13
|
-
const res = await fetch(`${CORE_URL}/api-key`, {
|
|
14
|
-
headers,
|
|
15
|
-
signal: AbortSignal.timeout(7000)
|
|
16
|
-
});
|
|
17
|
-
if (!res.ok) throw new Error(`API key fetch failed: ${res.status}`);
|
|
18
|
-
|
|
19
|
-
const e = await res.text();
|
|
20
|
-
const t = Buffer.from(e.replace(/\s+/g, ''), 'base64');
|
|
21
|
-
|
|
22
|
-
const s = new Uint8Array(t.length - 28 + 16);
|
|
23
|
-
s.set(t.subarray(28), 0);
|
|
24
|
-
s.set(t.subarray(12, 28), t.length - 28);
|
|
25
|
-
|
|
26
|
-
const keyMat = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode('c4a8f1d7e2b9a6c3d0f5e8a1b7c4d9e2'));
|
|
27
|
-
const key = await webcrypto.subtle.importKey('raw', keyMat, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
10
|
+
function decryptAes(encryptedBase64) {
|
|
11
|
+
const keyBuf = Buffer.from(KEY, 'utf8');
|
|
12
|
+
const ciphertext = Buffer.from(encryptedBase64, 'base64');
|
|
28
13
|
|
|
29
14
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
cachedKeyTs = now;
|
|
38
|
-
return cachedKey;
|
|
39
|
-
} catch (err) {
|
|
40
|
-
throw new Error('Failed to decrypt Vidzee master key');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
15
|
+
const decipher = crypto.createDecipheriv('aes-256-ecb', keyBuf, null);
|
|
16
|
+
decipher.setAutoPadding(true);
|
|
17
|
+
let dec = decipher.update(ciphertext);
|
|
18
|
+
dec = Buffer.concat([dec, decipher.final()]);
|
|
19
|
+
const text = dec.toString('utf8');
|
|
20
|
+
if (text.startsWith('http') || text.includes('{')) return text;
|
|
21
|
+
} catch { }
|
|
43
22
|
|
|
44
|
-
function decrypt(enc, key) {
|
|
45
23
|
try {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
24
|
+
const iv = Buffer.alloc(16, 0);
|
|
25
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuf, iv);
|
|
26
|
+
decipher.setAutoPadding(true);
|
|
27
|
+
let dec = decipher.update(ciphertext);
|
|
28
|
+
dec = Buffer.concat([dec, decipher.final()]);
|
|
29
|
+
const text = dec.toString('utf8');
|
|
30
|
+
if (text.startsWith('http') || text.includes('{')) return text;
|
|
31
|
+
} catch { }
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return decrypted.toString(CryptoJS.enc.Utf8) || '';
|
|
65
|
-
} catch (err) {
|
|
66
|
-
return '';
|
|
67
|
-
}
|
|
33
|
+
try {
|
|
34
|
+
const iv = ciphertext.subarray(0, 16);
|
|
35
|
+
const data = ciphertext.subarray(16);
|
|
36
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuf, iv);
|
|
37
|
+
decipher.setAutoPadding(true);
|
|
38
|
+
let dec = decipher.update(data);
|
|
39
|
+
dec = Buffer.concat([dec, decipher.final()]);
|
|
40
|
+
const text = dec.toString('utf8');
|
|
41
|
+
if (text.startsWith('http') || text.includes('{')) return text;
|
|
42
|
+
} catch { }
|
|
43
|
+
|
|
44
|
+
return null;
|
|
68
45
|
}
|
|
69
46
|
|
|
70
47
|
export async function getStream({ id, s, e, clientIP }) {
|
|
@@ -77,44 +54,53 @@ export async function getStream({ id, s, e, clientIP }) {
|
|
|
77
54
|
};
|
|
78
55
|
|
|
79
56
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
57
|
+
const isTv = s != null && e != null;
|
|
58
|
+
const servers = ['tik', 'ipcloud', 'v6:Hindi'];
|
|
59
|
+
const results = [];
|
|
83
60
|
|
|
84
|
-
for (const sr of
|
|
61
|
+
for (const sr of servers) {
|
|
85
62
|
try {
|
|
86
|
-
const url =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
63
|
+
const url = isTv
|
|
64
|
+
? `${CORE_URL}/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(sr)}&e=${e}`
|
|
65
|
+
: `${CORE_URL}/streams/movie/${id}?s=${encodeURIComponent(sr)}&e=1`;
|
|
66
|
+
|
|
67
|
+
const data = await fetchJson(url, { headers, signal: AbortSignal.timeout(6000) });
|
|
68
|
+
|
|
69
|
+
if (data && data.c) {
|
|
70
|
+
const decryptedText = decryptAes(data.c);
|
|
71
|
+
if (decryptedText) {
|
|
72
|
+
let streamUrl = decryptedText;
|
|
73
|
+
let extraHeaders = {};
|
|
74
|
+
|
|
75
|
+
if (decryptedText.startsWith('{')) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(decryptedText);
|
|
78
|
+
streamUrl = parsed.url || parsed.link || parsed.file;
|
|
79
|
+
if (parsed.headers) extraHeaders = parsed.headers;
|
|
80
|
+
} catch { }
|
|
81
|
+
}
|
|
95
82
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
};
|
|
83
|
+
if (streamUrl && streamUrl.startsWith('http')) {
|
|
84
|
+
results.push({
|
|
85
|
+
url: streamUrl,
|
|
86
|
+
server: `VidZee - ${sr}`,
|
|
87
|
+
skipHlsCheck: true,
|
|
88
|
+
headers: {
|
|
89
|
+
'User-Agent': USER_AGENT,
|
|
90
|
+
'Referer': PLAYER_URL,
|
|
91
|
+
'Origin': PLAYER_URL,
|
|
92
|
+
...extraHeaders
|
|
93
|
+
}
|
|
94
|
+
});
|
|
109
95
|
}
|
|
110
96
|
}
|
|
111
97
|
}
|
|
112
98
|
} catch (err) {
|
|
113
|
-
continue;
|
|
114
99
|
}
|
|
115
100
|
}
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
|
|
102
|
+
return results.length ? { allUrls: results } : null;
|
|
103
|
+
} catch {
|
|
118
104
|
return null;
|
|
119
105
|
}
|
|
120
106
|
}
|
package/src/sources/vixsrc.js
CHANGED
|
@@ -13,7 +13,11 @@ const HEADERS = {
|
|
|
13
13
|
function extract(regex, text) {
|
|
14
14
|
const m = text?.match(regex);
|
|
15
15
|
if (!m) return null;
|
|
16
|
-
|
|
16
|
+
let val = m[1];
|
|
17
|
+
val = val.replace(/\\u0026/gi, '&');
|
|
18
|
+
val = val.replace(/\\\//g, '/');
|
|
19
|
+
val = val.replace(/\\/g, '');
|
|
20
|
+
return val;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export async function getStream({ id, s, e, absoluteBase }) {
|
|
@@ -22,36 +26,40 @@ export async function getStream({ id, s, e, absoluteBase }) {
|
|
|
22
26
|
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
|
|
23
27
|
: `${BASE_URL}/api/movie/${id}`;
|
|
24
28
|
|
|
25
|
-
const apiProxy = absoluteBase
|
|
29
|
+
const apiProxy = absoluteBase && absoluteBase !== '/'
|
|
26
30
|
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
|
27
31
|
: apiUrl;
|
|
28
32
|
|
|
29
|
-
const apiText = await fetchText(apiProxy).catch(() => null);
|
|
33
|
+
const apiText = await fetchText(apiProxy, { headers: HEADERS }).catch(() => null);
|
|
30
34
|
const apiData = JSON.parse(apiText || '{}');
|
|
31
35
|
if (!apiData.src) return null;
|
|
32
36
|
|
|
33
37
|
const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
|
|
34
|
-
const embedProxy = absoluteBase
|
|
38
|
+
const embedProxy = absoluteBase && absoluteBase !== '/'
|
|
35
39
|
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
|
36
40
|
: embedUrl;
|
|
37
41
|
|
|
38
|
-
const html = await fetchText(embedProxy).catch(() => null);
|
|
42
|
+
const html = await fetchText(embedProxy, { headers: HEADERS }).catch(() => null);
|
|
39
43
|
if (!html) return null;
|
|
40
44
|
|
|
41
45
|
const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
|
|
42
46
|
const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
|
|
43
47
|
let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
|
|
48
|
+
|
|
44
49
|
if (!token || !expires || !playlist) return null;
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
if (playlist.startsWith('/')) {
|
|
52
|
+
playlist = `${BASE_URL}${playlist}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
|
48
56
|
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
|
|
49
57
|
|
|
50
|
-
const masterProxy = absoluteBase
|
|
51
|
-
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
|
58
|
+
const masterProxy = absoluteBase && absoluteBase !== '/'
|
|
59
|
+
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}`
|
|
52
60
|
: rawMasterUrl;
|
|
53
61
|
|
|
54
|
-
const playlistText = await fetchText(
|
|
62
|
+
const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
|
|
55
63
|
if (!playlistText?.includes('#EXTM3U')) return null;
|
|
56
64
|
|
|
57
65
|
let finalUrl = null;
|
|
@@ -74,17 +82,23 @@ export async function getStream({ id, s, e, absoluteBase }) {
|
|
|
74
82
|
if (!finalUrl) finalUrl = rawMasterUrl;
|
|
75
83
|
|
|
76
84
|
if (!finalUrl.startsWith('http')) {
|
|
77
|
-
const
|
|
78
|
-
|
|
85
|
+
const baseMatch = rawMasterUrl.match(/^(https?:\/\/[^\/]+)/);
|
|
86
|
+
const base = baseMatch ? baseMatch[1] : BASE_URL;
|
|
87
|
+
if (finalUrl.startsWith('/')) {
|
|
88
|
+
finalUrl = `${base}${finalUrl}`;
|
|
89
|
+
} else {
|
|
90
|
+
const pathParts = rawMasterUrl.split('/');
|
|
91
|
+
pathParts.pop();
|
|
92
|
+
finalUrl = `${pathParts.join('/')}/${finalUrl}`;
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
const alreadyProxied = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\?url=/.test(finalUrl);
|
|
82
|
-
|
|
83
96
|
return {
|
|
84
97
|
allUrls: [
|
|
85
98
|
{
|
|
86
99
|
url: finalUrl,
|
|
87
|
-
|
|
100
|
+
server: 'VixSrc',
|
|
101
|
+
skipProxy: true,
|
|
88
102
|
headers: {
|
|
89
103
|
...HEADERS,
|
|
90
104
|
'Referer': `${BASE_URL}/`
|
package/src/utils/helpers.js
CHANGED
|
@@ -67,8 +67,7 @@ function cacheGet(cache, key) {
|
|
|
67
67
|
|
|
68
68
|
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
|
|
69
69
|
|
|
70
|
-
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=null) {
|
|
71
|
-
const getAbsoluteBase = host => (host.startsWith('localhost') || host.startsWith('127.0.0.1')) ? `http://${host}` : `https://${host}`;
|
|
70
|
+
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP = null) {
|
|
72
71
|
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
|
|
73
72
|
return {
|
|
74
73
|
id: id,
|
|
@@ -83,13 +82,25 @@ export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=nu
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
|
|
86
|
-
|
|
85
|
+
let apiKey = tmdbApiKey;
|
|
86
|
+
let id = tmdbId;
|
|
87
|
+
let type = mediaType;
|
|
88
|
+
if (id === 'movie' || id === 'tv' || !id) {
|
|
89
|
+
if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
|
|
90
|
+
type = id || 'movie';
|
|
91
|
+
id = apiKey;
|
|
92
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!apiKey) {
|
|
96
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
97
|
+
}
|
|
98
|
+
const key = `${id}-${type}`;
|
|
87
99
|
const cached = cacheGet(tmdbValidationCache, key);
|
|
88
100
|
if (cached !== undefined) return cached;
|
|
89
|
-
|
|
90
|
-
if (!k) return { valid: false, error: 'TMDB API key not configured' };
|
|
101
|
+
if (!apiKey) return { valid: false, error: 'TMDB API key not configured' };
|
|
91
102
|
try {
|
|
92
|
-
const res = await fetch(`https://api.themoviedb.org/3/${
|
|
103
|
+
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
|
|
93
104
|
if (res.ok) {
|
|
94
105
|
const data = await res.json();
|
|
95
106
|
const isValid = !!(data?.id && (data?.title || data?.name));
|
|
@@ -108,15 +119,30 @@ export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
|
|
111
|
-
|
|
122
|
+
let apiKey = tmdbApiKey;
|
|
123
|
+
let id = tmdbId;
|
|
124
|
+
let type = mediaType;
|
|
125
|
+
let s = season;
|
|
126
|
+
if (apiKey === 'movie' || apiKey === 'tv') {
|
|
127
|
+
type = apiKey;
|
|
128
|
+
id = tmdbId;
|
|
129
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
130
|
+
} else if (id === 'movie' || id === 'tv') {
|
|
131
|
+
type = id;
|
|
132
|
+
id = apiKey;
|
|
133
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
134
|
+
}
|
|
135
|
+
if (!apiKey) {
|
|
136
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
137
|
+
}
|
|
138
|
+
const key = `${id}-${type}-${s || ''}`;
|
|
112
139
|
const cached = cacheGet(tmdbInfoCache, key);
|
|
113
140
|
if (cached !== undefined) return cached;
|
|
114
|
-
|
|
115
|
-
if (!k) return { isAnime: false, titles: [], year: null, imdbId: null };
|
|
141
|
+
if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null };
|
|
116
142
|
try {
|
|
117
143
|
const [mainRes, seasonRes] = await Promise.all([
|
|
118
|
-
fetch(`https://api.themoviedb.org/3/${
|
|
119
|
-
|
|
144
|
+
fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
|
|
145
|
+
s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
|
|
120
146
|
]);
|
|
121
147
|
let mainData = null, seasonData = null;
|
|
122
148
|
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
|
|
@@ -125,10 +151,10 @@ export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
|
|
|
125
151
|
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
|
|
126
152
|
const titles = [];
|
|
127
153
|
if (seasonData?.name) titles.push(seasonData.name);
|
|
128
|
-
const
|
|
154
|
+
const titleText = mainData?.title || mainData?.name || '';
|
|
129
155
|
const ot = mainData?.original_title || mainData?.original_name || '';
|
|
130
|
-
if (
|
|
131
|
-
if (ot && ot !==
|
|
156
|
+
if (titleText) titles.push(titleText);
|
|
157
|
+
if (ot && ot !== titleText) titles.push(ot);
|
|
132
158
|
const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
|
|
133
159
|
const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
|
|
134
160
|
cacheSet(tmdbInfoCache, key, result, 600000);
|
|
@@ -178,16 +204,26 @@ export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year
|
|
|
178
204
|
}
|
|
179
205
|
|
|
180
206
|
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
|
|
181
|
-
|
|
207
|
+
let apiKey = tmdbApiKey;
|
|
208
|
+
let id = imdbId;
|
|
209
|
+
let type = mediaType;
|
|
210
|
+
if (id === 'movie' || id === 'tv') {
|
|
211
|
+
type = id;
|
|
212
|
+
id = apiKey;
|
|
213
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
214
|
+
}
|
|
215
|
+
if (!apiKey) {
|
|
216
|
+
apiKey = process.env.TMDB_API_KEY;
|
|
217
|
+
}
|
|
218
|
+
const key = `imdb-${id}`;
|
|
182
219
|
const cached = cacheGet(tmdbInfoCache, key);
|
|
183
220
|
if (cached !== undefined) return cached;
|
|
184
|
-
|
|
185
|
-
if (!k) return null;
|
|
221
|
+
if (!apiKey) return null;
|
|
186
222
|
try {
|
|
187
|
-
const data = await fetchJson(`https://api.themoviedb.org/3/find/${
|
|
223
|
+
const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
|
|
188
224
|
let result = null;
|
|
189
|
-
if (
|
|
190
|
-
else if (
|
|
225
|
+
if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
|
|
226
|
+
else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
|
|
191
227
|
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
|
|
192
228
|
return result;
|
|
193
229
|
} catch { return null; }
|