開発未分類

要約: 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/notebookloadURLexecuteJavaScript で 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。デフォルトブランチは mastermain ではない)。src/amazonRegion.ts で amazon.co.jp が正規にサポートされている。

2. 認証フロー — Electron BrowserWindow を直接開く

仮説は「webview iframe を埋め込んでログイン」だったが、実装は Obsidian の Electron remote.BrowserWindow を別ウィンドウとして直接開く 方式だった。

  • 実装: src/components/amazonLoginModal/index.ts
  • 流れ:
    1. BrowserWindow を新規生成し loadURL(notebookUrl) で Amazon のログインページを開く
    2. ユーザーが Amazon の通常ログインフォームに ID/パスワードを入力する(プラグインは認証情報に一切触らない)
    3. Amazon 側がログイン成功で read.amazon.xx 系の URL にリダイレクトする
    4. プラグインは did-navigate イベントを購読していて、URL が kindleReaderUrl プレフィックスに合致したらログイン成功扱いでウィンドウを閉じる

OAuth は使っておらず、ブラウザのログインセッションをそのまま借りる pass-through 認証。プラグイン自体は ID もパスワードも見ない。

ログイン時の BrowserWindow には次の webPreferences が渡されている。

{
  partition: 'persist:kindle-highlights',
  // ...
}

Electron の partition は「同じ partition 名を持つ BrowserWindow が cookie とストレージを共有する」仕組み。persist: を頭につけるとファイルシステムに永続化される(Obsidian のユーザーデータディレクトリ配下)。スクレイピング側の hidden BrowserWindow (loadRemoteDom.ts) もログアウト処理 (session.ts) も同じ partition を指定するので、一度ログインすればその後の同期処理は cookie を引き継いで動く。

ログアウトは src/scraper/session.tswebContents.session.clearStorageData() でこの partition の cookie/ストレージを丸ごと消す方式。

4. ハイライト取得 — JSON API ではなく HTML スクレイピング

Amazon Notebook ページの裏側に JSON エンドポイントがあるかと思ったが、プラグインは HTML を取ってから cheerio でパースする 方針だった。

4.1 ロード方法 (loadRemoteDom)

src/scraper/loadRemoteDom.ts:

  1. hidden BrowserWindow を作る(同じ persistent partition)
  2. loadURL(targetUrl) で対象ページを開く
  3. did-finish-load を待つ
  4. webContents.executeJavaScript("document.querySelector('body').innerHTML") で HTML 文字列を抜く
  5. cheerio.load(html) で Cheerio Root を返す

fetch 直叩きではなく BrowserWindow で実際にレンダリングしてから取るので、JS 実行が必要なケースにも対応できる。

4.2 書籍リスト (scrapeBooks)

src/scraper/scrapeBooks.ts:

  • 取得 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日火曜日"
  • ページネーション: 末尾の 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 PluginChrome 拡張 + DevTools MCP
ログインフォーム表示Electron BrowserWindow を新規に開く不要(ユーザーがそもそも Chrome に Amazon ログインしている)
Cookie 保持persist:kindle-highlights partitionChrome 本体の cookie 保存に乗っかる
HTML 取得hidden BrowserWindow + executeJavaScript で innerHTML 抜くログイン済みタブ上で fetch(url, { credentials: 'include' })
HTML パースNode 側で cheerioブラウザ内で DOMParser
出力先Obsidian vault の MarkdownTurso DB (kindle_library / kindle_highlights)
差分判定lastAnnotatedDate vs lastSyncDatelastAnnotatedDate を 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. 参考