diff --git a/package-lock.json b/package-lock.json index 3facef2..f5536e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/package.json b/package.json index 25c4d07..f21eb89 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/providers/all.ts b/src/providers/all.ts index 13e9039..0f3ce9e 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -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'; @@ -23,6 +24,8 @@ import { ridooScraper } from './embeds/ridoo'; import { smashyStreamDScraper } from './embeds/smashystream/dued'; import { smashyStreamFScraper } from './embeds/smashystream/video1'; import { vidplayScraper } from './embeds/vidplay'; +import { wootlyScraper } from './embeds/wootly'; +import { goojaraScraper } from './sources/goojara'; import { ridooMoviesScraper } from './sources/ridomovies'; import { smashyStreamScraper } from './sources/smashystream'; import { vidSrcToScraper } from './sources/vidsrcto'; @@ -41,6 +44,7 @@ export function gatherAllSources(): Array { smashyStreamScraper, ridooMoviesScraper, vidSrcToScraper, + goojaraScraper, ]; } @@ -62,5 +66,7 @@ export function gatherAllEmbeds(): Array { closeLoadScraper, fileMoonScraper, vidplayScraper, + wootlyScraper, + doodScraper, ]; } diff --git a/src/providers/embeds/dood.ts b/src/providers/embeds/dood.ts new file mode 100644 index 0000000..faa79d8 --- /dev/null +++ b/src/providers/embeds/dood.ts @@ -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(`/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(`/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/', + }, + }, + }, + }, + ], + }; + }, +}); diff --git a/src/providers/embeds/wootly.ts b/src/providers/embeds/wootly.ts new file mode 100644 index 0000000..0119926 --- /dev/null +++ b/src/providers/embeds/wootly.ts @@ -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(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(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(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(`/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, + }, + }, + }, + ], + }; + }, +}); diff --git a/src/providers/sources/goojara/getEmbeds.ts b/src/providers/sources/goojara/getEmbeds.ts new file mode 100644 index 0000000..50f574a --- /dev/null +++ b/src/providers/sources/goojara/getEmbeds.ts @@ -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 { + 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; +} diff --git a/src/providers/sources/goojara/index.ts b/src/providers/sources/goojara/index.ts new file mode 100644 index 0000000..ea85d3d --- /dev/null +++ b/src/providers/sources/goojara/index.ts @@ -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 { + 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, +}); diff --git a/src/providers/sources/goojara/type.ts b/src/providers/sources/goojara/type.ts new file mode 100644 index 0000000..ff1923f --- /dev/null +++ b/src/providers/sources/goojara/type.ts @@ -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; +} diff --git a/src/providers/sources/goojara/util.ts b/src/providers/sources/goojara/util.ts new file mode 100644 index 0000000..30d63a9 --- /dev/null +++ b/src/providers/sources/goojara/util.ts @@ -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 { + data = await ctx.fetcher(`/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 { + // Find the relevant id + let id = null; + if (media.type === 'movie') { + id = result.slug; + } else if (media.type === 'show') { + data = await ctx.fetcher(`/${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(`/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; +} diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts new file mode 100644 index 0000000..20fd3e4 --- /dev/null +++ b/src/utils/cookie.ts @@ -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 { + return Object.entries(cookies) + .map(([name, value]) => cookie.serialize(name, value)) + .join('; '); +} + +export function parseSetCookie(headerValue: string): Record { + const parsedCookies = setCookieParser.parse(headerValue, { + map: true, + }); + return parsedCookies; +}