bollwerk/import-books-prepper.py

272 lines
10 KiB
Python
Raw Permalink 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 Prepper-/Survival-Bücher (Project Gutenberg) in die Bollwerk-Ressourcen.
Verwendung:
python import-books-prepper.py
python import-books-prepper.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": 521,
"title": "The Life and Adventures of Robinson Crusoe",
"author": "Daniel Defoe",
"description": "Der Urtext des Survival-Genres: 28 Jahre allein auf einer Insel. Crusoe baut Unterkunft, züchtet Getreide, fertigt Werkzeuge ein Lehrbuch der Selbstversorgung in Romanform.",
"tags": ["abenteuer", "handbuch"],
"language": "en",
"release_date": "1719-04-25",
"edition": "First Edition",
},
{
"gutenberg_id": 205,
"title": "Walden, and On The Duty Of Civil Disobedience",
"author": "Henry David Thoreau",
"description": "Thoreaus Experiment des einfachen Lebens in der Wildnis. Philosophie der Selbstversorgung, Konsumkritik und bewusstem Verzicht das intellektuelle Fundament jeder Prepper-Bewegung.",
"tags": ["nachschlagewerk", "handbuch"],
"language": "en",
"release_date": "1854-08-09",
"edition": "First Edition",
},
{
"gutenberg_id": 28255,
"title": "Shelters, Shacks and Shanties",
"author": "Daniel Carter Beard",
"description": "Praktische Anleitung zum Bau von Unterkünften aus Naturmaterialien von einfachen Notschutzhütten bis zu Blockhäusern. Mit detaillierten Illustrationen und Schritt-für-Schritt-Anleitungen.",
"tags": ["handbuch"],
"language": "en",
"release_date": "1914-01-01",
"edition": "First Edition",
},
{
"gutenberg_id": 34607,
"title": "Woodcraft and Camping",
"author": "George Washington Sears",
"description": "Klassiker der Outdoor-Literatur: Ausrüstung, Feuer machen, Lagerplatz wählen, Kochen im Freien, Kanufahren. Geschrieben von 'Nessmuk', dem Vater des Leichtgewicht-Campings.",
"tags": ["handbuch"],
"language": "en",
"release_date": "1884-01-01",
"edition": "First Edition",
},
{
"gutenberg_id": 15158,
"title": "In Time of Emergency",
"author": "United States Office of Civil Defense",
"description": "Offizielles US-Handbuch für den Katastrophenfall (1968): Atomarer Angriff, Naturkatastrophen, Evakuierung, Strahlenschutz, Wasservorräte, Erste Hilfe. DAS Prepper-Dokument der Kalten-Krieg-Ära.",
"tags": ["handbuch", "nachschlagewerk"],
"language": "en",
"release_date": "1968-01-01",
"edition": "First Edition",
},
{
"gutenberg_id": 44215,
"title": "The Book of Camp-Lore and Woodcraft",
"author": "Daniel Carter Beard",
"description": "Umfassendes Nachschlagewerk für das Leben im Freien: Feuermachen ohne Streichhölzer, essbare Wildpflanzen, Orientierung, Fallen bauen, Signalgebung und Notfallmedizin.",
"tags": ["handbuch"],
"language": "en",
"release_date": "1920-01-01",
"edition": "First Edition",
},
{
"gutenberg_id": 34501,
"title": "Wolf and Coyote Trapping",
"author": "A. R. Harding",
"description": "Detailliertes Praxishandbuch: Fallenbau, Köder, Fährten lesen, Pelzverarbeitung. Unverzichtbares Wissen für die Nahrungsbeschaffung und den Schutz von Nutztieren in der Wildnis.",
"tags": ["handbuch"],
"language": "en",
"release_date": "1909-01-01",
"edition": "First Edition",
},
{
"gutenberg_id": 49513,
"title": "The Complete Herbal",
"author": "Nicholas Culpeper",
"description": "Umfassendes Kräuterbuch mit hunderten Heilpflanzen, ihren Eigenschaften und medizinischen Anwendungen. Seit 1653 die Referenz für Pflanzenheilkunde unverzichtbar, wenn Apotheken fehlen.",
"tags": ["nachschlagewerk", "handbuch"],
"language": "en",
"release_date": "1653-01-01",
"edition": "Original Edition",
},
{
"gutenberg_id": 9550,
"title": "Manual of Gardening",
"author": "L. H. Bailey",
"description": "Vollständiges Garten-Handbuch: Bodenbearbeitung, Aussaat, Schädlingsbekämpfung, Obstbau, Gemüseanbau. Praktisches Wissen für die Selbstversorgung mit Nahrungsmitteln.",
"tags": ["handbuch"],
"language": "en",
"release_date": "1910-01-01",
"edition": "Second Edition",
},
{
"gutenberg_id": 59977,
"title": "Canning, Freezing, Storing Garden Produce",
"author": "United States Department of Agriculture",
"description": "USDA-Anleitung zur Haltbarmachung von Lebensmitteln: Einkochen, Einfrieren, Trocknen, Lagern. Überlebenswichtiges Wissen, um Ernteüberschüsse über den Winter zu retten.",
"tags": ["handbuch"],
"language": "en",
"release_date": "1965-01-01",
"edition": "USDA Bulletin",
},
]
# ---------------------------------------------------------------------------
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} Prepper / Survival) ===")
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()