diff --git a/anime/hianime/source.js b/anime/hianime/source.js index 007163e..ecf1a25 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,7 +1,7 @@ class HiAnime { constructor() { this.type = "anime-streaming"; - this.version = "1.0.5"; + this.version = "1.0.6"; this.baseUrl = "https://hianime.to"; } @@ -70,9 +70,11 @@ class HiAnime { const str = this._safeStr(s); if (!str) return ""; return str + .replace(/\\u0026/g, "&") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") + .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&#(\d+);/g, (_, n) => { @@ -81,15 +83,15 @@ class HiAnime { }); } - _parseQuery(q) { - if (typeof q === "string") { - const s = q.trim(); + _parseArg(a1) { + if (typeof a1 === "string") { + const s = a1.trim(); if (s.startsWith("{") || s.startsWith("[")) { try { return JSON.parse(s); } catch (e) { return { query: s }; } } return { query: s }; } - return q || {}; + return a1 || {}; } _getSubOrDubFromAny(obj) { @@ -110,71 +112,259 @@ class HiAnime { return "sub"; } + _normalizeTitle(title) { + return this._safeStr(title) + .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, ""); + } + + _levSim(a, b) { + a = this._safeStr(a); + b = this._safeStr(b); + const lenA = a.length, lenB = b.length; + if (!lenA && !lenB) return 1; + if (!lenA || !lenB) return 0; + + const dp = new Array(lenA + 1); + for (let i = 0; i <= lenA; i++) dp[i] = new Array(lenB + 1).fill(0); + + for (let i = 0; i <= lenA; 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; + } + + _isSingleWordStrictTarget(title) { + const n = this._safeStr(title).trim(); + if (!n) return false; + const parts = n.split(/\s+/).filter(Boolean); + if (parts.length !== 1) return false; + const t = this._normalizeTitle(n); + return t.length >= 3 && t.length <= 10; + } + + _bestTitleFromSuggestItem(x) { + if (!x) return ""; + const t = x.title; + if (typeof t === "string") return t; + if (t && typeof t === "object") { + return String(t.english || t.romaji || t.native || t.userPreferred || ""); + } + return String(x.name || x.english || x.romaji || ""); + } + + _parseStartDate(dateStr) { + const s = this._safeStr(dateStr).trim(); // "Jul 4, 2025" + if (!s) return null; + + const monthMap = { + Jan: 1, Feb: 2, Mar: 3, Apr: 4, May: 5, Jun: 6, + Jul: 7, Aug: 8, Sep: 9, 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 mon = monthMap[m[1]]; + const day = parseInt(m[2], 10); + const year = parseInt(m[3], 10); + + if (!mon || !isFinite(day) || !isFinite(year)) return null; + return { year, month: mon, day }; + } + _extractWatchIdFromUrl(url) { const s = String(url || ""); const m = s.match(/\/watch\/[^\/]+-(\d+)/i); return m ? m[1] : ""; } - _bestTitle(obj) { - if (!obj) return ""; - const t = obj.title; - if (typeof t === "string") return t; - if (t && typeof t === "object") { - return String(t.english || t.romaji || t.native || t.userPreferred || ""); + _extractImageFromHtmlChunk(chunk) { + const c = String(chunk || ""); + const m = c.match(/]+src="([^"]+)"|]+data-src="([^"]+)"/i); + return m ? (m[1] || m[2] || "") : ""; + } + + _fetchSuggestMatches(q) { + const url = `${this.baseUrl}/ajax/search/suggest?keyword=${encodeURIComponent(q)}`; + const j = this._getJson(url, this._headersJson()); + + if (j && Array.isArray(j.results)) { + return j.results.map((x) => { + const title = this._decodeHtml(this._bestTitleFromSuggestItem(x) || String(x.name || "")); + const u = x.url ? String(x.url) : ""; + const full = u.startsWith("http") ? u : (u ? (this.baseUrl + u) : ""); + const id = x.id ? String(x.id) : this._extractWatchIdFromUrl(full); + const image = x.image ? String(x.image) : ""; + const dateStr = x.released ? String(x.released) : ""; + const startDate = this._parseStartDate(dateStr); + return { id, title, url: full, image, startDate }; + }).filter((x) => x.id && x.url); } - return String(obj.name || obj.english || obj.romaji || ""); + + const html = this._decodeHtml(String(j.html || j.result || "")); + if (!html) return []; + + const re = /([\s\S]*?)<\/a>/g; + const out = []; + let m; + while ((m = re.exec(html)) !== null) { + const pageUrl = String(m[1] || "").trim(); // e.g. "monster-2004-12345" + if (!pageUrl || pageUrl.startsWith("search?")) continue; + + const chunk = String(m[2] || ""); + const titleM = chunk.match(/]*class="film-name"[^>]*>([^<]+)<\/h3>/i); + const title = this._decodeHtml(titleM ? titleM[1] : ""); + + const dateM = chunk.match(/
\s*([^<]+)<\/span>/i); + const startDate = this._parseStartDate(dateM ? dateM[1] : ""); + + const fullUrl = `${this.baseUrl}/${pageUrl}`; + const idMatch = pageUrl.match(/-(\d+)$/); + const id = idMatch ? idMatch[1] : this._extractWatchIdFromUrl(fullUrl); + + const image = this._extractImageFromHtmlChunk(chunk); + + if (!id) continue; + out.push({ id, title: title || q, url: fullUrl, image, startDate }); + } + return out; + } + + _fetchFallbackSearchMatches(q) { + const url2 = `${this.baseUrl}/search?keyword=${encodeURIComponent(q)}`; + const html = this._getText(url2, this._headersHtml()); + + const out = []; + const re = /= 20) break; + } + return out; } search(a1) { - const arg = this._parseQuery(a1); - + const arg = this._parseArg(a1); const q = this._safeStr(arg.query || arg.q || "").trim(); if (!q) return "[]"; const subOrDub = this._getSubOrDubFromAny(arg); - const url = `${this.baseUrl}/ajax/search/suggest?keyword=${encodeURIComponent(q)}`; - const j = this._getJson(url, this._headersJson()); + const media = arg.media || {}; + const start = media.startDate || {}; + const targetYear = parseInt(start.year, 10); + const targetMonth = parseInt(start.month, 10); - let results = []; - if (j && Array.isArray(j.results)) { - results = j.results - .map((x) => { - const title = this._bestTitle(x) || String(x.name || ""); - const u = x.url ? String(x.url) : ""; - const full = u.startsWith("http") ? u : (u ? (this.baseUrl + u) : ""); - const id = x.id ? String(x.id) : this._extractWatchIdFromUrl(full); - return { id, title, url: full }; - }) - .filter((x) => x.id && x.url); - } + const en = this._safeStr(media.englishTitle || media.english || "").trim(); + const ro = this._safeStr(media.romajiTitle || media.romaji || "").trim(); - if (!results.length) { - const url2 = `${this.baseUrl}/search?keyword=${encodeURIComponent(q)}`; - const html = this._getText(url2, this._headersHtml()); + const targetNormJP = this._normalizeTitle(ro); + const targetNormEN = this._normalizeTitle(en); + const targetNorm = targetNormEN || targetNormJP || this._normalizeTitle(q); - const out = []; - const re = /href="(\/watch\/[^"]+)"/gi; - const seen = {}; - let m; - while ((m = re.exec(html)) !== null) { - const href = String(m[1] || ""); - const full = href.startsWith("http") ? href : (this.baseUrl + href); - const id = this._extractWatchIdFromUrl(full); - if (!id || seen[id]) continue; - seen[id] = true; - out.push({ id, title: q, url: full }); - if (out.length >= 20) break; + const strictTargetRaw = (this._isSingleWordStrictTarget(en) ? en : (this._isSingleWordStrictTarget(ro) ? ro : "")); + const strictNorm = strictTargetRaw ? this._normalizeTitle(strictTargetRaw) : ""; + + let matches = this._fetchSuggestMatches(q); + if (!matches.length) matches = this._fetchFallbackSearchMatches(q); + if (!matches.length) return "[]"; + + const hasYear = isFinite(targetYear) && targetYear > 0; + const hasMonth = isFinite(targetMonth) && targetMonth > 0 && targetMonth <= 12; + + const withNorm = matches.map((m) => { + const title = this._safeStr(m.title); + return { + id: String(m.id || ""), + title, + url: String(m.url || ""), + image: String(m.image || ""), + startDate: m.startDate || null, + normTitle: this._normalizeTitle(title) + }; + }).filter((m) => m.id && m.url); + + const exactTitleMatch = (m) => { + if (!m.normTitle) return false; + if (strictNorm) return m.normTitle === strictNorm; + return (m.normTitle === targetNorm) || (targetNormJP && m.normTitle === targetNormJP); + }; + + const fuzzyTitleMatch = (m) => { + if (!m.normTitle) return false; + if (strictNorm) return m.normTitle === strictNorm; + + const a = m.normTitle; + const b = targetNorm; + + if (a === b) return true; + if (a.includes(b) || b.includes(a)) return true; + + if (targetNormJP) { + if (a.includes(targetNormJP) || targetNormJP.includes(a)) return true; + if (this._levSim(a, targetNormJP) > 0.72) return true; } - results = out; - } - const mapped = results.map((r) => ({ + return this._levSim(a, b) > 0.72; + }; + + const dateMatchYM = (m) => { + if (!hasYear) return true; + if (!m.startDate || !m.startDate.year) return false; + if (m.startDate.year !== targetYear) return false; + if (!hasMonth) return true; + return m.startDate.month === targetMonth; + }; + + const dateMatchY = (m) => { + if (!hasYear) return true; + if (!m.startDate || !m.startDate.year) return false; + return m.startDate.year === targetYear; + }; + + let filtered = withNorm.filter((m) => exactTitleMatch(m) && dateMatchYM(m)); + if (!filtered.length) filtered = withNorm.filter((m) => exactTitleMatch(m) && dateMatchY(m)); + if (!filtered.length) filtered = withNorm.filter((m) => fuzzyTitleMatch(m) && dateMatchYM(m)); + if (!filtered.length) filtered = withNorm.filter((m) => fuzzyTitleMatch(m) && dateMatchY(m)); + + if (!filtered.length && strictNorm) return "[]"; + + if (!filtered.length) filtered = withNorm.filter((m) => fuzzyTitleMatch(m)); + if (!filtered.length) return "[]"; + + filtered.sort((a, b) => { + const A = a.normTitle.length; + const B = b.normTitle.length; + if (A !== B) return A - B; + return a.normTitle.localeCompare(b.normTitle); + }); + + const mapped = filtered.map((r) => ({ id: `${r.id}/${subOrDub}`, title: this._decodeHtml(r.title || ""), + image: r.image || "", url: r.url, - subOrDub + subOrDub, + startDate: r.startDate || null })); return JSON.stringify(mapped); @@ -246,7 +436,10 @@ class HiAnime { let serverId = mmExact ? String(mmExact[1] || "").trim() : ""; if (!serverId) { - const reFirst = new RegExp(`class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="([^"]+)"`, "i"); + const reFirst = new RegExp( + `class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="([^"]+)"`, + "i" + ); const mmFirst = reFirst.exec(html); if (mmFirst) serverId = String(mmFirst[1] || "").trim(); } @@ -339,7 +532,6 @@ class HiAnime { const fileId = fileIdMatch[1]; let nonce = null; - const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); if (match48) nonce = match48[0];