diff --git a/ashi/ashi.js b/ashi/ashi.js index 07c2528..5d93922 100644 --- a/ashi/ashi.js +++ b/ashi/ashi.js @@ -4,8 +4,52 @@ // // +const DENO_PROXY_PREFIX = "https://deno-proxies-sznvnpnxwhbv.deno.dev/?url="; +const ANIKAI_HOME_TITLE_REGEX = /Home - AnimeKai - Watch Free Anime Online, Stream Subbed & Dubbed Anime in HD<\/title>/i; +const ANIKAI_CHECK_TIMEOUT_MS = 900; +let animekaiBlockCheckPromise = null; + +function proxyUrl(url) { + return DENO_PROXY_PREFIX + encodeURIComponent(url); +} + +async function isAnimekaiBlockedForUser() { + if (!animekaiBlockCheckPromise) { + animekaiBlockCheckPromise = (async () => { + let timeoutId; + try { + const htmlText = await Promise.race([ + fetchv2("https://anikai.to/home").then(response => response.text()), + new Promise(resolve => { + timeoutId = setTimeout(() => resolve(""), ANIKAI_CHECK_TIMEOUT_MS); + }) + ]); + + if (timeoutId) { + clearTimeout(timeoutId); + } + + return !ANIKAI_HOME_TITLE_REGEX.test(htmlText); + } catch (error) { + if (timeoutId) { + clearTimeout(timeoutId); + } + console.error("Animekai accessibility check failed:" + error); + return true; + } + })(); + } + + return animekaiBlockCheckPromise; +} + async function searchResults(query) { const encodeQuery = keyword => encodeURIComponent(keyword); + const MIN_RESULTS_FAST_RETURN = 8; + const HIGH_CONFIDENCE_SCORE = 900; + const queryNormalized = query.toLowerCase().trim(); + const queryTokens = queryNormalized.split(/\s+/).filter(Boolean); + const isSpecificQuery = queryTokens.length <= 2 && queryNormalized.length >= 5; const decodeHtmlEntities = (str) => { if (!str) return str; @@ -59,6 +103,14 @@ async function searchResults(query) { if (!isStopword) significantMatches++; } else if (qToken.length >= 4 && tToken.length >= 4) { + if (Math.abs(qToken.length - tToken.length) > 2) { + return; + } + + if (qToken[0] !== tToken[0]) { + return; + } + const dist = levenshteinDistance(qToken, tToken); const maxLen = Math.max(qToken.length, tToken.length); const similarity = 1 - (dist / maxLen); @@ -149,6 +201,7 @@ async function searchResults(query) { const extractHrefRegex = /href="([^"]*)"/; const extractImageRegex = /data-src="([^"]*)"/; const extractTitleRegex = /title="([^"]*)"/; + let useProxy = true; const extractResultsFromHTML = (htmlText) => { const results = []; @@ -170,7 +223,7 @@ async function searchResults(query) { if (fullHref && imageSrc && cleanTitle) { results.push({ href: `Animekai:${fullHref}`, - image: "https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(imageSrc), + image: useProxy ? proxyUrl(imageSrc) : imageSrc, title: cleanTitle }); } @@ -181,21 +234,33 @@ async function searchResults(query) { try { const encodedQuery = encodeQuery(query); - const urls = [ - `${searchBaseUrl}${encodedQuery}`, - `${searchBaseUrl}${encodedQuery}&page=2`, - `${searchBaseUrl}${encodedQuery}&page=3` - ]; + useProxy = await isAnimekaiBlockedForUser(); + const fetchPages = async (pages) => { + const urls = pages.map(page => + page === 1 + ? `${searchBaseUrl}${encodedQuery}` + : `${searchBaseUrl}${encodedQuery}&page=${page}` + ); - const responses = await Promise.all(urls.map(url => fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(url)))); - const htmlTexts = await Promise.all(responses.map(res => res.text())); + const responses = await Promise.all(urls.map(url => fetchv2(useProxy ? proxyUrl(url) : url))); + const htmlTexts = await Promise.all(responses.map(res => res.text())); - const allResults = []; - htmlTexts.forEach(html => allResults.push(...extractResultsFromHTML(html))); - return allResults; + const allResults = []; + htmlTexts.forEach(html => allResults.push(...extractResultsFromHTML(html))); + return allResults; + }; + + const page1Results = await fetchPages([1]); + return { + page1Results, + fetchRemaining: () => fetchPages([2, 3]) + }; } catch (error) { console.error("Animekai search error:" + error); - return []; + return { + page1Results: [], + fetchRemaining: async () => [] + }; } }; @@ -228,41 +293,123 @@ async function searchResults(query) { try { const encodedQuery = encodeQuery(query); - const urls = [ - `${searchBaseUrl}${encodedQuery}`, - `${searchBaseUrl}${encodedQuery}&page=2`, - `${searchBaseUrl}${encodedQuery}&page=3` - ]; + const fetchPages = async (pages) => { + const urls = pages.map(page => + page === 1 + ? `${searchBaseUrl}${encodedQuery}` + : `${searchBaseUrl}${encodedQuery}&page=${page}` + ); - const responses = await Promise.all(urls.map(url => fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(url)))); - const htmlTexts = await Promise.all(responses.map(res => res.text())); + const responses = await Promise.all(urls.map(url => fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(url)))); + const htmlTexts = await Promise.all(responses.map(res => res.text())); - const allResults = []; - htmlTexts.forEach(html => allResults.push(...extractResultsFromHTML(html))); - return allResults; + const allResults = []; + htmlTexts.forEach(html => allResults.push(...extractResultsFromHTML(html))); + return allResults; + }; + + const page1Results = await fetchPages([1]); + return { + page1Results, + fetchRemaining: () => fetchPages([2, 3]) + }; } catch (error) { console.error("1Movies search error:" + error); - return []; + return { + page1Results: [], + fetchRemaining: async () => [] + }; } }; try { - const [animekaiResults, oneMoviesResults] = await Promise.all([ - animekaiSearch(), - oneMoviesSearch() + const animekaiPromise = animekaiSearch(); + + const animekaiData = await animekaiPromise; + + const dedupeResults = (results) => { + const seen = new Set(); + return results.filter(item => { + const key = `${item.href}|${item.title}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + }; + + const scoreCache = new Map(); + const rankResults = (results) => { + const scoredResults = results.map(r => { + const cacheKey = r.title; + const cached = scoreCache.get(cacheKey); + const score = cached !== undefined ? cached : fuzzyMatch(query, r.title); + + if (cached === undefined) { + scoreCache.set(cacheKey, score); + } + + return { + ...r, + score + }; + }); + + const sorted = scoredResults + .filter(r => r.score > 50) + .sort((a, b) => b.score - a.score); + + return { + topScore: sorted[0]?.score || 0, + items: sorted.map(({ score, ...rest }) => rest) + }; + }; + + const shouldFastReturn = (resultCount, topScore) => { + if (resultCount >= MIN_RESULTS_FAST_RETURN) { + return true; + } + + return isSpecificQuery && resultCount >= 1 && topScore >= HIGH_CONFIDENCE_SCORE; + }; + + let mergedResults = dedupeResults([...animekaiData.page1Results]); + + let rankedResults = rankResults(mergedResults); + let filteredResults = rankedResults.items; + + if (shouldFastReturn(filteredResults.length, rankedResults.topScore)) { + return JSON.stringify(filteredResults); + } + + const oneMoviesData = await oneMoviesSearch(); + + mergedResults = dedupeResults([ + ...mergedResults, + ...oneMoviesData.page1Results ]); - const mergedResults = [...animekaiResults, ...oneMoviesResults]; + rankedResults = rankResults(mergedResults); + filteredResults = rankedResults.items; - const scoredResults = mergedResults.map(r => ({ - ...r, - score: fuzzyMatch(query, r.title) - })); + if (shouldFastReturn(filteredResults.length, rankedResults.topScore)) { + return JSON.stringify(filteredResults); + } - const filteredResults = scoredResults - .filter(r => r.score > 50) - .sort((a, b) => b.score - a.score) - .map(({ score, ...rest }) => rest); + const [animekaiExtra, oneMoviesExtra] = await Promise.all([ + animekaiData.fetchRemaining(), + oneMoviesData.fetchRemaining() + ]); + + mergedResults = dedupeResults([ + ...mergedResults, + ...animekaiExtra, + ...oneMoviesExtra + ]); + + rankedResults = rankResults(mergedResults); + filteredResults = rankedResults.items; return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{ href: "", @@ -284,7 +431,8 @@ async function extractDetails(url) { const actualUrl = url.replace("Animekai:", "").trim(); try { - const response = await fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(actualUrl)); + const useProxy = await isAnimekaiBlockedForUser(); + const response = await fetchv2(useProxy ? proxyUrl(actualUrl) : actualUrl); const htmlText = await response.text(); const descriptionMatch = (/<div class="desc text-expand">([\s\S]*?)<\/div>/.exec(htmlText) || [])[1]; @@ -332,7 +480,8 @@ async function extractEpisodes(url) { try { if (url.startsWith("Animekai:")) { const actualUrl = url.replace("Animekai:", "").trim(); - const htmlText = await (await fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(actualUrl))).text(); + const useProxy = await isAnimekaiBlockedForUser(); + const htmlText = await (await fetchv2(useProxy ? proxyUrl(actualUrl) : actualUrl)).text(); const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1]; if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]); @@ -341,7 +490,7 @@ async function extractEpisodes(url) { const token = tokenData.result; const episodeListUrl = `https://anikai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`; - const episodeListData = await (await fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(episodeListUrl))).json(); + const episodeListData = await (await fetchv2(useProxy ? proxyUrl(episodeListUrl) : episodeListUrl)).json(); const cleanedHtml = cleanJsonHtml(episodeListData.result); const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g; @@ -398,6 +547,7 @@ async function extractStreamUrl(url) { if (source === "Animekai") { try { + const useProxy = await isAnimekaiBlockedForUser(); const tokenMatch = actualUrl.match(/token=([^&]+)/); if (tokenMatch && tokenMatch[1]) { const rawToken = tokenMatch[1]; @@ -407,7 +557,7 @@ async function extractStreamUrl(url) { actualUrl = actualUrl.replace('&_=ENCRYPT_ME', `&_=${encryptedToken}`); } - const response = await fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(actualUrl)); + const response = await fetchv2(useProxy ? proxyUrl(actualUrl) : actualUrl); const text = await response.text(); const cleanedHtml = cleanJsonHtml(text); const subRegex = /<div class="server-items lang-group" data-id="sub"[^>]*>([\s\S]*?)<\/div>/; @@ -454,7 +604,7 @@ async function extractStreamUrl(url) { const streamResponses = await Promise.all( streamUrls.map(async ({ type, url }) => { try { - const res = await fetchv2("https://deno-proxies-sznvnpnxwhbv.deno.dev/?url=" + encodeURIComponent(url)); + const res = await fetchv2(useProxy ? proxyUrl(url) : url); const json = await res.json(); return { type: type,