9 Commits

Author SHA1 Message Date
2028219f74 1.0.7 2026-10-24 23:33:01 -04:00
bb1c084c4c Fixed player UI dismissal bug 2026-10-24 23:31:46 -04:00
1c4ee48141 1.0.6 2026-10-05 20:42:23 -07:00
09ddbe6bae 1.0.6 2026-10-05 20:40:56 -07:00
494d8e6835 fixed watch history 2026-10-05 20:38:56 -07:00
1c3ffacccd 1.0.5 2026-10-05 15:50:17 -07:00
44a6e3cef9 1.0.5 2026-10-05 15:47:25 -07:00
9c0c5bf17d tweaked ui 2026-10-05 15:47:10 -07:00
01e532a6a5 v1.0.4 2026-10-05 13:22:52 -07:00
59 changed files with 335 additions and 4954 deletions

View File

@@ -1,49 +0,0 @@
import SOURCES from './sdk/config.js';
import { createStreamArgs } from './sdk/utils/helpers.js';
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from './sdk/subtitles.js';
import * as m0 from './sdk/sources/123anime.js';
import * as m1 from './sdk/sources/111477.js';
import * as m2 from './sdk/sources/anihq.js';
import * as m3 from './sdk/sources/anineko.js';
import * as m4 from './sdk/sources/bcine.js';
import * as m5 from './sdk/sources/dulo.js';
import * as m6 from './sdk/sources/flaxmovies.js';
import * as m7 from './sdk/sources/fsonline.js';
import * as m8 from './sdk/sources/kisskh.js';
import * as m9 from './sdk/sources/lmscript.js';
import * as m10 from './sdk/sources/lookmovie.js';
import * as m11 from './sdk/sources/movienight.js';
import * as m12 from './sdk/sources/movy.js';
import * as m13 from './sdk/sources/pengu.js';
import * as m14 from './sdk/sources/pstream.js';
import * as m15 from './sdk/sources/purstream.js';
import * as m16 from './sdk/sources/rivestream.js';
import * as m17 from './sdk/sources/streamaggregator.js';
import * as m18 from './sdk/sources/vidapi.js';
import * as m19 from './sdk/sources/vidcore.js';
import * as m20 from './sdk/sources/vidfast.js';
import * as m21 from './sdk/sources/vidgod.js';
import * as m22 from './sdk/sources/vidlink.js';
import * as m23 from './sdk/sources/vidnest.js';
import * as m24 from './sdk/sources/vidrock.js';
import * as m25 from './sdk/sources/vidup.js';
import * as m26 from './sdk/sources/vidvault.js';
import * as m27 from './sdk/sources/vidzee.js';
import * as m28 from './sdk/sources/vixsrc.js';
import * as m29 from './sdk/sources/xdownloader.js';
const MODS = { '123anime': m0, '111477': m1, 'anihq': m2, 'anineko': m3, 'bcine': m4, 'dulo': m5, 'flaxmovies': m6, 'fsonline': m7, 'kisskh': m8, 'lmscript': m9, 'lookmovie': m10, 'movienight': m11, 'movy': m12, 'pengu': m13, 'pstream': m14, 'purstream': m15, 'rivestream': m16, 'streamaggregator': m17, 'vidapi': m18, 'vidcore': m19, 'vidfast': m20, 'vidgod': m21, 'vidlink': m22, 'vidnest': m23, 'vidrock': m24, 'vidup': m25, 'vidvault': m26, 'vidzee': m27, 'vixsrc': m28, 'xdownloader': m29 };
const core = { tmdbApiKey: null, penguManifest: null };
const live = () => SOURCES.filter(c => !c.disabled && MODS[c.sourceFile]);
globalThis.VylaLocal = {
init(key) { core.tmdbApiKey = key; globalThis.process.env.TMDB_API_KEY = key; },
sources() { return live(); },
async stream(key, id, s, e, ip) {
const cfg = live().find(c => c.key === key);
if (!cfg) throw new Error('Source not found');
return await MODS[cfg.sourceFile].getStream(await createStreamArgs(cfg, core, id, s, e, ip));
},
async subtitles(id, s, e) {
const paths = s != null && e != null ? await getSubPathsTv(id, s, e) : await getSubPathsMovie(id);
return await fetchSubtitles(paths);
}
};

View File

@@ -1,480 +0,0 @@
export const SOURCES = [
{
key: '123anime',
label: '123Anime',
sourceFile: '123anime',
proxyParam: 'a1',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: false,
skipProxy: true,
cdnHeaders: [{
pattern: /hlsx\d+cdn\.|burntburst\d+\.store|echovideo\.ru/i,
headers: {
Referer: 'https://play2.echovideo.ru/',
Origin: 'https://play2.echovideo.ru',
},
},],
},
{
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)',
sourceFile: 'anihq',
proxyParam: 'ahsub',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true
},
{
key: 'anihq-dub',
label: 'AniHQ (Dub)',
sourceFile: 'anihq',
proxyParam: 'ahdub',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true
},
{
key: 'anineko-sub',
sourceFile: 'anineko',
label: 'AniNeko (Sub)',
proxyParam: 'anksub',
timeout: 25000,
jitter: 500,
retries: 2
},
{
key: 'anineko-dub',
sourceFile: 'anineko',
label: 'AniNeko (Dub)',
proxyParam: 'ankdub',
timeout: 25000,
jitter: 500,
retries: 2
},
{
key: 'bcine',
label: 'Bcine',
sourceFile: 'bcine',
proxyParam: 'bc',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
cdnHeaders: [{
pattern: /1embed\.cc|videasy\.to/i,
headers: {
Referer: 'https://bcine.ru/',
Origin: 'https://bcine.ru',
},
}],
},
{
key: 'dulo',
label: 'Dulo',
sourceFile: 'dulo',
proxyParam: 'dl',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /dulo\.mov/i,
headers: {
Referer: 'https://dulo.mov/',
Origin: 'https://dulo.mov',
},
}],
},
{
key: 'flaxmovies',
label: 'FlaxMovies',
sourceFile: 'flaxmovies',
proxyParam: 'fx',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /flix2watch\.pro/i,
headers: {
Referer: 'https://flaxmovies.xyz/',
Origin: 'https://flaxmovies.xyz',
},
},],
},
{
key: 'fsonline',
label: 'FSOnline',
sourceFile: 'fsonline',
proxyParam: 'fo',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
skipProxy: true,
},
{
key: 'kisskh',
label: 'KissKH',
sourceFile: 'kisskh',
proxyParam: 'kk',
timeout: 30000,
jitter: 500,
retries: 1,
},
{
key: 'lmscript',
label: 'LMScript',
sourceFile: 'lmscript',
proxyParam: 'lmsc',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
},
{
key: 'lookmovie',
label: 'LookMovie',
sourceFile: 'lookmovie',
proxyParam: 'lm',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
verifyHeaders: {
'Accept-Language': 'en-US,en;q=0.9',
},
},
{
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: 'pengu',
label: 'Pengu',
sourceFile: 'pengu',
proxyParam: 'pg',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true,
skipProxy: true,
disabled: false,
},
{
key: 'pstream',
label: 'PStream',
sourceFile: 'pstream',
proxyParam: 'ps',
timeout: 35000,
jitter: 500,
retries: 1,
multiUrl: true,
},
{
key: 'purstream',
sourceFile: 'purstream',
label: 'Purstream',
proxyParam: 'ps',
timeout: 20000,
jitter: 500,
retries: 2,
skipProxy: true
},
{
key: 'rivestream',
label: 'RiveStream',
sourceFile: 'rivestream',
proxyParam: 'rs',
timeout: 30000,
jitter: 500,
retries: 2,
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: 'vidapi',
label: 'VidAPI',
sourceFile: 'vidapi',
proxyParam: 'va',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true
},
{
key: 'vidcore',
label: 'VidCore',
sourceFile: 'vidcore',
proxyParam: 'vc',
timeout: 35000,
jitter: 500,
retries: 1,
multiUrl: true,
},
{
key: 'vidfast',
label: 'VidFast',
sourceFile: 'vidfast',
proxyParam: 'vf',
timeout: 35000,
jitter: 500,
retries: 1,
multiUrl: true,
},
{
key: 'vidgod',
label: 'VidGod',
sourceFile: 'vidgod',
proxyParam: 'vg',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
},
{
key: 'vidlink',
label: 'Vidlink',
sourceFile: 'vidlink',
proxyParam: 'vl',
timeout: 20000,
jitter: 500,
retries: 2,
skipProxy: true,
multiUrl: true,
},
{
key: 'vidnest',
label: 'VidNest',
sourceFile: 'vidnest',
proxyParam: 'vdn',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
},
{
key: 'vidnest-sub',
label: 'VidNest (Sub)',
sourceFile: 'vidnest',
proxyParam: 'vdn',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
skipProxy: true,
},
{
key: 'vidnest-dub',
label: 'VidNest (Dub)',
sourceFile: 'vidnest',
proxyParam: 'vdn',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
skipProxy: true,
},
{
key: 'vidrock',
label: 'VidRock',
sourceFile: 'vidrock',
proxyParam: 'vr',
timeout: 20000,
jitter: 800,
retries: 3,
multiUrl: true,
cdnHeaders: [{
pattern: /./,
headers: {
Accept: '/',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://vidrock.ru/',
Origin: 'https://vidrock.ru',
},
},],
},
{
key: 'vidup',
label: 'VidUp',
sourceFile: 'vidup',
proxyParam: 'vu',
timeout: 35000,
jitter: 500,
retries: 1,
multiUrl: true
},
{
key: 'vidvault',
label: 'VidVault',
sourceFile: 'vidvault',
proxyParam: 'vv',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
},
{
key: 'vidzee',
label: 'VidZee',
sourceFile: 'vidzee',
proxyParam: 'vz',
timeout: 20000,
sourcesTimeout: 10000,
jitter: 400,
retries: 3,
verifyHeaders: {
Accept: '/',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://player.vidzee.wtf',
Origin: 'https://player.vidzee.wtf',
},
},
{
key: 'vixsrc',
label: 'VixSrc',
sourceFile: 'vixsrc',
proxyParam: 'vx',
timeout: 35000,
jitter: 0,
retries: 2,
multiUrl: false,
skipProxy: true,
verifyHeaders: {
Accept: 'application/json, text/javascript, /; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://vixsrc.to/',
Origin: 'https://vixsrc.to',
},
},
{
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',
},
}],
},
];
export const HEALTH_PROBE_ID = '155';
export const SOURCE_MAP = Object.fromEntries(SOURCES.map(s => [s.key, s]));
export default SOURCES;

View File

@@ -1,53 +0,0 @@
import SOURCES from "../config.js";
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;
}

View File

@@ -1,38 +0,0 @@
import { HEALTH_PROBE_ID } from "./config.js";
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
async function withRetry(fn, attempts = 2, delay = 300) {
for (let i = 0; i < attempts; i++) {
try {
const result = await fn();
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));
}
}
return null;
}
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 t = Date.now();
const result = await withTimeout(
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
cfg.timeout
);
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
}

122
.build/sdk/sdk.d.ts vendored
View File

@@ -1,122 +0,0 @@
export interface SourceConfig {
key: string;
label: string;
sourceFile: string;
proxyParam: string;
timeout: number;
jitter: number;
retries: number;
multiUrl: boolean;
skipProxy?: boolean;
cdnHeaders?: {
pattern: RegExp;
headers: {
Referer: string;
Origin: string;
};
}[];
skipVerify?: boolean;
verifyHeaders?: boolean;
disabled?: boolean;
sourcesTimeout?: number;
}
export interface ProbeResult {
ok: boolean;
ms: number | null;
}
export interface StreamResult {
url: string;
headers: {
"User-Agent": string;
Referer: string;
Origin: string;
[key: string]: string;
};
server: string;
quality: string;
type: "mp4" | "hls";
skipVerify?: boolean;
skipProxy?: boolean;
}
export interface DownloadResult {
url: string;
quality: string;
size: number | null;
type: "mkv";
active: boolean;
}
export interface SubtitleResult {
label: string;
file: string;
type: "vtt" | "srt";
source: string;
}
export type ProbeAllResult = Record<string, ProbeResult>;
export interface VylaSDKOptions {
tmdbApiKey?: string;
}
export default class VylaSDK {
tmdbApiKey?: string;
constructor(options?: VylaSDKOptions);
/**
* Get the list of configured sources.
* @param excludeDisabled If true, filters out disabled sources.
*/
getSources(excludeDisabled?: boolean): SourceConfig[];
/**
* Probe all active (non-disabled) sources for availability.
*/
probeAllSources(): Promise<ProbeAllResult>;
/**
* Probe a single source by key.
*/
probeSource(key: string): Promise<ProbeResult>;
/**
* Get a playable stream for a given source/media id.
* @param key Source key
* @param id Media id (e.g. TMDB id)
* @param s Season number (for TV), or null for movies
* @param e Episode number (for TV), or null for movies
* @param clientIP Optional client IP for source-specific logic
*/
getStream(
key: string,
id: string | number,
s?: number | null,
e?: number | null,
clientIP?: string | null
): Promise<StreamResult | { allUrls: StreamResult[] }>;
/**
* Get subtitles for a movie or TV episode.
* @param id Media id
* @param s Season number (for TV), or null for movies
* @param e Episode number (for TV), or null for movies
*/
getSubtitles(
id: string | number,
s?: number | null,
e?: number | null
): Promise<SubtitleResult[]>;
/**
* Get download links for a movie or TV episode.
* @param id Media id
* @param s Season number (for TV), or null for movies
* @param e Episode number (for TV), or null for movies
*/
getDownloads(
id: string | number,
s?: number | null,
e?: number | null
): Promise<DownloadResult[]>;
}

View File

@@ -1,68 +0,0 @@
import SOURCES, { SOURCE_MAP } from "./config.js";
import { fetchDownloads } from "./downloads/downloads.js";
import { probeSource } from "./health.js";
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
import { createStreamArgs } from "./utils/helpers.js";
const ALL_SOURCE_MODULES = Object.fromEntries(
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
);
const SOURCE_MODULES = Object.fromEntries(
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
);
export default class VylaSDK {
tmdbApiKey = null;
penguManifest = null;
constructor({ tmdbApiKey, penguManifest } = {}) {
this.tmdbApiKey = tmdbApiKey || null;
this.penguManifest = penguManifest || null;
}
getSources(excludeDisabled = false) {
if (excludeDisabled) {
return SOURCES.filter(cfg => !cfg.disabled);
}
return SOURCES;
}
async probeAllSources() {
const active = SOURCES.filter(cfg => !cfg.disabled);
const results = await Promise.allSettled(
active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key]))
);
const sources = Object.fromEntries(
active.map((cfg, i) => [
cfg.key,
results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null },
])
);
return sources;
}
async probeSource(key) {
const cfg = SOURCES.find(cfg => cfg.key === key);
if (!cfg) throw new Error(`Source with key "${key}" not found`);
const mod = SOURCE_MODULES[key];
if (!mod) throw new Error(`Source module for key "${key}" not found`);
return await probeSource(this, cfg, mod);
}
async getStream(key, id, s = null, e = null, clientIP = null) {
const cfg = SOURCES.find(cfg => cfg.key === key);
if (!cfg) throw new Error(`Source with key "${key}" not found`);
const mod = SOURCE_MODULES[key];
if (!mod) throw new Error(`Source module for key "${key}" not found`);
const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP);
return await mod.getStream(streamArgs);
}
async getSubtitles(id, s = null, e = null) {
const paths = s != null && e != null
? await getSubPathsTv(id, s, e)
: await getSubPathsMovie(id);
return await fetchSubtitles(paths);
}
async getDownloads(id, s = null, e = null) {
return await fetchDownloads(this, id, s, e);
}
}

View File

@@ -1,101 +0,0 @@
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,
};
}

View File

@@ -1,57 +0,0 @@
import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js';
async function resolveSlug(id, s, e, info) {
const titles = info?.titles || [];
const keywords = [];
for (const title of titles) {
const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
if (!keywords.includes(t)) keywords.push(t);
const words = title.split(/[\s:]+/).filter(w => w.length > 2);
if (words.length >= 2) {
const short = words.slice(0, 3).join(' ').toLowerCase();
if (!keywords.includes(short)) keywords.push(short);
}
}
const slugCandidates = [];
for (const keyword of keywords) {
try {
const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) });
let html = data.html || '', idx = 0;
while ((idx = html.indexOf('href="/anime/', idx)) !== -1) {
const start = idx + 13;
const end = html.indexOf('"', start);
const slug = html.slice(start, end);
if (!slugCandidates.includes(slug)) slugCandidates.push(slug);
idx = end;
}
} catch { }
if (slugCandidates.length) break;
}
for (const slug of slugCandidates) {
try {
const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
if (data?.target) return data.target;
} catch { }
}
return null;
}
export async function getStream({ id, s, e }) {
if (!s) return null;
const info = await getTmdbInfo(id, 'tv', s);
if (!info.isAnime) return null;
const target = await resolveSlug(id, s, e, info);
if (!target) return null;
const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/);
if (!embedMatch) return null;
const origin = new URL(target).origin;
try {
const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
if (!embedRes.ok) return null;
const setCookie = embedRes.headers.get('set-cookie');
const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) });
const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null);
if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } };
} catch { }
return null;
}

View File

@@ -1,124 +0,0 @@
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
async function extractVoe(voeUrl) {
try {
let res = await fetch(voeUrl, {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://anihq.cc/'
}
});
let html = await res.text();
const redirectMatch = html.match(/window\.location\.href\s*=\s*['"](https?:\/\/[^'"]+)['"]/);
if (redirectMatch) {
res = await fetch(redirectMatch[1], {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://anihq.cc/'
}
});
html = await res.text();
}
const mirrorUrl = res.url;
const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
if (!scriptMatch) return null;
let str = scriptMatch[1];
str = str.replace(/[a-zA-Z]/g, (c) => {
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
});
const junk = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
junk.forEach(j => { str = str.split(j).join(''); });
str = Buffer.from(str, 'base64').toString('utf-8');
let shifted = '';
for (let i = 0; i < str.length; i++) {
shifted += String.fromCharCode(str.charCodeAt(i) - 3);
}
let reversed = shifted.split('').reverse().join('');
let finalJsonStr = Buffer.from(reversed, 'base64').toString('utf-8');
const finalData = JSON.parse(finalJsonStr);
if (finalData.file || finalData.source) {
return {
url: finalData.file || finalData.source,
mirror: mirrorUrl
};
}
} catch (e) {
return null;
}
return null;
}
function cleanSlug(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')
.replace(/^-+|-+$/g, '');
}
export async function getStream({ id, s, e, audio }) {
if (!s) return null;
const info = await getTmdbInfo(id, 'tv', s);
if (!info.isAnime) return null;
const seriesTitle = info.titles.find(t =>
t.toLowerCase() !== 'east blue' &&
t.length > 3
) || info.titles[0];
const cleanTitle = cleanSlug(seriesTitle);
const typeSuffix = audio === 'dub' ? 'english-dubbed' : 'english-subbed';
const url = `https://anihq.cc/watch/${cleanTitle}-episode-${e}-${typeSuffix}/`;
try {
let pageRes = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
let html = await pageRes.text();
if (!pageRes.ok) {
const searchUrl = `https://anihq.cc/search?keyword=${encodeURIComponent(seriesTitle)}`;
const searchRes = await fetch(searchUrl, { headers: { 'User-Agent': USER_AGENT } });
const searchHtml = await searchRes.text();
const searchRegex = new RegExp(`href="(https://anihq.cc/watch/[^"]+-episode-${e}-${typeSuffix}/)"`, 'i');
const match = searchHtml.match(searchRegex);
if (match) {
const retryRes = await fetch(match[1], { headers: { 'User-Agent': USER_AGENT } });
html = await retryRes.text();
} else return null;
}
const iframeMatch = html.match(/iframe[^>]+src=["'](https:\/\/(?:[a-z0-9.]+|ellenpoliticalfollow\.com)\/e\/[a-zA-Z0-9]+)["']/i);
if (!iframeMatch) return null;
const result = await extractVoe(iframeMatch[1]);
if (result && result.url) {
const mirrorOrigin = new URL(result.mirror).origin;
return {
url: result.url,
headers: {
'Referer': mirrorOrigin + '/',
'User-Agent': USER_AGENT,
'Origin': mirrorOrigin
}
};
}
} catch (err) {
return null;
}
return null;
}

View File

@@ -1,190 +0,0 @@
import { getTmdbInfo, USER_AGENT, fetchText } from '../utils/helpers.js';
const BASE_URL = "https://anineko.to";
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
function decodeEntities(str) {
return str.replace(/&(nbsp|amp|quot|lt|gt);/g, (m, e) => ({ "nbsp": " ", "amp": "&", "quot": "\"", "lt": "<", "gt": ">" })[e]).replace(/&#(\d+);/gi, (m, n) => String.fromCharCode(parseInt(n, 10)));
}
async function search(query) {
try {
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, {
headers: { 'User-Agent': USER_AGENT },
signal: AbortSignal.timeout(6000)
});
const results = [];
let idx = 0;
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
const startHref = html.lastIndexOf('<a', idx);
if (startHref === -1) { idx += 14; continue; }
const hrefMatch = html.slice(startHref, html.indexOf('>', startHref)).match(/href=["']([^"']+)["']/i);
if (!hrefMatch) { idx += 14; continue; }
const slug = hrefMatch[1].split('/').pop();
const h3Start = html.indexOf('nv-anime-title', idx);
if (h3Start !== -1) {
const contentStart = html.indexOf('>', h3Start) + 1;
const contentEnd = html.indexOf('<', contentStart);
if (contentEnd !== -1) results.push({ slug, text: html.slice(contentStart, contentEnd).trim() });
}
idx += 14;
}
if (results.length === 0) {
const linkMatches = [...html.matchAll(/href=["']\/watch\/([^"']+)["']/gi)];
for (const match of linkMatches) {
if (!results.find(r => r.slug === match[1])) {
const linkIdx = html.indexOf(match[0]);
const titleStart = html.lastIndexOf('>', linkIdx);
const titleEnd = html.indexOf('<', linkIdx);
if (titleStart !== -1 && titleEnd !== -1 && titleEnd > linkIdx) {
const title = html.slice(titleStart + 1, Math.min(titleEnd, linkIdx + 200)).trim();
if (title.length > 2 && title.length < 100) {
results.push({ slug: match[1], text: title });
}
}
}
}
}
return results;
} catch { return []; }
}
async function extractHls(embedUrl) {
try {
const html = await fetchText(embedUrl, {
headers: {
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/`
},
signal: AbortSignal.timeout(6000)
});
const m = html.match(/const\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
html.match(/file\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
html.match(/["'](https?:\/\/[^"']+\/master\.m3u8[^"']*)["']/i) ||
html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
html.match(/src:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
if (m) return decodeEntities(m[1]);
} catch { }
return null;
}
export async function getStream({ id, s, e, audio, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const info = await getTmdbInfo(tmdbApiKey, id, s ? 'tv' : 'movie', s);
if (!info || !info.isAnime) return null;
let seriesSlug = null;
for (const title of info.titles) {
const potentialSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
if (!potentialSlug) continue;
try {
const checkHtml = await fetchText(`${BASE_URL}/watch/${potentialSlug}/ep-${e || 1}`, {
headers: { 'User-Agent': USER_AGENT, Referer: `${BASE_URL}/watch/${potentialSlug}` },
signal: AbortSignal.timeout(4000)
});
if (checkHtml && checkHtml.includes('nv-watch-page')) {
seriesSlug = potentialSlug;
break;
}
} catch {}
}
if (!seriesSlug) {
for (const title of info.titles) {
const results = await search(title);
const targetClean = cleanTitle(title);
const exactSlugMatch = results.find(r => {
const slugClean = cleanTitle(r.slug);
return slugClean === targetClean || r.slug === targetClean;
});
if (exactSlugMatch) {
seriesSlug = exactSlugMatch.slug;
break;
}
const sortedResults = [...results].sort((a, b) => a.slug.length - b.slug.length);
for (const r of sortedResults) {
const resultClean = cleanTitle(r.text);
const isExactMatch = resultClean === targetClean;
const isCloseMatch = r.text.toLowerCase().includes(title.toLowerCase());
if (isExactMatch || isCloseMatch) {
seriesSlug = r.slug;
break;
}
}
if (seriesSlug) break;
}
}
if (!seriesSlug && info.titles.length) {
const fSearch = await search(info.titles[0]);
const sortedResults = [...fSearch].sort((a, b) => a.slug.length - b.slug.length);
if (sortedResults.length) seriesSlug = sortedResults[0].slug;
}
if (!seriesSlug) return null;
const watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, {
headers: {
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/watch/${seriesSlug}`
},
signal: AbortSignal.timeout(6000)
});
const byAudio = { sub: [], dub: [] };
const iframeMatch = watchHtml.match(/<iframe[^>]*src=["']([^"']+)["'][^>]*>/i);
if (iframeMatch) {
const iframeSrc = decodeEntities(iframeMatch[1]);
const hls = await extractHls(iframeSrc);
if (hls) {
const audioType = audio === 'dub' ? 'dub' : 'sub';
byAudio[audioType].push(iframeSrc);
}
}
if (byAudio.sub.length === 0 && byAudio.dub.length === 0) {
const tabMatches = [...watchHtml.matchAll(/<button[^>]*class=["'][^"']*nv-server-tab[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>/gi)];
const tabMap = {};
for (const match of tabMatches) {
const tabId = match[1].toLowerCase();
const tabClassMatch = match[0].match(/class=["'][^"']*(tab_\d+)[^"']/i);
if (tabClassMatch) {
const tabClass = tabClassMatch[1];
tabMap[tabClass] = tabId.includes('dub') ? 'dub' : 'sub';
}
}
const btnRegex = /<button\s+[^>]*class=["'][^"']*server-video[^"']*["'][^>]*>/gi;
const btnMatches = [...watchHtml.matchAll(btnRegex)];
for (const match of btnMatches) {
const btnHtml = match[0];
const videoMatch = btnHtml.match(/data-video=["']([^"']+)["']/i);
const tabMatch = btnHtml.match(/data-tab=["']([^"']+)["']/i);
if (videoMatch && tabMatch) {
const videoUrl = decodeEntities(videoMatch[1]);
const tabClass = tabMatch[1];
const audioType = tabMap[tabClass] || 'sub';
byAudio[audioType].push(videoUrl);
}
}
}
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
const allUrls = [];
for (const aud of audiosToTry) {
for (const embed of byAudio[aud]) {
const hls = await extractHls(embed);
if (hls) allUrls.push({ url: hls, type: "hls", audio: aud, server: "AniNeko", skipProxy: false });
}
if (allUrls.length) break;
}
return allUrls.length ? { allUrls } : 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))] : [];
}

View File

@@ -1,202 +0,0 @@
import { USER_AGENT } from '../utils/helpers.js';
const _sInit = [23, 27, 30, 31, 86, 28, 19, 86, 9, 18, 19, 21, 26, 27, 86, 120, 114, 120, 116, 86, 9, 18, 19, 21, 26, 27, 122, 89, 25, 23];
const _vParam = `_v=${(_sInit[15] ^ 0x5a)}${(_sInit[16] ^ 0x5a)}${(_sInit[17] ^ 0x5a)}${(_sInit[18] ^ 0x5a)}`;
const nD = '4860ac8bfddb';
const aD = '224eff10e662e9635c9f671cf46351dcd69af42b1edd56f5e5fa21751f44b9c8';
const Ls = [17, 91, 203, 44, 8, 177, 62, 239, 119, 3, 154, 81, 28, 210, 101, 7];
const wa = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const SERVERS = [
{ id: 'NIGHT', endpoint: '/server/night' },
{ id: 'EMP', endpoint: '/server/emp' },
{ id: 'MAIN', endpoint: '/server/vidsrc' }
];
const CORS_PROXY = 'https://eve.pstream.cfd';
let cachedToken = null;
let tokenExpiry = 0;
function ab(e) {
let t = e >>> 0;
t ^= t >>> 16;
t = Math.imul(t, 2146121005) >>> 0;
t ^= t >>> 15;
t = Math.imul(t, 2221713035) >>> 0;
return (t ^ (t >>> 16)) >>> 0;
}
function sD(e) {
const t = new TextEncoder().encode(aD);
const r = Math.max(32, Math.min(128, e + 17));
const n = new Uint8Array(r);
let a = 2166136261;
for (let s = 0; s < r; s += 1) {
a ^= t[s % t.length] ?? s;
a = ab((a + Ls[s % Ls.length] + ((2654435761 * s) >>> 0)) >>> 0);
n[s] = a & 255;
}
return n;
}
function iD(e) {
let t = '';
for (let r = 0; r < e.length; r += 3) {
const n = e[r];
const a = e[r + 1];
const s = e[r + 2];
t += wa[n >>> 2];
t += wa[((3 & n) << 4) | ((a ?? 0) >>> 4)];
if (a === undefined) break;
t += wa[((15 & a) << 2) | ((s ?? 0) >>> 6)];
if (s === undefined) break;
t += wa[63 & s];
}
return t;
}
function generateDirectHlsUrl(id, s, e) {
const isTv = s != null && e != null;
const tmdbId = Math.floor(Number(id));
const season = isTv ? Math.floor(Number(s || 1)) : 0;
const episode = isTv ? Math.floor(Number(e || 1)) : 0;
const str = `${nD}:${isTv ? 's' : 'm'}:${tmdbId}:${season}:${episode}`;
const a = new TextEncoder().encode(str);
const sArr = sD(a.length);
const i = new Uint8Array(a.length + 2);
i[0] = a.length & 255;
i[1] = (a.length >>> 8) & 255;
let o = (2654435769 ^ a.length) >>> 0;
for (let l = 0; l < a.length; l += 1) {
o = ab((o + sArr[l % sArr.length] + Ls[l % Ls.length] + l) >>> 0);
i[l + 2] = (a[l] ^ (255 & o)) ^ sArr[(7 * l + 3) % sArr.length];
}
return `https://glendale-plumbing.com/c/v1/${iD(i)}/master.m3u8`;
}
async function fetchInternalToken() {
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
try {
const res = await fetch('https://1embed.cc/api/token', {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://1embed.cc/',
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(4000)
});
if (res.ok) {
const data = await res.json();
if (data?.token) {
cachedToken = data.token;
tokenExpiry = Date.now() + 1800 * 1000;
return cachedToken;
}
}
} catch { }
try {
const proxiedUrl = `${CORS_PROXY}/?destination=${encodeURIComponent('https://1embed.cc/api/token')}`;
const res = await fetch(proxiedUrl, {
headers: {
'X-Referer': 'https://1embed.cc/',
'X-User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(4000)
});
if (res.ok) {
const data = await res.json();
if (data?.token) {
cachedToken = data.token;
tokenExpiry = Date.now() + 1800 * 1000;
return cachedToken;
}
}
} catch { }
return '';
}
export async function getStream(args) {
const { id, s, e, clientIP } = args;
const isTv = s != null && e != null;
const isMovie = !isTv;
const allUrls = [];
const directHlsUrl = generateDirectHlsUrl(id, s, e);
const masterUrlWithMark = `${directHlsUrl}?${_vParam}`;
allUrls.push({
url: masterUrlWithMark,
server: 'Bcine (Direct Master)',
quality: 'Auto',
type: 'hls',
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://bcine.ru/',
'Origin': 'https://bcine.ru',
...(clientIP && { 'X-Forwarded-For': clientIP })
}
});
const token = await fetchInternalToken();
await Promise.allSettled(
SERVERS.map(async (server) => {
const query = isTv
? `id=${id}?type=tv&s=${s || 1}&e=${e || 1}`
: `id=${id}?type=movie`;
const stParam = token ? `&_st=${token}` : '';
const targetUrl = `https://1embed.cc${server.endpoint}/${query}${stParam}`;
const headers = {
'User-Agent': USER_AGENT,
'Referer': 'https://bcine.ru/',
'Accept': 'application/json, text/plain, */*',
...(token && { 'X-Stream-Token': token }),
...(clientIP && { 'X-Forwarded-For': clientIP })
};
let res = await fetch(targetUrl, { headers, signal: AbortSignal.timeout(5000) }).catch(() => null);
if (!res || !res.ok) {
const proxiedUrl = `${CORS_PROXY}/?destination=${encodeURIComponent(targetUrl)}`;
res = await fetch(proxiedUrl, {
headers: {
'X-Referer': 'https://bcine.ru/',
'X-User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(5000)
}).catch(() => null);
}
if (res && res.ok) {
try {
const data = await res.json();
if (data && data.success !== false) {
const streamUrl = data.streams?.proxy_m3u8 || data.streams?.raw_m3u8 || data.streams?.m3u8 || data.streamUrl;
if (streamUrl && typeof streamUrl === 'string') {
const urlWithMark = streamUrl.includes('?') ? `${streamUrl}&${_vParam}` : `${streamUrl}?${_vParam}`;
allUrls.push({
url: urlWithMark,
server: `Bcine (${data.selectedSource || data.provider || server.id})`,
quality: 'Auto',
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://bcine.ru/',
'Origin': 'https://bcine.ru'
}
});
}
}
} catch { }
}
})
);
return allUrls.length ? { allUrls } : null;
}

View File

@@ -1,150 +0,0 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://dulo.mov';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
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}`, {
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 ? releaseDate.split('-')[0] : '';
const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90);
return { title, year, runtime };
} catch {
return null;
}
}
async function fetchSource(url, headers) {
try {
const res = await fetch(url, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const mediaType = isTv ? 'tv' : 'movie';
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || '';
const year = details?.year || '';
const runtime = details?.runtime || '';
const referer = isTv
? `${BASE_URL}/watch/tv/${id}/${s}/${e}`
: `${BASE_URL}/watch/movie/${id}`;
const defaultHeaders = {
'User-Agent': ua,
Referer: referer,
Origin: BASE_URL,
Accept: '*/*',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
};
const baseParams = new URLSearchParams({
tmdbId: String(id),
type: mediaType,
});
const additionalParams = new URLSearchParams({
tmdbId: String(id),
type: mediaType,
progressive: 'true',
});
if (title) additionalParams.set('title', title);
if (year) additionalParams.set('year', String(year));
if (runtime) additionalParams.set('runtime', String(runtime));
if (isTv) {
baseParams.set('season', String(s));
baseParams.set('episode', String(e));
additionalParams.set('season', String(s));
additionalParams.set('episode', String(e));
}
const endpoints = [
`${BASE_URL}/api/sources?${baseParams.toString()}`,
`${BASE_URL}/api/sources/willow?${baseParams.toString()}`,
`${BASE_URL}/api/sources/additional?${additionalParams.toString()}`,
];
const seenUrls = new Set();
const allUrls = [];
let pendingUrls = [...endpoints];
const maxPollTime = Date.now() + 16000;
while (pendingUrls.length > 0 && Date.now() < maxPollTime) {
const results = await Promise.all(
pendingUrls.map((url) => fetchSource(url, defaultHeaders))
);
const nextPending = [];
results.forEach((data, index) => {
if (!data) return;
if (Array.isArray(data.sources) && data.sources.length > 0) {
for (const src of data.sources) {
let streamUrl = String(src?.url || '').trim();
if (!streamUrl) continue;
if (streamUrl.startsWith('/')) {
streamUrl = `${BASE_URL}${streamUrl}`;
}
if (!streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
seenUrls.add(streamUrl);
allUrls.push({
url: streamUrl,
label: src.label || 'Dulo',
headers: {
'User-Agent': ua,
Referer: referer,
},
captions: Array.isArray(src.captions) ? src.captions : [],
});
}
}
if (data.pending) {
nextPending.push(pendingUrls[index]);
}
});
pendingUrls = nextPending;
if (pendingUrls.length > 0) {
await sleep(1500);
}
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -1,118 +0,0 @@
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
const BASE_URL = 'https://flaxmovies.xyz';
async function getWorkerUrls(embedUrl) {
const foundUrls = [];
try {
const res = await fetch(embedUrl, {
headers: {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
},
signal: AbortSignal.timeout(8000)
});
if (res.ok) {
const html = await res.text();
const directMatch = html.match(/WORKER_URL\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
if (directMatch && directMatch[1]) {
foundUrls.push(directMatch[1].replace(/\/+$/, ''));
}
const scriptMatches = [...html.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)];
for (const sm of scriptMatches) {
let scriptUrl = sm[1];
if (scriptUrl.startsWith('//')) scriptUrl = `https:${scriptUrl}`;
else if (scriptUrl.startsWith('/')) scriptUrl = `${BASE_URL}${scriptUrl}`;
else if (!scriptUrl.startsWith('http')) scriptUrl = `${BASE_URL}/${scriptUrl}`;
if (scriptUrl.includes('jsdelivr') || scriptUrl.includes('google') || scriptUrl.includes('cloudflare')) continue;
try {
const sRes = await fetch(scriptUrl, {
headers: { 'User-Agent': USER_AGENT, 'Referer': embedUrl },
signal: AbortSignal.timeout(6000)
});
if (sRes.ok) {
const sCode = await sRes.text();
const m = sCode.match(/WORKER_URL\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
if (m && m[1]) {
const wUrl = m[1].replace(/\/+$/, '');
if (!foundUrls.includes(wUrl)) foundUrls.push(wUrl);
}
}
} catch { }
}
}
} catch { }
const defaults = ['https://freakyniki.elaxo.lol', 'https://vidlove.nabilekson.workers.dev'];
for (const d of defaults) {
if (!foundUrls.includes(d)) foundUrls.push(d);
}
return foundUrls;
}
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const embedUrl = isTv
? `${BASE_URL}/embed/tv/${id}/${s}/${e}`
: `${BASE_URL}/embed/movie/${id}`;
const workerUrls = await getWorkerUrls(embedUrl);
const query = isTv
? `tmdb_id=${id}&tmdbId=${id}&season=${s}&episode=${e}`
: `tmdb_id=${id}&tmdbId=${id}`;
for (const worker of workerUrls) {
try {
const url = `${worker}/?${query}`;
const data = await fetchJson(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL,
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(12000)
});
if (data && Array.isArray(data.streams) && data.streams.length > 0) {
let allUrls = data.streams
.filter(st => st && typeof st.url === 'string')
.map(st => {
const isHls = (st.format || '').toLowerCase().includes('hls') || st.url.includes('.m3u8');
return {
url: st.url,
server: `FlaxMovies - ${st.provider || 'Default'}`,
quality: st.resolution || 'Auto',
type: isHls ? 'hls' : 'mp4',
headers: {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
}
};
});
if (server && server !== 'all') {
const clean = server.toLowerCase().replace('flaxmovies - ', '');
const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
if (filtered.length) allUrls = filtered;
}
if (allUrls.length) {
return { allUrls };
}
}
} catch { }
}
return null;
}
export async function getSources() {
return ['FlaxMovies - Airflix', 'FlaxMovies - Xpass-VIP 1'];
}

View File

@@ -1,48 +0,0 @@
import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const ORIGIN = 'https://www3.fsonline.app';
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(tmdbApiKey, id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) return null;
const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(info.titles[0] + (info.year ? ' ' + info.year : ''))}`, { headers: HEADERS });
const typeFolder = isTv ? 'seriale' : 'film';
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
if (!linkMatch) return null;
const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1];
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
if (!movieId) return null;
const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` });
const results = [];
let idx = 0;
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
const embedStart = idx + 9;
const embedEnd = ajaxHtml.indexOf('"', embedStart);
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
if (serverLabel.includes('filesun')) {
try {
const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } });
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } });
} catch { }
}
idx = spanEnd;
}
return results.length ? { allUrls: results } : null;
} catch { return null; }
}
export async function getSources(args) {
const res = await getStream(args);
return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : [];
}

View File

@@ -1,60 +0,0 @@
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
export const SKIP_VERIFY = true;
export const MULTI_URL = false;
const ENC_API = "https://enc-dec.app/api";
const BASE = "https://kisskh.do";
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; }
async function encKey(episodeId, type) {
try {
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
return data?.status === 200 ? data.result : null;
} catch { return null; }
}
export async function getStream({ id, s, e }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
if (!info) return null;
let drama = null, bestScore = -1;
for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) {
const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []);
const targetClean = cleanTitle(title);
for (const item of results) {
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
let score = 0;
if (parts.some(p => p === targetClean)) score += 5;
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
else continue;
if (score > bestScore) { bestScore = score; drama = item; }
}
if (drama && bestScore >= 5) break;
}
if (!drama) return null;
const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
if (!episodeId) return null;
const vidKey = await encKey(episodeId, 'vid');
if (!vidKey) return null;
const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
if (!videoData?.Video) return null;
let subtitles = [];
const subKey = await encKey(episodeId, 'sub');
if (subKey) {
try {
const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default }));
} catch { }
}
return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] };
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

View File

@@ -1,43 +0,0 @@
import { getTmdbInfo } from '../utils/helpers.js';
export async function getStream(args) {
const { id, s, e, tmdbApiKey } = args;
const type = (s != null && e != null) ? 'tv' : 'movie';
try {
const tmdbInfo = await getTmdbInfo(tmdbApiKey, id, type, s);
const name = tmdbInfo.titles[0] || '';
if (!name) return null;
if (type === 'tv') {
return null;
} else {
const url = `https://lmscript.xyz/v1/movies?filters[q]=${encodeURIComponent(name)}&expand=streams`;
const response = await fetch(url);
const data = await response.json();
const movie = data.items?.find(item => `${item.tmdb_prefix}` === `${id}` || `${item.tmdb_id}` === `${id}`);
if (!movie) return null;
const streams = movie.streams || {};
const SOURCE_ORDER = ["1080p", "720p", "480p", "360p"];
const sortedStreams = Object.keys(streams).sort((a, b) => SOURCE_ORDER.indexOf(a) - SOURCE_ORDER.indexOf(b));
if (!sortedStreams.length) return null;
const allUrls = sortedStreams.map(quality => {
const streamUrl = streams[quality];
return {
url: streamUrl,
quality,
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: {}
};
});
return { allUrls };
}
} catch (err) {
return null;
}
}

View File

@@ -1,80 +0,0 @@
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
const TMDB_BASE = 'https://api.themoviedb.org/3';
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
async function searchLookMovie(type, title, year, clientIP) {
for (const base of LM_DOMAINS) {
try {
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
const results = data?.result;
if (results?.length) {
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
if (match) return { match, base };
}
} catch { }
}
return null;
}
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
function getEpisodeIdFast(html, s, e) {
const sMatch = html.match(STORAGE_RE);
if (sMatch) {
const block = sMatch[1], sm = block.match(SEASONS_RE);
if (sm) {
try {
const seasons = JSON.parse(sm[1]);
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
if (season) {
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
if (ep) return String(ep.id_episode ?? ep.id);
}
} catch { }
}
}
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
for (let i = 1; i < parts.length; i += 2) {
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
}
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
return am ? am[1] : null;
}
export async function getStream({ id, s, e, sdk, clientIP }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const tmdbKey = tmdbApiKey;
if (!tmdbKey) return null;
const isTV = s != null && e != null;
const typeStr = isTV ? 'shows' : 'movies';
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
const title = tmdbData?.title || tmdbData?.name;
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
if (!title) return null;
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
if (!searchRes?.match?.slug) return null;
const { match, base } = searchRes;
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
const storageMatch = html.match(STORAGE_RE);
if (!storageMatch) return null;
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
if (!hashMatch || !expiresMatch) return null;
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
if (!streamId) return null;
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}

View File

@@ -1,117 +0,0 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://movienig.ht';
const PRIORITY_SERVERS = [
{ id: 'dallas', label: 'Dallas 4K' },
{ id: 'austin', label: 'Austin' },
{ id: 'helena', label: 'Helena' },
{ id: 'seattle', label: 'Seattle 4K' },
{ id: 'vixsrc-1', label: 'Newport Beach' },
{ id: 'tucson', label: 'Tucson' },
{ id: 'salem', label: 'Salem' },
];
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;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || String(id);
const year = details?.year;
const imdbId = details?.imdbId;
const encTitle = encodeURIComponent(title);
const yearQuery = year ? `&year=${year}` : '';
const imdbQuery = imdbId ? `&imdbId=${imdbId}` : '';
const seenUrls = new Set();
const allUrls = [];
const defaultHeaders = {
'User-Agent': ua,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
Accept: 'text/event-stream',
};
const tasks = PRIORITY_SERVERS.map(async (server) => {
try {
const url = isTv
? `${BASE_URL}/api/stream/v1/tv/${id}/${s}/${e}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`
: `${BASE_URL}/api/stream/v1/movie/${id}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`;
const res = await fetch(url, {
headers: defaultHeaders,
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return;
const bodyStr = await res.text();
if (bodyStr.includes('event: done')) {
const doneIdx = bodyStr.indexOf('event: done');
const dataIdx = bodyStr.indexOf('data: ', doneIdx);
if (dataIdx !== -1) {
const jsonStart = dataIdx + 6;
const jsonEnd = bodyStr.indexOf('\n', jsonStart);
const jsonText = (jsonEnd !== -1 ? bodyStr.substring(jsonStart, jsonEnd) : bodyStr.substring(jsonStart)).trim();
const data = JSON.parse(jsonText);
if (Array.isArray(data?.sources)) {
for (const src of data.sources) {
if (!src || typeof src !== 'object') continue;
const rawUrl = String(src.url || '').trim();
if (!rawUrl || seenUrls.has(rawUrl)) continue;
seenUrls.add(rawUrl);
const quality = src.quality ? String(src.quality) : 'Auto';
const titleQuality = quality !== 'Auto' ? ` (${quality})` : '';
allUrls.push({
url: rawUrl,
label: `MovieNight ${server.label}${titleQuality}`,
headers: {
'User-Agent': ua,
Referer: `${BASE_URL}/`,
},
});
}
}
}
}
} catch { }
});
await Promise.all(tasks);
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -1,282 +0,0 @@
import { getUA } from '../utils/helpers.js';
const API_BASE = 'https://api.wecollege.net';
const REFERER = 'https://www.movy.bz/';
const ORIGIN = 'https://www.movy.bz';
const MAGIC = [109, 118, 109, 49];
const SERVERS = [
{ endpoint: 'miami', name: 'Miami', note: 'Original audio (Up to 4K)' },
{ endpoint: 'seattle', name: 'Seattle', note: 'Original audio' },
{ endpoint: 'denver', name: 'Denver', note: 'Original audio' },
{ endpoint: 'chicago', name: 'Chicago', note: 'Original audio' },
{ endpoint: 'dallas', name: 'Dallas', note: 'Original audio' },
{ endpoint: 'atlanta', name: 'Atlanta', note: 'Original audio' },
{ endpoint: 'houston', name: 'Houston', note: 'Original audio' },
{ endpoint: 'austin', name: 'Austin', note: 'Original audio' },
{ endpoint: 'boston', name: 'Boston', note: 'Original audio' },
{ endpoint: 'munich', name: 'Munich', note: 'German audio', extra: 'language=german' },
{ endpoint: 'berlin', name: 'Berlin', note: 'German audio' },
{ endpoint: 'paris', name: 'Paris', note: 'French audio' },
{ endpoint: 'delhi', name: 'Delhi', note: 'Hindi audio' },
{ endpoint: 'cancun', name: 'Cancun', note: 'Spanish audio' },
];
const seedCache = new Map();
function _l(e) {
let v = e >>> 0;
v = (v ^ (v >>> 16)) >>> 0;
v = Math.imul(v, 0x85ebca6b) >>> 0;
v = (v ^ (v >>> 13)) >>> 0;
v = Math.imul(v, 0xc2b2ae35) >>> 0;
return (v ^ (v >>> 16)) >>> 0;
}
function _u(e, t) {
const shift = t & 31;
if (shift === 0) return e >>> 0;
return (((e << shift) >>> 0) | (e >>> (32 - shift))) >>> 0;
}
function _fnv1a(str) {
let t = 0x811c9dc5 >>> 0;
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
t = Math.imul((t ^ code) >>> 0, 0x1000193) >>> 0;
}
return _l(t);
}
function _initKeyState(seed, tmdbId) {
const s = new Array(61).fill(0);
const isSet = new Array(61).fill(false);
const idVal = ((tmdbId >>> 0) ^ 0x9e3779b9) >>> 0;
let r = _l((_fnv1a(seed) ^ _l(idVal)) >>> 0);
for (let e = 0; e < 8; e++) {
const t = r % 61;
r = _u((r + 0x9e3779b9) >>> 0, 7 + (7 & e));
s[t] = (r ^ _l(r)) >>> 0;
isSet[t] = true;
r = _l((r + t) >>> 0);
}
const acc = _l((0xa5a5a5a5 ^ r) >>> 0);
return { s, isSet, acc };
}
function _nextKeystreamWord(state, t) {
const r = state.s;
let nState = state.acc;
const i = nState % 61;
const d = state.isSet[i] ? r[i] : 0;
const c = Math.imul((t + 1) >>> 0, 0x9e3779b9) >>> 0;
const a = nState;
const sVal = (d ^ c) >>> 0;
const h = state.isSet[i] ? ((a | sVal) >>> 0) : ((a ^ sVal) >>> 0);
const term1 = _u((h + nState) >>> 0, 31 & i);
const term2 = _u(nState, 31 & (i * 7));
nState = _l((((term1 ^ term2) >>> 0) + 0x9e3779b9) >>> 0);
r[i] = nState;
state.isSet[i] = true;
state.acc = nState;
return nState >>> 0;
}
function _generateKeyStream(seed, tmdbId, len) {
const state = _initKeyState(seed, tmdbId);
const out = new Uint8Array(len);
let wordIdx = 0;
let byteIdx = 0;
while (byteIdx < len) {
const word = _nextKeystreamWord(state, wordIdx++);
out[byteIdx++] = word & 0xFF;
if (byteIdx < len) out[byteIdx++] = (word >>> 8) & 0xFF;
if (byteIdx < len) out[byteIdx++] = (word >>> 16) & 0xFF;
if (byteIdx < len) out[byteIdx++] = (word >>> 24) & 0xFF;
}
return out;
}
function decrypt(cipherB64, seed, tmdbId) {
try {
let normalized = cipherB64.replace(/-/g, '+').replace(/_/g, '/');
while (normalized.length % 4 !== 0) normalized += '=';
const cipherBytes = Buffer.from(normalized, 'base64');
if (cipherBytes.length <= MAGIC.length) return null;
const ks = _generateKeyStream(seed, tmdbId, cipherBytes.length);
const decrypted = new Uint8Array(cipherBytes.length);
for (let i = 0; i < cipherBytes.length; i++) {
decrypted[i] = cipherBytes[i] ^ ks[i];
}
for (let k = 0; k < MAGIC.length; k++) {
if (decrypted[k] !== MAGIC[k]) return null;
}
return Buffer.from(decrypted.subarray(MAGIC.length)).toString('utf8');
} catch {
return null;
}
}
async function getSeed(tmdbId, ua) {
const now = Date.now();
const cached = seedCache.get(tmdbId);
if (cached && cached.expiresAt > now + 5000) {
return cached.seed;
}
try {
const res = await fetch(`${API_BASE}/seed?mediaId=${tmdbId}`, {
headers: {
'User-Agent': ua,
Referer: REFERER,
Origin: ORIGIN,
},
signal: AbortSignal.timeout(8000),
});
if (res.ok) {
const data = await res.json();
const seed = data?.seed ? String(data.seed) : '';
const ttlMs = typeof data?.ttlMs === 'number' ? data.ttlMs : 30000;
if (seed) {
seedCache.set(tmdbId, { seed, expiresAt: now + ttlMs });
return seed;
}
}
} catch { }
return null;
}
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;
}
}
function formatQuality(raw) {
const lower = String(raw).toLowerCase();
if (lower.includes('2160') || lower.includes('4k')) return '4K';
if (lower.includes('1080')) return '1080p';
if (lower.includes('720')) return '720p';
if (lower.includes('480')) return '480p';
if (lower.includes('360')) return '360p';
if (raw) return String(raw);
return 'Auto';
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const mediaType = isTv ? 'tv' : 'movie';
const tmdbId = parseInt(id, 10);
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || String(id);
const year = details?.year;
const imdbId = details?.imdbId;
const seed = await getSeed(tmdbId, ua);
if (!seed) return null;
const params = new URLSearchParams({
title,
mediaType,
tmdbId: String(tmdbId),
enc: '2',
seed,
});
if (year) params.set('year', String(year));
if (isTv) {
if (s) params.set('seasonId', String(s));
if (e) params.set('episodeId', String(e));
}
if (imdbId) params.set('imdbId', imdbId);
const baseQuery = params.toString();
const seenUrls = new Set();
const allUrls = [];
const defaultHeaders = {
'User-Agent': ua,
Referer: REFERER,
Origin: ORIGIN,
};
const tasks = SERVERS.map(async (server) => {
let fullUrl = `${API_BASE}/${server.endpoint}/sources?${baseQuery}`;
if (server.extra) fullUrl += `&${server.extra}`;
try {
const res = await fetch(fullUrl, {
headers: defaultHeaders,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return;
const encText = (await res.text()).trim();
if (!encText || encText.startsWith('<')) return;
const decJsonStr = decrypt(encText, seed, tmdbId);
if (!decJsonStr) return;
const parsed = JSON.parse(decJsonStr);
if (!Array.isArray(parsed?.sources)) return;
for (const src of parsed.sources) {
if (!src || typeof src !== 'object') continue;
const streamUrl = String(src.url || '').trim();
if (!streamUrl || seenUrls.has(streamUrl)) continue;
seenUrls.add(streamUrl);
const cleanQuality = formatQuality(src.quality || 'Auto');
const isSpecificAudio = server.note.toLowerCase().includes('audio') && !server.note.toLowerCase().includes('original');
const langLabel = isSpecificAudio ? server.note.split(' ')[0] : '';
const label = isSpecificAudio
? `${server.name} (${langLabel}) · ${cleanQuality}`
: `${server.name} · ${cleanQuality}`;
allUrls.push({
url: streamUrl,
label,
headers: {
'User-Agent': ua,
Referer: REFERER,
},
});
}
} catch { }
});
await Promise.all(tasks);
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -1,45 +0,0 @@
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
if (!key) return null;
try {
const url = s != null && e != null
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
const res = await fetchJson(url);
return res?.imdb_id || null;
} catch { return null; }
}
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
const manifest = sdk?.penguManifest || null;
if (!tmdbApiKey || !manifest || (manifest && !manifest.endsWith('manifest.json'))) return null;
const BASE_URL = manifest ? manifest.split('/manifest.json')[0] : null;
if (!BASE_URL) return null;
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;
const path = s != null && e != null
? `/stream/series/${imdbId}:${s}:${e}.json`
: `/stream/movie/${imdbId}.json`;
const res = await fetchJson(`${BASE_URL}${path}`);
if (!res?.streams?.length) return null;
const allUrls = res.streams.map(stream => {
if (!stream.url) return null;
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
return {
url: stream.url,
server: `Pengu - ${stream.name || 'Server'}`,
headers: Object.keys(headers).length > 0 ? headers : undefined,
skipProxy: stream.behaviorHints?.notWebReady === false
};
}).filter(Boolean);
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}

View File

@@ -1,68 +0,0 @@
import { USER_AGENT } from '../utils/helpers.js';
async function safeImport(moduleName) {
try {
return await import(`./${moduleName}.js`);
} catch {
return null;
}
}
export async function getStream(args) {
const { id, s, e, clientIP, sdk } = args;
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
const allUrls = [];
const providers = [
{ name: 'CineSu', key: 'cinesu', fnName: 'fetchCineSuStreams' },
{ name: 'ZxcStream', key: 'zxcstream', fnName: 'fetchZxcStreams' },
{ name: 'VidFast', key: 'vidfast', fnName: 'fetchVidFastStreams' },
{ name: '1Embed', key: '1embed', fnName: 'fetchOneEmbedStreams' },
];
await Promise.all(
providers.map(async ({ name, key, fnName }) => {
try {
let streamResult = null;
if (sdk) {
try {
streamResult = await sdk.getStream(key, id, s, e, clientIP);
} catch { }
}
if (!streamResult) {
const mod = await safeImport(key);
if (mod) {
if (typeof mod.getStream === 'function') {
streamResult = await mod.getStream(args);
} else if (typeof mod[fnName] === 'function') {
const raw = await mod[fnName]({ type, id, season: s, episode: e });
if (Array.isArray(raw)) {
streamResult = { allUrls: raw };
}
}
}
}
if (!streamResult) return;
const streams = streamResult.allUrls || (streamResult.url ? [streamResult] : []);
for (const item of streams) {
allUrls.push({
url: item.url,
server: `FlyStream (${item.source || item.server || name})`,
quality: item.quality || 'Auto',
type: item.type || item.format || (item.url?.includes('.m3u8') ? 'hls' : 'mp4'),
headers: item.headers || {
'User-Agent': USER_AGENT,
},
});
}
} catch { }
})
);
return allUrls.length ? { allUrls } : null;
}

View File

@@ -1,104 +0,0 @@
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const DOMAIN = 'https://purstream.ad';
const API_BASE = 'https://api.purstream.ad/api/v1';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Referer': `${DOMAIN}/`,
'Origin': DOMAIN,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-site'
};
export async function getStream({ id, s, e }) {
try {
const isTv = s != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) {
return null;
}
const title = info.titles[0];
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
const searchData = await fetchJson(searchUrl, {
headers: HEADERS,
signal: AbortSignal.timeout(10000)
});
const items = searchData?.data?.items?.movies?.items || [];
const type = isTv ? 'tv' : 'movie';
const lowerTitle = title.toLowerCase();
let match = items.find(item =>
item.type === type &&
item.title?.toLowerCase() === lowerTitle &&
(!info.year || item.release_date?.startsWith(String(info.year)))
);
if (!match) {
match = items.find(item => item.type === type);
}
if (!match?.id) {
return null;
}
const streamUrl = isTv
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
: `${API_BASE}/stream/${match.id}`;
const json = await fetchJson(streamUrl, {
headers: HEADERS,
signal: AbortSignal.timeout(10000)
});
const sources = json?.data?.items?.sources;
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
return null;
}
const chosen = sources.find(src => src.stream_url);
if (!chosen?.stream_url) {
return null;
}
const result = {
url: chosen.stream_url,
headers: {
'User-Agent': USER_AGENT,
'Referer': `${DOMAIN}/`
},
server: 'purstream',
quality: chosen.source_name || 'Auto',
type: chosen.format || 'hls',
skipProxy: true
};
return result;
} catch (err) {
return null;
}
}
export async function getSources(args) {
const stream = await getStream(args);
return stream ? [stream] : [];
}
export const VERIFY_HEADERS = {
...HEADERS
};

View File

@@ -1,177 +0,0 @@
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
const C_ARRAY = [
'4Z7lUo', 'gwIVSMD', 'PLmz2elE2v', 'Z4OFV0', 'SZ6RZq6Zc', 'zhJEFYxrz8', 'FOm7b0', 'axHS3q4KDq', 'o9zuXQ', '4Aebt',
'wgjjWwKKx', 'rY4VIxqSN', 'kfjbnSo', '2DyrFA1M', 'YUixDM9B', 'JQvgEj0', 'mcuFx6JIek', 'eoTKe26gL', 'qaI9EVO1rB', '0xl33btZL',
'1fszuAU', 'a7jnHzst6P', 'wQuJkX', 'cBNhTJlEOf', 'KNcFWhDvgT', 'XipDGjST', 'PCZJlbHoyt', '2AYnMZkqd', 'HIpJh', 'KH0C3iztrG',
'W81hjts92', 'rJhAT', 'NON7LKoMQ', 'NMdY3nsKzI', 't4En5v', 'Qq5cOQ9H', 'Y9nwrp', 'VX5FYVfsf', 'cE5SJG', 'x1vj1',
'HegbLe', 'zJ3nmt4OA', 'gt7rxW57dq', 'clIE9b', 'jyJ9g', 'B5jXjMCSx', 'cOzZBZTV', 'FTXGy', 'Dfh1q1', 'ny9jqZ2POI',
'X2NnMn', 'MBtoyD', 'qz4Ilys7wB', '68lbOMye', '3YUJnmxp', '1fv5Imona', 'PlfvvXD7mA', 'ZarKfHCaPR', 'owORnX', 'dQP1YU',
'dVdkx', 'qgiK0E', 'cx9wQ', '5F9bGa', '7UjkKrp', 'Yvhrj', 'wYXez5Dg3', 'pG4GMU', 'MwMAu', 'rFRD5wlM'
];
const DEFAULT_SERVICES = [
'apex', 'pulse', 'solstice', 'quasar', 'horizon', 'primevids',
'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru'
];
function generateRiveSecretKey(e) {
if (e === undefined || e === null) return 'rive';
try {
let t, n;
const r = String(e);
if (isNaN(Number(e))) {
const sum = r.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
t = C_ARRAY[sum % C_ARRAY.length] || Buffer.from(r).toString('base64');
n = Math.floor((sum % r.length) / 2);
} else {
const i = Number(e);
t = C_ARRAY[i % C_ARRAY.length] || Buffer.from(r).toString('base64');
n = Math.floor((i % r.length) / 2);
}
const i = r.slice(0, n) + t + r.slice(n);
const o = (function (str) {
const t = String(str);
let n = 3735928559 ^ t.length;
for (let e = 0; e < t.length; e++) {
const r = t.charCodeAt(e);
const rXor = (131 * e + 89 ^ (r << (e % 5))) & 255;
const r2 = r ^ rXor;
n = (((n << 7) | (n >>> 25)) >>> 0) ^ r2;
const idx = (65535 & n) * 60205;
const o2 = ((n >>> 16) * 60205) << 16;
n = (idx + o2) >>> 0;
n ^= n >>> 11;
}
n ^= n >>> 15;
n = ((65535 & n) * 49842 + (((n >>> 16) * 49842) << 16)) >>> 0;
n ^= n >>> 13;
n = ((65535 & n) * 40503 + (((n >>> 16) * 40503) << 16)) >>> 0;
n ^= n >>> 16;
n = ((65535 & n) * 10196 + (((n >>> 16) * 10196) << 16)) >>> 0;
return ((n ^= n >>> 15)).toString(16).padStart(8, '0');
})(
(function (str) {
const s = String(str);
let t = 0;
for (let n = 0; n < s.length; n++) {
const r = s.charCodeAt(n);
const i = (((t = (r + (t << 6) + (t << 16) - t) >>> 0) << (n % 5)) | (t >>> (32 - (n % 5)))) >>> 0;
t ^= (i ^ ((r << (n % 7)) | (r >>> (8 - (n % 7))))) >>> 0;
t = (t + ((t >>> 11) ^ (t << 3))) >>> 0;
}
t ^= t >>> 15;
t = ((65535 & t) * 49842 + ((((t >>> 16) * 49842) & 65535) << 16)) >>> 0;
t ^= t >>> 13;
t = ((65535 & t) * 40503 + ((((t >>> 16) * 40503) & 65535) << 16)) >>> 0;
return ((t ^= t >>> 16)).toString(16).padStart(8, '0');
})(i)
);
return Buffer.from(o, 'utf-8').toString('base64');
} catch {
return 'topSecret';
}
}
async function fetchRiveServices() {
try {
const secretKey = generateRiveSecretKey();
const url = `https://www.rivestream.app/api/backendfetch?requestID=VideoProviderServices&secretKey=${encodeURIComponent(secretKey)}`;
const res = await fetchJson(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://www.rivestream.app/',
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(6000)
});
return Array.isArray(res?.data) && res.data.length ? res.data : DEFAULT_SERVICES;
} catch {
return DEFAULT_SERVICES;
}
}
async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, referer) {
try {
const cb = (service === 'primevids' || service === 'citadel')
? `&cb=${Math.floor(Date.now() / 3e6)}`
: '';
const reqID = isMovie ? 'movieVideoProvider' : 'tvVideoProvider';
const params = isMovie
? `requestID=${reqID}&id=${encodeURIComponent(id)}&service=${service}${cb}&secretKey=${encodeURIComponent(secretKey)}`
: `requestID=${reqID}&id=${encodeURIComponent(id)}&season=${encodeURIComponent(s)}&episode=${encodeURIComponent(e)}&service=${service}${cb}&secretKey=${encodeURIComponent(secretKey)}`;
const url = `https://www.rivestream.app/api/backendfetch?${params}`;
const res = await fetchJson(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': referer,
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(10000)
});
if (!res?.data?.sources || !Array.isArray(res.data.sources)) return [];
const streams = [];
for (const item of res.data.sources) {
if (!item || !item.url) continue;
const format = String(item.format || '').toLowerCase();
const urlStr = String(item.url || '').toLowerCase();
const isM3U8 = format === 'hls' || format === 'm3u8' || urlStr.includes('.m3u8') || urlStr.includes('.txt');
streams.push({
url: item.url,
server: `Rive (${item.source || service})`,
quality: String(item.quality || 'Auto'),
type: isM3U8 ? 'hls' : 'mp4',
headers: {
'User-Agent': USER_AGENT,
'Referer': referer
}
});
}
return streams;
} catch {
return [];
}
}
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const isMovie = !isTv;
const secretKey = generateRiveSecretKey(id);
const referer = isMovie
? `https://www.rivestream.app/watch?type=movie&id=${id}`
: `https://www.rivestream.app/watch?type=tv&id=${id}&season=${s}&episode=${e}`;
let services = await fetchRiveServices();
if (server && server !== 'all') {
const clean = server.toLowerCase().replace('rive (', '').replace(')', '').replace('rive', '').trim();
services = services.filter(srv => srv.toLowerCase().includes(clean));
if (!services.length) services = await fetchRiveServices();
}
const settled = await Promise.allSettled(
services.map(srv => fetchServiceStreams(srv, isMovie, id, s, e, secretKey, referer))
);
const allUrls = [];
for (const r of settled) {
if (r.status === 'fulfilled' && Array.isArray(r.value)) {
allUrls.push(...r.value);
}
}
return allUrls.length ? { allUrls } : null;
}
export async function getSources() {
const services = await fetchRiveServices();
return services.map(s => `Rive (${s})`);
}

View File

@@ -1,129 +0,0 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://streamaggregator.in';
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}`, {
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 ? releaseDate.split('-')[0] : null;
return { title, year };
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const mediaType = isTv ? 'tv' : 'movie';
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || String(id);
const year = details?.year || '';
const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+');
const params = new URLSearchParams({
type: mediaType,
stream: 'true',
});
if (year) params.set('year', String(year));
if (isTv) {
params.set('season', String(s));
params.set('episode', String(e));
}
const scrapeUrl = `${BASE_URL}/api/scrape?title=${formattedTitle}&${params.toString()}`;
const res = await fetch(scrapeUrl, {
headers: {
'User-Agent': ua,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
Accept: 'text/event-stream, application/x-ndjson, text/plain, */*',
},
signal: AbortSignal.timeout(90000),
});
if (!res.ok) return null;
const lines = [];
if (res.body?.getReader) {
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n');
buffer = parts.pop() || '';
for (const part of parts) {
const trimmed = part.trim();
if (trimmed) lines.push(trimmed);
}
}
} catch { }
if (buffer.trim()) lines.push(buffer.trim());
} else {
const text = await res.text();
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (trimmed) lines.push(trimmed);
}
}
const seenUrls = new Set();
const allUrls = [];
for (const line of lines) {
try {
const raw = line.startsWith('data:') ? line.slice(5).trim() : line;
if (!raw || raw === '[DONE]') continue;
const item = JSON.parse(raw);
const streamUrl = String(item?.url || '').trim();
if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
seenUrls.add(streamUrl);
const serverName = item.server || item.providerKey || 'Stream';
const quality = item.quality ? ` · ${item.quality}` : '';
const size = item.size ? ` (${item.size})` : '';
const label = `${serverName}${quality}${size}`;
const headers = {
'User-Agent': ua,
...(item.headers && typeof item.headers === 'object' ? item.headers : {}),
};
if (item.referer) {
headers.Referer = item.referer;
}
allUrls.push({
url: streamUrl,
label,
headers,
});
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -1,115 +0,0 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
export const VERIFY_HEADERS = {
'User-Agent': USER_AGENT,
'Referer': `${EMBED_ORIGIN}/`,
'Origin': EMBED_ORIGIN
};
function extractStreamUrls(obj, urls = new Set()) {
if (!obj) return urls;
if (typeof obj === 'string') {
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
urls.add(obj);
}
} else if (Array.isArray(obj)) {
for (const item of obj) extractStreamUrls(item, urls);
} else if (typeof obj === 'object') {
for (const val of Object.values(obj)) {
extractStreamUrls(val, urls);
}
}
return urls;
}
export async function getStream({ id, s, e }) {
try {
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
const data = await fetchJson(url, {
headers: {
'Accept': '*/*',
'Origin': EMBED_ORIGIN,
'Referer': `${EMBED_ORIGIN}/`,
'User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(10000)
});
const streamUrls = Array.from(extractStreamUrls(data));
if (!streamUrls.length) return null;
let subs = [];
if (data.subtitles && Array.isArray(data.subtitles)) {
subs = data.subtitles;
} else if (data.captions && Array.isArray(data.captions)) {
subs = data.captions;
}
const subtitles = subs.map(sub => ({
url: sub.url || sub.file || sub.src,
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown'
})).filter(s => s.url);
const allUrls = streamUrls.map(streamUrl => {
let type = 'mp4';
if (streamUrl.includes('.mpd')) type = 'dash';
else if (streamUrl.includes('.m3u8')) type = 'hls';
return {
url: streamUrl,
server: 'VidAPI',
quality: 'Auto',
type,
headers: VERIFY_HEADERS,
subtitles: subtitles.length > 0 ? subtitles : undefined,
skipProxy: false
};
});
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}
export async function getSources() {
return ['VidAPI'];
}
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
try {
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
if (!upstream) return res.end();
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
if (isM3u8) {
const chunks = [];
for await (const c of upstream) chunks.push(c);
let body = Buffer.concat(chunks).toString('utf8');
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Access-Control-Allow-Origin', '*');
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
}
res.setHeader('Content-Type', ct || 'application/octet-stream');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'public, max-age=3600');
upstream.pipe(res);
} catch {
if (!res.headersSent) {
res.statusCode = 502;
res.end();
}
}
}

View File

@@ -1,123 +0,0 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://vidcore.io';
const API = 'https://enc-dec.app/api';
export async function getStream({ id, s, e }) {
const isTv = Boolean(s && e);
const targetUrl = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}/`
: `${BASE_URL}/movie/${id}`;
const ua = getUA();
const baseHeaders = {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
};
try {
const pageRes = await fetch(targetUrl, {
headers: { 'User-Agent': ua },
signal: AbortSignal.timeout(10000),
});
if (!pageRes.ok) return null;
const html = await pageRes.text();
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
if (!match || !match[1]) return null;
const enToken = match[1];
const s1Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(enToken)}&stage=1`, {
signal: AbortSignal.timeout(8000),
});
const s1Data = await s1Res.json();
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
const token1 = s1Data.result.token;
const stage1Url = s1Data.result.stage1;
const post1Res = await fetch(stage1Url, {
method: 'POST',
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
signal: AbortSignal.timeout(10000),
});
if (!post1Res.ok) return null;
const stage1Text = await post1Res.text();
const s2Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(stage1Text)}&stage=2`, {
signal: AbortSignal.timeout(8000),
});
const s2Data = await s2Res.json();
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
const serversRes = await fetch(serversUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(10000),
});
if (!serversRes.ok) return null;
const serversEncrypted = await serversRes.text();
const decServersRes = await fetch(`${API}/dec-vidcore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted }),
signal: AbortSignal.timeout(8000),
});
const decServersData = await decServersRes.json();
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
const allUrls = [];
for (const server of decServersData.result) {
if (!server?.data) continue;
try {
const streamUrl = `${streamBaseUrl}/${server.data}`;
const streamEncRes = await fetch(streamUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(8000),
});
if (!streamEncRes.ok) continue;
const streamEncText = await streamEncRes.text();
const decStreamRes = await fetch(`${API}/dec-vidcore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncText }),
signal: AbortSignal.timeout(8000),
});
const decStreamData = await decStreamRes.json();
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
const streamResult = decStreamData.result;
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
allUrls.push({
url: resultUrl,
label: server.name || server.title || 'VidCore',
headers: {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
},
});
}
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
} catch {
return null;
}
}

View File

@@ -1,123 +0,0 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://vidfast.vc';
const API = 'https://enc-dec.app/api';
export async function getStream({ id, s, e }) {
const isTv = Boolean(s && e);
const targetUrl = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}/`
: `${BASE_URL}/movie/${id}`;
const ua = getUA();
const baseHeaders = {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
};
try {
const pageRes = await fetch(targetUrl, {
headers: { 'User-Agent': ua },
signal: AbortSignal.timeout(10000),
});
if (!pageRes.ok) return null;
const html = await pageRes.text();
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
if (!match || !match[1]) return null;
const enToken = match[1];
const s1Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(enToken)}&stage=1`, {
signal: AbortSignal.timeout(8000),
});
const s1Data = await s1Res.json();
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
const token1 = s1Data.result.token;
const stage1Url = s1Data.result.stage1;
const post1Res = await fetch(stage1Url, {
method: 'POST',
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
signal: AbortSignal.timeout(10000),
});
if (!post1Res.ok) return null;
const stage1Text = await post1Res.text();
const s2Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(stage1Text)}&stage=2`, {
signal: AbortSignal.timeout(8000),
});
const s2Data = await s2Res.json();
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
const serversRes = await fetch(serversUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(10000),
});
if (!serversRes.ok) return null;
const serversEncrypted = await serversRes.text();
const decServersRes = await fetch(`${API}/dec-vidfast`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted }),
signal: AbortSignal.timeout(8000),
});
const decServersData = await decServersRes.json();
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
const allUrls = [];
for (const server of decServersData.result) {
if (!server?.data) continue;
try {
const streamUrl = `${streamBaseUrl}/${server.data}`;
const streamEncRes = await fetch(streamUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(8000),
});
if (!streamEncRes.ok) continue;
const streamEncText = await streamEncRes.text();
const decStreamRes = await fetch(`${API}/dec-vidfast`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncText }),
signal: AbortSignal.timeout(8000),
});
const decStreamData = await decStreamRes.json();
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
const streamResult = decStreamData.result;
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
allUrls.push({
url: resultUrl,
label: server.name || server.title || 'VidFast',
headers: {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
},
});
}
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
} catch {
return null;
}
}

View File

@@ -1,147 +0,0 @@
import { USER_AGENT } from '../utils/helpers.js';
const REDIS_URL = 'https://vidnest-redis-fell-prism-rest.cloud.layerbase.dev/';
const REDIS_AUTH = 'Bearer ve8z9XSKatu74M7FjLU8eQ29';
const ORIGIN = 'https://vidgod.space';
const SERVERS = [
{
name: 'Pulsar',
key: 'pulsar',
base: 'https://vidnest-extractor.vividdubbing.workers.dev/api',
serverParam: 'prime'
},
{
name: 'Orion',
key: 'orion',
base: 'https://404-vidnest.lofiserver.workers.dev/api',
serverParam: 'gama'
},
{
name: 'Stellar',
key: 'stellar',
base: 'https://404-vidnest.lofiserver.workers.dev/api',
serverParam: 'sigma'
}
];
function buildCacheKey(serverKey, type, id, s, e) {
const parts = [serverKey, type, id];
if (type === 'tv') {
parts.push(s || '1', e || '1');
}
return parts.join(':');
}
async function getFromCache(key) {
try {
const res = await fetch(REDIS_URL, {
method: 'POST',
headers: {
'Authorization': REDIS_AUTH,
'Content-Type': 'application/json',
'Accept': '*/*',
'Origin': ORIGIN,
'Referer': `${ORIGIN}/`,
'User-Agent': USER_AGENT
},
body: JSON.stringify(['GET', key]),
signal: AbortSignal.timeout(6000)
});
if (!res.ok) return null;
const data = await res.json();
if (!data || !data.result) return null;
const parsed = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
return parsed?.streams?.length ? parsed : null;
} catch {
return null;
}
}
async function fetchFromWorker(srv, type, id, s, e) {
try {
const endpoint = type === 'movie'
? `${srv.base}/movie/${id}?server=${srv.serverParam}`
: `${srv.base}/tv/${id}/${s || '1'}/${e || '1'}?server=${srv.serverParam}`;
const res = await fetch(endpoint, {
headers: {
'Accept': 'application/json',
'Origin': ORIGIN,
'Referer': `${ORIGIN}/`,
'User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(8000)
});
if (!res.ok) return null;
const data = await res.json();
return data?.streams?.length ? data : null;
} catch {
return null;
}
}
async function resolveServer(srv, type, id, s, e) {
const cacheKey = buildCacheKey(srv.key, type, id, s, e);
const cached = await getFromCache(cacheKey);
if (cached) return { server: srv.name, data: cached };
const live = await fetchFromWorker(srv, type, id, s, e);
if (live) return { server: srv.name, data: live };
return null;
}
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
let targets = SERVERS;
if (server && server !== 'all') {
const clean = server.toLowerCase().trim();
targets = SERVERS.filter(srv => srv.name.toLowerCase() === clean || srv.key.toLowerCase() === clean);
if (!targets.length) targets = SERVERS;
}
const settled = await Promise.allSettled(
targets.map(srv => resolveServer(srv, type, id, s, e))
);
const allUrls = [];
for (const r of settled) {
if (r.status !== 'fulfilled' || !r.value) continue;
const { server: serverName, data } = r.value;
for (const item of data.streams || []) {
if (!item || !item.url) continue;
const typeStr = String(item.type || '').toLowerCase();
const urlStr = String(item.url || '').toLowerCase();
const isM3U8 =
typeStr === 'hls' ||
typeStr === 'application/vnd.apple.mpegurl' ||
typeStr === 'cloudflare' ||
typeStr === 'tiktok' ||
urlStr.includes('.m3u8') ||
urlStr.includes('.txt');
allUrls.push({
url: item.url,
server: serverName,
quality: item.quality || 'Auto',
type: isM3U8 ? 'hls' : 'mp4',
headers: item.headers || {
'User-Agent': USER_AGENT,
'Referer': `${ORIGIN}/`
}
});
}
}
return allUrls.length ? { allUrls } : null;
}

View File

@@ -1,28 +0,0 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const BASE = "https://vidlink.pro";
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
export async function getStream({ id, s, e }) {
try {
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
if (encData?.status !== 200 || !encData?.result) return null;
const data = await fetchJson(s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
const stream = data?.stream;
if (!stream) return null;
if (stream.type === 'file' && stream.qualities) {
let picked = null;
for (const q of ['1080', '720', '480', '360']) if (stream.qualities[q]?.url) { picked = stream.qualities[q].url; break; }
if (!picked) picked = stream.qualities[Object.keys(stream.qualities)[0]]?.url;
if (picked) return { allUrls: [{ url: picked, type: 'mp4', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
} else if (stream.playlist) {
return { allUrls: [{ url: stream.playlist, type: 'hls', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
}
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))] : [];
}

View File

@@ -1,79 +0,0 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vidnest.fun';
const API_BASE_URL = 'https://new.vidnest.fun';
const REQUEST_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/`, 'Origin': BASE_URL };
const CDN_PROXY_HEADERS = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.5', 'origin': 'https://megaplay.buzz', 'referer': 'https://megaplay.buzz/' };
export const CDN_HEADERS = [
{ pattern: /letsgocdn\d+\.shop/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } },
{ pattern: /cdn\.mewstream\.buzz/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } }
];
const ALPHABET = 'RB0fpH8ZEyVLkv7c2i6MAJ5u3IKFDxlS1NTsnGaqmXYdUrtzjwObCgQP94hoeW+/=';
const REV_MAP = new Uint8Array(256);
for (let i = 0; i < ALPHABET.length; i++) REV_MAP[ALPHABET.charCodeAt(i)] = i;
function decrypt(payload) {
const len = payload.length;
const bytes = new Uint8Array(len);
let j = 0;
for (let i = 0; i < len; i += 4) {
const c0 = REV_MAP[payload.charCodeAt(i)] ?? 64;
const c1 = REV_MAP[payload.charCodeAt(i + 1)] ?? 64;
const c2 = REV_MAP[payload.charCodeAt(i + 2)] ?? 64;
const c3 = REV_MAP[payload.charCodeAt(i + 3)] ?? 64;
bytes[j++] = (c0 << 2) | (c1 >> 4);
if (c2 !== 64) bytes[j++] = ((c1 & 15) << 4) | (c2 >> 2);
if (c3 !== 64) bytes[j++] = ((c2 & 3) << 6) | c3;
}
return JSON.parse(new TextDecoder().decode(bytes.subarray(0, j)));
}
async function tmdbToAnilist(tmdbId, season) {
try {
const data = await fetchJson(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=tv&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
return data?.mappings?.[0]?.anilist_id || null;
} catch { return null; }
}
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
export async function getStream({ id, s, e, sdk, audio }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
if (s) {
const k = tmdbApiKey;
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;
if (showData?.genres?.some(g => g.id === 16)) {
const anilistId = await tmdbToAnilist(id, s);
if (anilistId) {
try {
const json = await fetchJson(`${API_BASE_URL}/hianime/anime/${anilistId}/${e || 1}/${audio === 'dub' ? 'dub' : 'sub'}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(15000) });
const data = json.encrypted ? decrypt(json.data) : json.data;
const file = data?.sources?.[0]?.file;
if (file) return { url: `https://megacloud.animanga.fun/proxy?url=${encodeURIComponent(file)}&headers=${encodeURIComponent(JSON.stringify(CDN_PROXY_HEADERS))}`, headers: REQUEST_HEADERS };
} catch { }
}
}
}
if (audio === 'dub') return null;
const segment = s ? `tv/${id}/${s}/${e || 1}` : `movie/${id}`;
const settled = await Promise.allSettled(SERVER_KEYS.map(async server => {
const json = await fetchJson(`${API_BASE_URL}/${server}/${segment}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(10000) });
const data = json.encrypted ? decrypt(json.data) : json.data;
const file = data?.sources?.[0]?.file ?? data?.streams?.[0]?.url ?? data?.url?.[0]?.link ?? data?.data?.stream?.playlist;
if (file) return { url: file, server, headers: REQUEST_HEADERS };
throw new Error();
}));
const allUrls = [];
for (const r of settled) if (r.status === 'fulfilled') allUrls.push(r.value);
return allUrls.length ? { allUrls } : null;
}
export async function getSources(args) {
const res = await getStream(args);
return res?.allUrls ? res.allUrls.map(u => u.server) : [];
}
export const VERIFY_HEADERS = { ...REQUEST_HEADERS };

View File

@@ -1,145 +0,0 @@
import { webcrypto } from 'node:crypto';
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f';
const BASE_URL = 'https://vidrock.ru/';
const PROXY_PREFIX = 'https://proxy.vidrock.store/';
let cryptoKey = null;
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function base64UrlToBytes(value) {
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const padding = base64.length % 4;
if (padding === 2) {
base64 += '==';
} else if (padding === 3) {
base64 += '=';
} else if (padding === 1) {
throw new Error('Invalid base64url');
}
return new Uint8Array(Buffer.from(base64, 'base64'));
}
async function getKey() {
if (cryptoKey) return cryptoKey;
const keyBytes = hexToBytes(GCM_HEX_KEY);
cryptoKey = await webcrypto.subtle.importKey(
'raw',
keyBytes,
'AES-GCM',
false,
['decrypt']
);
return cryptoKey;
}
async function decryptStreamUrl(value) {
try {
const data = base64UrlToBytes(value);
if (data.length < 28) {
throw new Error('Ciphertext too short');
}
const iv = data.slice(0, 12);
const ciphertext = data.slice(12);
const key = await getKey();
const decrypted = await webcrypto.subtle.decrypt(
{
name: 'AES-GCM',
iv
},
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
} catch (error) {
return null;
}
}
export async function getStream({ id, s, e }) {
try {
const path = s
? `tv/${id}/${s}/${e || 1}`
: `movie/${id}`;
const apiData = await fetchJson(`${BASE_URL}api/${path}`, {
headers: {
'User-Agent': USER_AGENT,
Referer: BASE_URL,
Origin: BASE_URL
}
});
if (!apiData || typeof apiData !== 'object') {
return null;
}
const streams = [];
for (const [provider, info] of Object.entries(apiData)) {
if (!info?.url) {
continue;
}
const decryptedUrl = await decryptStreamUrl(info.url);
if (!decryptedUrl) {
continue;
}
let url = decryptedUrl;
if (url.startsWith(PROXY_PREFIX)) {
url = decodeURIComponent(
url.slice(PROXY_PREFIX.length).replace(/^\/+/, '')
);
}
const isXpass = url.includes('xpass.top');
streams.push({
name: provider,
url,
file: url,
stream: url,
src: url,
quality: info.type || 'auto',
type: info.type || 'hls',
headers: {
'User-Agent': USER_AGENT,
Referer: isXpass
? 'https://play.xpass.top/'
: BASE_URL,
Origin: isXpass
? 'https://play.xpass.top'
: 'https://vidrock.ru'
}
});
}
return streams.length ? { streams } : null;
} catch (error) {
return null;
}
}

View File

@@ -1,123 +0,0 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://vidup.to';
const API = 'https://enc-dec.app/api';
export async function getStream({ id, s, e }) {
const isTv = Boolean(s && e);
const targetUrl = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}/`
: `${BASE_URL}/movie/${id}`;
const ua = getUA();
const baseHeaders = {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
};
try {
const pageRes = await fetch(targetUrl, {
headers: { 'User-Agent': ua },
signal: AbortSignal.timeout(10000),
});
if (!pageRes.ok) return null;
const html = await pageRes.text();
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
if (!match || !match[1]) return null;
const enToken = match[1];
const s1Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(enToken)}&stage=1`, {
signal: AbortSignal.timeout(8000),
});
const s1Data = await s1Res.json();
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
const token1 = s1Data.result.token;
const stage1Url = s1Data.result.stage1;
const post1Res = await fetch(stage1Url, {
method: 'POST',
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
signal: AbortSignal.timeout(10000),
});
if (!post1Res.ok) return null;
const stage1Text = await post1Res.text();
const s2Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(stage1Text)}&stage=2`, {
signal: AbortSignal.timeout(8000),
});
const s2Data = await s2Res.json();
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
const serversRes = await fetch(serversUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(10000),
});
if (!serversRes.ok) return null;
const serversEncrypted = await serversRes.text();
const decServersRes = await fetch(`${API}/dec-vidup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted }),
signal: AbortSignal.timeout(8000),
});
const decServersData = await decServersRes.json();
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
const allUrls = [];
for (const server of decServersData.result) {
if (!server?.data) continue;
try {
const streamUrl = `${streamBaseUrl}/${server.data}`;
const streamEncRes = await fetch(streamUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(8000),
});
if (!streamEncRes.ok) continue;
const streamEncText = await streamEncRes.text();
const decStreamRes = await fetch(`${API}/dec-vidup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncText }),
signal: AbortSignal.timeout(8000),
});
const decStreamData = await decStreamRes.json();
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
const streamResult = decStreamData.result;
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
allUrls.push({
url: resultUrl,
label: server.name || server.title || 'VidUp',
headers: {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
},
});
}
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
} catch {
return null;
}
}

View File

@@ -1,188 +0,0 @@
import { USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vidvault.to';
const API_BASE = `${BASE_URL}/api`;
const DL_PROXY_BASE = 'https://dl.gemlelispe.workers.dev';
const SUB_PROXY_BASE = 'https://sub.k5s7sjozpn.workers.dev';
const HEADERS = {
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL,
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
function formatBytes(bytes, decimals = 2) {
if (!bytes || bytes <= 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
}
async function getToken() {
const res = await fetch(`${API_BASE}/get-token`, {
method: 'GET',
headers: HEADERS,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
const data = await res.json();
return data?.t;
}
function getCaptionType(url) {
return String(url).toLowerCase().includes('.vtt') ? 'vtt' : 'srt';
}
export async function getStream(args) {
const { id, s, e } = args;
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
try {
const token = await getToken();
if (!token) return null;
const payload = {
type,
tmdbId: String(id),
};
if (isTv) {
payload.season = Number(s);
payload.episode = Number(e);
}
const res = await fetch(`${API_BASE}/download-proxy`, {
method: 'POST',
headers: {
...HEADERS,
'Content-Type': 'application/json',
'x-request-token': token,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) return null;
const data = await res.json();
const allUrls = [];
const subtitles = [];
const titleTag = encodeURIComponent(isTv ? `Show_${id}_S${s}E${e}` : `Movie_${id}`);
const mp4 = data?.mp4Data;
const downloadData = mp4?.downloadInfo?.data || mp4?.data?.data || mp4?.data || mp4;
if (downloadData) {
const streams = downloadData.streams || downloadData.downloads || [];
for (const item of streams) {
if (!item?.url) continue;
const res = item.resolutions || item.resolution || 'Auto';
const sizeStr = item.size ? ` (${typeof item.size === 'number' ? formatBytes(item.size) : item.size})` : '';
const proxiedUrl = `${DL_PROXY_BASE}/${encodeURIComponent(item.url)}?n=${titleTag}`;
allUrls.push({
url: proxiedUrl,
server: `VidVault - MP4 ${res}p${sizeStr}`.trim(),
quality: typeof res === 'number' ? `${res}p` : res,
type: 'mp4',
headers: HEADERS,
});
}
const captions = downloadData.captions || [];
for (const cap of captions) {
if (!cap?.url) continue;
const subUrl = `${SUB_PROXY_BASE}/?url=${encodeURIComponent(cap.url)}&title=${titleTag}`;
subtitles.push({
label: cap.lanName || cap.lan || 'Unknown',
type: getCaptionType(cap.url),
language: cap.lan || '',
url: subUrl,
});
}
}
const mkvFiles = data?.mkvData?.files || [];
for (const file of mkvFiles) {
if (!file?.url) continue;
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
allUrls.push({
url: file.url,
server: `VidVault - MKV 480p${sizeStr}`.trim(),
quality: '480p',
type: 'mp4',
headers: HEADERS,
});
}
const mkvV2 = data?.mkvV2Data;
const mkvV2List = Array.isArray(mkvV2)
? mkvV2
: Array.isArray(mkvV2?.files)
? mkvV2.files
: (mkvV2?.url ? [mkvV2] : []);
for (const file of mkvV2List) {
if (!file?.url) continue;
const quality = file.quality ? String(file.quality).replace(/p$/i, '') : 'Auto';
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
const meta = [file.country, file.language].filter(Boolean).join(' • ');
const metaStr = meta ? ` [${meta}]` : '';
allUrls.push({
url: file.url,
server: `VidVault - MKV v2 ${quality}p${sizeStr}${metaStr}`.trim(),
quality: `${quality}p`,
type: 'mp4',
headers: HEADERS,
});
}
const mkvV3 = data?.mkvV3Data;
if (mkvV3?.downloads && Array.isArray(mkvV3.downloads)) {
const country = mkvV3.country || '';
const language = mkvV3.language || '';
for (const down of mkvV3.downloads) {
if (Array.isArray(down?.qualities)) {
for (const qObj of down.qualities) {
for (const ep of qObj?.episodes || []) {
if (!ep?.url) continue;
const quality = qObj.quality ? String(qObj.quality).replace(/p$/i, '') : 'Auto';
const sizeStr = ep.size ? ` (${typeof ep.size === 'number' ? formatBytes(ep.size) : ep.size})` : '';
const meta = [ep.country || country, ep.language || language].filter(Boolean).join(' • ');
const metaStr = meta ? ` [${meta}]` : '';
allUrls.push({
url: ep.url,
server: `VidVault - MKV v3 ${quality}p${sizeStr}${metaStr}`.trim(),
quality: `${quality}p`,
type: 'mp4',
headers: HEADERS,
});
}
}
} else if (down?.url) {
const quality = down.quality ? String(down.quality).replace(/p$/i, '') : 'Auto';
const sizeStr = down.size ? ` (${typeof down.size === 'number' ? formatBytes(down.size) : down.size})` : '';
allUrls.push({
url: down.url,
server: `VidVault - MKV v3 ${quality}p${sizeStr}`.trim(),
quality: `${quality}p`,
type: 'mp4',
headers: HEADERS,
});
}
}
}
return allUrls.length ? { allUrls, subtitles: subtitles.length ? subtitles : undefined } : null;
} catch (err) {
return null;
}
}

View File

@@ -1,184 +0,0 @@
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
const BASE_URL = 'https://core.vidzee.wtf';
const PLAYER_URL = 'https://player.vidzee.wtf';
const SERVICES = ['dcloud', 'ipcloud', 'tik', 'v6:Hindi', '3AHindi'];
const WASM_B64 = 'AGFzbQEAAAABPgtgAX8Bf2ACf38Bf2ABfwBgAn9/AGADf39/AGAAAGAEf39/fwBgBH9/f38Bf2ADf39/AX9gA39/fgBgAAF/Ag0BA2VudgVhYm9ydAAGAygnAgEAAAQBAAEDBAADBQAAAgMHAAEAAAQIAwEJAgIBBQAAAgEABQoBBQMBAAEGJgd/AUEAC38BQQALfwFBAAt/AUEAC38BQQALfwFBAAt/AEGA2wALB0gHB2RlY3J5cHQAJwVfX25ldwAGBV9fcGluACQHX191bnBpbgAiCV9fY29sbGVjdAAlC19fcnR0aV9iYXNlAwYGbWVtb3J5AgAIAR8MAqkBCogkJyUAIABFBEAPCyAAQRRrIgAQByMCRgRAIAAQECAAIwUjAkUQCgsLCgAgACABai0AAAsKACAAKAIEQXxxCw0AIABBFGsoAhBBAXYLDAAgACABaiACOgAAC1MBAX8gAEHs////A0sEQEHwDEGwDUH9AEEeEAAACyMARQRAEA0LIwAgAEEQahAaIgIgATYCDCACIAA2AhAgAiMBIwIQCiACEAsjA2okAyACQRRqCwoAIAAoAgRBA3ELKQAgACAAIAFBCHZqLQAAQQV0aiABQf8BcUEDdmotAAAgAUEHcXZBAXELwwEBBH8gASgCAEF8cSIDQYACSQR/IANBBHYFQR9B/P///wMgAyADQfz///8DTxsiA2drIgRBB2shAiADIARBBGt2QRBzCyEEIAEoAgghBSABKAIEIgMEQCADIAU2AggLIAUEQCAFIAM2AgQLIAEgACACQQR0IARqQQJ0aiIBKAJgRgRAIAEgBTYCYCAFRQRAIAAgAkECdGoiASgCBEF+IAR3cSEDIAEgAzYCBCADRQRAIAAgACgCAEF+IAJ3cTYCAAsLCwspAQF/IAEoAgghAyAAIAEgAnI2AgQgACADNgIIIAMgABARIAEgADYCCAsNACAAKAIAQXxxQQRqC7wCAQV/IAEoAgAhAyABQQRqIAEoAgBBfHFqIgQoAgAiAkEBcQRAIAAgBBAJIAEgA0EEaiACQXxxaiIDNgIAIAFBBGogASgCAEF8cWoiBCgCACECCyADQQJxBEAgAUEEaygCACIBKAIAIQYgACABEAkgASAGQQRqIANBfHFqIgM2AgALIAQgAkECcjYCACAEQQRrIAE2AgAgACADQXxxIgJBgAJJBH8gAkEEdgVBH0H8////AyACIAJB/P///wNPGyICZ2siA0EHayEFIAIgA0EEa3ZBEHMLIgIgBUEEdGpBAnRqKAJgIQMgAUEANgIEIAEgAzYCCCADBEAgAyABNgIECyAAIAVBBHQgAmpBAnRqIAE2AmAgACAAKAIAQQEgBXRyNgIAIAAgBUECdGoiACAAKAIEQQEgAnRyNgIEC5cBAQJ/PwAiAEEATAR/QQEgAGtAAEEASAVBAAsEQAALQbDbAEEANgIAQdDnAEEANgIAA0AgAUEXSQRAIAFBAnRBsNsAakEANgIEQQAhAANAIABBEEkEQCABQQR0IABqQQJ0QbDbAGkBADYCYCAAQQFqIQAMAQsLIAFBAWohAQwBCwtBsNsAQdTnAD8ArEIQhhAbQbDbACQACyYBAX8gAEEEayEBIABBD3FBASAAGwR/QQEFIAEoAgBBAXELGiABCxIAIAAgADYCBCAAIAA2AgggAAsnAQF/IAAQAyIBRQRAIAAoAggaDwsgASAAKAIIIgA2AgggACABEBELEgAgACABIAAoAgRBA3FyNgIEC0kBAX8gACABQQF0aiEBA0AgAyIAQQFrIQMgAARAIAEvAQAiACACLwEAIgRHBEAgACAEaw8LIAFBAmohASACQQJqIQIMAQsLQQALbgECf0EMQQYQBiIBRQRAQQxBAxAGIQELIAFBADYCACABQQA2AgQgAUEANgIIIABB/P///wNLBEBB4NcAQZDYAEETQTkQAAALIABBARAGIgJBACAA/AsAIAEgAjYCACABIAI2AgQgASAANgIIIAELjgEBAn8gAUGAAkkEfyABQQR2BUEfIAEQFSIBZ2siA0EHayECIAEgA0EEa3ZBEHMLIgEgACACQQJ0aigCBEF/IAF0cSIBBH8gACABaCACQQR0akECdGooAmAFIAAoAgBBfyACQQFqdHEiAQR/IAAgACABaCIAQQJ0aigCBGggAEEEdGpBAnRqKAJgBUEACwsLHQAgAEEBQRsgAGdrdGpBAWsgACAAQf7///8BSRsLLwAgAEH8////A0sEQEHwDEHwDUHNA0EdEAAAC0EMIABBE2pBcHFBBGsgAEEMTRsLZwECfyABKAIAIgNBfHEgAmsiBEEQTwRAIAEgAiADQQJxcjYCACABQQRqIAJqIgEgBEEEa0EBcjYCACAAIAEQDAUgASADQX5xNgIAIAFBBGogASgCAEF8cWoiACAAKAIAQX1xNgIACwswACAAIAIQGiICQQRqIAFBBGogASgCAEF8cfwKAAAgAUGk2wBPBEAgACABEBkLIAILFQAgASABKAIAQQFyNgIAIAAgARAMC48BAQJ/IAAgARAWIgIQFCIBRQRAQQQgACgCoAw/ACIBQRB0QQRrR3QgAhAVIAIgAkGAAk8bakH//wNqQYCAfHFBEHYhAyABIAMgASADShtAAEEASARAIANAAEEASARAAAsLIAAgAUEQdD8ArEIQhhAbIAAgAhAUIQELIAEoAgAaIAAgARAJIAAgASACEBcgAQuGAQEDfyABQRNqQXBxQQRrIQEgACgCoAwiAwRAIAFBEGsiBSADRgRAIAMoAgAhBCAFIQELCyACp0FwcSABayIDQRRJBEAPCyABIARBAnEgA0EIayIDQQFycjYCACABQQA2AgQgAUEANgIIIAFBBGogA2oiA0ECNgIAIAAgAzYCoAwgACABEAwLhQEBA38CQAJAAkACQAJAAkACQAJAAkAgAEEIaygCAA4IAAECAwQFBgcICw8LDwsPCyAAEB0PCyAAKAIAEAEPCyAAKAIEIgEgACgCDEECdGohAgNAIAEgAkkEQCABKAIAIgMEQCADEAELIAFBBGohAQwBCwsgACgCABABDwsgABAdDwsPCwALCQAgACgCABABC0MAIAEgACgCDE8EQEHw1QBBsNYAQfIAQSoQAAALIAAoAgQgAUECdGooAgAiAEUEQEHg1gBBsNYAQfYAQSgQAAALIAALGQBBoA4QDyQBQYDaABAPJARB4NoAEA8kBQv9AQEGfyAAIgFBCHYiAkHMzgBqLQAAIAJBlDJqLQAAQdYAbEGUMmogAEH/AXEiA0EDbmotAAAgA0EDcEECdEGAxwBqKAIAbEELdkEGcGpBAnRBjbrowserigCACICQf8BcSEAIAJBCHUhAgJAIABBAkkNACACQf8BcSEAIAJBCHYhAgNAIAAEQCAAQQF2IgYgAmpBAXRBzNIAaiIELQAAIgUgA0YEfyAELQABQQJ0QYzHAGooAgAiAkH/AXEhACACQQh1IQIgAEECSQ0DIAFBAWoPBSADIAVJBH8gBgUgAiAGaiECIAAgBmsLCyEADAELCyABDwsgASACQQAgAGtxag==';
let wasmInstanceCache = null;
async function getWasmInstance() {
if (wasmInstanceCache) return wasmInstanceCache;
try {
const wasmBuffer = Buffer.from(WASM_B64, 'base64');
const wasmModule = await WebAssembly.compile(wasmBuffer);
let memory;
let exports;
const env = {
abort() {
throw new Error('WASM abort');
}
};
const res = await WebAssembly.instantiate(wasmModule, { env });
exports = res.exports;
memory = exports.memory;
const refMap = new Map();
function pin(ptr) {
if (ptr) {
const count = refMap.get(ptr);
if (count) refMap.set(ptr, count + 1);
else refMap.set(exports.__pin(ptr), 1);
}
return ptr;
}
function unpin(ptr) {
if (ptr) {
const count = refMap.get(ptr);
if (count === 1) {
exports.__unpin(ptr);
refMap.delete(ptr);
} else if (count) {
refMap.set(ptr, count - 1);
}
}
}
wasmInstanceCache = {
decrypt(encryptedBytes, keyStr) {
let dataPtr = 0;
let keyPtr = 0;
try {
const len = encryptedBytes.length;
const pinPtr = exports.__pin(exports.__new(len, 1)) >>> 0;
dataPtr = exports.__new(12, 6) >>> 0;
const view = new DataView(memory.buffer);
view.setUint32(dataPtr, pinPtr, true);
view.setUint32(dataPtr + 4, pinPtr, true);
view.setUint32(dataPtr + 8, len, true);
new Uint8Array(memory.buffer, pinPtr, len).set(encryptedBytes);
exports.__unpin(pinPtr);
pin(dataPtr);
const kLen = keyStr.length;
keyPtr = exports.__new(kLen << 1, 2) >>> 0;
const u16View = new Uint16Array(memory.buffer);
for (let i = 0; i < kLen; i++) {
u16View[(keyPtr >>> 1) + i] = keyStr.charCodeAt(i);
}
const resPtr = exports.decrypt(dataPtr, keyPtr) >>> 0;
if (!resPtr) return new Uint8Array(0);
const resView = new DataView(memory.buffer);
const outPtr = resView.getUint32(resPtr + 4, true);
const outLen = resView.getUint32(resPtr + 8, true);
return new Uint8Array(memory.buffer, outPtr, outLen).slice();
} finally {
unpin(dataPtr);
}
}
};
return wasmInstanceCache;
} catch {
return null;
}
}
async function requestEndpoint(url, headers) {
try {
return await fetchJson(url, {
headers,
signal: AbortSignal.timeout(6000)
});
} catch {
return null;
}
}
async function resolveServiceStream(service, isMovie, id, s, e, headers) {
const directEndpoint = isMovie
? `${BASE_URL}/streams/movie/${id}?s=${service}`
: `${BASE_URL}/streams/tv/${id}/${s || 1}/${e || 1}?s=${service}`;
let res = await requestEndpoint(directEndpoint, headers);
if (!res || !res.url) {
const encEndpoint = `${directEndpoint}&e=1`;
const encRes = await requestEndpoint(encEndpoint, headers);
if (encRes && encRes.c) {
const instance = await getWasmInstance();
if (instance) {
const encryptedBytes = new Uint8Array(Buffer.from(encRes.c, 'base64'));
const decryptedBytes = instance.decrypt(encryptedBytes, 'player.vidzee.wtf');
if (decryptedBytes.length > 0) {
const str = new TextDecoder().decode(decryptedBytes);
try {
res = JSON.parse(str);
} catch { }
}
}
}
}
if (res && res.url) {
const format = res.url.includes('.m3u8') ? 'hls' : 'mp4';
return {
url: res.url,
server: `VidZee (${service})`,
quality: res.language || 'Auto',
type: format,
headers: {
'User-Agent': USER_AGENT,
'Referer': `${PLAYER_URL}/`,
'Origin': PLAYER_URL,
...(res.headers || {})
}
};
}
return null;
}
export async function getStream(args) {
const { id, s, e, server, clientIP } = args;
const isTv = s != null && e != null;
const isMovie = !isTv;
const headers = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Referer': `${PLAYER_URL}/`,
'Origin': PLAYER_URL,
...(clientIP && { 'X-Forwarded-For': clientIP })
};
let targets = SERVICES;
if (server && server !== 'all') {
const clean = server.toLowerCase().replace('vidzee (', '').replace(')', '').replace('vidzee', '').trim();
targets = SERVICES.filter(srv => srv.toLowerCase().includes(clean));
if (!targets.length) targets = SERVICES;
}
const settled = await Promise.allSettled(
targets.map(service => resolveServiceStream(service, isMovie, id, s, e, headers))
);
const allUrls = [];
for (const r of settled) {
if (r.status === 'fulfilled' && r.value) {
allUrls.push(r.value);
}
}
return allUrls.length ? { allUrls } : null;
}
export async function getSources() {
return SERVICES.map(s => `VidZee (${s})`);
}

View File

@@ -1,112 +0,0 @@
import { fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vixsrc.to';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
};
function extract(regex, text) {
const m = text?.match(regex);
if (!m) return null;
let val = m[1];
val = val.replace(/\\u0026/gi, '&');
val = val.replace(/\\\//g, '/');
val = val.replace(/\\/g, '');
return val;
}
export async function getStream({ id, s, e, absoluteBase }) {
try {
const apiUrl = s && e
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
: `${BASE_URL}/api/movie/${id}`;
const apiProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: apiUrl;
const apiText = await fetchText(apiProxy, { headers: HEADERS }).catch(() => null);
const apiData = JSON.parse(apiText || '{}');
if (!apiData.src) return null;
const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
const embedProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: embedUrl;
const html = await fetchText(embedProxy, { headers: HEADERS }).catch(() => null);
if (!html) return null;
const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
if (!token || !expires || !playlist) return null;
if (playlist.startsWith('/')) {
playlist = `${BASE_URL}${playlist}`;
}
const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
const masterProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}`
: rawMasterUrl;
const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
if (!playlistText?.includes('#EXTM3U')) return null;
let finalUrl = null;
let bestHeight = 0;
const lines = playlistText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.startsWith('#EXT-X-STREAM-INF')) continue;
const height = parseInt(line.match(/RESOLUTION=\d+x(\d+)/)?.[1] || '0', 10);
let next = lines[i + 1]?.trim();
if (!next || next.startsWith('#')) continue;
if (height > bestHeight) {
bestHeight = height;
finalUrl = next;
}
}
if (!finalUrl) finalUrl = rawMasterUrl;
if (!finalUrl.startsWith('http')) {
const baseMatch = rawMasterUrl.match(/^(https?:\/\/[^\/]+)/);
const base = baseMatch ? baseMatch[1] : BASE_URL;
if (finalUrl.startsWith('/')) {
finalUrl = `${base}${finalUrl}`;
} else {
const pathParts = rawMasterUrl.split('/');
pathParts.pop();
finalUrl = `${pathParts.join('/')}/${finalUrl}`;
}
}
return {
allUrls: [
{
url: finalUrl,
server: 'VixSrc',
skipProxy: true,
headers: {
...HEADERS,
'Referer': `${BASE_URL}/`
}
}
]
};
} catch (err) {
return null;
}
}

View File

@@ -1,129 +0,0 @@
const BASE_URL = 'https://www.films365.org';
const HEADERS = {
Authorization: 'Bearer 79a02956be35835728a044b11e2ae793149d45fb2c89cb6d029ec01aac19bfdb',
'Content-Type': 'application/json',
'User-Agent': 'MovieDownloader/1.0',
};
async function resolveTitle(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(6000),
});
if (!res.ok) return null;
const data = await res.json();
return isTv ? data.name : data.title;
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const targetType = isTv ? 'tv' : 'movie';
const title = await resolveTitle(sdk, id, isTv);
const query = title || String(id);
const searchRes = await fetch(`${BASE_URL}/api/mobile/search`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ query }),
signal: AbortSignal.timeout(8000),
});
if (!searchRes.ok) return null;
const searchJson = await searchRes.json();
const resultsObj = searchJson?.results;
if (!resultsObj) return null;
const items = resultsObj.all || (targetType === 'tv' ? resultsObj.tvs : resultsObj.movies) || [];
if (!Array.isArray(items) || items.length === 0) return null;
const cleanSearchTitle = query.toLowerCase().replace(/[^a-z0-9]/g, '');
let matchedItem = null;
for (const item of items) {
const itemType = String(item?.type || '').toLowerCase();
if (itemType && itemType !== targetType) continue;
const itemTitle = String(item?.title || '');
const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, '');
if (cleanItemTitle === cleanSearchTitle || cleanItemTitle.includes(cleanSearchTitle)) {
matchedItem = item;
break;
}
}
if (!matchedItem) {
matchedItem = items.find(i => String(i?.type || '').toLowerCase() === targetType) || items[0];
}
const itemId = matchedItem?.id || matchedItem?.tmdbId;
if (!itemId) return null;
const detailsRes = await fetch(`${BASE_URL}/api/mobile/details?id=${itemId}&type=${targetType}`, {
headers: HEADERS,
signal: AbortSignal.timeout(8000),
});
if (!detailsRes.ok) return null;
const detailsJson = await detailsRes.json();
const data = detailsJson?.data;
if (!data) return null;
const spoken = Array.isArray(data.spokenLanguages)
? data.spokenLanguages.map(l => String(l).trim()).filter(Boolean)
: [];
const langSuffix = spoken.length > 0 ? ` · ${spoken.join(', ')}` : '';
const allUrls = [];
if (!isTv) {
const streamUrl = data.downloadUrl || data.videoUrl;
if (streamUrl && typeof streamUrl === 'string') {
allUrls.push({
url: streamUrl.trim(),
label: `X-Downloader${langSuffix}`,
headers: {
'User-Agent': HEADERS['User-Agent'],
},
});
}
} else {
const seasonNum = Number(s);
const episodeNum = Number(e);
const seasons = Array.isArray(data.seasons) ? data.seasons : [];
const targetSeason = seasons.find(sn => Number(sn.seasonNumber) === seasonNum);
if (targetSeason) {
const episodes = Array.isArray(targetSeason.episodes) ? targetSeason.episodes : [];
const targetEp = episodes.find(ep => Number(ep.episodeNumber) === episodeNum);
if (targetEp) {
const streamUrl = targetEp.downloadUrl || targetEp.videoUrl;
if (streamUrl && typeof streamUrl === 'string') {
allUrls.push({
url: streamUrl.trim(),
label: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`,
headers: {
'User-Agent': HEADERS['User-Agent'],
},
});
}
}
}
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -1,122 +0,0 @@
import { getUA } from './utils/helpers.js';
const SUBTITLE_BASES = [
'https://sub.vdrk.site/v1',
'https://sub.vdrk.site/v2',
'https://opensubtitles-v3.strem.io'
];
async function resolveImdbId(id, isTv) {
if (typeof id === 'string' && id.startsWith('tt')) return id;
const apiKey = process.env.TMDB_API_KEY;
if (!apiKey) return `tt${id}`;
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(5000),
});
if (!res.ok) return `tt${id}`;
const data = await res.json();
return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`;
} catch {
return `tt${id}`;
}
}
export async function fetchSubtitles(paths = []) {
try {
const results = await Promise.all(
paths.map(async ({ base, path }) => {
try {
const res = await fetch(`${base}${path}`, {
headers: { 'User-Agent': getUA() },
signal: AbortSignal.timeout(6000),
});
if (!res.ok) {
res.body?.cancel();
return [];
}
const data = await res.json();
if (base.includes('/v2')) {
return Array.isArray(data)
? data.map(x => ({
label: x.label,
file: x.file || x.url,
type: 'vtt',
source: 'v2'
}))
: [];
}
if (base.includes('opensubtitles-v3.strem.io')) {
if (!Array.isArray(data?.subtitles)) return [];
return data.subtitles.map(sub => {
if (!sub?.url) return null;
const isSrt = sub.url.toLowerCase().endsWith('.srt');
return {
label: sub.lang ? sub.lang.toUpperCase() : 'OpenSubtitles',
file: sub.url,
type: isSrt ? 'srt' : 'vtt',
source: 'opensubtitles'
};
}).filter(Boolean);
}
if (base.includes('fed-subs.pstream.mov')) {
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
return Object.entries(data.subtitles)
.map(([language, sub]) => {
if (!sub?.subtitle_link) return null;
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
return {
label: sub.subtitle_name || language,
file: sub.subtitle_link,
type: ext === 'vtt' ? 'vtt' : 'srt',
source: 'febbox'
};
})
.filter(Boolean);
}
const v1 = Array.isArray(data) ? data : [];
return v1.map(x => ({
label: x.label,
file: x.file || x.url,
type: 'vtt',
source: 'v1'
}));
} catch {
return [];
}
})
);
return results.flat();
} catch {
return [];
}
}
export async function getSubPathsMovie(id) {
const imdbId = await resolveImdbId(id, false);
return [
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
{ base: SUBTITLE_BASES[2], path: `/subtitles/movie/${imdbId}.json` }
];
}
export async function getSubPathsTv(id, season, episode) {
const imdbId = await resolveImdbId(id, true);
return [
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
{ base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` }
];
}
export { SUBTITLE_BASES };

View File

@@ -1,230 +0,0 @@
import crypto from 'node:crypto';
export const UA_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
];
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
export const USER_AGENT = UA_LIST[0];
let sharedDcKey = null;
export function decryptPartner(responseText, keyEnv) {
try {
const sep = responseText.indexOf(':');
if (sep === -1) return null;
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
const encrypted = responseText.slice(sep + 1);
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
} catch {
return null;
}
}
export async function fetchJson(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function fetchText(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
}
const tmdbInfoCache = new Map();
const anilistCache = new Map();
const tmdbValidationCache = new Map();
function pruneHelperCache(cache) {
const now = Date.now();
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
if (cache.size > 3000) {
const overflow = cache.size - 3000;
const it = cache.keys();
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
}
}
setInterval(() => {
pruneHelperCache(tmdbValidationCache);
pruneHelperCache(tmdbInfoCache);
pruneHelperCache(anilistCache);
}, 60000);
function cacheGet(cache, key) {
const entry = cache.get(key);
if (!entry) return undefined;
if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
return entry.val;
}
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
return {
id: id,
s: s,
e: e,
clientIP: clientIP,
absoluteBase: '/',
audio,
sdk,
config: source,
}
}
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
let apiKey = tmdbApiKey;
let id = tmdbId;
let type = mediaType;
if (id === 'movie' || id === 'tv' || !id) {
if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
type = id || 'movie';
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `${id}-${type}`;
const cached = cacheGet(tmdbValidationCache, key);
if (cached !== undefined) return cached;
if (!apiKey) return { valid: false, error: 'TMDB API key not configured' };
try {
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
if (res.ok) {
const data = await res.json();
const isValid = !!(data?.id && (data?.title || data?.name));
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
cacheSet(tmdbValidationCache, key, result, 300000);
return result;
}
const result = { valid: false, error: `TMDB API error: ${res.status}` };
cacheSet(tmdbValidationCache, key, result, 60000);
return result;
} catch (error) {
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
cacheSet(tmdbValidationCache, key, result, 60000);
return result;
}
}
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
let apiKey = tmdbApiKey;
let id = tmdbId;
let type = mediaType;
let s = season;
if (apiKey === 'movie' || apiKey === 'tv') {
type = apiKey;
id = tmdbId;
apiKey = process.env.TMDB_API_KEY;
} else if (id === 'movie' || id === 'tv') {
type = id;
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `${id}-${type}-${s || ''}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null };
try {
const [mainRes, seasonRes] = await Promise.all([
fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
]);
let mainData = null, seasonData = null;
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
const genres = mainData?.genres || [];
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
const titles = [];
if (seasonData?.name) titles.push(seasonData.name);
const titleText = mainData?.title || mainData?.name || '';
const ot = mainData?.original_title || mainData?.original_name || '';
if (titleText) titles.push(titleText);
if (ot && ot !== titleText) titles.push(ot);
const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
}
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
const key = `${tmdbId}-${mediaType}-${season || ''}`;
const cached = cacheGet(anilistCache, key);
if (cached !== undefined) return cached;
try {
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
if (res.ok) {
const id = (await res.json())?.mappings?.[0]?.anilist_id;
if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
} else res.body?.cancel();
} catch { }
if (!titles.length) return null;
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
let bestId = null, bestScore = -1;
for (const searchTitle of titles) {
try {
const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
if (!res.ok) { res.body?.cancel(); continue; }
for (const entry of (await res.json())?.data?.Page?.media || []) {
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
const sl = searchTitle.toLowerCase();
let score = 0;
if (entryTitles.some(t => t === sl)) score += 5;
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
else continue;
if (year && entry.startDate?.year) {
const diff = Math.abs(entry.startDate.year - year);
if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
}
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
if (score > bestScore) { bestScore = score; bestId = entry.id; }
}
if (bestScore >= 8) break;
} catch { }
}
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
return bestId;
}
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
let apiKey = tmdbApiKey;
let id = imdbId;
let type = mediaType;
if (id === 'movie' || id === 'tv') {
type = id;
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `imdb-${id}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
if (!apiKey) return null;
try {
const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
let result = null;
if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return null; }
}

View File

@@ -1,4 +0,0 @@
export const webcrypto = globalThis.crypto;
export function createHash() { throw new Error('unsupported'); }
export function createDecipheriv() { throw new Error('unsupported'); }
export default { webcrypto, createHash, createDecipheriv };

View File

@@ -1,7 +0,0 @@
import { Buffer } from 'buffer';
globalThis.Buffer = globalThis.Buffer || Buffer;
globalThis.process = globalThis.process || { env: {} };
if (!AbortSignal.timeout) {
AbortSignal.timeout = ms => { const c = new AbortController(); setTimeout(() => c.abort(), ms); return c.signal; };
}
export { Buffer };

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ android/app/build/
android/.gradle/
.gradle/
*.apk
.env
.env
.build

View File

@@ -16,4 +16,4 @@ An Android TV streaming app built with Jetpack Compose and ExoPlayer.
npm run dev
```
This builds the APK as `vyla.apk`.
This builds the APK as `vyla.apk`.

View File

@@ -24,6 +24,7 @@ import tv.vyla.app.ui.components.NavDestination
import tv.vyla.app.ui.components.Sidebar
import tv.vyla.app.ui.screens.*
import tv.vyla.app.ui.theme.VylaTvTheme
import androidx.compose.ui.draw.drawWithContent
class MainActivity : ComponentActivity() {
@@ -47,6 +48,7 @@ class MainActivity : ComponentActivity() {
val watchLaterItems by libraryManager.watchLater.collectAsState()
val favoriteItems by libraryManager.favorites.collectAsState()
val historyItems by libraryManager.history.collectAsState()
val sidebarFocusRequester = remember { FocusRequester() }
val contentFocusRequester = remember { FocusRequester() }
@@ -63,6 +65,7 @@ class MainActivity : ComponentActivity() {
if (playingItem == null) {
Sidebar(
currentDestination = currentDestination,
hasHistory = historyItems.isNotEmpty(),
hasWatchLater = watchLaterItems.isNotEmpty(),
hasFavorites = favoriteItems.isNotEmpty(),
onNavigate = { dest ->
@@ -81,10 +84,12 @@ class MainActivity : ComponentActivity() {
)
}
val contentCovered = playingItem != null || activeDetailItem != null
Box(
modifier = Modifier
.fillMaxSize()
.focusRequester(contentFocusRequester)
.drawWithContent { if (!contentCovered) drawContent() }
) {
when (currentDestination) {
NavDestination.DISCOVERY,
@@ -95,6 +100,7 @@ class MainActivity : ComponentActivity() {
mode = currentDestination,
onOpenDetail = { item -> activeDetailItem = item },
onPlay = { item ->
libraryManager.addHistory(item)
playingItem = item
playingEpisode = null
},
@@ -115,6 +121,14 @@ class MainActivity : ComponentActivity() {
}
)
}
NavDestination.HISTORY -> {
LibraryScreen(
title = "Watch History",
items = historyItems,
onOpenDetail = { item -> activeDetailItem = item },
emptyMessage = "Nothing watched yet. Press play on a title and it shows up here."
)
}
NavDestination.WATCH_LATER -> {
LibraryScreen(
title = "Watch Later",
@@ -144,6 +158,7 @@ class MainActivity : ComponentActivity() {
onToggleWatchLater = { libraryManager.toggle("wl", it) },
onToggleFavorite = { libraryManager.toggle("fav", it) },
onPlay = { itm, ep ->
libraryManager.addHistory(itm)
playingItem = itm
playingEpisode = ep
},

View File

@@ -308,7 +308,14 @@ object TmdbApi {
return list
}
private const val CACHE_TTL_MS = 10 * 60 * 1000L
private val responseCache = android.util.LruCache<String, Pair<Long, JSONObject>>(80)
private suspend fun fetchJson(path: String): JSONObject? = withContext(Dispatchers.IO) {
val now = System.currentTimeMillis()
responseCache.get(path)?.let { (at, cached) ->
if (now - at < CACHE_TTL_MS) return@withContext cached
}
val separator = if (path.contains("?")) "&" else "?"
val url = "$BASE_URL$path${separator}api_key=$API_KEY&language=en-US"
val req = Request.Builder().url(url).build()
@@ -316,10 +323,12 @@ object TmdbApi {
client.newCall(req).execute().use { resp ->
if (resp.isSuccessful) {
val body = resp.body?.string()
if (!body.isNullOrEmpty()) JSONObject(body) else null
if (!body.isNullOrEmpty()) {
JSONObject(body).also { responseCache.put(path, now to it) }
} else null
} else null
}
} catch (_: IOException) {
} catch (_: Exception) {
null
}
}

View File

@@ -30,10 +30,21 @@ class LibraryManager private constructor(context: Context) {
private val _favorites = MutableStateFlow<List<MediaItem>>(emptyList())
val favorites: StateFlow<List<MediaItem>> = _favorites.asStateFlow()
private val _history = MutableStateFlow<List<MediaItem>>(emptyList())
val history: StateFlow<List<MediaItem>> = _history.asStateFlow()
init {
load()
}
fun addHistory(item: MediaItem) {
val current = _history.value.toMutableList()
current.removeAll { it.id == item.id && it.mediaType == item.mediaType }
current.add(0, item)
_history.value = current.take(100)
save()
}
private fun load() {
val raw = prefs.getString("vyla.lib", null)
if (raw.isNullOrEmpty()) return
@@ -41,6 +52,7 @@ class LibraryManager private constructor(context: Context) {
val root = JSONObject(raw)
_watchLater.value = parseList(root.optJSONArray("wl"))
_favorites.value = parseList(root.optJSONArray("fav"))
_history.value = parseList(root.optJSONArray("hist"))
} catch (e: Exception) {
e.printStackTrace()
}
@@ -51,6 +63,7 @@ class LibraryManager private constructor(context: Context) {
val root = JSONObject()
root.put("wl", serializeList(_watchLater.value))
root.put("fav", serializeList(_favorites.value))
root.put("hist", serializeList(_history.value))
prefs.edit().putString("vyla.lib", root.toString()).apply()
} catch (e: Exception) {
e.printStackTrace()

View File

@@ -13,6 +13,8 @@ import java.io.InputStream
import java.net.URLEncoder
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import java.util.Collections
import java.util.concurrent.Executors
class LocalProxy(port: Int = PORT) : NanoHTTPD("127.0.0.1", port) {
@@ -73,6 +75,28 @@ class LocalProxy(port: Int = PORT) : NanoHTTPD("127.0.0.1", port) {
override fun getDescription(): String = "$code Status"
}
private class PoolRunner : AsyncRunner {
private val pool = Executors.newCachedThreadPool()
private val running = Collections.synchronizedList(ArrayList<ClientHandler>())
override fun closeAll() {
synchronized(running) { ArrayList(running).forEach { it.close() } }
}
override fun closed(clientHandler: ClientHandler) {
running.remove(clientHandler)
}
override fun exec(clientHandler: ClientHandler) {
running.add(clientHandler)
pool.execute(clientHandler)
}
}
init {
setAsyncRunner(PoolRunner())
}
override fun serve(session: IHTTPSession): Response {
val r: Response = when {
session.method == Method.OPTIONS -> newFixedLengthResponse(Code(204), "text/plain", "")

View File

@@ -0,0 +1,70 @@
package tv.vyla.app.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage
import coil.request.ImageRequest
val HeroFadeStops: Array<Pair<Float, Float>> = arrayOf(
0f to 1f,
0.38f to 1f,
0.50f to 0.92f,
0.62f to 0.70f,
0.74f to 0.42f,
0.86f to 0.16f,
1f to 0f
)
@Composable
fun AmbientBackdrop(url: String?, modifier: Modifier = Modifier, durationMs: Int = 600) {
val context = LocalContext.current
Crossfade(
targetState = url,
modifier = modifier,
animationSpec = tween(durationMs),
label = "ambient_backdrop"
) { target ->
if (!target.isNullOrEmpty()) {
val request = remember(target) {
ImageRequest.Builder(context)
.data(target)
.size(48, 27)
.allowHardware(false)
.build()
}
AsyncImage(
model = request,
contentDescription = null,
contentScale = ContentScale.Crop,
filterQuality = FilterQuality.Medium,
modifier = Modifier.fillMaxSize()
)
}
}
}
fun Modifier.verticalFadeMask(vararg stops: Pair<Float, Float>): Modifier =
this
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
val brush = Brush.verticalGradient(
*Array(stops.size) { stops[it].first to Color.Black.copy(alpha = stops[it].second) }
)
onDrawWithContent {
drawContent()
drawRect(brush = brush, blendMode = BlendMode.DstIn)
}
}

View File

@@ -26,6 +26,7 @@ import coil.compose.AsyncImage
import tv.vyla.app.model.MediaItem
import tv.vyla.app.ui.theme.VylaCardBackground
import tv.vyla.app.ui.theme.VylaTextPrimary
import androidx.compose.ui.graphics.graphicsLayer
@Composable
fun MediaCard(
@@ -48,7 +49,10 @@ fun MediaCard(
modifier = modifier
.width(width.dp)
.aspectRatio(2f / 3f)
.scale(scale)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.clip(shape)
.background(VylaCardBackground)
.then(

View File

@@ -51,6 +51,7 @@ enum class NavDestination(val key: String, val title: String, val icon: ImageVec
MOVIES("movie", "Movies", Icons.Default.Movie),
TV_SHOWS("tv", "TV Shows", Icons.Default.Tv),
ANIME("anime", "Anime", Icons.Default.AutoAwesome),
HISTORY("history", "History", Icons.Default.History),
WATCH_LATER("wl", "Watch Later", Icons.Default.Schedule),
FAVORITES("fav", "Favorites", Icons.Default.Favorite)
}
@@ -59,6 +60,7 @@ enum class NavDestination(val key: String, val title: String, val icon: ImageVec
@OptIn(ExperimentalComposeUiApi::class)
fun Sidebar(
currentDestination: NavDestination,
hasHistory: Boolean,
hasWatchLater: Boolean,
hasFavorites: Boolean,
onNavigate: (NavDestination) -> Unit,
@@ -148,6 +150,7 @@ fun Sidebar(
) {
NavDestination.values().forEach { destination ->
val shouldShow = when (destination) {
NavDestination.HISTORY -> hasHistory || currentDestination == NavDestination.HISTORY
NavDestination.WATCH_LATER -> hasWatchLater || currentDestination == NavDestination.WATCH_LATER
NavDestination.FAVORITES -> hasFavorites || currentDestination == NavDestination.FAVORITES
else -> true

View File

@@ -88,7 +88,7 @@ fun VideoTintSampler(active: Boolean, viewProvider: () -> View?) {
bitmap.getPixels(pixels, 0, GRID_W, 0, 0, GRID_W, GRID_H)
VideoTintState.frame = VideoFrameColors(pixels.copyOf(), GRID_W, GRID_H)
}
delay(400)
delay(700)
}
}
}
@@ -163,7 +163,7 @@ fun BackdropTintSampler(url: String?) {
}
val request = ImageRequest.Builder(context)
.data(url)
.size(96, 54)
.size(48, 27)
.allowHardware(false)
.build()
val result = context.imageLoader.execute(request)

View File

@@ -57,6 +57,10 @@ import kotlinx.coroutines.withTimeoutOrNull
import tv.vyla.app.ui.components.ContentFocusBridge
import tv.vyla.app.ui.components.BackdropTintSampler
import tv.vyla.app.ui.components.videoTint
import androidx.compose.foundation.lazy.itemsIndexed
import tv.vyla.app.ui.components.AmbientBackdrop
import tv.vyla.app.ui.components.HeroFadeStops
import tv.vyla.app.ui.components.verticalFadeMask
data class GenreRow(
@@ -93,17 +97,24 @@ fun BrowseScreen(
val listState = rememberLazyListState()
val cardRequesters = remember { mutableMapOf<String, FocusRequester>() }
var lastFocusedKey by remember { mutableStateOf<String?>(null) }
var ambientUrl by remember { mutableStateOf<String?>(null) }
var lastRowIndex by remember { mutableIntStateOf(-1) }
fun updateHero(item: MediaItem) {
heroJob?.cancel()
if (heroItem?.id == item.id && heroItem?.mediaType == item.mediaType) return
heroJob = coroutineScope.launch {
delay(130)
delay(200)
heroItem = item
ambientUrl = item.ambientBackdropUrl
heroLogoUrl = TmdbApi.getLogo(item.mediaType, item.id)
}
}
fun pinAmbientToHero() {
heroItem?.let { ambientUrl = it.ambientBackdropUrl }
}
suspend fun loadMoreRows(count: Int) {
if (isLoadingMore) return
if (isLoadingMore || rows.size >= 48) return
isLoadingMore = true
val generation = loadGeneration
val activeMode = currentMode
@@ -147,6 +158,7 @@ suspend fun loadMoreRows(count: Int) {
val first = rows[0].items.firstOrNull()
if (first != null) {
heroItem = first
ambientUrl = first.ambientBackdropUrl
coroutineScope.launch {
heroLogoUrl = TmdbApi.getLogo(first.mediaType, first.id)
}
@@ -191,6 +203,8 @@ LaunchedEffect(mode) {
rows.clear()
rowIdx = 0
heroItem = null
ambientUrl = null
lastRowIndex = -1
heroLogoUrl = null
movieGenres.clear()
tvGenres.clear()
@@ -215,22 +229,11 @@ LaunchedEffect(mode) {
BackdropTintSampler(heroItem?.ambientBackdropUrl)
Box(modifier = modifier.fillMaxSize().background(VylaBackground)) {
Crossfade(
targetState = heroItem?.ambientBackdropUrl,
animationSpec = tween(700),
label = "ambient_bg"
) { url ->
if (!url.isNullOrEmpty()) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.blur(50.dp)
)
}
}
AmbientBackdrop(
url = ambientUrl,
modifier = Modifier.fillMaxSize(),
durationMs = 700
)
Box(
modifier = Modifier
.fillMaxSize()
@@ -254,6 +257,8 @@ LaunchedEffect(mode) {
playButtonFocusRequester = playButtonFocusRequester,
onFocusedTop = {
lastFocusedKey = null
lastRowIndex = -1
pinAmbientToHero()
coroutineScope.launch {
listState.animateScrollToItem(0)
}
@@ -268,9 +273,8 @@ LaunchedEffect(mode) {
}
)
}
items(rows) { row ->
itemsIndexed(rows, key = { i, row -> "$i:${row.title}" }) { rowIndex, row ->
val rowState = rememberLazyListState()
val rowIndex = rows.indexOf(row)
Column(modifier = Modifier.padding(vertical = 10.dp)) {
Text(
text = row.title,
@@ -284,10 +288,9 @@ LaunchedEffect(mode) {
contentPadding = PaddingValues(horizontal = 78.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(row.items) { item ->
val index = row.items.indexOf(item)
val isFirstInRow = row.items.firstOrNull() == item
val cardKey = "${row.title}:${item.id}"
itemsIndexed(row.items, key = { _, it -> it.id }) { index, item ->
val isFirstInRow = index == 0
val cardKey = "$rowIndex:${item.id}"
val cardRequester = cardRequesters.getOrPut(cardKey) { FocusRequester() }
MediaCard(
item = item,
@@ -302,11 +305,16 @@ LaunchedEffect(mode) {
index,
-(viewportWidth / 2 - 68)
)
val viewportHeight = listState.layoutInfo.viewportEndOffset - listState.layoutInfo.viewportStartOffset
listState.animateScrollToItem(
rowIndex + 1,
-(viewportHeight / 2 - 100)
)
}
if (lastRowIndex != rowIndex) {
lastRowIndex = rowIndex
coroutineScope.launch {
val viewportHeight = listState.layoutInfo.viewportEndOffset - listState.layoutInfo.viewportStartOffset
listState.animateScrollToItem(
rowIndex + 1,
-(viewportHeight / 2 - 100)
)
}
}
},
modifier = Modifier.focusRequester(cardRequester).onKeyEvent { event ->
@@ -345,39 +353,32 @@ private fun HeroSection(
.fillMaxWidth()
.height(460.dp)
) {
val backdropUrl = item?.backdropUrl
if (!backdropUrl.isNullOrEmpty()) {
AsyncImage(
model = backdropUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
drawRect(
brush = Brush.verticalGradient(
0.0f to Color.Black,
0.45f to Color.Black,
1.0f to Color.Transparent
),
blendMode = BlendMode.DstIn
)
}
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(
0.0f to Color(0xCC000000),
0.40f to Color(0x59000000),
0.70f to Color.Transparent
)
.verticalFadeMask(*HeroFadeStops)
) {
val backdropUrl = item?.backdropUrl
if (!backdropUrl.isNullOrEmpty()) {
AsyncImage(
model = backdropUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(
0.0f to Color(0xCC000000),
0.40f to Color(0x59000000),
0.70f to Color.Transparent
)
)
)
}
Column(
modifier = Modifier
.align(Alignment.BottomStart)
@@ -503,7 +504,10 @@ private fun HeroPillButton(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.scale(scale)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.height(48.dp)
.clip(RoundedCornerShape(24.dp))
.then(

View File

@@ -109,6 +109,12 @@ import tv.vyla.app.model.*
import tv.vyla.app.ui.components.MediaCard
import tv.vyla.app.ui.theme.*
import androidx.compose.ui.graphics.FilterQuality
import coil.request.ImageRequest
import tv.vyla.app.ui.components.AmbientBackdrop
import tv.vyla.app.ui.components.HeroFadeStops
import tv.vyla.app.ui.components.verticalFadeMask
@Composable
fun DetailScreen(
@@ -141,7 +147,6 @@ fun DetailScreen(
if (d != null && d.seasons.isNotEmpty()) {
val initialSeason = d.seasons.firstOrNull { it.seasonNumber == 1 } ?: d.seasons.first()
selectedSeason = initialSeason
seasonEpisodes = TmdbApi.getSeasonEpisodes(item.id, initialSeason.seasonNumber)
}
delay(150)
try {
@@ -190,42 +195,23 @@ fun DetailScreen(
}
}
Box(modifier = modifier.fillMaxSize().background(Color(0xFF07070A))) {
val bg = item.ambientBackdropUrl
if (!bg.isNullOrEmpty()) {
AsyncImage(
model = bg,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.blur(60.dp)
)
}
AmbientBackdrop(
url = item.ambientBackdropUrl,
modifier = Modifier.fillMaxSize()
)
val backdrop = item.backdropUrl
if (!backdrop.isNullOrEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.82f)
.verticalFadeMask(*HeroFadeStops)
) {
AsyncImage(
model = backdrop,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
drawRect(
brush = Brush.verticalGradient(
0.0f to Color.Black,
0.45f to Color.Black,
1.0f to Color.Transparent
),
blendMode = BlendMode.DstIn
)
}
modifier = Modifier.fillMaxSize()
)
}
}
@@ -952,15 +938,27 @@ private fun DetailTrailerCard(
onFocused: (() -> Unit)? = null,
onClick: () -> Unit
) {
val context = LocalContext.current
var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (isFocused) 1.05f else 1.0f, label = "tr_scale")
val shape = RoundedCornerShape(16.dp)
val tinyRequest = remember(trailer.key) {
ImageRequest.Builder(context)
.data(trailer.thumbnailUrl)
.size(32, 18)
.allowHardware(false)
.build()
}
Box(
modifier = Modifier
.width(280.dp)
.scale(scale)
.clip(RoundedCornerShape(16.dp))
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.clip(shape)
.background(Color(0xFF101016))
.then(if (isFocused) Modifier.border(2.dp, Color.White, RoundedCornerShape(16.dp)) else Modifier)
.then(if (isFocused) Modifier.border(2.dp, Color.White, shape) else Modifier)
.onFocusChanged {
isFocused = it.isFocused
if (it.isFocused) onFocused?.invoke()
@@ -971,11 +969,35 @@ private fun DetailTrailerCard(
indication = null
) { onClick() }
) {
AsyncImage(
model = tinyRequest,
contentDescription = null,
contentScale = ContentScale.Crop,
filterQuality = FilterQuality.Medium,
modifier = Modifier.matchParentSize()
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
0.0f to Color(0x26000000),
1.0f to Color(0x80000000)
)
)
)
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.verticalFadeMask(
0f to 1f,
0.55f to 1f,
0.72f to 0.72f,
0.86f to 0.30f,
1f to 0f
)
) {
AsyncImage(
model = trailer.thumbnailUrl,
@@ -998,62 +1020,43 @@ private fun DetailTrailerCard(
modifier = Modifier.size(26.dp)
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.align(Alignment.BottomCenter)
.background(
Brush.verticalGradient(
0.0f to Color.Transparent,
1.0f to Color(0xE6101016)
)
)
)
}
Box(
Column(
modifier = Modifier
.fillMaxWidth()
.height(76.dp)
.padding(start = 14.dp, end = 14.dp, bottom = 14.dp)
) {
AsyncImage(
model = trailer.thumbnailUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.blur(36.dp)
Text(
text = trailer.type.uppercase(),
color = Color(0xB3FFFFFF),
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = 1.2.sp
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
0.0f to Color(0xE6101016),
0.35f to Color(0x99101016),
1.0f to Color(0xF2101016)
)
)
Text(
text = trailer.name,
color = Color.White,
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(top = 6.dp)
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.Center
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 8.dp)
) {
Text(
text = trailer.name,
color = Color(0xFFF2F2F5),
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
tint = Color(0xCCFFFFFF),
modifier = Modifier.size(14.dp)
)
Text(
text = "▶ ${trailer.type}",
color = Color(0xFFD0D0DA),
text = trailer.site,
color = Color(0xCCFFFFFF),
fontSize = 12.sp,
modifier = Modifier.padding(top = 4.dp)
modifier = Modifier.padding(start = 4.dp)
)
}
}

View File

@@ -27,32 +27,23 @@ import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage
import tv.vyla.app.ui.components.AmbientBackdrop
@Composable
fun LibraryScreen(
title: String,
items: List<MediaItem>,
onOpenDetail: (MediaItem) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
emptyMessage: String = "Nothing here yet. Open a title, press the ⋮ button and add it."
) {
var ambientUrl by remember(items) { mutableStateOf(items.firstOrNull()?.ambientBackdropUrl) }
Box(modifier = modifier.fillMaxSize().background(VylaBackground)) {
Crossfade(
targetState = ambientUrl,
animationSpec = tween(700),
label = "library_ambient"
) { url ->
if (!url.isNullOrEmpty()) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.blur(50.dp)
)
}
}
AmbientBackdrop(
url = ambientUrl,
modifier = Modifier.fillMaxSize(),
durationMs = 700
)
Box(
modifier = Modifier
.fillMaxSize()
@@ -96,7 +87,7 @@ fun LibraryScreen(
contentAlignment = androidx.compose.ui.Alignment.TopStart
) {
Text(
text = "Nothing here yet. Open a title, press the ⋮ button and add it.",
text = emptyMessage,
color = VylaTextMuted,
fontSize = 17.sp
)

View File

@@ -136,6 +136,7 @@ fun PlayerScreen(
val setButtonFocusRequester = remember { FocusRequester() }
val moreButtonFocusRequester = remember { FocusRequester() }
var hideControlsJob by remember { mutableStateOf<Job?>(null) }
val playerContainerFocusRequester = remember { FocusRequester() }
fun resetHideTimer() {
hideControlsJob?.cancel()
if (activeMenu == ActivePlayerMenu.NONE && !isEpisodesDrawerOpen && isPlaying) {
@@ -144,6 +145,9 @@ fun PlayerScreen(
if (isPlaying && activeMenu == ActivePlayerMenu.NONE && !isEpisodesDrawerOpen) {
areControlsVisible = false
}
try {
playerContainerFocusRequester.requestFocus()
} catch (_: Exception) {}
}
}
}
@@ -178,17 +182,22 @@ fun PlayerScreen(
}
LaunchedEffect(exoPlayer) {
while (true) {
currentPositionMs = exoPlayer.currentPosition.coerceAtLeast(0L)
bufferedPositionMs = exoPlayer.bufferedPosition.coerceAtLeast(0L)
val pos = exoPlayer.currentPosition.coerceAtLeast(0L)
val dur = exoPlayer.duration
if (dur > 0L) durationMs = dur
val currentSec = currentPositionMs / 1000.0
if (dur > 0L && dur != durationMs) durationMs = dur
val nearEnd = dur > 0L && dur - pos <= 300000L
if (areControlsVisible || nearEnd) {
if (pos != currentPositionMs) currentPositionMs = pos
val buffered = exoPlayer.bufferedPosition.coerceAtLeast(0L)
if (buffered != bufferedPositionMs) bufferedPositionMs = buffered
}
val currentSec = pos / 1000.0
val seg = segments.firstOrNull { s ->
val endSec = s.end
currentSec >= s.start && (endSec == null || currentSec < endSec)
}
activeSegment = seg
delay(300)
if (seg != activeSegment) activeSegment = seg
delay(if (areControlsVisible) 300L else 600L)
}
}
DisposableEffect(Unit) {
@@ -415,6 +424,7 @@ fun PlayerScreen(
modifier = modifier
.fillMaxSize()
.background(Color.Black)
.focusRequester(playerContainerFocusRequester)
.focusable()
.onKeyEvent { event ->
if (event.type == KeyEventType.KeyDown) {

View File

@@ -47,6 +47,9 @@ import tv.vyla.app.model.MediaItem
import tv.vyla.app.ui.theme.*
import androidx.compose.ui.graphics.vector.ImageVector
import tv.vyla.app.ui.components.SearchIcons
import tv.vyla.app.ui.components.AmbientBackdrop
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.graphicsLayer
@Composable
@@ -131,22 +134,10 @@ fun SearchScreen(
false
}
) {
Crossfade(
targetState = focusedItem?.ambientBackdropUrl,
animationSpec = tween(600),
label = "search_bg"
) { url ->
if (!url.isNullOrEmpty()) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.blur(70.dp)
)
}
}
AmbientBackdrop(
url = focusedItem?.ambientBackdropUrl,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.fillMaxSize()
@@ -192,7 +183,7 @@ fun SearchScreen(
.padding(start = 6.dp)
.width(3.dp)
.height(30.dp)
.background(Color.White.copy(alpha = cursorAlpha))
.drawBehind { drawRect(Color.White, alpha = cursorAlpha) }
)
}
}
@@ -566,7 +557,10 @@ private fun SearchMediaCard(
Column(
modifier = Modifier
.width(140.dp)
.scale(scale)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.offset(y = if (isFocused) (-3).dp else 0.dp)
.onFocusChanged {
isFocused = it.isFocused

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.darkColorScheme
import androidx.compose.ui.graphics.graphicsLayer
val VylaBackground = Color(0xFF07070A)
val VylaCardBackground = Color(0xFF1B1B24)
@@ -60,7 +61,10 @@ fun Modifier.tvFocusable(
)
return this
.scale(scale)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.then(
if (isFocused) {
Modifier

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "tv",
"version": "1.0.4",
"version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tv",
"version": "1.0.4",
"version": "1.0.7",
"license": "ISC",
"dependencies": {
"@vyla-entertainment/sdk": "^1.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "tv",
"version": "1.0.4",
"version": "1.0.7",
"description": "",
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "Vyla TV",
"version": "1.0.3",
"package": "Vyla-TV_1.0.3.apk",
"releaseDate": "2026-10-05T18:09:19.183Z"
"version": "1.0.6",
"package": "Vyla-TV_1.0.6.apk",
"releaseDate": "2026-10-06T03:41:56.445Z"
}