---
bundle_template: true
please_customize: true
type: referenz
description: "CLI-Quirks des notebooklm-CLI-Wrappers, die der NotebookLM Kurator bei jedem Aufruf beachten muss. Pflichtlektüre vor jedem Helper, der das CLI anspricht."
tags:
  - notebooklm
  - kurator
  - cli
  - referenz
  - cheatsheet
---

# NotebookLM CLI — Cheatsheet für den Kurator

Dies sind die Stolperfallen des `notebooklm`-CLI-Wrappers, die der Kurator zwingend einhalten muss. Jeder Helper, der ein CLI-Kommando absetzt, liest vorher diese Datei.

## 1. `PYTHONIOENCODING=utf-8` ist Pflicht

Ohne diese Umgebungsvariable crasht `source wait` (und einige andere Kommandos) auf Windows mit einem `UnicodeEncodeError`, weil stdout sonst auf `cp1252` fällt und Emojis oder Umlaute nicht kodieren kann.

**Beispiel (korrekt):**

```bash
PYTHONIOENCODING=utf-8 notebooklm source wait <source-id> --notebook <notebook-uuid> --timeout 300
```

**Regel:** Jeder CLI-Aufruf des Kurators setzt diese Variable, egal ob der konkrete Befehl aktuell crashen würde oder nicht. Konsistenz vor Optimierung.

## 2. `source fulltext --json`: Matched-Vorzeile plus Feldname ist `content`

`source fulltext --json` schreibt **vor** das JSON eine Zeile der Form:

```
Matched: <source-id-prefix> (Titel der Quelle)
{
  "source_id": "...",
  "title": "...",
  "content": "...",
  "_type_code": "...",
  "url": "https://...",
  "char_count": 18501
}
```

Ein naives `json.loads(stdout)` crasht damit. Der Parser muss bis zur ersten `{` trimmen.

**Wichtig — der Fulltext liegt unter `content`, nicht `fulltext`.** Eine spätere Korrektur (2026-04-09) hat das bestätigt: ein Test mit einem 19-Min-YouTube-Video hat gezeigt, dass das Feld tatsächlich `content` heißt. Die API liefert daneben auch noch `char_count` — eine bequeme Schnell-Prüfung, wie viel Inhalt überhaupt extrahiert wurde.

**Beispiel (korrekt):**

```python
out = subprocess.check_output(
    ["notebooklm", "source", "fulltext", src_id, "--notebook", nb_id, "--json"],
    env={**os.environ, "PYTHONIOENCODING": "utf-8"},
    text=True,
)
json_start = out.index("{")
data = json.loads(out[json_start:])
fulltext = data["content"]       # primärer Feldname
char_count = data["char_count"]  # für Schnell-Plausibilitätschecks
```

Zur Defensive hat `helpers/content_verify.py` eine Fallback-Kette `fulltext → content → text` eingebaut, damit spätere CLI-Änderungen den Kurator nicht brechen. Primäre Erwartung ist aber `content`.

## 3. `source list --json` ist sauberes JSON, aber als Dict gewrapped

Keine `Matched:`-Vorzeile wie bei `fulltext`, aber der Output ist **nicht** ein bare Array der Quellen, sondern ein Dict mit dieser Struktur:

```json
{
  "sources": [
    {
      "id": "<source-uuid>",
      "title": "Titel der Quelle",
      "type": "SourceType.YOUTUBE",
      "url": "https://www.youtube.com/watch?v=...",
      "status": "ready",
      "status_id": 2,
      "created_at": "2026-03-23T16:26:08"
    }
  ],
  "count": 153
}
```

**Beispiel (korrekt):**

```python
out = subprocess.check_output(
    ["notebooklm", "source", "list", "--notebook", nb_id, "--json"],
    env={**os.environ, "PYTHONIOENCODING": "utf-8"},
    text=True,
)
data = json.loads(out)
sources = data["sources"]
count = data["count"]
```

**WICHTIG — das `url`-Feld ist nicht durchgängig gesetzt.** Bei einem echten Test gegen ein Notebook mit 153 Quellen (139 davon YouTube) war nur bei **14 Quellen** das `url`-Feld gefüllt. Die restlichen 125 YouTube-Quellen hatten `url: null`. Vermutung: Das Feld wird nur bei Quellen gefüllt, die über einen bestimmten API-Pfad hinzugefügt wurden, ältere Quellen haben es nicht. Konsequenz für den Kurator:

- **Primär** das `url`-Feld nutzen, wenn vorhanden.
- **Fallback**: `source fulltext` ziehen und die URL aus dem Fulltext-Anfang extrahieren (`resync.py` macht genau das).
- Niemals davon ausgehen, dass `url` bei jeder Quelle gesetzt ist.

## 4. `source add <datei.md>` braucht ABSOLUTEN Pfad

Mit relativem Pfad scheitert der Upload stumm: HTTP-Statuscode ist 200, die Quelle wird als `ready` markiert, aber der Content ist Schrott (Pasted-Text-Fallback mit einer Fehlermeldung als Inhalt, statt der Markdown-Datei).

**Beispiel (falsch):**

```bash
notebooklm source add ./backup.md --notebook <notebook-uuid>   # landet im Pasted-Text-Fallback
```

**Beispiel (korrekt):**

```bash
notebooklm source add "C:/Beispiel-Vault/.../backup.md" --notebook <notebook-uuid>
```

**Regel:** Vor jedem `source add` einer lokalen Datei per `Path.resolve()` oder `os.path.abspath()` in einen absoluten Pfad wandeln. Und nach jedem Upload per `content_verify` prüfen, ob der echte Inhalt angekommen ist.

## 5. Titel einer hochgeladenen `.md`-Datei = Dateiname

Der `title:`-Wert im Frontmatter der hochgeladenen Markdown-Datei wird von NotebookLM **ignoriert**. Der angezeigte Titel ist immer der Dateiname ohne Extension.

**Konsequenz:** Wenn der Kurator einen bestimmten Titel im Notebook haben will, muss er die Datei entsprechend benennen. Das ist der Grund, warum Backup-Dateinamen in `backup.py` mit einem ASCII-Slug gebaut werden (Windows-CLI mag Umlaute in Dateinamen nicht zuverlässig) — der Original-Titel lebt im Backup-Frontmatter weiter.

## 6. `source delete` ist irreversibel

Es gibt keinen Papierkorb. Einmal `source delete` ist weg. Das ist der Grund für die Backup-Pflicht.

**Beispiel:**

```bash
notebooklm source delete <source-id> --notebook <notebook-uuid> -y
```

**Regel:** Der Kurator ruft dieses Kommando in Phase 1 **niemals ohne Approval des Nutzers** und **niemals ohne vorher geschriebenes Backup** auf. Die Helper `backup.py` und der Haupt-Ablauf in `SKILL.md` verdrahten beide Bedingungen als harte Vorbedingungen.

## 7. Kein Auto-Dedup

Zweimal `source add` mit derselben URL erzeugt zwei Quellen mit unterschiedlichen Source-IDs. NotebookLM prüft nicht, ob eine URL schon existiert.

**Konsequenz:** Der Kurator braucht einen eigenen URL-Hash-Index (`state/url-hashes.json`), der vor jedem `source add` konsultiert wird. Die Normalisierung in `url_extract.py` muss so strikt sein, dass `youtu.be/X`, `youtube.com/watch?v=X` und `youtube.com/watch?v=X&t=42&si=abc` alle auf denselben Hash fallen.

## 8. Partial-IDs erst ab 6 Zeichen, Automation nutzt volle UUIDs

Das CLI akzeptiert Präfixe ab 6 Zeichen zum manuellen Arbeiten (`notebooklm source fulltext <source-id-prefix> --notebook ...`). In Automation ist das zu riskant, weil bei 300 Quellen Kollisionen möglich werden.

**Regel:** Der Kurator speichert und benutzt immer die volle UUID (sowohl `source_id` als auch `notebook_id`). Partial-IDs tauchen höchstens in menschenlesbaren Audit-Log-Zeilen als Hilfe auf.

## 9. Immer `--notebook <id>` mitgeben, nicht auf `notebooklm use` vertrauen

Der CLI-Wrapper merkt sich zwar per `notebooklm use` den aktuellen Notebook-Kontext, aber das ist globaler Zustand, der zwischen Aufrufen wechseln kann (z.B. durch einen parallelen Aufruf in einem anderen Terminal). Der Kurator verlässt sich nicht darauf.

**Regel:** Jedes `source`-Kommando bekommt explizit `--notebook <id>` mit, mit der vollen UUID. Der aktive Kontext wird höchstens zur Diagnose in `kurator-meta.json` mitgeschrieben.
