From dbb3c3f9240e294f020a7fb2dcb72ed14ed1f9a2 Mon Sep 17 00:00:00 2001 From: lenafx Date: Mon, 12 Jan 2026 17:05:38 +0100 Subject: [PATCH] extension allows filtering for search --- desktop/src/api/anime/anime.controller.ts | 37 +++++++- desktop/src/api/anime/anime.routes.ts | 1 + desktop/src/api/anime/anime.service.ts | 89 +++++++++++++++++-- desktop/src/api/books/books.controller.ts | 16 +++- desktop/src/api/books/books.service.ts | 19 ++-- .../api/extensions/extensions.controller.ts | 21 +++++ .../src/api/extensions/extensions.routes.ts | 1 + desktop/src/api/types.ts | 2 +- docker/src/api/anime/anime.controller.ts | 36 +++++++- docker/src/api/anime/anime.routes.ts | 1 + docker/src/api/anime/anime.service.ts | 89 +++++++++++++++++-- docker/src/api/books/books.controller.ts | 16 +++- docker/src/api/books/books.service.ts | 13 +-- .../api/extensions/extensions.controller.ts | 21 +++++ .../src/api/extensions/extensions.routes.ts | 1 + docker/src/api/types.ts | 2 +- 16 files changed, 319 insertions(+), 46 deletions(-) diff --git a/desktop/src/api/anime/anime.controller.ts b/desktop/src/api/anime/anime.controller.ts index 0b09689..f0477be 100644 --- a/desktop/src/api/anime/anime.controller.ts +++ b/desktop/src/api/anime/anime.controller.ts @@ -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: [] }; + } } \ No newline at end of file diff --git a/desktop/src/api/anime/anime.routes.ts b/desktop/src/api/anime/anime.routes.ts index 38dc810..f1a7a08 100644 --- a/desktop/src/api/anime/anime.routes.ts +++ b/desktop/src/api/anime/anime.routes.ts @@ -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); diff --git a/desktop/src/api/anime/anime.service.ts b/desktop/src/api/anime/anime.service.ts index 8c98168..5151fb4 100644 --- a/desktop/src/api/anime/anime.service.ts +++ b/desktop/src/api/anime/anime.service.ts @@ -272,6 +272,70 @@ export async function searchAnimeLocal(query: string): Promise { 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 { + + 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 { 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, diff --git a/desktop/src/api/books/books.controller.ts b/desktop/src/api/books/books.controller.ts index 414465f..bc0ec8f 100644 --- a/desktop/src/api/books/books.controller.ts +++ b/desktop/src/api/books/books.controller.ts @@ -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: [] }; } } diff --git a/desktop/src/api/books/books.service.ts b/desktop/src/api/books/books.service.ts index 2620039..c3f8cef 100644 --- a/desktop/src/api/books/books.service.ts +++ b/desktop/src/api/books/books.service.ts @@ -123,10 +123,10 @@ export async function getBookById(id: string | number): Promise { +export async function searchBooksInExtension(ext: Extension | null, name: string, searchObj: { query: string; filters?: any }): Promise { 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); } diff --git a/desktop/src/api/extensions/extensions.controller.ts b/desktop/src/api/extensions/extensions.controller.ts index e7f9b99..3f4403f 100644 --- a/desktop/src/api/extensions/extensions.controller.ts +++ b/desktop/src/api/extensions/extensions.controller.ts @@ -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: {} }; + } } \ No newline at end of file diff --git a/desktop/src/api/extensions/extensions.routes.ts b/desktop/src/api/extensions/extensions.routes.ts index bb68bfa..dae46c3 100644 --- a/desktop/src/api/extensions/extensions.routes.ts +++ b/desktop/src/api/extensions/extensions.routes.ts @@ -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; \ No newline at end of file diff --git a/desktop/src/api/types.ts b/desktop/src/api/types.ts index 83b3341..512811e 100644 --- a/desktop/src/api/types.ts +++ b/desktop/src/api/types.ts @@ -97,7 +97,7 @@ export interface Extension { getMetadata: any; type: 'anime-board' | 'book-board' | 'manga-board'; mediaType?: 'manga' | 'ln'; - search?: (options: ExtensionSearchOptions) => Promise; + search?: (options: any) => Promise; findEpisodes?: (id: string) => Promise; findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise; findChapters?: (id: string) => Promise; diff --git a/docker/src/api/anime/anime.controller.ts b/docker/src/api/anime/anime.controller.ts index 76ab45b..7f0a768 100644 --- a/docker/src/api/anime/anime.controller.ts +++ b/docker/src/api/anime/anime.controller.ts @@ -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: [] }; diff --git a/docker/src/api/anime/anime.routes.ts b/docker/src/api/anime/anime.routes.ts index f23976d..72ed8dd 100644 --- a/docker/src/api/anime/anime.routes.ts +++ b/docker/src/api/anime/anime.routes.ts @@ -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); } diff --git a/docker/src/api/anime/anime.service.ts b/docker/src/api/anime/anime.service.ts index 8c98168..5151fb4 100644 --- a/docker/src/api/anime/anime.service.ts +++ b/docker/src/api/anime/anime.service.ts @@ -272,6 +272,70 @@ export async function searchAnimeLocal(query: string): Promise { 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 { + + 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 { 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, diff --git a/docker/src/api/books/books.controller.ts b/docker/src/api/books/books.controller.ts index 414465f..bc0ec8f 100644 --- a/docker/src/api/books/books.controller.ts +++ b/docker/src/api/books/books.controller.ts @@ -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: [] }; } } diff --git a/docker/src/api/books/books.service.ts b/docker/src/api/books/books.service.ts index 2620039..9e2ff96 100644 --- a/docker/src/api/books/books.service.ts +++ b/docker/src/api/books/books.service.ts @@ -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 { +export async function searchBooksInExtension(ext: Extension | null, name: string, searchObj: { query: string; filters?: any }): Promise { 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); } diff --git a/docker/src/api/extensions/extensions.controller.ts b/docker/src/api/extensions/extensions.controller.ts index e7f9b99..3f4403f 100644 --- a/docker/src/api/extensions/extensions.controller.ts +++ b/docker/src/api/extensions/extensions.controller.ts @@ -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: {} }; + } } \ No newline at end of file diff --git a/docker/src/api/extensions/extensions.routes.ts b/docker/src/api/extensions/extensions.routes.ts index bb68bfa..dae46c3 100644 --- a/docker/src/api/extensions/extensions.routes.ts +++ b/docker/src/api/extensions/extensions.routes.ts @@ -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; \ No newline at end of file diff --git a/docker/src/api/types.ts b/docker/src/api/types.ts index 83b3341..512811e 100644 --- a/docker/src/api/types.ts +++ b/docker/src/api/types.ts @@ -97,7 +97,7 @@ export interface Extension { getMetadata: any; type: 'anime-board' | 'book-board' | 'manga-board'; mediaType?: 'manga' | 'ln'; - search?: (options: ExtensionSearchOptions) => Promise; + search?: (options: any) => Promise; findEpisodes?: (id: string) => Promise; findEpisodeServer?: (s: any, server1: string | undefined, category: string | undefined) => Promise; findChapters?: (id: string) => Promise;