add
Fetch and Save Remote Content / fetch (push) Successful in 17s
Sync Versions to index.json / sync-versions (push) Successful in 28s

This commit is contained in:
aka paul
2026-06-03 20:40:55 +02:00
parent df7a8b3c92
commit 4838578ca6
3 changed files with 516 additions and 27 deletions
+435
View File
@@ -0,0 +1,435 @@
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(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&#39;/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;
}
+27
View File
@@ -0,0 +1,27 @@
{
"sourceName": "HDRezka",
"iconUrl": "https://rezka.ag/apple-touch-icon-144.png",
"author": {
"name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
},
"version": "1.0.0",
"language": "Russian",
"streamType": "MP4 & HLS",
"quality": "4K",
"baseUrl": "https://rezka.ag",
"searchBaseUrl": "https://rezka.ag",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/hdrezka/hdrezka.js",
"type": "movies/shows/anime",
"asyncJS": true,
"softsub": true,
"downloadSupport": true,
"supportsMojuru": true,
"supportsDartotsu": true,
"supportsSora": true,
"supportsLuna": true,
"supportsAnymex": true,
"supportsTsumi": true,
"supportsHiyoku": true,
"supportsShirox": true
}
+54 -27
View File
@@ -1,31 +1,58 @@
{
"animeverse/animeverse.json": {
"sourceName": "AnimeVerse",
"iconUrl": "https://animeverse.to/apple-touch-icon.png",
"author": {
"name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
},
"version": "1.0.0",
"language": "English",
"streamType": "HLS",
"quality": "1080p",
"baseUrl": "https://animeverse.to/",
"searchBaseUrl": "https://animeverse.to/",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animeverse/animeverse.js",
"type": "anime",
"asyncJS": true,
"softsub": true,
"downloadSupport": true,
"supportsSora": true,
"supportsLuna": true,
"supportsMojuru": true,
"supportsDartotsu": true,
"supportsAnymex": true,
"supportsTsumi": true,
"supportsHiyoku": true,
"supportsShirox": true
"hdrezka/hdrezka.json": {
"sourceName": "HDRezka",
"iconUrl": "https://rezka.ag/apple-touch-icon-144.png",
"author": {
"name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
},
"version": "1.0.0",
"language": "Russian",
"streamType": "MP4 & HLS",
"quality": "4K",
"baseUrl": "https://rezka.ag",
"searchBaseUrl": "https://rezka.ag",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/hdrezka/hdrezka.js",
"type": "movies/shows/anime",
"asyncJS": true,
"softsub": true,
"downloadSupport": true,
"supportsMojuru": true,
"supportsDartotsu": true,
"supportsSora": true,
"supportsLuna": true,
"supportsAnymex": true,
"supportsTsumi": true,
"supportsHiyoku": true,
"supportsShirox": true
},
"animeverse/animeverse.json": {
"sourceName": "AnimeVerse",
"iconUrl": "https://animeverse.to/apple-touch-icon.png",
"author": {
"name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
},
"version": "1.0.0",
"language": "English",
"streamType": "HLS",
"quality": "1080p",
"baseUrl": "https://animeverse.to/",
"searchBaseUrl": "https://animeverse.to/",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animeverse/animeverse.js",
"type": "anime",
"asyncJS": true,
"softsub": true,
"downloadSupport": true,
"supportsSora": true,
"supportsLuna": true,
"supportsMojuru": true,
"supportsDartotsu": true,
"supportsAnymex": true,
"supportsTsumi": true,
"supportsHiyoku": true,
"supportsShirox": true
},
"lordflix/lordflix.json": {
"sourceName": "LordFlix",
"iconUrl": "https://lordflix.org/mstile-150x150.png",
@@ -1903,4 +1930,4 @@
"supportsLuna": true,
"supportsAnymex": true
}
}
}