from quart import Quart, render_template, request, jsonify import httpx from anilistapi.queries.manga import ( REQUESTS_QUERY, TAGS_QUERY, GENRES_QUERY, REQUESTED_QUERY, ) from anilistapi.schemas.manga import Manga from komconfig import KomConfig from komcache import KomCache from komgapi import komgapi as KOMGAPI from typing import Any, Dict, List from limit import limit import loguru import sys from komsuite_nyaapy import Nyaa log = loguru.logger log.remove() log.add("application.log", rotation="1 week", retention="1 month") log.add(sys.stdout) app = Quart(__name__) settings = KomConfig() # else: # cache.create_table( # "CREATE TABLE IF NOT EXISTS manga_requests (id INT AUTO_INCREMENT PRIMARY KEY, anilist_id INT NOT NULL, grabbed BOOLEAN DEFAULT 0)" komga = KOMGAPI( url=settings.komga.url, username=settings.komga.user, password=settings.komga.password, ) # komga_series = komga.seriesList() @limit(limit=1, every=2) async def fetch_data( inputdata: Dict[str, Any], check_downloads: bool = False ) -> List[Dict[str, Any]]: log.error(f"fetch_data called with data: {inputdata}") async with httpx.AsyncClient() as client: try: # Log the incoming data for debugging # Ensure 'query' key exists in the data dictionary if "query" not in inputdata or not inputdata["query"]: raise ValueError("Missing or empty 'query' key in data") variables: Dict[str, Any] = {"search": inputdata["query"]} if inputdata.get("genres"): variables["genres"] = inputdata["genres"] if inputdata.get("tags"): variables["tags"] = inputdata["tags"] if inputdata.get("type"): if inputdata["type"] in ["MANGA", "NOVEL"]: variables["format"] = inputdata["type"] log.debug(f"GraphQL variables: {variables}") response = await client.post( "https://graphql.anilist.co", json={ "query": REQUESTS_QUERY, "variables": variables, }, headers={ "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "KomPage Searcher API/1.0 (contact: discord-id: 908987973264089139)", }, ) response.raise_for_status() data = response.json() log.debug(f"GraphQL response: {data}") results: List[Dict[str, Any]] = [] cache = KomCache() for item in data.get("data", {}).get("Page", {}).get("media", []): manga = Manga(**item) in_komga = komga.getSeries( manga.title.english if manga.title.english else manga.title.romaji ) requested = cache.fetch_one( query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = :id", args={"id": manga.id}, ) komga_request = bool(requested) results.append( { "id": manga.id, "title": manga.title.english if manga.title.english else manga.title.romaji, "image": manga.coverImage.get("large") if manga.coverImage else "https://demofree.sirv.com/nope-not-here.jpg", "status": manga.status, "type": manga.format, "genres": manga.genres or [], "tags": [tag.name for tag in (manga.tags or [])], "description": manga.description.replace("
", "\n") if manga.description else "No description available", "isAdult": manga.isAdult, "in_komga": in_komga, "requested": komga_request, "siteUrl": manga.siteUrl, } ) if check_downloads: for result in results: downloads = Nyaa().search(result.get("title"), 3, 1) else: downloads = [] result["download"] = len(downloads) if downloads else 0 log.debug(f"Fetched {len(results)} results for query: {inputdata['query']}") log.error(f"Results: {results}") return results except ValueError as ve: log.error(f"ValueError in fetch_data: {ve}") return [] except httpx.RequestError as e: log.error(f"HTTP request error in fetch_data: {e}") return [] except Exception as e: log.error(f"Unexpected error in fetch_data: {e}") return [] @limit(limit=1, every=2) async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]: requested_data: list[dict[str, Any]] = [] cache = KomCache() for manga_id in data: async with httpx.AsyncClient() as client: try: variables: Dict[str, Any] = { "search": manga_id, "type": "MANGA", } # TODO: replace the type once the UI has been updated # print(data, variables) response = await client.post( "https://graphql.anilist.co", json={ "query": REQUESTED_QUERY, "variables": variables, }, ) response.raise_for_status() data = response.json() # print(data) entry = data.get("data", {}).get("Media", {}) if entry: manga = Manga(**entry) in_komga = komga.getSeries( manga.title.english if manga.title.english else manga.title.romaji ) requested = cache.fetch_one( query="SELECT grabbed FROM manga_requests WHERE manga_id = :id", args={"id": manga.id}, ) komga_request = bool(requested) requested_data.append( { "id": manga.id, "title": manga.title.english if manga.title.english else manga.title.romaji, "image": manga.coverImage.get("large") if manga.coverImage else None, "status": manga.status, "type": manga.format, "genres": manga.genres or [], "tags": [tag.name for tag in (manga.tags or [])], "description": manga.description.replace("
", "\n") if manga.description else None, "isAdult": manga.isAdult, "in_komga": bool(in_komga), "requested": bool(komga_request), } ) log.debug( requested_data, ) except httpx.RequestError as e: print(f"An error occurred while requesting data: {e}") requested_data.append({"id": manga_id}) except Exception as e: print(f"Unexpected error: {e}") requested_data.append({"id": manga_id}) return requested_data @app.route("/api/genres") async def get_genres(): async with httpx.AsyncClient() as client: try: response = await client.post( "https://graphql.anilist.co", json={"query": GENRES_QUERY}, ) response.raise_for_status() data = response.json() results = data.get("data", {}).get("genres", []) return jsonify(results) except Exception as e: print(f"Error fetching genres: {e}") return jsonify([]) @app.route("/api/tags") async def get_tags(): async with httpx.AsyncClient() as client: try: response = await client.post( "https://graphql.anilist.co", json={"query": TAGS_QUERY}, ) response.raise_for_status() data = response.json() results = data.get("data", {}).get("tags", []) return jsonify(results) except Exception as e: print(f"Error fetching genres: {e}") return jsonify([]) @app.route("/search", methods=["POST"]) async def search(): data = await request.get_json() print(data) results = await fetch_data(data) if not results: return jsonify({"error": "No results found"}), 404 return jsonify(results) @app.route("/api/search", methods=["GET"]) async def api_search(): """ API endpoint for searching manga. Parameters: - q: Query string to search for Returns: - JSON array of matching manga entries """ try: query = request.args.get("q") if not query: return jsonify({"error": "Missing required parameter: q"}), 400 # Build search parameters search_data: Dict[str, Any] = {"query": query} # Add optional filters if provided log.info(f"API search request: {search_data}") # Fetch results using existing fetch_data function results = await fetch_data(search_data, check_downloads=False) if not results: return jsonify({"results": [], "count": 0}), 404 return jsonify({"results": results, "count": len(results)}) except Exception as e: log.error(f"Error in API search: {e}") return jsonify({"error": "An unexpected error occurred"}), 500 @app.route("/", methods=["GET"]) async def index(): return await render_template("index.html") # , komga_series=komga_series) @app.route("/request", methods=["POST"]) async def log_request(): data = await request.get_json() log.debug(f"Received request data: {data}") item = data.get("item") if item: data = await fetch_requested_data([item]) if not data: return jsonify({"status": "failed", "message": "Item not found"}), 404 data = data[0] title = data.get("title") image_url = data.get("image") log.debug( f"Logging request for item: {item}, title: {title}, image_url: {image_url}" ) asynccache = KomCache() asynccache.insert( f"INSERT INTO manga_requests (manga_id, title, image) VALUES ({item}, '{title}', :image)", args={"image": image_url}, ) return jsonify({"status": "success"}) return jsonify({"status": "failed"}), 400 @app.route("/requests", methods=["GET"]) async def requests_page(): cache = KomCache() requests = ( cache.fetch_all( query="SELECT manga_id, title, image grabbed FROM manga_requests" ) or [] ) entries: List[Dict[str, Any]] = [] req_ids = [req[0] for req in requests] if req_ids: entries = [ {"manga_id": req[0], "title": req[1], "image": req[2]} for req in requests ] else: entries = [] return await render_template("requests.html", requests=entries) @app.route("/delete", methods=["POST"]) async def delete_request(): # Delete a request from the database. ID is sent after the /delete endpoint, so: /delete/ data = await request.get_json() item_id = data.get("item") if item_id: asynccache = KomCache() asynccache.query( "DELETE FROM manga_requests WHERE manga_id = :id", args={"id": item_id} ) return jsonify({"status": "success"}) return jsonify({"status": "failed"}), 400 if __name__ == "__main__": log.info("Starting Komga Manga Grabber API") # use hypercorn to run the app import uvicorn uvicorn.run(app, host="0.0.0.0", port=5001) # run in test mode