mirror of
https://github.com/p-stream/providers.git
synced 2026-01-11 20:10:33 +00:00
Merge branch 'pr/44' into production
This commit is contained in:
commit
5c320a5a5c
13 changed files with 10786 additions and 1630 deletions
8972
package-lock.json
generated
Normal file
8972
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
2621
pnpm-lock.yaml
2621
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
97
src/providers/sources/dopebox/index.ts
Normal file
97
src/providers/sources/dopebox/index.ts
Normal 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,
|
||||
}),
|
||||
];
|
||||
176
src/providers/sources/dopebox/search.ts
Normal file
176
src/providers/sources/dopebox/search.ts
Normal 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}`);
|
||||
}
|
||||
173
src/providers/sources/dopebox/upcloud.ts
Normal file
173
src/providers/sources/dopebox/upcloud.ts
Normal 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[],
|
||||
};
|
||||
}
|
||||
17
src/providers/sources/dopebox/utils.ts
Normal file
17
src/providers/sources/dopebox/utils.ts
Normal 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();
|
||||
}
|
||||
127
src/providers/sources/fsonline/doodstream.ts
Normal file
127
src/providers/sources/fsonline/doodstream.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
145
src/providers/sources/fsonline/index.ts
Normal file
145
src/providers/sources/fsonline/index.ts
Normal 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'],
|
||||
// }),
|
||||
];
|
||||
44
src/providers/sources/fsonline/utils.ts
Normal file
44
src/providers/sources/fsonline/utils.ts
Normal 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
19
src/utils/tmdb.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue