mirror of
https://github.com/movie-web/providers.git
synced 2025-09-13 10:43:25 +00:00
Merge branch 'dev' into vidcloud
This commit is contained in:
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"name": "@movie-web/providers",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@movie-web/providers",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"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,
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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,11 +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';
|
||||
|
||||
@@ -37,7 +44,10 @@ export function gatherAllSources(): Array<Sourcerer> {
|
||||
vidsrcScraper,
|
||||
lookmovieScraper,
|
||||
smashyStreamScraper,
|
||||
ridooMoviesScraper,
|
||||
vidSrcToScraper,
|
||||
nepuScraper,
|
||||
goojaraScraper,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -56,7 +66,11 @@ export function gatherAllEmbeds(): Array<Embed> {
|
||||
streambucketScraper,
|
||||
smashyStreamFScraper,
|
||||
smashyStreamDScraper,
|
||||
ridooScraper,
|
||||
closeLoadScraper,
|
||||
fileMoonScraper,
|
||||
vidplayScraper,
|
||||
wootlyScraper,
|
||||
doodScraper,
|
||||
];
|
||||
}
|
||||
|
71
src/providers/embeds/closeload.ts
Normal file
71
src/providers/embeds/closeload.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
54
src/providers/embeds/dood.ts
Normal file
54
src/providers/embeds/dood.ts
Normal 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/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
34
src/providers/embeds/ridoo.ts
Normal file
34
src/providers/embeds/ridoo.ts
Normal 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],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
83
src/providers/embeds/wootly.ts
Normal file
83
src/providers/embeds/wootly.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
62
src/providers/sources/goojara/getEmbeds.ts
Normal file
62
src/providers/sources/goojara/getEmbeds.ts
Normal 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;
|
||||
}
|
29
src/providers/sources/goojara/index.ts
Normal file
29
src/providers/sources/goojara/index.ts
Normal 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,
|
||||
});
|
14
src/providers/sources/goojara/type.ts
Normal file
14
src/providers/sources/goojara/type.ts
Normal 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;
|
||||
}
|
112
src/providers/sources/goojara/util.ts
Normal file
112
src/providers/sources/goojara/util.ts
Normal 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;
|
||||
}
|
83
src/providers/sources/nepu/index.ts
Normal file
83
src/providers/sources/nepu/index.ts
Normal 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,
|
||||
});
|
8
src/providers/sources/nepu/types.ts
Normal file
8
src/providers/sources/nepu/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type SearchResults = {
|
||||
data: {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
type: 'Movie' | 'Serie';
|
||||
}[];
|
||||
};
|
75
src/providers/sources/ridomovies/index.ts
Normal file
75
src/providers/sources/ridomovies/index.ts
Normal 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,
|
||||
});
|
78
src/providers/sources/ridomovies/types.ts
Normal file
78
src/providers/sources/ridomovies/types.ts
Normal 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;
|
||||
}[];
|
||||
};
|
@@ -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'));
|
||||
|
20
src/utils/cookie.ts
Normal file
20
src/utils/cookie.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user