@vyla-entertainment/sdk 1.15.0 → 1.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vyla-entertainment/sdk",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "scripts": {
5
5
  "test": "node test/index.js"
6
6
  },
@@ -29,5 +29,8 @@
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "node-tls-client": "^2.1.0"
32
35
  }
33
36
  }
package/src/config.js CHANGED
@@ -1,11 +1,23 @@
1
- // Currently active sources reported on 6:18:22 PM PST 9/13/2026
2
-
3
- // None Anime Tested: 936075
4
- // Anime Tested: 37854
5
-
6
- // Fshare/Fsonic Tested: 155 ( they don't have Michael )
7
-
8
1
  export const SOURCES = [
2
+ {
3
+ key: '3eyedraven',
4
+ label: '3eyedraven',
5
+ sourceFile: '3eyedraven',
6
+ proxyParam: '3er',
7
+ timeout: 25000,
8
+ jitter: 500,
9
+ retries: 2,
10
+ multiUrl: false,
11
+ skipProxy: true,
12
+ cdnHeaders: [{
13
+ pattern: /3eyedraven\.watch/i,
14
+ headers: {
15
+ Referer: 'https://3eyedraven.watch/',
16
+ Origin: 'https://3eyedraven.watch',
17
+ },
18
+ }],
19
+ },
20
+
9
21
  {
10
22
  key: '123anime',
11
23
  label: '123Anime',
@@ -106,6 +118,25 @@ export const SOURCES = [
106
118
  }],
107
119
  },
108
120
 
121
+ {
122
+ key: 'cinemaarmy',
123
+ label: 'CinemaArmy',
124
+ sourceFile: 'cinemaarmy',
125
+ proxyParam: 'ca',
126
+ timeout: 30000,
127
+ jitter: 500,
128
+ retries: 2,
129
+ multiUrl: true,
130
+ skipProxy: true,
131
+ cdnHeaders: [{
132
+ pattern: /cinema\.army/i,
133
+ headers: {
134
+ Referer: 'https://cinema.army/',
135
+ Origin: 'https://cinema.army',
136
+ },
137
+ }],
138
+ },
139
+
109
140
  {
110
141
  key: 'dulo',
111
142
  label: 'Dulo',
@@ -191,6 +222,25 @@ export const SOURCES = [
191
222
  },
192
223
  },
193
224
 
225
+ {
226
+ key: 'movenerds',
227
+ label: 'MoverNerds',
228
+ sourceFile: 'movenerds',
229
+ proxyParam: 'mnr',
230
+ timeout: 30000,
231
+ jitter: 500,
232
+ retries: 2,
233
+ multiUrl: true,
234
+ skipProxy: true,
235
+ cdnHeaders: [{
236
+ pattern: /movenerds\.space/i,
237
+ headers: {
238
+ Referer: 'https://movenerds.space/',
239
+ Origin: 'https://movenerds.space',
240
+ },
241
+ }],
242
+ },
243
+
194
244
  {
195
245
  key: 'movienight',
196
246
  label: 'MovieNight',
@@ -229,6 +279,25 @@ export const SOURCES = [
229
279
  }],
230
280
  },
231
281
 
282
+ {
283
+ key: 'peestream',
284
+ label: 'PeeStream',
285
+ sourceFile: 'peestream',
286
+ proxyParam: 'pee',
287
+ timeout: 25000,
288
+ jitter: 500,
289
+ retries: 2,
290
+ multiUrl: true,
291
+ skipProxy: true,
292
+ cdnHeaders: [{
293
+ pattern: /peestream\.in/i,
294
+ headers: {
295
+ Referer: 'https://peestream.in/',
296
+ Origin: 'https://peestream.in',
297
+ },
298
+ }],
299
+ },
300
+
232
301
  {
233
302
  key: 'pengu',
234
303
  label: 'Pengu',
@@ -293,6 +362,26 @@ export const SOURCES = [
293
362
  },
294
363
  }],
295
364
  },
365
+
366
+ {
367
+ key: 'thugtv',
368
+ label: 'ThugTV',
369
+ sourceFile: 'thugtv',
370
+ proxyParam: 'tv',
371
+ timeout: 30000,
372
+ jitter: 500,
373
+ retries: 2,
374
+ multiUrl: true,
375
+ skipProxy: true,
376
+ cdnHeaders: [{
377
+ pattern: /thugtv\.online|thuktv\.onrender\.com/i,
378
+ headers: {
379
+ Referer: 'https://thugtv.online/',
380
+ Origin: 'https://thugtv.online',
381
+ },
382
+ }],
383
+ },
384
+
296
385
  {
297
386
  key: 'vidapi',
298
387
  label: 'VidAPI',
@@ -426,6 +515,25 @@ export const SOURCES = [
426
515
  multiUrl: true,
427
516
  },
428
517
 
518
+ {
519
+ key: 'vidy',
520
+ label: 'Vidy',
521
+ sourceFile: 'vidy',
522
+ proxyParam: 'vy',
523
+ timeout: 25000,
524
+ jitter: 500,
525
+ retries: 2,
526
+ multiUrl: true,
527
+ skipProxy: true,
528
+ cdnHeaders: [{
529
+ pattern: /wecollege\.net|vidy\.st/i,
530
+ headers: {
531
+ Referer: 'https://www.vidy.st/',
532
+ Origin: 'https://www.vidy.st',
533
+ },
534
+ }],
535
+ },
536
+
429
537
  {
430
538
  key: 'vidzee',
431
539
  label: 'VidZee',
@@ -0,0 +1,168 @@
1
+ import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
2
+
3
+ const BASE_URL = "https://atsu.moe";
4
+ const CDN_URL = "https://cdn.atsu.moe";
5
+
6
+ const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
7
+
8
+ const HEADERS = {
9
+ "User-Agent": UA,
10
+ "Accept": "*/*",
11
+ "Accept-Language": "en-US,en;q=0.9",
12
+ "Accept-Encoding": "gzip, deflate, br",
13
+ "Cache-Control": "no-cache",
14
+ "Pragma": "no-cache",
15
+ "Referer": `${BASE_URL}/`,
16
+ "Origin": BASE_URL,
17
+ "Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
18
+ "Sec-Ch-Ua-Mobile": "?0",
19
+ "Sec-Ch-Ua-Platform": '"Windows"',
20
+ "Sec-Fetch-Dest": "empty",
21
+ "Sec-Fetch-Mode": "cors",
22
+ "Sec-Fetch-Site": "same-origin",
23
+ "Priority": "u=1, i"
24
+ };
25
+
26
+ let tlsInitialized = false;
27
+ let session = null;
28
+
29
+ async function ensureSession() {
30
+ if (!tlsInitialized) {
31
+ await initTLS();
32
+ tlsInitialized = true;
33
+ }
34
+ if (!session) {
35
+ session = new Session({
36
+ clientIdentifier: ClientIdentifier.chrome_131,
37
+ timeout: 15000,
38
+ insecureSkipVerify: false,
39
+ });
40
+ }
41
+ return session;
42
+ }
43
+
44
+ const formatAssetUrl = (url) => {
45
+ if (!url) return null;
46
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
47
+ if (url.startsWith("/static/")) return `${CDN_URL}${url}`;
48
+ if (url.startsWith("/")) return `${CDN_URL}${url}`;
49
+ return `${CDN_URL}/static/${url}`;
50
+ };
51
+
52
+ async function atsuFetch(url) {
53
+ const sess = await ensureSession();
54
+ const res = await sess.get(url, { headers: HEADERS });
55
+ if (res.status !== 200) {
56
+ throw new Error(`Atsu request failed: ${res.status}`);
57
+ }
58
+ const text = await res.text();
59
+ return JSON.parse(text);
60
+ }
61
+
62
+ export async function searchManga(query, page = 1, perPage = 12) {
63
+ const params = new URLSearchParams({
64
+ q: query,
65
+ query_by: "title,englishTitle,otherNames,authors,acronyms",
66
+ query_by_weights: "4,3,2,2,1",
67
+ num_typos: "4,3,2,1,0",
68
+ prefix: "true,true,true,true,false",
69
+ include_fields: "id,title,englishTitle,authors,poster,posterSmall,posterMedium,type,medium,isAdult,status,year,mbRating,popularity,dateAdded",
70
+ filter_by: "isAdult:=false && views:>0 && hidden:!=true",
71
+ page: String(page),
72
+ per_page: String(perPage),
73
+ infix: "off,off,fallback,off,off"
74
+ });
75
+
76
+ const data = await atsuFetch(`${BASE_URL}/collections/manga/documents/search?${params.toString()}`);
77
+ const results = (data?.hits || []).map(hit => {
78
+ const doc = hit.document || {};
79
+ return {
80
+ id: doc.id,
81
+ title: doc.title,
82
+ englishTitle: doc.englishTitle || null,
83
+ authors: Array.isArray(doc.authors) ? doc.authors : [],
84
+ type: doc.type,
85
+ medium: doc.medium,
86
+ status: doc.status,
87
+ year: doc.year,
88
+ rating: doc.mbRating ?? null,
89
+ popularity: doc.popularity ?? null,
90
+ poster: formatAssetUrl(doc.poster),
91
+ posterSmall: formatAssetUrl(doc.posterSmall),
92
+ posterMedium: formatAssetUrl(doc.posterMedium)
93
+ };
94
+ });
95
+
96
+ return {
97
+ page: data.page,
98
+ total: data.found,
99
+ results
100
+ };
101
+ }
102
+
103
+ export async function getMangaDetails(id) {
104
+ const data = await atsuFetch(`${BASE_URL}/api/manga/page?id=${encodeURIComponent(id)}`);
105
+ const page = data?.mangaPage;
106
+ if (!page) throw new Error(`Manga with ID "${id}" not found`);
107
+
108
+ return {
109
+ id: page.id,
110
+ title: page.title,
111
+ englishTitle: page.englishTitle || null,
112
+ synopsis: page.synopsis || null,
113
+ status: page.status,
114
+ type: page.type,
115
+ medium: page.medium,
116
+ rating: page.avgRating ?? null,
117
+ views: page.views ?? null,
118
+ authors: Array.isArray(page.authors) ? page.authors.map(a => a.name) : [],
119
+ genres: Array.isArray(page.genres) ? page.genres.map(g => g.name) : [],
120
+ tags: Array.isArray(page.tags) ? page.tags.map(t => t.name) : [],
121
+ poster: formatAssetUrl(page.poster?.largeImage || page.poster?.image),
122
+ totalChapters: page.totalChapterCount || page.chapters?.length || 0,
123
+ chapters: (page.chapters || []).map(ch => ({
124
+ id: ch.id,
125
+ number: ch.number,
126
+ title: ch.title,
127
+ pageCount: ch.pageCount,
128
+ createdAt: ch.createdAt ? new Date(ch.createdAt).toISOString() : null
129
+ }))
130
+ };
131
+ }
132
+
133
+ export async function getChapterPages(mangaId, chapterId) {
134
+ const data = await atsuFetch(
135
+ `${BASE_URL}/api/read/chapter?mangaId=${encodeURIComponent(mangaId)}&chapterId=${encodeURIComponent(chapterId)}`
136
+ );
137
+ const chapter = data?.readChapter;
138
+ if (!chapter) throw new Error(`Chapter "${chapterId}" not found for manga "${mangaId}"`);
139
+
140
+ return {
141
+ id: chapter.id,
142
+ number: chapter.number,
143
+ title: chapter.title,
144
+ scanlatorId: chapter.scanlationMangaId || null,
145
+ pages: (chapter.pages || []).map(p => ({
146
+ number: p.number,
147
+ url: formatAssetUrl(p.image),
148
+ width: p.width,
149
+ height: p.height,
150
+ aspectRatio: p.aspectRatio
151
+ }))
152
+ };
153
+ }
154
+
155
+ export async function cleanup() {
156
+ if (session) {
157
+ try {
158
+ await session.close();
159
+ } catch { }
160
+ session = null;
161
+ }
162
+ if (tlsInitialized) {
163
+ try {
164
+ await destroyTLS();
165
+ } catch { }
166
+ tlsInitialized = false;
167
+ }
168
+ }
package/src/sdk.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import SOURCES, { SOURCE_MAP } from "./config.js";
2
2
  import { fetchDownloads } from "./downloads/downloads.js";
3
3
  import { probeSource } from "./health.js";
4
+ import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js";
4
5
  import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
5
6
  import { createStreamArgs } from "./utils/helpers.js";
6
7
 
@@ -65,4 +66,13 @@ export default class VylaSDK {
65
66
  async getDownloads(id, s = null, e = null) {
66
67
  return await fetchDownloads(this, id, s, e);
67
68
  }
69
+ async searchManga(query, page = 1, perPage = 12) {
70
+ return await searchManga(query, page, perPage);
71
+ }
72
+ async getManga(id) {
73
+ return await getMangaDetails(id);
74
+ }
75
+ async getMangaChapter(mangaId, chapterId) {
76
+ return await getChapterPages(mangaId, chapterId);
77
+ }
68
78
  }
@@ -0,0 +1,136 @@
1
+ import { getUA } from "../utils/helpers.js";
2
+
3
+ const BASE_URL = "https://3eyedraven.watch";
4
+ const API_BASE = "https://3eyedraven.watch/api";
5
+
6
+ function getHeaders() {
7
+ return {
8
+ "User-Agent": getUA(),
9
+ Accept: "application/json",
10
+ Referer: `${BASE_URL}/`,
11
+ Origin: BASE_URL,
12
+ };
13
+ }
14
+
15
+ function resolveApiKey(args) {
16
+ return (
17
+ args?.tmdbApiKey ||
18
+ args?.sdk?.tmdbApiKey ||
19
+ process.env.TMDB_API_KEY ||
20
+ null
21
+ );
22
+ }
23
+
24
+ async function getTitle(id, isTv, apiKey) {
25
+ if (!apiKey) return null;
26
+ const type = isTv ? "tv" : "movie";
27
+ try {
28
+ const res = await fetch(
29
+ `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
30
+ { signal: AbortSignal.timeout(5000) }
31
+ );
32
+ if (!res.ok) return null;
33
+ const data = await res.json();
34
+ return isTv ? data.name : data.title;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function slugify(text) {
41
+ return text
42
+ .toLowerCase()
43
+ .normalize("NFKD")
44
+ .replace(/[\u0300-\u036f]/g, "")
45
+ .replace(/[^a-z0-9]+/g, "-")
46
+ .replace(/^-|-$/g, "");
47
+ }
48
+
49
+ function absolute(url) {
50
+ if (!url) return "";
51
+ if (/^https?:\/\//.test(url)) return url;
52
+ return `${BASE_URL}${url.startsWith("/") ? "" : "/"}${url}`;
53
+ }
54
+
55
+ export async function getStream(args) {
56
+ const { id, s = null, e = null } = args;
57
+ const isTv = s != null && e != null;
58
+
59
+ const apiKey = resolveApiKey(args);
60
+ const title = await getTitle(id, isTv, apiKey);
61
+
62
+ if (!title) {
63
+ if (process.env.DEBUG_CAUSE) {
64
+ console.error(
65
+ `[3eyedraven] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
66
+ `(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
67
+ );
68
+ }
69
+ return null;
70
+ }
71
+
72
+ const identifier = `${slugify(title)}-${id}`;
73
+
74
+ const url = isTv
75
+ ? `${API_BASE}/stream/${identifier}?season=${s}&episode=${e}`
76
+ : `${API_BASE}/stream/${identifier}`;
77
+
78
+ try {
79
+ const res = await fetch(url, {
80
+ headers: getHeaders(),
81
+ signal: AbortSignal.timeout(15000),
82
+ });
83
+
84
+ if (!res.ok) {
85
+ if (process.env.DEBUG_CAUSE) {
86
+ console.error(`[3eyedraven] HTTP ${res.status} for ${url}`);
87
+ }
88
+ return null;
89
+ }
90
+
91
+ const data = await res.json();
92
+ const stream = data?.stream;
93
+ if (!stream?.url) {
94
+ if (process.env.DEBUG_CAUSE) {
95
+ console.error(`[3eyedraven] No stream.url in response for ${url}`);
96
+ }
97
+ return null;
98
+ }
99
+
100
+ const streamUrl = absolute(stream.url);
101
+ if (!/^https?:\/\//.test(streamUrl)) return null;
102
+
103
+ const subtitles = (stream.subtitles || [])
104
+ .map(sub => {
105
+ const subUrl = absolute(sub.url);
106
+ if (!subUrl) return null;
107
+ return {
108
+ label: sub.label || sub.language || "Subtitle",
109
+ file: subUrl,
110
+ type: "vtt",
111
+ source: "3eyedraven",
112
+ };
113
+ })
114
+ .filter(Boolean);
115
+
116
+ return {
117
+ url: streamUrl,
118
+ headers: {
119
+ "User-Agent": getUA(),
120
+ Referer: `${BASE_URL}/`,
121
+ Origin: BASE_URL,
122
+ },
123
+ server: "3eyedraven",
124
+ quality: "1080p",
125
+ type: stream.format === "hls" ? "hls" : "mp4",
126
+ subtitles,
127
+ skipProxy: true,
128
+ };
129
+ } catch (err) {
130
+ if (process.env.DEBUG_CAUSE) {
131
+ console.error(`[3eyedraven] fetch error for ${url}:`, err.message);
132
+ if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
133
+ }
134
+ return null;
135
+ }
136
+ }
@@ -0,0 +1,181 @@
1
+ import crypto from "crypto";
2
+ import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
3
+
4
+ const BASE_URL = "https://cinema.army";
5
+ const API_BASE = "https://cinema.army/api";
6
+
7
+ const XOR_KEY = "f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548";
8
+ const HMAC_KEY = "a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5";
9
+
10
+ const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
11
+
12
+ const SERVERS = [
13
+ { key: "cookiebakers", method: "POST", label: "CookieBakers" },
14
+ { key: "tcloud", method: "GET", label: "TCloud" },
15
+ { key: "ipcloud", method: "GET", label: "IPcloud" },
16
+ { key: "dcloud", method: "GET", label: "Dcloud" },
17
+ ];
18
+
19
+ let tlsInitialized = false;
20
+ let session = null;
21
+
22
+ async function ensureSession() {
23
+ if (!tlsInitialized) {
24
+ await initTLS();
25
+ tlsInitialized = true;
26
+ }
27
+ if (!session) {
28
+ session = new Session({
29
+ clientIdentifier: ClientIdentifier.chrome_131,
30
+ timeout: 15000,
31
+ insecureSkipVerify: false,
32
+ });
33
+ }
34
+ return session;
35
+ }
36
+
37
+ function getHeaders(token, id, isTv, s, e) {
38
+ const referer = isTv
39
+ ? `${BASE_URL}/watch?id=${id}&type=tv&season=${s}&episode=${e}`
40
+ : `${BASE_URL}/watch?id=${id}&type=movie`;
41
+
42
+ return {
43
+ "User-Agent": UA,
44
+ "Accept": "application/json",
45
+ "Accept-Language": "en-US,en;q=0.9",
46
+ "Accept-Encoding": "gzip, deflate, br",
47
+ "Cache-Control": "no-cache",
48
+ "Pragma": "no-cache",
49
+ "Referer": referer,
50
+ "Origin": BASE_URL,
51
+ "Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
52
+ "Sec-Ch-Ua-Mobile": "?0",
53
+ "Sec-Ch-Ua-Platform": '"Windows"',
54
+ "Sec-Fetch-Dest": "empty",
55
+ "Sec-Fetch-Mode": "cors",
56
+ "Sec-Fetch-Site": "same-origin",
57
+ "Priority": "u=1, i",
58
+ "X-Stream-Token": token,
59
+ };
60
+ }
61
+
62
+ function generateToken(id) {
63
+ try {
64
+ const minute = Math.floor(Date.now() / 1000 / 60);
65
+ return crypto
66
+ .createHmac("sha256", HMAC_KEY)
67
+ .update(`${id}:${minute}`)
68
+ .digest("hex");
69
+ } catch {
70
+ return "";
71
+ }
72
+ }
73
+
74
+ function decryptStreamUrl(encoded) {
75
+ if (!encoded) return "";
76
+ if (encoded.startsWith("http://") || encoded.startsWith("https://")) return encoded;
77
+ try {
78
+ const decoded = Buffer.from(encoded, "base64").toString("binary");
79
+ let out = "";
80
+ for (let i = 0; i < decoded.length; i++) {
81
+ out += String.fromCharCode(
82
+ decoded.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length)
83
+ );
84
+ }
85
+ return out;
86
+ } catch {
87
+ return encoded;
88
+ }
89
+ }
90
+
91
+ export async function getStream(args) {
92
+ const { id, s = null, e = null } = args;
93
+ const isTv = s != null && e != null;
94
+
95
+ const token = generateToken(id);
96
+ const headers = getHeaders(token, id, isTv, s, e);
97
+
98
+ const sess = await ensureSession();
99
+
100
+ const requests = SERVERS.map(server => {
101
+ const path = isTv
102
+ ? `/${server.key}/tv/${id}/${s}/${e}`
103
+ : `/${server.key}/movie/${id}`;
104
+ return { url: `${API_BASE}${path}`, server };
105
+ });
106
+
107
+ const results = await Promise.allSettled(
108
+ requests.map(async ({ url, server }) => {
109
+ try {
110
+ const res = server.method === "POST"
111
+ ? await sess.post(url, { headers })
112
+ : await sess.get(url, { headers });
113
+
114
+ if (res.status !== 200) {
115
+ if (process.env.DEBUG_CAUSE) {
116
+ console.error(`[CinemaArmy ${server.key}] HTTP ${res.status}`);
117
+ }
118
+ return null;
119
+ }
120
+
121
+ const text = await res.text();
122
+ let data;
123
+ try {
124
+ data = JSON.parse(text);
125
+ } catch {
126
+ return null;
127
+ }
128
+
129
+ const encrypted = data?.stream?.url;
130
+ if (!encrypted) return null;
131
+
132
+ const streamUrl = decryptStreamUrl(encrypted);
133
+ if (!streamUrl || !/^https?:\/\//.test(streamUrl)) return null;
134
+
135
+ return {
136
+ url: streamUrl,
137
+ headers: {
138
+ "User-Agent": UA,
139
+ Referer: `${BASE_URL}/`,
140
+ Origin: BASE_URL,
141
+ },
142
+ server: `CinemaArmy (${server.label})`,
143
+ quality: "1080p",
144
+ type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
145
+ };
146
+ } catch (err) {
147
+ if (process.env.DEBUG_CAUSE) {
148
+ console.error(`[CinemaArmy ${server.key}] ${err.message}`);
149
+ if (err.cause) {
150
+ console.error(` cause.code: ${err.cause.code || "n/a"}`);
151
+ console.error(` cause.message: ${err.cause.message || err.cause}`);
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ })
157
+ );
158
+
159
+ const allUrls = results
160
+ .filter(r => r.status === "fulfilled" && r.value)
161
+ .map(r => r.value);
162
+
163
+ if (!allUrls.length) return null;
164
+
165
+ return { allUrls };
166
+ }
167
+
168
+ export async function cleanup() {
169
+ if (session) {
170
+ try {
171
+ await session.close();
172
+ } catch { }
173
+ session = null;
174
+ }
175
+ if (tlsInitialized) {
176
+ try {
177
+ await destroyTLS();
178
+ } catch { }
179
+ tlsInitialized = false;
180
+ }
181
+ }
@@ -0,0 +1,125 @@
1
+ import { getUA } from "../utils/helpers.js";
2
+
3
+ const API_BASE = "https://api.movenerds.space/api";
4
+
5
+ function getHeaders() {
6
+ return {
7
+ "User-Agent": getUA(),
8
+ Accept: "application/json",
9
+ Referer: "https://movenerds.space/",
10
+ Origin: "https://movenerds.space",
11
+ };
12
+ }
13
+
14
+ function resolveApiKey(args) {
15
+ return (
16
+ args?.tmdbApiKey ||
17
+ args?.sdk?.tmdbApiKey ||
18
+ process.env.TMDB_API_KEY ||
19
+ null
20
+ );
21
+ }
22
+
23
+ async function getMeta(id, isTv, apiKey) {
24
+ if (!apiKey) return null;
25
+ const type = isTv ? "tv" : "movie";
26
+ try {
27
+ const res = await fetch(
28
+ `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
29
+ { signal: AbortSignal.timeout(5000) }
30
+ );
31
+ if (!res.ok) return null;
32
+ const data = await res.json();
33
+ const title = isTv ? data.name : data.title;
34
+ const date = isTv ? data.first_air_date : data.release_date;
35
+ const year = date ? parseInt(date.slice(0, 4), 10) : null;
36
+ return { title, year };
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function mapSource(source) {
43
+ if (!source || !source.url) return null;
44
+ if (source.type !== "hls" && source.type !== "mp4") return null;
45
+ if (!/^https?:\/\//.test(source.url)) return null;
46
+
47
+ return {
48
+ url: source.url,
49
+ headers: {
50
+ "User-Agent": source.headers?.["User-Agent"] || getUA(),
51
+ Referer: source.headers?.Referer || "https://movenerds.space/",
52
+ Origin: source.headers?.Origin || "https://movenerds.space",
53
+ },
54
+ server: source.name || source.providerName || "MoverNerds",
55
+ quality: source.quality || "1080p",
56
+ type: source.type,
57
+ skipProxy: true,
58
+ };
59
+ }
60
+
61
+ export async function getStream(args) {
62
+ const { id, s = null, e = null } = args;
63
+ const isTv = s != null && e != null;
64
+
65
+ const apiKey = resolveApiKey(args);
66
+ const meta = await getMeta(id, isTv, apiKey);
67
+
68
+ if (!meta?.title) {
69
+ if (process.env.DEBUG_CAUSE) {
70
+ console.error(
71
+ `[movenerds] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
72
+ `(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
73
+ );
74
+ }
75
+ return null;
76
+ }
77
+
78
+ const params = new URLSearchParams({
79
+ tmdbId: String(id),
80
+ type: isTv ? "tv" : "movie",
81
+ title: meta.title,
82
+ });
83
+ if (meta.year) params.set("year", String(meta.year));
84
+ if (isTv) {
85
+ params.set("season", String(s));
86
+ params.set("episode", String(e));
87
+ params.set("language", "en");
88
+ }
89
+
90
+ const url = `${API_BASE}/stream?${params.toString()}`;
91
+
92
+ try {
93
+ const res = await fetch(url, {
94
+ headers: getHeaders(),
95
+ signal: AbortSignal.timeout(20000),
96
+ });
97
+
98
+ if (!res.ok) {
99
+ if (process.env.DEBUG_CAUSE) {
100
+ console.error(`[movenerds] HTTP ${res.status} for ${url}`);
101
+ }
102
+ return null;
103
+ }
104
+
105
+ const data = await res.json();
106
+ const primary = Array.isArray(data?.primarySources) ? data.primarySources : [];
107
+ if (!primary.length) {
108
+ if (process.env.DEBUG_CAUSE) {
109
+ console.error(`[movenerds] No primarySources in response for ${url}`);
110
+ }
111
+ return null;
112
+ }
113
+
114
+ const allUrls = primary.map(mapSource).filter(Boolean);
115
+ if (!allUrls.length) return null;
116
+
117
+ return { allUrls };
118
+ } catch (err) {
119
+ if (process.env.DEBUG_CAUSE) {
120
+ console.error(`[movenerds] fetch error for ${url}:`, err.message);
121
+ if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
122
+ }
123
+ return null;
124
+ }
125
+ }
@@ -0,0 +1,139 @@
1
+ import { USER_AGENT, fetchJson, getTmdbInfo } from '../utils/helpers.js';
2
+
3
+ const BASE_URL = 'https://providers.peestream.in';
4
+
5
+ const PROVIDERS = {
6
+ purstream: 'Athena',
7
+ vidrock: 'Thor',
8
+ vaplayer: 'Poseidon',
9
+ netmirror: 'Ra'
10
+ };
11
+
12
+ export async function getStream(args) {
13
+ const { id, s, e, server, tmdbApiKey } = args;
14
+ const isTv = s != null && e != null;
15
+ const type = isTv ? 'tv' : 'movie';
16
+
17
+ let title = args.title || '';
18
+ let releaseYear = args.year || '';
19
+ let imdbId = args.imdbId || '';
20
+ let seasonTmdbId = args.seasonTmdbId || '';
21
+ let episodeTmdbId = args.episodeTmdbId || '';
22
+
23
+ try {
24
+ const info = await getTmdbInfo(id, type, s, tmdbApiKey);
25
+ if (info) {
26
+ title = title || info.name || info.title || '';
27
+
28
+ if (!releaseYear) {
29
+ const date = info.release_date || info.first_air_date || '';
30
+ releaseYear = date ? date.split('-')[0] : (info.year || '');
31
+ }
32
+
33
+ if (!imdbId && info.external_ids?.imdb_id) {
34
+ imdbId = info.external_ids.imdb_id;
35
+ } else if (!imdbId && info.imdb_id) {
36
+ imdbId = info.imdb_id;
37
+ }
38
+
39
+ if (isTv) {
40
+ if (!seasonTmdbId && Array.isArray(info.seasons)) {
41
+ const matchedSeason = info.seasons.find(
42
+ item => item.season_number === Number(s)
43
+ );
44
+ if (matchedSeason) {
45
+ seasonTmdbId = String(matchedSeason.id);
46
+ }
47
+ }
48
+
49
+ if (!episodeTmdbId && tmdbApiKey) {
50
+ try {
51
+ const seasonData = await fetchJson(
52
+ `https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${tmdbApiKey}`
53
+ );
54
+ if (seasonData?.episodes) {
55
+ const matchedEp = seasonData.episodes.find(
56
+ ep => ep.episode_number === Number(e)
57
+ );
58
+ if (matchedEp) {
59
+ episodeTmdbId = String(matchedEp.id);
60
+ }
61
+ }
62
+ } catch { }
63
+ }
64
+ }
65
+ }
66
+ } catch { }
67
+
68
+ const queryParams = new URLSearchParams({
69
+ type,
70
+ tmdbId: String(id),
71
+ title: title || String(id)
72
+ });
73
+
74
+ if (releaseYear) queryParams.set('releaseYear', String(releaseYear));
75
+ if (imdbId) queryParams.set('imdbId', String(imdbId));
76
+
77
+ if (isTv) {
78
+ queryParams.set('seasonNumber', String(s));
79
+ queryParams.set('episodeNumber', String(e));
80
+ if (seasonTmdbId) queryParams.set('seasonTmdbId', String(seasonTmdbId));
81
+ if (episodeTmdbId) queryParams.set('episodeTmdbId', String(episodeTmdbId));
82
+ }
83
+
84
+ const allUrls = [];
85
+
86
+ try {
87
+ const scrapeUrl = `${BASE_URL}/scrape?${queryParams.toString()}`;
88
+ const res = await fetch(scrapeUrl, {
89
+ headers: {
90
+ 'User-Agent': USER_AGENT,
91
+ 'Accept': 'text/event-stream',
92
+ 'Referer': `${BASE_URL}/`
93
+ },
94
+ signal: AbortSignal.timeout(20000)
95
+ });
96
+
97
+ if (res.ok) {
98
+ const text = await res.text();
99
+ const events = text.split('\n\n');
100
+
101
+ for (const ev of events) {
102
+ if (ev.includes('event: completed')) {
103
+ const dataMatch = ev.match(/data:\s*(.+)/);
104
+ if (dataMatch) {
105
+ try {
106
+ const parsed = JSON.parse(dataMatch[1].trim());
107
+ const stream = parsed.stream;
108
+ const streamUrl = stream?.playlist || stream?.url || stream?.file;
109
+ if (streamUrl) {
110
+ const sourceName = PROVIDERS[parsed.sourceId] || parsed.sourceId || 'Poseidon';
111
+ allUrls.push({
112
+ url: streamUrl,
113
+ server: `PeeStream - ${sourceName}`,
114
+ quality: 'Auto',
115
+ type: stream.type === 'hls' || streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
116
+ headers: stream.headers || {
117
+ 'User-Agent': USER_AGENT
118
+ }
119
+ });
120
+ }
121
+ } catch { }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ } catch { }
127
+
128
+ if (server && server !== 'all') {
129
+ const clean = server.toLowerCase().replace('peestream - ', '').trim();
130
+ const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
131
+ if (filtered.length) return { allUrls: filtered };
132
+ }
133
+
134
+ return allUrls.length ? { allUrls } : null;
135
+ }
136
+
137
+ export async function getSources() {
138
+ return Object.values(PROVIDERS).map(name => `PeeStream - ${name}`);
139
+ }
@@ -1,5 +1,3 @@
1
- // im being salty
2
-
3
1
  import { USER_AGENT, fetchJson } from '../utils/helpers.js';
4
2
 
5
3
  const C_ARRAY = [
@@ -0,0 +1,62 @@
1
+ import { getUA } from "../utils/helpers.js";
2
+
3
+ const API_BASE = "https://thuktv.onrender.com/api";
4
+
5
+ function getHeaders() {
6
+ return {
7
+ "User-Agent": getUA(),
8
+ Accept: "application/json",
9
+ Referer: "https://thugtv.online/",
10
+ Origin: "https://thugtv.online",
11
+ };
12
+ }
13
+
14
+ function mapSource(source) {
15
+ if (!source || !source.url) return null;
16
+ if (source.type !== "hls" && source.type !== "mp4") return null;
17
+ if (!/^https?:\/\//.test(source.url)) return null;
18
+
19
+ return {
20
+ url: source.url,
21
+ headers: {
22
+ "User-Agent": getUA(),
23
+ Referer: "https://thugtv.online/",
24
+ Origin: "https://thugtv.online",
25
+ },
26
+ server: source.name || source.label || "ThugTV",
27
+ quality: source.quality || "1080p",
28
+ type: source.type,
29
+ skipProxy: true,
30
+ };
31
+ }
32
+
33
+ export async function getStream(args) {
34
+ const { id, s = null, e = null } = args;
35
+ const isTv = s != null && e != null;
36
+
37
+ const url = isTv
38
+ ? `${API_BASE}/stream/tv/${id}/${s}/${e}`
39
+ : `${API_BASE}/stream/movie/${id}`;
40
+
41
+ try {
42
+ const res = await fetch(url, {
43
+ headers: getHeaders(),
44
+ signal: AbortSignal.timeout(15000),
45
+ });
46
+ if (!res.ok) return null;
47
+
48
+ const data = await res.json();
49
+ const sources = Array.isArray(data?.sources) ? data.sources : [];
50
+ if (!sources.length) return null;
51
+
52
+ const allUrls = sources
53
+ .map(mapSource)
54
+ .filter(Boolean);
55
+
56
+ if (!allUrls.length) return null;
57
+
58
+ return { allUrls };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
@@ -0,0 +1,187 @@
1
+ import { getUA } from "../utils/helpers.js";
2
+
3
+ const API_URL = "https://api.wecollege.net";
4
+ const SERVERS = ["miami", "atlanta", "seattle", "denver", "dallas", "tampa"];
5
+ const MAGIC = [109, 118, 109, 49];
6
+
7
+ function imul(a, b) {
8
+ return Math.imul(a | 0, b | 0) >>> 0;
9
+ }
10
+
11
+ function pFn(e) {
12
+ e = e >>> 0;
13
+ e = (e ^ (e >>> 16)) >>> 0;
14
+ e = imul(e, 2246822507);
15
+ e = (e ^ (e >>> 13)) >>> 0;
16
+ e = imul(e, 3266489909);
17
+ e = (e ^ (e >>> 16)) >>> 0;
18
+ return e;
19
+ }
20
+
21
+ function gFn(e, t) {
22
+ e = e >>> 0;
23
+ t = t & 31;
24
+ if (t === 0) return e;
25
+ return ((e << t) | (e >>> (32 - t))) >>> 0;
26
+ }
27
+
28
+ function decryptVidy(ciphertext, seedStr, mediaId) {
29
+ let padded = ciphertext.replace(/-/g, "+").replace(/_/g, "/");
30
+ while (padded.length % 4) padded += "=";
31
+
32
+ const raw = new Uint8Array(Buffer.from(padded, "base64"));
33
+ const length = raw.length;
34
+
35
+ let tFnv = 2166136261;
36
+ for (let i = 0; i < seedStr.length; i++) {
37
+ tFnv = imul((tFnv ^ seedStr.charCodeAt(i)) >>> 0, 16777619);
38
+ }
39
+ const fnvRes = pFn(tFnv);
40
+
41
+ const innerArg = (((mediaId >>> 0) ^ 2654435769) >>> 0);
42
+ const inner = pFn(innerArg);
43
+ let n = pFn(((fnvRes ^ inner) >>> 0));
44
+
45
+ const S = {};
46
+ for (let e = 0; e < 8; e++) {
47
+ const t = n % 61;
48
+ n = gFn((n + 2654435769) >>> 0, 7 + (7 & e));
49
+ S[t] = ((n ^ pFn(n)) >>> 0);
50
+ n = pFn((n + t) >>> 0);
51
+ }
52
+
53
+ let acc = pFn(((2779096485 ^ n) >>> 0));
54
+ const keystream = new Uint8Array(length);
55
+ let sCnt = 0;
56
+ let idx = 0;
57
+
58
+ while (idx < length) {
59
+ const sVal = acc;
60
+ const o = sVal % 61;
61
+ const hasO = Object.prototype.hasOwnProperty.call(S, o);
62
+ const iVal = hasO ? -1 : 0;
63
+ const d = hasO ? S[o] : 0;
64
+ const nTerm = ((d ^ imul(2654435769, sCnt + 1)) >>> 0);
65
+ sCnt += 1;
66
+ const aTerm = sVal;
67
+ let lVal = (((aTerm ^ nTerm) | (aTerm & nTerm & iVal)) >>> 0);
68
+ const term1 = gFn((lVal + sVal) >>> 0, 31 & o);
69
+ const term2 = gFn(sVal, (31 & imul(o, 7)) >>> 0);
70
+ lVal = ((term1 ^ term2) >>> 0);
71
+ const newS = pFn((lVal + 2654435769) >>> 0);
72
+ S[o] = newS;
73
+ acc = newS;
74
+ const tVal = newS;
75
+
76
+ keystream[idx++] = tVal & 0xFF;
77
+ if (idx < length) keystream[idx++] = (tVal >>> 8) & 0xFF;
78
+ if (idx < length) keystream[idx++] = (tVal >>> 16) & 0xFF;
79
+ if (idx < length) keystream[idx++] = (tVal >>> 24) & 0xFF;
80
+ }
81
+
82
+ for (let i = 0; i < length; i++) {
83
+ raw[i] ^= keystream[i];
84
+ }
85
+
86
+ for (let i = 0; i < MAGIC.length; i++) {
87
+ if (raw[i] !== MAGIC[i]) {
88
+ throw new Error("Vidy decrypt failed: magic header mismatch");
89
+ }
90
+ }
91
+
92
+ return new TextDecoder().decode(raw.slice(MAGIC.length));
93
+ }
94
+
95
+ export async function getStream(args) {
96
+ const { id, s = null, e = null } = args;
97
+ const isTv = s != null && e != null;
98
+ const mediaId = Number(id);
99
+
100
+ const headers = {
101
+ "User-Agent": getUA(),
102
+ Referer: "https://www.vidy.st/",
103
+ Origin: "https://www.vidy.st",
104
+ };
105
+
106
+ try {
107
+ const seedUrl = new URL(`${API_URL}/seed`);
108
+ seedUrl.searchParams.set("mediaId", String(id));
109
+
110
+ const seedRes = await fetch(seedUrl.toString(), {
111
+ headers,
112
+ signal: AbortSignal.timeout(6000),
113
+ });
114
+ if (!seedRes.ok) return null;
115
+
116
+ const seedData = await seedRes.json();
117
+ const seed = seedData?.seed;
118
+ if (!seed) return null;
119
+
120
+ for (const server of SERVERS) {
121
+ try {
122
+ const srcUrl = new URL(`${API_URL}/${server}/sources`);
123
+ srcUrl.searchParams.set("mediaType", isTv ? "tv" : "movie");
124
+ srcUrl.searchParams.set("tmdbId", String(id));
125
+ srcUrl.searchParams.set("enc", "2");
126
+ srcUrl.searchParams.set("seed", seed);
127
+ if (isTv) {
128
+ srcUrl.searchParams.set("seasonId", String(s));
129
+ srcUrl.searchParams.set("episodeId", String(e));
130
+ }
131
+
132
+ const res = await fetch(srcUrl.toString(), {
133
+ headers,
134
+ signal: AbortSignal.timeout(6000),
135
+ });
136
+ if (!res.ok) continue;
137
+
138
+ const text = await res.text();
139
+
140
+ let data;
141
+ try {
142
+ data = JSON.parse(decryptVidy(text, seed, mediaId));
143
+ } catch {
144
+ continue;
145
+ }
146
+
147
+ const sources = data?.sources || [];
148
+ const playlist = data?.playlist || "";
149
+ if (!sources.length && !playlist) continue;
150
+
151
+ let streamUrl = playlist;
152
+ let quality = "1080p";
153
+
154
+ for (const src of sources) {
155
+ if (src?.quality === "1080p" && src?.url) {
156
+ streamUrl = src.url;
157
+ quality = "1080p";
158
+ break;
159
+ }
160
+ }
161
+ if (!streamUrl && sources.length) {
162
+ streamUrl = sources[0]?.url || "";
163
+ quality = sources[0]?.quality || "";
164
+ }
165
+ if (!streamUrl) continue;
166
+
167
+ return {
168
+ url: streamUrl,
169
+ headers: {
170
+ "User-Agent": getUA(),
171
+ Referer: "https://www.vidy.st/",
172
+ Origin: "https://www.vidy.st",
173
+ },
174
+ server: "Vidy",
175
+ quality,
176
+ type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
177
+ };
178
+ } catch {
179
+ continue;
180
+ }
181
+ }
182
+
183
+ return null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }