diff --git a/src/providers/all.ts b/src/providers/all.ts index 1fabf33..e2b0e90 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -81,6 +81,7 @@ import { coitusScraper } from './sources/coitus'; import { cuevana3Scraper } from './sources/cuevana3'; import { debridScraper } from './sources/debrid'; import { embedsuScraper } from './sources/embedsu'; +import { fullhdfilmizleScraper } from './sources/fullhdfilmizle'; import { hdRezkaScraper } from './sources/hdrezka'; import { iosmirrorScraper } from './sources/iosmirror'; import { iosmirrorPVScraper } from './sources/iosmirrorpv'; @@ -149,6 +150,7 @@ export function gatherAllSources(): Array { movies4fScraper, debridScraper, cinehdplusScraper, + fullhdfilmizleScraper, ]; } diff --git a/src/providers/sources/fullhdfilmizle/decrypt.ts b/src/providers/sources/fullhdfilmizle/decrypt.ts new file mode 100644 index 0000000..dccb5b6 --- /dev/null +++ b/src/providers/sources/fullhdfilmizle/decrypt.ts @@ -0,0 +1,80 @@ +import { PackerParams } from './types'; + +export function rtt(str: string) { + return str.replace(/[a-z]/gi, (c) => { + return String.fromCharCode(c.charCodeAt(0) + (c.toLowerCase() < 'n' ? 13 : -13)); + }); +} + +export function decodeAtom(e: string) { + const t = atob(e.split('').reverse().join('')); + let o = ''; + for (let i = 0; i < t.length; i++) { + const r = 'K9L'[i % 3]; + const n = t.charCodeAt(i) - ((r.charCodeAt(0) % 5) + 1); + o += String.fromCharCode(n); + } + return atob(o); +} + +export function extractPackerParams(rawInput: string): PackerParams | null { + const regex = /'((?:[^'\\]|\\.)*)',\s*(\d+),\s*(\d+),\s*'((?:[^'\\]|\\.)*)'\.split\('\|'\)/; + + const match = regex.exec(rawInput); + + if (!match) { + console.error('Could not parse parameters. Format is not as expected.'); + return null; + } + + return { + payload: match[1], + radix: parseInt(match[2], 10), + count: parseInt(match[3], 10), + keywords: match[4].split('|'), + }; +} + +export function decodeDeanEdwards(params: PackerParams): string { + const { payload, radix, count, keywords } = params; + + const dict: { [key: string]: string } = Object.create(null); + + const encodeBase = (num: number): string => { + if (num < radix) { + const char = num % radix; + return char > 35 ? String.fromCharCode(char + 29) : char.toString(36); + } + + const prefix = encodeBase(Math.floor(num / radix)); + + const char = num % radix; + const suffix = char > 35 ? String.fromCharCode(char + 29) : char.toString(36); + + return prefix + suffix; + }; + + let i = count; + while (i--) { + const key = encodeBase(i); + const value = keywords[i] || key; + dict[key] = value; + } + + return payload.replace(/\b\w+\b/g, (word) => { + if (word in dict) { + return dict[word]; + } + return word; + }); +} + +export function decodeHex(str: string): string { + return str.replace(/\\x([0-9A-Fa-f]{2})/g, (_match, hexGroup) => { + return String.fromCharCode(parseInt(hexGroup, 16)); + }); +} + +export function unescapeString(str: string) { + return str.replace(/\\(.)/g, (match, char) => char); +} diff --git a/src/providers/sources/fullhdfilmizle/index.ts b/src/providers/sources/fullhdfilmizle/index.ts new file mode 100644 index 0000000..f390847 --- /dev/null +++ b/src/providers/sources/fullhdfilmizle/index.ts @@ -0,0 +1,127 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; + +import { decodeAtom, decodeDeanEdwards, decodeHex, extractPackerParams, rtt, unescapeString } from './decrypt'; + +const baseUrl = 'https://www.fullhdfilmizlesene.tv'; + +const headers = { + Referer: baseUrl, + Accept: 'application/json, text/plain, */*', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', +}; + +function extractVidmoxy(body: string) { + const regex = /eval\(function\(p,a,c,k,e,d\){.+}}return p}\((\\?'.+.split\(\\?'\|\\?'\)).+$/m; + + let decoded = body; + let i = 0; + + while (decoded.includes('eval(')) { + const decodedMatch = decoded.match(regex); + if (!decodedMatch) { + throw new NotFoundError('Decryption unsuccessful'); + } + + const parameters = extractPackerParams(i > 0 ? unescapeString(decodedMatch[1]) : decodedMatch[1]); + if (!parameters) throw new NotFoundError('Decryption unsuccessful'); + + decoded = decodeDeanEdwards(parameters); + i++; + } + + const fileMatch = decoded.match(/"file":"(.+?)"/); + if (!fileMatch) throw new NotFoundError('No playlist found'); + + const playlistUrl = unescapeString(decodeHex(fileMatch[1])); + return playlistUrl; +} + +function extractAtom(body: string) { + const fileMatch = body.match(/"file": av\('(.+)'\),$/m); + + if (!fileMatch) throw new NotFoundError('No playlist found'); + + const playlistUrl = decodeAtom(fileMatch[1]); + return playlistUrl; +} + +async function scrapeMovie(ctx: MovieScrapeContext): Promise { + if (!ctx.media.imdbId) { + throw new NotFoundError('IMDb id not provided'); + } + + const searchJson = await ctx.proxiedFetcher<{ prefix: string; dizilink: string }[]>( + `/autocomplete/q.php?q=${ctx.media.imdbId}`, + { + baseUrl, + headers, + }, + ); + + ctx.progress(30); + if (!searchJson.length) throw new NotFoundError('Media not found'); + + const searchResult = searchJson[0]; + + const mediaUrl = `/${searchResult.prefix}/${searchResult.dizilink}`; + const mediaPage = await ctx.proxiedFetcher(mediaUrl, { + baseUrl, + headers, + }); + + const playerMatch = mediaPage.match(/var scx = {.+"t":\["(.+)"\]},/); + if (!playerMatch) throw new NotFoundError('No source found'); + + ctx.progress(60); + + const playerUrl = atob(rtt(playerMatch[1])); + const isVidmoxy = playerUrl.startsWith('https://vidmoxy.com'); + + const playerResponse = await ctx.proxiedFetcher(playerUrl + (isVidmoxy ? '?vst=1' : ''), { + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + Referer: baseUrl, + 'Sec-Fetch-Dest': 'iframe', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'cross-site', + 'Sec-Fetch-User': '?1', + 'Sec-GPC': '1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }, + }); + + ctx.progress(80); + if (!playerResponse || playerResponse === '404') throw new NotFoundError('Player 404: Source is inaccessible'); + + const playlistUrl = isVidmoxy ? extractVidmoxy(playerResponse) : extractAtom(playerResponse); + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'hls', + playlist: createM3U8ProxyUrl(playlistUrl, ctx.features, headers), + headers, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; +} + +export const fullhdfilmizleScraper = makeSourcerer({ + id: 'fullhdfilmizle', + name: 'FullHDFilmizle (Turkish)', + rank: 3, + disabled: false, + flags: [flags.CORS_ALLOWED], + scrapeMovie, +}); diff --git a/src/providers/sources/fullhdfilmizle/types.ts b/src/providers/sources/fullhdfilmizle/types.ts new file mode 100644 index 0000000..affe3d8 --- /dev/null +++ b/src/providers/sources/fullhdfilmizle/types.ts @@ -0,0 +1,6 @@ +export interface PackerParams { + payload: string; // p + radix: number; // a + count: number; // c + keywords: string[]; // k +}