Get started with port of custom providers

This commit is contained in:
vlOd2 2025-12-10 00:46:06 +02:00
parent 2a66410857
commit 6c5af97081
12 changed files with 10897 additions and 1614 deletions

8984
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -88,6 +88,7 @@
"cookie": "^0.6.0",
"crypto-js": "^4.2.0",
"form-data": "^4.0.4",
"fuse.js": "^7.1.0",
"hls-parser": "^0.13.6",
"iso-639-1": "^3.1.5",
"json5": "^2.2.3",

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,6 @@
import { Embed, Sourcerer } from '@/providers/base';
import { dopeboxEmbeds, dopeboxScraper } from '@/providers/custom/dopebox/index';
import { fsOnlineEmbeds, fsOnlineScraper } from '@/providers/custom/fsonline/index';
import { doodScraper } from '@/providers/embeds/dood';
import { filemoonScraper } from '@/providers/embeds/filemoon';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
@ -107,6 +109,8 @@ import { zunimeScraper } from './sources/zunime';
export function gatherAllSources(): Array<Sourcerer> {
// all sources are gathered here
return [
fsOnlineScraper,
dopeboxScraper,
cuevana3Scraper,
ridooMoviesScraper,
hdRezkaScraper,
@ -155,6 +159,8 @@ export function gatherAllSources(): Array<Sourcerer> {
export function gatherAllEmbeds(): Array<Embed> {
// all embeds are gathered here
return [
...fsOnlineEmbeds,
...dopeboxEmbeds,
serverMirrorEmbed,
upcloudScraper,
vidCloudScraper,

View file

@ -0,0 +1,95 @@
import Fuse from 'fuse.js';
import { SourcererEmbed, SourcererOutput, makeEmbed, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { Media, MediaPlayer, getEpisodePlayers, getEpisodes, getMoviePlayers, getSeasons, searchMedia } from './search';
import { scrapeUpCloudEmbed } from './upcloud';
import { getSearchQuery } from './utils';
async function handleContext(ctx: ShowScrapeContext | MovieScrapeContext) {
if (ctx.media.type !== 'movie' && ctx.media.type !== 'show') {
return [];
}
const mediaType = ctx.media.type === 'show' ? 'TV' : 'Movie';
const results = (await searchMedia(ctx, getSearchQuery(ctx.media.title))).filter((r) => r.info.includes(mediaType));
const fuse = new Fuse<Media>(results, {
keys: ['title'],
});
const media = fuse.search(ctx.media.title).find((r) => r.item.info.includes(ctx.media.releaseYear.toString()))?.item;
if (!media) {
throw new Error('Could not find movie');
}
if (ctx.media.type === 'show') {
const seasonNumber = ctx.media.season.number;
const epNumber = ctx.media.episode.number;
const season = (await getSeasons(ctx, media)).find((s) => s.number === seasonNumber);
if (!season) {
throw new Error('Could not find season');
}
const episode = (await getEpisodes(ctx, season)).find((ep) => ep.number === epNumber);
if (!episode) {
throw new Error('Could not find episode');
}
return getEpisodePlayers(ctx, media, episode);
}
return getMoviePlayers(ctx, media);
}
function addEmbedFromPlayer(name: string, players: MediaPlayer[], embeds: SourcererEmbed[]) {
const player = players.find((p) => p.name.toLowerCase().trim() === name.toLowerCase().trim());
if (!player) {
return;
}
embeds.push({
embedId: `dopebox-${player.name.toLowerCase().trim()}`,
url: player.url,
});
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const players = await handleContext(ctx);
if (!players) {
return {
embeds: [],
stream: [],
};
}
const embeds: SourcererEmbed[] = [];
addEmbedFromPlayer('UpCloud', players, embeds);
if (embeds.length < 1) {
throw new Error('No valid sources were found');
}
return {
embeds,
};
}
export const dopeboxScraper = makeSourcerer({
id: 'dopebox',
name: 'Dopebox',
rank: 600,
flags: ['cors-allowed'],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
export const dopeboxEmbeds = [
makeEmbed({
id: 'dopebox-upcloud',
name: 'UpCloud',
rank: 6001,
flags: [],
scrape: scrapeUpCloudEmbed,
}),
];

View file

@ -0,0 +1,176 @@
import * as cheerio from 'cheerio';
import { ScrapeContext } from '@/utils/context';
import { BASE_URL, EPISODES_URL, MOVIE_SERVERS_URL, SEARCH_URL, SEASONS_URL, SHOW_SERVERS_URL } from './utils';
export interface Media {
url: URL;
id: string;
title: string;
info: string[];
}
export interface MediaSeason {
id: string;
number: number;
}
export interface MediaEpisode {
id: string;
number: number;
title: string | undefined;
}
export interface MediaPlayer {
id: string;
url: string;
name: string;
}
export async function searchMedia(ctx: ScrapeContext, query: string): Promise<Media[]> {
const response = await ctx.proxiedFetcher.full(`${SEARCH_URL}${query}`, {
headers: {
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
},
});
const $ = cheerio.load(response.body);
const results: Media[] = [];
$('.flw-item').each((_, film) => {
const detail = $(film).find('.film-detail').first();
const nameURL = detail?.find('.film-name').first()?.find('a').first();
if (!detail || !nameURL) {
return;
}
const pathname = nameURL.attr('href')?.trim();
const title = nameURL.attr('title')?.trim();
const info = detail
.find('.fd-infor')
.first()
?.find('span')
.map((__, span) => $(span).text().trim())
.toArray();
if (!pathname || !title || !info || info.length === 0) {
return;
}
const url = URL.parse(pathname, BASE_URL);
const id = url?.pathname.split('-').pop();
if (!url || !id) {
console.error('Could not parse media URL', pathname);
return;
}
results.push({
url,
id,
title,
info,
});
});
return results;
}
export async function getSeasons(ctx: ScrapeContext, media: Media): Promise<MediaSeason[]> {
const response = await ctx.proxiedFetcher.full(`${SEASONS_URL}${media.id}`, {
headers: {
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
});
const $ = cheerio.load(response.body);
const seasons: MediaSeason[] = [];
$('.ss-item').each((_, s) => {
const id = $(s).attr('data-id')?.trim();
const number = /(\d+)/.exec($(s).text().trim())?.[1].trim();
if (!id || !number) {
return;
}
seasons.push({
id,
number: parseInt(number, 10),
});
});
return seasons;
}
export async function getEpisodes(ctx: ScrapeContext, season: MediaSeason): Promise<MediaEpisode[]> {
const response = await ctx.proxiedFetcher.full(`${EPISODES_URL}${season.id}`, {
headers: {
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
});
const $ = cheerio.load(response.body);
const episodes: MediaEpisode[] = [];
$('.eps-item').each((_, ep) => {
const id = $(ep).attr('data-id')?.trim();
const number = /(\d+)/.exec($(ep).find('.episode-number').first().text())?.[1].trim();
const title = $(ep).find('.film-name').first()?.find('a').first()?.attr('title')?.trim();
if (!id || !number) {
return;
}
episodes.push({
id,
number: parseInt(number, 10),
title,
});
});
return episodes;
}
async function getPlayers(ctx: ScrapeContext, media: Media, url: string): Promise<MediaPlayer[]> {
const response = await ctx.proxiedFetcher.full(url, {
headers: {
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
});
const $ = cheerio.load(response.body);
const players: MediaPlayer[] = [];
$('.link-item').each((_, p) => {
const id = $(p).attr('data-id')?.trim();
const name = $(p).find('span').first()?.text().trim();
if (!id || !name) {
return;
}
players.push({
id,
url: `${media.url.href.replace(/\/tv\//, '/watch-tv/').replace(/\/movie\//, '/watch-movie/')}.${id}`,
name,
});
});
return players;
}
export async function getEpisodePlayers(
ctx: ScrapeContext,
media: Media,
episode: MediaEpisode,
): Promise<MediaPlayer[]> {
return getPlayers(ctx, media, `${SHOW_SERVERS_URL}${episode.id}`);
}
export async function getMoviePlayers(ctx: ScrapeContext, media: Media): Promise<MediaPlayer[]> {
return getPlayers(ctx, media, `${MOVIE_SERVERS_URL}${media.id}`);
}

View file

@ -0,0 +1,173 @@
import * as cheerio from 'cheerio';
import { EmbedOutput } from '@/providers/base';
import { Stream } from '@/providers/streams';
import { EmbedScrapeContext } from '@/utils/context';
import {
BASE_URL,
CLIENT_KEY_PATTERN_1,
CLIENT_KEY_PATTERN_2,
CLIENT_KEY_PATTERN_3,
FETCH_EMBEDS_URL,
FETCH_SOURCES_URL,
} from './utils';
async function getEmbedLink(ctx: EmbedScrapeContext, playerURL: string): Promise<string> {
const sourceID = playerURL.split('.').pop();
const response = await ctx.proxiedFetcher.full(`${FETCH_EMBEDS_URL}${sourceID}`, {
headers: {
Referer: playerURL,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
});
return response.body.link;
}
async function getClientKey(ctx: EmbedScrapeContext, embedURL: string): Promise<string | undefined> {
const response = await ctx.proxiedFetcher.full(embedURL, {
headers: {
Referer: `${BASE_URL}/`,
'Sec-Fetch-Dest': 'iframe',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'cross-site',
},
});
const $ = cheerio.load(response.body);
let key: string | undefined = '';
// script containing key (either plain or encoded)
$('script').each((_, script) => {
if (key) {
return false;
}
const text = $(script).text().trim();
// encoded key
let match = CLIENT_KEY_PATTERN_2.exec(text);
if (match) {
key = match.slice(1).join('').trim();
return;
}
// direct client key
match = CLIENT_KEY_PATTERN_1.exec(text);
if (!match) {
// no key
return;
}
key = match[1].trim();
});
// dummy script with key as attribute
$('script').each((_, script) => {
if (key) {
return false;
}
const attr = $(script).attr('nonce');
if (!attr) {
return;
}
key = attr.trim();
});
// dummy div with key as attribute
$('div').each((_, div) => {
if (key) {
return false;
}
const attr = $(div).attr('data-dpi');
if (!attr) {
return;
}
key = attr.trim();
});
// custom meta tag with key
$('meta').each((_, meta) => {
if (key) {
return false;
}
const name = $(meta).attr('name')?.trim();
const content = $(meta).attr('content')?.trim();
if (!name || !content || name !== '_gg_fb') {
return;
}
key = content.trim();
});
// comment containing key
$('*')
.contents()
.each((_, node) => {
if (key) {
return false;
}
if (node.nodeType === 8) {
const match = CLIENT_KEY_PATTERN_3.exec(node.nodeValue.trim());
if (!match) {
return;
}
key = match[1].trim();
}
});
return key;
}
export async function scrapeUpCloudEmbed(ctx: EmbedScrapeContext): Promise<EmbedOutput> {
const embedURL = URL.parse(await getEmbedLink(ctx, ctx.url));
if (!embedURL) {
throw new Error('Failed to get embed URL (invalid movie?)');
}
console.log('Embed URL', embedURL.href);
const embedID = embedURL.pathname.split('/').pop();
if (!embedID) {
throw new Error('Failed to get embed ID');
}
console.log('Embed ID', embedID);
const clientKey = await getClientKey(ctx, embedURL.href);
if (!clientKey) {
throw new Error('Failed to get client key');
}
console.log('Client key', clientKey);
const response = await ctx.proxiedFetcher.full(`${FETCH_SOURCES_URL}?id=${embedID}&_k=${clientKey}`, {
headers: {
Referer: embedURL.href,
Origin: 'https://streameeeeee.site',
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
});
if (!response.body.sources || response.body.sources.length == 0) {
console.warn('Server gave no sources', response.body);
return {
stream: [],
};
}
return {
stream: (response.body.sources as any[]).map((source: any, i: number): Stream => {
return {
type: 'hls',
id: `stream-${i}`,
flags: ['cors-allowed'],
captions: [],
playlist: source.file,
headers: {
Referer: 'https://streameeeeee.site/',
Origin: 'https://streameeeeee.site',
},
};
}) as Stream[],
};
}

View file

@ -0,0 +1,17 @@
export const BASE_URL = 'https://dopebox.to';
export const SEARCH_URL = `${BASE_URL}/search/`;
export const SEASONS_URL = `${BASE_URL}/ajax/season/list/`; // <media-id>
export const EPISODES_URL = `${BASE_URL}/ajax/season/episodes/`; // <season-id>
export const SHOW_SERVERS_URL = `${BASE_URL}/ajax/episode/servers/`; // <episode-id>
export const MOVIE_SERVERS_URL = `${BASE_URL}/ajax/episode/list/`; // <media-id>
export const FETCH_EMBEDS_URL = `${BASE_URL}/ajax/episode/sources/`;
export const FETCH_SOURCES_URL = 'https://streameeeeee.site/embed-1/v3/e-1/getSources';
export const CLIENT_KEY_PATTERN_1 =
/window\._lk_db\s*?=\s*?{\s*?x:\s*?"(\w+)?",\s*?y:\s*?"(\w+)?",\s*?z:\s*?"(\w+)?"\s*?}/;
export const CLIENT_KEY_PATTERN_2 = /window\._xy_ws\s*?=\s*?"(\w+)?"/;
export const CLIENT_KEY_PATTERN_3 = /\s*?_is_th:\s*?(\w+)\s*?/;
export function getSearchQuery(title: string): string {
return title.trim().split(' ').join('-').toLowerCase();
}

View file

@ -0,0 +1,127 @@
import * as cheerio from 'cheerio';
import type { CheerioAPI } from 'cheerio';
import { FetcherResponse } from '@/fetchers/types';
import { EmbedScrapeContext, ScrapeContext } from '@/utils/context';
import { ORIGIN_HOST, fetchIFrame, throwOnResponse } from './utils';
import { EmbedOutput } from '../../base';
const LOG_PREFIX = `[Doodstream]`;
const STREAM_REQ_PATERN = /\$\.get\('(\/pass_md5\/.+?)'/;
const TOKEN_PARAMS_PATERN = /\+ "\?(token=.+?)"/;
// This nonsense is added to the end of the URL alongside the token params
function generateStreamKey(): string {
const CHARS: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let o = 0; o < 10; o++) {
result += CHARS.charAt(Math.floor(Math.random() * CHARS.length));
}
return result;
}
function extractStreamInfo($: CheerioAPI): [string | undefined, string | undefined] {
let streamReq: string | undefined;
let tokenParams: string | undefined;
$('script').each((_, script) => {
if (streamReq && tokenParams) {
return;
}
const text = $(script).text().trim();
if (!streamReq) {
streamReq = text.match(STREAM_REQ_PATERN)?.[1];
}
if (!tokenParams) {
tokenParams = text.match(TOKEN_PARAMS_PATERN)?.[1];
}
});
tokenParams = `${generateStreamKey()}?${tokenParams}${Date.now()}`;
return [streamReq, tokenParams];
}
async function getStream(ctx: ScrapeContext, url: string): Promise<[string, string] | undefined> {
console.log(LOG_PREFIX, 'Fetching iframe');
let $: CheerioAPI;
let streamHost: string;
let reqReferer: string;
try {
const response: FetcherResponse | undefined = await fetchIFrame(ctx, url);
if (!response) {
return undefined;
}
$ = cheerio.load(response.body);
streamHost = new URL(response.finalUrl).hostname;
reqReferer = response.finalUrl;
} catch (error) {
console.error(LOG_PREFIX, 'Failed to fetch iframe', error);
return undefined;
}
const [streamReq, tokenParams] = extractStreamInfo($);
if (!streamReq || !tokenParams) {
console.error(LOG_PREFIX, "Couldn't find stream info", streamReq, tokenParams);
return undefined;
}
console.log(LOG_PREFIX, 'Stream info', streamReq, tokenParams);
let streamURL: string;
try {
const response: FetcherResponse = await ctx.proxiedFetcher.full(`https://${streamHost}${streamReq}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
Referer: reqReferer,
Origin: ORIGIN_HOST,
},
});
throwOnResponse(response);
streamURL = (await response.body) + tokenParams;
} catch (error) {
console.error(LOG_PREFIX, 'Failed to request stream URL', error);
return undefined;
}
console.log(LOG_PREFIX, 'Stream URL', streamURL);
return [streamURL, streamHost];
}
export async function scrapeDoodstreamEmbed(ctx: EmbedScrapeContext): Promise<EmbedOutput> {
console.log(LOG_PREFIX, 'Scraping stream URL', ctx.url);
let streamURL: string | undefined;
let streamHost: string | undefined;
try {
const stream = await getStream(ctx, ctx.url);
if (!stream || !stream[0]) {
return {
stream: [],
};
}
[streamURL, streamHost] = stream;
} catch (error) {
console.warn(LOG_PREFIX, 'Failed to get stream', error);
throw error;
}
return {
stream: [
{
type: 'file',
id: 'primary',
flags: ['cors-allowed'],
captions: [],
qualities: {
unknown: {
type: 'mp4',
url: streamURL,
},
},
headers: {
Referer: `https://${streamHost}/`,
Origin: ORIGIN_HOST,
},
},
],
};
}

View file

@ -0,0 +1,122 @@
import * as cheerio from 'cheerio';
import type { CheerioAPI } from 'cheerio';
import { FetcherResponse } from '@/fetchers/types';
import { EmbedScrapeContext, ScrapeContext } from '@/utils/context';
import { ORIGIN_HOST, fetchIFrame, throwOnResponse } from './utils';
import { EmbedOutput } from '../../base';
const LOG_PREFIX = `[Filemoon]`;
const UNPACK_PARAMS_PATERN = /eval\(.+?}\(('.+'),(\d+),(\d+),('.+')\.split\('(.)'\).+/;
function unpack(payload: string, radix: number, id: number, map: string[]) {
while (id--) {
if (map[id]) {
payload = payload.replace(new RegExp(`\\b${id.toString(radix)}\\b`, 'g'), map[id]);
}
}
return payload;
}
function deobfuscatePlayerCfg(data: string): string | undefined {
const match = data.match(UNPACK_PARAMS_PATERN);
if (!match) {
return undefined;
}
const obfPayload: string = match[1];
const radix: number = Number.parseInt(match[2]);
const id: number = Number.parseInt(match[3]);
const obfMap: string = match[4];
const mapChar: string = match[5];
return unpack(obfPayload, radix, id, obfMap.split(mapChar));
}
async function getStream(ctx: ScrapeContext, url: string): Promise<string | undefined> {
console.log(LOG_PREFIX, 'Fetching iframe');
let $: CheerioAPI;
let vpReferer: string;
try {
const response: FetcherResponse | undefined = await fetchIFrame(ctx, url);
if (!response) {
return undefined;
}
$ = cheerio.load(await response.body);
vpReferer = response.finalUrl;
} catch (error) {
console.error(LOG_PREFIX, 'Failed to fetch iframe', error);
return undefined;
}
const videoPlayerURL: string | undefined = $('#iframe-holder').find('iframe').first().attr('src');
if (!videoPlayerURL) {
console.error(LOG_PREFIX, 'Could not find video player URL');
return undefined;
}
console.log(LOG_PREFIX, 'Video player URL', videoPlayerURL);
try {
const response: FetcherResponse = await ctx.proxiedFetcher.full(videoPlayerURL, {
headers: {
Referer: vpReferer,
Origin: ORIGIN_HOST,
},
});
throwOnResponse(response);
$ = cheerio.load(await response.body);
} catch (error) {
console.error(LOG_PREFIX, 'Failed to fetch video player', error);
return undefined;
}
let streamURL: string | undefined;
$('script').each((_, script) => {
if (streamURL) {
return;
}
const cfgScript = deobfuscatePlayerCfg($(script).text());
if (!cfgScript) {
return undefined;
}
const url = cfgScript.match('file:"(https?://.+?)"')?.[1];
if (!url) {
return;
}
streamURL = url;
});
console.log(LOG_PREFIX, 'Stream URL', streamURL);
return streamURL;
}
export async function scrapeFilemoonEmbed(ctx: EmbedScrapeContext): Promise<EmbedOutput> {
console.log(LOG_PREFIX, 'Scraping stream URL', ctx.url);
let streamURL: string | undefined;
try {
streamURL = await getStream(ctx, ctx.url);
} catch (error) {
console.warn(LOG_PREFIX, 'Failed to get stream', error);
throw error;
}
if (!streamURL) {
return {
stream: [],
};
}
return {
stream: [
{
type: 'hls',
id: 'primary',
flags: ['cors-allowed'],
captions: [],
playlist: streamURL,
headers: {
Referer: ORIGIN_HOST,
Origin: ORIGIN_HOST,
},
},
],
};
}

View file

@ -0,0 +1,145 @@
import * as cheerio from 'cheerio';
import type { CheerioAPI } from 'cheerio';
import { FetcherResponse } from '@/fetchers/types';
import { SourcererEmbed, SourcererOutput, makeEmbed, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ScrapeContext, ShowScrapeContext } from '@/utils/context';
import { scrapeDoodstreamEmbed } from './doodstream';
import { scrapeFilemoonEmbed } from './filemoon';
import { EMBED_URL, ORIGIN_HOST, getMoviePageURL, throwOnResponse } from './utils';
export const LOG_PREFIX = '[FSOnline]';
async function getMovieID(ctx: ScrapeContext, url: string): Promise<string | undefined> {
console.log(LOG_PREFIX, 'Scraping movie ID from', url);
let $: CheerioAPI;
try {
const response: FetcherResponse = await ctx.proxiedFetcher.full(url, {
headers: {
Origin: ORIGIN_HOST,
Referer: ORIGIN_HOST,
},
});
throwOnResponse(response);
$ = cheerio.load(await response.body);
} catch (error) {
console.error(LOG_PREFIX, 'Failed to fetch movie page', url, error);
return undefined;
}
const movieID: string | undefined = $('#show_player_lazy').attr('movie-id');
if (!movieID) {
console.error(LOG_PREFIX, 'Could not find movie ID', url);
return undefined;
}
console.log(LOG_PREFIX, 'Movie ID', movieID);
return movieID;
}
async function getMovieSources(ctx: ScrapeContext, id: string, refererHeader: string): Promise<Map<string, string>> {
console.log(LOG_PREFIX, 'Scraping movie sources for', id);
const sources: Map<string, string> = new Map<string, string>();
let $: CheerioAPI;
try {
const response: FetcherResponse = await ctx.proxiedFetcher.full(EMBED_URL, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Referer: refererHeader,
Origin: ORIGIN_HOST,
},
body: `action=lazy_player&movieID=${id}`,
});
throwOnResponse(response);
$ = cheerio.load(await response.body);
} catch (error) {
console.error(LOG_PREFIX, 'Could not fetch source index', error);
return sources;
}
$('li.dooplay_player_option').each((_, element) => {
const name: string = $(element).find('span').text().trim();
const url: string | undefined = $(element).attr('data-vs');
if (!url) {
console.warn(LOG_PREFIX, 'Skipping invalid source', name);
return;
}
console.log(LOG_PREFIX, 'Found movie source for', id, name, url);
sources.set(name, url);
});
return sources;
}
function addEmbedFromSources(name: string, sources: Map<string, string>, embeds: SourcererEmbed[]) {
const url = sources.get(name);
if (!url) {
return;
}
embeds.push({
embedId: `fsonline-${name.toLowerCase()}`,
url,
});
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
// always use the english title
const moviePageURL = getMoviePageURL(
ctx.media.type === 'movie' ? `${ctx.media.title} ${ctx.media.releaseYear}` : ctx.media.title,
ctx.media.type === 'show' ? ctx.media.season.number : undefined,
ctx.media.type === 'show' ? ctx.media.episode.number : undefined,
);
console.log(LOG_PREFIX, 'Movie page URL', moviePageURL);
const movieID = await getMovieID(ctx, moviePageURL);
if (!movieID) {
return {
embeds: [],
stream: [],
};
}
const embeds: SourcererEmbed[] = [];
const sources: Map<string, string> = await getMovieSources(ctx, movieID, moviePageURL);
addEmbedFromSources('Filemoon', sources, embeds);
addEmbedFromSources('Doodstream', sources, embeds);
if (embeds.length < 1) {
throw new Error('No valid sources were found');
}
return {
embeds,
};
}
export const fsOnlineScraper = makeSourcerer({
id: 'fsonline',
name: 'FSOnline',
rank: 500,
flags: ['cors-allowed'],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
export const fsOnlineEmbeds = [
makeEmbed({
id: 'fsonline-doodstream',
name: 'Doodstream',
rank: 5001,
scrape: scrapeDoodstreamEmbed,
flags: ['cors-allowed'],
}),
makeEmbed({
id: 'fsonline-filemoon',
name: 'Filemoon',
rank: 5002,
scrape: scrapeFilemoonEmbed,
flags: ['cors-allowed'],
}),
];

View file

@ -0,0 +1,44 @@
import { FetcherResponse } from '@/fetchers/types';
import { ScrapeContext } from '@/utils/context';
export const ORIGIN_HOST = 'https://www3.fsonline.app';
export const MOVIE_PAGE_URL = 'https://www3.fsonline.app/film/';
export const SHOW_PAGE_URL = 'https://www3.fsonline.app/episoade/{{MOVIE}}-sezonul-{{SEASON}}-episodul-{{EPISODE}}/';
export const EMBED_URL = 'https://www3.fsonline.app/wp-admin/admin-ajax.php';
export function throwOnResponse(response: FetcherResponse) {
if (response.statusCode >= 400) {
throw new Error(`Response does not indicate success: ${response.statusCode}`);
}
}
export function getMoviePageURL(name: string, season?: number, episode?: number): string {
name = name
.trim()
.normalize('NFD')
.toLowerCase()
.replace(/[^a-zA-Z0-9. ]+/g, '')
.replace('.', ' ')
.split(' ')
.join('-');
if (season && episode) {
return SHOW_PAGE_URL.replace('{{MOVIE}}', name)
.replace('{{SEASON}}', `${season}`)
.replace('{{EPISODE}}', `${episode}`);
}
return `${MOVIE_PAGE_URL}${name}/`;
}
export async function fetchIFrame(ctx: ScrapeContext, url: string): Promise<FetcherResponse | undefined> {
const response: FetcherResponse = await ctx.proxiedFetcher.full(url, {
headers: {
Referer: `${ORIGIN_HOST}/`,
// Origin: ORIGIN_HOST,
'sec-fetch-dest': 'iframe',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'cross-site',
},
});
throwOnResponse(response);
return response;
}