{ "name": "Comix Reader", "version": "1.0.7", "author": "Animex", "description": "Comix.to Manga Reader - Fixed WAF validation, URL encoding, and Anti-Leech headers.", "type": "MANGA_READER", "requirements": ["httpx", "re", "json"] } --- import re import json import urllib.parse import inspect import httpx # Extended headers to satisfy WAF & Anti-Leech checks HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'application/json, text/plain, */*', 'Referer': 'https://comix.to/', 'X-Requested-With': 'XMLHttpRequest' } async def _smart_fetch(method: str, url: str, **kwargs): """Uses injected HybridClient if available, otherwise falls back to real httpx.""" client_or_lib = globals().get('httpx') func = getattr(client_or_lib, method.lower(), None) if func and inspect.iscoroutinefunction(func): resp = await func(url, **kwargs) return resp # Fallback: real httpx.AsyncClient import httpx as _real_httpx async with _real_httpx.AsyncClient(follow_redirects=True) as client: resp = await getattr(client, method.lower())(url, **kwargs) return resp def get_nested(data, *keys, default=None): """Helper to safely traverse deeply nested dictionaries even if keys are None.""" for key in keys: if isinstance(data, dict): data = data.get(key) else: return default return data if data is not None else default async def get_title_from_mal(mal_id: int): """Fetches the title from MyAnimeList via Jikan.""" url = f"https://api.jikan.moe/v4/manga/{mal_id}" try: resp = await _smart_fetch("GET", url) if not resp: return None data = resp.json() if hasattr(resp, "json") else None # Safe traversal of data['data']['title'] return get_nested(data, 'data', 'title') except Exception as e: print(f"[Comix] MAL Fetch Error: {e}") return None async def search_manga(query: str): """Searches Comix.to and returns (manga_id, hash_id, slug).""" if not query: return None # Safely URL encode parameters (prevents WAF bracket blocks) params = {"keyword": query, "order[relevance]": "desc"} qs = urllib.parse.urlencode(params) url = f"https://comix.to/api/v2/manga?{qs}" try: resp = await _smart_fetch("GET", url, headers=HEADERS) print(f"[Comix] Search status: {getattr(resp, 'status_code', 500)}") data = resp.json() if hasattr(resp, "json") else None if data is None: return None # Safely get first item from result -> items items = get_nested(data, 'result', 'items', default=[]) if items and isinstance(items, list) and len(items) > 0: first = items[0] if isinstance(first, dict): # Return manga_id too for WAF API fallback purposes return first.get('manga_id'), first.get('hash_id'), first.get('slug') except Exception as e: print(f"[Comix] Search Error: {e}") return None async def get_chapters(mal_id: int): try: print(f"[Comix] get_chapters called for MAL ID: {mal_id}") title = await get_title_from_mal(mal_id) if not title: return None manga_info = await search_manga(title) if not manga_info: return None manga_id, hash_id, slug = manga_info # Paginate since API caps at 100 per request all_items = [] page = 1 while True: # Dynamically spoof the Referer for the specific manga to bypass Anti-Leech req_headers = HEADERS.copy() req_headers["Referer"] = f"https://comix.to/title/{hash_id}-{slug}" # Safely encode the query string to prevent 403 Bracket Rejection params = { "order[number]": "asc", "limit": 100, "page": page } qs = urllib.parse.urlencode(params) # Primary Try: Try requesting chapters using the Hash ID url = f"https://comix.to/api/v2/manga/{hash_id}/chapters?{qs}&_=xQm9tJfLwGhz_0Eq8S_YAHYkwp-q1PLfm50W5QJnyd1NnNYpAjXjyCoAzoOLRgUaJOoxWS0NeDGz_rNrbqBjLLP1H9qi" resp = await _smart_fetch("GET", url, headers=req_headers) data = resp.json() if hasattr(resp, "json") else None # Fallback: If framework validation fails (400, 403, 404), it might be strictly # expecting the internal integer primary key instead of the string hash. if data and data.get("status") in [400, 403, 404]: print(f"[Comix] Hash ID rejected ({data.get('status')}). Falling back to Integer Manga ID...") url_fallback = f"https://comix.to/api/v2/manga/{manga_id}/chapters?{qs}&_=xQm9tJfLwGhz_0Eq8S_YAHYkwp-q1PLfm50W5QJnyd1NnNYpAjXjyCoAzoOLRgUaJOoxWS0NeDGz_rNrbqBjLLP1H9qi" resp = await _smart_fetch("GET", url_fallback, headers=req_headers) data = resp.json() if hasattr(resp, "json") else None if data is None or data.get("status") != 200: error_msg = data.get('message') if data else getattr(resp, 'text', '')[:200] print(f"[Comix] Bad response on page {page}: {error_msg}") break items = get_nested(data, 'result', 'items', default=[]) pagination = get_nested(data, 'result', 'pagination', default={}) last_page = pagination.get('last_page', 1) if not items: break all_items.extend(items) print(f"[Comix] Page {page}/{last_page} — got {len(items)} chapters, total so far: {len(all_items)}") if page >= last_page: break page += 1 seen_numbers = {} formatted = [] for item in all_items: if not isinstance(item, dict): continue num = str(item.get('number', '0')) # Safely fallback to any available ID key c_id = item.get('chapter_id') or item.get('id') or item.get('hash_id') if num not in seen_numbers: seen_numbers[num] = True formatted.append({ "title": item.get('name') or f"Chapter {num}", "url": f"{hash_id}:{slug}:{c_id}", "chapter_number": num, "is_external": False }) def safe_float(v): try: return float(v) except: return 0.0 formatted.sort(key=lambda x: safe_float(x['chapter_number']), reverse=True) return formatted except Exception as e: print(f"[Comix] Chapter Fetch Exception: {e}") import traceback traceback.print_exc() return None async def get_chapter_images(mal_id: int, chapter_num: str): """Public API: Scrapes image URLs from the chapter page.""" try: chapters = await get_chapters(mal_id) if not chapters: return None target_chapter = None target_f = None try: target_f = float(chapter_num) except: pass for ch in chapters: if target_f is not None: try: if float(ch["chapter_number"]) == target_f: target_chapter = ch break except: pass if ch["chapter_number"] == str(chapter_num): target_chapter = ch break if not target_chapter: return None hash_id, slug, chapter_id = target_chapter["url"].split(":") url = f"https://comix.to/title/{hash_id}-{slug}/{chapter_id}-chapter-{chapter_num}" # Mirror the Referer just as we do for chapters req_headers = HEADERS.copy() req_headers["Referer"] = f"https://comix.to/title/{hash_id}-{slug}" resp = await _smart_fetch("GET", url, headers=req_headers) if not resp or not hasattr(resp, "text"): return None regex = r'["\\]*images["\\]*\s*:\s*(\[[^\]]*\])' match = re.search(regex, resp.text, re.DOTALL) if match: raw_json = match.group(1).replace('\\"', '"') images_data = json.loads(raw_json) if isinstance(images_data, list): return [img['url'] for img in images_data if isinstance(img, dict) and 'url' in img] return [] except Exception as e: print(f"[Comix] Scraper Error: {e}") return None