要約: Obsidian の hadynz/obsidian-kindle-plugin は、Amazon Kindle Cloud Reader の Notebook ページから「メモとハイライト」を Markdown に同期するプラグイン。実装は (1) Electron remote.BrowserWindow でログイン画面を別ウィンドウとして開き、ユーザーが Amazon に直接ログインした cookie を persist:kindle-highlights partition に保持、(2) 同じ partition の hidden BrowserWindow で read.amazon.xx/notebook を loadURL → executeJavaScript で HTML を抜き出し cheerio でパース、(3) 書籍単位の lastAnnotatedDate と vault 側 lastSyncDate を突き合わせて差分同期、で構成される。OAuth は使っておらず、ユーザーログイン済みセッションを cookie ごと借用する pass-through 方式。
1. 調査の動機
Kindle Cloud Reader の Notebook ページ (https://read.amazon.co.jp/notebook) には自分の本のハイライトとメモが集約されている。Amazon 公式の API はなく、ページの裏側で叩かれる内部エンドポイントが HTML を返す。これを集めて自前 DB に正規化したいが、まず先行実装である Obsidian プラグインが「どこで認証して、どうやってスクレイピングして、差分はどうとっているか」を確認しておきたかった。
リポジトリは hadynz/obsidian-kindle-plugin。デフォルトブランチは master(main ではない)。src/amazonRegion.ts で amazon.co.jp が正規にサポートされている。
2. 認証フロー — Electron BrowserWindow を直接開く
仮説は「webview iframe を埋め込んでログイン」だったが、実装は Obsidian の Electron remote.BrowserWindow を別ウィンドウとして直接開く 方式だった。
- 実装:
src/components/amazonLoginModal/index.ts - 流れ:
BrowserWindowを新規生成しloadURL(notebookUrl)で Amazon のログインページを開く- ユーザーが Amazon の通常ログインフォームに ID/パスワードを入力する(プラグインは認証情報に一切触らない)
- Amazon 側がログイン成功で
read.amazon.xx系の URL にリダイレクトする - プラグインは
did-navigateイベントを購読していて、URL がkindleReaderUrlプレフィックスに合致したらログイン成功扱いでウィンドウを閉じる
OAuth は使っておらず、ブラウザのログインセッションをそのまま借りる pass-through 認証。プラグイン自体は ID もパスワードも見ない。
3. Cookie 保持 — persistent session partition
ログイン時の BrowserWindow には次の webPreferences が渡されている。
{
partition: 'persist:kindle-highlights',
// ...
}
Electron の partition は「同じ partition 名を持つ BrowserWindow が cookie とストレージを共有する」仕組み。persist: を頭につけるとファイルシステムに永続化される(Obsidian のユーザーデータディレクトリ配下)。スクレイピング側の hidden BrowserWindow (loadRemoteDom.ts) もログアウト処理 (session.ts) も同じ partition を指定するので、一度ログインすればその後の同期処理は cookie を引き継いで動く。
ログアウトは src/scraper/session.ts の webContents.session.clearStorageData() でこの partition の cookie/ストレージを丸ごと消す方式。
4. ハイライト取得 — JSON API ではなく HTML スクレイピング
Amazon Notebook ページの裏側に JSON エンドポイントがあるかと思ったが、プラグインは HTML を取ってから cheerio でパースする 方針だった。
4.1 ロード方法 (loadRemoteDom)
- hidden BrowserWindow を作る(同じ persistent partition)
loadURL(targetUrl)で対象ページを開くdid-finish-loadを待つwebContents.executeJavaScript("document.querySelector('body').innerHTML")で HTML 文字列を抜くcheerio.load(html)で Cheerio Root を返す
fetch 直叩きではなく BrowserWindow で実際にレンダリングしてから取るので、JS 実行が必要なケースにも対応できる。
4.2 書籍リスト (scrapeBooks)
- 取得 URL:
https://read.amazon.co.jp/notebookのトップ - セレクタ:
.kp-notebook-library-each-book - 各書籍要素から:
- ASIN:
bookEl.id(要素 id 属性に ASIN がそのまま入っている) - タイトル:
h2.kp-notebook-searchable - 著者:
p.kp-notebook-searchable(「著者: 〜」を整形) lastAnnotatedDate:#kp-notebook-annotated-date-{ASIN}hidden input の value(例:"2025年12月2日火曜日")
- ASIN:
- ページネーション: 末尾の
input.kp-notebook-library-next-page-start[value]を token として再帰取得
4.3 書籍別ハイライト (scrapeBookHighlights)
src/scraper/scrapeBookHighlights.ts:
- 取得 URL:
${notebookUrl}?asin={asin}&contentLimitState={state}&token={token} - 次ページ token は
.kp-notebook-annotations-next-page-startと.kp-notebook-content-limit-stateの hidden input から拾う - annotation 行:
.a-row.a-spacing-base内#kp-annotation-location[value]: 位置(Kindleの "location" 番号)#annotationHighlightHeader: 「黄色のハイライト | 位置: 155」のような色+位置ラベル#highlight: ハイライト本文#note: メモ本文(空のことあり)
ハイライト色はラベルの文言(黄色 / ピンク色 / 青 / オレンジ)からパースするか、#highlight-{id} の class名(kp-notebook-highlight-{color})から取れる。
5. 差分同期 — lastAnnotatedDate ベース
「全書籍を毎回フル fetch する」は重いので、プラグインは 2 段階で絞り込む。
5.1 書籍差分 (filterBooksToSync)
src/sync/syncManager/index.ts:
- リモート:
scrapeBooksで取ったlastAnnotatedDate一覧 - ローカル (vault): 同期済み Markdown ファイルの ASIN と
lastSyncDate diffBooks(remoteBooks, vaultBooks, lastSyncDate)で「lastAnnotatedDate が新しい本」または「ASIN がまだ vault に無い本」だけ抜き出す- この結果のみ
scrapeBookHighlightsを叩く
5.2 ハイライト差分 (diffManager)
src/sync/diffManager/index.ts:
- 既存の Markdown 中に
HighlightIdBlockRefPrefix付きの行を埋め込んでおく(例:^abc123のような Obsidian ブロック参照) - 同期時に既存 MD をパースして「すでに書き込み済みのハイライト ID」を集合化
- リモートのハイライト ID と diff を取って、未挿入のものだけ
insertLinesAt/appendで追記する
これでユーザーが MD ファイルを編集していても、ハイライト追加分だけが反映される設計になっている。
6. Chrome 拡張 + DevTools MCP 経由で実装する場合の差分
自分は今回 Chrome 拡張側で同じことをやろうとしているので、Obsidian 実装との差分を整理しておく。
| 項目 | Obsidian Plugin | Chrome 拡張 + DevTools MCP |
|---|---|---|
| ログインフォーム表示 | Electron BrowserWindow を新規に開く | 不要(ユーザーがそもそも Chrome に Amazon ログインしている) |
| Cookie 保持 | persist:kindle-highlights partition | Chrome 本体の cookie 保存に乗っかる |
| HTML 取得 | hidden BrowserWindow + executeJavaScript で innerHTML 抜く | ログイン済みタブ上で fetch(url, { credentials: 'include' }) |
| HTML パース | Node 側で cheerio | ブラウザ内で DOMParser |
| 出力先 | Obsidian vault の Markdown | Turso DB (kindle_library / kindle_highlights) |
| 差分判定 | lastAnnotatedDate vs lastSyncDate | lastAnnotatedDate を DB に保持して比較(同じ思想) |
Chrome 側はそもそもログイン UI を出す必要がない(ユーザーがブラウザに既にログインしているという前提が成立する)ので、Obsidian 実装より一段シンプルになる。一方、Obsidian 側は「ブラウザを持っていない環境(モバイル含む)」でも動かす必要があるので Electron BrowserWindow まで自前で抱える。
7. URL / セレクタの再利用
実装方針として大事なのは、Amazon の内部エンドポイント URL とセレクタは Obsidian プラグインの実装をそのまま使ってよい という点だ。具体的には以下:
https://read.amazon.co.jp/notebook(または.com/.de/.fr/.es/.it/.in)https://read.amazon.{tld}/notebook?library=list&token={next}https://read.amazon.{tld}/notebook?asin={ASIN}&contentLimitState={state}&token={token}- 書籍ブロックセレクタ:
.kp-notebook-library-each-book - 書籍 ID:
div.id(= ASIN) - 最終アノテーション日:
#kp-notebook-annotated-date-{ASIN}hidden input - annotation 行:
.a-row.a-spacing-base[id] - 位置:
#kp-annotation-location[value] - ハイライト本文:
#highlight - メモ本文:
#note - 色:
#annotationHighlightHeaderのテキスト or#highlight-{id}の class
これらは 2026-06-18 時点で Chrome DevTools MCP 経由で実物観察した結果とも一致している。Amazon 側で UI を大きく変えない限り当面再利用可能と思われる。
8. 参考
- リポジトリ: hadynz/obsidian-kindle-plugin
- 認証実装:
src/components/amazonLoginModal/index.ts - DOM ロード:
src/scraper/loadRemoteDom.ts - 書籍リストスクレイピング:
src/scraper/scrapeBooks.ts - ハイライトスクレイピング:
src/scraper/scrapeBookHighlights.ts - 同期マネージャ:
src/sync/syncManager/index.ts - 差分マネージャ:
src/sync/diffManager/index.ts