Merge branch 'pr/44' into production

This commit is contained in:
Pas 2025-12-09 23:07:55 -07:00
commit 5c320a5a5c
13 changed files with 10786 additions and 1630 deletions

8972
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

@ -6,8 +6,10 @@ import { serverMirrorEmbed } from '@/providers/embeds/server-mirrors';
import { turbovidScraper } from '@/providers/embeds/turbovid';
import { upcloudScraper } from '@/providers/embeds/upcloud';
import { autoembedScraper } from '@/providers/sources/autoembed';
import { dopeboxEmbeds, dopeboxScraper } from '@/providers/sources/dopebox/index';
import { ee3Scraper } from '@/providers/sources/ee3';
import { fsharetvScraper } from '@/providers/sources/fsharetv';
import { fsOnlineEmbeds, fsOnlineScraper } from '@/providers/sources/fsonline/index';
import { insertunitScraper } from '@/providers/sources/insertunit';
import { mp4hydraScraper } from '@/providers/sources/mp4hydra';
import { nepuScraper } from '@/providers/sources/nepu';
@ -108,6 +110,8 @@ import { zunimeScraper } from './sources/zunime';
export function gatherAllSources(): Array<Sourcerer> {
// all sources are gathered here
return [
fsOnlineScraper,
dopeboxScraper,
cuevana3Scraper,
ridooMoviesScraper,
hdRezkaScraper,
@ -157,6 +161,8 @@ export function gatherAllSources(): Array<Sourcerer> {
export function gatherAllEmbeds(): Array<Embed> {
// all embeds are gathered here
return [
...fsOnlineEmbeds,
...dopeboxEmbeds,
serverMirrorEmbed,
upcloudScraper,
vidCloudScraper,

View file

@ -4,6 +4,7 @@ import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { fetchTMDBName } from '@/utils/tmdb';
const baseUrl = 'https://www.cuevana3.eu';
@ -88,20 +89,6 @@ async function extractVideos(ctx: MovieScrapeContext | ShowScrapeContext, videos
return videoList;
}
async function fetchTmdbTitleInSpanish(tmdbId: number, apiKey: string, mediaType: 'movie' | 'show'): Promise<string> {
const endpoint =
mediaType === 'movie'
? `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${apiKey}&language=es-ES`
: `https://api.themoviedb.org/3/tv/${tmdbId}?api_key=${apiKey}&language=es-ES`;
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`Error fetching TMDB data: ${response.statusText}`);
}
const tmdbData = await response.json();
return mediaType === 'movie' ? tmdbData.title : tmdbData.name;
}
async function fetchTitleSubstitutes(): Promise<Record<string, string>> {
try {
const response = await fetch('https://raw.githubusercontent.com/moonpic/fixed-titles/refs/heads/main/main.json');
@ -115,13 +102,12 @@ async function fetchTitleSubstitutes(): Promise<Record<string, string>> {
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const mediaType = ctx.media.type;
const tmdbId = ctx.media.tmdbId;
const apiKey = 'a500049f3e06109fe3e8289b06cf5685';
if (!tmdbId) {
throw new NotFoundError('TMDB ID is required to fetch the title in Spanish');
}
const translatedTitle = await fetchTmdbTitleInSpanish(Number(tmdbId), apiKey, mediaType);
const translatedTitle = await fetchTMDBName(ctx, 'es-ES');
let normalizedTitle = normalizeTitle(translatedTitle);
let pageUrl =

View file

@ -0,0 +1,97 @@
import Fuse from 'fuse.js';
import { SourcererEmbed, SourcererOutput, makeEmbed, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { fetchTMDBName } from '@/utils/tmdb';
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 mediaTitle = await fetchTMDBName(ctx);
const results = (await searchMedia(ctx, getSearchQuery(mediaTitle))).filter((r) => r.info.includes(mediaType));
const fuse = new Fuse<Media>(results, {
keys: ['title'],
});
const media = fuse.search(mediaTitle).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: 210,
flags: ['cors-allowed'],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
export const dopeboxEmbeds = [
makeEmbed({
id: 'dopebox-upcloud',
name: 'UpCloud',
rank: 101,
flags: ['cors-allowed'],
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,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 { fetchTMDBName } from '@/utils/tmdb';
import { scrapeDoodstreamEmbed } from './doodstream';
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> {
const movieName = await fetchTMDBName(ctx);
const moviePageURL = getMoviePageURL(
ctx.media.type === 'movie' ? `${movieName} ${ctx.media.releaseYear}` : movieName,
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: 200,
flags: ['cors-allowed'],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
export const fsOnlineEmbeds = [
makeEmbed({
id: 'fsonline-doodstream',
name: 'Doodstream',
rank: 172,
scrape: scrapeDoodstreamEmbed,
flags: ['cors-allowed'],
}),
// makeEmbed({
// id: 'fsonline-filemoon',
// name: 'Filemoon',
// rank: 2002,
// 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 {
const n = name
.trim()
.normalize('NFD')
.toLowerCase()
.replace(/[^a-zA-Z0-9. ]+/g, '')
.replace('.', ' ')
.split(' ')
.join('-');
if (season && episode) {
return SHOW_PAGE_URL.replace('{{MOVIE}}', n)
.replace('{{SEASON}}', `${season}`)
.replace('{{EPISODE}}', `${episode}`);
}
return `${MOVIE_PAGE_URL}${n}/`;
}
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;
}

19
src/utils/tmdb.ts Normal file
View file

@ -0,0 +1,19 @@
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
const TMDB_API_KEY = 'a500049f3e06109fe3e8289b06cf5685';
export async function fetchTMDBName(
ctx: ShowScrapeContext | MovieScrapeContext,
lang: string = 'en-US',
): Promise<string> {
const type = ctx.media.type === 'movie' ? 'movie' : 'tv';
const url = `https://api.themoviedb.org/3/${type}/${ctx.media.tmdbId}?api_key=${TMDB_API_KEY}&language=${lang}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error fetching TMDB data: ${response.statusText}`);
}
const data = await response.json();
return ctx.media.type === 'movie' ? data.title : data.name;
}