import { SamehadakuScraper } from '#scraper/samehadaku.js' import * as AnimeIndo from '#scraper/animeindo.js' import { searchAnimeLovers, getAnimeLoversDetail, getAnimeLoversStream } from '#system/lib/scraper/animelovers.js' const ANIME_ALIASES = { 'tenkosaki': 'tenkou', 'tenkousaki': 'tenkou', 'roshidere': 'alya', 'alya-san': 'alya', 'alya san': 'alya', 'bokuyaba': 'boku no kokoro', 'kimetsu': 'demon slayer', 'kny': 'demon slayer', 'snk': 'shingeki no kyojin', 'aot': 'shingeki no kyojin', 'jjk': 'jujutsu kaisen', 'mha': 'my hero academia', 'op': 'one piece', 'jojos': 'jojo', 'frieren': 'sousou no frieren', 'sololev': 'solo leveling', 'chainsaw': 'chainsaw man', 'csm': 'chainsaw man', 'slime': 'tensei shitara slime' } async function samehadakuSearch(query) { try { const s = new SamehadakuScraper() const res = await s.search(query) return (res.data || []).map(r => ({ source: 'samehadaku', title: r.title, url: r.url, image: r.thumbnail || '', rating: r.score || '', type: r.type || 'TV', })) } catch { return [] } } async function animeIndoSearch(query) { try { const res = await AnimeIndo.searchAnime(query) return (res || []).map(r => ({ source: 'animeindo', title: r.title, url: r.url, image: r.image || '', type: r.type || '', })) } catch { return [] } } async function animeLoversSearchEngine(query) { try { const qClean = query.toLowerCase().trim() const alias = ANIME_ALIASES[qClean] || (Object.keys(ANIME_ALIASES).find(k => qClean.includes(k)) ? ANIME_ALIASES[Object.keys(ANIME_ALIASES).find(k => qClean.includes(k))] : null) let list = await searchAnimeLovers(query) if (!list.length && alias) { list = await searchAnimeLovers(alias) } if (!list.length && qClean.length >= 6) { const rootWord = qClean.slice(0, 6).replace(/saki$/, '').replace(/ko$/, 'kou') if (rootWord !== qClean) { list = await searchAnimeLovers(rootWord) } } return (list || []).map(r => ({ source: 'animelovers', title: r.title, url: r.slug, image: r.cover || '', rating: r.score || '', status: r.status || '', type: 'TV' })) } catch { return [] } } export async function searchAnime(query) { const [animelovers, samehadaku, animeindo] = await Promise.allSettled([ animeLoversSearchEngine(query), samehadakuSearch(query), animeIndoSearch(query), ]) const results = [ ...(animelovers.status === 'fulfilled' ? animelovers.value : []), ...(samehadaku.status === 'fulfilled' ? samehadaku.value : []), ...(animeindo.status === 'fulfilled' ? animeindo.value : []), ] const seen = new Set() return results.filter(r => { if (!r.url || seen.has(r.url)) return false seen.add(r.url) return true }) } export async function getAnimeDetail(source, url) { if (source === 'animelovers') { const d = await getAnimeLoversDetail(url) return { source, title: d.title, image: d.cover || '', rating: d.rating || '', status: d.status || '', studio: d.author || '', genres: (d.genres || []).join(', '), synopsis: d.synopsis || '', episodes: (d.episodes || []).map(e => ({ number: parseInt(String(e.episode).replace(/\D/g, '')) || null, title: `Episode ${e.episode}`, url: `${url}|${e.episode}`, date: e.date || '' })) } } if (source === 'samehadaku') { const s = new SamehadakuScraper() const d = await s.anime(url) return { source, title: d.title, image: d.poster || '', rating: d.score || '', status: d.status || '', studio: d.metadata?.studio || '', genres: d.metadata?.genre || '', synopsis: d.synopsis || '', episodes: (d.episodes || []).map(e => ({ number: parseInt(String(e.title).replace(/\D/g, '')) || null, title: e.title, url: e.url, date: e.date || '', })), } } if (source === 'animeindo') { const d = await AnimeIndo.getAnimeDetail(url) return { source, title: d.title, image: d.image || '', rating: '', status: '', studio: '', genres: d.genres || [], synopsis: d.synopsis || '', episodes: (d.episodes || []).map(e => ({ number: e.number, title: e.title, url: e.url, date: e.date || '', })), } } throw new Error(`Detail tidak didukung untuk sumber ${source}.`) } export async function getEpisode(source, url, chosenResolution = null) { if (source === 'animelovers') { const [slug, epNum] = url.split('|') const stream = await getAnimeLoversStream(slug, epNum || '1') const availableResolutions = Object.keys(stream.streams || {}).filter( r => Array.isArray(stream.streams[r]) && stream.streams[r].length > 0 ) let targetReso = chosenResolution if (!targetReso || !stream.streams[targetReso]?.length) { targetReso = ['720p', '480p', '360p', '1080p'].find(r => stream.streams[r]?.length) || availableResolutions[0] } const servers = stream.streams[targetReso] || [] const directVideos = servers.map(s => s.url).filter(Boolean) const allLinks = [] for (const resKey of availableResolutions) { for (const srv of stream.streams[resKey] || []) { if (srv.url) allLinks.push(srv.url) } } return { source, title: `${stream.title} Episode ${stream.episode}`, image: stream.cover, episodeNumber: String(stream.episode).match(/\d+/)?.[0] || '1', selectedResolution: targetReso, availableResolutions, streamsByResolution: stream.streams, directVideo: directVideos[0] || allLinks[0] || null, downloadLinks: directVideos.length ? directVideos : allLinks } } if (source === 'samehadaku') { const s = new SamehadakuScraper() const ep = await s.episode(url) const qualityLinks = [] for (const [format, sections] of Object.entries(ep.downloads || {})) { for (const sec of sections) { for (const mirror of (sec.mirrors || [])) { if (mirror.url) qualityLinks.push(mirror.url) } } } return { source, title: ep.title, selectedResolution: '720p', availableResolutions: ['360p', '480p', '720p'], directVideo: ep.iframe || qualityLinks[0] || null, downloadLinks: qualityLinks.length ? qualityLinks : (ep.downloadLinks || []) } } if (source === 'animeindo') { const ep = await AnimeIndo.getEpisode(url) return { source, title: ep.title, selectedResolution: '720p', availableResolutions: ['360p', '480p', '720p'], directVideo: ep.iframe || null, downloadLinks: (ep.downloadLinks || []).map(l => l.url).filter(Boolean) } } throw new Error(`Episode tidak didukung untuk sumber ${source}.`) } export default { searchAnime, getAnimeDetail, getEpisode }