diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a643bc8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +TMDB_API_KEY=your_tmdb_api_key_here \ No newline at end of file diff --git a/README.md b/README.md index d1edc22..4a9c745 100644 --- a/README.md +++ b/README.md @@ -11,36 +11,8 @@ npm install @vyla-entertainment/sdk pnpm install @vyla-entertainment/sdk ``` -## Usage -Start by creating a VylaSDK element with your TMDB api key. -```js -import VylaSDK from "@vyla-entertainment/sdk"; +View our full documentation at https://docs.vyla.cc/sdk -const sdk = new VylaSDK({ - tmdbApiKey: ... -}); -``` - -You can now use this to access Vyla's content. -```js -// Get sources -// Args: excludeDisabled -sdk.getSources(false); -> {key, timeout, label, ...}[] - -// Get subtitles -// Args: tmdb id, season?, episode? -await sdk.getSubtitles('0000000', null, null); -> {label, file, type, source}[] - -// Probe & test a source -// Args: source key -await sdk.probeSource('nebula'); -> {ok, ms} - -// Probe & test all sources -await sdk.probeAllSources(); -> {'key': {ok, ms}, ...} - -// Get HLS streams -// Args: source key, tmdb id, season?, episode?, clientIP? -await sdk.getStream('nebula', '0000000', null, null, null) -> {allUrls: {url, quality, label?, server?, type?, skipProxy?, skipVerify?, headers? ...}[]} ``` # [DMCA](https://docs.vyla.cc/misc/dmca) diff --git a/package.json b/package.json index 57b530e..474f578 100644 --- a/package.json +++ b/package.json @@ -29,4 +29,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/src/config.js b/src/config.js index efd2ddb..05d00cc 100644 --- a/src/config.js +++ b/src/config.js @@ -1,3 +1,10 @@ +// Currently active sources reported on 7:47:50 PM PST 7/6/2026 + +// None Anime Tested: 936075 +// Anime Tested: 37854 + +// Fshare/Fsonic Tested: 155 ( they don't have Michael ) + export const SOURCES = [ { key: '1embed', @@ -107,6 +114,18 @@ export const SOURCES = [ }, }, + { + key: 'cinejoy', + label: 'Cinejoy', + sourceFile: 'cinejoy', + proxyParam: 'cj', + timeout: 30000, + jitter: 500, + retries: 1, + multiUrl: true, + skipProxy: true, + }, + { key: 'flaxmovies', label: 'FlaxMovies', @@ -512,6 +531,7 @@ export const SOURCES = [ retries: 2, skipProxy: true, }, + ]; export const HEALTH_PROBE_ID = '155'; diff --git a/src/downloads/downloads.js b/src/downloads/downloads.js index e553685..709994a 100644 --- a/src/downloads/downloads.js +++ b/src/downloads/downloads.js @@ -1,8 +1,12 @@ import { getDownloads, getDownloadsTv } from "./sources/trendimovies.js"; export async function fetchDownloads(id, s = null, e = null) { - const res = s != null && e != null - ? await getDownloadsTv(id, s, e) - : await getDownloads(id); - return res; + try { + const res = s != null && e != null + ? await getDownloadsTv(id, s, e) + : await getDownloads(id); + return res || []; + } catch { + return []; + } } \ No newline at end of file diff --git a/src/sources/cinejoy.js b/src/sources/cinejoy.js new file mode 100644 index 0000000..4c73159 --- /dev/null +++ b/src/sources/cinejoy.js @@ -0,0 +1,210 @@ +import crypto from "crypto"; + +export const ID = "cinejoy"; +export const DOMAIN = "https://cinejoy.to"; +export const API_DOMAIN = "https://api.shegu.st"; +export const ENC_DEC_API = "https://enc-dec.app/api"; +export const RETURN_ALL_URLS = true; + +const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"; + +function countLeadingZeroBits(buffer) { + let count = 0; + for (const value of buffer) { + if (value === 0) { + count += 8; + continue; + } + count += 8 - value.toString(2).length; + break; + } + return count; +} + +function solveChallenge(data) { + const saltStr = `pow2-salt|${data.s}|${data.b}`; + const salt = crypto.createHash("sha256").update(saltStr).digest(); + const n = data.n; + const r = data.r; + const p = data.p; + const d = data.d; + const b = data.b; + const s = data.s; + + for (let counter = 0; counter < 1000000; counter++) { + const payload = `pow2|${b}|${s}|${counter}`; + const result = crypto.scryptSync(payload, salt, 32, { + N: n, + r: r, + p: p, + maxmem: 128 * n * r * 2 + }); + + if (countLeadingZeroBits(result) >= d) { + const solved = { ...data, c: counter }; + return Buffer.from(JSON.stringify(solved)).toString("base64"); + } + } + throw new Error("Challenge solve failed"); +} + +async function getTmdbMetadata(tmdbId, isTv) { + const apiKey = process.env.TMDB_API_KEY; + if (!apiKey) return null; + const type = isTv ? "tv" : "movie"; + try { + const url = `https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${apiKey}&append_to_response=external_ids`; + const res = await fetch(url, { signal: AbortSignal.timeout(10000) }); + if (!res.ok) return null; + const data = await res.json(); + const title = isTv ? data.name : data.title; + const year = isTv ? (data.first_air_date ? data.first_air_date.split("-")[0] : "") : (data.release_date ? data.release_date.split("-")[0] : ""); + const imdbId = data.external_ids?.imdb_id || ""; + return { title, year, imdbId }; + } catch (err) { + return null; + } +} + +export async function getStream({ id, s, e, clientIP }) { + try { + const isTv = s && e && s !== "null" && e !== "null"; + const meta = await getTmdbMetadata(id, isTv); + if (!meta) return null; + + let servers = []; + try { + const serverRes = await fetch(`${API_DOMAIN}/servers`, { + headers: { + "Origin": DOMAIN, + "Referer": DOMAIN + "/", + "User-Agent": UA + }, + signal: AbortSignal.timeout(15000) + }); + if (serverRes.ok) { + const data = await serverRes.json(); + servers = data.servers || []; + } + } catch (err) { } + + const activeServers = servers + .filter(srv => srv.status === "ok" && srv.name) + .map(srv => srv.name); + + if (activeServers.length === 0) return null; + + const allUrls = []; + + for (const serverName of activeServers) { + try { + const queryParts = [ + `title=${encodeURIComponent(meta.title)}`, + `type=${isTv ? "series" : "movie"}`, + `year=${meta.year}`, + `imdb=${meta.imdbId}`, + `tmdb=${id}`, + `server=${serverName}` + ]; + if (isTv) { + queryParts.push(`season=${s}`, `episode=${e}`); + } + + const plainUrl = `${API_DOMAIN}/?${queryParts.join("&")}`; + + const encRes = await fetch(`${ENC_DEC_API}/enc-cinejoy?url=${encodeURIComponent(plainUrl)}`, { + headers: { "User-Agent": UA }, + signal: AbortSignal.timeout(15000) + }); + if (!encRes.ok) continue; + const encData = await encRes.json(); + if (encData.status !== 200) continue; + + const rid = encData.result; + + const challengeRes = await fetch(`${API_DOMAIN}/challenge?rid=${rid}`, { + headers: { + "Origin": DOMAIN, + "Referer": DOMAIN + "/", + "User-Agent": UA + }, + signal: AbortSignal.timeout(30000) + }); + if (!challengeRes.ok) continue; + + const challengeData = await challengeRes.json(); + + const solved = solveChallenge(challengeData); + + const streamRes = await fetch(`${API_DOMAIN}/${rid}`, { + headers: { + "Origin": DOMAIN, + "Referer": DOMAIN + "/", + "User-Agent": UA, + "X-At": solved + }, + signal: AbortSignal.timeout(30000) + }); + + if (!streamRes.ok) continue; + + const encryptedText = await streamRes.text(); + + const decRes = await fetch(`${ENC_DEC_API}/dec-cinejoy`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": UA + }, + body: JSON.stringify({ text: encryptedText }), + signal: AbortSignal.timeout(15000) + }); + if (!decRes.ok) continue; + const decData = await decRes.json(); + if (decData.status !== 200) continue; + + let decryptedText = decData.result; + + if (!decryptedText) continue; + + if (typeof decryptedText === "string") { + try { + decryptedText = JSON.parse(decryptedText); + } catch (e) { } + } + + let streamUrl = ""; + if (decryptedText && typeof decryptedText === "object") { + if (decryptedText.stream && decryptedText.stream[0]) { + streamUrl = decryptedText.stream[0].playlist || decryptedText.stream[0].url || ""; + } else { + streamUrl = decryptedText.url || decryptedText.playlist || decryptedText.file || ""; + } + } else if (typeof decryptedText === "string") { + streamUrl = decryptedText.trim(); + } + + if (streamUrl.startsWith("http")) { + allUrls.push({ + url: streamUrl, + headers: { + "User-Agent": UA, + "Referer": DOMAIN + "/", + "Origin": DOMAIN + }, + server: serverName, + quality: "Auto", + type: "hls", + skipVerify: true, + skipProxy: true + }); + } + } catch (err) { } + } + + if (allUrls.length > 0) { + return { ...allUrls[0], allUrls }; + } + } catch (err) { } + return null; +} \ No newline at end of file diff --git a/test/index.js b/test/index.js index b764776..b43a0eb 100644 --- a/test/index.js +++ b/test/index.js @@ -1,54 +1,54 @@ -import { HEALTH_PROBE_ID } from "../src/config.js"; +// usage: npm test +// comment out the tests you don't want to run + import VylaSDK from "../src/sdk.js"; import dotenv from "dotenv"; dotenv.config(); -const BACKROOMS = '1083381'; - const sdk = new VylaSDK({ tmdbApiKey: process.env.TMDB_API_KEY }); -// 'true' is excludeDisabled -const sources = sdk.getSources(true); -const source = sources.find(s => s.key === 'goated'); +try { + const movieSubs = await sdk.getSubtitles("155"); + console.log("Movie Subtitles Count:", movieSubs.length); + console.log("First Subtitle Sample:", movieSubs[0] || "None"); +} catch (err) { + console.error("Movie Subtitles Error:", err.message); +} -const result = await sdk.getStream(source.key, BACKROOMS); -console.log("Result:", result); +try { + const movieDownloads = await sdk.getDownloads("155"); + console.log("Movie Downloads:", movieDownloads); +} catch (err) { + console.error("Movie Downloads Error:", err.message); +} -// const tests = { -// probeAll(log) { -// const sources = sdk.getSources(true); -// let index = 0; +try { + const tvDownloads = await sdk.getDownloads("1399", "1", "1"); + console.log("TV Downloads:", tvDownloads); +} catch (err) { + console.error("TV Downloads Error:", err.message); +} -// Promise.all(sources.map(async (source, idx) => { -// try { -// const result = await sdk.probeSource(source.key); -// index++; -// log(`(${index}/${sources.length}) source #${idx + 1} (${source.label}):`, result); -// } catch (err) { -// log(`Error probing source ${source.key}:`, err); -// } -// })).then(() => { -// log("All sources probed."); -// }); -// }, -// tryHealthProbe(log) { -// const sources = sdk.getSources(true); -// let index = 0; +try { + const movieResult = await sdk.getStream("cinejoy", "155"); + console.log("Movie Stream:", movieResult); +} catch (err) { + console.error("Movie Stream Error:", err.message); +} -// Promise.all(sources.map(async (source, idx) => { -// try { -// const result = await sdk.getStream(source.key, HEALTH_PROBE_ID, null, null, null); -// index++; -// log(`(${index}/${sources.length}) source #${idx + 1} (${source.label}):`, result?.allUrls?.[0]?.url || result?.url || "no sources :("); -// } catch (err) { -// log(`Error probing source ${source.key}:`, err); -// } -// })).then(() => { -// log("All sources probed."); -// }); -// } -// } +try { + const tvResult = await sdk.getStream("cinejoy", "1399", "1", "1"); + console.log("TV Stream:", tvResult); +} catch (err) { + console.error("TV Stream Error:", err.message); +} -// tests.tryHealthProbe(console.log); \ No newline at end of file +try { + const tvSubs = await sdk.getSubtitles("1399", "1", "1"); + console.log("TV Subtitles Count:", tvSubs.length); + console.log("First TV Subtitle Sample:", tvSubs[0] || "None"); +} catch (err) { + console.error("TV Subtitles Error:", err.message); +} \ No newline at end of file