Files
platform/gateway/integrations/kindle.py
Yusuf Suleman 7cd81181ed Refactor gateway into modular architecture
Split 1878-line server.py into 15 focused modules:
- config.py: all env vars and constants
- database.py: schema, init, seed logic
- sessions.py: session/token CRUD
- proxy.py: proxy_request, SERVICE_MAP, resolve_service
- responses.py: ResponseMixin for handler helpers
- auth.py: login/logout/register handlers
- dashboard.py: dashboard, apps, connections, pinning
- command.py: AI command bar
- integrations/booklore.py: auth, books, cover, import
- integrations/kindle.py: send-to-kindle, file finder
- integrations/karakeep.py: save/delete bookmarks
- integrations/qbittorrent.py: download status
- integrations/image_proxy.py: external image proxy

server.py is now thin routing only (~344 lines).
All routes, methods, status codes, and responses preserved exactly.
Added PYTHONUNBUFFERED=1 to Dockerfile for live logging.
2026-03-29 00:14:46 -05:00

203 lines
7.2 KiB
Python

"""
Platform Gateway — Kindle integration (send books via SMTP2GO).
"""
import json
import base64
import urllib.request
from pathlib import Path
from config import (
BOOKLORE_URL, BOOKLORE_BOOKS_DIR, BOOKDROP_DIR,
SMTP2GO_API_KEY, SMTP2GO_FROM_EMAIL, SMTP2GO_FROM_NAME,
KINDLE_EMAIL_1, KINDLE_EMAIL_2,
)
from proxy import proxy_request
from integrations.booklore import booklore_auth
def _find_book_file(book_id: str) -> tuple:
"""Find the actual ebook file for a Booklore book ID.
Returns (file_path, book_metadata) or (None, None)."""
token = booklore_auth()
if not token:
return None, None
# Get book metadata from Booklore
s, _, r = proxy_request(
f"{BOOKLORE_URL}/api/v1/books/{book_id}", "GET",
{"Authorization": f"Bearer {token}"}, timeout=10
)
if s != 200:
return None, None
book = json.loads(r)
meta = book.get("metadata", {})
title = meta.get("title", "")
if not title or not BOOKLORE_BOOKS_DIR.exists():
return None, meta
# Search for the file in the library directory
title_lower = title.lower()
title_words = set(title_lower.split()[:4]) # First 4 words for matching
best_match = None
best_score = 0
for ext in ["epub", "pdf", "mobi", "azw3"]:
for filepath in BOOKLORE_BOOKS_DIR.rglob(f"*.{ext}"):
fname = filepath.stem.lower()
# Check if title words appear in filename
matches = sum(1 for w in title_words if w in fname)
score = matches / len(title_words) if title_words else 0
# Prefer epub > pdf > mobi > azw3
ext_bonus = {"epub": 0.1, "pdf": 0.05, "mobi": 0.03, "azw3": 0.02}.get(ext, 0)
score += ext_bonus
if score > best_score:
best_score = score
best_match = filepath
if best_match and best_score >= 0.5:
return best_match, meta
return None, meta
def handle_send_to_kindle(handler, book_id: str, body: bytes):
"""Send a book file to a Kindle email via SMTP2GO API."""
if not SMTP2GO_API_KEY or not SMTP2GO_FROM_EMAIL:
handler._send_json({"error": "Email not configured"}, 502)
return
try:
data = json.loads(body)
except Exception as e:
handler._send_json({"error": "Invalid JSON"}, 400)
return
target = data.get("target", "1")
kindle_email = KINDLE_EMAIL_1 if target == "1" else KINDLE_EMAIL_2
if not kindle_email:
handler._send_json({"error": f"Kindle target {target} not configured"}, 400)
return
# Find the book file
file_path, meta = _find_book_file(book_id)
if not file_path or not file_path.exists():
handler._send_json({"error": "Book file not found on disk"}, 404)
return
title = meta.get("title", "Book") if meta else "Book"
author = ", ".join(meta.get("authors", [])) if meta else ""
# Read file and encode as base64
file_data = file_path.read_bytes()
file_b64 = base64.b64encode(file_data).decode("ascii")
filename = file_path.name
# Determine MIME type
ext = file_path.suffix.lower()
mime_map = {".epub": "application/epub+zip", ".pdf": "application/pdf", ".mobi": "application/x-mobipocket-ebook", ".azw3": "application/x-mobi8-ebook"}
mime_type = mime_map.get(ext, "application/octet-stream")
# Send via SMTP2GO API
email_payload = {
"api_key": SMTP2GO_API_KEY,
"sender": f"{SMTP2GO_FROM_NAME} <{SMTP2GO_FROM_EMAIL}>",
"to": [kindle_email],
"subject": f"{title}" + (f" - {author}" if author else ""),
"text_body": f"Sent from Platform: {title}" + (f" by {author}" if author else ""),
"attachments": [{
"filename": filename,
"fileblob": file_b64,
"mimetype": mime_type,
}]
}
try:
req_body = json.dumps(email_payload).encode("utf-8")
req = urllib.request.Request(
"https://api.smtp2go.com/v3/email/send",
data=req_body,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read())
if result.get("data", {}).get("succeeded", 0) > 0:
handler._send_json({
"success": True,
"title": title,
"sentTo": kindle_email,
"format": ext.lstrip(".").upper(),
"size": len(file_data),
})
else:
handler._send_json({"error": "Email send failed", "detail": result}, 500)
except Exception as e:
handler._send_json({"error": f"SMTP2GO error: {str(e)}"}, 500)
def handle_send_file_to_kindle(handler, body: bytes):
"""Send a downloaded file to Kindle by filename from bookdrop directory."""
if not SMTP2GO_API_KEY or not SMTP2GO_FROM_EMAIL:
handler._send_json({"error": "Email not configured"}, 502)
return
try:
data = json.loads(body)
except Exception as e:
handler._send_json({"error": "Invalid JSON"}, 400)
return
filename = data.get("filename", "")
target = data.get("target", "1")
title = data.get("title", filename)
kindle_email = KINDLE_EMAIL_1 if target == "1" else KINDLE_EMAIL_2
if not kindle_email:
handler._send_json({"error": f"Kindle target {target} not configured"}, 400)
return
# Find file in bookdrop or booklore-books
file_path = None
for search_dir in [BOOKDROP_DIR, BOOKLORE_BOOKS_DIR]:
if not search_dir.exists():
continue
for fp in search_dir.rglob("*"):
if fp.is_file() and fp.name == filename:
file_path = fp
break
if file_path:
break
if not file_path or not file_path.exists():
handler._send_json({"error": f"File not found: {filename}"}, 404)
return
file_data = file_path.read_bytes()
file_b64 = base64.b64encode(file_data).decode("ascii")
ext = file_path.suffix.lower()
mime_map = {".epub": "application/epub+zip", ".pdf": "application/pdf", ".mobi": "application/x-mobipocket-ebook", ".azw3": "application/x-mobi8-ebook"}
mime_type = mime_map.get(ext, "application/octet-stream")
email_payload = {
"api_key": SMTP2GO_API_KEY,
"sender": f"{SMTP2GO_FROM_NAME} <{SMTP2GO_FROM_EMAIL}>",
"to": [kindle_email],
"subject": title,
"text_body": f"Sent from Platform: {title}",
"attachments": [{"filename": filename, "fileblob": file_b64, "mimetype": mime_type}]
}
try:
req_body = json.dumps(email_payload).encode("utf-8")
req = urllib.request.Request("https://api.smtp2go.com/v3/email/send", data=req_body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read())
if result.get("data", {}).get("succeeded", 0) > 0:
handler._send_json({"success": True, "title": title, "sentTo": kindle_email, "format": ext.lstrip(".").upper()})
else:
handler._send_json({"error": "Email send failed", "detail": result}, 500)
except Exception as e:
handler._send_json({"error": f"SMTP2GO error: {str(e)}"}, 500)