Metadata-Version: 2.4
Name: notability-extractor
Version: 0.9.1
Summary: Extract Notability Learn quizzes, summaries, and note text; export to Anki / JSON / Markdown with a PySide6 GUI
Project-URL: Homepage, https://github.com/mdeguzis/notability-extractor
Project-URL: Issues, https://github.com/mdeguzis/notability-extractor/issues
License: MIT
Keywords: anki,flashcards,gui,notability,ocr,pyside6,spaced-repetition
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Education
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Requires-Dist: genanki>=0.13.1
Requires-Dist: pyside6>=6.6
Description-Content-Type: text/markdown

# notability-extractor

[![CI Linux](https://github.com/mdeguzis/notability-extractor/actions/workflows/ci-linux.yml/badge.svg?branch=main)](https://github.com/mdeguzis/notability-extractor/actions/workflows/ci-linux.yml?query=branch%3Amain)
[![CI macOS](https://github.com/mdeguzis/notability-extractor/actions/workflows/ci-macos.yml/badge.svg?branch=main)](https://github.com/mdeguzis/notability-extractor/actions/workflows/ci-macos.yml?query=branch%3Amain)
[![PyPI version](https://img.shields.io/pypi/v/notability-extractor.svg)](https://pypi.org/project/notability-extractor/)
[![Python versions](https://img.shields.io/pypi/pyversions/notability-extractor.svg)](https://pypi.org/project/notability-extractor/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Extract Notability Learn content (AI-generated quizzes, summaries, and OCR'd
note text) and export it as an Anki `.apkg` deck, JSON, or Markdown for review.

## How it works

Notability Learn generates quizzes via Claude Haiku and summaries via Gemini
2.5 Flash, server-side. Both get cached locally in the app's HTTP cache.
Handwriting OCR and PDF text live inside `.nbn` note bundles.

The tool runs in two phases:

1. **Extract** (macOS only): walks the iCloud Drive `.nbn` bundles and the
   HTTP cache (`Cache.db` + `fsCachedData/`), writes a normalized export
   directory at `~/notability_export/`.
2. **Build** (any OS): reads the export directory, merges flashcards into a
   persistent JSONL archive at `~/.notability_extractor/cards.jsonl`, and
   emits outputs: `.apkg` for Anki, `.json` for programmatic review, `.md`
   for human reading.

Linux and Windows machines can skip phase 1 by pointing `--input-dir` at a
directory produced on a Mac. Useful if you want to do the Anki packaging on a
different machine than the one Notability runs on.

The JSONL archive is the source of truth for flashcards. Both the CLI and the
GUI read from and write to it, and the build always reconstructs `.apkg` from
the archive (not the input dir) so edits and tags persist across rebuilds.

## Install

### End users (recommended)

Install from PyPI with pip. You do NOT need to clone this repo and you do NOT
need `uv` or `make`.

**Linux:**

```bash
pip install --user notability-extractor
```

**macOS:**

```bash
# macOS only ships `pip3`, not `pip`. Use this:
pip3 install --user notability-extractor
# or equivalently:
python3 -m pip install --user notability-extractor
```

This installs two console scripts to `~/.local/bin/`:

- `notability-extractor` - the CLI
- `notability-extractor-gui` - the desktop GUI

Make sure `~/.local/bin` is on your `PATH`. On macOS that usually means adding
`export PATH="$HOME/.local/bin:$PATH"` to `~/.zshrc`.

The first time you launch `notability-extractor-gui`, it auto-installs a
desktop shortcut (a `.desktop` entry on Linux, a `.app` bundle in
`~/Applications/` on macOS) so you can launch it from your app menu next time.
If you'd rather skip that step, run `notability-extractor --remove-shortcut`.

### Developers

This part only matters if you want to hack on the code:

```bash
git clone https://github.com/mdeguzis/notability-extractor.git
cd notability-extractor
make install-dev   # requires `uv` - see https://docs.astral.sh/uv/
```

`make install-dev` uses [uv](https://docs.astral.sh/uv/) to set up `.venv/`
for dev work and drops the console scripts into `~/.local/bin/`. uv is a
dev-only tool here, end users don't need it. (`make install` is intentionally
not the dev target - it just prints these instructions, so people who try the
obvious thing don't accidentally pull in the dev toolchain.)

## Usage

```bash
# macOS: auto-extract and build all outputs in the current dir
notability-extractor

# Anywhere: build from a pre-extracted directory
notability-extractor --input-dir ~/notability_export

# Custom output directory
notability-extractor --input-dir ~/notability_export --out-dir ./decks

# macOS: just run phase 1 (produce export dir, no .apkg/json/md)
notability-extractor --extract-only

# Custom Anki deck name (only affects what shows up inside Anki)
notability-extractor --deck-name "Biology 101"
```

Output filenames are fixed:

| File | Contents |
|---|---|
| `notability_flashcards.apkg` | Anki deck (quiz questions only) |
| `notability_flashcards.json` | Full structured dump for programmatic review |
| `notability_flashcards.md` | Human-readable flashcards |
| `notability_notes.{json,md}` | Note transcripts |
| `notability_summaries.{json,md}` | Generated summaries |

### Archive management CLI flags

```bash
# Open a prompt-driven editor against the archive
notability-extractor --edit-flashcards

# One-shot interactive add
notability-extractor --add-card

# Print archive contents (optionally filter by tag)
notability-extractor --list-cards
notability-extractor --list-cards --tag biology

# Backup / restore round-trip
notability-extractor --backup
notability-extractor --export ~/cards-backup.jsonl
notability-extractor --import ~/cards-backup.jsonl --mode merge
notability-extractor --import ~/cards-backup.jsonl --mode replace

# Launch the GUI
notability-extractor --gui
```

## GUI

A PySide6 desktop companion ships in the same package. After install:

```bash
notability-extractor-gui
```

Pages: Library (browse / edit / add / delete cards with tag filter), Notes
(read-only), Summaries (read-only), Build (export apkg / json / md), Settings
(theme, paths, backups, schedule).

On Wayland with no `DISPLAY` set, the GUI auto-applies
`QT_QPA_PLATFORM=wayland` so SSH/login sessions don't need a manual export.

## Backups

The archive at `~/.notability_extractor/cards.jsonl` is snapshotted on every
save to `~/.notability_extractor/backups/cards-YYYYMMDD-HHMMSS.jsonl`,
hash-deduped so unchanged saves don't make redundant copies. Default retention
is the last 10 snapshots (configurable in Settings).

For a scheduled backup when the GUI is closed, run from cron:

```
0 * * * * notability-extractor --backup
```

The Settings page surfaces this exact line for convenience.

## Importing into Anki

1. Open Anki on your desktop.
2. `File > Import` and select the generated `.apkg` file.
3. The deck appears as "Notability Flashcards" (or whatever you passed to
   `--deck-name`).

## Caveats

- The Learn cache only contains content from sessions you've actively opened
  in Notability. If a note has never had Learn run on it, no quiz is cached.
- Notability does not provide a stable export API. The tool reads on-disk
  formats that could change between app versions. If extraction breaks after
  a Notability update, open an issue.
- iPadOS-only setups need iCloud Drive sync enabled so the `.nbn` bundles and
  cache files are present on a Mac. Without sync, you'd need physical access
  to the iPad's sandbox (not currently supported).

## Releasing

Releases are semi-automated via GitHub Actions. Publishing to PyPI is gated
on a maintainer explicitly publishing a GitHub Release - normal commits to
`main` never push to PyPI.

Releases are semi-automated. CI proves the code is shippable (tests + builds +
draft GitHub Release with the wheel attached). The maintainer drives the
actual ship via two explicit local commands. **Nothing publishes automatically
from CI.**

To cut a new release:

1. Bump the `version = "X.Y.Z"` line in `pyproject.toml`
2. Commit and push to `main`
3. CI runs on the push: tests pass, wheel + sdist build, smoke-installed
   in a fresh venv. CI does not tag, does not create a Release, does not
   publish anywhere.
4. (optional) `make smoke-test` to verify the wheel pip-installs cleanly
   in a throwaway venv
5. `make github-release` does the entire GitHub release in one shot:
   creates the `vX.Y.Z` tag, pushes it, builds the wheel (if `dist/`
   is empty), creates the draft Release with the wheel attached,
   seeds notes from `git log` since the last tag, opens them in
   `$EDITOR` (vim default), and on save+quit flips the draft to
   published. This does NOT push to PyPI.
6. `make pypi-release` — confirms the version, checks PyPI doesn't already
   have it, then builds + uploads the wheel via `upload-to-pypi.sh`
   (twine + `~/.pypirc` API token).

Run steps 5 and 6 in either order, or run only one of them. If you want a
release-notes-only update on a previously-published version, run just
`make github-release` (which short-circuits and exits cleanly if the
release is already published).

No CI auto-tag. No CI auto-publish. The whole release path is local.

### Testing pip install before pushing to PyPI

To verify a pre-release wheel installs cleanly the same way `pip3 install
notability-extractor` would for end users, run:

```bash
make smoke-test
```

This is a self-contained PASS/FAIL check:

1. cleans `dist/`, builds a fresh wheel
2. creates a throwaway venv at `.venv-smoke/`
3. `pip install dist/*.whl` (real pip, mimics PyPI)
4. runs `notability-extractor --version` and checks the GUI binary is there
5. `pip uninstall` then nukes the venv

If anything in that chain breaks, you find out before pushing to PyPI.

To separately exercise the GUI's first-launch desktop-shortcut auto-install:

```bash
make reset-shortcut       # clear any existing launcher + first-launch marker
notability-extractor-gui  # should auto-install the shortcut on first launch
ls ~/.local/share/applications/notability-extractor.desktop   # (Linux) verify
ls ~/Applications/'Notability Extractor.app'                  # (macOS) verify
```

If `smoke-test` ever gets interrupted halfway and leaves state behind:

```bash
make smoke-clean   # remove the .venv-smoke dir + any leftover shortcut
```

### One-time PyPI setup

`make pypi-release` uses `~/.pypirc` + twine to upload (not OIDC), so the
only setup is:

1. Get a PyPI API token: https://pypi.org/manage/account/token/
2. Drop it into `~/.pypirc`:

   ```ini
   [distutils]
   index-servers = pypi testpypi

   [pypi]
   username = __token__
   password = pypi-<your-token>

   [testpypi]
   repository = https://test.pypi.org/legacy/
   username = __token__
   password = pypi-<your-test-token>
   ```

That's it. No trusted-publisher config, no GitHub `pypi` environment, no OIDC
exchange. The maintainer's local machine is the trust anchor.

### Manual ad-hoc upload

Only needed if CI is broken:

```bash
./upload-to-pypi.sh --test   # TestPyPI dry-run first
./upload-to-pypi.sh          # then prod PyPI
```
