diff --git a/src/providers/all.ts b/src/providers/all.ts index 63be995..2f01869 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -31,6 +31,7 @@ import { streamvidScraper } from './embeds/streamvid'; import { vidCloudScraper } from './embeds/vidcloud'; import { vidplayScraper } from './embeds/vidplay'; import { voeScraper } from './embeds/voe'; +import { warezcdnembedScraper } from './embeds/warezcdn'; import { wootlyScraper } from './embeds/wootly'; import { goojaraScraper } from './sources/goojara'; import { hdRezkaScraper } from './sources/hdrezka'; @@ -39,6 +40,7 @@ import { primewireScraper } from './sources/primewire'; import { ridooMoviesScraper } from './sources/ridomovies'; import { smashyStreamScraper } from './sources/smashystream'; import { vidSrcToScraper } from './sources/vidsrcto'; +import { warezcdnScraper } from './sources/warezcdn'; export function gatherAllSources(): Array { // all sources are gathered here @@ -58,6 +60,7 @@ export function gatherAllSources(): Array { goojaraScraper, hdRezkaScraper, primewireScraper, + warezcdnScraper, ]; } @@ -88,5 +91,6 @@ export function gatherAllEmbeds(): Array { droploadScraper, filelionsScraper, vTubeScraper, + warezcdnembedScraper, ]; } diff --git a/src/providers/embeds/warezcdn.ts b/src/providers/embeds/warezcdn.ts new file mode 100644 index 0000000..8223bfa --- /dev/null +++ b/src/providers/embeds/warezcdn.ts @@ -0,0 +1,128 @@ +import { makeEmbed } from '@/providers/base'; +import { warezcdnPlayerBase } from '@/providers/sources/warezcdn/common'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { Stream } from '../streams'; + +const cdnListing = [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]; + +function decrypt(input: string) { + let output = atob(input); + + // Remove leading and trailing whitespaces + output = output.trim(); + + // Reverse the string + output = output.split('').reverse().join(''); + + // Get the last 5 characters and reverse them + let last = output.slice(-5); + last = last.split('').reverse().join(''); + + // Remove the last 5 characters from the original string + output = output.slice(0, -5); + + // Return the original string concatenated with the reversed last 5 characters + return `${output}${last}`; +} + +async function checkUrls(ctx: EmbedScrapeContext, fileId: string) { + for (const id of cdnListing) { + const url = `https://cloclo${id}.cloud.mail.ru/weblink/view/${fileId}`; + const response = await ctx.proxiedFetcher.full(url, { + method: 'GET', + headers: { + Range: 'bytes=0-1', + }, + }); + if (response.statusCode === 206) return url; + } + return null; +} + +async function getVideoOwlStream(ctx: EmbedScrapeContext, decryptedId: string) { + const sharePage = await ctx.proxiedFetcher('https://cloud.mail.ru/public/uaRH/2PYWcJRpH'); + + const cloudSettings = sharePage.match(/window\.cloudSettings=(\{.*?\})<\/script>/)?.[1]; + if (!cloudSettings) throw new NotFoundError('Failed to get cloudSettings'); + + const parsedCloudSettings = JSON.parse(JSON.stringify(cloudSettings)); + + console.log(parsedCloudSettings); + + const videoOwlUrl = parsedCloudSettings.dispatcher.videowl_view.url; + if (!videoOwlUrl) throw new NotFoundError('Failed to get videoOwlUrl'); + + return `${videoOwlUrl}/${btoa(decryptedId)}.m3u8?${new URLSearchParams({ + double_encode: '1', + })}`; +} + +async function getStream(ctx: EmbedScrapeContext, decryptedId: string): Promise { + try { + const streamUrl = await getVideoOwlStream(ctx, decryptedId); + console.log(streamUrl); + return { + id: 'primary', + type: 'hls', + flags: [], + captions: [], + playlist: streamUrl, + }; + } catch (err) { + console.error(err); + const streamUrl = await checkUrls(ctx, decryptedId); + return { + id: 'primary', + type: 'file', + flags: [], + captions: [], + qualities: { + unknown: { + type: 'mp4', + url: streamUrl!, + }, + }, + }; + } +} + +export const warezcdnembedScraper = makeEmbed({ + id: 'warezcdnembed', // WarezCDN is both a source and an embed host + name: 'WarezCDN', + rank: 82, + async scrape(ctx) { + const page = await ctx.proxiedFetcher(`/player.php?${new URLSearchParams({ id: ctx.url })}`, { + baseUrl: warezcdnPlayerBase, + headers: { + Referer: `${warezcdnPlayerBase}/getEmbed.php?${new URLSearchParams({ + id: ctx.url, + sv: 'warezcdn', + })}`, + }, + }); + const allowanceKey = page.match(/let allowanceKey = "(.*?)";/)?.[1]; + if (!allowanceKey) throw new NotFoundError('Failed to get allowanceKey'); + + const streamData = await ctx.proxiedFetcher('/functions.php', { + baseUrl: warezcdnPlayerBase, + method: 'POST', + body: new URLSearchParams({ + getVideo: ctx.url, + key: allowanceKey, + }), + }); + const stream = JSON.parse(streamData); + + if (!stream.id) throw new NotFoundError("can't get stream id"); + + const decryptedId = decrypt(stream.id); + + if (!decryptedId) throw new NotFoundError("can't get file id"); + + return { + stream: [await getStream(ctx, decryptedId)], + }; + }, +}); diff --git a/src/providers/sources/warezcdn/common.ts b/src/providers/sources/warezcdn/common.ts new file mode 100644 index 0000000..5fcde55 --- /dev/null +++ b/src/providers/sources/warezcdn/common.ts @@ -0,0 +1,3 @@ +export const warezcdnBase = 'https://embed.warezcdn.com'; +export const warezcdnApiBase = 'https://warezcdn.com/embed'; +export const warezcdnPlayerBase = 'https://warezcdn.com/player'; diff --git a/src/providers/sources/warezcdn/index.ts b/src/providers/sources/warezcdn/index.ts new file mode 100644 index 0000000..f2660dc --- /dev/null +++ b/src/providers/sources/warezcdn/index.ts @@ -0,0 +1,74 @@ +import { load } from 'cheerio'; + +import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base'; +import { mixdropScraper } from '@/providers/embeds/mixdrop'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { warezcdnApiBase, warezcdnBase } from './common'; + +const embeds = { + warezcdn: mixdropScraper.id, + mixdrop: mixdropScraper.id, +}; + +const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => { + if (!ctx.media.imdbId) throw new NotFoundError('This source requires IMDB id.'); + + let id = `filme/${ctx.media.imdbId}`; + if (ctx.media.type === 'show') + id = `serie/${ctx.media.imdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + + const serversPage = await ctx.proxiedFetcher(`/${id}`, { + baseUrl: warezcdnBase, + }); + const $ = load(serversPage); + + const servers = await Promise.all( + $('.hostList.active [data-load-embed]') + .filter((_index, element) => { + const embed = $(element).attr('data-load-embed-host'); + return !!embed && !!embeds[embed as keyof typeof embeds]; + }) + .map(async (_index, element) => { + const embedHost = $(element).attr('data-load-embed-host'); + const embedId = embeds[embedHost as keyof typeof embeds]; + let embedUrl = $(element).attr('data-load-embed')!; + + if (embedHost === 'mixdrop') { + const params = new URLSearchParams({ + id: embedUrl, + sv: 'mixdrop', + }); + const realUrl = await ctx.proxiedFetcher(`/getPlay.php?${params}`, { + baseUrl: warezcdnApiBase, + headers: { + Referer: `${warezcdnApiBase}/getEmbed.php?${params}`, + }, + }); + + const realEmbedUrl = realUrl.match(/window\.location\.href="([^"]*)";/); + embedUrl = realEmbedUrl[1]; + } + + return { + embedId, + url: embedUrl, + } as SourcererEmbed; + }) + .get(), + ); + + return { + embeds: servers, + } as SourcererOutput; +}; + +export const warezcdnScraper = makeSourcerer({ + id: 'warezcdn', + name: 'WarezCDN', + rank: 81, + flags: [], + scrapeMovie: universalScraper, + scrapeShow: universalScraper, +});