diff --git a/src/providers/all.ts b/src/providers/all.ts index e5f05a2..f198557 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -68,6 +68,7 @@ import { VidsrcsuServer9Scraper, } from './embeds/vidsrcsu'; import { viperScraper } from './embeds/viper'; +import { voeScraper } from './embeds/voe'; import { warezcdnembedHlsScraper } from './embeds/warezcdn/hls'; import { warezcdnembedMp4Scraper } from './embeds/warezcdn/mp4'; import { warezPlayerScraper } from './embeds/warezcdn/warezplayer'; @@ -217,5 +218,6 @@ export function gatherAllEmbeds(): Array { filelionsScraper, droploadScraper, supervideoScraper, + voeScraper, ]; } diff --git a/src/providers/embeds/voe.ts b/src/providers/embeds/voe.ts new file mode 100644 index 0000000..36fc68d --- /dev/null +++ b/src/providers/embeds/voe.ts @@ -0,0 +1,105 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { NotFoundError } from '@/utils/errors'; + +const userAgent = + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36'; + +function cleanSymbols(s: string): string { + let result = s; + for (const p of ['@$', '^^', '~@', '%?', '*~', '!!', '#&']) { + result = result.replaceAll(p, '_'); + } + return result; +} + +function cleanUnderscores(s: string): string { + return s.replace(/_/g, ''); +} + +function shiftBack(s: string, n: number): string { + return Array.from(s) + .map((c) => String.fromCharCode(c.charCodeAt(0) - n)) + .join(''); +} + +function rot13(str: string): string { + return str.replace(/[a-zA-Z]/g, (c) => { + const base = c <= 'Z' ? 65 : 97; + return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base); + }); +} + +export const voeScraper = makeEmbed({ + id: 'voe', + name: 'Voe', + rank: 180, + flags: [flags.IP_LOCKED], + async scrape(ctx) { + const url = ctx.url; + const defaultDomain = (() => { + try { + const u = new URL(url); + return `${u.protocol}//${u.host}/`; + } catch { + return undefined; + } + })(); + + const headers: Record = { + 'User-Agent': userAgent, + }; + if (defaultDomain) { + headers.Referer = defaultDomain; + } + + let html = await ctx.proxiedFetcher(url, { headers }); + + // Handle redirect page + if (html.includes('Redirecting...')) { + const match = html.match(/href\s*=\s*'(.*?)';/); + if (!match) throw new NotFoundError('Redirect target not found'); + const redirectUrl = match[1]; + html = await ctx.proxiedFetcher(redirectUrl, { headers }); + } + + const jsonScriptMatch = html.match(/]+type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/i); + if (!jsonScriptMatch) throw new NotFoundError('Obfuscated script not found'); + + const obfuscatedScript = jsonScriptMatch[1]; + const encodedMatch = obfuscatedScript.match(/\["(.*?)"\]/); + if (!encodedMatch) throw new NotFoundError('Encoded data not found'); + + const encodedData = encodedMatch[1]; + + // Decoding steps + let decoded = rot13(encodedData); + decoded = cleanSymbols(decoded); + decoded = cleanUnderscores(decoded); + decoded = Buffer.from(decoded, 'base64').toString('utf-8'); + decoded = shiftBack(decoded, 3); + decoded = decoded.split('').reverse().join(''); + decoded = Buffer.from(decoded, 'base64').toString('utf-8'); + + const json = JSON.parse(decoded); + const videoUrl = json?.source; + if (!videoUrl) throw new NotFoundError('No video URL found'); + + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: videoUrl, + flags: [flags.IP_LOCKED], + captions: [], + headers: { + Referer: defaultDomain || url, + Origin: defaultDomain?.replace(/\/$/, '') || new URL(url).origin, + 'User-Agent': userAgent, + }, + }, + ], + }; + }, +});