mirror of
https://github.com/sussy-code/providers.git
synced 2026-08-07 19:29:50 +00:00
new sources + rearanged some
This commit is contained in:
parent
eff6660ddf
commit
25bd750c9e
21 changed files with 1245 additions and 157 deletions
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
|||
74
src/providers/embeds/animekai.ts
Normal file
74
src/providers/embeds/animekai.ts
Normal 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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
101
src/providers/embeds/vidking.ts
Normal file
101
src/providers/embeds/vidking.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -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'),
|
||||
});
|
||||
|
|
@ -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'),
|
||||
});
|
||||
63
src/providers/sources/animekai.ts
Normal file
63
src/providers/sources/animekai.ts
Normal 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,
|
||||
});
|
||||
74
src/providers/sources/coitus.ts
Normal file
74
src/providers/sources/coitus.ts
Normal 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,
|
||||
});
|
||||
114
src/providers/sources/disabled/moviebox.ts
Normal file
114
src/providers/sources/disabled/moviebox.ts
Normal 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'),
|
||||
});
|
||||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
162
src/providers/sources/fed/fedapi.ts
Normal file
162
src/providers/sources/fed/fedapi.ts
Normal 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,
|
||||
});
|
||||
229
src/providers/sources/fed/fedapidb.ts
Normal file
229
src/providers/sources/fed/fedapidb.ts
Normal 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,
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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],
|
||||
// }),
|
||||
];
|
||||
];
|
||||
|
|
@ -41,4 +41,4 @@ export async function fetchIFrame(ctx: ScrapeContext, url: string): Promise<Fetc
|
|||
});
|
||||
throwOnResponse(response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
@ -75,4 +75,4 @@ export type IframeSourceResult = {
|
|||
data: {
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
30
src/providers/sources/vidKing.ts
Normal file
30
src/providers/sources/vidKing.ts
Normal 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,
|
||||
});
|
||||
113
src/providers/sources/vidlink.ts
Normal file
113
src/providers/sources/vidlink.ts
Normal 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,
|
||||
});
|
||||
Loading…
Reference in a new issue