extension allows filtering for search
This commit is contained in:
@@ -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: [] };
|
||||
@@ -339,4 +346,28 @@ export async function openInMPV(req: any, reply: any) {
|
||||
} catch (e) {
|
||||
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: [] };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +123,10 @@ export async function getBookById(id: string | number): Promise<Book | { error:
|
||||
const insertSql = `
|
||||
INSERT INTO books (id, title, updatedAt, full_data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
updatedAt = EXCLUDED.updatedAt,
|
||||
full_data = EXCLUDED.full_data;
|
||||
updatedAt = EXCLUDED.updatedAt,
|
||||
full_data = EXCLUDED.full_data;
|
||||
`;
|
||||
|
||||
await run(insertSql, [
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -152,4 +152,25 @@ export async function uninstallExtension(req: any, reply: FastifyReply) {
|
||||
req.server.log.error(`Failed to uninstall extension ${fileName}:`, error);
|
||||
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: {} };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -152,4 +152,25 @@ export async function uninstallExtension(req: any, reply: FastifyReply) {
|
||||
req.server.log.error(`Failed to uninstall extension ${fileName}:`, error);
|
||||
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: {} };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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[]>;
|
||||
|
||||
Reference in New Issue
Block a user