""" Platform Gateway — Booklore integration (book library manager). """ import json import time from config import ( BOOKLORE_URL, BOOKLORE_USER, BOOKLORE_PASS, BOOKLORE_BOOKS_DIR, _booklore_token, ) from proxy import proxy_request def booklore_auth(): """Get a valid Booklore JWT token, refreshing if needed.""" global _booklore_token if _booklore_token["access"] and time.time() < _booklore_token["expires"] - 60: return _booklore_token["access"] if not BOOKLORE_USER or not BOOKLORE_PASS: return None try: body = json.dumps({"username": BOOKLORE_USER, "password": BOOKLORE_PASS}).encode() status, _, resp = proxy_request( f"{BOOKLORE_URL}/api/v1/auth/login", "POST", {"Content-Type": "application/json"}, body, timeout=10 ) if status == 200: data = json.loads(resp) _booklore_token["access"] = data["accessToken"] _booklore_token["refresh"] = data.get("refreshToken", "") _booklore_token["expires"] = time.time() + 3600 # 1hr return _booklore_token["access"] except Exception as e: print(f"[Booklore] Auth failed: {e}") return None def handle_booklore_libraries(handler): """Return Booklore libraries with their paths.""" token = booklore_auth() if not token: handler._send_json({"error": "Booklore auth failed"}, 502) return status, _, resp = proxy_request( f"{BOOKLORE_URL}/api/v1/libraries", "GET", {"Authorization": f"Bearer {token}"}, timeout=10 ) if status == 200: libs = json.loads(resp) result = [] for lib in libs: paths = [{"id": p["id"], "path": p.get("path", "")} for p in lib.get("paths", [])] result.append({"id": lib["id"], "name": lib["name"], "paths": paths}) handler._send_json({"libraries": result}) else: handler._send_json({"error": "Failed to fetch libraries"}, status) def handle_booklore_import(handler, body): """Auto-import a file from bookdrop into a Booklore library. Expects: {"fileName": "...", "libraryId": N, "pathId": N} Flow: rescan bookdrop -> find file -> finalize import """ try: data = json.loads(body) except Exception as e: handler._send_json({"error": "Invalid JSON"}, 400) return file_name = data.get("fileName", "") library_id = data.get("libraryId") path_id = data.get("pathId") if not file_name or not library_id or not path_id: handler._send_json({"error": "Missing fileName, libraryId, or pathId"}, 400) return token = booklore_auth() if not token: handler._send_json({"error": "Booklore auth failed"}, 502) return headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} # 1. Trigger bookdrop rescan proxy_request(f"{BOOKLORE_URL}/api/v1/bookdrop/rescan", "POST", headers, b"{}", timeout=15) # 2. Poll for the file to appear (up to 15 seconds) file_id = None file_meta = None for _ in range(6): time.sleep(2.5) s, _, r = proxy_request( f"{BOOKLORE_URL}/api/v1/bookdrop/files?status=pending&page=0&size=100", "GET", {"Authorization": f"Bearer {token}"}, timeout=10 ) if s == 200: files_data = json.loads(r) for f in files_data.get("content", []): if f.get("fileName", "") == file_name: file_id = f["id"] file_meta = f.get("originalMetadata") or f.get("fetchedMetadata") break if file_id: break if not file_id: handler._send_json({"error": "File not found in bookdrop after rescan", "fileName": file_name}, 404) return # 3. Build metadata with thumbnailUrl (required by Booklore) metadata = { "title": (file_meta or {}).get("title", file_name), "subtitle": "", "authors": (file_meta or {}).get("authors", []), "categories": (file_meta or {}).get("categories", []), "moods": [], "tags": [], "publisher": (file_meta or {}).get("publisher", ""), "publishedDate": (file_meta or {}).get("publishedDate", ""), "description": (file_meta or {}).get("description", ""), "isbn": (file_meta or {}).get("isbn13", (file_meta or {}).get("isbn10", "")), "language": (file_meta or {}).get("language", ""), "seriesName": (file_meta or {}).get("seriesName", ""), "seriesNumber": (file_meta or {}).get("seriesNumber"), "seriesTotal": (file_meta or {}).get("seriesTotal"), "thumbnailUrl": (file_meta or {}).get("thumbnailUrl", ""), } # 4. Finalize import payload = json.dumps({"files": [{"fileId": file_id, "libraryId": library_id, "pathId": path_id, "metadata": metadata}]}).encode() s, _, r = proxy_request( f"{BOOKLORE_URL}/api/v1/bookdrop/imports/finalize", "POST", headers, payload, timeout=30 ) if s == 200: result = json.loads(r) handler._send_json(result) else: print(f"[Booklore] Finalize failed ({s}): {r[:200]}") handler._send_json({"error": "Finalize import failed", "status": s}, s) def handle_booklore_books(handler): """Return all books from Booklore.""" token = booklore_auth() if not token: handler._send_json({"error": "Booklore auth failed"}, 502) return try: s, _, r = proxy_request( f"{BOOKLORE_URL}/api/v1/books", "GET", {"Authorization": f"Bearer {token}"}, timeout=15 ) if s == 200: books_raw = json.loads(r) books = [] for b in books_raw: m = b.get("metadata") or {} books.append({ "id": b["id"], "title": m.get("title") or "Untitled", "authors": m.get("authors") or [], "libraryId": b.get("libraryId"), "libraryName": b.get("libraryName"), "categories": m.get("categories") or [], "pageCount": m.get("pageCount"), "publisher": m.get("publisher"), "isbn13": m.get("isbn13"), "isbn10": m.get("isbn10"), "googleId": m.get("googleId"), "addedOn": b.get("addedOn"), }) # Resolve file formats from disk if BOOKLORE_BOOKS_DIR.exists(): # Build index: lowercase title words -> file path file_index = {} for ext in ["epub", "pdf", "mobi", "azw3"]: for fp in BOOKLORE_BOOKS_DIR.rglob(f"*.{ext}"): file_index[fp.stem.lower()] = fp for book in books: title_words = set(book["title"].lower().split()[:4]) best_match = None best_score = 0 for fname, fp in file_index.items(): matches = sum(1 for w in title_words if w in fname) score = matches / len(title_words) if title_words else 0 if score > best_score: best_score = score best_match = fp if best_match and best_score >= 0.5: book["format"] = best_match.suffix.lstrip(".").upper() else: book["format"] = None handler._send_json({"books": books, "total": len(books)}) else: handler._send_json({"error": "Failed to fetch books"}, s) except Exception as e: handler._send_json({"error": str(e)}, 500) def handle_booklore_cover(handler, book_id): """Proxy book cover image from Booklore.""" token = booklore_auth() if not token: handler._send_json({"error": "Booklore auth failed"}, 502) return try: s, headers_raw, body = proxy_request( f"{BOOKLORE_URL}/api/v1/books/{book_id}/cover", "GET", {"Authorization": f"Bearer {token}"}, timeout=10 ) if s == 200 and isinstance(body, bytes): ct = "image/jpeg" if isinstance(headers_raw, dict): ct = headers_raw.get("Content-Type", ct) handler.send_response(200) handler.send_header("Content-Type", ct) handler.send_header("Cache-Control", "public, max-age=86400") handler.end_headers() if isinstance(body, str): handler.wfile.write(body.encode()) else: handler.wfile.write(body) return handler._send_json({"error": "Cover not found"}, 404) except Exception as e: handler._send_json({"error": str(e)}, 500)