"""FlowDeck — Standard Notes importer (v5.6.0, Phase 4). Imports a Standard Notes backup (``.json``): each non-encrypted note becomes a FlowDeck page. Encrypted notes are reported as warnings. """ from __future__ import annotations import json from typing import Any from app.services.importers.base import ( Importer, ImportPage, ImportResult, decode_text, register_importer, ) _NOTE_TYPES = ("note", "org.standardnotes.sn", "org.standardnotes.plain-text") def _note_body(content: Any) -> tuple[str, str, bool]: """Return (title, markdown, encrypted).""" if isinstance(content, dict): if content.get("encrypted"): return "", "", True text = content.get("text") or content.get("preview_plain") or "" title = content.get("title") or "" return title, text, False if isinstance(content, str): stripped = content.strip() if stripped.startswith("{"): try: parsed = json.loads(stripped) if isinstance(parsed, dict) and ("encrypted" in parsed or "000" in parsed): return "", "", True if isinstance(parsed, dict): return parsed.get("title", ""), parsed.get("text", "") or parsed.get("preview_plain", ""), False except json.JSONDecodeError: pass return "", content, False return "", "", False @register_importer class StandardNotesImporter(Importer): source_id = "standard_notes" label = "Standard Notes" description = "Sauvegarde JSON Standard Notes → pages (notes chiffrées ignorées)." extensions = (".json",) order = 39 def _items(self, data: bytes) -> list[dict] | None: try: obj = json.loads(decode_text(data)) except Exception: # noqa: BLE001 return None if isinstance(obj, dict) and isinstance(obj.get("items"), list): return [x for x in obj["items"] if isinstance(x, dict)] return None def detect(self, filename: str, data: bytes) -> bool: if not filename.lower().endswith(".json"): return False items = self._items(data) if not items: return False return any("content_type" in it for it in items) def parse(self, filename: str, data: bytes) -> ImportResult: result = ImportResult(source=self.source_id) items = self._items(data) or [] count = 0 for item in items: if item.get("deleted"): continue ctype = str(item.get("content_type", "")).lower() if ctype and not any(t in ctype for t in _NOTE_TYPES): continue title, body, encrypted = _note_body(item.get("content")) if encrypted: result.warn("Note chiffrée ignorée (déchiffrement non pris en charge)") continue if not body.strip(): continue if not title: title = next((ln.strip(" #") for ln in body.splitlines() if ln.strip()), "Note") result.pages.append(ImportPage( title=title[:200] or "Note", markdown=body, source_path=item.get("uuid") or filename, external_id=item.get("uuid") or f"{filename}#{count}", )) count += 1 result.stats["rows"] = count return result.finalize()