added 4movies, aether, castle, framextv, meowtv, and re-added vidora

This commit is contained in:
2026-10-31 23:40:45 -07:00
parent 8e56a0684b
commit 9fb01f8b49
8 changed files with 1118 additions and 20 deletions

View File

@@ -18,6 +18,25 @@ export const SOURCES = [
}],
},
{
key: '4movies',
label: '4Movies',
sourceFile: '4movies',
proxyParam: '4m',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /4movie\.in|aapanel\.devcorp\.me/i,
headers: {
Referer: 'https://4movie.in/',
Origin: 'https://4movie.in',
},
}],
},
{
key: '123anime',
label: '123Anime',
@@ -56,6 +75,37 @@ export const SOURCES = [
}],
},
{
key: 'aether',
label: 'Aether',
sourceFile: 'aether',
proxyParam: 'aet',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /raccoonunbutteredbagel\.zip|1x2\.space|kaxate\.fun|mechanicalserenity\.xyz|reelvault\.click|visionaryfounderslab\.site|goldenkite\.top|aether\.ist/i,
headers: {
Referer: 'https://aether.st/',
Origin: 'https://aether.st',
},
}],
},
{
key: 'castle',
label: 'Castle',
sourceFile: 'castle',
proxyParam: 'cst',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
},
{
key: 'cinemaarmy',
label: 'CinemaArmy',
@@ -151,6 +201,25 @@ export const SOURCES = [
}],
},
{
key: 'framextv',
label: 'FrameXTV',
sourceFile: 'framextv',
proxyParam: 'fxt',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /framextv\.cc|streamos\.top|workers\.dev|films365\.org/i,
headers: {
Referer: 'https://framextv.cc/',
Origin: 'https://framextv.cc',
},
}],
},
{
key: 'frost',
label: 'FrostStream',
@@ -278,6 +347,25 @@ export const SOURCES = [
},
},
{
key: 'meowtv',
label: 'MeowTV',
sourceFile: 'meowtv',
proxyParam: 'mtv',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /meowtv\.ru/i,
headers: {
Referer: 'https://meowtv.ru/',
Origin: 'https://meowtv.ru',
},
}],
},
{
key: 'movenerds',
label: 'MoverNerds',
@@ -481,6 +569,25 @@ export const SOURCES = [
multiUrl: true,
},
{
key: 'vidora',
label: 'Vidora',
sourceFile: 'vidora',
proxyParam: 'vd',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /vidoraplayer\.duckdns\.org/i,
headers: {
Referer: 'https://vidoraplayer.duckdns.org/',
Origin: 'https://vidoraplayer.duckdns.org',
},
}],
},
{
key: 'vidup',
label: 'VidUp',

View File

@@ -1,6 +1,15 @@
import { HEALTH_PROBE_ID } from "./config.js";
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
const MOVIE_PROBE_ID = "936075";
const TV_PROBE_ID = "37854";
const TV_PROBE_SEASON = "1";
const TV_PROBE_EPISODE = "1";
const withTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise(resolve => setTimeout(() => resolve(null), ms))
]);
async function withRetry(fn, attempts = 2, delay = 300) {
for (let i = 0; i < attempts; i++) {
try {
@@ -8,31 +17,62 @@ async function withRetry(fn, attempts = 2, delay = 300) {
if (result != null) return result;
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
await new Promise(resolve =>
setTimeout(resolve, delay + Math.random() * delay * 0.5)
);
}
}
return null;
}
function hasStream(result) {
const urls = [
result?.url,
...(result?.allUrls || []).map(item => item?.url)
];
return urls.some(url =>
typeof url === "string" && url.startsWith("http")
);
}
export async function probeSource(sdk, cfg, mod) {
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
const streamArgs = {
id: HEALTH_PROBE_ID,
s: null,
e: null,
clientIP: null,
absoluteBase: null,
audio,
sdk,
config: cfg,
const audio = /dub$/.test(cfg.key) ? "dub" : "sub";
const start = Date.now();
const test = async (id, s = null, e = null) => {
const streamArgs = {
id,
s,
e,
clientIP: null,
absoluteBase: null,
audio,
sdk,
config: cfg,
};
const result = await withTimeout(
withRetry(
() => mod.getStream(streamArgs),
cfg.retries,
300
),
cfg.timeout
);
return hasStream(result);
};
const t = Date.now();
const result = await withTimeout(
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
cfg.timeout
);
const [movie, tv] = await Promise.all([
test(MOVIE_PROBE_ID),
test(TV_PROBE_ID, TV_PROBE_SEASON, TV_PROBE_EPISODE)
]);
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
return {
ok: movie || tv,
movie,
tv,
ms: Date.now() - start
};
}

77
src/sources/4movies.js Normal file
View File

@@ -0,0 +1,77 @@
const BASE_URL = 'https://4movie.in';
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';
const HEADERS = {
'User-Agent': UA,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
const PLAYBACK_HEADERS = {
'User-Agent': UA,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
};
export async function getStream(args) {
const { id, s, e } = args || {};
const isTv = s != null && e != null;
let query = isTv
? `tmdb=${id}&type=tv&season=${s}&episode=${e}`
: `tmdb=${id}&type=movie`;
if (args?.title) query += `&title=${encodeURIComponent(args.title)}`;
if (args?.year) query += `&year=${encodeURIComponent(args.year)}`;
const targetUrl = `${BASE_URL}/api/hubcloud?${query}`;
const res = await fetch(targetUrl, { headers: HEADERS }).catch(() => null);
if (!res || !res.ok) return null;
const data = await res.json().catch(() => null);
if (!data?.ok || !Array.isArray(data.streams) || !data.streams.length) return null;
const allUrls = [];
const seen = new Set();
for (const stream of data.streams) {
if (stream.rawUrl && !seen.has(stream.rawUrl)) {
seen.add(stream.rawUrl);
allUrls.push({
url: stream.rawUrl,
server: `4Movies - ${stream.name || 'Direct'}`,
quality: stream.quality || 'Auto',
type: stream.type || (stream.rawUrl.includes('.m3u8') ? 'hls' : 'mp4'),
headers: PLAYBACK_HEADERS,
});
}
if (stream.url) {
const proxyUrl = stream.url.startsWith('http')
? stream.url
: `${BASE_URL}${stream.url.startsWith('/') ? '' : '/'}${stream.url}`;
if (!seen.has(proxyUrl)) {
seen.add(proxyUrl);
allUrls.push({
url: proxyUrl,
server: `4Movies - ${stream.name ? `${stream.name} (Proxy)` : 'Proxy'}`,
quality: stream.quality || 'Auto',
type: stream.type || 'hls',
headers: PLAYBACK_HEADERS,
});
}
}
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}

99
src/sources/aether.js Normal file
View File

@@ -0,0 +1,99 @@
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';
const HEADERS = {
'User-Agent': UA,
Origin: 'https://aether.st',
Referer: 'https://aether.st/',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
const PLAYBACK_HEADERS = {
'User-Agent': UA,
Referer: 'https://aether.st/',
Origin: 'https://aether.st',
};
function extractStreamUrls(data, target) {
const urls = [];
if (!data) return urls;
if (Array.isArray(data.streams)) {
for (const s of data.streams) {
if (s?.url) {
urls.push({
url: s.url,
name: `${target.name} (${s.name || s.language || 'Default'})`,
quality: s.quality || 'Auto',
type: s.type || (s.url.includes('.m3u8') ? 'hls' : 'mp4'),
});
}
}
}
const singleUrl = data.directUrl || data.url || data.stream;
if (typeof singleUrl === 'string' && /^https?:\/\//.test(singleUrl)) {
urls.push({
url: singleUrl,
name: `${target.name}${data.server ? ` - ${data.server}` : ''}`,
quality: data.quality || 'Auto',
type: data.streamType || data.container || (singleUrl.includes('.m3u8') ? 'hls' : 'mp4'),
});
}
return urls;
}
export async function getStream(args) {
const { id, s, e } = args || {};
const isTv = s != null && e != null;
const endpoints = [
{ name: 'Meridian', url: isTv ? `https://meridian.raccoonunbutteredbagel.zip/show/${id}/${s}/${e}` : `https://meridian.raccoonunbutteredbagel.zip/movie/${id}` },
{ name: 'Rock', url: isTv ? `https://rock.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://rock.raccoonunbutteredbagel.zip/movie/${id}` },
{ name: 'Shore', url: isTv ? `https://shore.raccoonunbutteredbagel.zip/show/${id}?s=${s}&e=${e}` : `https://shore.raccoonunbutteredbagel.zip/movie/${id}` },
{ name: 'Filler', url: isTv ? `https://filler.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://filler.raccoonunbutteredbagel.zip/movie/${id}` },
{ name: 'Tiki', url: isTv ? `https://tiki.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://tiki.raccoonunbutteredbagel.zip/movie/${id}` },
{ name: 'Link', url: isTv ? `https://link.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://link.raccoonunbutteredbagel.zip/movie/${id}` },
{ name: 'VFast Rapid', url: isTv ? `https://vfast.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}?server=vRapid` : `https://vfast.raccoonunbutteredbagel.zip/movie/${id}?server=vRapid` },
{ name: 'VFast Blaze', url: isTv ? `https://vfast.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}?server=vBlaze` : `https://vfast.raccoonunbutteredbagel.zip/movie/${id}?server=vBlaze` },
];
const responses = await Promise.allSettled(
endpoints.map(async target => {
const res = await fetch(target.url, { headers: HEADERS });
if (!res.ok) return null;
const data = await res.json();
return { target, data };
})
);
const allUrls = [];
const seen = new Set();
for (const res of responses) {
if (res.status !== 'fulfilled' || !res.value?.data) continue;
const { target, data } = res.value;
const found = extractStreamUrls(data, target);
for (const item of found) {
if (!item?.url || seen.has(item.url)) continue;
seen.add(item.url);
allUrls.push({
url: item.url,
server: `Aether - ${item.name}`,
quality: item.quality || 'Auto',
type: item.type || (item.url.includes('.m3u8') ? 'hls' : 'mp4'),
headers: PLAYBACK_HEADERS,
});
}
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}

415
src/sources/castle.js Normal file
View File

@@ -0,0 +1,415 @@
import crypto from 'node:crypto';
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
const CASTLE_BASE = 'https://api.hlowb.com';
const PKG = 'com.external.castle';
const CHANNEL = 'IndiaA';
const CLIENT = '1';
const LANG = 'en-US';
const SUFFIX = 'T!BgJB';
const APK_SIGN_KEY = 'ED0955EB04E67A1D9F3305B95454FED485261475';
const WORKING_HEADERS = {
'User-Agent': 'okhttp/4.9.3',
Accept: 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
Connection: 'Keep-Alive',
Referer: CASTLE_BASE,
};
export const CASTLE_PLAYBACK_HEADERS = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
Accept:
'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'identity',
Connection: 'keep-alive',
'Sec-Fetch-Dest': 'video',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
DNT: '1',
};
const QUALITY_MAP = { 1: '480p', 2: '720p', 3: '1080p' };
function safeJsonParse(text) {
const safe = text.replace(/:(\s*)(-?\d{16,})(\s*[,}\]])/g, ':$1"$2"$3');
return JSON.parse(safe);
}
class CastleScraper {
constructor(apiKey) {
this.tmdbKey =
apiKey ||
process.env.CASTLE_TMDB_API_KEY ||
process.env.TMDB_API_KEY ||
'439c478a771f35c05022f9feabcca01c';
this.securityKey = null;
}
deriveKey(apiKeyB64) {
const apiKeyBytes = Buffer.from(apiKeyB64, 'base64');
let keyMaterial = Buffer.concat([apiKeyBytes, Buffer.from(SUFFIX, 'ascii')]);
if (keyMaterial.length < 16) {
keyMaterial = Buffer.concat([
keyMaterial,
Buffer.alloc(16 - keyMaterial.length),
]);
} else if (keyMaterial.length > 16) {
keyMaterial = keyMaterial.subarray(0, 16);
}
return keyMaterial;
}
decryptCastle(encryptedB64, securityKeyB64) {
const aesKey = this.deriveKey(securityKeyB64);
const iv = aesKey;
const encrypted = Buffer.from(encryptedB64, 'base64');
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
decipher.setAutoPadding(true);
return Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]).toString('utf-8');
}
async request(url, init) {
const r = await fetch(url, {
...init,
headers: { ...WORKING_HEADERS, ...(init?.headers || {}) },
});
if (!r.ok) throw new Error(`Castle request failed ${r.status} for ${url}`);
return r;
}
async extractCipher(response) {
const text = (await response.text()).trim();
if (!text) throw new Error('Empty response');
try {
const j = JSON.parse(text);
if (j && typeof j.data === 'string') return j.data.trim();
} catch { }
return text;
}
async decryptedJson(response, securityKey) {
const cipher = await this.extractCipher(response);
return safeJsonParse(this.decryptCastle(cipher, securityKey));
}
async getSecurityKey() {
if (this.securityKey) return this.securityKey;
const url = `${CASTLE_BASE}/v0.1/system/getSecurityKey/1?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}`;
const r = await this.request(url);
const data = await r.json();
if (data.code !== 200 || !data.data) {
throw new Error(`Security key API error: ${JSON.stringify(data)}`);
}
this.securityKey = data.data;
return this.securityKey;
}
async searchCastle(securityKey, keyword, page = 1, size = 30) {
const params = new URLSearchParams({
channel: CHANNEL,
clientType: CLIENT,
keyword,
lang: LANG,
mode: '1',
packageName: PKG,
page: String(page),
size: String(size),
});
const url = `${CASTLE_BASE}/film-api/v1.1.0/movie/searchByKeyword?${params}`;
return this.decryptedJson(await this.request(url), securityKey);
}
async getDetails(securityKey, movieId) {
const url = `${CASTLE_BASE}/film-api/v1.1/movie?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}&movieId=${movieId}&packageName=${PKG}`;
return this.decryptedJson(await this.request(url), securityKey);
}
async getVideo2(securityKey, movieId, episodeId, resolution = 2) {
const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
const body = {
mode: '1',
appMarket: 'GuanWang',
clientType: '1',
woolUser: 'false',
apkSignKey: APK_SIGN_KEY,
androidVersion: '13',
movieId: String(movieId),
episodeId: String(episodeId),
isNewUser: 'true',
resolution: String(resolution),
packageName: PKG,
};
const r = await this.request(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return this.decryptedJson(r, securityKey);
}
async getVideoV1(securityKey, movieId, episodeId, languageId, resolution = 2) {
const params = new URLSearchParams({
apkSignKey: APK_SIGN_KEY,
channel: CHANNEL,
clientType: CLIENT,
episodeId: String(episodeId),
lang: LANG,
languageId: String(languageId),
mode: '1',
movieId: String(movieId),
packageName: PKG,
resolution: String(resolution),
});
const url = `${CASTLE_BASE}/film-api/v1.9.1/movie/getVideo?${params}`;
return this.decryptedJson(await this.request(url), securityKey);
}
extractDataBlock(obj) {
if (obj && typeof obj.data === 'object' && obj.data) return obj.data;
return obj && typeof obj === 'object' ? obj : {};
}
qualityValue(quality) {
if (!quality) return 0;
let q = String(quality).toLowerCase();
q = q.replace(/^(sd|hd|fhd|uhd|4k)\s*/, '').replace(/p$/, '').trim();
if (q === '4k' || q === '2160') return 2160;
const known = [1440, 1080, 720, 480, 360, 240];
const n = parseInt(q, 10);
if (known.includes(n)) return n;
return n > 0 ? n : 0;
}
formatSize(size) {
if (typeof size === 'number' && size > 0) {
return size > 1_000_000_000
? `${(size / 1_000_000_000).toFixed(2)} GB`
: `${Math.round(size / 1_000_000)} MB`;
}
return 'Unknown';
}
async getTmdbDetails(tmdbId, mediaType) {
const endpoint = mediaType === 'tv' ? 'tv' : 'movie';
const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${this.tmdbKey}&append_to_response=external_ids`;
const r = await this.request(url);
const data = await r.json();
const title = mediaType === 'tv' ? data.name : data.title;
const releaseDate =
mediaType === 'tv' ? data.first_air_date : data.release_date;
const year = releaseDate ? parseInt(String(releaseDate).split('-')[0], 10) : null;
return { title, year, tmdbId };
}
findMovieId(searchResult, tmdbInfo) {
const data = this.extractDataBlock(searchResult);
const rows = data.rows || [];
if (!rows.length) throw new Error('No search results found');
const searchTitle = (tmdbInfo.title || '').toLowerCase();
for (const item of rows) {
const itemTitle = String(item.title || item.name || '').toLowerCase();
if (
itemTitle &&
(itemTitle.includes(searchTitle) || searchTitle.includes(itemTitle))
) {
const id = item.id || item.redirectId || item.redirectIdStr;
if (id) return String(id);
}
}
const first = rows[0];
const id = first.id || first.redirectId || first.redirectIdStr;
if (id) return String(id);
throw new Error('Could not extract movie ID from search results');
}
processVideoResponse(videoData, media, season, episode, resolution, languageInfo) {
const streams = [];
const data = this.extractDataBlock(videoData);
const videoUrl = data.videoUrl;
if (!videoUrl) return streams;
let mediaTitle = media.title || 'Unknown';
if (media.year) mediaTitle += ` (${media.year})`;
if (season && episode) {
mediaTitle = `${media.title} S${String(season).padStart(2, '0')}E${String(
episode
).padStart(2, '0')}`;
}
const quality = QUALITY_MAP[resolution] || `${resolution}p`;
if (Array.isArray(data.videos)) {
for (const video of data.videos) {
let q = String(
video.resolutionDescription || video.resolution || quality
).replace(/^(SD|HD|FHD)\s+/i, '');
const name = languageInfo
? `Castle ${languageInfo} - ${q}`
: `Castle - ${q}`;
streams.push({
name,
title: mediaTitle,
url: video.url || videoUrl,
quality: q,
size: this.formatSize(video.size),
provider: 'castle',
});
}
} else {
const name = languageInfo
? `Castle ${languageInfo} - ${quality}`
: `Castle - ${quality}`;
streams.push({
name,
title: mediaTitle,
url: videoUrl,
quality,
size: this.formatSize(data.size),
provider: 'castle',
});
}
return streams;
}
async fetchSources(tmdbId, season = 0, episode = 0) {
const mediaType = season && episode ? 'tv' : 'movie';
try {
const tmdbInfo = await this.getTmdbDetails(tmdbId, mediaType);
const securityKey = await this.getSecurityKey();
const searchTerm = tmdbInfo.year
? `${tmdbInfo.title} ${tmdbInfo.year}`
: tmdbInfo.title;
const searchResult = await this.searchCastle(securityKey, searchTerm);
let movieId = this.findMovieId(searchResult, tmdbInfo);
let details = await this.getDetails(securityKey, movieId);
if (mediaType === 'tv') {
const data = this.extractDataBlock(details);
const seasons = data.seasons || [];
const s = seasons.find((x) => x.number === season);
if (s && s.movieId && String(s.movieId) !== movieId) {
movieId = String(s.movieId);
details = await this.getDetails(securityKey, movieId);
}
}
const data = this.extractDataBlock(details);
const episodes = data.episodes || [];
let episodeId = null;
if (mediaType === 'tv') {
const ep = episodes.find((e) => e.number === episode);
if (ep && ep.id) episodeId = String(ep.id);
} else if (episodes.length) {
episodeId = String(episodes[0].id);
}
if (!episodeId) throw new Error('Could not find episode ID');
const epObj = episodes.find((e) => String(e.id) === episodeId);
const tracks = epObj?.tracks || [];
const resolution = 2;
let allStreams = [];
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
const langName =
track.languageName || track.abbreviate || `Lang${i + 1}`;
if (track.existIndividualVideo && track.languageId) {
try {
const videoData = await this.getVideoV1(
securityKey,
movieId,
episodeId,
String(track.languageId),
resolution
);
const langStreams = this.processVideoResponse(
videoData,
tmdbInfo,
season,
episode,
resolution,
`[${langName}]`
);
allStreams.push(...langStreams);
} catch (e) {
console.error(`[Castle] ${langName}: v1 failed - ${e?.message || e}`);
}
}
}
if (!allStreams.length) {
const videoData = await this.getVideo2(
securityKey,
movieId,
episodeId,
resolution
);
allStreams = this.processVideoResponse(
videoData,
tmdbInfo,
season,
episode,
resolution,
'[Shared]'
);
}
allStreams.sort(
(a, b) => this.qualityValue(b.quality) - this.qualityValue(a.quality)
);
return allStreams;
} catch (e) {
console.error(`[Castle] Error: ${e?.message || e}`);
return [];
}
}
}
export async function getStream(args) {
const { id, s, e, sdk } = args || {};
const scraper = new CastleScraper(sdk?.tmdbApiKey);
const tmdbId = parseInt(String(id), 10);
const season = s != null ? parseInt(String(s), 10) : 0;
const episode = e != null ? parseInt(String(e), 10) : 0;
const streams = await scraper.fetchSources(tmdbId, season, episode).catch(() => []);
if (!streams || !streams.length) return null;
const seen = new Set();
const allUrls = [];
for (const item of streams) {
if (!item?.url || seen.has(item.url)) continue;
seen.add(item.url);
allUrls.push({
url: item.url,
server: item.name || 'Castle',
quality: item.quality || 'Auto',
type: item.url.includes('.m3u8')
? 'hls'
: item.url.includes('.mpd')
? 'dash'
: 'mp4',
headers: CASTLE_PLAYBACK_HEADERS,
});
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}

126
src/sources/framextv.js Normal file
View File

@@ -0,0 +1,126 @@
const FRAMEX_BASE = 'https://api.framextv.cc';
const STREAMOS_BASE = 'https://api.streamos.top';
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';
const FRAMEX_HEADERS = {
'User-Agent': UA,
Referer: 'https://framextv.cc/',
Origin: 'https://framextv.cc',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
const STREAMOS_HEADERS = {
'User-Agent': UA,
Referer: 'https://streamos.top/',
Origin: 'https://streamos.top',
};
const STREAMOS_SERVERS = [
'auto',
'miami',
'boise',
'vegas',
'reno',
'oakland',
'seattle',
'phoenix',
'atlanta',
'portland',
'austin',
'dallas',
'tampa',
'orlando',
'munich',
'berlin',
'france',
'india',
'mexico',
'delhi',
];
export async function getStream(args) {
const { id, s, e } = args || {};
const isTv = s != null && e != null;
const framexQuery = isTv
? `type=tv&id=${id}&season=${s}&episode=${e}&codecs=hevc`
: `type=movie&id=${id}&codecs=hevc`;
const framexUrls = [
`${FRAMEX_BASE}/api/stream?${framexQuery}`,
`${FRAMEX_BASE}/api/stream?${framexQuery}&provider=sparky`,
`${FRAMEX_BASE}/api/stream?${framexQuery}&provider=titan`,
];
const streamOsBaseQuery = isTv
? `id=${id}&season=${s}&episode=${e}&kind=tv`
: `id=${id}&kind=movie`;
const allUrls = [];
const seen = new Set();
const framexPromises = Promise.all(
framexUrls.map(url =>
fetch(url, { headers: FRAMEX_HEADERS })
.then(r => (r.ok ? r.json() : null))
.catch(() => null)
)
);
const streamOsChecks = Promise.all(
STREAMOS_SERVERS.slice(0, 8).map(async server => {
const serverParam = server === 'auto' ? '' : `&server=${server}`;
const url = `${STREAMOS_BASE}/api/relay/hls?${streamOsBaseQuery}${serverParam}`;
try {
const res = await fetch(url, { method: 'HEAD', headers: STREAMOS_HEADERS });
if (res.ok) {
return {
url,
server: `StreamOS - ${server.toUpperCase()}`,
quality: '1080p',
type: 'hls',
headers: STREAMOS_HEADERS,
};
}
} catch { }
return null;
})
);
const [framexResults, streamOsResults] = await Promise.all([framexPromises, streamOsChecks]);
for (const data of framexResults) {
if (!data?.success || !Array.isArray(data.sources)) continue;
for (const src of data.sources) {
if (!src?.url || seen.has(src.url)) continue;
seen.add(src.url);
allUrls.push({
url: src.url,
server: `FrameX - ${src.server || data.provider || 'Direct'}`,
quality: src.quality || 'Auto',
type: src.type || (src.url.includes('.m3u8') ? 'hls' : 'mp4'),
headers: {
'User-Agent': UA,
Referer: 'https://framextv.cc/',
Origin: 'https://framextv.cc',
},
});
}
}
for (const stream of streamOsResults) {
if (stream && !seen.has(stream.url)) {
seen.add(stream.url);
allUrls.push(stream);
}
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}

140
src/sources/meowtv.js Normal file
View File

@@ -0,0 +1,140 @@
const BASE_URL = 'https://api.meowtv.ru';
const DEC_API = 'https://enc-dec.app/api';
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';
const HEADERS = {
'User-Agent': UA,
Origin: 'https://meowtv.ru',
Referer: 'https://meowtv.ru/',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
const SERVERS = [
{ id: 'acme', label: 'Acme' },
{ id: 'dcloud', label: 'Dcloud' },
{ id: 'tik', label: 'TCloud' },
{ id: 'ipcloud', label: 'IPcloud' },
{ id: 'turkce', label: 'Türkçe', movieOnly: true },
{ id: 'hindi', label: 'Hindi' },
{ id: 'english', label: 'English' },
];
async function decryptMeow(data) {
try {
const res = await fetch(`${DEC_API}/dec-meowtv`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': UA,
},
body: JSON.stringify({ data }),
});
if (!res.ok) return null;
const json = await res.json();
return json?.status === 200 ? json.result : null;
} catch {
return null;
}
}
function extractUrls(result) {
const urls = [];
if (!result) return urls;
if (typeof result === 'string' && /^https?:\/\//.test(result)) {
urls.push({ url: result, quality: 'Auto', type: result.includes('.m3u8') ? 'hls' : 'mp4' });
} else if (result?.url && typeof result.url === 'string') {
urls.push({
url: result.url,
quality: result.quality || 'Auto',
type: result.type || (result.url.includes('.m3u8') ? 'hls' : 'mp4'),
});
} else if (Array.isArray(result?.sources)) {
for (const s of result.sources) {
if (s?.url) {
urls.push({
url: s.url,
quality: s.quality || 'Auto',
type: s.type || (s.url.includes('.m3u8') ? 'hls' : 'mp4'),
});
}
}
} else if (result?.stream) {
const sUrl = typeof result.stream === 'string' ? result.stream : result.stream?.url;
if (sUrl) {
urls.push({
url: sUrl,
quality: result.stream?.quality || 'Auto',
type: sUrl.includes('.m3u8') ? 'hls' : 'mp4',
});
}
}
return urls;
}
export async function getStream(args) {
const { id, s, e } = args || {};
const isTv = s != null && e != null;
const ticketRes = await fetch(`${BASE_URL}/streams/ticket`, {
method: 'POST',
headers: HEADERS,
}).then(r => (r.ok ? r.json() : null)).catch(() => null);
const ticket = ticketRes?.ticket;
if (!ticket) return null;
const requestHeaders = {
...HEADERS,
'X-Stream-Ticket': ticket,
};
const targetServers = isTv ? SERVERS.filter(srv => !srv.movieOnly) : SERVERS;
const responses = await Promise.allSettled(
targetServers.map(async server => {
const url = isTv
? `${BASE_URL}/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(server.id)}`
: `${BASE_URL}/streams/movie/${id}?s=${encodeURIComponent(server.id)}`;
const res = await fetch(url, { headers: requestHeaders });
if (!res.ok) return null;
const data = await res.json();
const decrypted = await decryptMeow(data);
return { server, decrypted };
})
);
const allUrls = [];
const seen = new Set();
for (const res of responses) {
if (res.status !== 'fulfilled' || !res.value?.decrypted) continue;
const { server, decrypted } = res.value;
const found = extractUrls(decrypted);
for (const item of found) {
if (!item?.url || seen.has(item.url)) continue;
seen.add(item.url);
allUrls.push({
url: item.url,
server: `MeowTV - ${server.label}`,
quality: item.quality || 'Auto',
type: item.type || (item.url.includes('.m3u8') ? 'hls' : 'mp4'),
headers: {
Referer: 'https://meowtv.ru/',
Origin: 'https://meowtv.ru',
'User-Agent': UA,
},
});
}
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}

94
src/sources/vidora.js Normal file
View File

@@ -0,0 +1,94 @@
const BASE_URL = 'https://vidoraplayer.duckdns.org';
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';
const HEADERS = {
'User-Agent': UA,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
Accept: 'application/x-ndjson, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
function parseNdjson(text) {
if (!text) return [];
const normalized = text.replace(/\}\s*\{/g, '}\n{');
const lines = normalized.split('\n');
const items = [];
for (const raw of lines) {
const line = raw.trim();
if (!line) continue;
try {
items.push(JSON.parse(line));
} catch { }
}
return items;
}
export async function getStream(args) {
const { id, s, e } = args || {};
const isTv = s != null && e != null;
const query = isTv
? `type=tv&id=${id}&season=${s}&episode=${e}`
: `type=movie&id=${id}`;
const urls = [
`${BASE_URL}/api/sources?${query}`,
`${BASE_URL}/api/sources?${query}&gen=2`,
];
const responses = await Promise.all(
urls.map(url =>
fetch(url, { headers: HEADERS })
.then(r => (r.ok ? r.text() : ''))
.catch(() => '')
)
);
const allUrls = [];
const seen = new Set();
for (const text of responses) {
const events = parseNdjson(text);
for (const item of events) {
const list = [];
if (item?.event === 'source' && item?.source) {
list.push(item.source);
} else if (item?.event === 'done' && Array.isArray(item?.sources)) {
list.push(...item.sources);
} else if (item?.url) {
list.push(item);
}
for (const src of list) {
if (!src?.url) continue;
const fullUrl = src.url.startsWith('http')
? src.url
: `${BASE_URL}${src.url.startsWith('/') ? '' : '/'}${src.url}`;
if (!seen.has(fullUrl)) {
seen.add(fullUrl);
allUrls.push({
url: fullUrl,
server: `Vidora - ${src.label || src.server || src.providerGroup || 'Default'}`,
quality: src.quality || 'Auto',
type: src.type || (fullUrl.includes('.m3u8') ? 'hls' : 'mp4'),
headers: {
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
'User-Agent': UA,
},
});
}
}
}
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}