added anime entries from extensions

This commit is contained in:
2025-11-28 22:22:23 +01:00
parent 03ebb5d88e
commit 09a89507e7
8 changed files with 180 additions and 32 deletions

View File

@@ -4,7 +4,19 @@ const { getExtension, getExtensionsList } = require('../shared/extensions');
async function getAnime(req, reply) { async function getAnime(req, reply) {
try { try {
const { id } = req.params; 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; return anime;
} catch (err) { } catch (err) {
return { error: "Database error" }; return { error: "Database error" };
@@ -29,11 +41,18 @@ async function getTopAiring(req, reply) {
} }
} }
async function searchLocal(req, reply) { async function search(req, reply) {
try { try {
const query = req.query.q; const query = req.query.q;
const results = await animeService.searchAnimeLocal(query); 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) { } catch (err) {
return { results: [] }; return { results: [] };
} }
@@ -63,24 +82,30 @@ async function getWatchStream(req, reply) {
const { animeId, episode, server, category, ext } = req.query; const { animeId, episode, server, category, ext } = req.query;
const extension = getExtension(ext); const extension = getExtension(ext);
if (!extension) { if (!extension) return { error: "Extension not found" };
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); return await animeService.getStreamData(
if (animeData.error) {
return { error: "Anime metadata not found" };
}
const streamData = await animeService.getStreamData(
extension, extension,
animeData, anime,
episode, episode,
server, server,
category category
); );
return streamData;
} catch (err) { } catch (err) {
return { error: err.message }; return { error: err.message };
} }
@@ -90,7 +115,7 @@ module.exports = {
getAnime, getAnime,
getTrending, getTrending,
getTopAiring, getTopAiring,
searchLocal, search,
getExtensions, getExtensions,
getExtensionSettings, getExtensionSettings,
getWatchStream getWatchStream

View File

@@ -1,11 +1,10 @@
const controller = require('./anime.controller'); const controller = require('./anime.controller');
async function animeRoutes(fastify, options) { async function animeRoutes(fastify, options) {
fastify.get('/anime/:id', controller.getAnime); fastify.get('/anime/:id', controller.getAnime);
fastify.get('/trending', controller.getTrending); fastify.get('/trending', controller.getTrending);
fastify.get('/top-airing', controller.getTopAiring); fastify.get('/top-airing', controller.getTopAiring);
fastify.get('/search/local', controller.searchLocal); fastify.get('/search', controller.search);
fastify.get('/extensions', controller.getExtensions); fastify.get('/extensions', controller.getExtensions);
fastify.get('/extension/:name/settings', controller.getExtensionSettings); fastify.get('/extension/:name/settings', controller.getExtensionSettings);
fastify.get('/watch/stream', controller.getWatchStream); fastify.get('/watch/stream', controller.getWatchStream);

View File

@@ -1,4 +1,5 @@
const { queryOne, queryAll } = require('../shared/database'); const { queryOne, queryAll } = require('../shared/database');
const {getAllExtensions} = require("../shared/extensions");
async function getAnimeById(id) { async function getAnimeById(id) {
const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [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); 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) { async function getStreamData(extension, animeData, episode, server, category) {
const searchOptions = { const searchOptions = {
query: animeData.title.english || animeData.title.romaji, query: animeData.title.english || animeData.title.romaji,
@@ -81,5 +124,7 @@ module.exports = {
getTrendingAnime, getTrendingAnime,
getTopAiringAnime, getTopAiringAnime,
searchAnimeLocal, searchAnimeLocal,
searchAnimeExtensions,
searchAnimeInExtension,
getStreamData getStreamData
}; };

View File

@@ -10,9 +10,25 @@ tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0]; var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
let extensionName;
async function loadAnime() { async function loadAnime() {
try { 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(); const data = await res.json();
if(data.error) { 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('genres').innerText = data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : '';
document.getElementById('format').innerText = data.format || 'TV'; document.getElementById('format').innerText = data.format || 'TV';
document.getElementById('status').innerText = data.status || 'Unknown'; 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 = ''; let seasonText = '';
if (data.season && data.seasonYear) { if (data.season && data.seasonYear) {
@@ -180,7 +203,9 @@ function createEpisodeButton(num, container) {
const btn = document.createElement('div'); const btn = document.createElement('div');
btn.className = 'episode-btn'; btn.className = 'episode-btn';
btn.innerText = `Ep ${num}`; 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); container.appendChild(btn);
} }

View File

@@ -12,7 +12,7 @@ searchInput.addEventListener('input', (e) => {
return; return;
} }
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
fetchLocalSearch(query); fetchSearh(query);
}, 300); }, 300);
}); });
@@ -23,14 +23,25 @@ document.addEventListener('click', (e) => {
} }
}); });
async function fetchLocalSearch(query) { async function fetchSearh(query) {
try { 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(); const data = await res.json();
renderSearchResults(data.results); renderSearchResults(data.results);
} catch (err) { console.error("Search Error:", err); } } 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) { function renderSearchResults(results) {
searchResults.innerHTML = ''; searchResults.innerHTML = '';
if (results.length === 0) { if (results.length === 0) {
@@ -43,9 +54,24 @@ function renderSearchResults(results) {
const year = anime.seasonYear || ''; const year = anime.seasonYear || '';
const format = anime.format || 'TV'; 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
? `<span>• ${extName}</span>`
: '';
const item = document.createElement('a'); const item = document.createElement('a');
item.className = 'search-item'; item.className = 'search-item';
item.href = `/anime/${anime.id}`; item.href = href;
item.innerHTML = ` item.innerHTML = `
<img src="${img}" class="search-poster" alt="${title}"> <img src="${img}" class="search-poster" alt="${title}">
<div class="search-info"> <div class="search-info">
@@ -54,6 +80,7 @@ function renderSearchResults(results) {
<span class="rating-pill">${rating}</span> <span class="rating-pill">${rating}</span>
<span>• ${year}</span> <span>• ${year}</span>
<span>• ${format}</span> <span>• ${format}</span>
${extPill}
</div> </div>
</div> </div>
`; `;

View File

@@ -7,7 +7,16 @@ let currentExtension = '';
let plyrInstance; let plyrInstance;
let hlsInstance; 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}`; document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`;
async function loadMetadata() { async function loadMetadata() {
@@ -29,18 +38,26 @@ async function loadExtensions() {
const select = document.getElementById('extension-select'); const select = document.getElementById('extension-select');
if (data.extensions && data.extensions.length > 0) { if (data.extensions && data.extensions.length > 0) {
data.extensions.forEach(extName => { data.extensions.forEach(ext => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = extName; opt.value = opt.innerText = ext;
opt.innerText = extName;
select.appendChild(opt); select.appendChild(opt);
}); });
if (data.extensions.includes(extName ?? "")) {
select.value = extName;
currentExtension = extName;
onExtensionChange();
}
} else { } else {
select.innerHTML = '<option>No Extensions</option>'; select.innerHTML = '<option>No Extensions</option>';
select.disabled = true; select.disabled = true;
setLoading("No extensions found in WaifuBoards folder."); setLoading("No extensions found in WaifuBoards folder.");
} }
} catch(e) { console.error("Extension Error:", e); }
} catch(e) {
console.error("Extension Error:", e);
}
} }
async function onExtensionChange() { async function onExtensionChange() {
@@ -114,7 +131,7 @@ async function loadStream() {
setLoading(`Searching & Resolving Stream (${audioMode})...`); setLoading(`Searching & Resolving Stream (${audioMode})...`);
try { 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 res = await fetch(url);
const data = await res.json(); const data = await res.json();
@@ -199,11 +216,15 @@ function setLoading(msg) {
text.innerText = msg; text.innerText = msg;
} }
const extParam = extName ? `?${extName}` : "";
document.getElementById('prev-btn').onclick = () => { 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 = () => { 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; if(currentEpisode <= 1) document.getElementById('prev-btn').disabled = true;

View File

@@ -18,6 +18,11 @@ async function viewsRoutes(fastify, options) {
reply.type('text/html').send(stream); 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) => { fastify.get('/watch/:id/:episode', (req, reply) => {
const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'watch.html')); const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'watch.html'));
reply.type('text/html').send(stream); reply.type('text/html').send(stream);

View File

@@ -80,6 +80,7 @@
<h1 class="anime-title" id="title">Loading...</h1> <h1 class="anime-title" id="title">Loading...</h1>
<div class="meta-row"> <div class="meta-row">
<div class="pill extension-pill" id="extension-pill" style="display: none; background: #8b5cf6;"></div>
<div class="pill score" id="score">--% Score</div> <div class="pill score" id="score">--% Score</div>
<div class="pill" id="year">----</div> <div class="pill" id="year">----</div>
<div class="pill" id="genres">Action</div> <div class="pill" id="genres">Action</div>
@@ -127,6 +128,6 @@
</div> </div>
<script src="../src/scripts/anime/anime.js"></script> <script src="/src/scripts/anime/anime.js"></script>
</body> </body>
</html> </html>