diff --git a/src/providers/all.ts b/src/providers/all.ts index 2f01869..e32790e 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -31,7 +31,8 @@ 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 { warezcdnembedHlsScraper } from './embeds/warezcdn/hls'; +import { warezcdnembedMp4Scraper } from './embeds/warezcdn/mp4'; import { wootlyScraper } from './embeds/wootly'; import { goojaraScraper } from './sources/goojara'; import { hdRezkaScraper } from './sources/hdrezka'; @@ -91,6 +92,7 @@ export function gatherAllEmbeds(): Array { droploadScraper, filelionsScraper, vTubeScraper, - warezcdnembedScraper, + warezcdnembedHlsScraper, + warezcdnembedMp4Scraper, ]; } diff --git a/src/providers/embeds/warezcdn.ts b/src/providers/embeds/warezcdn.ts deleted file mode 100644 index 8223bfa..0000000 --- a/src/providers/embeds/warezcdn.ts +++ /dev/null @@ -1,128 +0,0 @@ -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/embeds/warezcdn/common.ts b/src/providers/embeds/warezcdn/common.ts new file mode 100644 index 0000000..3a18bb6 --- /dev/null +++ b/src/providers/embeds/warezcdn/common.ts @@ -0,0 +1,55 @@ +import { warezcdnPlayerBase } from '@/providers/sources/warezcdn/common'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +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}`; +} + +export async function getDecryptedId(ctx: EmbedScrapeContext) { + 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 decryptedId; +} diff --git a/src/providers/embeds/warezcdn/hls.ts b/src/providers/embeds/warezcdn/hls.ts new file mode 100644 index 0000000..957dd07 --- /dev/null +++ b/src/providers/embeds/warezcdn/hls.ts @@ -0,0 +1,44 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { getDecryptedId } from './common'; + +// Method found by atpn +async function getVideowlUrlStream(ctx: EmbedScrapeContext, decryptedId: string) { + const sharePage = await ctx.proxiedFetcher('https://cloud.mail.ru/public/uaRH/2PYWcJRpH'); + const regex = /"videowl_view":\{"count":"(\d+)","url":"([^"]+)"\}/g; + const videowlUrl = regex.exec(sharePage)?.[2]; + + if (!videowlUrl) throw new NotFoundError('Failed to get videoOwlUrl'); + + return `${videowlUrl}/${btoa(decryptedId)}.m3u8?${new URLSearchParams({ + double_encode: '1', + })}`; +} + +export const warezcdnembedHlsScraper = makeEmbed({ + id: 'warezcdnembedhls', // WarezCDN is both a source and an embed host + name: 'WarezCDN HLS', + rank: 82, + async scrape(ctx) { + const decryptedId = await getDecryptedId(ctx); + + if (!decryptedId) throw new NotFoundError("can't get file id"); + + const streamUrl = await getVideowlUrlStream(ctx, decryptedId); + + return { + stream: [ + { + id: 'primary', + type: 'hls', + flags: [flags.IP_LOCKED], + captions: [], + playlist: streamUrl, + }, + ], + }; + }, +}); diff --git a/src/providers/embeds/warezcdn/mp4.ts b/src/providers/embeds/warezcdn/mp4.ts new file mode 100644 index 0000000..53b85dd --- /dev/null +++ b/src/providers/embeds/warezcdn/mp4.ts @@ -0,0 +1,58 @@ +import { makeEmbed } from '@/providers/base'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { getDecryptedId } from './common'; + +const cdnListing = [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]; + +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; +} + +export const warezcdnembedMp4Scraper = makeEmbed({ + id: 'warezcdnembedmp4', // WarezCDN is both a source and an embed host + name: 'WarezCDN MP4', + rank: 83, + disabled: true, // crashes movie-web player, disabling for now + async scrape(ctx) { + const decryptedId = await getDecryptedId(ctx); + + if (!decryptedId) throw new NotFoundError("can't get file id"); + + const streamUrl = await checkUrls(ctx, decryptedId); + + if (!streamUrl) throw new NotFoundError("can't get stream id"); + + return { + stream: [ + { + id: 'primary', + captions: [], + qualities: { + unknown: { + type: 'mp4', + url: streamUrl, + }, + }, + type: 'file', + flags: [], + headers: { + Origin: 'https://cloud.mail.ru', + Referer: 'https://cloud.mail.ru/', + }, + }, + ], + }; + }, +}); diff --git a/src/providers/sources/warezcdn/index.ts b/src/providers/sources/warezcdn/index.ts index f2660dc..6925855 100644 --- a/src/providers/sources/warezcdn/index.ts +++ b/src/providers/sources/warezcdn/index.ts @@ -2,16 +2,13 @@ import { load } from 'cheerio'; import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base'; import { mixdropScraper } from '@/providers/embeds/mixdrop'; +import { warezcdnembedHlsScraper } from '@/providers/embeds/warezcdn/hls'; +import { warezcdnembedMp4Scraper } from '@/providers/embeds/warezcdn/mp4'; 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.'); @@ -24,43 +21,47 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => }); 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')!; + const embedsHost = $('.hostList.active [data-load-embed]').get(); - 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 embeds: SourcererEmbed[] = []; - const realEmbedUrl = realUrl.match(/window\.location\.href="([^"]*)";/); - embedUrl = realEmbedUrl[1]; - } + embedsHost.forEach(async (element) => { + const embedHost = $(element).attr('data-load-embed-host')!; + const embedUrl = $(element).attr('data-load-embed')!; - return { - embedId, + 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="([^"]*)";/); + embeds.push({ + embedId: mixdropScraper.id, + url: realEmbedUrl[1], + }); + } else if (embedHost === 'warezcdn') { + embeds.push( + { + embedId: warezcdnembedHlsScraper.id, url: embedUrl, - } as SourcererEmbed; - }) - .get(), - ); + }, + { + embedId: warezcdnembedMp4Scraper.id, + url: embedUrl, + }, + ); + } + }); return { - embeds: servers, + embeds, } as SourcererOutput; };