bollwerk/import-books.py
Jens Reinemann 26117ac23f feat(app): add ResourceDetailScreen with markdown rendering and clickable cards
- ResourceDetailScreen: full metadata display, download button, markdown description
- ResourceListScreen: cards now clickable, navigates to detail view
- Navigation: added ResourceDetail route with guid parameter
- Dependencies: compose-markdown 0.5.4 (JitPack), added toRoute import
- settings.gradle.kts: added JitPack repository
2026-05-18 23:37:00 +02:00

291 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Importiert 10 freie Bücher (Project Gutenberg) in die Bollwerk-Ressourcen.
Voraussetzungen:
- Server läuft unter SERVER_URL
- Admin-Credentials als Env-Vars: BOLLWERK_ADMIN_USER, BOLLWERK_ADMIN_PASS
Verwendung:
python import-books.py
python import-books.py --dry-run (nur Download, kein Upload)
"""
import json
import os
import sys
import time
import uuid
import urllib.request
import urllib.error
from io import BytesIO
from email.mime.multipart import MIMEMultipart
SERVER_URL = "https://bollwerk.online"
# --- 10 ausgewählte freie Bücher (Project Gutenberg, Public Domain) ---
BOOKS = [
{
"gutenberg_id": 132,
"title": "The Art of War",
"author": "Sun Tzu",
"description": "Der älteste bekannte Militärstrategie-Text. Behandelt Taktik, Täuschung, Anpassungsfähigkeit und die Kunst, Konflikte ohne Kampf zu gewinnen.",
"tags": ["nachschlagewerk", "handbuch"],
"language": "en",
"release_date": "-500",
"edition": "Lionel Giles Translation, 1910",
},
{
"gutenberg_id": 2680,
"title": "Meditations",
"author": "Marcus Aurelius",
"description": "Persönliche Aufzeichnungen des römischen Kaisers über stoische Philosophie, Selbstdisziplin und innere Ruhe ein zeitloser Leitfaden für schwierige Zeiten.",
"tags": ["nachschlagewerk"],
"language": "en",
"release_date": "180",
"edition": "George Long Translation, 1862",
},
{
"gutenberg_id": 84,
"title": "Frankenstein; or, The Modern Prometheus",
"author": "Mary Wollstonecraft Shelley",
"description": "Der Urtext der Science-Fiction: Victor Frankenstein erschafft künstliches Leben und muss sich den Konsequenzen seiner Hybris stellen.",
"tags": ["novel"],
"language": "en",
"release_date": "1818-01-01",
"edition": "1831 Revised Edition",
},
{
"gutenberg_id": 345,
"title": "Dracula",
"author": "Bram Stoker",
"description": "Der Briefroman über den transsilvanischen Vampirgrafen Grundlage für unzählige Horror-RPG-Szenarien und der wohl einflussreichste Horrorroman überhaupt.",
"tags": ["novel", "abenteuer"],
"language": "en",
"release_date": "1897-05-26",
"edition": "First Edition",
},
{
"gutenberg_id": 8492,
"title": "The King in Yellow",
"author": "Robert W. Chambers",
"description": "Kurzgeschichtensammlung über ein übernatürliches Theaterstück, das Wahnsinn bringt. Inspiration für Lovecrafts Mythos und das Call-of-Cthulhu-Rollenspiel.",
"tags": ["novel", "quellenbuch"],
"language": "en",
"release_date": "1895-01-01",
"edition": "First Edition",
},
{
"gutenberg_id": 120,
"title": "Treasure Island",
"author": "Robert Louis Stevenson",
"description": "Der klassische Piraten-Abenteuerroman: Jim Hawkins, Long John Silver und die Jagd nach Captain Flints vergrabenen Schatz perfekte Vorlage für Rollenspiel-Szenarien.",
"tags": ["novel", "abenteuer"],
"language": "en",
"release_date": "1883-11-14",
"edition": "First Edition",
},
{
"gutenberg_id": 1661,
"title": "The Adventures of Sherlock Holmes",
"author": "Arthur Conan Doyle",
"description": "Zwölf Detektivgeschichten um den genialen Sherlock Holmes und Dr. Watson. Meisterhafte Rätsel, die sich hervorragend als Inspiration für Investigativ-Abenteuer eignen.",
"tags": ["novel", "abenteuer"],
"language": "en",
"release_date": "1892-10-14",
"edition": "First Edition",
},
{
"gutenberg_id": 1232,
"title": "The Prince",
"author": "Niccolò Machiavelli",
"description": "Die berühmte Abhandlung über politische Macht, Herrschaft und Staatskunst unverzichtbar als Hintergrundlektüre für Intrigen-Kampagnen.",
"tags": ["nachschlagewerk", "handbuch"],
"language": "en",
"release_date": "1532-01-01",
"edition": "W. K. Marriott Translation, 1908",
},
{
"gutenberg_id": 43,
"title": "The Strange Case of Dr. Jekyll and Mr. Hyde",
"author": "Robert Louis Stevenson",
"description": "Die Novelle über die dunkle Doppelnatur des Menschen ein Meisterwerk des viktorianischen Horrors und Inspiration für zahllose RPG-Antagonisten.",
"tags": ["novel"],
"language": "en",
"release_date": "1886-01-05",
"edition": "First Edition",
},
{
"gutenberg_id": 215,
"title": "The Call of the Wild",
"author": "Jack London",
"description": "Die Geschichte des Hundes Buck, der im Yukon zum Anführer eines Wolfsrudels wird. Ein packender Überlebensroman über Instinkt, Wildnis und Anpassung.",
"tags": ["novel", "abenteuer"],
"language": "en",
"release_date": "1903-01-01",
"edition": "First Edition",
},
]
# ---------------------------------------------------------------------------
RESET = "\033[0m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
CYAN = "\033[96m"
def ok(msg): print(f"{GREEN}[OK] {msg}{RESET}", flush=True)
def fail(msg): print(f"{RED}[!!] {msg}{RESET}", flush=True)
def step(msg): print(f"\n{YELLOW}{msg}{RESET}", flush=True)
def info(msg): print(f" {msg}", flush=True)
def login(username: str, password: str) -> str:
"""Login and return JWT access token."""
payload = json.dumps({"username": username, "password": password}).encode()
req = urllib.request.Request(
f"{SERVER_URL}/api/auth/login",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
return data["accessToken"]
except urllib.error.HTTPError as e:
body = e.read().decode() if e.fp else ""
fail(f"Login fehlgeschlagen: {e.code} {body}")
sys.exit(1)
def download_epub(gutenberg_id: int) -> bytes:
"""Download ePub from Project Gutenberg (prefer lightweight no-images version)."""
urls = [
f"https://www.gutenberg.org/ebooks/{gutenberg_id}.epub.noimages",
f"https://www.gutenberg.org/ebooks/{gutenberg_id}.epub3.images",
f"https://www.gutenberg.org/cache/epub/{gutenberg_id}/pg{gutenberg_id}.epub",
]
for url in urls:
try:
req = urllib.request.Request(url, headers={"User-Agent": "BollwerkImporter/1.0"})
with urllib.request.urlopen(req, timeout=60) as resp:
data = resp.read()
if len(data) > 1000: # sanity check
return data
except (urllib.error.HTTPError, urllib.error.URLError):
continue
raise RuntimeError(f"Konnte Gutenberg #{gutenberg_id} nicht herunterladen")
def upload_resource(token: str, book: dict, file_bytes: bytes) -> dict:
"""Upload resource via multipart POST to admin API."""
guid = str(uuid.uuid4())
now = int(time.time() * 1000)
metadata = {
"guid": guid,
"title": book["title"],
"description": book["description"],
"tags": book["tags"],
"fileFormat": "epub",
"mimeType": "application/epub+zip",
"fileSize": len(file_bytes),
"releaseDate": book.get("release_date"),
"createdAt": now,
"updatedAt": now,
"author": book.get("author"),
"language": book.get("language", "en"),
"edition": book.get("edition"),
"downloadUrl": "",
}
# Build multipart/form-data manually
boundary = f"----BollwerkBoundary{uuid.uuid4().hex[:16]}"
body = bytearray()
# Part 1: metadata JSON
body += f"--{boundary}\r\n".encode()
body += b'Content-Disposition: form-data; name="metadata"\r\n'
body += b"Content-Type: application/json\r\n\r\n"
body += json.dumps(metadata).encode()
body += b"\r\n"
# Part 2: file
body += f"--{boundary}\r\n".encode()
body += f'Content-Disposition: form-data; name="file"; filename="{guid}.epub"\r\n'.encode()
body += b"Content-Type: application/epub+zip\r\n\r\n"
body += file_bytes
body += b"\r\n"
# End boundary
body += f"--{boundary}--\r\n".encode()
req = urllib.request.Request(
f"{SERVER_URL}/api/admin/resources",
data=bytes(body),
headers={
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
raise RuntimeError(f"Upload fehlgeschlagen: {e.code} {error_body}")
def main():
dry_run = "--dry-run" in sys.argv
step("=== Bollwerk Bücher-Import (10 freie Klassiker) ===")
if not dry_run:
username = os.environ.get("BOLLWERK_ADMIN_USER", "")
password = os.environ.get("BOLLWERK_ADMIN_PASS", "")
if not username or not password:
fail("Setze BOLLWERK_ADMIN_USER und BOLLWERK_ADMIN_PASS als Env-Vars")
sys.exit(1)
step("1/3 Login als Admin...")
token = login(username, password)
ok(f"Eingeloggt als '{username}'")
else:
token = ""
info("DRY-RUN: Kein Login, kein Upload")
step("2/3 Bücher herunterladen...")
downloads = []
for i, book in enumerate(BOOKS, 1):
info(f" [{i:2d}/10] {book['title']} (Gutenberg #{book['gutenberg_id']})...")
try:
data = download_epub(book["gutenberg_id"])
downloads.append((book, data))
ok(f" {book['title']} {len(data) / 1024:.0f} KB")
except RuntimeError as e:
fail(f" {e}")
if not dry_run:
step("3/3 Upload auf Bollwerk-Server...")
success = 0
for i, (book, data) in enumerate(downloads, 1):
info(f" [{i:2d}/{len(downloads)}] {book['title']}...")
try:
result = upload_resource(token, book, data)
ok(f" {book['title']} → guid={result['guid']}")
success += 1
except RuntimeError as e:
fail(f" {e}")
time.sleep(0.5) # rate limiting
step(f"Fertig: {success}/{len(downloads)} Bücher erfolgreich importiert.")
else:
step(f"DRY-RUN abgeschlossen: {len(downloads)} Bücher heruntergeladen, 0 hochgeladen.")
if __name__ == "__main__":
main()