From 61b5c2e711f0852f100a7b1aec2349edb7786fd0 Mon Sep 17 00:00:00 2001 From: MrGus Date: Fri, 26 Dec 2025 20:22:58 +0100 Subject: [PATCH] revert dbd833e6ad51ec847b049b1123476d877cd701a9 revert Update anime/hianime/source.js --- anime/hianime/source.js | 525 ++++++++++++++++++++++++++-------------- 1 file changed, 340 insertions(+), 185 deletions(-) diff --git a/anime/hianime/source.js b/anime/hianime/source.js index 2a39a59..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.2"; + this.version = "1.0.6"; this.baseUrl = "https://hianime.to"; } @@ -42,82 +42,60 @@ class HiAnime { } } - safeString(str) { - return typeof str === "string" ? str : (str == null ? "" : String(str)); + _safeStr(v) { + return typeof v === "string" ? v : (v == null ? "" : String(v)); + } + + _headersHtml() { + return { + "User-Agent": "Mozilla/5.0", + "Accept": "text/html", + "Referer": this.baseUrl + "/", + "Origin": this.baseUrl, + "X-Requested-With": "XMLHttpRequest" + }; + } + + _headersJson() { + return { + "User-Agent": "Mozilla/5.0", + "Accept": "application/json", + "Referer": this.baseUrl + "/", + "Origin": this.baseUrl, + "X-Requested-With": "XMLHttpRequest" + }; } _decodeHtml(s) { - const t = this.safeString(s); - if (!t) return ""; - return t + 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(/>/g, ">") - .replace(/ /g, " ") .replace(/&#(\d+);/g, (_, n) => { - try { return String.fromCharCode(parseInt(n, 10)); } catch (e) { return ""; } + const code = parseInt(n, 10); + return isFinite(code) ? String.fromCharCode(code) : ""; }); } - normalizeSeasonParts(title) { - const s = this.safeString(title); - return s.toLowerCase() - .replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, "")) - .replace(/season|cour|part/g, "") - .replace(/[^a-z0-9]+/g, ""); - } - - _levSim(a, b) { - a = this.safeString(a); - b = this.safeString(b); - if (!a && !b) return 1; - if (!a || !b) return 0; - - const la = a.length, lb = b.length; - const dp = new Array(la + 1); - for (let i = 0; i <= la; i++) dp[i] = new Array(lb + 1).fill(0); - for (let i = 0; i <= la; i++) dp[i][0] = i; - for (let j = 0; j <= lb; j++) dp[0][j] = j; - - for (let i = 1; i <= la; i++) { - for (let j = 1; j <= lb; j++) { - const cost = a[i - 1] === b[j - 1] ? 0 : 1; - dp[i][j] = Math.min( - dp[i - 1][j] + 1, - dp[i][j - 1] + 1, - dp[i - 1][j - 1] + cost - ); - } - } - const dist = dp[la][lb]; - const maxLen = Math.max(la, lb) || 1; - return 1 - (dist / maxLen); - } - - _titleOk(candidateNorm, targetNorm, targetNormJP) { - if (!candidateNorm) return false; - if (candidateNorm === targetNorm || candidateNorm === targetNormJP) return true; - if (candidateNorm.includes(targetNorm) || targetNorm.includes(candidateNorm)) return true; - if (candidateNorm.includes(targetNormJP) || targetNormJP.includes(candidateNorm)) return true; - return (this._levSim(candidateNorm, targetNorm) >= 0.72) || (this._levSim(candidateNorm, targetNormJP) >= 0.72); - } - - _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 || {}; } - _wantTrackFromEpisode(e) { - if (!e) return "sub"; - + _getSubOrDubFromAny(obj) { + const e = obj || {}; if (typeof e.dub === "boolean") return e.dub ? "dub" : "sub"; const sod = String(e.subOrDub || "").toLowerCase(); @@ -134,116 +112,290 @@ class HiAnime { return "sub"; } - search(a1) { - const query = this._parseQuery(a1); - const media = query.media || {}; - const start = media.startDate || {}; - const q = this.safeString(query.query || "").trim(); - if (!q) return "[]"; + _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, ""); + } - const targetEn = this.safeString(media.englishTitle || media.english || media.titleEnglish || ""); - const targetRo = this.safeString(media.romajiTitle || media.romaji || media.titleRomaji || ""); - const targetNorm = this.normalizeSeasonParts(this._decodeHtml(targetEn || targetRo || q)); - const targetNormJP = this.normalizeSeasonParts(this._decodeHtml(targetRo || q)); + _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 url = - `${this.baseUrl}/search?keyword=${encodeURIComponent(q)}` + - `&sy=${encodeURIComponent(String(start.year || ""))}` + - `&sm=${encodeURIComponent(String(start.month || ""))}` + - `&sort=default`; + const dp = new Array(lenA + 1); + for (let i = 0; i <= lenA; i++) dp[i] = new Array(lenB + 1).fill(0); - const html = this._getText(url, { - "User-Agent": "Mozilla/5.0", - "Accept": "text/html", - "Referer": this.baseUrl + "/", - "Origin": this.baseUrl - }); + for (let i = 0; i <= lenA; i++) dp[i][0] = i; + for (let j = 0; j <= lenB; j++) dp[0][j] = j; - const re = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; - const matches = []; + 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] : ""; + } + + _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); + } + + 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 id = String(m[3] || ""); - const pageUrl = String(m[1] || ""); - const title = this._decodeHtml(String(m[2] || "")); + const pageUrl = String(m[1] || "").trim(); // e.g. "monster-2004-12345" + if (!pageUrl || pageUrl.startsWith("search?")) continue; - const jnameRe = new RegExp( - `

[\\s\\S]*?]+href="\\/${pageUrl}[^"]*"[^>]+data-jname="([^"]+)"`, - "i" - ); - const jnameMatch = html.match(jnameRe); - const jname = jnameMatch ? this._decodeHtml(jnameMatch[1]) : ""; + const chunk = String(m[2] || ""); + const titleM = chunk.match(/]*class="film-name"[^>]*>([^<]+)<\/h3>/i); + const title = this._decodeHtml(titleM ? titleM[1] : ""); - const imageRe = new RegExp( - `]+data-src="([^"]+)"`, - "i" - ); - const imageMatch = html.match(imageRe); - const image = imageMatch ? String(imageMatch[1]) : ""; + const dateM = chunk.match(/
\s*([^<]+)<\/span>/i); + const startDate = this._parseStartDate(dateM ? dateM[1] : ""); - const normTitle = this.normalizeSeasonParts(title); - const normJP = this.normalizeSeasonParts(jname); + const fullUrl = `${this.baseUrl}/${pageUrl}`; + const idMatch = pageUrl.match(/-(\d+)$/); + const id = idMatch ? idMatch[1] : this._extractWatchIdFromUrl(fullUrl); - matches.push({ - id, - pageUrl, - title, - image, - normTitle, - normJP - }); + 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._parseArg(a1); + const q = this._safeStr(arg.query || arg.q || "").trim(); + if (!q) return "[]"; + + const subOrDub = this._getSubOrDubFromAny(arg); + + const media = arg.media || {}; + const start = media.startDate || {}; + const targetYear = parseInt(start.year, 10); + const targetMonth = parseInt(start.month, 10); + + const en = this._safeStr(media.englishTitle || media.english || "").trim(); + const ro = this._safeStr(media.romajiTitle || media.romaji || "").trim(); + + const targetNormJP = this._normalizeTitle(ro); + const targetNormEN = this._normalizeTitle(en); + const targetNorm = targetNormEN || targetNormJP || this._normalizeTitle(q); + + 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 wantTrack = query.dub ? "dub" : "sub"; + const hasYear = isFinite(targetYear) && targetYear > 0; + const hasMonth = isFinite(targetMonth) && targetMonth > 0 && targetMonth <= 12; - let filtered = matches.filter(x => this._titleOk(x.normTitle, targetNorm, targetNormJP) || this._titleOk(x.normJP, targetNorm, targetNormJP)); + 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); - if (!filtered.length) { - const qNorm = this.normalizeSeasonParts(q); - filtered = matches.filter(x => this._titleOk(x.normTitle, qNorm, qNorm) || this._titleOk(x.normJP, qNorm, qNorm)); - } + 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; + } + + 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 sa = Math.max(this._levSim(a.normTitle, targetNorm), this._levSim(a.normJP, targetNormJP)); - const sb = Math.max(this._levSim(b.normTitle, targetNorm), this._levSim(b.normJP, targetNormJP)); - return sb - sa; + const A = a.normTitle.length; + const B = b.normTitle.length; + if (A !== B) return A - B; + return a.normTitle.localeCompare(b.normTitle); }); - return JSON.stringify(filtered.map(x => ({ - id: `${x.id}/${wantTrack}`, - title: x.title, - image: x.image, - url: `${this.baseUrl}/watch/${x.pageUrl}`, - subOrDub: wantTrack - }))); + const mapped = filtered.map((r) => ({ + id: `${r.id}/${subOrDub}`, + title: this._decodeHtml(r.title || ""), + image: r.image || "", + url: r.url, + subOrDub, + startDate: r.startDate || null + })); + + return JSON.stringify(mapped); } findEpisodes(animeId) { const parts = String(animeId || "").split("/"); const id = parts[0]; - const subOrDub = (parts[1] || "sub").toLowerCase(); + const subOrDub = parts[1] || "sub"; + if (!id) return "[]"; - const json = this._getJson(`${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, { - "X-Requested-With": "XMLHttpRequest" - }); + const j = this._getJson( + `${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, + this._headersJson() + ); - const html = this._decodeHtml(String(json.html || json.result || "")); + const html = this._decodeHtml(String(j.html || j.result || "")); const episodes = []; - const regex = - /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*title="([^"]+)"/g; - - let match; - while ((match = regex.exec(html)) !== null) { + const re = /data-number="([^"]+)"[^>]*data-id="([^"]+)"/gi; + let m; + while ((m = re.exec(html)) !== null) { + const numRaw = String(m[1] || "").trim(); + const epId = String(m[2] || "").trim(); + const num = parseFloat(numRaw); + if (!epId || !isFinite(num)) continue; episodes.push({ - id: `${match[2]}/${subOrDub}`, - number: parseInt(match[1], 10), - url: this.baseUrl + match[3], - title: this._decodeHtml(match[4] || "") + id: `${epId}/${subOrDub}`, + number: num, + title: "", + url: "" }); } @@ -251,55 +403,52 @@ class HiAnime { return JSON.stringify(episodes); } - findEpisodeServer(episodeObj, _server) { - let episode = episodeObj; - if (typeof episode === "string") { - try { episode = JSON.parse(episode); } catch (e) { episode = {}; } + findEpisodeServer(episodeObj, serverName) { + let ep = episodeObj; + if (typeof ep === "string") { + try { ep = JSON.parse(ep); } catch (e) { ep = {}; } } - episode = episode || {}; - const idParts = String(episode.id || "").split("/"); - const episodeId = idParts[0]; - if (!episodeId) throw new Error("Missing episode id"); + const epIdRaw = this._safeStr(ep && ep.id ? ep.id : "").trim(); + if (!epIdRaw) throw new Error("Missing episode id"); - const wantTrack = this._wantTrackFromEpisode(episode); + const parts = epIdRaw.split("/"); + const epId = parts[0]; + const subOrDub = (parts[1] || "sub").toLowerCase(); - let serverName = String(_server || "").trim(); - if (!serverName || serverName === "default") serverName = "HD-1"; - if (serverName === "HD-4") throw new Error("HD-4 not implemented"); + const server = String(serverName || "").trim() || "HD-1"; + if (server === "HD-4") return "null"; - const serverJson = this._getJson( - `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(episodeId)}`, - { "X-Requested-With": "XMLHttpRequest" } + const j = this._getJson( + `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(epId)}`, + this._headersJson() ); - const serverHtml = this._decodeHtml(String(serverJson.html || serverJson.result || "")); - if (!serverHtml) throw new Error("Empty server list"); + const html = this._decodeHtml(String(j.html || j.result || "")); - const esc = (s) => String(s || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - - const strictRe = new RegExp( - `]*class="item\\s+server-item"[^>]*data-type="${esc(wantTrack)}"[^>]*data-id="(\\d+)"[^>]*>[\\s\\S]*?]*>[\\s\\S]*?${esc(serverName)}[\\s\\S]*?<\\/a>`, + const escapedServer = server.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const reExact = new RegExp( + `class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="([^"]+)"[^>]*>[\\s\\S]*?>\\s*${escapedServer}\\s*<`, "i" ); + const mmExact = reExact.exec(html); - let mm = strictRe.exec(serverHtml); - let serverId = mm ? String(mm[1] || "").trim() : ""; + let serverId = mmExact ? String(mmExact[1] || "").trim() : ""; if (!serverId) { - const trackAnyRe = new RegExp( - `]*class="item\\s+server-item"[^>]*data-type="${esc(wantTrack)}"[^>]*data-id="(\\d+)"`, + const reFirst = new RegExp( + `class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="([^"]+)"`, "i" ); - mm = trackAnyRe.exec(serverHtml); - serverId = mm ? String(mm[1] || "").trim() : ""; + const mmFirst = reFirst.exec(html); + if (mmFirst) serverId = String(mmFirst[1] || "").trim(); } - if (!serverId) throw new Error(`No server id found for track=${wantTrack}`); + if (!serverId) throw new Error(`No server id found for track=${subOrDub}`); const sourcesJson = this._getJson( `${this.baseUrl}/ajax/v2/episode/sources?id=${encodeURIComponent(serverId)}`, - { "X-Requested-With": "XMLHttpRequest" } + this._headersJson() ); const embed = (sourcesJson && sourcesJson.link) ? String(sourcesJson.link) : ""; @@ -332,7 +481,7 @@ class HiAnime { const sources = Array.isArray(decryptData.sources) ? decryptData.sources : []; const tracks = Array.isArray(decryptData.tracks) ? decryptData.tracks : []; - let streamSource = + const streamSource = sources.find((s) => s && s.type === "hls" && s.file) || sources.find((s) => s && s.type === "mp4" && s.file) || sources.find((s) => s && s.file); @@ -341,23 +490,27 @@ class HiAnime { const subtitles = tracks .filter((t) => t && String(t.kind || "").toLowerCase() === "captions" && t.file) - .map((t, idx) => ({ - id: `sub-${idx}`, - language: this._decodeHtml(t.label || "Unknown"), + .map((t, index) => ({ + id: `sub-${index}`, + language: String(t.label || "Unknown"), url: String(t.file), isDefault: !!t.default })); - return JSON.stringify({ - server: serverName, + const out = { + server: server, headers: requiredHeaders, - videoSources: [{ - url: String(streamSource.file), - type: (String(streamSource.type || "").toLowerCase() === "hls" || String(streamSource.file).includes(".m3u8")) ? "m3u8" : "mp4", - quality: "auto", - subtitles - }] - }); + videoSources: [ + { + url: String(streamSource.file), + type: String(streamSource.type || "").toLowerCase() === "hls" ? "m3u8" : "mp4", + quality: "auto", + subtitles: subtitles + } + ] + }; + + return JSON.stringify(out); } extractMegaCloudSync(embedUrl) { @@ -369,10 +522,10 @@ class HiAnime { "X-Requested-With": "XMLHttpRequest", "Referer": baseDomain, "Origin": `${url.protocol}//${url.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" }; - const html = this._getText(embedUrl, headers); + const html = this._getText(String(embedUrl), headers); const fileIdMatch = html.match(/\s*File\s+#([a-zA-Z0-9]+)\s*-/i); if (!fileIdMatch) throw new Error("file_id not found in embed page"); @@ -381,12 +534,14 @@ class HiAnime { let nonce = null; const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); if (match48) nonce = match48[0]; - else { - const match3x16 = Array.from(html.matchAll(/["']([A-Za-z0-9]{16})["']/g)); + + if (!nonce) { + const match3x16 = [...html.matchAll(/["']([A-Za-z0-9]{16})["']/g)]; if (match3x16.length >= 3) { nonce = match3x16[0][1] + match3x16[1][1] + match3x16[2][1]; } } + if (!nonce) throw new Error("nonce not found"); const sourcesJson = this._getJson( @@ -395,7 +550,7 @@ class HiAnime { ); return { - sources: sourcesJson.sources, + sources: sourcesJson.sources || [], tracks: sourcesJson.tracks || [], intro: sourcesJson.intro || null, outro: sourcesJson.outro || null, @@ -405,4 +560,4 @@ class HiAnime { } } -module.exports = new HiAnime(); +module.exports = HiAnime;