436 lines
15 KiB
JavaScript
436 lines
15 KiB
JavaScript
const defaultHeaders = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
|
||
'Cookie': 'hdmbbs=1'
|
||
};
|
||
|
||
async function searchResults(keyword) {
|
||
const results = [];
|
||
try {
|
||
const response = await fetchv2(`https://rezka.ag/search/?do=search&subaction=search&q=${encodeURIComponent(keyword)}`, defaultHeaders);
|
||
const html = await response.text();
|
||
|
||
const parts = html.split('class="b-content__inline_item"');
|
||
for (let i = 1; i < parts.length; i++) {
|
||
const part = parts[i];
|
||
const imgMatch = part.match(/<img[^>]+src="([^"]+)"/);
|
||
const linkMatch = part.match(/<div class="b-content__inline_item-link">[\s\S]*?<a href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
|
||
|
||
if (linkMatch && imgMatch) {
|
||
results.push({
|
||
title: decodeHtmlEntities(linkMatch[2].trim()),
|
||
image: imgMatch[1].trim(),
|
||
href: linkMatch[1].trim()
|
||
});
|
||
}
|
||
}
|
||
|
||
return JSON.stringify(results);
|
||
} catch (err) {
|
||
return JSON.stringify([{
|
||
title: "Error",
|
||
image: "Error",
|
||
href: "Error"
|
||
}]);
|
||
}
|
||
}
|
||
|
||
async function extractDetails(url) {
|
||
try {
|
||
const response = await fetchv2(url, defaultHeaders);
|
||
const html = await response.text();
|
||
|
||
const descMatch = html.match(/class="b-post__description_text"[^>]*>([\s\S]*?)<\/div>/);
|
||
const description = descMatch ? decodeHtmlEntities(descMatch[1].trim()) : "N/A";
|
||
|
||
const origMatch = html.match(/<div class="b-post__origtitle"[^>]*>([\s\S]*?)<\/div>/);
|
||
const aliases = origMatch ? decodeHtmlEntities(origMatch[1].trim()) : "N/A";
|
||
|
||
const yearMatch = html.match(/href="[^"]*?\/year\/[^"]*?">([^<]+)<\/a>/);
|
||
const airdate = yearMatch ? yearMatch[1].trim() : "N/A";
|
||
|
||
return JSON.stringify([{
|
||
description: description,
|
||
aliases: aliases,
|
||
airdate: airdate
|
||
}]);
|
||
} catch (err) {
|
||
return JSON.stringify([{
|
||
description: "Error",
|
||
aliases: "Error",
|
||
airdate: "Error"
|
||
}]);
|
||
}
|
||
}
|
||
|
||
async function extractEpisodes(url) {
|
||
try {
|
||
const response = await fetchv2(url, defaultHeaders);
|
||
const html = await response.text();
|
||
|
||
const typeMatch = html.match(/<meta property="og:type" content="([^"]+)"/);
|
||
const isTV = typeMatch && typeMatch[1] === 'video.tv_series';
|
||
|
||
if (!isTV) {
|
||
return JSON.stringify([{
|
||
href: url,
|
||
number: 1,
|
||
season: 1
|
||
}]);
|
||
}
|
||
|
||
const postId = getPostId(html, url);
|
||
if (!postId) {
|
||
throw new Error("Post ID not found");
|
||
}
|
||
|
||
const translators = parseTranslators(html);
|
||
if (translators.length === 0) {
|
||
throw new Error("No translators found");
|
||
}
|
||
|
||
const trId = translators[0].id;
|
||
const origin = getOrigin(url);
|
||
const postData = `id=${postId}&translator_id=${trId}&action=get_episodes`;
|
||
const headers = {
|
||
...defaultHeaders,
|
||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||
"X-Requested-With": "XMLHttpRequest"
|
||
};
|
||
|
||
const apiResponse = await fetchv2(`${origin}/ajax/get_cdn_series/`, headers, "POST", postData);
|
||
const data = await apiResponse.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error("Failed to fetch episodes from API");
|
||
}
|
||
|
||
const episodesHtml = data.episodes || "";
|
||
const results = [];
|
||
const episodeRegex = /class="[^"]*b-simple_episode__item[^"]*"[^>]*data-season_id="(\d+)"[^>]*data-episode_id="(\d+)"[^>]*>([\s\S]*?)<\/li>/g;
|
||
let match;
|
||
while ((match = episodeRegex.exec(episodesHtml)) !== null) {
|
||
const seasonId = match[1];
|
||
const episodeId = match[2];
|
||
|
||
results.push({
|
||
href: appendQueryParams(url, { post_id: postId, season: seasonId, episode: episodeId }),
|
||
number: parseInt(episodeId, 10),
|
||
season: parseInt(seasonId, 10)
|
||
});
|
||
}
|
||
|
||
return JSON.stringify(results);
|
||
} catch (err) {
|
||
return JSON.stringify([{
|
||
href: "Error",
|
||
number: "Error",
|
||
season: "Error"
|
||
}]);
|
||
}
|
||
}
|
||
|
||
async function extractStreamUrl(url) {
|
||
try {
|
||
let postId = getQueryParam(url, "post_id");
|
||
const season = getQueryParam(url, "season");
|
||
const episode = getQueryParam(url, "episode");
|
||
const isTV = season && episode;
|
||
|
||
const basePageUrl = url.split('?')[0];
|
||
const response = await fetchv2(basePageUrl, defaultHeaders);
|
||
const html = await response.text();
|
||
|
||
if (!postId) {
|
||
postId = getPostId(html, basePageUrl);
|
||
}
|
||
if (!postId) {
|
||
throw new Error("Post ID not found");
|
||
}
|
||
|
||
const translators = parseTranslators(html);
|
||
if (translators.length === 0) {
|
||
throw new Error("No translators found");
|
||
}
|
||
|
||
const origin = getOrigin(basePageUrl);
|
||
const postHeaders = {
|
||
"User-Agent": defaultHeaders["User-Agent"],
|
||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||
"X-Requested-With": "XMLHttpRequest",
|
||
"Referer": basePageUrl
|
||
};
|
||
|
||
const streamPromises = translators.map(async (tr) => {
|
||
try {
|
||
const postData = isTV
|
||
? `id=${postId}&translator_id=${tr.id}&season=${season}&episode=${episode}&action=get_stream`
|
||
: `id=${postId}&translator_id=${tr.id}&action=get_movie`;
|
||
|
||
const apiResponse = await fetchv2(`${origin}/ajax/get_cdn_series/`, postHeaders, "POST", postData);
|
||
const data = await apiResponse.json();
|
||
|
||
let translatorSubtitle = "";
|
||
if (data.success && data.subtitle) {
|
||
const subParts = data.subtitle.split(',');
|
||
const subs = [];
|
||
for (const p of subParts) {
|
||
const match = p.match(/\[([^\]]+)\]\s*([^,\s]+)/);
|
||
if (match) {
|
||
subs.push({ lang: match[1], url: match[2] });
|
||
}
|
||
}
|
||
const enSub = subs.find(s => s.lang.toLowerCase().includes('eng'));
|
||
if (enSub) {
|
||
translatorSubtitle = enSub.url;
|
||
} else if (subs.length > 0) {
|
||
translatorSubtitle = subs[0].url;
|
||
}
|
||
}
|
||
|
||
if (data.success && data.url) {
|
||
const rawUrl = data.url;
|
||
const decoded = rawUrl.startsWith('[') ? rawUrl : clearTrash(rawUrl);
|
||
const parts = decoded.split(',');
|
||
const translatorStreams = [];
|
||
for (const part of parts) {
|
||
const match = part.match(/\[([^\]]+)\]\s*([^,\s]+(?:\s+or\s+[^,\s]+)*)/);
|
||
if (match) {
|
||
const quality = match[1];
|
||
if (quality.includes('<')) continue;
|
||
const links = match[2].split(/\s+or\s+/);
|
||
for (const link of links) {
|
||
if (link) {
|
||
translatorStreams.push({
|
||
title: `${tr.name} (${quality})`,
|
||
streamUrl: link,
|
||
headers: {
|
||
"User-Agent": defaultHeaders["User-Agent"],
|
||
"Referer": basePageUrl
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return { streams: translatorStreams, subtitle: translatorSubtitle };
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
return { streams: [], subtitle: "" };
|
||
});
|
||
|
||
const results = await Promise.all(streamPromises);
|
||
const allStreams = [];
|
||
let finalSubtitle = "";
|
||
|
||
for (const res of results) {
|
||
if (res.streams) {
|
||
allStreams.push(...res.streams);
|
||
}
|
||
if (!finalSubtitle && res.subtitle) {
|
||
finalSubtitle = res.subtitle;
|
||
}
|
||
}
|
||
|
||
allStreams.sort((a, b) => {
|
||
const getRes = (title) => {
|
||
const match = title.match(/(\d+)p/);
|
||
return match ? parseInt(match[1], 10) : 0;
|
||
};
|
||
return getRes(b.title) - getRes(a.title);
|
||
});
|
||
|
||
return JSON.stringify({
|
||
streams: allStreams,
|
||
subtitle: finalSubtitle
|
||
});
|
||
} catch (err) {
|
||
return JSON.stringify({
|
||
streams: [],
|
||
subtitle: ""
|
||
});
|
||
}
|
||
}
|
||
|
||
function decodeHtmlEntities(str) {
|
||
return str
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
function getOrigin(url) {
|
||
try {
|
||
const parsed = new URL(url);
|
||
return `${parsed.protocol}//${parsed.host}`;
|
||
} catch (e) {
|
||
const match = url.match(/^(https?:\/\/[^\/]+)/);
|
||
return match ? match[1] : "https://rezka.ag";
|
||
}
|
||
}
|
||
|
||
function getQueryParam(url, name) {
|
||
try {
|
||
const parsed = new URL(url);
|
||
return parsed.searchParams.get(name);
|
||
} catch (e) {
|
||
const regex = new RegExp('[?&]' + name + '=([^&#]*)');
|
||
const match = regex.exec(url);
|
||
return match ? decodeURIComponent(match[1]) : null;
|
||
}
|
||
}
|
||
|
||
function appendQueryParams(url, params) {
|
||
const separator = url.includes('?') ? '&' : '?';
|
||
const queryString = Object.entries(params)
|
||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||
.join('&');
|
||
return `${url}${separator}${queryString}`;
|
||
}
|
||
|
||
function getCombinations(arr, length) {
|
||
if (length === 1) return arr.map(x => [x]);
|
||
const results = [];
|
||
const subCombos = getCombinations(arr, length - 1);
|
||
for (const val of arr) {
|
||
for (const sub of subCombos) {
|
||
results.push([val, ...sub]);
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
|
||
function btoa(input) {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||
let str = '';
|
||
for (let i = 0; i < input.length; i += 3) {
|
||
const char1 = input.charCodeAt(i);
|
||
const char2 = i + 1 < input.length ? input.charCodeAt(i + 1) : NaN;
|
||
const char3 = i + 2 < input.length ? input.charCodeAt(i + 2) : NaN;
|
||
|
||
const byte1 = char1 >> 2;
|
||
const byte2 = ((char1 & 3) << 4) | (isNaN(char2) ? 0 : char2 >> 4);
|
||
const byte3 = isNaN(char2) ? 64 : ((char2 & 15) << 2) | (isNaN(char3) ? 0 : char3 >> 6);
|
||
const byte4 = isNaN(char3) ? 64 : char3 & 63;
|
||
|
||
str += chars.charAt(byte1) + chars.charAt(byte2) + chars.charAt(byte3) + chars.charAt(byte4);
|
||
}
|
||
return str;
|
||
}
|
||
|
||
function atob(input) {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||
let str = '';
|
||
let buffer = 0;
|
||
let bits = 0;
|
||
for (let i = 0; i < input.length; i++) {
|
||
const char = input.charAt(i);
|
||
if (char === '=') break;
|
||
const index = chars.indexOf(char);
|
||
if (index === -1) continue;
|
||
buffer = (buffer << 6) | index;
|
||
bits += 6;
|
||
if (bits >= 8) {
|
||
bits -= 8;
|
||
str += String.fromCharCode((buffer >> bits) & 0xFF);
|
||
buffer &= (1 << bits) - 1;
|
||
}
|
||
}
|
||
return str;
|
||
}
|
||
|
||
function decodeUTF8(str) {
|
||
try {
|
||
return decodeURIComponent(escape(str));
|
||
} catch (e) {
|
||
return str;
|
||
}
|
||
}
|
||
|
||
function clearTrash(data) {
|
||
const trashList = ["@", "#", "!", "^", "$"];
|
||
const trashCodesSet = [];
|
||
|
||
for (let i = 2; i <= 3; i++) {
|
||
const combos = getCombinations(trashList, i);
|
||
for (const combo of combos) {
|
||
const comboStr = combo.join('');
|
||
const base64 = btoa(comboStr);
|
||
trashCodesSet.push(base64);
|
||
}
|
||
}
|
||
|
||
let trashString = data.replace("#h", "").split("//_//").join("");
|
||
|
||
for (const temp of trashCodesSet) {
|
||
trashString = trashString.split(temp).join("");
|
||
}
|
||
|
||
try {
|
||
const finalString = atob(trashString);
|
||
return decodeUTF8(finalString);
|
||
} catch (e) {
|
||
return trashString;
|
||
}
|
||
}
|
||
|
||
function getPostId(html, url) {
|
||
const match1 = html.match(/id="post_id"\s+value="(\d+)"/) || html.match(/value="(\d+)"\s+id="post_id"/);
|
||
if (match1) return match1[1];
|
||
|
||
const match2 = html.match(/id="send-video-issue"\s+data-id="(\d+)"/) || html.match(/data-id="(\d+)"\s+id="send-video-issue"/);
|
||
if (match2) return match2[1];
|
||
|
||
const match3 = html.match(/id="user-favorites-holder"\s+data-post_id="(\d+)"/) || html.match(/data-post_id="(\d+)"\s+id="user-favorites-holder"/);
|
||
if (match3) return match3[1];
|
||
|
||
const urlParts = url.split('/');
|
||
const lastPart = urlParts[urlParts.length - 1];
|
||
const match4 = lastPart.match(/^(\d+)/);
|
||
if (match4) return match4[1];
|
||
|
||
return null;
|
||
}
|
||
|
||
function parseTranslators(html) {
|
||
const translators = [];
|
||
|
||
const listMatch = html.match(/<ul[^>]+id="translators-list"[^>]*>([\s\S]*?)<\/ul>/);
|
||
if (listMatch) {
|
||
const liRegex = /<li[^>]+data-translator_id="(\d+)"[^>]*>([\s\S]*?)<\/li>/g;
|
||
let match;
|
||
while ((match = liRegex.exec(listMatch[1])) !== null) {
|
||
const id = match[1];
|
||
let name = match[2].replace(/<[^>]*>/g, '').trim();
|
||
const imgMatch = match[2].match(/<img[^>]+title="([^"]+)"/);
|
||
if (imgMatch) {
|
||
const lang = imgMatch[1];
|
||
if (!name.includes(lang)) {
|
||
name += ` (${lang})`;
|
||
}
|
||
}
|
||
translators.push({ id: parseInt(id, 10), name });
|
||
}
|
||
}
|
||
|
||
if (translators.length === 0) {
|
||
const scriptMatch = html.match(/sof\.tv\.initCDN(?:Series|Movies)Events\(\s*\d+,\s*(\d+)/);
|
||
if (scriptMatch) {
|
||
const id = scriptMatch[1];
|
||
let name = "Default";
|
||
const tableMatch = html.match(/<li>\s*<b>В переводе:<\/b>\s*([^<]+)<\/li>/) ||
|
||
html.match(/<tr>\s*<td>В переводе:<\/td>\s*<td>([^<]+)<\/td>/);
|
||
if (tableMatch) {
|
||
name = tableMatch[1].trim();
|
||
}
|
||
translators.push({ id: parseInt(id, 10), name });
|
||
}
|
||
}
|
||
|
||
return translators;
|
||
}
|