Merge pull request #92 from movie-web/dev

Version 2.2
This commit is contained in:
mrjvs
2024-02-10 21:12:32 +01:00
committed by GitHub
26 changed files with 881 additions and 29 deletions

View File

@@ -2,6 +2,16 @@
title: 'Changelog'
---
# Version 2.2.0
- Fixed vidsrc.me URL decoding.
- Added ridomovies with Ridoo and Closeload embed.
- Added Goojara.to source.
- Fixed VidSrcTo crashing if no subtitles are found.
- Added Nepu Provider.
- Added vidcloud to flixhq and zoechip.
- Add thumbnail track option to response (Not supported by any providers yet).
- Disabled Lookmovie and swapped Showbox and VidSrcTo in ranking.
# Version 2.1.1
- Fixed vidplay decryption keys being wrong and switched the domain to one that works

36
package-lock.json generated
View File

@@ -1,26 +1,30 @@
{
"name": "@movie-web/providers",
"version": "2.1.0",
"version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@movie-web/providers",
"version": "2.1.0",
"version": "2.2.0",
"license": "MIT",
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"cookie": "^0.6.0",
"crypto-js": "^4.1.1",
"form-data": "^4.0.0",
"iso-639-1": "^3.1.0",
"nanoid": "^3.3.6",
"node-fetch": "^2.7.0",
"set-cookie-parser": "^2.6.0",
"unpacker": "^1.0.1"
},
"devDependencies": {
"@types/cookie": "^0.6.0",
"@types/crypto-js": "^4.1.1",
"@types/node-fetch": "^2.6.6",
"@types/randombytes": "^2.0.1",
"@types/set-cookie-parser": "^2.4.7",
"@types/spinnies": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
@@ -687,6 +691,12 @@
"@types/chai": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true
},
"node_modules/@types/crypto-js": {
"version": "4.2.1",
"dev": true,
@@ -751,6 +761,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/set-cookie-parser": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.7.tgz",
"integrity": "sha512-+ge/loa0oTozxip6zmhRIk8Z/boU51wl9Q6QdLZcokIGMzY5lFXYy/x7Htj2HTC6/KZP1hUbZ1ekx8DYXICvWg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/spinnies": {
"version": "0.5.3",
"dev": true,
@@ -1764,6 +1783,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@@ -4789,6 +4816,11 @@
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
},
"node_modules/set-function-length": {
"version": "1.1.1",
"dev": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@movie-web/providers",
"version": "2.1.1",
"version": "2.2.0",
"description": "Package that contains all the providers of movie-web",
"main": "./lib/index.umd.js",
"types": "./lib/index.d.ts",
@@ -48,9 +48,11 @@
"prepublishOnly": "npm test && npm run lint"
},
"devDependencies": {
"@types/cookie": "^0.6.0",
"@types/crypto-js": "^4.1.1",
"@types/node-fetch": "^2.6.6",
"@types/randombytes": "^2.0.1",
"@types/set-cookie-parser": "^2.4.7",
"@types/spinnies": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
@@ -80,11 +82,13 @@
},
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"cookie": "^0.6.0",
"crypto-js": "^4.1.1",
"form-data": "^4.0.0",
"iso-639-1": "^3.1.0",
"nanoid": "^3.3.6",
"node-fetch": "^2.7.0",
"set-cookie-parser": "^2.6.0",
"unpacker": "^1.0.1"
}
}

View File

@@ -1,4 +1,5 @@
import { Embed, Sourcerer } from '@/providers/base';
import { doodScraper } from '@/providers/embeds/dood';
import { febboxHlsScraper } from '@/providers/embeds/febbox/hls';
import { febboxMp4Scraper } from '@/providers/embeds/febbox/mp4';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
@@ -17,10 +18,17 @@ import { showboxScraper } from '@/providers/sources/showbox/index';
import { vidsrcScraper } from '@/providers/sources/vidsrc/index';
import { zoechipScraper } from '@/providers/sources/zoechip';
import { closeLoadScraper } from './embeds/closeload';
import { fileMoonScraper } from './embeds/filemoon';
import { ridooScraper } from './embeds/ridoo';
import { smashyStreamDScraper } from './embeds/smashystream/dued';
import { smashyStreamFScraper } from './embeds/smashystream/video1';
import { vidCloudScraper } from './embeds/vidcloud';
import { vidplayScraper } from './embeds/vidplay';
import { wootlyScraper } from './embeds/wootly';
import { goojaraScraper } from './sources/goojara';
import { nepuScraper } from './sources/nepu';
import { ridooMoviesScraper } from './sources/ridomovies';
import { smashyStreamScraper } from './sources/smashystream';
import { vidSrcToScraper } from './sources/vidsrcto';
@@ -36,7 +44,10 @@ export function gatherAllSources(): Array<Sourcerer> {
vidsrcScraper,
lookmovieScraper,
smashyStreamScraper,
ridooMoviesScraper,
vidSrcToScraper,
nepuScraper,
goojaraScraper,
];
}
@@ -44,6 +55,7 @@ export function gatherAllEmbeds(): Array<Embed> {
// all embeds are gathered here
return [
upcloudScraper,
vidCloudScraper,
mp4uploadScraper,
streamsbScraper,
upstreamScraper,
@@ -54,7 +66,11 @@ export function gatherAllEmbeds(): Array<Embed> {
streambucketScraper,
smashyStreamFScraper,
smashyStreamDScraper,
ridooScraper,
closeLoadScraper,
fileMoonScraper,
vidplayScraper,
wootlyScraper,
doodScraper,
];
}

View File

@@ -0,0 +1,71 @@
import { load } from 'cheerio';
import { unpack } from 'unpacker';
import { flags } from '@/entrypoint/utils/targets';
import { NotFoundError } from '@/utils/errors';
import { makeEmbed } from '../base';
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '../captions';
const referer = 'https://ridomovies.tv/';
export const closeLoadScraper = makeEmbed({
id: 'closeload',
name: 'CloseLoad',
rank: 106,
async scrape(ctx) {
const baseUrl = new URL(ctx.url).origin;
const iframeRes = await ctx.proxiedFetcher<string>(ctx.url, {
headers: { referer },
});
const iframeRes$ = load(iframeRes);
const captions: Caption[] = iframeRes$('track')
.map((_, el) => {
const track = iframeRes$(el);
const url = `${baseUrl}${track.attr('src')}`;
const label = track.attr('label') ?? '';
const language = labelToLanguageCode(label);
const captionType = getCaptionTypeFromUrl(url);
if (!language || !captionType) return null;
return {
id: url,
language,
hasCorsRestrictions: true,
type: captionType,
url,
};
})
.get()
.filter((x) => x !== null);
const evalCode = iframeRes$('script')
.filter((_, el) => {
const script = iframeRes$(el);
return (script.attr('type') === 'text/javascript' && script.html()?.includes('eval')) ?? false;
})
.html();
if (!evalCode) throw new Error("Couldn't find eval code");
const decoded = unpack(evalCode);
const regexPattern = /var\s+(\w+)\s*=\s*"([^"]+)";/g;
const base64EncodedUrl = regexPattern.exec(decoded)?.[2];
if (!base64EncodedUrl) throw new NotFoundError('Unable to find source url');
const url = atob(base64EncodedUrl);
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: url,
captions,
flags: [flags.IP_LOCKED],
headers: {
Referer: 'https://closeload.top/',
Origin: 'https://closeload.top',
},
},
],
};
},
});

View File

@@ -0,0 +1,54 @@
import { customAlphabet } from 'nanoid';
import { makeEmbed } from '@/providers/base';
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 10);
export const doodScraper = makeEmbed({
id: 'dood',
name: 'dood',
rank: 173,
async scrape(ctx) {
const baseUrl = 'https://do0od.com';
const id = ctx.url.split('/d/')[1] || ctx.url.split('/e/')[1];
const doodData = await ctx.proxiedFetcher<string>(`/e/${id}`, {
method: 'GET',
baseUrl,
});
const dataForLater = doodData.match(/a\+"\?token=([^"]+)/)?.[1];
const path = doodData.match(/\$\.get\('\/pass_md5([^']+)/)?.[1];
const doodPage = await ctx.proxiedFetcher<string>(`/pass_md5/${path}`, {
headers: {
referer: `${baseUrl}/e/${id}`,
},
method: 'GET',
baseUrl,
});
const downloadURL = `${doodPage}${nanoid()}?token=${dataForLater}${Date.now()}`;
return {
stream: [
{
id: 'primary',
type: 'file',
flags: [],
captions: [],
qualities: {
unknown: {
type: 'mp4',
url: downloadURL,
headers: {
referer: 'https://do0od.com/',
},
},
},
},
],
};
},
});

View File

@@ -0,0 +1,34 @@
import { flags } from '@/entrypoint/utils/targets';
import { NotFoundError } from '@/utils/errors';
import { makeEmbed } from '../base';
const referer = 'https://ridomovies.tv/';
export const ridooScraper = makeEmbed({
id: 'ridoo',
name: 'Ridoo',
rank: 105,
async scrape(ctx) {
const res = await ctx.proxiedFetcher<string>(ctx.url, {
headers: {
referer,
},
});
const regexPattern = /file:"([^"]+)"/g;
const url = regexPattern.exec(res)?.[1];
if (!url) throw new NotFoundError('Unable to find source url');
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: url,
captions: [],
flags: [flags.CORS_ALLOWED],
},
],
};
},
});

View File

@@ -0,0 +1,19 @@
import { makeEmbed } from '@/providers/base';
import { upcloudScraper } from './upcloud';
export const vidCloudScraper = makeEmbed({
id: 'vidcloud',
name: 'VidCloud',
rank: 201,
async scrape(ctx) {
// Both vidcloud and upcloud have the same embed domain (rabbitstream.net)
const result = await upcloudScraper.scrape(ctx);
return {
stream: result.stream.map((s) => ({
...s,
flags: [],
})),
};
},
});

View File

@@ -9,8 +9,12 @@ export const referer = `${vidplayBase}/`;
// Full credits to @Ciarands!
export const getDecryptionKeys = async (ctx: EmbedScrapeContext): Promise<string[]> => {
const res = await ctx.fetcher<string>('https://raw.githubusercontent.com/Ciarands/vidsrc-keys/main/keys.json');
return JSON.parse(res);
const res = await ctx.proxiedFetcher<string>('https://github.com/Ciarands/vidsrc-keys/blob/main/keys.json');
const regex = /"rawLines":\s*\[([\s\S]*?)\]/;
const rawLines = res.match(regex)?.[1];
if (!rawLines) throw new Error('No keys found');
const keys = JSON.parse(`${rawLines.substring(1).replace(/\\"/g, '"')}]`);
return keys;
};
export const getEncodedId = async (ctx: EmbedScrapeContext) => {

View File

@@ -4,6 +4,14 @@ import { makeEmbed } from '@/providers/base';
const hlsURLRegex = /file:"(.*?)"/;
const setPassRegex = /var pass_path = "(.*set_pass\.php.*)";/;
function formatHlsB64(data: string): string {
const encodedB64 = data.replace(/\/@#@\/[^=/]+==/g, '');
if (encodedB64.match(/\/@#@\/[^=/]+==/)) {
return formatHlsB64(encodedB64);
}
return encodedB64;
}
export const vidsrcembedScraper = makeEmbed({
id: 'vidsrcembed', // VidSrc is both a source and an embed host
name: 'VidSrc',
@@ -15,10 +23,12 @@ export const vidsrcembedScraper = makeEmbed({
},
});
const match = html.match(hlsURLRegex)?.[1]?.replace(/(\/\/\S+?=)|#2|=/g, '');
if (!match) throw new Error('Unable to find HLS playlist');
const finalUrl = atob(match);
// When this eventually breaks see the player js @ pjs_main.js
// If you know what youre doing and are slightly confused about how to reverse this feel free to reach out to ciaran_ds on discord with any queries
let hlsMatch = html.match(hlsURLRegex)?.[1]?.slice(2);
if (!hlsMatch) throw new Error('Unable to find HLS playlist');
hlsMatch = formatHlsB64(hlsMatch);
const finalUrl = atob(hlsMatch);
if (!finalUrl.includes('.m3u8')) throw new Error('Unable to find HLS playlist');
let setPassLink = html.match(setPassRegex)?.[1];

View File

@@ -0,0 +1,83 @@
import { load } from 'cheerio';
import { flags } from '@/entrypoint/utils/targets';
import { makeEmbed } from '@/providers/base';
import { makeCookieHeader, parseSetCookie } from '@/utils/cookie';
export const wootlyScraper = makeEmbed({
id: 'wootly',
name: 'wootly',
rank: 172,
async scrape(ctx) {
const baseUrl = 'https://www.wootly.ch';
const wootlyData = await ctx.proxiedFetcher.full<string>(ctx.url, {
method: 'GET',
readHeaders: ['Set-Cookie'],
});
const cookies = parseSetCookie(wootlyData.headers.get('Set-Cookie') || '');
const wootssesCookie = cookies.wootsses.value;
let $ = load(wootlyData.body); // load the html data
const iframeSrc = $('iframe').attr('src') ?? '';
const woozCookieRequest = await ctx.proxiedFetcher.full<string>(iframeSrc, {
method: 'GET',
readHeaders: ['Set-Cookie'],
headers: {
cookie: makeCookieHeader({ wootsses: wootssesCookie }),
},
});
const woozCookies = parseSetCookie(woozCookieRequest.headers.get('Set-Cookie') || '');
const woozCookie = woozCookies.wooz.value;
const iframeData = await ctx.proxiedFetcher<string>(iframeSrc, {
method: 'POST',
body: new URLSearchParams({ qdf: '1' }),
headers: {
cookie: makeCookieHeader({ wooz: woozCookie }),
Referer: iframeSrc,
},
});
$ = load(iframeData);
const scriptText = $('script').html() ?? '';
// Regular expressions to match the variables
const tk = scriptText.match(/tk=([^;]+)/)?.[0].replace(/tk=|["\s]/g, '');
const vd = scriptText.match(/vd=([^,]+)/)?.[0].replace(/vd=|["\s]/g, '');
if (!tk || !vd) throw new Error('wootly source not found');
const url = await ctx.proxiedFetcher<string>(`/grabd`, {
baseUrl,
query: { t: tk, id: vd },
method: 'GET',
headers: {
cookie: makeCookieHeader({ wooz: woozCookie, wootsses: wootssesCookie }),
},
});
if (!url) throw new Error('wootly source not found');
return {
stream: [
{
id: 'primary',
type: 'file',
flags: [flags.IP_LOCKED],
captions: [],
qualities: {
unknown: {
type: 'mp4',
url,
},
},
},
],
};
},
});

View File

@@ -1,6 +1,7 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeSourcerer } from '@/providers/base';
import { SourcererEmbed, makeSourcerer } from '@/providers/base';
import { upcloudScraper } from '@/providers/embeds/upcloud';
import { vidCloudScraper } from '@/providers/embeds/vidcloud';
import { getFlixhqMovieSources, getFlixhqShowSources, getFlixhqSourceDetails } from '@/providers/sources/flixhq/scrape';
import { getFlixhqId } from '@/providers/sources/flixhq/search';
import { NotFoundError } from '@/utils/errors';
@@ -15,16 +16,25 @@ export const flixhqScraper = makeSourcerer({
if (!id) throw new NotFoundError('no search results match');
const sources = await getFlixhqMovieSources(ctx, ctx.media, id);
const upcloudStream = sources.find((v) => v.embed.toLowerCase() === 'upcloud');
if (!upcloudStream) throw new NotFoundError('upcloud stream not found for flixhq');
const embeds: SourcererEmbed[] = [];
for (const source of sources) {
if (source.embed.toLowerCase() === 'upcloud') {
embeds.push({
embedId: upcloudScraper.id,
url: await getFlixhqSourceDetails(ctx, source.episodeId),
});
} else if (source.embed.toLowerCase() === 'vidcloud') {
embeds.push({
embedId: vidCloudScraper.id,
url: await getFlixhqSourceDetails(ctx, source.episodeId),
});
}
}
return {
embeds: [
{
embedId: upcloudScraper.id,
url: await getFlixhqSourceDetails(ctx, upcloudStream.episodeId),
},
],
embeds,
};
},
async scrapeShow(ctx) {
@@ -32,16 +42,24 @@ export const flixhqScraper = makeSourcerer({
if (!id) throw new NotFoundError('no search results match');
const sources = await getFlixhqShowSources(ctx, ctx.media, id);
const upcloudStream = sources.find((v) => v.embed.toLowerCase() === 'server upcloud');
if (!upcloudStream) throw new NotFoundError('upcloud stream not found for flixhq');
const embeds: SourcererEmbed[] = [];
for (const source of sources) {
if (source.embed.toLowerCase() === 'server upcloud') {
embeds.push({
embedId: upcloudScraper.id,
url: await getFlixhqSourceDetails(ctx, source.episodeId),
});
} else if (source.embed.toLowerCase() === 'server vidcloud') {
embeds.push({
embedId: vidCloudScraper.id,
url: await getFlixhqSourceDetails(ctx, source.episodeId),
});
}
}
return {
embeds: [
{
embedId: upcloudScraper.id,
url: await getFlixhqSourceDetails(ctx, upcloudStream.episodeId),
},
],
embeds,
};
},
});

View File

@@ -0,0 +1,62 @@
import { load } from 'cheerio';
import { ScrapeContext } from '@/utils/context';
import { makeCookieHeader, parseSetCookie } from '@/utils/cookie';
import { EmbedsResult, baseUrl, baseUrl2 } from './type';
export async function getEmbeds(ctx: ScrapeContext, id: string): Promise<EmbedsResult> {
const data = await ctx.fetcher.full(`/${id}`, {
baseUrl: baseUrl2,
headers: {
Referer: baseUrl,
},
readHeaders: ['Set-Cookie'],
method: 'GET',
});
const cookies = parseSetCookie(data.headers.get('Set-Cookie') || '');
const aGoozCookie = cookies.aGooz.value;
const $ = load(data.body);
const RandomCookieName = data.body.split(`_3chk('`)[1].split(`'`)[0];
const RandomCookieValue = data.body.split(`_3chk('`)[1].split(`'`)[2];
const embedRedirectURLs = $('a')
.map((index, element) => $(element).attr('href'))
.get()
.filter((href) => href && href.includes(`${baseUrl2}/go.php`));
const embedPages = await Promise.all(
embedRedirectURLs.map(
(url) =>
ctx.fetcher
.full(url, {
headers: {
cookie: makeCookieHeader({
aGooz: aGoozCookie,
[RandomCookieName]: RandomCookieValue,
}),
Referer: baseUrl2,
},
method: 'GET',
})
.catch(() => null), // Handle errors gracefully
),
);
// Initialize an array to hold the results
const results: EmbedsResult = [];
// Process each page result
for (const result of embedPages) {
if (result) {
const embedId = ['wootly', 'upstream', 'mixdrop', 'dood'].find((a) => result.finalUrl.includes(a));
if (embedId) {
results.push({ embedId, url: result.finalUrl });
}
}
}
return results;
}

View File

@@ -0,0 +1,29 @@
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { scrapeIds, searchAndFindMedia } from './util';
async function universalScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const goojaraData = await searchAndFindMedia(ctx, ctx.media);
if (!goojaraData) throw new NotFoundError('Media not found');
ctx.progress(30);
const embeds = await scrapeIds(ctx, ctx.media, goojaraData);
if (embeds?.length === 0) throw new NotFoundError('No embeds found');
ctx.progress(60);
return {
embeds,
};
}
export const goojaraScraper = makeSourcerer({
id: 'goojara',
name: 'goojara',
rank: 225,
flags: [],
scrapeShow: universalScraper,
scrapeMovie: universalScraper,
});

View File

@@ -0,0 +1,14 @@
export const baseUrl = 'https://www.goojara.to';
export const baseUrl2 = 'https://ww1.goojara.to';
export type EmbedsResult = { embedId: string; url: string }[];
export interface Result {
title: string;
slug: string;
year: string;
type: string;
id_movie?: string;
id_show?: string;
}

View File

@@ -0,0 +1,112 @@
import { load } from 'cheerio';
import { MovieMedia, ShowMedia } from '@/entrypoint/utils/media';
import { compareMedia } from '@/utils/compare';
import { ScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { getEmbeds } from './getEmbeds';
import { EmbedsResult, Result, baseUrl } from './type';
let data;
// The cookie for this headerData doesn't matter, Goojara just checks it's there.
const headersData = {
cookie: `aGooz=t9pmkdtef1b3lg3pmo1u2re816; bd9aa48e=0d7b89e8c79844e9df07a2; _b414=2151C6B12E2A88379AFF2C0DD65AC8298DEC2BF4; 9d287aaa=8f32ad589e1c4288fe152f`,
Referer: 'https://www.goojara.to/',
};
export async function searchAndFindMedia(
ctx: ScrapeContext,
media: MovieMedia | ShowMedia,
): Promise<Result | undefined> {
data = await ctx.fetcher<string>(`/xhrr.php`, {
baseUrl,
headers: headersData,
method: 'POST',
body: new URLSearchParams({ q: media.title }),
});
const $ = load(data);
const results: Result[] = [];
$('.mfeed > li').each((index, element) => {
const title = $(element).find('strong').text();
const yearMatch = $(element)
.text()
.match(/\((\d{4})\)/);
const typeDiv = $(element).find('div').attr('class');
const type = typeDiv === 'it' ? 'show' : typeDiv === 'im' ? 'movie' : '';
const year = yearMatch ? yearMatch[1] : '';
const slug = $(element).find('a').attr('href')?.split('/')[3];
if (!slug) throw new NotFoundError('Not found');
if (media.type === type) {
results.push({ title, year, slug, type });
}
});
const result = results.find((res: Result) => compareMedia(media, res.title, Number(res.year)));
return result;
}
export async function scrapeIds(
ctx: ScrapeContext,
media: MovieMedia | ShowMedia,
result: Result,
): Promise<EmbedsResult> {
// Find the relevant id
let id = null;
if (media.type === 'movie') {
id = result.slug;
} else if (media.type === 'show') {
data = await ctx.fetcher<string>(`/${result.slug}`, {
baseUrl,
headers: headersData,
method: 'GET',
});
const $1 = load(data);
const dataId = $1('#seon').attr('data-id');
if (!dataId) throw new NotFoundError('Not found');
data = await ctx.fetcher<string>(`/xhrc.php`, {
baseUrl,
headers: headersData,
method: 'POST',
body: new URLSearchParams({ s: media.season.number.toString(), t: dataId }),
});
let episodeId = '';
const $2 = load(data);
$2('.seho').each((index, element) => {
// Extracting the episode number as a string
const episodeNumber = $2(element).find('.seep .sea').text().trim();
// Comparing with the desired episode number as a string
if (parseInt(episodeNumber, 10) === media.episode.number) {
const href = $2(element).find('.snfo h1 a').attr('href');
const idMatch = href?.match(/\/([a-zA-Z0-9]+)$/);
if (idMatch && idMatch[1]) {
episodeId = idMatch[1];
return false; // Break out of the loop once the episode is found
}
}
});
id = episodeId;
}
// Check ID
if (id === null) throw new NotFoundError('Not found');
const embeds = await getEmbeds(ctx, id);
return embeds;
}

View File

@@ -32,6 +32,7 @@ async function universalScraper(ctx: MovieScrapeContext | ShowScrapeContext): Pr
export const lookmovieScraper = makeSourcerer({
id: 'lookmovie',
name: 'LookMovie',
disabled: true,
rank: 700,
flags: [flags.IP_LOCKED],
scrapeShow: universalScraper,

View File

@@ -0,0 +1,83 @@
import { load } from 'cheerio';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { compareTitle } from '@/utils/compare';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { SearchResults } from './types';
const nepuBase = 'https://nepu.to';
const nepuReferer = `${nepuBase}/`;
const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => {
const searchResultRequest = await ctx.proxiedFetcher<string>('/ajax/posts', {
baseUrl: nepuBase,
query: {
q: ctx.media.title,
},
});
// json isn't parsed by proxiedFetcher due to content-type being text/html.
const searchResult = JSON.parse(searchResultRequest) as SearchResults;
const show = searchResult.data.find((item) => {
if (!item) return false;
if (ctx.media.type === 'movie' && item.type !== 'Movie') return false;
if (ctx.media.type === 'show' && item.type !== 'Serie') return false;
return compareTitle(ctx.media.title, item.name);
});
if (!show) throw new NotFoundError('No watchable item found');
let videoUrl = show.url;
if (ctx.media.type === 'show') {
videoUrl = `${show.url}/season/${ctx.media.season.number}/episode/${ctx.media.episode.number}`;
}
const videoPage = await ctx.proxiedFetcher<string>(videoUrl, {
baseUrl: nepuBase,
});
const videoPage$ = load(videoPage);
const embedId = videoPage$('a[data-embed]').attr('data-embed');
if (!embedId) throw new NotFoundError('No embed found.');
const playerPage = await ctx.proxiedFetcher<string>('/ajax/embed', {
method: 'POST',
baseUrl: nepuBase,
body: new URLSearchParams({ id: embedId }),
});
const streamUrl = playerPage.match(/"file":"(http[^"]+)"/);
if (!streamUrl) throw new NotFoundError('No stream found.');
return {
embeds: [],
stream: [
{
id: 'primary',
captions: [],
playlist: streamUrl[1],
type: 'hls',
flags: [],
headers: {
Origin: nepuBase,
Referer: nepuReferer,
},
},
],
} as SourcererOutput;
};
export const nepuScraper = makeSourcerer({
id: 'nepu',
name: 'Nepu',
rank: 111,
flags: [],
scrapeMovie: universalScraper,
scrapeShow: universalScraper,
});

View File

@@ -0,0 +1,8 @@
export type SearchResults = {
data: {
id: number;
name: string;
url: string;
type: 'Movie' | 'Serie';
}[];
};

View File

@@ -0,0 +1,75 @@
import { load } from 'cheerio';
import { flags } from '@/entrypoint/utils/targets';
import { SourcererEmbed, makeSourcerer } from '@/providers/base';
import { closeLoadScraper } from '@/providers/embeds/closeload';
import { ridooScraper } from '@/providers/embeds/ridoo';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { IframeSourceResult, SearchResult } from './types';
const ridoMoviesBase = `https://ridomovies.tv`;
const ridoMoviesApiBase = `${ridoMoviesBase}/core/api`;
const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => {
const searchResult = await ctx.proxiedFetcher<SearchResult>('/search', {
baseUrl: ridoMoviesApiBase,
query: {
q: ctx.media.title,
},
});
const show = searchResult.data.items[0];
if (!show) throw new NotFoundError('No watchable item found');
let iframeSourceUrl = `/${show.fullSlug}/videos`;
if (ctx.media.type === 'show') {
const showPageResult = await ctx.proxiedFetcher<string>(`/${show.fullSlug}`, {
baseUrl: ridoMoviesBase,
});
const fullEpisodeSlug = `${show.fullSlug}/season-${ctx.media.season.number}/episode-${ctx.media.episode.number}`;
const regexPattern = new RegExp(
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\\\"fullSlug\\\\\\":\\\\\\"${fullEpisodeSlug}\\\\\\")`,
'g',
);
const matches = [...showPageResult.matchAll(regexPattern)];
const episodeIds = matches.map((match) => match[1]);
if (episodeIds.length === 0) throw new NotFoundError('No watchable item found');
const episodeId = episodeIds.at(-1);
iframeSourceUrl = `/episodes/${episodeId}/videos`;
}
const iframeSource = await ctx.proxiedFetcher<IframeSourceResult>(iframeSourceUrl, {
baseUrl: ridoMoviesApiBase,
});
const iframeSource$ = load(iframeSource.data[0].url);
const iframeUrl = iframeSource$('iframe').attr('data-src');
if (!iframeUrl) throw new NotFoundError('No watchable item found');
const embeds: SourcererEmbed[] = [];
if (iframeUrl.includes('closeload')) {
embeds.push({
embedId: closeLoadScraper.id,
url: iframeUrl,
});
}
if (iframeUrl.includes('ridoo')) {
embeds.push({
embedId: ridooScraper.id,
url: iframeUrl,
});
}
return {
embeds,
};
};
export const ridooMoviesScraper = makeSourcerer({
id: 'ridomovies',
name: 'RidoMovies',
rank: 105,
flags: [flags.CORS_ALLOWED],
scrapeMovie: universalScraper,
scrapeShow: universalScraper,
});

View File

@@ -0,0 +1,78 @@
export interface Content {
id: string;
type: string;
slug: string;
title: string;
metaTitle: any;
metaDescription: any;
usersOnly: boolean;
userLevel: number;
vipOnly: boolean;
copyrighted: boolean;
status: string;
publishedAt: string;
createdAt: string;
updatedAt: string;
fullSlug: string;
}
export interface Contentable {
id: string;
contentId: string;
revisionId: any;
originalTitle: string;
overview: string;
releaseDate: string;
releaseYear: string;
videoNote: any;
posterNote: any;
userRating: number;
imdbRating: number;
imdbVotes: number;
imdbId: string;
duration: number;
countryCode: string;
posterPath: string;
backdropPath: string;
apiPosterPath: string;
apiBackdropPath: string;
trailerUrl: string;
mpaaRating: string;
tmdbId: number;
manual: number;
directorId: number;
createdAt: string;
updatedAt: string;
content: Content;
}
export interface SearchResultItem {
id: string;
type: string;
slug: string;
title: string;
metaTitle: any;
metaDescription: any;
usersOnly: boolean;
userLevel: number;
vipOnly: boolean;
copyrighted: boolean;
status: string;
publishedAt: string;
createdAt: string;
updatedAt: string;
fullSlug: string;
contentable: Contentable;
}
export type SearchResult = {
data: {
items: SearchResultItem[];
};
};
export type IframeSourceResult = {
data: {
url: string;
}[];
};

View File

@@ -41,7 +41,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
export const showboxScraper = makeSourcerer({
id: 'showbox',
name: 'Showbox',
rank: 300,
rank: 400,
flags: [flags.CORS_ALLOWED, flags.CF_BLOCKED],
scrapeShow: comboScraper,
scrapeMovie: comboScraper,

View File

@@ -46,7 +46,10 @@ const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Pr
}
// Originally Filemoon does not have subtitles. But we can use the ones from Vidplay.
const subtitleUrl = new URL(embedUrls.find((v) => v.includes('sub.info')) ?? '').searchParams.get('sub.info');
const urlWithSubtitles = embedUrls.find((v) => v.includes('sub.info'));
let subtitleUrl: string | null = null;
if (urlWithSubtitles) subtitleUrl = new URL(urlWithSubtitles).searchParams.get('sub.info');
for (const source of sources.result) {
if (source.title === 'Vidplay') {
const embedUrl = embedUrls.find((v) => v.includes('vidplay'));
@@ -80,5 +83,5 @@ export const vidSrcToScraper = makeSourcerer({
scrapeMovie: universalScraper,
scrapeShow: universalScraper,
flags: [],
rank: 400,
rank: 300,
});

View File

@@ -1,6 +1,7 @@
import { mixdropScraper } from '@/providers/embeds/mixdrop';
import { upcloudScraper } from '@/providers/embeds/upcloud';
import { upstreamScraper } from '@/providers/embeds/upstream';
import { vidCloudScraper } from '@/providers/embeds/vidcloud';
import { getZoeChipSourceURL, getZoeChipSources } from '@/providers/sources/zoechip/scrape';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
@@ -55,6 +56,11 @@ export async function createZoeChipStreamData(ctx: MovieScrapeContext | ShowScra
for (const source of sources) {
const formatted = await formatSource(ctx, source);
if (formatted) {
// Zoechip does not return titles for their sources, so we can not check if a source is upcloud or vidcloud because the domain is the same.
const upCloudAlreadyExists = embeds.find((e) => e.embedId === upcloudScraper.id);
if (formatted.embedId === upcloudScraper.id && upCloudAlreadyExists) {
formatted.embedId = vidCloudScraper.id;
}
embeds.push(formatted);
}
}

View File

@@ -8,10 +8,16 @@ export type StreamFile = {
export type Qualities = 'unknown' | '360' | '480' | '720' | '1080' | '4k';
type ThumbnailTrack = {
type: 'vtt';
url: string;
};
type StreamCommon = {
id: string; // only unique per output
flags: Flags[];
captions: Caption[];
thumbnailTrack?: ThumbnailTrack;
headers?: Record<string, string>; // these headers HAVE to be set to watch the stream
preferredHeaders?: Record<string, string>; // these headers are optional, would improve the stream
};

20
src/utils/cookie.ts Normal file
View File

@@ -0,0 +1,20 @@
import cookie from 'cookie';
import setCookieParser from 'set-cookie-parser';
export interface Cookie {
name: string;
value: string;
}
export function makeCookieHeader(cookies: Record<string, string>): string {
return Object.entries(cookies)
.map(([name, value]) => cookie.serialize(name, value))
.join('; ');
}
export function parseSetCookie(headerValue: string): Record<string, Cookie> {
const parsedCookies = setCookieParser.parse(headerValue, {
map: true,
});
return parsedCookies;
}