Update anime/anicrush/source.js

This commit is contained in:
2025-12-26 00:45:29 +01:00
parent 5bd2d259fb
commit 05b77e18b0

View File

@@ -43,21 +43,42 @@ class AniCrush {
} }
} }
_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) { _safeStr(v) {
return typeof v === "string" ? v : (v == null ? "" : String(v)); return typeof v === "string" ? v : (v == null ? "" : String(v));
} }
_normalizeTitle(t) { _headers() {
return this._safeStr(t) 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 || {};
}
_normalize(title) {
return (this._safeStr(title))
.toLowerCase()
.replace(/(season|cour|part)/g, "")
.replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, ""))
.replace(/[^a-z0-9]+/g, "")
.replace(/(?<!i)ii(?!i)/g, "2");
}
_normalizeTitle(title) {
return (this._safeStr(title))
.toLowerCase() .toLowerCase()
.replace(/(season|cour|part|uncensored)/g, "") .replace(/(season|cour|part|uncensored)/g, "")
.replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, "")) .replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, ""))
@@ -101,30 +122,9 @@ class AniCrush {
} }
} }
const dist = dp[lenA][lenB]; const distance = dp[lenA][lenB];
const maxLen = Math.max(lenA, lenB); const maxLen = Math.max(lenA, lenB);
return 1 - dist / maxLen; return 1 - distance / 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) { search(query) {
@@ -135,18 +135,16 @@ class AniCrush {
const media = query.media || {}; const media = query.media || {};
const start = (media.startDate || {}); const start = (media.startDate || {});
const wantYear = start.year || 0; const targetNormJP = this._normalize(media.romajiTitle);
const wantMonth = start.month || 0; const targetNorm = media.englishTitle ? this._normalize(media.englishTitle) : targetNormJP;
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 url = `${this.apiBase}/shared/v2/movie/list?keyword=${encodeURIComponent(q)}&limit=48&page=1`;
const json = this._getJson(url, this._headers()); 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 movies = (((json || {}).result || {}).movies) || [];
if (!Array.isArray(movies) || !movies.length) return [];
let matches = movies.map((movie) => {
const id = movie && movie.id != null ? String(movie.id) : ""; const id = movie && movie.id != null ? String(movie.id) : "";
const slug = movie && movie.slug ? String(movie.slug) : ""; const slug = movie && movie.slug ? String(movie.slug) : "";
if (!id || !slug) return null; if (!id || !slug) return null;
@@ -157,11 +155,11 @@ class AniCrush {
return { return {
id, id,
slug, pageUrl: slug,
title, title,
titleJP, titleJP,
normTitle: this._normalizeTitle(title), normTitleJP: this._normalize(titleJP),
normTitleJP: this._normalizeTitle(titleJP), normTitle: this._normalize(title),
dub: !!(movie && movie.has_dub), dub: !!(movie && movie.has_dub),
startDate: this._normalizeDate(movie && movie.aired_from ? String(movie.aired_from) : "") startDate: this._normalizeDate(movie && movie.aired_from ? String(movie.aired_from) : "")
}; };
@@ -169,55 +167,90 @@ class AniCrush {
if (query.dub) matches = matches.filter(m => m.dub); if (query.dub) matches = matches.filter(m => m.dub);
let filtered = matches; let filtered = matches.filter(m => {
const titleMatch = (m.normTitle === targetNorm) || (m.normTitleJP === targetNormJP);
const dateMatch =
(m.startDate && m.startDate.year === start.year) &&
(m.startDate && m.startDate.month === start.month);
return titleMatch && dateMatch;
});
if (wantYear) { if (!filtered.length) {
filtered = matches.filter(m => { filtered = matches.filter(m => {
const tMatch = (m.normTitle === targetNorm) || (m.normTitleJP === targetNormJP); const titleMatch = (m.normTitle === targetNorm) || (m.normTitleJP === targetNormJP);
const d = m.startDate || {}; const dateMatch = (m.startDate && m.startDate.year === start.year);
const dMatch = wantMonth ? ((d.year === wantYear) && (d.month === wantMonth)) : (d.year === wantYear); return titleMatch && dateMatch;
return tMatch && dMatch;
}); });
}
if (!filtered.length) { if (!filtered.length) {
filtered = matches.filter(m => { filtered = matches.filter(m => {
const a = m.normTitle; const titleMatch =
const b = targetNorm; m.normTitle.includes(targetNorm) ||
const aj = m.normTitleJP; m.normTitleJP.includes(targetNormJP) ||
const bj = targetNormJP; targetNorm.includes(m.normTitle) ||
targetNormJP.includes(m.normTitleJP) ||
this._levSim(m.normTitle, targetNorm) > 0.7 ||
this._levSim(m.normTitleJP, targetNormJP) > 0.7;
const fuzzy = const dateMatch =
(a && b && (a.includes(b) || b.includes(a) || this._levSim(a, b) > 0.72)) || (m.startDate && m.startDate.year === start.year) &&
(aj && bj && (aj.includes(bj) || bj.includes(aj) || this._levSim(aj, bj) > 0.72)); (m.startDate && m.startDate.month === start.month);
const d = m.startDate || {}; return titleMatch && dateMatch;
const dMatch = wantMonth ? ((d.year === wantYear) && (d.month === wantMonth)) : (d.year === wantYear); });
return fuzzy && dMatch; }
});
} if (!filtered.length) {
} else { filtered = matches.filter(m => {
const titleMatch =
m.normTitle.includes(targetNorm) ||
m.normTitleJP.includes(targetNormJP) ||
targetNorm.includes(m.normTitle) ||
targetNormJP.includes(m.normTitleJP) ||
this._levSim(m.normTitle, targetNorm) > 0.7 ||
this._levSim(m.normTitleJP, targetNormJP) > 0.7;
const dateMatch = (m.startDate && m.startDate.year === start.year);
return titleMatch && dateMatch;
});
}
let results = filtered.map(m => ({
id: `${m.id}/${query.dub ? "dub" : "sub"}`,
title: m.title,
url: `${this.baseUrl}/detail/${m.pageUrl}.${m.id}`,
subOrDub: query.dub ? "dub" : "sub"
}));
if (!media.startDate || !media.startDate.year) {
const qn = this._normalizeTitle(q); const qn = this._normalizeTitle(q);
filtered = matches.filter(m => { filtered = matches.filter(m => {
const a = this._normalizeTitle(m.title); const a = this._normalizeTitle(m.title);
const aj = this._normalizeTitle(m.titleJP); const aj = this._normalizeTitle(m.titleJP);
return (a === qn) || (aj === qn) || a.includes(qn) || aj.includes(qn) || qn.includes(a) || qn.includes(aj); return (
a === qn || aj === qn ||
a.includes(qn) || aj.includes(qn) ||
qn.includes(a) || qn.includes(aj)
);
}); });
filtered.sort((x, y) => { filtered.sort((a, b) => {
const A = this._normalizeTitle(x.title); const A = this._normalizeTitle(a.title);
const B = this._normalizeTitle(y.title); const B = this._normalizeTitle(b.title);
if (A.length !== B.length) return A.length - B.length; if (A.length !== B.length) return A.length - B.length;
return A.localeCompare(B); return A.localeCompare(B);
}); });
results = filtered.map(m => ({
id: `${m.id}/${query.dub ? "dub" : "sub"}`,
title: m.title,
url: `${this.baseUrl}/detail/${m.pageUrl}.${m.id}`,
subOrDub: query.dub ? "dub" : "sub"
}));
} }
const subOrDub = query.dub ? "dub" : "sub"; return results;
return filtered.map(m => ({
id: `${m.id}/${subOrDub}`,
title: m.title,
url: `${this.baseUrl}/detail/${m.slug}.${m.id}`,
subOrDub
}));
} }
findEpisodes(Id) { findEpisodes(Id) {
@@ -228,19 +261,17 @@ class AniCrush {
const url = `${this.apiBase}/shared/v2/episode/list?_movieId=${encodeURIComponent(id)}`; const url = `${this.apiBase}/shared/v2/episode/list?_movieId=${encodeURIComponent(id)}`;
const epJson = this._getJson(url, this._headers()); const epJson = this._getJson(url, this._headers());
const groups = (epJson && epJson.result) ? epJson.result : {}; const episodeGroups = (epJson && epJson.result) ? epJson.result : {};
const episodes = []; const episodes = [];
const keys = Object.keys(episodeGroups || {});
const keys = Object.keys(groups || {});
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const group = groups[keys[i]]; const group = episodeGroups[keys[i]];
if (!Array.isArray(group)) continue; if (!Array.isArray(group)) continue;
for (let j = 0; j < group.length; j++) { for (let j = 0; j < group.length; j++) {
const ep = group[j] || {}; const ep = group[j] || {};
const num = Number(ep.number); const num = Number(ep.number);
if (!Number.isFinite(num)) continue; if (!Number.isFinite(num)) continue;
episodes.push({ episodes.push({
id: `${id}/${subOrDub}`, id: `${id}/${subOrDub}`,
number: num, number: num,
@@ -254,33 +285,23 @@ class AniCrush {
return episodes; return episodes;
} }
_getEpisodeSourcesLink(movieId, epNumber, sv, sc) { findEpisodeServer(episode, _server) {
const linkUrl = if (typeof episode === "string") {
`${this.apiBase}/shared/v2/episode/sources?_movieId=${encodeURIComponent(movieId)}` + try { episode = JSON.parse(episode); } catch (e) {}
`&ep=${encodeURIComponent(String(epNumber))}` + }
`&sv=${encodeURIComponent(String(sv))}` + episode = episode || {};
`&sc=${encodeURIComponent(String(sc))}`;
const json = this._getJson(linkUrl, this._headers()); const parts = String(episode.id || "").split("/");
const link = (((json || {}).result || {}).link) ? String(json.result.link) : "";
return { json, link, linkUrl };
}
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 id = parts[0];
const subOrDub = parts[1] || "sub"; const subOrDub = parts[1] || "sub";
if (!id) throw new Error("Missing id"); if (!id) throw new Error("Missing id");
const num = Number(ep.number); const epNum = Number(episode.number);
if (!Number.isFinite(num)) throw new Error("Missing episode number"); if (!Number.isFinite(epNum)) throw new Error("Missing episode number");
let server = String(_server || "").trim(); let server = String(_server || "").trim();
if (!server || server === "default") server = "Southcloud-1"; if (!server || server === "default") server = "Southcloud-1";
if (server === "HD-1") server = "Southcloud-1"; if (server === "HD-1") server = "Southcloud-1";
if (server === "HD-2") server = "Southcloud-2"; if (server === "HD-2") server = "Southcloud-2";
if (server === "HD-3") server = "Southcloud-3"; if (server === "HD-3") server = "Southcloud-3";
@@ -288,26 +309,16 @@ class AniCrush {
const serverMap = { "Southcloud-1": 4, "Southcloud-2": 1, "Southcloud-3": 6 }; const serverMap = { "Southcloud-1": 4, "Southcloud-2": 1, "Southcloud-3": 6 };
const sv = serverMap[server] != null ? serverMap[server] : 4; const sv = serverMap[server] != null ? serverMap[server] : 4;
const scCandidates = const encryptedLinkUrl =
subOrDub === "dub" `${this.apiBase}/shared/v2/episode/sources?_movieId=${encodeURIComponent(id)}` +
? ["dub", "dubbed", "en", "eng", "2", "1"] `&ep=${encodeURIComponent(String(epNum))}` +
: ["sub", "jp", "0", "1"]; `&sv=${encodeURIComponent(String(sv))}` +
`&sc=${encodeURIComponent(String(subOrDub))}`;
let encryptedIframe = "";
let usedSc = null;
for (let i = 0; i < scCandidates.length; i++) {
const sc = scCandidates[i];
const r = this._getEpisodeSourcesLink(id, num, sv, sc);
if (r.link) {
encryptedIframe = r.link;
usedSc = sc;
break;
}
}
const json = this._getJson(encryptedLinkUrl, this._headers());
const encryptedIframe = (((json || {}).result || {}).link) ? String(json.result.link) : "";
if (!encryptedIframe) { if (!encryptedIframe) {
throw new Error(`Missing encrypted iframe link (movieId=${id} ep=${num} server=${server} sv=${sv} scTried=${scCandidates.join(",")})`); throw new Error(`Missing encrypted iframe link (server=${server} sv=${sv} sc=${subOrDub})`);
} }
let decryptData = null; let decryptData = null;
@@ -326,24 +337,22 @@ class AniCrush {
requiredHeaders = { requiredHeaders = {
Referer: "https://megacloud.club/", Referer: "https://megacloud.club/",
Origin: "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", "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" "X-Requested-With": "XMLHttpRequest"
}; };
} }
if (!decryptData) throw new Error("No video sources"); if (!decryptData || !decryptData.sources) throw new Error("No video sources from any decrypter");
const sources = decryptData.sources || []; const sources = decryptData.sources || [];
const streamSource = const streamSource =
sources.find((s) => s && s.type === "hls") || sources.find((s) => s && s.type === "hls") ||
sources.find((s) => s && s.type === "mp4"); sources.find((s) => s && s.type === "mp4");
if (!streamSource || !streamSource.file) { if (!streamSource || !streamSource.file) throw new Error("No valid stream file found");
throw new Error(`No valid stream file found (scUsed=${usedSc || "none"})`);
}
const tracks = decryptData.tracks || []; const subtitles = (decryptData.tracks || [])
const subtitles = tracks
.filter((t) => t && t.kind === "captions" && t.file) .filter((t) => t && t.kind === "captions" && t.file)
.map((track, index) => ({ .map((track, index) => ({
id: `sub-${index}`, id: `sub-${index}`,
@@ -352,19 +361,19 @@ class AniCrush {
isDefault: !!track.default isDefault: !!track.default
})); }));
const st = String(streamSource.type || ""); const outType = (String(streamSource.type || "") === "hls") ? "m3u8" : "mp4";
const outType = (st === "hls" || st === "m3u8") ? "m3u8" : "mp4";
return { return {
server: server, server: server,
headers: requiredHeaders || {}, headers: requiredHeaders || {},
videoSources: [{ videoSources: [
url: streamSource.file, {
type: outType, url: streamSource.file,
quality: "auto", type: outType,
subtitles: subtitles quality: "auto",
}], subtitles
_debug: { scUsed: usedSc, sv: sv } }
]
}; };
} }
@@ -382,13 +391,14 @@ class AniCrush {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
Referer: baseDomain, Referer: baseDomain,
Origin: `${protocol}://${host}`, 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" "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 html = this._getText(embedUrl, headers);
const fileIdMatch = html.match(/<title>\s*File\s+#([a-zA-Z0-9]+)\s*-/i); const fileIdMatch = html.match(/<title>\s*File\s+#([a-zA-Z0-9]+)\s*-/i);
if (!fileIdMatch) throw new Error("file_id not found"); if (!fileIdMatch) throw new Error("file_id not found in embed page");
const fileId = fileIdMatch[1]; const fileId = fileIdMatch[1];
let nonce = null; let nonce = null;
@@ -403,7 +413,10 @@ class AniCrush {
} }
if (!nonce) throw new Error("nonce not found"); if (!nonce) throw new Error("nonce not found");
const sourcesJson = this._getJson(`${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, headers); const sourcesJson = this._getJson(
`${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`,
headers
);
return { return {
sources: sourcesJson.sources || [], sources: sourcesJson.sources || [],