new sources + rearanged some

This commit is contained in:
XSWY 2026-03-29 15:55:13 +00:00
parent eff6660ddf
commit 25bd750c9e
21 changed files with 1245 additions and 157 deletions

View file

@ -1,4 +1,11 @@
import { Embed, Sourcerer } from '@/providers/base';
import { AnimekaiScraper } from './embeds/animekai';
import { animekaiScraper } from './sources/animekai';
import { vidkingEmbedScraper } from './embeds/vidking';
import { vidkingScraper } from './sources/vidKing';
import { FedAPIScraper } from './sources/fed/fedapi';
//import { FedAPIDBScraper } from './sources/fed/fedapidb';
import { xprimeScraper } from './sources/fed/xprime';
import { doodScraper } from '@/providers/embeds/dood';
import { filemoonScraper } from '@/providers/embeds/filemoon';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
@ -13,12 +20,11 @@ import { thunderleafScraper } from '@/providers/sources/florence/thunderleaf';
import { fsharetvScraper } from '@/providers/sources/disabled/fsharetv';
import { fsOnlineEmbeds, fsOnlineScraper } from '@/providers/sources/fsonline/index';
import { insertunitScraper } from '@/providers/sources/disabled/insertunit';
import { movieboxScraper } from '@/providers/sources/Moviebox/moviebox';
import { movieboxScraper } from '@/providers/sources/disabled/moviebox';
import { flixerScraper } from '@/providers/sources/Moviebox/flixerz';
import { hdmovieScraper } from '@/providers/sources/Moviebox/hdmovie';
import { zyonScraper } from './sources/Moviebox/zyon';
import { smashyScraper } from '@/providers/sources/florence/smashy';
import { xprimeScraper } from '@/providers/sources/florence/xprime';
import { mp4hydraScraper } from '@/providers/sources/disabled/mp4hydra';
import { pirxcyScraper } from '@/providers/sources/disabled/pirxcy';
import { tugaflixScraper } from '@/providers/sources/tugaflix';
@ -91,7 +97,7 @@ import { nunflixScraper } from './sources/disabled/nunflix';
import { pelisplushdScraper } from './sources/pelisplushd';
import { primewireScraper } from './sources/disabled/primewire';
import { rgshowsScraper } from './sources/rgshows';
import { ridooMoviesScraper } from './sources/disabled/ridomovies';
import { ridooMoviesScraper } from './sources/ridomovies';
import { slidemoviesScraper } from './sources/disabled/slidemovies';
import { soaperTvScraper } from './sources/disabled/soapertv';
import { streamboxScraper } from './sources/disabled/streambox';
@ -128,12 +134,12 @@ export function gatherAllSources(): Array<Sourcerer> {
slidemoviesScraper,
vidapiClickScraper,
hdmovieScraper,
xprimeScraper,
flixerScraper,
coitusScraper,
streamboxScraper,
nunflixScraper,
EightStreamScraper,
xprimeScraper,
movieboxScraper,
wecimaScraper,
animeflvScraper,
@ -151,6 +157,10 @@ export function gatherAllSources(): Array<Sourcerer> {
debridScraper,
cinehdplusScraper,
fullhdfilmizleScraper,
animekaiScraper,
FedAPIScraper,
//FedAPIDBScraper,
vidkingScraper
];
}
@ -217,5 +227,7 @@ export function gatherAllEmbeds(): Array<Embed> {
droploadScraper,
supervideoScraper,
voeScraper,
AnimekaiScraper,
vidkingEmbedScraper
];
}

View file

@ -0,0 +1,74 @@
import { EmbedOutput, makeEmbed } from '@/providers/base';
import { NotFoundError } from '@/utils/errors';
import { Caption, labelToLanguageCode } from '../captions';
interface StreamData {
headers: {
Referer: string;
Origin?: string;
};
sources: Array<{
url: string;
isM3U8: boolean;
}>;
subtitles?: Array<{
url: string;
lang?: string;
kind?: string;
}>;
}
export const AnimekaiScraper = makeEmbed({
id: 'animekai-embed',
name: 'AnimeKai',
rank: 415,
flags: [], // ← REQUIRED
async scrape(ctx): Promise<EmbedOutput> {
const { episodeId } = JSON.parse(ctx.url);
const data = await ctx.fetcher<StreamData>(
`https://api.1anime.app/anime/animekai/watch/${encodeURIComponent(episodeId)}`,
);
if (!data?.sources?.length) throw new NotFoundError('No stream found');
ctx.progress(50);
const captions: Caption[] = (data.subtitles ?? [])
.filter((sub) => sub.lang && sub.kind !== 'thumbnails')
.map((sub) => ({
type: 'vtt',
id: sub.url,
url: sub.url,
language: labelToLanguageCode(sub.lang!.replace(/_\[.*?\]$/, '').trim()) || 'unknown',
hasCorsRestrictions: true,
}));
const hlsSource = data.sources.find((s) => s.isM3U8);
if (!hlsSource) throw new NotFoundError('No HLS stream found');
ctx.progress(90);
const headers: Record<string, string> = {};
if (data.headers.Referer) {
headers.Referer = data.headers.Referer;
try {
headers.Origin = new URL(data.headers.Referer).origin;
} catch {}
}
if (data.headers.Origin) headers.Origin = data.headers.Origin;
return {
stream: [
{
id: 'primary',
captions,
playlist: hlsSource.url,
headers,
type: 'hls',
flags: [],
},
],
};
},
});

View file

@ -0,0 +1,101 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeEmbed } from '@/providers/base';
const userAgent =
'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36';
export const vidkingEmbedScraper = makeEmbed({
id: 'vidking',
name: 'VidKing',
rank: 175,
flags: [flags.CORS_ALLOWED],
async scrape(ctx) {
const url = ctx.url;
const parsedUrl = new URL(url);
const origin = parsedUrl.origin;
const headers: Record<string, string> = {
'User-Agent': userAgent,
Referer: origin,
Origin: origin,
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
};
const html = await ctx.proxiedFetcher<string>(url, { headers });
const m3u8Match = html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
const sourceMatch = html.match(/src["']?\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
const manifestMatch = html.match(/(https?:\/\/[^"']+\.m3u8[^"']*)/i);
const hlsMatch = html.match(/hls["']?\s*[:=]\s*["']([^"']+)["']/i);
let videoUrl = m3u8Match?.[1] || manifestMatch?.[1] || hlsMatch?.[1] || sourceMatch?.[1];
if (!videoUrl) {
const scriptMatch = html.match(/var\s+(?:source|video|url|hlsUrl|manifest|stream)\s*=\s*["']([^"']+)["']/i);
if (scriptMatch) {
videoUrl = scriptMatch[1];
}
}
if (!videoUrl) {
const jsonMatch = html.match(/\{[^}]*"(?:src|source|url|hls|manifest)"\s*:\s*"([^"]+)"/i);
if (jsonMatch) {
videoUrl = jsonMatch[1];
}
}
if (!videoUrl) {
throw new Error('No video URL found in VidKing embed');
}
if (videoUrl.startsWith('//')) {
videoUrl = `https:${videoUrl}`;
} else if (videoUrl.startsWith('/')) {
videoUrl = `${origin}${videoUrl}`;
}
const isHls = videoUrl.includes('.m3u8') || /\.m3u8[?#]/.test(videoUrl);
if (isHls) {
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: videoUrl,
flags: [flags.CORS_ALLOWED],
captions: [],
headers: {
Referer: origin,
Origin: origin,
'User-Agent': userAgent,
},
},
],
};
}
return {
stream: [
{
id: 'primary',
type: 'file',
flags: [flags.CORS_ALLOWED],
captions: [],
qualities: {
unknown: {
type: 'mp4',
url: videoUrl,
},
},
preferredHeaders: {
Referer: origin,
Origin: origin,
'User-Agent': userAgent,
},
},
],
};
},
});

View file

@ -1,56 +1,180 @@
// src/providers/sources/flixerz/flixerz.ts
import { flags } from '@/entrypoint/utils/targets';
import { makeSourcerer, SourcererOutput } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const API_BASE = 'https://api.videasy.net';
const PROVIDER = 'flixerz';
async function fetchflixerStream(
// Helper: Detect hex token (64+ hex chars)
function isHexToken(str: string): boolean {
return /^[0-9a-f]{64,}$/i.test(str.trim());
}
// Helper: Extract body text from various response shapes
function extractResponseBody(res: unknown): string {
if (typeof res === 'string') return res;
if (res instanceof Buffer) return res.toString('utf-8');
if (res && typeof res === 'object') {
const obj = res as Record<string, unknown>;
// Common response body locations
const bodyFields = ['data', 'body', 'text', 'content', 'response'];
for (const field of bodyFields) {
if (typeof obj[field] === 'string') {
return obj[field] as string;
}
// Nested object with text/data inside
if (obj[field] && typeof obj[field] === 'object') {
const nested = obj[field] as Record<string, unknown>;
if (typeof nested.data === 'string') return nested.data;
if (typeof nested.body === 'string') return nested.body;
if (typeof nested.text === 'string') return nested.text;
}
}
// Try JSON stringify as last resort
return JSON.stringify(obj);
}
return String(res);
}
// Helper: Build playback URLs from token
function buildPlaybackUrls(token: string): string[] {
return [
`${API_BASE}/${PROVIDER}/play?token=${token}`,
`${API_BASE}/${PROVIDER}/stream?token=${token}`,
`${API_BASE}/${PROVIDER}/source?token=${token}`,
`${API_BASE}/${PROVIDER}/play/${token}`,
`${API_BASE}/${PROVIDER}/hls/${token}.m3u8`,
`https://cdn.videasy.net/${PROVIDER}/${token}/index.m3u8`,
`https://cdn.videasy.net/hls/${token}.m3u8?provider=${PROVIDER}`,
];
}
async function fetchFlixerzStream(
ctx: MovieScrapeContext | ShowScrapeContext,
type: 'movie' | 'tv',
): Promise<SourcererOutput> {
const params = new URLSearchParams({
title: ctx.media.title,
mediaType: type,
year: String(ctx.media.releaseYear),
tmdbId: String(ctx.media.tmdbId),
imdbId: ctx.media.imdbId ?? '',
providerId: PROVIDER,
});
if (type === 'tv') {
const showCtx = ctx as ShowScrapeContext;
if (showCtx.media?.season?.number && showCtx.media?.episode?.number) {
params.append('seasonId', String(showCtx.media.season.number));
params.append('episodeId', String(showCtx.media.episode.number));
}
}
const tokenUrl = `${API_BASE}/${PROVIDER}/sources-with-title?${params.toString()}`;
try {
const params = new URLSearchParams({
title: ctx.media.title,
mediaType: type,
year: String(ctx.media.releaseYear),
tmdbId: String(ctx.media.tmdbId),
imdbId: ctx.media.imdbId ?? '',
episodeId:
type === 'tv' && 'episode' in ctx.media
? String(ctx.media.episode.number)
: '1',
seasonId:
type === 'tv' && 'season' in ctx.media
? String(ctx.media.season.number)
: '1',
// Fetch without generic type to get raw response
const rawResponse = await ctx.proxiedFetcher(tokenUrl, {
headers: {
Accept: '*/*',
'User-Agent': 'Mozilla/5.0',
Referer: 'https://videasy.net/',
Origin: 'https://videasy.net',
},
});
const url = `${API_BASE}/myflixerzupcloud/sources-with-title?${params.toString()}`;
// Debug: Log response structure
console.log('[flixerz] Raw response type:', typeof rawResponse);
console.log('[flixerz] Raw response keys:', Object.keys(rawResponse || {}));
// Extract body text
const token = extractResponseBody(rawResponse).trim();
// Debug: Log extracted token
console.log('[flixerz] Extracted token preview:', token.slice(0, 100));
console.log('[flixerz] Token length:', token.length);
if (!isHexToken(token)) {
console.error(`[flixerz] Expected hex token, got: ${token.slice(0, 200)}`);
throw new Error('Invalid token response from API');
}
const stream = {
id: 'flixer',
type: 'hls' as const,
playlist: url,
flags: [flags.CORS_ALLOWED],
captions: [],
};
console.log(`[flixerz] Got token (${token.length} chars), testing playback URLs...`);
const candidates = buildPlaybackUrls(token);
for (const candidate of candidates) {
try {
const testRes = await ctx.proxiedFetcher<string>(candidate, {
headers: {
Referer: 'https://videasy.net/',
Origin: 'https://videasy.net',
},
});
if (testRes.startsWith('#EXTM3U') || testRes.includes('#EXTINF:')) {
console.log(`[flixerz] ✅ Found working playlist: ${candidate}`);
return {
embeds: [],
stream: [
{
id: 'flixerz-primary',
type: 'hls',
playlist: candidate,
headers: {
referer: 'https://videasy.net/',
origin: 'https://videasy.net',
},
flags: [flags.CORS_ALLOWED],
captions: [],
},
],
};
}
} catch {
continue;
}
}
// Fallback to first pattern
const fallback = candidates[0];
console.log(`[flixerz] Using fallback: ${fallback}`);
return {
embeds: [],
stream: [stream],
stream: [
{
id: 'flixerz-primary',
type: 'hls',
playlist: fallback,
headers: {
referer: 'https://videasy.net/',
origin: 'https://videasy.net',
},
flags: [flags.CORS_ALLOWED],
captions: [],
},
],
};
} catch {
throw new NotFoundError('flixerz stream not found');
} catch (err) {
console.error('[flixerz] Error:', err);
throw new NotFoundError('Flixerz stream not found');
}
}
export const flixerScraper = makeSourcerer({
id: 'flixerz',
name: 'Flixerz',
rank: 17,
rank: 99,
disabled: false,
flags: [flags.CORS_ALLOWED],
scrapeMovie: (ctx: MovieScrapeContext) => fetchflixerStream(ctx, 'movie'),
scrapeShow: (ctx: ShowScrapeContext) => fetchflixerStream(ctx, 'tv'),
scrapeMovie: (ctx) => fetchFlixerzStream(ctx, 'movie'),
scrapeShow: (ctx) => fetchFlixerzStream(ctx, 'tv'),
});

View file

@ -1,56 +0,0 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeSourcerer, SourcererOutput } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const API_BASE = 'https://api.videasy.net';
async function fetchMovieboxStream(
ctx: MovieScrapeContext | ShowScrapeContext,
type: 'movie' | 'tv',
): Promise<SourcererOutput> {
try {
const params = new URLSearchParams({
title: ctx.media.title,
mediaType: type,
year: String(ctx.media.releaseYear),
tmdbId: String(ctx.media.tmdbId),
imdbId: ctx.media.imdbId ?? '',
episodeId:
type === 'tv' && 'episode' in ctx.media
? String(ctx.media.episode.number)
: '1',
seasonId:
type === 'tv' && 'season' in ctx.media
? String(ctx.media.season.number)
: '1',
});
const url = `${API_BASE}/moviebox/sources-with-title?${params.toString()}`;
const stream = {
id: 'moviebox',
type: 'hls' as const,
playlist: url,
flags: [flags.CORS_ALLOWED],
captions: [],
};
return {
embeds: [],
stream: [stream],
};
} catch {
throw new NotFoundError('Moviebox stream not found');
}
}
export const movieboxScraper = makeSourcerer({
id: 'moviebox',
name: 'Moviebox',
rank: 99,
disabled: false,
flags: [flags.CORS_ALLOWED],
scrapeMovie: (ctx: MovieScrapeContext) => fetchMovieboxStream(ctx, 'movie'),
scrapeShow: (ctx: ShowScrapeContext) => fetchMovieboxStream(ctx, 'tv'),
});

View file

@ -0,0 +1,63 @@
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const consumetBase = 'https://api.1anime.app/anime/animekai';
interface SearchResult {
id: string;
title: string;
}
interface SearchResponse {
results: SearchResult[];
}
interface Episode {
id: string;
number: number;
}
interface InfoResponse {
episodes: Episode[];
}
function normalizeTitle(s: string): string {
return s
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.toLowerCase();
}
async function searchAnime(ctx: ShowScrapeContext, title: string): Promise<string> {
const data = await ctx.fetcher<SearchResponse>(`${consumetBase}/${encodeURIComponent(title)}`);
if (!data?.results?.length) throw new NotFoundError('Anime not found on AnimeKai');
const normalizedTitle = normalizeTitle(title);
const exact = data.results.find((r) => normalizeTitle(r.title) === normalizedTitle);
return (exact ?? data.results[0]).id;
}
async function scrapeAnimekai(ctx: ShowScrapeContext): Promise<SourcererOutput> {
const title = ctx.media.title;
const episodeNumber = ctx.media.episode.number;
const animeId = await searchAnime(ctx, title);
const info = await ctx.fetcher<InfoResponse>(`${consumetBase}/info?id=${animeId}`);
if (!info?.episodes?.length) throw new NotFoundError('No episodes found on AnimeKai');
const ep = info.episodes.find((e) => e.number === episodeNumber);
if (!ep) throw new NotFoundError('Episode not found on AnimeKai');
return {
embeds: [{ embedId: 'animekai-embed', url: JSON.stringify({ episodeId: ep.id }) }],
};
}
export const animekaiScraper = makeSourcerer({
id: 'animekai',
name: 'AnimeKai 🔥',
rank: 93,
flags: [],
scrapeShow: scrapeAnimekai,
});

View file

@ -0,0 +1,74 @@
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { createM3U8ProxyUrl } from '@/utils/proxy';
const baseUrl = 'https://api.coitus.ca';
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const apiUrl =
ctx.media.type === 'movie'
? `${baseUrl}/movie/${ctx.media.tmdbId}`
: `${baseUrl}/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`;
const apiRes = await ctx.proxiedFetcher(apiUrl);
if (!apiRes.videoSource) throw new NotFoundError('No watchable item found');
let processedUrl = apiRes.videoSource;
let streamHeaders: Record<string, string> = {};
if (processedUrl.includes('orbitproxy')) {
try {
const urlParts = processedUrl.split(/orbitproxy\.[^/]+\//);
if (urlParts.length >= 2) {
const encryptedPart = urlParts[1].split('.m3u8')[0];
try {
const decodedData = Buffer.from(encryptedPart, 'base64').toString('utf-8');
const jsonData = JSON.parse(decodedData);
const originalUrl = jsonData.u;
const referer = jsonData.r || '';
streamHeaders = { referer };
processedUrl = createM3U8ProxyUrl(originalUrl, ctx.features, streamHeaders);
} catch (jsonError) {
console.error('Error decoding/parsing orbitproxy data:', jsonError);
}
}
} catch (error) {
console.error('Error processing orbitproxy URL:', error);
}
}
// eslint-disable-next-line no-console
console.log(apiRes);
ctx.progress(90);
return {
embeds: [],
stream: [
{
id: 'primary',
captions: [],
playlist: processedUrl,
type: 'hls',
headers: streamHeaders,
flags: [flags.CORS_ALLOWED],
},
],
};
}
export const coitusScraper = makeSourcerer({
id: 'coitus',
name: 'Autoembed+',
rank: 94,
disabled: true,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -0,0 +1,114 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeSourcerer, SourcererOutput } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const API_BASE = 'https://api.videasy.net';
interface MovieboxApiResponse {
success: boolean;
data?: {
streamUrl?: string;
hls?: string;
url?: string;
sources?: Array<{ file: string; type: string }>;
};
}
async function fetchMovieboxStream(
ctx: MovieScrapeContext | ShowScrapeContext,
type: 'movie' | 'tv',
): Promise<SourcererOutput> {
const params = new URLSearchParams({
title: ctx.media.title,
mediaType: type,
year: String(ctx.media.releaseYear),
tmdbId: String(ctx.media.tmdbId),
imdbId: ctx.media.imdbId ?? '',
providerId: 'moviebox',
});
if (type === 'tv') {
const showCtx = ctx as ShowScrapeContext;
if (showCtx.media?.season?.number && showCtx.media?.episode?.number) {
params.append('seasonId', String(showCtx.media.season.number));
params.append('episodeId', String(showCtx.media.episode.number));
}
}
const apiUrl = `${API_BASE}/moviebox/sources-with-title?${params.toString()}`;
// DEBUG: Log the request URL
console.log('[moviebox] Request URL:', apiUrl);
try {
// Fetch as text first to see raw response
const rawResponse = await ctx.proxiedFetcher<string>(apiUrl, {
headers: {
Accept: 'application/json',
'User-Agent': 'Mozilla/5.0',
},
});
// DEBUG: Log raw response
console.log('[moviebox] Raw API Response:', rawResponse.slice(0, 2000));
// Try to parse as JSON
let response: MovieboxApiResponse;
try {
response = JSON.parse(rawResponse);
} catch (parseErr) {
console.error('[moviebox] JSON parse error:', parseErr);
console.error('[moviebox] Response was not valid JSON');
throw new Error('API response is not valid JSON');
}
console.log('[moviebox] Parsed response:', JSON.stringify(response, null, 2));
if (!response?.success || !response?.data) {
console.error('[moviebox] API error:', response?.error || 'No data field');
throw new Error('API returned no data');
}
const hlsUrl =
response.data.streamUrl ||
response.data.hls ||
response.data.url ||
response.data.sources?.find((s) => s.type === 'hls')?.file;
if (!hlsUrl || typeof hlsUrl !== 'string' || !hlsUrl.toLowerCase().includes('.m3u8')) {
console.error('[moviebox] No HLS URL found in:', response.data);
throw new Error('No valid HLS URL found');
}
return {
embeds: [],
stream: [
{
id: 'moviebox-primary',
type: 'hls',
playlist: hlsUrl,
headers: {
referer: 'https://videasy.net/',
origin: 'https://videasy.net',
},
flags: [flags.CORS_ALLOWED],
captions: [],
},
],
};
} catch (err) {
console.error('[moviebox] Fetch error:', err);
throw new NotFoundError('Moviebox stream not found');
}
}
export const movieboxScraper = makeSourcerer({
id: 'moviebox',
name: 'Moviebox',
rank: 99,
disabled: true,
flags: [flags.CORS_ALLOWED],
scrapeMovie: (ctx) => fetchMovieboxStream(ctx, 'movie'),
scrapeShow: (ctx) => fetchMovieboxStream(ctx, 'tv'),
});

View file

@ -1,5 +1,5 @@
export const apiBaseUrl = 'https://borg.rips.cc';
export const username = '_sf_'; // I'd appreciate if you made your own account "_sf_" seems to be removed. Invite codes are: fmhy or mpgh
export const username = '_ps_';
export const password = 'defonotscraping';
export const password = 'defonotscraping';

View file

@ -113,8 +113,8 @@ async function comboScraper(ctx: MovieScrapeContext): Promise<SourcererOutput> {
export const ee3Scraper = makeSourcerer({
id: 'ee3',
name: 'EE3',
rank: 23,
rank: 97,
disabled: false,
flags: [],
scrapeMovie: comboScraper,
});
});

View file

@ -0,0 +1,162 @@
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { getTurnstileToken } from '@/utils/turnstile';
import { Caption, labelToLanguageCode } from '../../captions';
const getUserToken = (): string | null => {
try {
if (typeof window === 'undefined') return null;
const prefData = window.localStorage.getItem('__MW::preferences');
if (!prefData) return null;
const parsedAuth = JSON.parse(prefData);
return parsedAuth?.state?.febboxKey || null;
} catch (e) {
console.warn('Unable to access localStorage or parse auth data:', e);
return null;
}
};
const BASE_URL = 'https://mznxiwqjdiq00239q.space';
interface StreamEntry {
type: 'hls' | 'mp4';
url: string;
}
interface StreamData {
streams: Record<string, StreamEntry | string>;
subtitles: Record<string, any>;
error?: string;
name?: string;
size?: string;
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const userToken = getUserToken();
if (!userToken) throw new NotFoundError('Requires a user token!');
let turnstileToken: string;
try {
turnstileToken = await getTurnstileToken('0x4AAAAAABgPwhrOT6x6sTjI');
} catch (error) {
// eslint-disable-next-line no-alert
alert('FED API Turnstile verification failed. Please refresh the page and try again.');
throw new NotFoundError(`Turnstile verification failed: ${error}`);
}
ctx.progress(50);
const name = ctx.media.title;
let apiUrl = `${BASE_URL}/fedapi?name=${encodeURIComponent(name)}&year=${ctx.media.releaseYear}&ui=${encodeURIComponent(userToken)}`;
if (ctx.media.type === 'show') {
apiUrl += `&season=${ctx.media.season.number}&episode=${ctx.media.episode.number}`;
}
const res = await fetch(apiUrl, { credentials: 'omit' });
if (!res.ok) throw new NotFoundError('API request failed');
const data: StreamData = await res.json();
if (data?.error && data.error.endsWith('not found in database')) {
throw new NotFoundError('No stream found');
}
if (!data) throw new NotFoundError('No response from API');
ctx.progress(90);
type StreamInfo = { url: string; type: 'hls' | 'mp4' };
const streams = Object.entries(data.streams).reduce((acc: Record<string, StreamInfo>, [quality, entry]) => {
const url = typeof entry === 'string' ? entry : entry.url;
const type = typeof entry === 'string' ? 'mp4' : entry.type;
let qualityKey: number;
if (quality === 'ORG') {
const urlPath = url.split('?')[0];
if (urlPath.toLowerCase().includes('.mp4') || type === 'hls') {
acc.unknown = { url, type };
}
return acc;
}
if (quality === '4K') {
qualityKey = 2160;
} else {
qualityKey = parseInt(quality.replace('P', ''), 10);
}
if (Number.isNaN(qualityKey) || acc[qualityKey]) return acc;
acc[qualityKey] = { url, type };
return acc;
}, {});
const captions: Caption[] = [];
if (data.subtitles) {
for (const [langKey, subtitleData] of Object.entries(data.subtitles)) {
const languageKeyPart = langKey.split('_')[0];
const languageName = languageKeyPart.charAt(0).toUpperCase() + languageKeyPart.slice(1);
const languageCode = labelToLanguageCode(languageName)?.toLowerCase() ?? 'unknown';
if (subtitleData.subtitle_link) {
const url = subtitleData.subtitle_link;
const isVtt = url.toLowerCase().endsWith('.vtt');
captions.push({
type: isVtt ? 'vtt' : 'srt',
id: url,
url,
language: languageCode,
hasCorsRestrictions: false,
});
}
}
}
ctx.progress(90);
const hlsStream = streams[2160] ?? streams[1080] ?? streams[720] ?? streams[480] ?? streams[360] ?? streams.unknown;
if (hlsStream?.type === 'hls') {
return {
embeds: [],
stream: [
{
id: 'primary',
captions,
playlist: hlsStream.url,
type: 'hls',
flags: [flags.CORS_ALLOWED],
},
],
};
}
return {
embeds: [],
stream: [
{
id: 'primary',
captions,
qualities: {
...(streams[2160] && { '4k': { type: 'mp4', url: streams[2160].url } }),
...(streams[1080] && { 1080: { type: 'mp4', url: streams[1080].url } }),
...(streams[720] && { 720: { type: 'mp4', url: streams[720].url } }),
...(streams[480] && { 480: { type: 'mp4', url: streams[480].url } }),
...(streams[360] && { 360: { type: 'mp4', url: streams[360].url } }),
...(streams.unknown && { unknown: { type: 'mp4', url: streams.unknown.url } }),
},
type: 'file',
flags: [flags.CORS_ALLOWED],
},
],
};
}
export const FedAPIScraper = makeSourcerer({
id: 'fedapi',
name: 'FED API 🔥',
rank: 101,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -0,0 +1,229 @@
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { getTurnstileToken } from '@/utils/turnstile';
import { Caption, labelToLanguageCode } from '../captions';
const getUserToken = (): string | null => {
try {
if (typeof window === 'undefined') return null;
const prefData = window.localStorage.getItem('__MW::preferences');
if (!prefData) return null;
const parsedAuth = JSON.parse(prefData);
return parsedAuth?.state?.febboxKey || null;
} catch (e) {
console.warn('Unable to access localStorage or parse auth data:', e);
return null;
}
};
const getRegion = (): string | null => {
try {
if (typeof window === 'undefined') return null;
const regionData = window.localStorage.getItem('__MW::region');
if (!regionData) return null;
const parsed = JSON.parse(regionData);
return parsed?.state?.region ?? null;
} catch (e) {
console.warn('Unable to access localStorage or parse auth data:', e);
return null;
}
};
const BASE_URL = 'https://fed-api-db.pstream.mov';
function selectSubdomainByRegion(input: string | null): string | null {
const region = (input || '').toLowerCase();
if (/(^|\b)(usa5|usa6|usa7|uk1|de2|hk1|ca1|au1|sg1|in1)(\b|$)/.test(region)) {
const match = region.match(/(usa5|usa6|usa7|uk1|de2|hk1|ca1|au1|sg1|in1)/);
if (match) return match[1];
}
if (region.includes('dallas')) return 'usa5';
if (region.includes('portland')) return 'usa6';
if (region.includes('new-york')) return 'usa7';
if (region.includes('paris')) return Math.random() < 0.5 ? 'uk1' : 'de2';
if (region.includes('hong-kong')) return 'hk1';
if (region.includes('kansas')) return Math.random() < 0.5 ? 'usa7' : 'usa6';
if (region.includes('sydney')) return 'au1';
if (region.includes('singapore')) return 'sg1';
if (region.includes('mumbai')) return 'in1';
if (region === 'east') return 'usa7';
if (region === 'west') return 'usa6';
if (region === 'south') return 'usa5';
if (region === 'europe') return Math.random() < 0.5 ? 'uk1' : 'de2';
if (region === 'asia') return 'sg1';
return null;
}
function rewriteSheguSubdomain(originalUrl: string, subdomain: string): string {
try {
const parsed = new URL(originalUrl);
if (parsed.hostname.endsWith('.shegu.net')) {
parsed.hostname = `${subdomain}.shegu.net`;
return parsed.toString();
}
return originalUrl;
} catch {
return originalUrl;
}
}
interface StreamData {
streams: Record<string, string>;
subtitles: Record<string, any>;
error?: string;
name?: string;
size?: string;
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const userToken = getUserToken();
if (!userToken) throw new NotFoundError('Requires a user token!');
const region = getRegion();
let turnstileToken: string;
try {
turnstileToken = await getTurnstileToken('0x4AAAAAACuH31Fvud7uaIMf');
} catch (error) {
// eslint-disable-next-line no-alert
alert('FED DB Turnstile verification failed. Please refresh the page and try again.');
throw new NotFoundError(`Turnstile verification failed: ${error}`);
}
ctx.progress(50);
const apiUrl =
ctx.media.type === 'movie'
? `${BASE_URL}/movie/${ctx.media.tmdbId}`
: `${BASE_URL}/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`;
const data = await ctx.fetcher<StreamData>(apiUrl);
if (data?.error && data.error.endsWith('not found in database')) {
throw new NotFoundError('No stream found');
}
if (!data) throw new NotFoundError('No response from API');
ctx.progress(90);
const streams = Object.entries(data.streams).reduce((acc: Record<string, string>, [quality, url]) => {
let qualityKey: number;
if (quality === 'ORG') {
const urlPath = url.split('?')[0];
if (urlPath.toLowerCase().includes('.mp4')) {
acc.unknown = url;
}
return acc;
}
if (quality === '4K') {
qualityKey = 2160;
} else {
qualityKey = parseInt(quality.replace('P', ''), 10);
}
if (Number.isNaN(qualityKey) || acc[qualityKey]) return acc;
acc[qualityKey] = url;
return acc;
}, {});
const filteredStreams = Object.entries(streams).reduce((acc: Record<string, string>, [quality, url]) => {
acc[quality] = url;
return acc;
}, {});
const selectedSubdomain = selectSubdomainByRegion(region);
if (selectedSubdomain) {
Object.keys(filteredStreams).forEach((q) => {
filteredStreams[q] = rewriteSheguSubdomain(filteredStreams[q], selectedSubdomain as string);
});
}
const captions: Caption[] = [];
if (data.subtitles) {
for (const [langKey, subtitleData] of Object.entries(data.subtitles)) {
const languageKeyPart = langKey.split('_')[0];
const languageName = languageKeyPart.charAt(0).toUpperCase() + languageKeyPart.slice(1);
const languageCode = labelToLanguageCode(languageName)?.toLowerCase() ?? 'unknown';
if (subtitleData.subtitle_link) {
const url = subtitleData.subtitle_link;
const isVtt = url.toLowerCase().endsWith('.vtt');
captions.push({
type: isVtt ? 'vtt' : 'srt',
id: url,
url,
language: languageCode,
hasCorsRestrictions: false,
});
}
}
}
ctx.progress(90);
return {
embeds: [],
stream: [
{
id: 'primary',
captions,
qualities: {
...(filteredStreams[2160] && {
'4k': {
type: 'mp4',
url: filteredStreams[2160],
},
}),
...(filteredStreams[1080] && {
1080: {
type: 'mp4',
url: filteredStreams[1080],
},
}),
...(filteredStreams[720] && {
720: {
type: 'mp4',
url: filteredStreams[720],
},
}),
...(filteredStreams[480] && {
480: {
type: 'mp4',
url: filteredStreams[480],
},
}),
...(filteredStreams[360] && {
360: {
type: 'mp4',
url: filteredStreams[360],
},
}),
...(filteredStreams.unknown && {
unknown: {
type: 'mp4',
url: filteredStreams.unknown,
},
}),
},
type: 'file',
flags: [flags.CORS_ALLOWED],
},
],
};
}
export const FedAPIDBScraper = makeSourcerer({
id: 'fedapidb',
name: 'FED DB 🔥',
rank: 299,
disabled: true,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -1,63 +1,63 @@
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { flags } from '@/entrypoint/utils/targets';
import { getTurnstileToken } from '@/utils/turnstile';
const baseUrl = 'mznxiwqjdiq00239q.space';
const UA = "Windows NT 10.0 Very nice person";
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
async function comboScraper(
ctx: ShowScrapeContext | MovieScrapeContext
): Promise<SourcererOutput> {
let turnstileToken: string;
try {
turnstileToken = await getTurnstileToken('0x4AAAAAACuH31Fvud7uaIMf');
} catch {
throw new NotFoundError('Turnstile verification failed');
}
const name = encodeURIComponent(ctx.media.title);
const year = ctx.media.releaseYear;
const tmdbId = ctx.media.tmdbId;
const imdbId = ctx.media.imdbId || '';
const season = (ctx as ShowScrapeContext).media.season?.number || 1;
const episode = (ctx as ShowScrapeContext).media.episode?.number || 1;
const turnstileToken = localStorage.getItem('turnstile_token');
const endpoints = [
"fed",
"bomber",
"primebox",
"vento",
"blackout"
];
const endpoints = ["primebox","fed","vento","bomber","blackout"];
const commonHeaders = {
'User-Agent': UA,
'Referer': 'https://xprime.today/',
'Origin': 'https://xprime.today',
'Accept': 'application/json',
'cf-turnstile-response': turnstileToken || '',
'cf-turnstile-response': turnstileToken,
};
for (const ep of endpoints) {
try {
let url = `https://${baseUrl}/${ep}?name=${name}&id=${tmdbId}&imdb=${imdbId}&season=${season}&episode=${episode}`;
let url =
`https://${baseUrl}/${ep}?name=${name}&id=${tmdbId}&imdb=${imdbId}` +
`&season=${season}&episode=${episode}&year=${year}`;
if (ep === "primebox") {
url = `https://${baseUrl}/${ep}?name=${name}&fallback_year=${year}&season=${season}&episode=${episode}`;
} else {
url += `&year=${year}`;
url =
`https://${baseUrl}/${ep}?name=${name}` +
`&fallback_year=${year}&season=${season}&episode=${episode}`;
}
const res = await ctx.proxiedFetcher(url, {
method: 'GET',
headers: commonHeaders
const res = await ctx.proxiedFetcher(url, {
headers: commonHeaders,
});
let streamUrl = '';
if (res?.servers && Array.isArray(res.servers) && res.servers.length > 0) {
streamUrl = res.servers[0].url;
} else if (res?.url) {
streamUrl = res.url;
}
if (res?.servers?.length) streamUrl = res.servers[0].url;
else if (res?.url) streamUrl = res.url;
if (streamUrl && streamUrl.includes('.m3u8')) {
ctx.progress(100);
if (streamUrl?.includes('.m3u8')) {
return {
embeds: [],
stream: [
@ -67,27 +67,26 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
playlist: streamUrl,
headers: {
...commonHeaders,
'Referer': `https://${baseUrl}/`,
Referer: `https://${baseUrl}/`,
},
flags: [],
flags: [flags.CORS_ALLOWED],
captions: [],
},
],
};
}
} catch (e) {
continue;
}
} catch {}
}
throw new NotFoundError('No valid Xprime streams found across all endpoints');
throw new NotFoundError('No valid streams found');
}
export const xprimeScraper = makeSourcerer({
id: 'xprime',
name: 'Xprime',
rank: 98,
flags: [],
rank: 92,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -127,7 +127,7 @@ async function smashyScrapy(
export const smashyScraper = makeSourcerer({
id: 'smashy',
name: 'SmashyStream',
rank: 90,
rank: 94,
flags: [flags.CORS_ALLOWED],
scrapeMovie: smashyScrapy,
scrapeShow: smashyScrapy,

View file

@ -86,7 +86,7 @@ async function getStream(ctx: ScrapeContext, url: string): Promise<[string, stri
}
// console.log(LOG_PREFIX, 'Stream URL', streamURL);
return [streamURL, streamHost];
return [streamURL, new URL(streamURL).hostname];
}
export async function scrapeDoodstreamEmbed(ctx: EmbedScrapeContext): Promise<EmbedOutput> {
@ -125,4 +125,4 @@ export async function scrapeDoodstreamEmbed(ctx: EmbedScrapeContext): Promise<Em
},
],
};
}
}

View file

@ -122,7 +122,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
export const fsOnlineScraper = makeSourcerer({
id: 'fsonline',
name: 'FSOnline',
rank: 22,
rank: 98,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
@ -132,7 +132,7 @@ export const fsOnlineEmbeds = [
makeEmbed({
id: 'fsonline-doodstream',
name: 'Doodstream',
rank: 22,
rank: 97,
scrape: scrapeDoodstreamEmbed,
flags: [flags.CORS_ALLOWED],
}),
@ -143,4 +143,4 @@ export const fsOnlineEmbeds = [
// scrape: scrapeFilemoonEmbed,
// flags: [flags.CORS_ALLOWED],
// }),
];
];

View file

@ -41,4 +41,4 @@ export async function fetchIFrame(ctx: ScrapeContext, url: string): Promise<Fetc
});
throwOnResponse(response);
return response;
}
}

View file

@ -1,8 +1,6 @@
import { load } from 'cheerio';
import { SourcererEmbed, makeSourcerer } from '@/providers/base';
import { closeLoadScraper } from '@/providers/embeds/closeload';
import { ridooScraper } from '@/providers/embeds/ridoo';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
@ -11,6 +9,14 @@ import { IframeSourceResult, SearchResult } from './types';
const ridoMoviesBase = `https://ridomovies.tv`;
const ridoMoviesApiBase = `${ridoMoviesBase}/core/api`;
const normalizeTitle = (title: string): string => {
return title
.toLowerCase()
.trim()
.replace(/[^\w\s]/g, '')
.replace(/\s+/g, ' ');
};
const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => {
const searchResult = await ctx.proxiedFetcher<SearchResult>('/search', {
baseUrl: ridoMoviesApiBase,
@ -18,14 +24,37 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
q: ctx.media.title,
},
});
if (!searchResult.data?.items || searchResult.data.items.length === 0) {
throw new NotFoundError('No search results found');
}
const mediaData = searchResult.data.items.map((movieEl) => {
const name = movieEl.title;
const year = movieEl.contentable.releaseYear;
const fullSlug = movieEl.fullSlug;
return { name, year, fullSlug };
});
const targetMedia = mediaData.find((m) => m.name === ctx.media.title && m.year === ctx.media.releaseYear.toString());
if (!targetMedia?.fullSlug) throw new NotFoundError('No watchable item found');
const normalizedSearchTitle = normalizeTitle(ctx.media.title);
const searchYear = ctx.media.releaseYear.toString();
let targetMedia = mediaData.find((m) => normalizeTitle(m.name) === normalizedSearchTitle && m.year === searchYear);
if (!targetMedia) {
targetMedia = mediaData.find((m) => {
const normalizedName = normalizeTitle(m.name);
return (
m.year === searchYear &&
(normalizedName.includes(normalizedSearchTitle) || normalizedSearchTitle.includes(normalizedName))
);
});
}
if (!targetMedia?.fullSlug) {
throw new NotFoundError('No matching media found');
}
ctx.progress(40);
let iframeSourceUrl = `/${targetMedia.fullSlug}/videos`;
@ -34,14 +63,20 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
const showPageResult = await ctx.proxiedFetcher<string>(`/${targetMedia.fullSlug}`, {
baseUrl: ridoMoviesBase,
});
const fullEpisodeSlug = `season-${ctx.media.season.number}/episode-${ctx.media.episode.number}`;
const regexPattern = new RegExp(
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\\\"fullSlug\\\\\\":\\\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\\\")`,
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\"fullSlug\\\\":\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\")`,
'g',
);
const matches = [...showPageResult.matchAll(regexPattern)];
const episodeIds = matches.map((match) => match[1]);
if (episodeIds.length === 0) throw new NotFoundError('No watchable item found');
if (episodeIds.length === 0) {
throw new NotFoundError('Episode not found');
}
const episodeId = episodeIds[episodeIds.length - 1];
iframeSourceUrl = `/episodes/${episodeId}/videos`;
}
@ -49,24 +84,38 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
const iframeSource = await ctx.proxiedFetcher<IframeSourceResult>(iframeSourceUrl, {
baseUrl: ridoMoviesApiBase,
});
if (!iframeSource.data || iframeSource.data.length === 0) {
throw new NotFoundError('No video sources found');
}
const iframeSource$ = load(iframeSource.data[0].url);
const iframeUrl = iframeSource$('iframe').attr('data-src');
if (!iframeUrl) throw new NotFoundError('No watchable item found');
if (!iframeUrl) {
throw new NotFoundError('No iframe URL found');
}
ctx.progress(60);
const embeds: SourcererEmbed[] = [];
if (iframeUrl.includes('closeload')) {
embeds.push({
embedId: closeLoadScraper.id,
url: iframeUrl,
});
}
let embedId = 'closeload';
if (iframeUrl.includes('ridoo')) {
embeds.push({
embedId: ridooScraper.id,
url: iframeUrl,
});
embedId = 'ridoo';
}
embeds.push({
embedId,
url: iframeUrl,
});
ctx.progress(80);
if (embeds.length === 0) {
throw new NotFoundError('No supported embeds found');
}
ctx.progress(90);
return {
@ -77,9 +126,9 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
export const ridooMoviesScraper = makeSourcerer({
id: 'ridomovies',
name: 'RidoMovies',
rank: 210,
rank: 96,
flags: [],
disabled: true,
disabled: false,
scrapeMovie: universalScraper,
scrapeShow: universalScraper,
});
});

View file

@ -75,4 +75,4 @@ export type IframeSourceResult = {
data: {
url: string;
}[];
};
};

View file

@ -0,0 +1,30 @@
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
const baseUrl = 'https://www.vidking.net';
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const url =
ctx.media.type === 'movie'
? `${baseUrl}/embed/movie/${ctx.media.tmdbId}`
: `${baseUrl}/embed/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`;
return {
embeds: [
{
embedId: 'vidking',
url,
},
],
};
}
export const vidkingScraper = makeSourcerer({
id: 'Vidking',
name: 'VidKing',
rank: 95,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -0,0 +1,113 @@
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const API_BASE = 'https://enc-dec.app/api';
const VIDLINK_BASE = 'https://vidlink.pro/api/b';
const headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
Connection: 'keep-alive',
Referer: 'https://vidlink.pro/',
Origin: 'https://vidlink.pro',
};
async function encryptTmdbId(ctx: MovieScrapeContext | ShowScrapeContext, tmdbId: string): Promise<string> {
const response = await ctx.proxiedFetcher<{ result: string }>(`${API_BASE}/enc-vidlink`, {
method: 'GET',
query: { text: tmdbId },
});
if (!response?.result) {
throw new NotFoundError('Failed to encrypt TMDB ID');
}
return response.result;
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const { tmdbId } = ctx.media;
ctx.progress(10);
const encryptedId = await encryptTmdbId(ctx, tmdbId.toString());
ctx.progress(30);
const apiUrl =
ctx.media.type === 'movie'
? `${VIDLINK_BASE}/movie/${encryptedId}`
: `${VIDLINK_BASE}/tv/${encryptedId}/${ctx.media.season.number}/${ctx.media.episode.number}`;
const vidlinkRaw = await ctx.proxiedFetcher<string>(apiUrl, {
headers,
});
if (!vidlinkRaw) {
throw new NotFoundError('No response from vidlink API');
}
ctx.progress(60);
let vidlinkData: { stream?: any };
try {
vidlinkData = typeof vidlinkRaw === 'string' ? JSON.parse(vidlinkRaw) : vidlinkRaw;
} catch {
throw new NotFoundError('Invalid JSON from vidlink API');
}
ctx.progress(80);
if (!vidlinkData.stream) {
throw new NotFoundError('No stream data found in vidlink response');
}
const { stream } = vidlinkData;
const captions = [];
if (stream.captions && Array.isArray(stream.captions)) {
for (const caption of stream.captions) {
const captionType = caption.type === 'srt' ? 'srt' : 'vtt';
captions.push({
id: caption.id || caption.url,
url: caption.url,
language: caption.language || 'Unknown',
type: captionType as 'srt' | 'vtt',
hasCorsRestrictions: caption.hasCorsRestrictions || false,
});
}
}
// const flags = stream.flags || [];
// if (vidlinkData.flags) {
// flags.push(...vidlinkData.flags);
// }
ctx.progress(90);
return {
embeds: [],
stream: [
{
id: stream.id || 'primary',
type: stream.type || 'file',
qualities: stream.qualities || {},
playlist: stream.playlist,
captions,
flags: [],
headers: stream.headers || headers,
},
],
};
}
export const vidlinkScraper = makeSourcerer({
id: 'vidlink',
name: 'VidLink 🔥',
rank: 96,
disabled: false,
flags: [],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});