pstreams-providers/src/providers/embeds/closeload.ts

201 lines
6.4 KiB
TypeScript

import { load } from 'cheerio';
import { unpack } from 'unpacker';
import { flags } from '@/entrypoint/utils/targets';
import { NotFoundError } from '@/utils/errors';
import { makeEmbed } from '../base';
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '../captions';
// Custom base64 decoder for problematic strings
function customAtob(input: string): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const str = input.replace(/=+$/, '');
let output = '';
if (str.length % 4 === 1) {
throw new Error('The string to be decoded is not correctly encoded.');
}
for (let bc = 0, bs = 0, i = 0; i < str.length; i++) {
const buffer = str.charAt(i);
const charIndex = chars.indexOf(buffer);
if (charIndex === -1) continue;
bs = bc % 4 ? bs * 64 + charIndex : charIndex;
if (bc++ % 4) {
output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)));
}
}
return output;
}
// Implement the closeload decoding function
function decodeCloseload(valueParts: string[]): string {
const value = valueParts.join('');
let result = value;
// Step 1: base64 decode
result = atob(result);
// Step 2: ROT13-like transformation
result = result.replace(/[a-zA-Z]/g, function rot13Transform(c) {
const charCode = c.charCodeAt(0);
const newCharCode = charCode + 13;
const maxCode = c <= 'Z' ? 90 : 122;
return String.fromCharCode(newCharCode <= maxCode ? newCharCode : newCharCode - 26);
});
// Step 3: reverse the string
result = result.split('').reverse().join('');
// Step 4: custom unmixing
let unmix = '';
for (let i = 0; i < result.length; i++) {
let charCode = result.charCodeAt(i);
charCode = (charCode - (399756995 % (i + 5)) + 256) % 256;
unmix += String.fromCharCode(charCode);
}
return unmix;
}
const referer = 'https://ridomovies.tv/';
export const closeLoadScraper = makeEmbed({
id: 'closeload',
name: 'CloseLoad',
rank: 106,
flags: [flags.IP_LOCKED],
disabled: true,
async scrape(ctx) {
const baseUrl = new URL(ctx.url).origin;
const iframeRes = await ctx.proxiedFetcher<string>(ctx.url, {
headers: { referer },
});
const iframeRes$ = load(iframeRes);
const captions: Caption[] = iframeRes$('track')
.map((_, el) => {
const track = iframeRes$(el);
const url = `${baseUrl}${track.attr('src')}`;
const label = track.attr('label') ?? '';
const language = labelToLanguageCode(label);
const captionType = getCaptionTypeFromUrl(url);
if (!language || !captionType) return null;
return {
id: url,
language,
hasCorsRestrictions: true,
type: captionType,
url,
};
})
.get()
.filter((x) => x !== null);
const evalCode = iframeRes$('script')
.filter((_, el) => {
const script = iframeRes$(el);
return (script.attr('type') === 'text/javascript' && script.html()?.includes('p,a,c,k,e,d')) ?? false;
})
.html();
if (!evalCode) throw new Error("Couldn't find eval code");
const decoded = unpack(evalCode);
let base64EncodedUrl: string | undefined;
// Look for the dc_* function call pattern (function names are dynamic)
const functionCallMatch = decoded.match(/dc_\w+\(\[([^\]]+)\]\)/);
if (functionCallMatch) {
// Extract the array of strings passed to the function
const arrayContent = functionCallMatch[1];
// Parse the array of strings
const stringMatches = arrayContent.match(/"([^"]+)"/g);
if (stringMatches) {
// Extract the strings from the array
const valueParts = stringMatches.map((s) => s.slice(1, -1));
// Use the closeload decoding function
try {
const decodedUrl = decodeCloseload(valueParts);
// Check if the decoded result looks like a URL
if (decodedUrl.startsWith('http://') || decodedUrl.startsWith('https://')) {
base64EncodedUrl = decodedUrl; // This will be the final URL, not base64
}
} catch (error) {
// Continue to fallback patterns if decoding fails
}
}
}
// Fallback to original patterns if function call not found
if (!base64EncodedUrl) {
const patterns = [/var\s+(\w+)\s*=\s*"([^"]+)";/g, /(\w+)\s*=\s*"([^"]+)"/g, /"([A-Za-z0-9+/=]+)"/g];
for (const pattern of patterns) {
const match = pattern.exec(decoded);
if (match) {
const potentialUrl = match[2] || match[1];
// Check if it looks like base64
if (/^[A-Za-z0-9+/]*={0,2}$/.test(potentialUrl) && potentialUrl.length > 10) {
base64EncodedUrl = potentialUrl;
break;
}
}
}
}
if (!base64EncodedUrl) throw new NotFoundError('Unable to find source url');
// If base64EncodedUrl is already a URL (from closeload decoding), use it directly
let url: string;
if (base64EncodedUrl.startsWith('http://') || base64EncodedUrl.startsWith('https://')) {
url = base64EncodedUrl;
} else {
// Fallback to original base64 decoding logic
// Validate base64 string before decoding
const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(base64EncodedUrl);
if (!isValidBase64) {
throw new NotFoundError('Invalid base64 encoding found in source url');
}
let decodedString: string;
try {
decodedString = atob(base64EncodedUrl);
} catch (error) {
// Try custom decoder as fallback
try {
decodedString = customAtob(base64EncodedUrl);
} catch (customError) {
throw new NotFoundError(`Failed to decode base64 source url: ${base64EncodedUrl.substring(0, 50)}...`);
}
}
// Try to find a URL in the decoded string
const urlMatch = decodedString.match(/(https?:\/\/[^\s"']+)/);
if (urlMatch) {
url = urlMatch[1];
} else if (decodedString.startsWith('http://') || decodedString.startsWith('https://')) {
url = decodedString;
} else {
throw new NotFoundError(`Decoded string is not a valid URL: ${decodedString.substring(0, 100)}...`);
}
}
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: url,
captions,
flags: [flags.IP_LOCKED],
headers: {
Referer: 'https://closeload.top/',
Origin: 'https://closeload.top',
},
},
],
};
},
});