bollwerk/import-books-handwerk.py

273 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 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()