# -*- coding: utf-8 -*- """ Importiert 10 freie Handwerk-/Technik-Bücher (Project Gutenberg) in die Bollwerk-Ressourcen. Fokus: Nützliches Wissen für die Postapokalypse. Verwendung: python import-books-handwerk.py python import-books-handwerk.py --dry-run """ import json import os import sys import time import uuid import urllib.request import urllib.error SERVER_URL = "https://bollwerk.online" BOOKS = [ { "gutenberg_id": 10136, "title": "The Book of Household Management", "author": "Mrs. Beeton", "description": "Das viktorianische Standardwerk für Haushaltsführung: Kochen, Konservieren, Reinigung, Krankenpflege, Tierhaltung, Budgetplanung. Ein Lexikon der praktischen Lebenskunst in 2000+ Seiten.", "tags": ["nachschlagewerk", "handbuch"], "language": "en", "release_date": "1861-10-01", "edition": "First Edition", }, { "gutenberg_id": 13510, "title": "Knots, Splices and Rope Work", "author": "A. Hyatt Verrill", "description": "Vollständige Anleitung zu Knoten, Spleißen und Seilwerk: über 100 Knoten mit Abbildungen. Unverzichtbar für Bauen, Sichern, Transportieren und Überleben ohne moderne Hilfsmittel.", "tags": ["handbuch"], "language": "en", "release_date": "1917-01-01", "edition": "First Edition", }, { "gutenberg_id": 47760, "title": "Three Hundred Things a Bright Boy Can Do", "author": "Various", "description": "Praktisches DIY-Kompendium: Elektrik, Mechanik, Holzarbeiten, Metallbearbeitung, Fotografie, Modellbau. 300 Projekte mit detaillierten Bauanleitungen – ein Maker-Handbuch aus dem frühen 20. Jahrhundert.", "tags": ["handbuch"], "language": "en", "release_date": "1911-01-01", "edition": "First Edition", }, { "gutenberg_id": 56601, "title": "A Text-book of Tanning", "author": "H. R. Procter", "description": "Wissenschaftliches Lehrbuch der Lederherstellung: Gerbstoffe, Hautbehandlung, pflanzliches und chemisches Gerben. Essentielles Handwerk für Kleidung, Schuhe und Ausrüstung ohne Industrie.", "tags": ["handbuch", "nachschlagewerk"], "language": "en", "release_date": "1885-01-01", "edition": "First Edition", }, { "gutenberg_id": 67636, "title": "The Useful Arts Employed in the Construction of Dwelling Houses", "author": "Anonymous", "description": "Detailliertes Handbuch über alle Gewerke beim Hausbau: Mauerwerk, Zimmerei, Dachdecken, Klempnerei, Verglasung, Putz und Anstrich. Praxis-Referenz für den Wiederaufbau.", "tags": ["handbuch"], "language": "en", "release_date": "1860-01-01", "edition": "Second Edition", }, { "gutenberg_id": 56776, "title": "Practical Hand Book of Gas, Oil and Steam Engines", "author": "John B. Rathbun", "description": "Praxishandbuch für Verbrennungs- und Dampfmotoren: Aufbau, Wartung, Reparatur, Fehlersuche. Wissen, um Energieerzeugung und Antriebstechnik ohne Stromnetz zu beherrschen.", "tags": ["handbuch"], "language": "en", "release_date": "1916-01-01", "edition": "First Edition", }, { "gutenberg_id": 21682, "title": "The Field and Garden Vegetables of America", "author": "Fearing Burr", "description": "Enzyklopädie mit fast 1100 Gemüsesorten: Beschreibung, Anbau, Ernte, Lagerung. Das umfassendste Nachschlagewerk für Selbstversorger-Gartenbau in der Public Domain.", "tags": ["nachschlagewerk"], "language": "en", "release_date": "1863-01-01", "edition": "First Edition", }, { "gutenberg_id": 50451, "title": "A Boy's Workshop", "author": "Harry Craigin", "description": "Einführung in grundlegende Werkstattarbeiten: Holzverbindungen, Metallarbeiten, Werkzeugherstellung, einfache Maschinenbau-Projekte. Praktisches Handwerk von Grund auf erklärt.", "tags": ["handbuch"], "language": "en", "release_date": "1894-01-01", "edition": "First Edition", }, { "gutenberg_id": 53139, "title": "Maxims and Instructions for the Boiler Room", "author": "N. Hawkins", "description": "Praxiswissen für Dampfkesselbetrieb: Druckregelung, Brennstoffeffizienz, Sicherheit, Wartung. Schlüsselkompetenz für jede postapokalyptische Dampf-Infrastruktur.", "tags": ["handbuch"], "language": "en", "release_date": "1903-01-01", "edition": "First Edition", }, { "gutenberg_id": 65967, "title": "The History of Silk, Cotton, Linen, Wool, and Other Fibrous Substances", "author": "Clinton G. Gilroy", "description": "Geschichte und Technik der Textilherstellung: Spinnen, Weben, Färben von der Rohfaser zum fertigen Stoff. Grundlagenwissen, um ohne Textilindustrie Kleidung zu produzieren.", "tags": ["nachschlagewerk", "handbuch"], "language": "en", "release_date": "1845-01-01", "edition": "First Edition", }, ] # --------------------------------------------------------------------------- RESET = "\033[0m" GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" 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: 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: 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: 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: 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": "", } boundary = f"----BollwerkBoundary{uuid.uuid4().hex[:16]}" body = bytearray() 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" 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" 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 count = len(BOOKS) step(f"=== Bollwerk Bücher-Import ({count} Handwerk & Technik) ===") 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}/{count}] {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) 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()