diff --git a/src/config.js b/src/config.js
index 0d132fa..547b70a 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,4 +1,4 @@
-// Currently active sources reported on 9:22:41 PM PST 7/30/2026
+// Currently active sources reported on 6:18:22 PM PST 9/13/2026
// None Anime Tested: 936075
// Anime Tested: 37854
@@ -6,6 +6,18 @@
// Fshare/Fsonic Tested: 155 ( they don't have Michael )
export const SOURCES = [
+ {
+ key: '4khdhub',
+ label: '4KHDHub',
+ sourceFile: '4khdhub',
+ proxyParam: 'khd',
+ timeout: 35000,
+ jitter: 500,
+ retries: 1,
+ multiUrl: true,
+ skipProxy: true,
+ },
+
{
key: '123anime',
label: '123Anime',
@@ -25,6 +37,25 @@ export const SOURCES = [
},],
},
+ {
+ key: '111477',
+ label: '111477',
+ sourceFile: '111477',
+ proxyParam: 'a11',
+ timeout: 25000,
+ jitter: 500,
+ retries: 2,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /111477\.xyz/i,
+ headers: {
+ Referer: 'https://st.111477.xyz/',
+ Origin: 'https://st.111477.xyz',
+ },
+ }],
+ },
+
{
key: 'anihq-sub',
label: 'AniHQ (Sub)',
@@ -69,40 +100,6 @@ export const SOURCES = [
retries: 2
},
- {
- key: 'anipm-sub',
- sourceFile: 'anipm',
- label: 'AniPM (Sub)',
- proxyParam: 'apsub',
- timeout: 25000,
- jitter: 500,
- retries: 2,
- skipProxy: true,
- skipVerify: true,
- multiUrl: true,
- verifyHeaders: {
- Referer: 'https://ani.pm/',
- Origin: 'https://ani.pm',
- },
- },
-
- {
- key: 'anipm-dub',
- sourceFile: 'anipm',
- label: 'AniPM (Dub)',
- proxyParam: 'apdub',
- timeout: 25000,
- jitter: 500,
- retries: 2,
- skipProxy: true,
- skipVerify: true,
- multiUrl: true,
- verifyHeaders: {
- Referer: 'https://ani.pm/',
- Origin: 'https://ani.pm',
- },
- },
-
{
key: 'bcine',
label: 'Bcine',
@@ -173,6 +170,25 @@ export const SOURCES = [
}],
},
+ {
+ key: 'downloadeverything',
+ label: 'DownloadEverything',
+ sourceFile: 'downloadeverything',
+ proxyParam: 'de',
+ timeout: 35000,
+ jitter: 500,
+ retries: 1,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /cloudflarestorage\.com|pixeldrain\.com|hakunaymatata\.com/i,
+ headers: {
+ Referer: 'https://downloadeverythingfromeverywhere.com/',
+ Origin: 'https://downloadeverythingfromeverywhere.com',
+ },
+ }],
+ },
+
{
key: 'flaxmovies',
label: 'FlaxMovies',
@@ -314,16 +330,6 @@ export const SOURCES = [
skipProxy: true,
},
- {
- key: 'mapple',
- label: 'Mapple',
- sourceFile: 'mapple',
- proxyParam: 'mp',
- timeout: 15000,
- retries: 1,
- jitter: 0
- },
-
{
key: 'meowtv',
label: 'MeowTV',
@@ -336,6 +342,44 @@ export const SOURCES = [
skipProxy: true
},
+ {
+ key: 'movienight',
+ label: 'MovieNight',
+ sourceFile: 'movienight',
+ proxyParam: 'mn',
+ timeout: 20000,
+ jitter: 500,
+ retries: 2,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /movienig\.ht/i,
+ headers: {
+ Referer: 'https://movienig.ht/',
+ Origin: 'https://movienig.ht',
+ },
+ }],
+ },
+
+ {
+ key: 'movy',
+ label: 'Movy',
+ sourceFile: 'movy',
+ proxyParam: 'mv',
+ timeout: 25000,
+ jitter: 500,
+ retries: 2,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /wecollege\.net|movy\.bz/i,
+ headers: {
+ Referer: 'https://www.movy.bz/',
+ Origin: 'https://www.movy.bz',
+ },
+ }],
+ },
+
{
key: 'peestream',
label: 'PeeStream',
@@ -372,28 +416,6 @@ export const SOURCES = [
multiUrl: true,
},
- {
- key: 'nova',
- label: 'Nova',
- sourceFile: 'nova',
- proxyParam: 'nv',
- timeout: 25000,
- jitter: 500,
- retries: 1,
- multiUrl: true,
- },
-
- {
- key: 'megasource',
- label: 'MegaSource',
- sourceFile: 'megasource',
- proxyParam: 'mg',
- timeout: 25000,
- jitter: 500,
- retries: 1,
- multiUrl: true,
- },
-
{
key: 'purstream',
sourceFile: 'purstream',
@@ -416,6 +438,44 @@ export const SOURCES = [
multiUrl: true,
},
+ {
+ key: 'streamaggregator',
+ label: 'StreamAggregator',
+ sourceFile: 'streamaggregator',
+ proxyParam: 'sa',
+ timeout: 25000,
+ jitter: 500,
+ retries: 2,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /streamaggregator\.in/i,
+ headers: {
+ Referer: 'https://streamaggregator.in/',
+ Origin: 'https://streamaggregator.in',
+ },
+ }],
+ },
+
+ {
+ key: 'vadapav',
+ label: 'Vadapav',
+ sourceFile: 'vadapav',
+ proxyParam: 'vp',
+ timeout: 25000,
+ jitter: 500,
+ retries: 2,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /vadapav\.mov/i,
+ headers: {
+ Referer: 'https://stremio.vadapav.mov/',
+ Origin: 'https://stremio.vadapav.mov',
+ },
+ }],
+ },
+
{
key: 'vidapi',
label: 'VidAPI',
@@ -438,23 +498,6 @@ export const SOURCES = [
sourcesTimeout: 10000,
},
- {
- key: 'videasy',
- label: 'Videasy',
- sourceFile: 'videasy',
- proxyParam: 'vy',
- timeout: 40000,
- sourcesTimeout: 10000,
- jitter: 900,
- retries: 3,
- multiUrl: true,
- verifyHeaders: {
- Accept: 'application/json, /; q=0.01',
- Referer: 'https://player.videasy.net/',
- Origin: 'https://player.videasy.net',
- },
- },
-
{
key: 'vidfast',
label: 'Vidfast',
@@ -487,7 +530,6 @@ export const SOURCES = [
retries: 2,
skipProxy: true,
multiUrl: true,
- disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
},
{
@@ -603,25 +645,41 @@ export const SOURCES = [
},
{
- key: 'xpass',
- sourceFile: 'xpass',
- label: 'XPass',
- proxyParam: 'xp',
- timeout: 20000,
- jitter: 500,
- retries: 2,
- skipProxy: true,
- },
-
- {
- key: 'zxcstream',
- label: 'ZxcStream',
- sourceFile: 'zxcstream',
- proxyParam: 'zxc',
+ key: 'vuflix',
+ label: 'Vuflix',
+ sourceFile: 'vuflix',
+ proxyParam: 'vfx',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /vuflix\.co/i,
+ headers: {
+ Referer: 'https://vuflix.co/',
+ Origin: 'https://vuflix.co',
+ },
+ }],
+ },
+
+ {
+ key: 'xdownloader',
+ label: 'XDownloader',
+ sourceFile: 'xdownloader',
+ proxyParam: 'xd',
+ timeout: 25000,
+ jitter: 500,
+ retries: 2,
+ multiUrl: true,
+ skipProxy: true,
+ cdnHeaders: [{
+ pattern: /films365\.org/i,
+ headers: {
+ Referer: 'https://www.films365.org/',
+ Origin: 'https://www.films365.org',
+ },
+ }],
},
];
diff --git a/src/downloads/downloads.js b/src/downloads/downloads.js
index 709994a..587ff74 100644
--- a/src/downloads/downloads.js
+++ b/src/downloads/downloads.js
@@ -1,12 +1,53 @@
-import { getDownloads, getDownloadsTv } from "./sources/trendimovies.js";
+import SOURCES from "../config.js";
-export async function fetchDownloads(id, s = null, e = null) {
- try {
- const res = s != null && e != null
- ? await getDownloadsTv(id, s, e)
- : await getDownloads(id);
- return res || [];
- } catch {
- return [];
+export async function fetchDownloads(sdkOrId, idOrS = null, sOrE = null, maybeE = null) {
+ let sdk = null;
+ let id = sdkOrId;
+ let s = idOrS;
+ let e = sOrE;
+
+ if (sdkOrId && typeof sdkOrId === 'object' && typeof sdkOrId.getStream === 'function') {
+ sdk = sdkOrId;
+ id = idOrS;
+ s = sOrE;
+ e = maybeE;
}
+
+ if (!sdk || !id) return [];
+
+ const activeSources = SOURCES.filter(cfg => !cfg.disabled && cfg.skipProxy);
+ const seenUrls = new Set();
+ const downloads = [];
+
+ const results = await Promise.allSettled(
+ activeSources.map(async (cfg) => {
+ try {
+ const res = await sdk.getStream(cfg.key, id, s, e);
+ if (!res) return;
+
+ const items = Array.isArray(res.allUrls) && res.allUrls.length > 0
+ ? res.allUrls
+ : (res.url ? [{ url: res.url, label: cfg.label }] : []);
+
+ for (const item of items) {
+ if (!item?.url || typeof item.url !== 'string') continue;
+ const url = item.url.trim();
+
+ if (!url.startsWith('http') || url.includes('.m3u8')) continue;
+ if (!/\.mp4(\?|$)/i.test(url) && !url.includes('.mp4')) continue;
+ if (seenUrls.has(url)) continue;
+
+ seenUrls.add(url);
+
+ downloads.push({
+ source: cfg.key,
+ label: item.label || cfg.label,
+ url,
+ });
+ }
+ } catch { }
+ })
+ );
+
+ return downloads;
}
\ No newline at end of file
diff --git a/src/downloads/sources/trendimovies.js b/src/downloads/sources/trendimovies.js
deleted file mode 100644
index b9f92d3..0000000
--- a/src/downloads/sources/trendimovies.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const BASE = "https://trendimovies.com";
-
-const HEADERS = {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
- "Accept-Language": "en-US,en;q=0.9",
- "Referer": "https://trendimovies.com/",
-};
-
-async function fetchPage(path) {
- const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
- if (!res.ok) throw new Error(`trendimovies ${res.status}`);
- return res.text();
-}
-
-function parseLinks(html) {
- const m = html.match(/DownloadSection[^>]*props="([^"]+)"/s) ||
- html.match(/props="([^"]+)"[^>]*ssr[^>]*client="load"[^>]*opts="[^"]*DownloadSection/s);
- if (!m) {
- const alt = html.match(/"DownloadSection"/);
- if (!alt) return [];
- const propsMatch = html.match(/props="({[^"]*DownloadSection[^"]*})"/s);
- if (!propsMatch) return [];
- return extractLinks(propsMatch[1].replace(/"/g, '"').replace(/'/g, "'"));
- }
- return extractLinks(m[1].replace(/"/g, '"').replace(/'/g, "'"));
-}
-
-function extractLinks(propsJson) {
- try {
- const parsed = JSON.parse(propsJson);
- const rawLinks = parsed?.links?.[1];
- if (!Array.isArray(rawLinks)) return [];
- return rawLinks
- .map(item => item?.[1])
- .filter(l => l && typeof l === "object")
- .map(l => ({
- url: unwrap(l.url),
- quality: unwrap(l.quality) || "HD",
- size: unwrap(l.file_size) || null,
- type: "mkv",
- active: unwrap(l.is_active) !== false,
- }))
- .filter(l => l.url && l.active);
- } catch {
- return [];
- }
-}
-
-function unwrap(v) {
- if (Array.isArray(v) && v.length === 2 && (v[0] === 0 || v[0] === 1)) return v[1];
- return v;
-}
-
-export async function getDownloads(tmdbId) {
- const html = await fetchPage(`/movie/${tmdbId}`);
- return parseLinks(html);
-}
-
-export async function getDownloadsTv(tmdbId, season, episode) {
- const html = await fetchPage(`/tv/${tmdbId}/season/${season}/episode/${episode}`);
- return parseLinks(html);
-}
\ No newline at end of file
diff --git a/src/sdk.d.ts b/src/sdk.d.ts
index fe3eba0..0f27998 100644
--- a/src/sdk.d.ts
+++ b/src/sdk.d.ts
@@ -59,7 +59,6 @@ export interface VylaSDKOptions {
tmdbApiKey?: string;
}
-// Descriptions written with Claude.
export default class VylaSDK {
tmdbApiKey?: string;
diff --git a/src/sdk.js b/src/sdk.js
index 0cfe748..e5fece4 100644
--- a/src/sdk.js
+++ b/src/sdk.js
@@ -63,6 +63,6 @@ export default class VylaSDK {
return await fetchSubtitles(paths);
}
async getDownloads(id, s = null, e = null) {
- return await fetchDownloads(id, s, e);
+ return await fetchDownloads(this, id, s, e);
}
}
\ No newline at end of file
diff --git a/src/sources/111477.js b/src/sources/111477.js
new file mode 100644
index 0000000..ca88abd
--- /dev/null
+++ b/src/sources/111477.js
@@ -0,0 +1,101 @@
+import { getUA } from '../utils/helpers.js';
+
+const SERVICE_ORIGIN = 'https://st.111477.xyz';
+
+function generateManifestBaseUrl(tmdbKey) {
+ let config = 'https://a.111477.xyz/::sort=file-desc::limit=3';
+ if (tmdbKey) {
+ config += `::tmdb=${tmdbKey}`;
+ }
+ const b64 = Buffer.from(config, 'utf8')
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '');
+ return `${SERVICE_ORIGIN}/config/${b64}`;
+}
+
+async function resolveImdbId(sdk, id, isTv) {
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
+ if (!apiKey) return null;
+ const type = isTv ? 'tv' : 'movie';
+ try {
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
+ signal: AbortSignal.timeout(6000),
+ });
+ if (!res.ok) return null;
+ const data = await res.json();
+ return data.imdb_id || data.external_ids?.imdb_id || null;
+ } catch {
+ return null;
+ }
+}
+
+export async function getStream({ id, s, e, sdk }) {
+ const isTv = Boolean(s && e);
+ const ua = getUA();
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
+
+ const imdbId = await resolveImdbId(sdk, id, isTv);
+ const targetIds = [];
+
+ if (imdbId && imdbId.startsWith('tt')) {
+ targetIds.push(imdbId);
+ }
+ targetIds.push(`tmdb:${id}`);
+
+ const addonBase = generateManifestBaseUrl(apiKey);
+ const seenUrls = new Set();
+ const allUrls = [];
+
+ const defaultHeaders = {
+ 'User-Agent': ua,
+ Accept: 'application/json, text/plain, */*',
+ };
+
+ for (const targetId of targetIds) {
+ const endpoint = isTv
+ ? `${addonBase}/stream/series/${targetId}:${s || 1}:${e || 1}.json`
+ : `${addonBase}/stream/movie/${targetId}.json`;
+
+ try {
+ const res = await fetch(endpoint, {
+ headers: defaultHeaders,
+ signal: AbortSignal.timeout(12000),
+ });
+
+ if (!res.ok) continue;
+ const data = await res.json();
+
+ if (Array.isArray(data?.streams) && data.streams.length > 0) {
+ for (const item of data.streams) {
+ if (!item || typeof item !== 'object') continue;
+ const url = String(item.url || '').trim();
+ if (!url || !url.startsWith('http') || seenUrls.has(url)) continue;
+
+ seenUrls.add(url);
+
+ const rawTitle = item.title ? String(item.title).trim() : '';
+ const rawName = item.name ? String(item.name).trim() : '111477';
+ const label = rawTitle || rawName;
+
+ allUrls.push({
+ url,
+ label,
+ headers: defaultHeaders,
+ });
+ }
+
+ if (allUrls.length > 0) break;
+ }
+ } catch { }
+ }
+
+ if (allUrls.length === 0) return null;
+
+ return {
+ url: allUrls[0].url,
+ allUrls,
+ headers: allUrls[0].headers,
+ };
+}
\ No newline at end of file
diff --git a/src/sources/4khdhub.js b/src/sources/4khdhub.js
new file mode 100644
index 0000000..fa96f27
--- /dev/null
+++ b/src/sources/4khdhub.js
@@ -0,0 +1,324 @@
+import { getUA } from '../utils/helpers.js';
+
+const INITIAL_BASE_URL = 'https://4khdhub.one';
+let cachedBaseUrl = null;
+let cachedBaseUrlExpiry = 0;
+
+function rot13(str) {
+ return str.replace(/[a-zA-Z]/g, (c) => {
+ const code = c.charCodeAt(0);
+ if (code >= 65 && code <= 90) {
+ return String.fromCharCode(((code - 65 + 13) % 26) + 65);
+ }
+ return String.fromCharCode(((code - 97 + 13) % 26) + 97);
+ });
+}
+
+function levenshtein(a, b) {
+ if (a === b) return 0;
+ if (!a.length) return b.length;
+ if (!b.length) return a.length;
+ const la = a.length;
+ const lb = b.length;
+ const d = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0));
+ for (let i = 0; i <= la; i++) d[i][0] = i;
+ for (let j = 0; j <= lb; j++) d[0][j] = j;
+ for (let i = 1; i <= la; i++) {
+ for (let j = 1; j <= lb; j++) {
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
+ }
+ }
+ return d[la][lb];
+}
+
+async function getBaseUrl(ua) {
+ if (cachedBaseUrl && Date.now() < cachedBaseUrlExpiry) {
+ return cachedBaseUrl;
+ }
+
+ let currentUrl = INITIAL_BASE_URL;
+ for (let i = 0; i < 10; i++) {
+ try {
+ const res = await fetch(currentUrl, {
+ method: 'HEAD',
+ headers: { 'User-Agent': ua },
+ redirect: 'manual',
+ signal: AbortSignal.timeout(10000),
+ });
+
+ if (res.status >= 300 && res.status < 400) {
+ const location = res.headers.get('location');
+ if (location) {
+ currentUrl = new URL(location, currentUrl).toString();
+ continue;
+ }
+ }
+ break;
+ } catch {
+ break;
+ }
+ }
+
+ const parsed = new URL(currentUrl);
+ cachedBaseUrl = `${parsed.protocol}//${parsed.host}`;
+ cachedBaseUrlExpiry = Date.now() + 3600000;
+ return cachedBaseUrl;
+}
+
+async function resolveMetadata(sdk, id, isTv) {
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
+ if (!apiKey) return null;
+ const type = isTv ? 'tv' : 'movie';
+ try {
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
+ signal: AbortSignal.timeout(8000),
+ });
+ if (!res.ok) return null;
+ const data = await res.json();
+ const title = isTv ? data.name : data.title;
+ const releaseDate = isTv ? data.first_air_date : data.release_date;
+ const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
+ return { title, year };
+ } catch {
+ return null;
+ }
+}
+
+function sanitizeStreamUrl(rawUrl) {
+ try {
+ const parsed = new URL(rawUrl);
+ parsed.pathname = parsed.pathname.replaceAll('::', '%3A%3A');
+ return parsed.toString();
+ } catch {
+ return rawUrl.replaceAll('::', '%3A%3A');
+ }
+}
+
+async function validateStreamUrl(url, ua) {
+ try {
+ const res = await fetch(url, {
+ headers: {
+ 'User-Agent': ua,
+ Range: 'bytes=0-1024',
+ },
+ signal: AbortSignal.timeout(4000),
+ });
+
+ const contentType = (res.headers.get('content-type') || '').toLowerCase();
+ const isSuccess = (res.status >= 200 && res.status < 300) || res.status === 206;
+ const isHtmlOrJsonError = contentType.includes('text/html') || contentType.includes('application/json');
+
+ if (!isSuccess || isHtmlOrJsonError) return false;
+ return true;
+ } catch {
+ return true;
+ }
+}
+
+async function resolveRedirectUrl(redirectUrl, ua) {
+ try {
+ const res = await fetch(redirectUrl, {
+ headers: { 'User-Agent': ua },
+ signal: AbortSignal.timeout(15000),
+ });
+ if (!res.ok) return null;
+ const html = await res.text();
+
+ let nextUrl = '';
+ const downloadMatch = html.match(/]*class="[^"]*btn-success/);
+ if (finalMatch) {
+ return finalMatch[1];
+ }
+
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+async function extractSourceResults(itemHtml, ua) {
+ const sizeMatch = itemHtml.match(/([\d.]+ ?[GM]B)/);
+ const sizeStr = sizeMatch ? sizeMatch[1] : null;
+
+ const heightMatch = itemHtml.match(/\d{3,}p/);
+ const heightStr = heightMatch ? heightMatch[0] : null;
+
+ const fileTitleMatch = itemHtml.match(/class="(?:file-title|episode-file-title)"[^>]*>([^<]+)/);
+ const fileTitle = fileTitleMatch ? fileTitleMatch[1].trim() : 'Unknown';
+
+ const displayParts = [fileTitle];
+ if (heightStr) displayParts.push(heightStr);
+ if (sizeStr) displayParts.push(sizeStr);
+
+ let hubCloudUrl = null;
+ let hubDriveUrl = null;
+
+ const linkRegex = /]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
+ let match;
+ while ((match = linkRegex.exec(itemHtml)) !== null) {
+ const href = match[1];
+ const text = match[2];
+ if (text.includes('HubCloud') && !hubCloudUrl) {
+ hubCloudUrl = href;
+ } else if (text.includes('HubDrive') && !hubDriveUrl) {
+ hubDriveUrl = href;
+ }
+ }
+
+ const redirectUrl = hubCloudUrl || hubDriveUrl;
+ if (!redirectUrl) return null;
+
+ const resolvedUrl = await resolveRedirectUrl(redirectUrl, ua);
+ if (!resolvedUrl) return null;
+
+ const safeUrl = sanitizeStreamUrl(resolvedUrl);
+ const isValid = await validateStreamUrl(safeUrl, ua);
+ if (!isValid) return null;
+
+ return {
+ url: safeUrl,
+ label: displayParts.join(' · '),
+ headers: {
+ 'User-Agent': ua,
+ },
+ };
+}
+
+export async function getStream({ id, s, e, sdk }) {
+ const isTv = Boolean(s && e);
+ const ua = getUA();
+
+ const meta = await resolveMetadata(sdk, id, isTv);
+ if (!meta?.title) return null;
+
+ const baseUrl = await getBaseUrl(ua);
+ const searchUrl = `${baseUrl}/?s=${encodeURIComponent(meta.title)}`;
+
+ const searchRes = await fetch(searchUrl, {
+ headers: { 'User-Agent': ua },
+ signal: AbortSignal.timeout(15000),
+ });
+ if (!searchRes.ok) return null;
+ const searchHtml = await searchRes.text();
+
+ const cardRegex = /]*\bclass="[^"]*movie-card[^"]*"[^>]*)>([\s\S]*?)<\/a>/gi;
+ const formatFilter = isTv ? 'Series' : 'Movies';
+ let targetPageUrl = null;
+
+ let cardMatch;
+ while ((cardMatch = cardRegex.exec(searchHtml)) !== null) {
+ const attributes = cardMatch[1];
+ const inner = cardMatch[2];
+
+ const hrefMatch = attributes.match(/href="([^"]+)"/);
+ if (!hrefMatch) continue;
+ const href = hrefMatch[1];
+
+ const formatMatches = [...inner.matchAll(/class="movie-card-format"[^>]*>([\s\S]*?)<\//gi)];
+ const formatTexts = formatMatches.map(m => m[1].trim());
+ if (!formatTexts.some(t => t.includes(formatFilter))) continue;
+
+ if (meta.year != null) {
+ const metaMatch = inner.match(/class="movie-card-meta"[^>]*>([\s\S]*?)<\//i);
+ const yearMatch = metaMatch ? metaMatch[1].match(/\d{4}/) : null;
+ if (yearMatch) {
+ const cardYear = parseInt(yearMatch[0], 10);
+ if (Math.abs(cardYear - meta.year) > 1) continue;
+ }
+ }
+
+ const titleMatch = inner.match(/class="movie-card-title"[^>]*>([\s\S]*?)<\//i);
+ let cardTitle = titleMatch ? titleMatch[1] : '';
+ cardTitle = cardTitle.replace(/\[.*?\]/g, '').trim();
+
+ const diff = levenshtein(cardTitle.toLowerCase(), meta.title.toLowerCase());
+ const isMatch = diff < 5 || (cardTitle.toLowerCase().includes(meta.title.toLowerCase()) && diff < 16);
+ if (!isMatch) continue;
+
+ targetPageUrl = new URL(href, baseUrl).toString();
+ break;
+ }
+
+ if (!targetPageUrl) return null;
+
+ const pageRes = await fetch(targetPageUrl, {
+ headers: { 'User-Agent': ua },
+ signal: AbortSignal.timeout(15000),
+ });
+ if (!pageRes.ok) return null;
+ const pageHtml = await pageRes.text();
+
+ const matchedHtmlBlocks = [];
+
+ if (isTv) {
+ const seasonStr = String(s).padStart(2, '0');
+ const episodeStr = String(e).padStart(2, '0');
+
+ const epItemRegex = /class="[^"]*episode-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-item"|<\/main>|$))/gi;
+ let epMatch;
+ while ((epMatch = epItemRegex.exec(pageHtml)) !== null) {
+ const epBlock = epMatch[1];
+ const titleMatch = epBlock.match(/class="episode-title"[^>]*>([\s\S]*?)<\//i);
+ const titleText = titleMatch ? titleMatch[1] : '';
+ if (!titleText.includes(`S${seasonStr}`)) continue;
+
+ const dlItemRegex = /class="[^"]*episode-download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-download-item"|<\/div>\s*<\/div>|$))/gi;
+ let dlMatch;
+ while ((dlMatch = dlItemRegex.exec(epBlock)) !== null) {
+ const dlBlock = dlMatch[1];
+ if (dlBlock.includes(`Episode-${episodeStr}`)) {
+ matchedHtmlBlocks.push(dlBlock);
+ }
+ }
+ }
+ } else {
+ const dlItemRegex = /class="[^"]*download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*download-item"|<\/main>|$))/gi;
+ let dlMatch;
+ while ((dlMatch = dlItemRegex.exec(pageHtml)) !== null) {
+ matchedHtmlBlocks.push(dlMatch[1]);
+ }
+ }
+
+ if (matchedHtmlBlocks.length === 0) return null;
+
+ const results = await Promise.all(matchedHtmlBlocks.map((block) => extractSourceResults(block, ua)));
+ const allUrls = results.filter(Boolean);
+
+ if (allUrls.length === 0) return null;
+
+ return {
+ url: allUrls[0].url,
+ allUrls,
+ headers: allUrls[0].headers,
+ };
+}
\ No newline at end of file
diff --git a/src/sources/anipm.js b/src/sources/anipm.js
deleted file mode 100644
index e0d1b09..0000000
--- a/src/sources/anipm.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { getTmdbInfo, fetchJson } from '../utils/helpers.js';
-
-const BASE_URL = "https://ani.pm";
-
-function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
-
-async function searchCatalog(query) {
- try {
- const data = await fetchJson(`${BASE_URL}/api/anime/catalog?q=${encodeURIComponent(query)}&page=1`, { signal: AbortSignal.timeout(6000) });
- return Array.isArray(data?.items) ? data.items : [];
- } catch { return []; }
-}
-
-async function resolveEntry(titles, year) {
- const orderedTitles = titles.length > 1 ? [...titles].reverse() : titles;
- let best = null, bestScore = -1;
- for (const title of orderedTitles) {
- const items = await searchCatalog(title);
- const targetClean = cleanTitle(title);
- for (const item of items) {
- const itemTitles = [item.title, item.native].filter(Boolean).map(cleanTitle);
- let score = 0;
- if (itemTitles.some(t => t === targetClean)) score += 5;
- else if (itemTitles.some(t => t.includes(targetClean) || targetClean.includes(t))) score += 3;
- else continue;
- if (item.type === 'TV') score += 4;
- else if (item.type === 'Movie' || item.type === 'Special' || item.type === 'ONA') score -= 2;
- if (year && item.year) {
- const diff = Math.abs(item.year - year);
- if (diff === 0) score += 3;
- else if (diff === 1) score += 1;
- else if (diff > 2) score -= 3;
- }
- if (score > bestScore) { bestScore = score; best = item; }
- }
- if (bestScore >= 9) break;
- }
- return best;
-}
-
-export async function getStream({ id, s, e, audio }) {
- try {
- const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
- if (!info || !info.isAnime) return null;
- const entry = await resolveEntry(info.titles, info.year);
- if (!entry) return null;
- const episodeNum = e || 1;
- const params = new URLSearchParams({ title: entry.title || '', ep: String(episodeNum) });
- if (entry.year) params.set('year', entry.year);
- if (entry.anilistId) params.set('anilistId', entry.anilistId);
- if (entry.malId) params.set('malId', entry.malId);
- const servers = await fetchJson(`${BASE_URL}/api/anime/src/servers?${params}`, { signal: AbortSignal.timeout(10000) });
- if (!servers) return null;
- const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
- for (const aud of audiosToTry) {
- const list = servers[aud];
- if (!Array.isArray(list) || !list.length) continue;
- const allUrls = list.filter(srv => (srv.kind === "hls" || srv.kind === "file") && srv.url)
- .sort((a, b) => (b.priority || 0) - (a.priority || 0))
- .map(srv => ({ url: srv.url.startsWith('/') ? `${BASE_URL}${srv.url}` : srv.url, type: srv.kind === "hls" ? "hls" : "mp4", audio: aud, server: srv.provider ? `AniPM-${srv.provider}` : "AniPM", headers: { Referer: `${BASE_URL}/`, Origin: BASE_URL }, skipProxy: false }));
- if (allUrls.length) return { allUrls };
- }
- return null;
- } catch { return null; }
-}
-
-export async function getSources(args) {
- const stream = await getStream(args);
- return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
-}
\ No newline at end of file
diff --git a/src/sources/downloadeverything.js b/src/sources/downloadeverything.js
new file mode 100644
index 0000000..29552cb
--- /dev/null
+++ b/src/sources/downloadeverything.js
@@ -0,0 +1,338 @@
+import { getUA } from '../utils/helpers.js';
+
+const SLAVE_URL = 'https://slave.downloadeverythingfromeverywhere.com/';
+const REFERER = 'https://downloadeverythingfromeverywhere.com/';
+const ORIGIN = 'https://downloadeverythingfromeverywhere.com';
+const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
+
+function extractJsonObjects(text) {
+ const objects = [];
+ let depth = 0;
+ let start = -1;
+ let inString = false;
+ let escape = false;
+
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+ if (escape) {
+ escape = false;
+ continue;
+ }
+ if (char === '\\') {
+ escape = true;
+ continue;
+ }
+ if (char === '"') {
+ inString = !inString;
+ continue;
+ }
+ if (!inString) {
+ if (char === '{') {
+ if (depth === 0) start = i;
+ depth++;
+ } else if (char === '}') {
+ depth--;
+ if (depth === 0 && start !== -1) {
+ const jsonStr = text.substring(start, i + 1);
+ try {
+ objects.push(JSON.parse(jsonStr));
+ } catch { }
+ start = -1;
+ }
+ }
+ }
+ }
+ return objects;
+}
+
+async function resolveDetails(sdk, id, isTv) {
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
+ if (!apiKey) return null;
+ const type = isTv ? 'tv' : 'movie';
+ try {
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
+ signal: AbortSignal.timeout(6000),
+ });
+ if (!res.ok) return null;
+ const data = await res.json();
+ const title = isTv ? data.name : data.title;
+ const releaseDate = isTv ? data.first_air_date : data.release_date;
+ const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
+ const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
+ return { title, year, imdbId };
+ } catch {
+ return null;
+ }
+}
+
+async function resolveHubCloud(hubUrl, ua) {
+ try {
+ const res1 = await fetch(hubUrl, {
+ headers: {
+ 'User-Agent': ua,
+ Referer: REFERER,
+ },
+ signal: AbortSignal.timeout(8000),
+ });
+
+ const html = await res1.text();
+ const hubPhpMatch = html.match(/https?:\/\/[^\s"<>]+\/hubcloud\.php\?[^\s"<>]+/i);
+ if (!hubPhpMatch) return null;
+
+ const phpUrl = hubPhpMatch[0];
+ const phpRes = await fetch(phpUrl, {
+ headers: {
+ 'User-Agent': ua,
+ Referer: hubUrl,
+ },
+ signal: AbortSignal.timeout(8000),
+ });
+
+ const phpBody = await phpRes.text();
+
+ const r2Match = phpBody.match(/https?:\/\/[a-zA-Z0-9.\-_]+\.r2\.cloudflarestorage\.com\/[^\s"<>]+/i);
+ if (r2Match) {
+ return r2Match[0].replaceAll('&', '&');
+ }
+
+ const pixelMatch = phpBody.match(/https?:\/\/pixel\.hubcloud\.[a-z]+\/\?id=[^\s"<>]+/i);
+ if (pixelMatch) {
+ return pixelMatch[0];
+ }
+ } catch { }
+ return null;
+}
+
+async function resolveClicknUpload(clicknUrl, ua) {
+ try {
+ const res1 = await fetch(clicknUrl, {
+ headers: {
+ 'User-Agent': ua,
+ Referer: REFERER,
+ },
+ signal: AbortSignal.timeout(8000),
+ });
+
+ const body1 = await res1.text();
+ const formMatch1 = body1.match(/