Refactoring to try to align to codebase

- also rip filemoon
This commit is contained in:
vlOd 2025-12-10 03:01:09 +02:00
parent 6c5af97081
commit cfb8da1366
10 changed files with 48 additions and 167 deletions

14
package-lock.json generated
View file

@ -1680,7 +1680,6 @@
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
@ -2358,7 +2357,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3472,8 +3470,7 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/diff-sequences": {
"version": "29.6.3",
@ -3892,7 +3889,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@ -3979,7 +3975,6 @@
"integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@ -4078,7 +4073,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -6902,7 +6896,6 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -7244,7 +7237,6 @@
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@ -8112,7 +8104,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -8340,7 +8331,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -8477,7 +8467,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@ -8589,7 +8578,6 @@
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "1.6.1",
"@vitest/runner": "1.6.1",

View file

@ -1,6 +1,4 @@
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';
@ -8,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';

View file

@ -1,122 +0,0 @@
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

@ -78,7 +78,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
export const dopeboxScraper = makeSourcerer({
id: 'dopebox',
name: 'Dopebox',
rank: 600,
rank: 210,
flags: ['cors-allowed'],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
@ -88,8 +88,8 @@ export const dopeboxEmbeds = [
makeEmbed({
id: 'dopebox-upcloud',
name: 'UpCloud',
rank: 6001,
flags: [],
rank: 2101,
flags: ['cors-allowed'],
scrape: scrapeUpCloudEmbed,
}),
];

View file

@ -123,19 +123,19 @@ export async function scrapeUpCloudEmbed(ctx: EmbedScrapeContext): Promise<Embed
if (!embedURL) {
throw new Error('Failed to get embed URL (invalid movie?)');
}
console.log('Embed URL', embedURL.href);
// 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);
// 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);
// console.log('Client key', clientKey);
const response = await ctx.proxiedFetcher.full(`${FETCH_SOURCES_URL}?id=${embedID}&_k=${clientKey}`, {
headers: {

View file

@ -43,7 +43,7 @@ function extractStreamInfo($: CheerioAPI): [string | undefined, string | undefin
}
async function getStream(ctx: ScrapeContext, url: string): Promise<[string, string] | undefined> {
console.log(LOG_PREFIX, 'Fetching iframe');
// console.log(LOG_PREFIX, 'Fetching iframe');
let $: CheerioAPI;
let streamHost: string;
@ -66,7 +66,7 @@ async function getStream(ctx: ScrapeContext, url: string): Promise<[string, stri
console.error(LOG_PREFIX, "Couldn't find stream info", streamReq, tokenParams);
return undefined;
}
console.log(LOG_PREFIX, 'Stream info', streamReq, tokenParams);
// console.log(LOG_PREFIX, 'Stream info', streamReq, tokenParams);
let streamURL: string;
try {
@ -83,13 +83,13 @@ async function getStream(ctx: ScrapeContext, url: string): Promise<[string, stri
console.error(LOG_PREFIX, 'Failed to request stream URL', error);
return undefined;
}
console.log(LOG_PREFIX, 'Stream URL', streamURL);
// 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);
// console.log(LOG_PREFIX, 'Scraping stream URL', ctx.url);
let streamURL: string | undefined;
let streamHost: string | undefined;
try {

View file

@ -6,13 +6,12 @@ import { SourcererEmbed, SourcererOutput, makeEmbed, makeSourcerer } from '@/pro
import { MovieScrapeContext, ScrapeContext, ShowScrapeContext } from '@/utils/context';
import { scrapeDoodstreamEmbed } from './doodstream';
import { scrapeFilemoonEmbed } from './filemoon';
import { EMBED_URL, ORIGIN_HOST, getMoviePageURL, throwOnResponse } from './utils';
import { EMBED_URL, ORIGIN_HOST, fetchENTMDBName, 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);
// console.log(LOG_PREFIX, 'Scraping movie ID from', url);
let $: CheerioAPI;
try {
@ -34,13 +33,13 @@ async function getMovieID(ctx: ScrapeContext, url: string): Promise<string | und
console.error(LOG_PREFIX, 'Could not find movie ID', url);
return undefined;
}
console.log(LOG_PREFIX, 'Movie ID', movieID);
// 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);
// console.log(LOG_PREFIX, 'Scraping movie sources for', id);
const sources: Map<string, string> = new Map<string, string>();
let $: CheerioAPI;
@ -69,7 +68,7 @@ async function getMovieSources(ctx: ScrapeContext, id: string, refererHeader: st
console.warn(LOG_PREFIX, 'Skipping invalid source', name);
return;
}
console.log(LOG_PREFIX, 'Found movie source for', id, name, url);
// console.log(LOG_PREFIX, 'Found movie source for', id, name, url);
sources.set(name, url);
});
@ -88,13 +87,13 @@ function addEmbedFromSources(name: string, sources: Map<string, string>, embeds:
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
// always use the english title
const movieName = await fetchENTMDBName(Number(ctx.media.tmdbId), ctx.media.type);
const moviePageURL = getMoviePageURL(
ctx.media.type === 'movie' ? `${ctx.media.title} ${ctx.media.releaseYear}` : ctx.media.title,
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);
// console.log(LOG_PREFIX, 'Movie page URL', moviePageURL);
const movieID = await getMovieID(ctx, moviePageURL);
if (!movieID) {
@ -121,7 +120,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
export const fsOnlineScraper = makeSourcerer({
id: 'fsonline',
name: 'FSOnline',
rank: 500,
rank: 200,
flags: ['cors-allowed'],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
@ -131,15 +130,15 @@ export const fsOnlineEmbeds = [
makeEmbed({
id: 'fsonline-doodstream',
name: 'Doodstream',
rank: 5001,
rank: 2001,
scrape: scrapeDoodstreamEmbed,
flags: ['cors-allowed'],
}),
makeEmbed({
id: 'fsonline-filemoon',
name: 'Filemoon',
rank: 5002,
scrape: scrapeFilemoonEmbed,
flags: ['cors-allowed'],
}),
// makeEmbed({
// id: 'fsonline-filemoon',
// name: 'Filemoon',
// rank: 2002,
// scrape: scrapeFilemoonEmbed,
// flags: ['cors-allowed'],
// }),
];

View file

@ -5,6 +5,7 @@ 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';
const TMDB_API_KEY = 'a500049f3e06109fe3e8289b06cf5685';
export function throwOnResponse(response: FetcherResponse) {
if (response.statusCode >= 400) {
@ -29,11 +30,26 @@ export function getMoviePageURL(name: string, season?: number, episode?: number)
return `${MOVIE_PAGE_URL}${name}/`;
}
export async function fetchENTMDBName(tmdbId: number, mediaType: 'movie' | 'show'): Promise<string> {
const endpoint =
mediaType === 'movie'
? `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US`
: `https://api.themoviedb.org/3/tv/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US`;
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;
}
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,
Referer: ORIGIN_HOST,
Origin: ORIGIN_HOST,
'sec-fetch-dest': 'iframe',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'cross-site',