mirror of
https://github.com/sussy-code/providers.git
synced 2026-08-04 01:56:06 +00:00
modified: src/providers/all.ts
modified: src/providers/embeds/vidking.ts deleted: src/providers/sources/coitus.ts modified: src/providers/sources/disabled/coitus.ts modified: src/providers/sources/disabled/moviebox.ts modified: src/providers/sources/fed/xprime.ts new file: src/providers/sources/florence/movielair.ts new file: src/providers/sources/test.ts
This commit is contained in:
parent
e28a5ed4eb
commit
172597e4c1
8 changed files with 553 additions and 233 deletions
|
|
@ -5,7 +5,10 @@ import { vidkingEmbedScraper } from './embeds/vidking';
|
|||
import { vidkingScraper } from './sources/vidKing';
|
||||
import { FedAPIScraper } from './sources/fed/fedapi';
|
||||
//import { FedAPIDBScraper } from './sources/fed/fedapidb';
|
||||
import { FEDIAPIScraper } from './sources/test';
|
||||
import { coitusScraper } from './sources/disabled/coitus'
|
||||
import { xprimeScraper } from './sources/fed/xprime';
|
||||
import { movielairScrape } from './sources/florence/movielair'
|
||||
import { doodScraper } from '@/providers/embeds/dood';
|
||||
import { filemoonScraper } from '@/providers/embeds/filemoon';
|
||||
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
||||
|
|
@ -85,7 +88,6 @@ import { EightStreamScraper } from './sources/disabled/8stream';
|
|||
import { animeflvScraper } from './sources/animeflv';
|
||||
import { animetsuScraper } from './sources/animetsu';
|
||||
import { cinehdplusScraper } from './sources/cinehdplus-es';
|
||||
import { coitusScraper } from './sources/disabled/coitus';
|
||||
import { cuevana3Scraper } from './sources/cuevana3';
|
||||
import { debridScraper } from './sources/debrid';
|
||||
import { embedsuScraper } from './sources/disabled/embedsu';
|
||||
|
|
@ -121,6 +123,7 @@ export function gatherAllSources(): Array<Sourcerer> {
|
|||
insertunitScraper,
|
||||
zyonScraper,
|
||||
soaperTvScraper,
|
||||
FEDIAPIScraper,
|
||||
smashyScraper,
|
||||
autoembedScraper,
|
||||
myanimeScraper,
|
||||
|
|
@ -129,6 +132,7 @@ export function gatherAllSources(): Array<Sourcerer> {
|
|||
fsharetvScraper,
|
||||
zoechipScraper,
|
||||
thunderleafScraper,
|
||||
movielairScrape,
|
||||
mp4hydraScraper,
|
||||
embedsuScraper,
|
||||
slidemoviesScraper,
|
||||
|
|
|
|||
|
|
@ -18,57 +18,43 @@ export const vidkingEmbedScraper = makeEmbed({
|
|||
'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 videoTagMatch = html.match(/<video[^>]+src=["']([^"']+)["']/i);
|
||||
|
||||
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);
|
||||
const scriptMatch = html.match(/var\s+(?:source|video|url|hlsUrl|manifest|stream)\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];
|
||||
}
|
||||
}
|
||||
let videoUrl = videoTagMatch?.[1] || m3u8Match?.[1] || scriptMatch?.[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}`;
|
||||
}
|
||||
if (videoUrl.startsWith('//')) videoUrl = `https:${videoUrl}`;
|
||||
if (videoUrl.startsWith('/')) videoUrl = `${origin}${videoUrl}`;
|
||||
|
||||
const isHls = videoUrl.includes('.m3u8') || /\.m3u8[?#]/.test(videoUrl);
|
||||
const isMp4Worker = videoUrl.includes('workers.dev') || videoUrl.includes('/mp4/');
|
||||
|
||||
if (isHls) {
|
||||
if (isMp4Worker) {
|
||||
return {
|
||||
stream: [
|
||||
{
|
||||
id: 'primary',
|
||||
type: 'hls',
|
||||
playlist: videoUrl,
|
||||
type: 'file',
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
headers: {
|
||||
Referer: origin,
|
||||
Origin: origin,
|
||||
qualities: {
|
||||
unknown: {
|
||||
type: 'mp4',
|
||||
url: videoUrl,
|
||||
},
|
||||
},
|
||||
preferredHeaders: {
|
||||
Referer: 'https://vidking.net/',
|
||||
Origin: 'https://vidking.net/',
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
},
|
||||
|
|
@ -80,22 +66,17 @@ export const vidkingEmbedScraper = makeEmbed({
|
|||
stream: [
|
||||
{
|
||||
id: 'primary',
|
||||
type: 'file',
|
||||
type: 'hls',
|
||||
playlist: videoUrl,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
qualities: {
|
||||
unknown: {
|
||||
type: 'mp4',
|
||||
url: videoUrl,
|
||||
},
|
||||
},
|
||||
preferredHeaders: {
|
||||
Referer: origin,
|
||||
Origin: origin,
|
||||
headers: {
|
||||
Referer: 'https://vidking.net/',
|
||||
Origin: 'https://vidking.net/',
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
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,
|
||||
});
|
||||
|
|
@ -65,7 +65,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
|
|||
|
||||
export const coitusScraper = makeSourcerer({
|
||||
id: 'coitus',
|
||||
name: 'Autoembed+',
|
||||
name: 'Coitus',
|
||||
rank: 91,
|
||||
disabled: true,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
|
|
|
|||
|
|
@ -1,114 +1,116 @@
|
|||
import { AES, enc, mode, pad } from 'crypto-js';
|
||||
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';
|
||||
// This salt must match the one currently in users.videasy.net/api/script.js
|
||||
const XOR_SALT = '8c465aa8af6cbfd4c1f91bf0c8d678ba';
|
||||
|
||||
interface MovieboxApiResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
streamUrl?: string;
|
||||
hls?: string;
|
||||
url?: string;
|
||||
sources?: Array<{ file: string; type: string }>;
|
||||
};
|
||||
/**
|
||||
* Reverses the XOR shift.
|
||||
* Using WordArray.create allows crypto-js to handle the raw bytes more reliably.
|
||||
*/
|
||||
function decodeVideasyHex(hex: string): any {
|
||||
const words: number[] = [];
|
||||
for (let i = 0; i < hex.length; i += 8) {
|
||||
let word = 0;
|
||||
for (let j = 0; j < 8; j += 2) {
|
||||
const index = (i + j) / 2;
|
||||
let byte = parseInt(hex.substring(i + j, i + j + 2), 16);
|
||||
byte ^= XOR_SALT.charCodeAt(index % XOR_SALT.length);
|
||||
word = (word << 8) | byte;
|
||||
}
|
||||
words.push(word);
|
||||
}
|
||||
return enc.Hex.parse(words.map(w => (w >>> 0).toString(16).padStart(8, '0')).join(''));
|
||||
}
|
||||
|
||||
async function fetchMovieboxStream(
|
||||
async function fetchVideasyStream(
|
||||
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',
|
||||
const tmdbId = String(ctx.media.tmdbId);
|
||||
|
||||
// Grey's Anatomy -> Grey%27s+Anatomy (matching your browser log)
|
||||
const encodedTitle = encodeURIComponent(ctx.media.title).replace(/%20/g, '+');
|
||||
|
||||
const params = [
|
||||
`title=${encodedTitle}`,
|
||||
`mediaType=${type}`,
|
||||
`year=${ctx.media.releaseYear}`,
|
||||
`episodeId=${type === 'tv' ? (ctx as ShowScrapeContext).media.episode.number : ''}`,
|
||||
`seasonId=${type === 'tv' ? (ctx as ShowScrapeContext).media.season.number : ''}`,
|
||||
`tmdbId=${tmdbId}`,
|
||||
`imdbId=${ctx.media.imdbId ?? ''}`
|
||||
].join('&');
|
||||
|
||||
const url = `${API_BASE}/moviebox/sources-with-title?${params}`;
|
||||
console.log(`[MovieBox] Sending Request: ${url}`);
|
||||
|
||||
const response: any = await ctx.proxiedFetcher(url, {
|
||||
headers: {
|
||||
'Origin': 'https://player.videasy.net',
|
||||
'Referer': 'https://player.videasy.net/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0',
|
||||
'Accept': '*/*',
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
const encryptedData = typeof response === 'string' ? response : response?.sources;
|
||||
if (!encryptedData || encryptedData.length < 100) throw new NotFoundError('No sources');
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
// Videasy uses the raw string bytes of the salt as key
|
||||
const key = enc.Utf8.parse(XOR_SALT);
|
||||
// IV is the first 16 bytes (128 bits) of the salt
|
||||
const iv = enc.Utf8.parse(XOR_SALT.substring(0, 16));
|
||||
|
||||
// DEBUG: Log raw response
|
||||
console.log('[moviebox] Raw API Response:', rawResponse.slice(0, 2000));
|
||||
const ciphertext = decodeVideasyHex(encryptedData);
|
||||
|
||||
const decrypted = AES.decrypt(
|
||||
{ ciphertext } as any,
|
||||
key,
|
||||
{ iv, mode: mode.CBC, padding: pad.Pkcs7 }
|
||||
);
|
||||
|
||||
// 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');
|
||||
const decryptedText = decrypted.toString(enc.Utf8);
|
||||
|
||||
// If this is empty, the XOR_SALT provided doesn't match the server's current salt
|
||||
if (!decryptedText) {
|
||||
console.log(`[MovieBox] Decryption failed. Raw Start: ${encryptedData.substring(0, 20)}`);
|
||||
throw new Error("Invalid Decryption Result");
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
const sources = JSON.parse(decryptedText);
|
||||
console.log(`[MovieBox] Found ${sources.length} sources.`);
|
||||
|
||||
return {
|
||||
embeds: [],
|
||||
stream: [
|
||||
{
|
||||
id: 'moviebox-primary',
|
||||
type: 'hls',
|
||||
playlist: hlsUrl,
|
||||
headers: {
|
||||
referer: 'https://videasy.net/',
|
||||
origin: 'https://videasy.net',
|
||||
},
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
},
|
||||
],
|
||||
stream: sources.map((s: any) => ({
|
||||
id: 'videasy',
|
||||
type: s.file.includes('m3u8') ? 'hls' : 'file',
|
||||
playlist: s.file,
|
||||
quality: s.label || 'Unknown',
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
...(s.file.includes('m3u8') ? {} : { qualities: { "unknown": { type: "mp4", url: s.file } } })
|
||||
}))
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('[moviebox] Fetch error:', err);
|
||||
throw new NotFoundError('Moviebox stream not found');
|
||||
} catch (err: any) {
|
||||
console.error(`[MovieBox] Critical Decryption Failure: ${err.message}`);
|
||||
throw new NotFoundError('Failed to process stream data');
|
||||
}
|
||||
}
|
||||
|
||||
export const movieboxScraper = makeSourcerer({
|
||||
id: 'moviebox',
|
||||
name: 'Moviebox',
|
||||
rank: 99,
|
||||
name: 'MovieBox',
|
||||
rank: 110,
|
||||
disabled: true,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
scrapeMovie: (ctx) => fetchMovieboxStream(ctx, 'movie'),
|
||||
scrapeShow: (ctx) => fetchMovieboxStream(ctx, 'tv'),
|
||||
});
|
||||
scrapeMovie: (ctx) => fetchVideasyStream(ctx, 'movie'),
|
||||
scrapeShow: (ctx) => fetchVideasyStream(ctx, 'tv'),
|
||||
});
|
||||
|
|
@ -5,15 +5,14 @@ import { flags } from '@/entrypoint/utils/targets';
|
|||
import { getTurnstileToken } from '@/utils/turnstile';
|
||||
|
||||
const baseUrl = 'mznxiwqjdiq00239q.space';
|
||||
const UA = 'Windows NT 10.0 Very nice person';
|
||||
const SITEKEY = '0x4AAAAAACuH31Fvud7uaIMf';
|
||||
|
||||
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
||||
let turnstileToken: string;
|
||||
try {
|
||||
turnstileToken = await getTurnstileToken('0x4AAAAAACuH31Fvud7uaIMf');
|
||||
} catch {
|
||||
throw new NotFoundError('Turnstile verification failed');
|
||||
}
|
||||
const UA =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36";
|
||||
|
||||
async function comboScraper(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext
|
||||
): Promise<SourcererOutput> {
|
||||
|
||||
const name = encodeURIComponent(ctx.media.title);
|
||||
const year = ctx.media.releaseYear;
|
||||
|
|
@ -23,36 +22,68 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
|
|||
const season = (ctx as ShowScrapeContext).media.season?.number || 1;
|
||||
const episode = (ctx as ShowScrapeContext).media.episode?.number || 1;
|
||||
|
||||
const endpoints = ['primebox', 'fed', 'vento', 'bomber', '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,
|
||||
};
|
||||
console.log(`[XPRIME] Starting scrape for "${ctx.media.title}" (${year})`);
|
||||
|
||||
for (const ep of endpoints) {
|
||||
try {
|
||||
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}`;
|
||||
console.log(`[XPRIME] Generating Turnstile token for endpoint "${ep}"...`);
|
||||
let turnstileToken: string;
|
||||
try {
|
||||
turnstileToken = await getTurnstileToken(SITEKEY);
|
||||
console.log(`[XPRIME] Token generated successfully.`);
|
||||
} catch (err) {
|
||||
console.warn(`[XPRIME] Turnstile token generation failed for "${ep}", skipping endpoint.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 350));
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
console.log(`[XPRIME] Fetching from endpoint "${ep}" -> ${url}`);
|
||||
const res = await ctx.proxiedFetcher(url, {
|
||||
headers: commonHeaders,
|
||||
headers: {
|
||||
'User-Agent': UA,
|
||||
'Referer': 'https://xprime.today/',
|
||||
'Origin': 'https://xprime.today',
|
||||
'Accept': 'application/json',
|
||||
'cf-turnstile-response': turnstileToken,
|
||||
},
|
||||
});
|
||||
|
||||
let streamUrl = '';
|
||||
if (res?.servers?.length) {
|
||||
streamUrl = res.servers[0].url;
|
||||
console.log(`[XPRIME] Found server stream URL: ${streamUrl}`);
|
||||
} else if (res?.url) {
|
||||
streamUrl = res.url;
|
||||
console.log(`[XPRIME] Found direct stream URL: ${streamUrl}`);
|
||||
} else {
|
||||
console.log(`[XPRIME] No stream URL found at this endpoint, continuing...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (res?.servers?.length) streamUrl = res.servers[0].url;
|
||||
else if (res?.url) streamUrl = res.url;
|
||||
|
||||
if (streamUrl?.includes('.m3u8')) {
|
||||
// HLS stream
|
||||
if (streamUrl.includes('.m3u8')) {
|
||||
console.log(`[XPRIME] Returning HLS stream.`);
|
||||
return {
|
||||
embeds: [],
|
||||
stream: [
|
||||
|
|
@ -61,8 +92,9 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
|
|||
type: 'hls',
|
||||
playlist: streamUrl,
|
||||
headers: {
|
||||
...commonHeaders,
|
||||
'User-Agent': UA,
|
||||
Referer: `https://${baseUrl}/`,
|
||||
Origin: `https://${baseUrl}`,
|
||||
},
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
|
|
@ -70,10 +102,37 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
|
|||
],
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// MP4 fallback
|
||||
if (streamUrl.includes('.mp4')) {
|
||||
console.log(`[XPRIME] Returning MP4 stream.`);
|
||||
return {
|
||||
embeds: [],
|
||||
stream: [
|
||||
{
|
||||
id: 'primary',
|
||||
type: 'file',
|
||||
qualities: {
|
||||
1080: {
|
||||
type: 'mp4',
|
||||
url: streamUrl,
|
||||
},
|
||||
},
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.warn(`[XPRIME] Error scraping endpoint "${ep}":`, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundError('No valid streams found');
|
||||
console.error(`[XPRIME] No valid streams found for "${ctx.media.title}"`);
|
||||
throw new NotFoundError('No valid Xprime streams found');
|
||||
}
|
||||
|
||||
export const xprimeScraper = makeSourcerer({
|
||||
|
|
@ -83,4 +142,4 @@ export const xprimeScraper = makeSourcerer({
|
|||
flags: [flags.CORS_ALLOWED],
|
||||
scrapeMovie: comboScraper,
|
||||
scrapeShow: comboScraper,
|
||||
});
|
||||
});
|
||||
124
src/providers/sources/florence/movielair.ts
Normal file
124
src/providers/sources/florence/movielair.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { load } from "cheerio";
|
||||
|
||||
import { flags } from "@/entrypoint/utils/targets";
|
||||
import { SourcererOutput, makeSourcerer } from "@/providers/base";
|
||||
import { MovieScrapeContext, ShowScrapeContext } from "@/utils/context";
|
||||
import { NotFoundError } from "@/utils/errors";
|
||||
|
||||
const BASE = "https://movielair.cc";
|
||||
|
||||
const HEADERS = {
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/145.0",
|
||||
referer: BASE,
|
||||
};
|
||||
|
||||
async function searchFirst(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext,
|
||||
query: string
|
||||
): Promise<string | null> {
|
||||
// IMPORTANT: /1 is required
|
||||
const url = `${BASE}/search/${encodeURIComponent(query)}/1`;
|
||||
|
||||
const html = await ctx.proxiedFetcher(url, { headers: HEADERS });
|
||||
const $ = load(html);
|
||||
|
||||
const first = $(".film-poster a").first().attr("href");
|
||||
if (!first) return null;
|
||||
|
||||
return first;
|
||||
}
|
||||
|
||||
function buildWatchUrl(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext,
|
||||
path: string
|
||||
) {
|
||||
const id = path.split("/").pop();
|
||||
|
||||
if (ctx.media.type === "movie") {
|
||||
return `${BASE}/watch-movie/${id}`;
|
||||
}
|
||||
|
||||
const season = (ctx as ShowScrapeContext).media.season.number;
|
||||
const episode = (ctx as ShowScrapeContext).media.episode.number;
|
||||
|
||||
return `${BASE}/watch-tv/${id}?season=${season}&episode=${episode}`;
|
||||
}
|
||||
|
||||
async function getIframe(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext,
|
||||
watchUrl: string
|
||||
) {
|
||||
const html = await ctx.proxiedFetcher(watchUrl, { headers: HEADERS });
|
||||
const $ = load(html);
|
||||
|
||||
const iframe = $("iframe").attr("src");
|
||||
if (!iframe) return null;
|
||||
|
||||
return iframe.startsWith("http") ? iframe : `https:${iframe}`;
|
||||
}
|
||||
|
||||
async function extractStream(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext,
|
||||
iframeUrl: string
|
||||
) {
|
||||
const html = await ctx.proxiedFetcher(iframeUrl, { headers: HEADERS });
|
||||
|
||||
const match = html.match(/file:\s*"(https?:\/\/[^"]+\.m3u8[^"]*)"/);
|
||||
if (match) return match[1];
|
||||
|
||||
const match2 = html.match(/sources:\s*\[\{file:"([^"]+)"/);
|
||||
if (match2) return match2[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function movielairScraper(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext
|
||||
): Promise<SourcererOutput> {
|
||||
ctx.progress(10);
|
||||
|
||||
const title = ctx.media.title;
|
||||
|
||||
const path = await searchFirst(ctx, title);
|
||||
if (!path) throw new NotFoundError("MovieLair search failed");
|
||||
|
||||
ctx.progress(30);
|
||||
|
||||
const watchUrl = buildWatchUrl(ctx, path);
|
||||
|
||||
ctx.progress(50);
|
||||
|
||||
const iframe = await getIframe(ctx, watchUrl);
|
||||
if (!iframe) throw new NotFoundError("MovieLair iframe not found");
|
||||
|
||||
ctx.progress(70);
|
||||
|
||||
const stream = await extractStream(ctx, iframe);
|
||||
if (!stream) throw new NotFoundError("MovieLair stream not found");
|
||||
|
||||
ctx.progress(90);
|
||||
|
||||
return {
|
||||
embeds: [],
|
||||
stream: [
|
||||
{
|
||||
id: "primary",
|
||||
type: "hls" as const,
|
||||
playlist: stream,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
captions: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export const movielairScrape = makeSourcerer({
|
||||
id: "movielair",
|
||||
name: "MovieLair",
|
||||
rank: 30,
|
||||
disabled: true,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
scrapeMovie: movielairScraper,
|
||||
scrapeShow: movielairScraper,
|
||||
});
|
||||
224
src/providers/sources/test.ts
Normal file
224
src/providers/sources/test.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
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';
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
const API_BASE = 'https://z6mgd9v3-8787.euw.devtunnels.ms';
|
||||
const TURNSTILE_SITE_KEY = '0x4AAAAAACuH31Fvud7uaIMf';
|
||||
|
||||
let FebToken =
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NzIyMTQxODAsIm5iZiI6MTc3MjIxNDE4MCwiZXhwIjoxODAzMzE4MjAwLCJkYXRhIjp7InVpZCI6OTMxMzA0LCJ0b2tlbiI6Ijc5NjhmYTc2ZDYzNTFjZTJjMDAxMjE4NzYyNjg4M2VkIn19.zeGmJN0gdC4ObOGIBMiHSqM0M6JtdJeT0dQELGUvVhA";
|
||||
|
||||
// ── Turnstile Cache ───────────────────────────────────────────────────────────
|
||||
let cachedTurnstile: { token: string; ts: number } | null = null;
|
||||
|
||||
async function getCachedTurnstile(): Promise<string> {
|
||||
const now = Date.now();
|
||||
|
||||
if (cachedTurnstile && now - cachedTurnstile.ts < 120000) {
|
||||
return cachedTurnstile.token;
|
||||
}
|
||||
|
||||
const token = await getTurnstileToken(TURNSTILE_SITE_KEY);
|
||||
cachedTurnstile = { token, ts: now };
|
||||
return token;
|
||||
}
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
interface StreamEntry {
|
||||
type: 'hls' | 'mp4';
|
||||
url: string;
|
||||
download?: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
streams: Record<string, StreamEntry>;
|
||||
subtitles: Record<
|
||||
string,
|
||||
{
|
||||
subtitle_link: string;
|
||||
language: string;
|
||||
type: 'vtt' | 'srt';
|
||||
}
|
||||
>;
|
||||
name?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Quality key → numeric for player
|
||||
const QUALITY_MAP: Record<string, number | 'unknown'> = {
|
||||
'4K': 2160,
|
||||
'2160P': 2160,
|
||||
'1080P': 1080,
|
||||
'720P': 720,
|
||||
'480P': 480,
|
||||
'360P': 360,
|
||||
'ORG': 'unknown',
|
||||
};
|
||||
|
||||
// ── Main scraper ──────────────────────────────────────────────────────────────
|
||||
async function comboScraper(
|
||||
ctx: ShowScrapeContext | MovieScrapeContext,
|
||||
): Promise<SourcererOutput> {
|
||||
const userToken = FebToken;
|
||||
if (!userToken)
|
||||
throw new NotFoundError('Febbox token not set');
|
||||
|
||||
// 1. Turnstile
|
||||
let turnstileToken: string;
|
||||
try {
|
||||
turnstileToken = await getCachedTurnstile();
|
||||
} catch (err) {
|
||||
console.log('Turnstile verification failed');
|
||||
throw new NotFoundError(`Turnstile failed: ${err}`);
|
||||
}
|
||||
|
||||
ctx.progress(25);
|
||||
|
||||
// 2. Require IMDB ID
|
||||
const imdbId = ctx.media.imdbId;
|
||||
if (!imdbId) throw new NotFoundError('No IMDB ID available');
|
||||
|
||||
// 3. Build params
|
||||
const params = new URLSearchParams({
|
||||
name: ctx.media.title,
|
||||
year: String(ctx.media.releaseYear ?? ''),
|
||||
ui: userToken,
|
||||
imdb_id: imdbId,
|
||||
type: ctx.media.type === 'show' ? 'show' : 'movie',
|
||||
});
|
||||
|
||||
if (ctx.media.type === 'show') {
|
||||
params.set('season', String(ctx.media.season.number));
|
||||
params.set('episode', String(ctx.media.episode.number));
|
||||
}
|
||||
|
||||
ctx.progress(40);
|
||||
|
||||
// 4. Fetch
|
||||
const res = await fetch(`${API_BASE}/fedapi?${params}`, {
|
||||
headers: {
|
||||
'cf-turnstile-response': turnstileToken,
|
||||
},
|
||||
credentials: 'omit',
|
||||
});
|
||||
|
||||
if (res.status === 401)
|
||||
throw new NotFoundError('Invalid Febbox token');
|
||||
if (res.status === 403)
|
||||
throw new NotFoundError('Turnstile rejected');
|
||||
if (res.status === 404)
|
||||
throw new NotFoundError('Content not found');
|
||||
if (!res.ok)
|
||||
throw new NotFoundError(`API error ${res.status}`);
|
||||
|
||||
const data: ApiResponse = await res.json();
|
||||
|
||||
if (data?.error)
|
||||
throw new NotFoundError(data.error);
|
||||
if (!data?.streams)
|
||||
throw new NotFoundError('No streams');
|
||||
|
||||
ctx.progress(80);
|
||||
|
||||
// 5. Normalize streams
|
||||
type StreamInfo = { url: string; type: 'hls' | 'mp4' };
|
||||
const streams: Record<string | number, StreamInfo> = {};
|
||||
|
||||
for (const [qualityKey, entry] of Object.entries(data.streams)) {
|
||||
const normKey = QUALITY_MAP[qualityKey.toUpperCase()];
|
||||
if (normKey === undefined) continue;
|
||||
if (streams[normKey]) continue;
|
||||
streams[normKey] = { url: entry.url, type: entry.type };
|
||||
}
|
||||
|
||||
// 6. Captions
|
||||
const captions: Caption[] = [];
|
||||
for (const sub of Object.values(data.subtitles ?? {})) {
|
||||
const url = sub.subtitle_link;
|
||||
if (!url) continue;
|
||||
|
||||
const langCode =
|
||||
labelToLanguageCode(sub.language)?.toLowerCase() ??
|
||||
'unknown';
|
||||
|
||||
captions.push({
|
||||
type: sub.type,
|
||||
id: url,
|
||||
url,
|
||||
language: langCode,
|
||||
hasCorsRestrictions: false,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.progress(95);
|
||||
|
||||
// 7. Return
|
||||
const orderedKeys = [2160, 1080, 720, 480, 360, 'unknown'] as const;
|
||||
const hlsStream = orderedKeys
|
||||
.map((k) => streams[k])
|
||||
.find((s) => s?.type === 'hls');
|
||||
|
||||
if (hlsStream) {
|
||||
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 FEDIAPIScraper = makeSourcerer({
|
||||
id: 'fediapi',
|
||||
name: 'FEDI API 🔥',
|
||||
rank: 105,
|
||||
flags: [flags.CORS_ALLOWED],
|
||||
scrapeMovie: comboScraper,
|
||||
scrapeShow: comboScraper,
|
||||
});
|
||||
Loading…
Reference in a new issue