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 = `