diff --git a/src/anime/anime.controller.js b/src/anime/anime.controller.js index aa969c1..f8ed23d 100644 --- a/src/anime/anime.controller.js +++ b/src/anime/anime.controller.js @@ -4,7 +4,19 @@ const { getExtension, getExtensionsList } = require('../shared/extensions'); async function getAnime(req, reply) { try { const { id } = req.params; - const anime = await animeService.getAnimeById(id); + const source = req.query.ext || 'anilist'; + + let anime; + if (source === 'anilist') { + anime = await animeService.getAnimeById(id); + } else { + const extensionName = source; + const ext = getExtension(extensionName); + + const results = await animeService.searchAnimeInExtension(ext, extensionName, id.replaceAll("-", " ")); + anime = results[0] || null; + } + return anime; } catch (err) { return { error: "Database error" }; @@ -29,11 +41,18 @@ async function getTopAiring(req, reply) { } } -async function searchLocal(req, reply) { +async function search(req, reply) { try { const query = req.query.q; const results = await animeService.searchAnimeLocal(query); - return { results }; + + if (results.length > 0) { + return { results: results }; + } + + const extResults = await animeService.searchAnimeExtensions(query); + return { results: extResults }; + } catch (err) { return { results: [] }; } @@ -63,24 +82,30 @@ async function getWatchStream(req, reply) { const { animeId, episode, server, category, ext } = req.query; const extension = getExtension(ext); - if (!extension) { - return { error: "Extension not found" }; + if (!extension) return { error: "Extension not found" }; + + let anime; + if (!isNaN(Number(animeId))) { + anime = await animeService.getAnimeById(animeId); + if (anime.error) return { error: "Anime metadata not found" }; + } + else { + const results = await animeService.searchAnimeInExtension( + extension, + ext, + animeId.replaceAll("-", " ") + ); + anime = results[0]; + if (!anime) return { error: "Anime not found in extension search" }; } - const animeData = await animeService.getAnimeById(animeId); - if (animeData.error) { - return { error: "Anime metadata not found" }; - } - - const streamData = await animeService.getStreamData( + return await animeService.getStreamData( extension, - animeData, + anime, episode, server, category ); - - return streamData; } catch (err) { return { error: err.message }; } @@ -90,7 +115,7 @@ module.exports = { getAnime, getTrending, getTopAiring, - searchLocal, + search, getExtensions, getExtensionSettings, getWatchStream diff --git a/src/anime/anime.routes.js b/src/anime/anime.routes.js index 9439543..1c2c712 100644 --- a/src/anime/anime.routes.js +++ b/src/anime/anime.routes.js @@ -1,11 +1,10 @@ const controller = require('./anime.controller'); async function animeRoutes(fastify, options) { - fastify.get('/anime/:id', controller.getAnime); fastify.get('/trending', controller.getTrending); fastify.get('/top-airing', controller.getTopAiring); - fastify.get('/search/local', controller.searchLocal); + fastify.get('/search', controller.search); fastify.get('/extensions', controller.getExtensions); fastify.get('/extension/:name/settings', controller.getExtensionSettings); fastify.get('/watch/stream', controller.getWatchStream); diff --git a/src/anime/anime.service.js b/src/anime/anime.service.js index 29e4457..2d4f2ab 100644 --- a/src/anime/anime.service.js +++ b/src/anime/anime.service.js @@ -1,4 +1,5 @@ const { queryOne, queryAll } = require('../shared/database'); +const {getAllExtensions} = require("../shared/extensions"); async function getAnimeById(id) { const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]); @@ -45,6 +46,48 @@ async function searchAnimeLocal(query) { return cleanResults.slice(0, 10); } +async function searchAnimeInExtension(ext, name, query) { + if ((ext.type === 'anime-board') && ext.search) { + try { + console.log(`[${name}] Searching for book: ${query}`); + const matches = await ext.search({ + query: query, + media: { + romajiTitle: query, + englishTitle: query, + startDate: { year: 0, month: 0, day: 0 } + } + }); + + if (matches && matches.length > 0) { + return matches.map(m => ({ + id: m.id, + extensionName: name, + title: { romaji: m.title, english: m.title }, + coverImage: { large: m.image || '' }, + averageScore: m.rating || m.score || null, + format: 'ANIME', + seasonYear: null, + isExtensionResult: true + })); + } + } catch (e) { + console.error(`Extension search failed for ${name}:`, e); + } + } +} + +async function searchAnimeExtensions(query) { + const extensions = getAllExtensions(); + + for (const [name, ext] of extensions) { + const results = await searchAnimeInExtension(ext, name, query); + if (results.length > 0) return results; + } + + return []; +} + async function getStreamData(extension, animeData, episode, server, category) { const searchOptions = { query: animeData.title.english || animeData.title.romaji, @@ -81,5 +124,7 @@ module.exports = { getTrendingAnime, getTopAiringAnime, searchAnimeLocal, + searchAnimeExtensions, + searchAnimeInExtension, getStreamData }; \ No newline at end of file diff --git a/src/scripts/anime/anime.js b/src/scripts/anime/anime.js index 2614ced..7f7817d 100644 --- a/src/scripts/anime/anime.js +++ b/src/scripts/anime/anime.js @@ -10,9 +10,25 @@ tag.src = "https://www.youtube.com/iframe_api"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); +let extensionName; + async function loadAnime() { try { - const res = await fetch(`/api/anime/${animeId}`); + const path = window.location.pathname; + const parts = path.split("/").filter(Boolean); + let animeId; + + if (parts.length === 3) { + extensionName = parts[1]; + animeId = parts[2]; + } else { + animeId = parts[1]; + } + + const fetchUrl = extensionName + ? `/api/anime/${animeId.slice(0,40)}?ext=${extensionName}` + : `/api/anime/${animeId}`; + const res = await fetch(fetchUrl); const data = await res.json(); if(data.error) { @@ -35,6 +51,13 @@ async function loadAnime() { document.getElementById('genres').innerText = data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : ''; document.getElementById('format').innerText = data.format || 'TV'; document.getElementById('status').innerText = data.status || 'Unknown'; + const extensionPill = document.getElementById('extension-pill'); + if (extensionName && extensionPill) { + extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`; + extensionPill.style.display = 'inline-flex'; + } else if (extensionPill) { + extensionPill.style.display = 'none'; + } let seasonText = ''; if (data.season && data.seasonYear) { @@ -180,7 +203,9 @@ function createEpisodeButton(num, container) { const btn = document.createElement('div'); btn.className = 'episode-btn'; btn.innerText = `Ep ${num}`; - btn.onclick = () => window.location.href = `/watch/${animeId}/${num}`; + btn.onclick = () => + window.location.href = `/watch/${animeId}/${num}` + (extensionName ? `?${extensionName}` : ""); + container.appendChild(btn); } diff --git a/src/scripts/anime/animes.js b/src/scripts/anime/animes.js index eccb71b..d355984 100644 --- a/src/scripts/anime/animes.js +++ b/src/scripts/anime/animes.js @@ -12,7 +12,7 @@ searchInput.addEventListener('input', (e) => { return; } searchTimeout = setTimeout(() => { - fetchLocalSearch(query); + fetchSearh(query); }, 300); }); @@ -23,14 +23,25 @@ document.addEventListener('click', (e) => { } }); -async function fetchLocalSearch(query) { +async function fetchSearh(query) { try { - const res = await fetch(`/api/search/local?q=${encodeURIComponent(query)}`); + const res = await fetch(`/api/search?q=${encodeURIComponent(query.slice(0, 30))}`); const data = await res.json(); renderSearchResults(data.results); } catch (err) { console.error("Search Error:", err); } } +function createSlug(text) { + if (!text) return ''; + return text + .replace(/([a-z])([A-Z])/g, '$1 $2') // separa CamelCase + .replace(/([a-z])(\d)/g, '$1 $2') // separa letras de números + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/[\s-]+/g, '-'); +} + function renderSearchResults(results) { searchResults.innerHTML = ''; if (results.length === 0) { @@ -43,9 +54,24 @@ function renderSearchResults(results) { const year = anime.seasonYear || ''; const format = anime.format || 'TV'; + let href; + + if (anime.isExtensionResult) { + const titleSlug = createSlug(title); + console.log(title); + href = `/anime/${anime.extensionName}/${titleSlug}`; + } else { + href = `/anime/${anime.id}`; + } + + const extName = anime.extensionName?.charAt(0).toUpperCase() + anime.extensionName?.slice(1); + const extPill = anime.isExtensionResult + ? `• ${extName}` + : ''; + const item = document.createElement('a'); item.className = 'search-item'; - item.href = `/anime/${anime.id}`; + item.href = href; item.innerHTML = ` ${title}
@@ -54,6 +80,7 @@ function renderSearchResults(results) { ${rating} • ${year} • ${format} + ${extPill}
`; diff --git a/src/scripts/anime/player.js b/src/scripts/anime/player.js index a053b54..8db8949 100644 --- a/src/scripts/anime/player.js +++ b/src/scripts/anime/player.js @@ -7,7 +7,16 @@ let currentExtension = ''; let plyrInstance; let hlsInstance; -document.getElementById('back-link').href = `/anime/${animeId}`; +const params = new URLSearchParams(window.location.search); +const firstKey = params.keys().next().value; +let extName; +if (firstKey) extName = firstKey; + +const href = extName + ? `/anime/${extName}/${animeId}` + : `/anime/${animeId}`; + +document.getElementById('back-link').href = href; document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`; async function loadMetadata() { @@ -29,18 +38,26 @@ async function loadExtensions() { const select = document.getElementById('extension-select'); if (data.extensions && data.extensions.length > 0) { - data.extensions.forEach(extName => { + data.extensions.forEach(ext => { const opt = document.createElement('option'); - opt.value = extName; - opt.innerText = extName; + opt.value = opt.innerText = ext; select.appendChild(opt); }); + + if (data.extensions.includes(extName ?? "")) { + select.value = extName; + currentExtension = extName; + onExtensionChange(); + } } else { select.innerHTML = ''; select.disabled = true; setLoading("No extensions found in WaifuBoards folder."); } - } catch(e) { console.error("Extension Error:", e); } + + } catch(e) { + console.error("Extension Error:", e); + } } async function onExtensionChange() { @@ -114,7 +131,7 @@ async function loadStream() { setLoading(`Searching & Resolving Stream (${audioMode})...`); try { - const url = `/api/watch/stream?animeId=${animeId}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`; + const url = `/api/watch/stream?animeId=${animeId.slice(0, 30)}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`; const res = await fetch(url); const data = await res.json(); @@ -199,11 +216,15 @@ function setLoading(msg) { text.innerText = msg; } +const extParam = extName ? `?${extName}` : ""; document.getElementById('prev-btn').onclick = () => { - if(currentEpisode > 1) window.location.href = `/watch/${animeId}/${currentEpisode - 1}`; + if (currentEpisode > 1) { + window.location.href = `/watch/${animeId}/${currentEpisode - 1}${extParam}`; + } }; + document.getElementById('next-btn').onclick = () => { - window.location.href = `/watch/${animeId}/${currentEpisode + 1}`; + window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; }; if(currentEpisode <= 1) document.getElementById('prev-btn').disabled = true; diff --git a/src/views/views.routes.js b/src/views/views.routes.js index b0ee3ae..9981ace 100644 --- a/src/views/views.routes.js +++ b/src/views/views.routes.js @@ -18,6 +18,11 @@ async function viewsRoutes(fastify, options) { reply.type('text/html').send(stream); }); + fastify.get('/anime/:extension/*', (req, reply) => { + const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'anime.html')); + reply.type('text/html').send(stream); + }); + fastify.get('/watch/:id/:episode', (req, reply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'watch.html')); reply.type('text/html').send(stream); diff --git a/views/anime.html b/views/anime.html index 0e464c4..70d68a4 100644 --- a/views/anime.html +++ b/views/anime.html @@ -80,6 +80,7 @@

Loading...

+
--% Score
----
Action
@@ -127,6 +128,6 @@
- + \ No newline at end of file