Add anime/anicrush/source.js
This commit is contained in:
368
anime/anicrush/source.js
Normal file
368
anime/anicrush/source.js
Normal file
@@ -0,0 +1,368 @@
|
||||
class AniCrush {
|
||||
constructor() {
|
||||
this.type = "anime-board";
|
||||
this.version = "1.0.0";
|
||||
this.baseUrl = "https://anicrush.to";
|
||||
this.apiBase = "https://api.anicrush.to";
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
return { episodeServers: ["Southcloud-1", "Southcloud-2", "Southcloud-3"], supportsDub: true };
|
||||
}
|
||||
|
||||
_nativeFetch(url, method, headers, body) {
|
||||
const raw = Native.fetch(String(url), method || "GET", JSON.stringify(headers || {}), body == null ? "" : String(body));
|
||||
try { return JSON.parse(raw || "{}"); } catch (e) { return { ok: false, status: 0, headers: {}, body: "" }; }
|
||||
}
|
||||
|
||||
_getText(url, headers) {
|
||||
const res = this._nativeFetch(url, "GET", headers, "");
|
||||
return String(res.body || "");
|
||||
}
|
||||
|
||||
_getJson(url, headers) {
|
||||
const res = this._nativeFetch(url, "GET", headers, "");
|
||||
try { return JSON.parse(String(res.body || "{}")); } catch (e) { return {}; }
|
||||
}
|
||||
|
||||
_postJson(url, headers, obj) {
|
||||
const res = this._nativeFetch(url, "POST", headers, JSON.stringify(obj || {}));
|
||||
try { return JSON.parse(String(res.body || "{}")); } catch (e) { return {}; }
|
||||
}
|
||||
|
||||
_safeStr(v) {
|
||||
return typeof v === "string" ? v : (v == null ? "" : String(v));
|
||||
}
|
||||
|
||||
_normalizeTitle(t) {
|
||||
return this._safeStr(t)
|
||||
.toLowerCase()
|
||||
.replace(/(season|cour|part|uncensored)/g, "")
|
||||
.replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, ""))
|
||||
.replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
|
||||
_normalizeDate(dateStr) {
|
||||
const s = this._safeStr(dateStr);
|
||||
if (!s) return null;
|
||||
const months = {
|
||||
Jan: "01", Feb: "02", Mar: "03", Apr: "04",
|
||||
May: "05", Jun: "06", Jul: "07", Aug: "08",
|
||||
Sep: "09", Oct: "10", Nov: "11", Dec: "12"
|
||||
};
|
||||
const m = s.match(/([A-Za-z]+)\s+\d{1,2},\s*(\d{4})/);
|
||||
if (!m) return null;
|
||||
const mm = months[m[1]];
|
||||
if (!mm) return null;
|
||||
return { year: parseInt(m[2], 10), month: parseInt(mm, 10) };
|
||||
}
|
||||
|
||||
_levSim(a, b) {
|
||||
a = this._safeStr(a);
|
||||
b = this._safeStr(b);
|
||||
const lenA = a.length;
|
||||
const lenB = b.length;
|
||||
if (!lenA && !lenB) return 1;
|
||||
if (!lenA || !lenB) return 0;
|
||||
|
||||
const dp = [];
|
||||
for (let i = 0; i <= lenA; i++) {
|
||||
dp[i] = [];
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (let j = 0; j <= lenB; j++) dp[0][j] = j;
|
||||
|
||||
for (let i = 1; i <= lenA; i++) {
|
||||
for (let j = 1; j <= lenB; j++) {
|
||||
if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1];
|
||||
else dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const dist = dp[lenA][lenB];
|
||||
const maxLen = Math.max(lenA, lenB);
|
||||
return 1 - dist / maxLen;
|
||||
}
|
||||
|
||||
_headers() {
|
||||
return {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Accept": "application/json",
|
||||
"Referer": this.baseUrl + "/",
|
||||
"Origin": this.baseUrl,
|
||||
"X-Site": "anicrush"
|
||||
};
|
||||
}
|
||||
|
||||
_parseQuery(q) {
|
||||
if (typeof q === "string") {
|
||||
const s = q.trim();
|
||||
if (s.startsWith("{") || s.startsWith("[")) {
|
||||
try { return JSON.parse(s); } catch (e) { return { query: s }; }
|
||||
}
|
||||
return { query: s };
|
||||
}
|
||||
return q || {};
|
||||
}
|
||||
|
||||
search(query) {
|
||||
query = this._parseQuery(query);
|
||||
|
||||
const q = this._safeStr(query.query).trim();
|
||||
if (!q) return [];
|
||||
|
||||
const media = query.media || {};
|
||||
const start = (media.startDate || {});
|
||||
const wantYear = start.year || 0;
|
||||
const wantMonth = start.month || 0;
|
||||
|
||||
const targetNormJP = this._normalizeTitle(media.romajiTitle);
|
||||
const targetNorm = media.englishTitle ? this._normalizeTitle(media.englishTitle) : targetNormJP;
|
||||
|
||||
const url = `${this.apiBase}/shared/v2/movie/list?keyword=${encodeURIComponent(q)}&limit=48&page=1`;
|
||||
const json = this._getJson(url, this._headers());
|
||||
const list = (((json || {}).result || {}).movies) || [];
|
||||
if (!Array.isArray(list) || !list.length) return [];
|
||||
|
||||
let matches = list.map((movie) => {
|
||||
const id = movie && movie.id != null ? String(movie.id) : "";
|
||||
const slug = movie && movie.slug ? String(movie.slug) : "";
|
||||
if (!id || !slug) return null;
|
||||
|
||||
const titleJP = movie && movie.name ? String(movie.name) : "";
|
||||
const titleEN = movie && movie.name_english ? String(movie.name_english) : "";
|
||||
const title = titleEN || titleJP || "Unknown";
|
||||
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
title,
|
||||
titleJP,
|
||||
normTitle: this._normalizeTitle(title),
|
||||
normTitleJP: this._normalizeTitle(titleJP),
|
||||
dub: !!(movie && movie.has_dub),
|
||||
startDate: this._normalizeDate(movie && movie.aired_from ? String(movie.aired_from) : "")
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
if (query.dub) matches = matches.filter(m => m.dub);
|
||||
|
||||
let filtered = matches;
|
||||
|
||||
if (wantYear) {
|
||||
filtered = matches.filter(m => {
|
||||
const tMatch = (m.normTitle === targetNorm) || (m.normTitleJP === targetNormJP);
|
||||
const d = m.startDate || {};
|
||||
const dMatch = wantMonth ? ((d.year === wantYear) && (d.month === wantMonth)) : (d.year === wantYear);
|
||||
return tMatch && dMatch;
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
filtered = matches.filter(m => {
|
||||
const a = m.normTitle;
|
||||
const b = targetNorm;
|
||||
const aj = m.normTitleJP;
|
||||
const bj = targetNormJP;
|
||||
|
||||
const fuzzy =
|
||||
(a && b && (a.includes(b) || b.includes(a) || this._levSim(a, b) > 0.72)) ||
|
||||
(aj && bj && (aj.includes(bj) || bj.includes(aj) || this._levSim(aj, bj) > 0.72));
|
||||
|
||||
const d = m.startDate || {};
|
||||
const dMatch = wantMonth ? ((d.year === wantYear) && (d.month === wantMonth)) : (d.year === wantYear);
|
||||
return fuzzy && dMatch;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const qn = this._normalizeTitle(q);
|
||||
filtered = matches.filter(m => {
|
||||
const a = this._normalizeTitle(m.title);
|
||||
const aj = this._normalizeTitle(m.titleJP);
|
||||
return (a === qn) || (aj === qn) || a.includes(qn) || aj.includes(qn) || qn.includes(a) || qn.includes(aj);
|
||||
});
|
||||
|
||||
filtered.sort((x, y) => {
|
||||
const A = this._normalizeTitle(x.title);
|
||||
const B = this._normalizeTitle(y.title);
|
||||
if (A.length !== B.length) return A.length - B.length;
|
||||
return A.localeCompare(B);
|
||||
});
|
||||
}
|
||||
|
||||
const subOrDub = query.dub ? "dub" : "sub";
|
||||
return filtered.map(m => ({
|
||||
id: `${m.id}/${subOrDub}`,
|
||||
title: m.title,
|
||||
url: `${this.baseUrl}/detail/${m.slug}.${m.id}`,
|
||||
subOrDub
|
||||
}));
|
||||
}
|
||||
|
||||
findEpisodes(Id) {
|
||||
const parts = String(Id || "").split("/");
|
||||
const id = parts[0];
|
||||
const subOrDub = parts[1] || "sub";
|
||||
if (!id) throw new Error("Missing id");
|
||||
|
||||
const url = `${this.apiBase}/shared/v2/episode/list?_movieId=${encodeURIComponent(id)}`;
|
||||
const epJson = this._getJson(url, this._headers());
|
||||
const groups = (epJson && epJson.result) ? epJson.result : {};
|
||||
const episodes = [];
|
||||
|
||||
const keys = Object.keys(groups || {});
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const group = groups[keys[i]];
|
||||
if (!Array.isArray(group)) continue;
|
||||
|
||||
for (let j = 0; j < group.length; j++) {
|
||||
const ep = group[j] || {};
|
||||
const num = Number(ep.number);
|
||||
if (!Number.isFinite(num)) continue;
|
||||
|
||||
episodes.push({
|
||||
id: `${id}/${subOrDub}`,
|
||||
number: num,
|
||||
title: ep.name_english ? String(ep.name_english) : (ep.name ? String(ep.name) : `Episode ${num}`),
|
||||
url: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
episodes.sort((a, b) => (a.number || 0) - (b.number || 0));
|
||||
return episodes;
|
||||
}
|
||||
|
||||
findEpisodeServer(episodeOrId, _server) {
|
||||
let ep = episodeOrId;
|
||||
if (typeof ep === "string") { try { ep = JSON.parse(ep); } catch (e) {} }
|
||||
ep = ep || {};
|
||||
|
||||
const parts = String(ep.id || "").split("/");
|
||||
const id = parts[0];
|
||||
const subOrDub = parts[1] || "sub";
|
||||
if (!id) throw new Error("Missing id");
|
||||
|
||||
const num = Number(ep.number);
|
||||
if (!Number.isFinite(num)) throw new Error("Missing episode number");
|
||||
|
||||
let server = String(_server || "").trim();
|
||||
if (!server || server === "default") server = "Southcloud-1";
|
||||
if (server === "HD-1") server = "Southcloud-1";
|
||||
if (server === "HD-2") server = "Southcloud-2";
|
||||
if (server === "HD-3") server = "Southcloud-3";
|
||||
|
||||
const serverMap = { "Southcloud-1": 4, "Southcloud-2": 1, "Southcloud-3": 6 };
|
||||
const sv = serverMap[server] != null ? serverMap[server] : 4;
|
||||
|
||||
const linkUrl =
|
||||
`${this.apiBase}/shared/v2/episode/sources?_movieId=${encodeURIComponent(id)}` +
|
||||
`&ep=${encodeURIComponent(String(num))}&sv=${encodeURIComponent(String(sv))}&sc=${encodeURIComponent(subOrDub)}`;
|
||||
|
||||
const json = this._getJson(linkUrl, this._headers());
|
||||
const encryptedIframe = (((json || {}).result || {}).link) ? String(json.result.link) : "";
|
||||
if (!encryptedIframe) throw new Error("Missing encrypted iframe link");
|
||||
|
||||
let decryptData = null;
|
||||
let requiredHeaders = null;
|
||||
|
||||
try {
|
||||
decryptData = this.extractMegaCloudSync(encryptedIframe);
|
||||
requiredHeaders = decryptData && decryptData.headersProvided ? decryptData.headersProvided : null;
|
||||
} catch (e) {}
|
||||
|
||||
if (!decryptData) {
|
||||
decryptData = this._getJson(
|
||||
`https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(encryptedIframe)}`,
|
||||
{}
|
||||
);
|
||||
requiredHeaders = {
|
||||
Referer: "https://megacloud.club/",
|
||||
Origin: "https://megacloud.club",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
};
|
||||
}
|
||||
|
||||
if (!decryptData) throw new Error("No video sources");
|
||||
|
||||
const sources = decryptData.sources || [];
|
||||
const streamSource =
|
||||
sources.find((s) => s && s.type === "hls") ||
|
||||
sources.find((s) => s && s.type === "mp4");
|
||||
|
||||
if (!streamSource || !streamSource.file) throw new Error("No valid stream file found");
|
||||
|
||||
const tracks = decryptData.tracks || [];
|
||||
const subtitles = tracks
|
||||
.filter((t) => t && t.kind === "captions" && t.file)
|
||||
.map((track, index) => ({
|
||||
id: `sub-${index}`,
|
||||
language: track.label || "Unknown",
|
||||
url: track.file,
|
||||
isDefault: !!track.default
|
||||
}));
|
||||
|
||||
const st = String(streamSource.type || "");
|
||||
const outType = (st === "hls" || st === "m3u8") ? "m3u8" : "mp4";
|
||||
|
||||
return {
|
||||
server: server,
|
||||
headers: requiredHeaders || {},
|
||||
videoSources: [{
|
||||
url: streamSource.file,
|
||||
type: outType,
|
||||
quality: "auto",
|
||||
subtitles: subtitles
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
extractMegaCloudSync(embedUrl) {
|
||||
const s = String(embedUrl || "");
|
||||
const mm = s.match(/^(https?):\/\/([^\/]+)(\/.*)?$/i);
|
||||
if (!mm) throw new Error("Invalid embedUrl");
|
||||
|
||||
const protocol = mm[1].toLowerCase();
|
||||
const host = mm[2];
|
||||
const baseDomain = `${protocol}://${host}/`;
|
||||
|
||||
const headers = {
|
||||
Accept: "*/*",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Referer: baseDomain,
|
||||
Origin: `${protocol}://${host}`,
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
|
||||
};
|
||||
|
||||
const html = this._getText(embedUrl, headers);
|
||||
|
||||
const fileIdMatch = html.match(/<title>\s*File\s+#([a-zA-Z0-9]+)\s*-/i);
|
||||
if (!fileIdMatch) throw new Error("file_id not found");
|
||||
const fileId = fileIdMatch[1];
|
||||
|
||||
let nonce = null;
|
||||
const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/);
|
||||
if (match48) nonce = match48[0];
|
||||
else {
|
||||
const match3x16 = [];
|
||||
const re = /["']([A-Za-z0-9]{16})["']/g;
|
||||
let m;
|
||||
while ((m = re.exec(html)) !== null) match3x16.push(m[1]);
|
||||
if (match3x16.length >= 3) nonce = match3x16[0] + match3x16[1] + match3x16[2];
|
||||
}
|
||||
if (!nonce) throw new Error("nonce not found");
|
||||
|
||||
const sourcesJson = this._getJson(`${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, headers);
|
||||
|
||||
return {
|
||||
sources: sourcesJson.sources || [],
|
||||
tracks: sourcesJson.tracks || [],
|
||||
intro: sourcesJson.intro || null,
|
||||
outro: sourcesJson.outro || null,
|
||||
server: sourcesJson.server || null,
|
||||
headersProvided: headers
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AniCrush;
|
||||
Reference in New Issue
Block a user