- 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
291 lines
11 KiB
Python
291 lines
11 KiB
Python
# -*- 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()
|