added anime entries from extensions
This commit is contained in:
@@ -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" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const animeData = await animeService.getAnimeById(animeId);
|
let anime;
|
||||||
if (animeData.error) {
|
if (!isNaN(Number(animeId))) {
|
||||||
return { error: "Anime metadata not found" };
|
anime = await animeService.getAnimeById(animeId);
|
||||||
|
if (anime.error) return { error: "Anime metadata not found" };
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
const streamData = await animeService.getStreamData(
|
const results = await animeService.searchAnimeInExtension(
|
||||||
extension,
|
extension,
|
||||||
animeData,
|
ext,
|
||||||
|
animeId.replaceAll("-", " ")
|
||||||
|
);
|
||||||
|
anime = results[0];
|
||||||
|
if (!anime) return { error: "Anime not found in extension search" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await animeService.getStreamData(
|
||||||
|
extension,
|
||||||
|
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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user