Merge branch 'dev' of https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Recode
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {FastifyReply, FastifyRequest} from 'fastify';
|
||||
import * as animeService from './anime.service';
|
||||
import { getExtension, getExtensionsList } from '../shared/extensions';
|
||||
import {AnimeRequest, SearchRequest, ExtensionNameRequest, WatchStreamRequest, Anime} from '../types';
|
||||
import {getExtension} from '../shared/extensions';
|
||||
import {Anime, AnimeRequest, SearchRequest, WatchStreamRequest} from '../types';
|
||||
|
||||
export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
|
||||
try {
|
||||
@@ -18,7 +18,7 @@ export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
|
||||
const results = await animeService.searchAnimeInExtension(
|
||||
ext,
|
||||
extensionName,
|
||||
id.replaceAll("-", " ")
|
||||
id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()
|
||||
);
|
||||
anime = results[0] || null;
|
||||
}
|
||||
@@ -29,6 +29,22 @@ export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAnimeEpisodes(req: AnimeRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const extensionName = req.query.ext || 'anilist';
|
||||
const ext = getExtension(extensionName);
|
||||
|
||||
return await animeService.searchChaptersInExtension(
|
||||
ext,
|
||||
extensionName,
|
||||
id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()
|
||||
);
|
||||
} catch (err) {
|
||||
return { error: "Database error" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTrending(req: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const results = await animeService.getTrendingAnime();
|
||||
@@ -64,25 +80,6 @@ export async function search(req: SearchRequest, reply: FastifyReply) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExtensions(req: FastifyRequest, reply: FastifyReply) {
|
||||
return { extensions: getExtensionsList() };
|
||||
}
|
||||
|
||||
export async function getExtensionSettings(req: ExtensionNameRequest, reply: FastifyReply) {
|
||||
const { name } = req.params;
|
||||
const ext = getExtension(name);
|
||||
|
||||
if (!ext) {
|
||||
return { error: "Extension not found" };
|
||||
}
|
||||
|
||||
if (!ext.getSettings) {
|
||||
return { episodeServers: ["default"], supportsDub: false };
|
||||
}
|
||||
|
||||
return ext.getSettings();
|
||||
}
|
||||
|
||||
export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { animeId, episode, server, category, ext } = req.query;
|
||||
@@ -98,7 +95,7 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl
|
||||
const results = await animeService.searchAnimeInExtension(
|
||||
extension,
|
||||
ext,
|
||||
animeId.replaceAll("-", " ")
|
||||
animeId.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()
|
||||
);
|
||||
anime = results[0];
|
||||
if (!anime) return { error: "Anime not found in extension search" };
|
||||
|
||||
@@ -3,11 +3,10 @@ import * as controller from './anime.controller';
|
||||
|
||||
async function animeRoutes(fastify: FastifyInstance) {
|
||||
fastify.get('/anime/:id', controller.getAnime);
|
||||
fastify.get('/anime/:id/:episodes', controller.getAnimeEpisodes);
|
||||
fastify.get('/trending', controller.getTrending);
|
||||
fastify.get('/top-airing', controller.getTopAiring);
|
||||
fastify.get('/search', controller.search);
|
||||
fastify.get('/extensions', controller.getExtensions);
|
||||
fastify.get('/extension/:name/settings', controller.getExtensionSettings);
|
||||
fastify.get('/watch/stream', controller.getWatchStream);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryOne, queryAll } from '../shared/database';
|
||||
import { getAllExtensions } from '../shared/extensions';
|
||||
import { Anime, Extension, StreamData } from '../types';
|
||||
import { getAnimeExtensionsMap } from '../shared/extensions';
|
||||
import {Anime, Episode, Extension, StreamData} from '../types';
|
||||
|
||||
export async function getAnimeById(id: string | number): Promise<Anime | { error: string }> {
|
||||
const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]);
|
||||
@@ -47,16 +47,12 @@ export async function searchAnimeLocal(query: string): Promise<Anime[]> {
|
||||
return cleanResults.slice(0, 10);
|
||||
}
|
||||
|
||||
export async function searchAnimeInExtension(
|
||||
ext: Extension | null,
|
||||
name: string,
|
||||
query: string
|
||||
): Promise<Anime[]> {
|
||||
export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string): Promise<Anime[]> {
|
||||
if (!ext) return [];
|
||||
|
||||
if ((ext.type === 'anime-board') && ext.search) {
|
||||
if (ext.type === 'anime-board' && ext.search) {
|
||||
try {
|
||||
console.log(`[${name}] Searching for book: ${query}`);
|
||||
console.log(`[${name}] Searching for anime: ${query}`);
|
||||
const matches = await ext.search({
|
||||
query: query,
|
||||
media: {
|
||||
@@ -86,10 +82,47 @@ export async function searchAnimeInExtension(
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function searchAnimeExtensions(query: string): Promise<Anime[]> {
|
||||
const extensions = getAllExtensions();
|
||||
export async function searchChaptersInExtension(ext: Extension | null, name: string, query: string): Promise<Episode[]> {
|
||||
if (!ext) return [];
|
||||
|
||||
for (const [name, ext] of extensions) {
|
||||
if (ext.type === "anime-board" && ext.search && typeof ext.findEpisodes === "function") {
|
||||
try {
|
||||
|
||||
const matches = await ext.search({
|
||||
query,
|
||||
media: {
|
||||
romajiTitle: query,
|
||||
englishTitle: query,
|
||||
startDate: { year: 0, month: 0, day: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
if (!matches || matches.length === 0) return [];
|
||||
|
||||
const res = matches[0];
|
||||
if (!res?.id) return [];
|
||||
|
||||
const chapterList = await ext.findEpisodes(res.id);
|
||||
if (!Array.isArray(chapterList)) return [];
|
||||
|
||||
return chapterList.map(ep => ({
|
||||
id: ep.id,
|
||||
number: ep.number,
|
||||
url: ep.url,
|
||||
title: ep.title
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(`Extension search failed for ${name}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function searchAnimeExtensions(query: string): Promise<Anime[]> {
|
||||
const animeExtensions = getAnimeExtensionsMap();
|
||||
|
||||
for (const [name, ext] of animeExtensions) {
|
||||
const results = await searchAnimeInExtension(ext, name, query);
|
||||
if (results.length > 0) return results;
|
||||
}
|
||||
@@ -97,13 +130,7 @@ export async function searchAnimeExtensions(query: string): Promise<Anime[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getStreamData(
|
||||
extension: Extension,
|
||||
animeData: Anime,
|
||||
episode: string,
|
||||
server?: string,
|
||||
category?: string
|
||||
): Promise<StreamData> {
|
||||
export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise<StreamData> {
|
||||
const searchOptions = {
|
||||
query: animeData.title.english || animeData.title.romaji,
|
||||
dub: category === 'dub',
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getBook(req: BookRequest, reply: FastifyReply) {
|
||||
const extensionName = source;
|
||||
const ext = getExtension(extensionName);
|
||||
|
||||
const results = await booksService.searchBooksInExtension(ext, extensionName, id.replaceAll("-", " "));
|
||||
const results = await booksService.searchBooksInExtension(ext, extensionName, id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim());
|
||||
book = results[0] || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { queryOne, queryAll } from '../shared/database';
|
||||
import { getAllExtensions } from '../shared/extensions';
|
||||
import { getAllExtensions, getBookExtensionsMap } from '../shared/extensions';
|
||||
import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types';
|
||||
|
||||
export async function getBookById(id: string | number): Promise<Book | { error: string }> {
|
||||
@@ -144,9 +144,9 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
|
||||
}
|
||||
|
||||
export async function searchBooksExtensions(query: string): Promise<Book[]> {
|
||||
const extensions = getAllExtensions();
|
||||
const bookExtensions = getBookExtensionsMap();
|
||||
|
||||
for (const [name, ext] of extensions) {
|
||||
for (const [name, ext] of bookExtensions) {
|
||||
const results = await searchBooksInExtension(ext, name, query);
|
||||
if (results.length > 0) return results;
|
||||
}
|
||||
@@ -154,12 +154,80 @@ export async function searchBooksExtensions(query: string): Promise<Book[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchBookMetadata(id: string): Promise<Book | null> {
|
||||
try {
|
||||
const query = `query ($id: Int) {
|
||||
Media(id: $id, type: MANGA) {
|
||||
title { romaji english }
|
||||
startDate { year month day }
|
||||
}
|
||||
}`;
|
||||
|
||||
const res = await fetch('https://graphql.anilist.co', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, variables: { id: parseInt(id) } })
|
||||
});
|
||||
|
||||
const d = await res.json();
|
||||
return d.data?.Media || null;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch book metadata:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, bookData: Book | null): Promise<ChapterWithProvider[]> {
|
||||
try {
|
||||
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
||||
|
||||
const matches = await ext.search!({
|
||||
query: searchTitle,
|
||||
media: bookData ? {
|
||||
romajiTitle: bookData.title.romaji,
|
||||
englishTitle: bookData.title.english || "",
|
||||
startDate: bookData.startDate || { year: 0, month: 0, day: 0 }
|
||||
} : {
|
||||
romajiTitle: searchTitle,
|
||||
englishTitle: searchTitle,
|
||||
startDate: { year: 0, month: 0, day: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
if (!matches?.length) {
|
||||
console.log(`[${name}] No matches found for book.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const best = matches[0];
|
||||
const chaps = await ext.findChapters!(best.id);
|
||||
|
||||
if (!chaps?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[${name}] Found ${chaps.length} chapters.`);
|
||||
|
||||
return chaps.map((ch) => ({
|
||||
id: ch.id,
|
||||
number: parseFloat(ch.number.toString()),
|
||||
title: ch.title,
|
||||
date: ch.releaseDate,
|
||||
provider: name
|
||||
}));
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
console.error(`Failed to fetch chapters from ${name}:`, error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getChaptersForBook(id: string): Promise<{ chapters: ChapterWithProvider[] }> {
|
||||
let bookData: Book | null = null;
|
||||
let searchTitle: string | null = null;
|
||||
|
||||
if (typeof id === "string" && isNaN(Number(id))) {
|
||||
searchTitle = id.replaceAll("-", " ");
|
||||
if (isNaN(Number(id))) {
|
||||
searchTitle = id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim();
|
||||
} else {
|
||||
const result = await getBookById(id);
|
||||
if (!('error' in result)) {
|
||||
@@ -167,80 +235,32 @@ export async function getChaptersForBook(id: string): Promise<{ chapters: Chapte
|
||||
}
|
||||
|
||||
if (!bookData) {
|
||||
try {
|
||||
const query = `query ($id: Int) {
|
||||
Media(id: $id, type: MANGA) {
|
||||
title { romaji english }
|
||||
startDate { year month day }
|
||||
}
|
||||
}`;
|
||||
|
||||
const res = await fetch('https://graphql.anilist.co', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, variables: { id: parseInt(id) } })
|
||||
});
|
||||
|
||||
const d = await res.json();
|
||||
if (d.data?.Media) bookData = d.data.Media;
|
||||
} catch (e) { }
|
||||
bookData = await fetchBookMetadata(id);
|
||||
}
|
||||
|
||||
if (!bookData) return { chapters: [] };
|
||||
if (!bookData) {
|
||||
return { chapters: [] };
|
||||
}
|
||||
|
||||
const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean) as string[];
|
||||
searchTitle = titles[0];
|
||||
}
|
||||
|
||||
const allChapters: ChapterWithProvider[] = [];
|
||||
const extensions = getAllExtensions();
|
||||
const bookExtensions = getBookExtensionsMap();
|
||||
|
||||
const searchPromises = Array.from(extensions.entries())
|
||||
.filter(([_, ext]) =>
|
||||
(ext.type === 'book-board' || ext.type === 'manga-board') &&
|
||||
ext.search && ext.findChapters
|
||||
)
|
||||
const searchPromises = Array.from(bookExtensions.entries())
|
||||
.filter(([_, ext]) => ext.search && ext.findChapters)
|
||||
.map(async ([name, ext]) => {
|
||||
try {
|
||||
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
||||
|
||||
const matches = await ext.search!({
|
||||
query: searchTitle!,
|
||||
media: bookData ? {
|
||||
romajiTitle: bookData.title.romaji,
|
||||
englishTitle: bookData.title.english || "",
|
||||
startDate: bookData.startDate || { year: 0, month: 0, day: 0 }
|
||||
} : { romajiTitle: searchTitle!, englishTitle: searchTitle!, startDate: { year: 0, month: 0, day: 0 } }
|
||||
});
|
||||
|
||||
if (matches?.length) {
|
||||
const best = matches[0];
|
||||
const chaps = await ext.findChapters!(best.id);
|
||||
|
||||
if (chaps?.length) {
|
||||
console.log(`[${name}] Found ${chaps.length} chapters.`);
|
||||
chaps.forEach((ch: { id: any; number: { toString: () => string; }; title: any; releaseDate: any; }) => {
|
||||
allChapters.push({
|
||||
id: ch.id,
|
||||
number: parseFloat(ch.number.toString()),
|
||||
title: ch.title,
|
||||
date: ch.releaseDate,
|
||||
provider: name
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(`[${name}] No matches found for book.`);
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
console.error(`Failed to fetch chapters from ${name}:`, error.message);
|
||||
}
|
||||
const chapters = await searchChaptersInExtension(ext, name, searchTitle!, bookData);
|
||||
allChapters.push(...chapters);
|
||||
});
|
||||
|
||||
await Promise.all(searchPromises);
|
||||
|
||||
return { chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) };
|
||||
return {
|
||||
chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number))
|
||||
};
|
||||
}
|
||||
|
||||
export async function getChapterContent(bookId: string, chapterIndex: string, providerName: string): Promise<ChapterContent> {
|
||||
|
||||
32
src/extensions/extensions.controller.ts
Normal file
32
src/extensions/extensions.controller.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { getExtension, getExtensionsList, getAllExtensions, getBookExtensionsMap, getAnimeExtensionsMap } from '../shared/extensions';
|
||||
import { ExtensionNameRequest } from '../types';
|
||||
|
||||
export async function getExtensions(req: FastifyRequest, reply: FastifyReply) {
|
||||
return { extensions: getExtensionsList() };
|
||||
}
|
||||
|
||||
export async function getAnimeExtensions(req: FastifyRequest, reply: FastifyReply) {
|
||||
const animeExtensions = getAnimeExtensionsMap();
|
||||
return { extensions: Array.from(animeExtensions.keys()) };
|
||||
}
|
||||
|
||||
export async function getBookExtensions(req: FastifyRequest, reply: FastifyReply) {
|
||||
const bookExtensions = getBookExtensionsMap();
|
||||
return { extensions: Array.from(bookExtensions.keys()) };
|
||||
}
|
||||
|
||||
export async function getExtensionSettings(req: ExtensionNameRequest, reply: FastifyReply) {
|
||||
const { name } = req.params;
|
||||
const ext = getExtension(name);
|
||||
|
||||
if (!ext) {
|
||||
return { error: "Extension not found" };
|
||||
}
|
||||
|
||||
if (!ext.getSettings) {
|
||||
return { episodeServers: ["default"], supportsDub: false };
|
||||
}
|
||||
|
||||
return ext.getSettings();
|
||||
}
|
||||
11
src/extensions/extensions.routes.ts
Normal file
11
src/extensions/extensions.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as controller from './extensions.controller';
|
||||
|
||||
async function extensionsRoutes(fastify: FastifyInstance) {
|
||||
fastify.get('/extensions', controller.getExtensions);
|
||||
fastify.get('/extensions/anime', controller.getAnimeExtensions);
|
||||
fastify.get('/extensions/book', controller.getBookExtensions);
|
||||
fastify.get('/extensions/:name/settings', controller.getExtensionSettings);
|
||||
}
|
||||
|
||||
export default extensionsRoutes;
|
||||
@@ -134,12 +134,25 @@ async function loadAnime() {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.nextAiringEpisode?.episode) {
|
||||
totalEpisodes = data.nextAiringEpisode.episode - 1;
|
||||
} else if (data.episodes) {
|
||||
totalEpisodes = data.episodes;
|
||||
let extensionEpisodes = [];
|
||||
|
||||
if (extensionName) {
|
||||
extensionEpisodes = await loadExtensionEpisodes(animeId, extensionName);
|
||||
|
||||
if (extensionEpisodes.length > 0) {
|
||||
totalEpisodes = extensionEpisodes.length;
|
||||
} else {
|
||||
totalEpisodes = 1;
|
||||
}
|
||||
} else {
|
||||
totalEpisodes = 12;
|
||||
// MODO NORMAL (AniList)
|
||||
if (data.nextAiringEpisode?.episode) {
|
||||
totalEpisodes = data.nextAiringEpisode.episode - 1;
|
||||
} else if (data.episodes) {
|
||||
totalEpisodes = data.episodes;
|
||||
} else {
|
||||
totalEpisodes = 12;
|
||||
}
|
||||
}
|
||||
|
||||
totalEpisodes = Math.min(Math.max(totalEpisodes, 1), 5000);
|
||||
@@ -153,6 +166,27 @@ async function loadAnime() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExtensionEpisodes(animeId, extName) {
|
||||
try {
|
||||
const url = `/api/anime/${animeId}/episodes?ext=${extName}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
if (!Array.isArray(data)) return [];
|
||||
|
||||
return data.map(ep => ({
|
||||
id: ep.id,
|
||||
number: ep.number,
|
||||
title: ep.title || `Episode ${ep.number}`,
|
||||
url: ep.url
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch extension episodes:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleDescription(text) {
|
||||
const tmp = document.createElement("DIV");
|
||||
tmp.innerHTML = text;
|
||||
|
||||
@@ -34,12 +34,11 @@ async function fetchSearh(query) {
|
||||
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, '-');
|
||||
.replace(/-/g, '--')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9\-]/g, '');
|
||||
}
|
||||
|
||||
function renderSearchResults(results) {
|
||||
|
||||
@@ -6,6 +6,7 @@ let audioMode = 'sub';
|
||||
let currentExtension = '';
|
||||
let plyrInstance;
|
||||
let hlsInstance;
|
||||
let totalEpisodes = 0;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const firstKey = params.keys().next().value;
|
||||
@@ -21,42 +22,130 @@ document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`
|
||||
|
||||
async function loadMetadata() {
|
||||
try {
|
||||
const res = await fetch(`/api/anime/${animeId}`);
|
||||
const extQuery = extName ? `?ext=${extName}` : "";
|
||||
|
||||
const res = await fetch(`/api/anime/${animeId}${extQuery}`);
|
||||
const data = await res.json();
|
||||
if(!data.error) {
|
||||
const title = data.title.english || data.title.romaji;
|
||||
document.getElementById('anime-title').innerText = title;
|
||||
document.title = `Watching ${title} - Ep ${currentEpisode}`;
|
||||
|
||||
if (!data.error) {
|
||||
const romajiTitle = data.title.romaji || data.title.english || 'Anime Title';
|
||||
|
||||
document.getElementById('anime-title').innerText = romajiTitle;
|
||||
document.title = `Watching ${romajiTitle} - Ep ${currentEpisode}`;
|
||||
|
||||
document.getElementById('detail-anime-title').innerText = romajiTitle;
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = data.description || 'No description available.';
|
||||
document.getElementById('detail-description').innerText =
|
||||
tempDiv.textContent || tempDiv.innerText;
|
||||
|
||||
document.getElementById('detail-format').innerText = data.format || '--';
|
||||
document.getElementById('detail-score').innerText =
|
||||
data.averageScore ? `${data.averageScore}%` : '--';
|
||||
|
||||
const season = data.season
|
||||
? data.season.charAt(0) + data.season.slice(1).toLowerCase()
|
||||
: '';
|
||||
document.getElementById('detail-season').innerText =
|
||||
data.seasonYear ? `${season} ${data.seasonYear}` : '--';
|
||||
|
||||
document.getElementById('detail-cover-image').src =
|
||||
data.coverImage.large || data.coverImage.medium || '';
|
||||
|
||||
if (!extName) {
|
||||
|
||||
totalEpisodes = data.episodes || 0;
|
||||
} else {
|
||||
|
||||
try {
|
||||
const res2 = await fetch(`/api/anime/${animeId}/episodes${extQuery}`);
|
||||
const data2 = await res2.json();
|
||||
totalEpisodes = Array.isArray(data2) ? data2.length : 0;
|
||||
} catch (e) {
|
||||
console.error("Error cargando episodios por extensión:", e);
|
||||
totalEpisodes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
populateEpisodeSelectors(totalEpisodes);
|
||||
|
||||
if (currentEpisode >= totalEpisodes && totalEpisodes > 0) {
|
||||
document.getElementById('next-btn').disabled = true;
|
||||
}
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
} catch (error) {
|
||||
console.error('Error loading metadata:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateEpisodeSelectors(count) {
|
||||
const list = document.getElementById('episode-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const extParam = extName ? `?${extName}` : "";
|
||||
|
||||
const btn = document.createElement('a');
|
||||
btn.href = `/watch/${animeId}/${i}${extParam}`;
|
||||
btn.classList.add('episode-btn');
|
||||
btn.dataset.episode = i;
|
||||
btn.innerText = i;
|
||||
|
||||
if (i === currentEpisode) {
|
||||
btn.classList.add('active-ep');
|
||||
}
|
||||
|
||||
list.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function filterEpisodes() {
|
||||
const searchInput = document.getElementById('episode-search');
|
||||
const filter = searchInput.value.toUpperCase().trim();
|
||||
const episodeList = document.getElementById('episode-list');
|
||||
const buttons = episodeList.getElementsByClassName('episode-btn');
|
||||
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const episodeNumber = buttons[i].dataset.episode;
|
||||
|
||||
if (episodeNumber.startsWith(filter) || filter === "") {
|
||||
buttons[i].style.display = "";
|
||||
} else {
|
||||
buttons[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExtensions() {
|
||||
try {
|
||||
const res = await fetch('/api/extensions');
|
||||
const res = await fetch('/api/extensions/anime');
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('extension-select');
|
||||
|
||||
if (data.extensions && data.extensions.length > 0) {
|
||||
select.innerHTML = '';
|
||||
data.extensions.forEach(ext => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = opt.innerText = ext;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
if (data.extensions.includes(extName ?? "")) {
|
||||
if (typeof extName === 'string' && data.extensions.includes(extName)) {
|
||||
select.value = extName;
|
||||
currentExtension = extName;
|
||||
onExtensionChange();
|
||||
} else {
|
||||
select.selectedIndex = 0;
|
||||
}
|
||||
|
||||
currentExtension = select.value;
|
||||
onExtensionChange();
|
||||
} else {
|
||||
select.innerHTML = '<option>No Extensions</option>';
|
||||
select.disabled = true;
|
||||
setLoading("No extensions found in WaifuBoards folder.");
|
||||
setLoading("No anime extensions found.");
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
console.error("Extension Error:", e);
|
||||
} catch (error) {
|
||||
console.error("Extension Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +155,7 @@ async function onExtensionChange() {
|
||||
setLoading("Fetching extension settings...");
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/extension/${currentExtension}/settings`);
|
||||
const res = await fetch(`/api/extensions/${currentExtension}/settings`);
|
||||
const settings = await res.json();
|
||||
|
||||
const toggle = document.getElementById('sd-toggle');
|
||||
@@ -93,9 +182,8 @@ async function onExtensionChange() {
|
||||
}
|
||||
|
||||
loadStream();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setLoading("Failed to load extension settings.");
|
||||
}
|
||||
}
|
||||
@@ -113,6 +201,7 @@ function setAudioMode(mode) {
|
||||
const dubOpt = document.getElementById('opt-dub');
|
||||
|
||||
toggle.setAttribute('data-state', mode);
|
||||
|
||||
if (mode === 'sub') {
|
||||
subOpt.classList.add('active');
|
||||
dubOpt.classList.remove('active');
|
||||
@@ -128,7 +217,7 @@ async function loadStream() {
|
||||
const serverSelect = document.getElementById('server-select');
|
||||
const server = serverSelect.value || "default";
|
||||
|
||||
setLoading(`Searching & Resolving Stream (${audioMode})...`);
|
||||
setLoading(`Loading stream (${audioMode})...`);
|
||||
|
||||
try {
|
||||
const url = `/api/watch/stream?animeId=${animeId.slice(0, 30)}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`;
|
||||
@@ -146,21 +235,18 @@ async function loadStream() {
|
||||
}
|
||||
|
||||
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
|
||||
|
||||
const headers = data.headers || {};
|
||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
||||
|
||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
||||
if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
|
||||
if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`;
|
||||
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
|
||||
|
||||
playVideo(proxyUrl, data.videoSources[0].subtitles);
|
||||
|
||||
document.getElementById('loading-overlay').style.display = 'none';
|
||||
|
||||
} catch (err) {
|
||||
setLoading("Stream Error. Check console.");
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
setLoading("Stream error. Check console.");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +255,7 @@ function playVideo(url, subtitles) {
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
if (hlsInstance) hlsInstance.destroy();
|
||||
|
||||
hlsInstance = new Hls({
|
||||
xhrSetup: (xhr, url) => {
|
||||
xhr.withCredentials = false;
|
||||
@@ -193,30 +280,52 @@ function playVideo(url, subtitles) {
|
||||
track.label = sub.language;
|
||||
track.srclang = sub.language.slice(0, 2).toLowerCase();
|
||||
track.src = sub.url;
|
||||
|
||||
if (sub.default || sub.language.toLowerCase().includes('english')) {
|
||||
track.default = true;
|
||||
}
|
||||
|
||||
video.appendChild(track);
|
||||
});
|
||||
}
|
||||
|
||||
plyrInstance = new Plyr(video, {
|
||||
captions: { active: true, update: true, language: 'en' },
|
||||
controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
|
||||
captions: {
|
||||
active: true,
|
||||
update: true,
|
||||
language: 'en'
|
||||
},
|
||||
controls: [
|
||||
'play-large',
|
||||
'play',
|
||||
'progress',
|
||||
'current-time',
|
||||
'duration',
|
||||
'mute',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'pip',
|
||||
'airplay',
|
||||
'fullscreen'
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed']
|
||||
});
|
||||
|
||||
video.play().catch(e => console.log("Auto-play blocked"));
|
||||
video.play().catch(error => {
|
||||
console.log("Autoplay blocked:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(msg) {
|
||||
function setLoading(message) {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
const text = document.getElementById('loading-text');
|
||||
overlay.style.display = 'flex';
|
||||
text.innerText = msg;
|
||||
text.innerText = message;
|
||||
}
|
||||
|
||||
const extParam = extName ? `?${extName}` : "";
|
||||
|
||||
document.getElementById('prev-btn').onclick = () => {
|
||||
if (currentEpisode > 1) {
|
||||
window.location.href = `/watch/${animeId}/${currentEpisode - 1}${extParam}`;
|
||||
@@ -226,7 +335,10 @@ document.getElementById('prev-btn').onclick = () => {
|
||||
document.getElementById('next-btn').onclick = () => {
|
||||
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;
|
||||
}
|
||||
|
||||
loadMetadata();
|
||||
loadExtensions();
|
||||
@@ -104,7 +104,6 @@ async function loadChapters() {
|
||||
? `/api/book/${bookId.slice(0, 40)}/chapters`
|
||||
: `/api/book/${bookId}/chapters`;
|
||||
|
||||
console.log(fetchUrl)
|
||||
const res = await fetch(fetchUrl);
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
@@ -49,11 +49,11 @@ async function fetchBookSearch(query) {
|
||||
function createSlug(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/[\s-]+/g, '-');
|
||||
.replace(/-/g, '--')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9\-]/g, '');
|
||||
}
|
||||
|
||||
function renderSearchResults(results) {
|
||||
|
||||
@@ -57,9 +57,31 @@ function getExtensionsList() {
|
||||
return Array.from(extensions.keys());
|
||||
}
|
||||
|
||||
function getAnimeExtensionsMap() {
|
||||
const animeExts = new Map();
|
||||
for (const [name, ext] of extensions) {
|
||||
if (ext.type === 'anime-board') {
|
||||
animeExts.set(name, ext);
|
||||
}
|
||||
}
|
||||
return animeExts;
|
||||
}
|
||||
|
||||
function getBookExtensionsMap() {
|
||||
const bookExts = new Map();
|
||||
for (const [name, ext] of extensions) {
|
||||
if (ext.type === 'book-board' || ext.type === 'manga-board') {
|
||||
bookExts.set(name, ext);
|
||||
}
|
||||
}
|
||||
return bookExts;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadExtensions,
|
||||
getExtension,
|
||||
getAllExtensions,
|
||||
getExtensionsList
|
||||
getExtensionsList,
|
||||
getAnimeExtensionsMap,
|
||||
getBookExtensionsMap
|
||||
};
|
||||
@@ -69,6 +69,7 @@ export interface ExtensionSearchResult {
|
||||
}
|
||||
|
||||
export interface Episode {
|
||||
url: string;
|
||||
id: string;
|
||||
number: number;
|
||||
title?: string;
|
||||
|
||||
Reference in New Issue
Block a user