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 os from 'os';
import path from 'path';
import jwt from "jsonwebtoken";
export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
try {
@@ -82,12 +81,20 @@ export async function search(req: SearchRequest, reply: FastifyReply) {
export async function searchInExtension(req: any, reply: FastifyReply) {
try {
const extensionName = req.params.extension;
const query = req.query.q;
const { q, ...rest } = req.query;
const ext = getExtension(extensionName);
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 };
} catch {
return { results: [] };
@@ -340,3 +347,27 @@ export async function openInMPV(req: any, reply: any) {
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('/top-airing', controller.getTopAiring);
fastify.get('/search', controller.search);
fastify.get('/search/advanced', controller.searchAdvanced);
fastify.get('/search/:extension', controller.searchInExtension);
fastify.get('/watch/stream', controller.getWatchStream);
fastify.post('/watch/mpv', controller.openInMPV);

View File

@@ -272,6 +272,70 @@ export async function searchAnimeLocal(query: string): Promise<Anime[]> {
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 }> {
if (!ext) return { error: "not found" };
@@ -316,22 +380,31 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string):
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.type === 'anime-board' && ext.search) {
try {
console.log(`[${name}] Searching for anime: ${query}`);
const matches = await ext.search({
query: query,
const payload: any = {
query: searchObj.query,
media: {
romajiTitle: query,
englishTitle: query,
romajiTitle: searchObj.query,
englishTitle: searchObj.query,
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 => ({
id: m.id,
extensionName: name,

View File

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

View File

@@ -300,16 +300,18 @@ export async function getBookInfoExtension(ext: Extension | null, id: string): P
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.type === 'book-board') && ext.search) {
if (ext.type === 'book-board' && ext.search) {
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({
query: query,
query,
filters,
media: {
romajiTitle: query,
englishTitle: query,
@@ -330,7 +332,6 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
url: m.url,
}));
}
} catch (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}.` });
}
}
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.post('/extensions/install', controller.installExtension);
fastify.post('/extensions/uninstall', controller.uninstallExtension);
fastify.get('/extensions/:extension/filters', controller.getExtensionFilters);
}
export default extensionsRoutes;

View File

@@ -97,7 +97,7 @@ export interface Extension {
getMetadata: any;
type: 'anime-board' | 'book-board' | 'manga-board';
mediaType?: 'manga' | 'ln';
search?: (options: ExtensionSearchOptions) => Promise<ExtensionSearchResult[]>;
search?: (options: any) => Promise<ExtensionSearchResult[]>;
findEpisodes?: (id: string) => Promise<Episode[]>;
findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise<any>;
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) {
try {
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) {
try {
const extensionName = req.params.extension;
const query = req.query.q;
const { q, ...rest } = req.query;
const ext = getExtension(extensionName);
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 };
} catch {
return { results: [] };

View File

@@ -7,6 +7,7 @@ async function animeRoutes(fastify: FastifyInstance) {
fastify.get('/trending', controller.getTrending);
fastify.get('/top-airing', controller.getTopAiring);
fastify.get('/search', controller.search);
fastify.get('/search/advanced', controller.searchAdvanced);
fastify.get('/search/:extension', controller.searchInExtension);
fastify.get('/watch/stream', controller.getWatchStream);
}

View File

@@ -272,6 +272,70 @@ export async function searchAnimeLocal(query: string): Promise<Anime[]> {
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 }> {
if (!ext) return { error: "not found" };
@@ -316,22 +380,31 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string):
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.type === 'anime-board' && ext.search) {
try {
console.log(`[${name}] Searching for anime: ${query}`);
const matches = await ext.search({
query: query,
const payload: any = {
query: searchObj.query,
media: {
romajiTitle: query,
englishTitle: query,
romajiTitle: searchObj.query,
englishTitle: searchObj.query,
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 => ({
id: m.id,
extensionName: name,

View File

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

View File

@@ -300,16 +300,18 @@ export async function getBookInfoExtension(ext: Extension | null, id: string): P
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.type === 'book-board') && ext.search) {
if (ext.type === 'book-board' && ext.search) {
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({
query: query,
query,
filters,
media: {
romajiTitle: query,
englishTitle: query,
@@ -330,7 +332,6 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
url: m.url,
}));
}
} catch (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}.` });
}
}
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.post('/extensions/install', controller.installExtension);
fastify.post('/extensions/uninstall', controller.uninstallExtension);
fastify.get('/extensions/:extension/filters', controller.getExtensionFilters);
}
export default extensionsRoutes;

View File

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