extension allows filtering for search

This commit is contained in:
2026-01-12 17:05:38 +01:00
parent 34f9e44020
commit dbb3c3f924
16 changed files with 319 additions and 46 deletions

View File

@@ -10,7 +10,6 @@ import net from 'net';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import jwt from "jsonwebtoken";
export async function getAnime(req: AnimeRequest, reply: FastifyReply) { export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
try { try {
@@ -82,12 +81,20 @@ export async function search(req: SearchRequest, reply: FastifyReply) {
export async function searchInExtension(req: any, reply: FastifyReply) { export async function searchInExtension(req: any, reply: FastifyReply) {
try { try {
const extensionName = req.params.extension; const extensionName = req.params.extension;
const query = req.query.q; const { q, ...rest } = req.query;
const ext = getExtension(extensionName); const ext = getExtension(extensionName);
if (!ext) return { results: [] }; if (!ext) return { results: [] };
const results = await animeService.searchAnimeInExtension(ext, extensionName, query); const results = await animeService.searchAnimeInExtension(
ext,
extensionName,
{
query: q || '',
filters: Object.keys(rest).length ? rest : undefined
}
);
return { results }; return { results };
} catch { } catch {
return { results: [] }; return { results: [] };
@@ -340,3 +347,27 @@ export async function openInMPV(req: any, reply: any) {
return { error: (e as Error).message }; return { error: (e as Error).message };
} }
} }
export async function searchAdvanced(req: FastifyRequest, reply: FastifyReply) {
try {
const {
q,
type = 'ANIME',
year,
season,
status,
format,
genre,
minScore,
sort
} = req.query as any;
const results = await animeService.searchMediaAdvanced(q, type, {
year, season, status, format, genre, minScore, sort
});
return { results };
} catch (e) {
return { results: [] };
}
}

View File

@@ -7,6 +7,7 @@ async function animeRoutes(fastify: FastifyInstance) {
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', controller.search); fastify.get('/search', controller.search);
fastify.get('/search/advanced', controller.searchAdvanced);
fastify.get('/search/:extension', controller.searchInExtension); fastify.get('/search/:extension', controller.searchInExtension);
fastify.get('/watch/stream', controller.getWatchStream); fastify.get('/watch/stream', controller.getWatchStream);
fastify.post('/watch/mpv', controller.openInMPV); fastify.post('/watch/mpv', controller.openInMPV);

View File

@@ -272,6 +272,70 @@ export async function searchAnimeLocal(query: string): Promise<Anime[]> {
return merged; return merged;
} }
type AdvancedFilters = {
year?: string;
season?: string;
status?: string;
format?: string;
genre?: string;
minScore?: string;
sort?: string;
};
export async function searchMediaAdvanced(
query: string,
type: 'ANIME' | 'MANGA',
filters: AdvancedFilters
): Promise<Anime[]> {
const gql = `
query (
$type: MediaType
$search: String
$seasonYear: Int
$season: MediaSeason
$status: MediaStatus
$format: MediaFormat
$genres: [String]
$minScore: Int
$sort: [MediaSort]
) {
Page(page: 1, perPage: 20) {
media(
type: $type
search: $search
seasonYear: $seasonYear
season: $season
status: $status
format: $format
genre_in: $genres
averageScore_greater: $minScore
sort: $sort
isAdult: false
) {
${MEDIA_FIELDS}
}
}
}
`;
const vars = {
type,
search: query || undefined,
seasonYear: filters.year ? Number(filters.year) : undefined,
season: filters.season || undefined,
status: filters.status || undefined,
format: filters.format || undefined,
genres: filters.genre ? [filters.genre] : undefined,
minScore: filters.minScore ? Number(filters.minScore) : undefined,
sort: filters.sort ? [filters.sort] : ['POPULARITY_DESC']
};
const data = await fetchAniList(gql, vars);
return data?.Page?.media || [];
}
export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise<Anime | { error: string }> { export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise<Anime | { error: string }> {
if (!ext) return { error: "not found" }; if (!ext) return { error: "not found" };
@@ -316,22 +380,31 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string):
return { error: "not found" }; return { error: "not found" };
} }
export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string) { export async function searchAnimeInExtension(
ext: Extension | null,
name: string,
searchObj: { query: string; filters?: any }
) {
if (!ext) return []; if (!ext) return [];
if (ext.type === 'anime-board' && ext.search) { if (ext.type === 'anime-board' && ext.search) {
try { try {
console.log(`[${name}] Searching for anime: ${query}`); const payload: any = {
const matches = await ext.search({ query: searchObj.query,
query: query,
media: { media: {
romajiTitle: query, romajiTitle: searchObj.query,
englishTitle: query, englishTitle: searchObj.query,
startDate: { year: 0, month: 0, day: 0 } startDate: { year: 0, month: 0, day: 0 }
} }
}); };
if (matches && matches.length > 0) { if (searchObj.filters) {
payload.filters = searchObj.filters;
}
const matches = await ext.search(payload);
if (matches?.length) {
return matches.map(m => ({ return matches.map(m => ({
id: m.id, id: m.id,
extensionName: name, extensionName: name,

View File

@@ -69,16 +69,24 @@ export async function searchBooks(req: SearchRequest, reply: FastifyReply) {
export async function searchBooksInExtension(req: any, reply: FastifyReply) { export async function searchBooksInExtension(req: any, reply: FastifyReply) {
try { try {
const extensionName = req.params.extension; const extensionName = req.params.extension;
const query = req.query.q;
const { q, ...rawFilters } = req.query;
const ext = getExtension(extensionName); const ext = getExtension(extensionName);
if (!ext) return { results: [] }; if (!ext) return { results: [] };
const results = await booksService.searchBooksInExtension(ext, extensionName, query); const results = await booksService.searchBooksInExtension(
ext,
extensionName,
{
query: q || '',
filters: rawFilters
}
);
return { results }; return { results };
} catch (e) { } catch (e) {
const error = e as Error; console.error("Search Error:", (e as Error).message);
console.error("Search Error:", error.message);
return { results: [] }; return { results: [] };
} }
} }

View File

@@ -123,10 +123,10 @@ export async function getBookById(id: string | number): Promise<Book | { error:
const insertSql = ` const insertSql = `
INSERT INTO books (id, title, updatedAt, full_data) INSERT INTO books (id, title, updatedAt, full_data)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
title = EXCLUDED.title, title = EXCLUDED.title,
updatedAt = EXCLUDED.updatedAt, updatedAt = EXCLUDED.updatedAt,
full_data = EXCLUDED.full_data; full_data = EXCLUDED.full_data;
`; `;
await run(insertSql, [ await run(insertSql, [
@@ -300,16 +300,18 @@ export async function getBookInfoExtension(ext: Extension | null, id: string): P
return []; return [];
} }
export async function searchBooksInExtension(ext: Extension | null, name: string, query: string): Promise<Book[]> { export async function searchBooksInExtension(ext: Extension | null, name: string, searchObj: { query: string; filters?: any }): Promise<Book[]> {
if (!ext) return []; if (!ext) return [];
if ((ext.type === 'book-board') && ext.search) { if (ext.type === 'book-board' && ext.search) {
try { try {
console.log(`[${name}] Searching for book: ${query}`); const { query, filters } = searchObj;
console.log(`[${name}] Searching for book: ${query}`, filters);
const matches = await ext.search({ const matches = await ext.search({
query: query, query,
filters,
media: { media: {
romajiTitle: query, romajiTitle: query,
englishTitle: query, englishTitle: query,
@@ -330,7 +332,6 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
url: m.url, url: m.url,
})); }));
} }
} catch (e) { } catch (e) {
console.error(`Extension search failed for ${name}:`, e); console.error(`Extension search failed for ${name}:`, e);
} }

View File

@@ -153,3 +153,24 @@ export async function uninstallExtension(req: any, reply: FastifyReply) {
return reply.code(500).send({ success: false, error: `Failed to uninstall extension ${fileName}.` }); return reply.code(500).send({ success: false, error: `Failed to uninstall extension ${fileName}.` });
} }
} }
export async function getExtensionFilters(req: any, reply: FastifyReply) {
try {
const extensionName = req.params.extension;
const ext = getExtension(extensionName);
if (!ext) {
return { filters: {} };
}
if (typeof ext.getFilters === 'function') {
const filters = ext.getFilters();
return { filters: filters || {} };
}
return { filters: {} };
} catch (e) {
console.error('getExtensionFilters error:', e);
return { filters: {} };
}
}

View File

@@ -12,6 +12,7 @@ async function extensionsRoutes(fastify: FastifyInstance) {
fastify.get('/extensions/:name/settings', controller.getExtensionSettings); fastify.get('/extensions/:name/settings', controller.getExtensionSettings);
fastify.post('/extensions/install', controller.installExtension); fastify.post('/extensions/install', controller.installExtension);
fastify.post('/extensions/uninstall', controller.uninstallExtension); fastify.post('/extensions/uninstall', controller.uninstallExtension);
fastify.get('/extensions/:extension/filters', controller.getExtensionFilters);
} }
export default extensionsRoutes; export default extensionsRoutes;

View File

@@ -97,7 +97,7 @@ export interface Extension {
getMetadata: any; getMetadata: any;
type: 'anime-board' | 'book-board' | 'manga-board'; type: 'anime-board' | 'book-board' | 'manga-board';
mediaType?: 'manga' | 'ln'; mediaType?: 'manga' | 'ln';
search?: (options: ExtensionSearchOptions) => Promise<ExtensionSearchResult[]>; search?: (options: any) => Promise<ExtensionSearchResult[]>;
findEpisodes?: (id: string) => Promise<Episode[]>; findEpisodes?: (id: string) => Promise<Episode[]>;
findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise<any>; findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise<any>;
findChapters?: (id: string) => Promise<Chapter[]>; findChapters?: (id: string) => Promise<Chapter[]>;

View File

@@ -56,6 +56,30 @@ export async function getTopAiring(req: FastifyRequest, reply: FastifyReply) {
} }
} }
export async function searchAdvanced(req: FastifyRequest, reply: FastifyReply) {
try {
const {
q,
type = 'ANIME',
year,
season,
status,
format,
genre,
minScore,
sort
} = req.query as any;
const results = await animeService.searchMediaAdvanced(q, type, {
year, season, status, format, genre, minScore, sort
});
return { results };
} catch (e) {
return { results: [] };
}
}
export async function search(req: SearchRequest, reply: FastifyReply) { export async function search(req: SearchRequest, reply: FastifyReply) {
try { try {
const query = req.query.q; const query = req.query.q;
@@ -73,12 +97,20 @@ export async function search(req: SearchRequest, reply: FastifyReply) {
export async function searchInExtension(req: any, reply: FastifyReply) { export async function searchInExtension(req: any, reply: FastifyReply) {
try { try {
const extensionName = req.params.extension; const extensionName = req.params.extension;
const query = req.query.q; const { q, ...rest } = req.query;
const ext = getExtension(extensionName); const ext = getExtension(extensionName);
if (!ext) return { results: [] }; if (!ext) return { results: [] };
const results = await animeService.searchAnimeInExtension(ext, extensionName, query); const results = await animeService.searchAnimeInExtension(
ext,
extensionName,
{
query: q || '',
filters: Object.keys(rest).length ? rest : undefined
}
);
return { results }; return { results };
} catch { } catch {
return { results: [] }; return { results: [] };

View File

@@ -7,6 +7,7 @@ async function animeRoutes(fastify: FastifyInstance) {
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', controller.search); fastify.get('/search', controller.search);
fastify.get('/search/advanced', controller.searchAdvanced);
fastify.get('/search/:extension', controller.searchInExtension); fastify.get('/search/:extension', controller.searchInExtension);
fastify.get('/watch/stream', controller.getWatchStream); fastify.get('/watch/stream', controller.getWatchStream);
} }

View File

@@ -272,6 +272,70 @@ export async function searchAnimeLocal(query: string): Promise<Anime[]> {
return merged; return merged;
} }
type AdvancedFilters = {
year?: string;
season?: string;
status?: string;
format?: string;
genre?: string;
minScore?: string;
sort?: string;
};
export async function searchMediaAdvanced(
query: string,
type: 'ANIME' | 'MANGA',
filters: AdvancedFilters
): Promise<Anime[]> {
const gql = `
query (
$type: MediaType
$search: String
$seasonYear: Int
$season: MediaSeason
$status: MediaStatus
$format: MediaFormat
$genres: [String]
$minScore: Int
$sort: [MediaSort]
) {
Page(page: 1, perPage: 20) {
media(
type: $type
search: $search
seasonYear: $seasonYear
season: $season
status: $status
format: $format
genre_in: $genres
averageScore_greater: $minScore
sort: $sort
isAdult: false
) {
${MEDIA_FIELDS}
}
}
}
`;
const vars = {
type,
search: query || undefined,
seasonYear: filters.year ? Number(filters.year) : undefined,
season: filters.season || undefined,
status: filters.status || undefined,
format: filters.format || undefined,
genres: filters.genre ? [filters.genre] : undefined,
minScore: filters.minScore ? Number(filters.minScore) : undefined,
sort: filters.sort ? [filters.sort] : ['POPULARITY_DESC']
};
const data = await fetchAniList(gql, vars);
return data?.Page?.media || [];
}
export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise<Anime | { error: string }> { export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise<Anime | { error: string }> {
if (!ext) return { error: "not found" }; if (!ext) return { error: "not found" };
@@ -316,22 +380,31 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string):
return { error: "not found" }; return { error: "not found" };
} }
export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string) { export async function searchAnimeInExtension(
ext: Extension | null,
name: string,
searchObj: { query: string; filters?: any }
) {
if (!ext) return []; if (!ext) return [];
if (ext.type === 'anime-board' && ext.search) { if (ext.type === 'anime-board' && ext.search) {
try { try {
console.log(`[${name}] Searching for anime: ${query}`); const payload: any = {
const matches = await ext.search({ query: searchObj.query,
query: query,
media: { media: {
romajiTitle: query, romajiTitle: searchObj.query,
englishTitle: query, englishTitle: searchObj.query,
startDate: { year: 0, month: 0, day: 0 } startDate: { year: 0, month: 0, day: 0 }
} }
}); };
if (matches && matches.length > 0) { if (searchObj.filters) {
payload.filters = searchObj.filters;
}
const matches = await ext.search(payload);
if (matches?.length) {
return matches.map(m => ({ return matches.map(m => ({
id: m.id, id: m.id,
extensionName: name, extensionName: name,

View File

@@ -69,16 +69,24 @@ export async function searchBooks(req: SearchRequest, reply: FastifyReply) {
export async function searchBooksInExtension(req: any, reply: FastifyReply) { export async function searchBooksInExtension(req: any, reply: FastifyReply) {
try { try {
const extensionName = req.params.extension; const extensionName = req.params.extension;
const query = req.query.q;
const { q, ...rawFilters } = req.query;
const ext = getExtension(extensionName); const ext = getExtension(extensionName);
if (!ext) return { results: [] }; if (!ext) return { results: [] };
const results = await booksService.searchBooksInExtension(ext, extensionName, query); const results = await booksService.searchBooksInExtension(
ext,
extensionName,
{
query: q || '',
filters: rawFilters
}
);
return { results }; return { results };
} catch (e) { } catch (e) {
const error = e as Error; console.error("Search Error:", (e as Error).message);
console.error("Search Error:", error.message);
return { results: [] }; return { results: [] };
} }
} }

View File

@@ -300,16 +300,18 @@ export async function getBookInfoExtension(ext: Extension | null, id: string): P
return []; return [];
} }
export async function searchBooksInExtension(ext: Extension | null, name: string, query: string): Promise<Book[]> { export async function searchBooksInExtension(ext: Extension | null, name: string, searchObj: { query: string; filters?: any }): Promise<Book[]> {
if (!ext) return []; if (!ext) return [];
if ((ext.type === 'book-board') && ext.search) { if (ext.type === 'book-board' && ext.search) {
try { try {
console.log(`[${name}] Searching for book: ${query}`); const { query, filters } = searchObj;
console.log(`[${name}] Searching for book: ${query}`, filters);
const matches = await ext.search({ const matches = await ext.search({
query: query, query,
filters,
media: { media: {
romajiTitle: query, romajiTitle: query,
englishTitle: query, englishTitle: query,
@@ -330,7 +332,6 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
url: m.url, url: m.url,
})); }));
} }
} catch (e) { } catch (e) {
console.error(`Extension search failed for ${name}:`, e); console.error(`Extension search failed for ${name}:`, e);
} }

View File

@@ -153,3 +153,24 @@ export async function uninstallExtension(req: any, reply: FastifyReply) {
return reply.code(500).send({ success: false, error: `Failed to uninstall extension ${fileName}.` }); return reply.code(500).send({ success: false, error: `Failed to uninstall extension ${fileName}.` });
} }
} }
export async function getExtensionFilters(req: any, reply: FastifyReply) {
try {
const extensionName = req.params.extension;
const ext = getExtension(extensionName);
if (!ext) {
return { filters: {} };
}
if (typeof ext.getFilters === 'function') {
const filters = ext.getFilters();
return { filters: filters || {} };
}
return { filters: {} };
} catch (e) {
console.error('getExtensionFilters error:', e);
return { filters: {} };
}
}

View File

@@ -12,6 +12,7 @@ async function extensionsRoutes(fastify: FastifyInstance) {
fastify.get('/extensions/:name/settings', controller.getExtensionSettings); fastify.get('/extensions/:name/settings', controller.getExtensionSettings);
fastify.post('/extensions/install', controller.installExtension); fastify.post('/extensions/install', controller.installExtension);
fastify.post('/extensions/uninstall', controller.uninstallExtension); fastify.post('/extensions/uninstall', controller.uninstallExtension);
fastify.get('/extensions/:extension/filters', controller.getExtensionFilters);
} }
export default extensionsRoutes; export default extensionsRoutes;

View File

@@ -97,7 +97,7 @@ export interface Extension {
getMetadata: any; getMetadata: any;
type: 'anime-board' | 'book-board' | 'manga-board'; type: 'anime-board' | 'book-board' | 'manga-board';
mediaType?: 'manga' | 'ln'; mediaType?: 'manga' | 'ln';
search?: (options: ExtensionSearchOptions) => Promise<ExtensionSearchResult[]>; search?: (options: any) => Promise<ExtensionSearchResult[]>;
findEpisodes?: (id: string) => Promise<Episode[]>; findEpisodes?: (id: string) => Promise<Episode[]>;
findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise<any>; findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise<any>;
findChapters?: (id: string) => Promise<Chapter[]>; findChapters?: (id: string) => Promise<Chapter[]>;