[{"data":1,"prerenderedAt":859},["ShallowReactive",2],{"content-/obsidian-kindle-plugin-internals":3,"all-pages-for-dir":857,"og-image-/obsidian-kindle-plugin-internals":858},{"id":4,"title":5,"body":6,"category":836,"description":837,"extension":838,"meta":839,"navigation":840,"ogImage":841,"path":842,"project_name":841,"published":843,"publishedAt":844,"seo":845,"stem":846,"tags":847,"todo":841,"unpublished":843,"updatedAt":841,"__hash__":856},"pages/2026-06/2026-06-18/obsidian-kindle-plugin-internals.md","Obsidian Kindle Highlights プラグインの実装メモ — 認証・スクレイピング・差分同期",{"type":7,"value":8,"toc":819},"minimark",[9,46,51,61,79,83,94,145,148,152,162,216,231,246,250,257,262,271,300,306,310,319,382,386,395,446,457,461,464,468,477,507,511,520,547,550,554,557,670,673,677,684,751,754,758,815],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,41,42,45],"p",{},"要約: Obsidian の ",[14,15,16],"code",{},"hadynz/obsidian-kindle-plugin"," は、Amazon Kindle Cloud Reader の Notebook ページから「メモとハイライト」を Markdown に同期するプラグイン。実装は (1) Electron ",[14,19,20],{},"remote.BrowserWindow"," でログイン画面を別ウィンドウとして開き、ユーザーが Amazon に直接ログインした cookie を ",[14,23,24],{},"persist:kindle-highlights"," partition に保持、(2) 同じ partition の hidden BrowserWindow で ",[14,27,28],{},"read.amazon.xx/notebook"," を ",[14,31,32],{},"loadURL"," → ",[14,35,36],{},"executeJavaScript"," で HTML を抜き出し cheerio でパース、(3) 書籍単位の ",[14,39,40],{},"lastAnnotatedDate"," と vault 側 ",[14,43,44],{},"lastSyncDate"," を突き合わせて差分同期、で構成される。OAuth は使っておらず、ユーザーログイン済みセッションを cookie ごと借用する pass-through 方式。",[47,48,50],"h2",{"id":49},"_1-調査の動機","1. 調査の動機",[10,52,53,54,60],{},"Kindle Cloud Reader の Notebook ページ (",[55,56,57],"a",{"href":57,"rel":58},"https://read.amazon.co.jp/notebook",[59],"nofollow",") には自分の本のハイライトとメモが集約されている。Amazon 公式の API はなく、ページの裏側で叩かれる内部エンドポイントが HTML を返す。これを集めて自前 DB に正規化したいが、まず先行実装である Obsidian プラグインが「どこで認証して、どうやってスクレイピングして、差分はどうとっているか」を確認しておきたかった。",[10,62,63,64,66,67,70,71,74,75,78],{},"リポジトリは ",[14,65,16],{},"。デフォルトブランチは ",[14,68,69],{},"master","（",[14,72,73],{},"main"," ではない）。",[14,76,77],{},"src/amazonRegion.ts"," で amazon.co.jp が正規にサポートされている。",[47,80,82],{"id":81},"_2-認証フロー-electron-browserwindow-を直接開く","2. 認証フロー — Electron BrowserWindow を直接開く",[10,84,85,86,93],{},"仮説は「webview iframe を埋め込んでログイン」だったが、実装は ",[87,88,89,90,92],"strong",{},"Obsidian の Electron ",[14,91,20],{}," を別ウィンドウとして直接開く"," 方式だった。",[95,96,97,108],"ul",{},[98,99,100,101],"li",{},"実装: ",[55,102,105],{"href":103,"rel":104},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/components/amazonLoginModal/index.ts",[59],[14,106,107],{},"src/components/amazonLoginModal/index.ts",[98,109,110,111],{},"流れ:\n",[112,113,114,124,127,134],"ol",{},[98,115,116,119,120,123],{},[14,117,118],{},"BrowserWindow"," を新規生成し ",[14,121,122],{},"loadURL(notebookUrl)"," で Amazon のログインページを開く",[98,125,126],{},"ユーザーが Amazon の通常ログインフォームに ID/パスワードを入力する（プラグインは認証情報に一切触らない）",[98,128,129,130,133],{},"Amazon 側がログイン成功で ",[14,131,132],{},"read.amazon.xx"," 系の URL にリダイレクトする",[98,135,136,137,140,141,144],{},"プラグインは ",[14,138,139],{},"did-navigate"," イベントを購読していて、URL が ",[14,142,143],{},"kindleReaderUrl"," プレフィックスに合致したらログイン成功扱いでウィンドウを閉じる",[10,146,147],{},"OAuth は使っておらず、ブラウザのログインセッションをそのまま借りる pass-through 認証。プラグイン自体は ID もパスワードも見ない。",[47,149,151],{"id":150},"_3-cookie-保持-persistent-session-partition","3. Cookie 保持 — persistent session partition",[10,153,154,155,157,158,161],{},"ログイン時の ",[14,156,118],{}," には次の ",[14,159,160],{},"webPreferences"," が渡されている。",[163,164,169],"pre",{"className":165,"code":166,"language":167,"meta":168,"style":168},"language-ts shiki shiki-themes vitesse-light vitesse-light","{\n  partition: 'persist:kindle-highlights',\n  // ...\n}\n","ts","",[14,170,171,180,203,210],{"__ignoreMap":168},[172,173,176],"span",{"class":174,"line":175},"line",1,[172,177,179],{"class":178},"shFtX","{\n",[172,181,183,187,190,194,197,200],{"class":174,"line":182},2,[172,184,186],{"class":185},"senZ8","  partition",[172,188,189],{"class":178},":",[172,191,193],{"class":192},"sMJiu"," '",[172,195,24],{"class":196},"sdGka",[172,198,199],{"class":192},"'",[172,201,202],{"class":178},",\n",[172,204,206],{"class":174,"line":205},3,[172,207,209],{"class":208},"sxvE3","  // ...\n",[172,211,213],{"class":174,"line":212},4,[172,214,215],{"class":178},"}\n",[10,217,218,219,222,223,226,227,230],{},"Electron の partition は「同じ partition 名を持つ BrowserWindow が cookie とストレージを共有する」仕組み。",[14,220,221],{},"persist:"," を頭につけるとファイルシステムに永続化される（Obsidian のユーザーデータディレクトリ配下）。スクレイピング側の hidden BrowserWindow (",[14,224,225],{},"loadRemoteDom.ts",") もログアウト処理 (",[14,228,229],{},"session.ts",") も同じ partition を指定するので、一度ログインすればその後の同期処理は cookie を引き継いで動く。",[10,232,233,234,241,242,245],{},"ログアウトは ",[55,235,238],{"href":236,"rel":237},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/scraper/session.ts",[59],[14,239,240],{},"src/scraper/session.ts"," の ",[14,243,244],{},"webContents.session.clearStorageData()"," でこの partition の cookie/ストレージを丸ごと消す方式。",[47,247,249],{"id":248},"_4-ハイライト取得-json-api-ではなく-html-スクレイピング","4. ハイライト取得 — JSON API ではなく HTML スクレイピング",[10,251,252,253,256],{},"Amazon Notebook ページの裏側に JSON エンドポイントがあるかと思ったが、プラグインは ",[87,254,255],{},"HTML を取ってから cheerio でパースする"," 方針だった。",[258,259,261],"h3",{"id":260},"_41-ロード方法-loadremotedom","4.1 ロード方法 (loadRemoteDom)",[10,263,264,189],{},[55,265,268],{"href":266,"rel":267},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/scraper/loadRemoteDom.ts",[59],[14,269,270],{},"src/scraper/loadRemoteDom.ts",[112,272,273,276,282,288,294],{},[98,274,275],{},"hidden BrowserWindow を作る（同じ persistent partition）",[98,277,278,281],{},[14,279,280],{},"loadURL(targetUrl)"," で対象ページを開く",[98,283,284,287],{},[14,285,286],{},"did-finish-load"," を待つ",[98,289,290,293],{},[14,291,292],{},"webContents.executeJavaScript(\"document.querySelector('body').innerHTML\")"," で HTML 文字列を抜く",[98,295,296,299],{},[14,297,298],{},"cheerio.load(html)"," で Cheerio Root を返す",[10,301,302,305],{},[14,303,304],{},"fetch"," 直叩きではなく BrowserWindow で実際にレンダリングしてから取るので、JS 実行が必要なケースにも対応できる。",[258,307,309],{"id":308},"_42-書籍リスト-scrapebooks","4.2 書籍リスト (scrapeBooks)",[10,311,312,189],{},[55,313,316],{"href":314,"rel":315},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/scraper/scrapeBooks.ts",[59],[14,317,318],{},"src/scraper/scrapeBooks.ts",[95,320,321,327,333,375],{},[98,322,323,324,326],{},"取得 URL: ",[14,325,57],{}," のトップ",[98,328,329,330],{},"セレクタ: ",[14,331,332],{},".kp-notebook-library-each-book",[98,334,335,336],{},"各書籍要素から:\n",[95,337,338,348,354,361],{},[98,339,340,343,344,347],{},[87,341,342],{},"ASIN",": ",[14,345,346],{},"bookEl.id","（要素 id 属性に ASIN がそのまま入っている）",[98,349,350,351],{},"タイトル: ",[14,352,353],{},"h2.kp-notebook-searchable",[98,355,356,357,360],{},"著者: ",[14,358,359],{},"p.kp-notebook-searchable","（「著者: 〜」を整形）",[98,362,363,343,367,370,371,374],{},[87,364,365],{},[14,366,40],{},[14,368,369],{},"#kp-notebook-annotated-date-{ASIN}"," hidden input の value（例: ",[14,372,373],{},"\"2025年12月2日火曜日\"","）",[98,376,377,378,381],{},"ページネーション: 末尾の ",[14,379,380],{},"input.kp-notebook-library-next-page-start[value]"," を token として再帰取得",[258,383,385],{"id":384},"_43-書籍別ハイライト-scrapebookhighlights","4.3 書籍別ハイライト (scrapeBookHighlights)",[10,387,388,189],{},[55,389,392],{"href":390,"rel":391},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/scraper/scrapeBookHighlights.ts",[59],[14,393,394],{},"src/scraper/scrapeBookHighlights.ts",[95,396,397,402,413],{},[98,398,323,399],{},[14,400,401],{},"${notebookUrl}?asin={asin}&contentLimitState={state}&token={token}",[98,403,404,405,408,409,412],{},"次ページ token は ",[14,406,407],{},".kp-notebook-annotations-next-page-start"," と ",[14,410,411],{},".kp-notebook-content-limit-state"," の hidden input から拾う",[98,414,415,416,419,420],{},"annotation 行: ",[14,417,418],{},".a-row.a-spacing-base"," 内\n",[95,421,422,428,434,440],{},[98,423,424,427],{},[14,425,426],{},"#kp-annotation-location[value]",": 位置（Kindleの \"location\" 番号）",[98,429,430,433],{},[14,431,432],{},"#annotationHighlightHeader",": 「黄色のハイライト | 位置: 155」のような色＋位置ラベル",[98,435,436,439],{},[14,437,438],{},"#highlight",": ハイライト本文",[98,441,442,445],{},[14,443,444],{},"#note",": メモ本文（空のことあり）",[10,447,448,449,452,453,456],{},"ハイライト色はラベルの文言（黄色 / ピンク色 / 青 / オレンジ）からパースするか、",[14,450,451],{},"#highlight-{id}"," の class名（",[14,454,455],{},"kp-notebook-highlight-{color}","）から取れる。",[47,458,460],{"id":459},"_5-差分同期-lastannotateddate-ベース","5. 差分同期 — lastAnnotatedDate ベース",[10,462,463],{},"「全書籍を毎回フル fetch する」は重いので、プラグインは 2 段階で絞り込む。",[258,465,467],{"id":466},"_51-書籍差分-filterbookstosync","5.1 書籍差分 (filterBooksToSync)",[10,469,470,189],{},[55,471,474],{"href":472,"rel":473},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/sync/syncManager/index.ts",[59],[14,475,476],{},"src/sync/syncManager/index.ts",[95,478,479,489,494,500],{},[98,480,481,482,485,486,488],{},"リモート: ",[14,483,484],{},"scrapeBooks"," で取った ",[14,487,40],{}," 一覧",[98,490,491,492],{},"ローカル (vault): 同期済み Markdown ファイルの ASIN と ",[14,493,44],{},[98,495,496,499],{},[14,497,498],{},"diffBooks(remoteBooks, vaultBooks, lastSyncDate)"," で「lastAnnotatedDate が新しい本」または「ASIN がまだ vault に無い本」だけ抜き出す",[98,501,502,503,506],{},"この結果のみ ",[14,504,505],{},"scrapeBookHighlights"," を叩く",[258,508,510],{"id":509},"_52-ハイライト差分-diffmanager","5.2 ハイライト差分 (diffManager)",[10,512,513,189],{},[55,514,517],{"href":515,"rel":516},"https://github.com/hadynz/obsidian-kindle-plugin/blob/master/src/sync/diffManager/index.ts",[59],[14,518,519],{},"src/sync/diffManager/index.ts",[95,521,522,533,536],{},[98,523,524,525,528,529,532],{},"既存の Markdown 中に ",[14,526,527],{},"HighlightIdBlockRefPrefix"," 付きの行を埋め込んでおく（例: ",[14,530,531],{},"^abc123"," のような Obsidian ブロック参照）",[98,534,535],{},"同期時に既存 MD をパースして「すでに書き込み済みのハイライト ID」を集合化",[98,537,538,539,542,543,546],{},"リモートのハイライト ID と diff を取って、未挿入のものだけ ",[14,540,541],{},"insertLinesAt"," / ",[14,544,545],{},"append"," で追記する",[10,548,549],{},"これでユーザーが MD ファイルを編集していても、ハイライト追加分だけが反映される設計になっている。",[47,551,553],{"id":552},"_6-chrome-拡張-devtools-mcp-経由で実装する場合の差分","6. Chrome 拡張 + DevTools MCP 経由で実装する場合の差分",[10,555,556],{},"自分は今回 Chrome 拡張側で同じことをやろうとしているので、Obsidian 実装との差分を整理しておく。",[558,559,560,576],"table",{},[561,562,563],"thead",{},[564,565,566,570,573],"tr",{},[567,568,569],"th",{},"項目",[567,571,572],{},"Obsidian Plugin",[567,574,575],{},"Chrome 拡張 + DevTools MCP",[577,578,579,591,604,621,635,653],"tbody",{},[564,580,581,585,588],{},[582,583,584],"td",{},"ログインフォーム表示",[582,586,587],{},"Electron BrowserWindow を新規に開く",[582,589,590],{},"不要（ユーザーがそもそも Chrome に Amazon ログインしている）",[564,592,593,596,601],{},[582,594,595],{},"Cookie 保持",[582,597,598,600],{},[14,599,24],{}," partition",[582,602,603],{},"Chrome 本体の cookie 保存に乗っかる",[564,605,606,609,615],{},[582,607,608],{},"HTML 取得",[582,610,611,612,614],{},"hidden BrowserWindow + ",[14,613,36],{}," で innerHTML 抜く",[582,616,617,618],{},"ログイン済みタブ上で ",[14,619,620],{},"fetch(url, { credentials: 'include' })",[564,622,623,626,629],{},[582,624,625],{},"HTML パース",[582,627,628],{},"Node 側で cheerio",[582,630,631,632],{},"ブラウザ内で ",[14,633,634],{},"DOMParser",[564,636,637,640,643],{},[582,638,639],{},"出力先",[582,641,642],{},"Obsidian vault の Markdown",[582,644,645,646,542,649,652],{},"Turso DB (",[14,647,648],{},"kindle_library",[14,650,651],{},"kindle_highlights",")",[564,654,655,658,665],{},[582,656,657],{},"差分判定",[582,659,660,662,663],{},[14,661,40],{}," vs ",[14,664,44],{},[582,666,667,669],{},[14,668,40],{}," を DB に保持して比較（同じ思想）",[10,671,672],{},"Chrome 側はそもそもログイン UI を出す必要がない（ユーザーがブラウザに既にログインしているという前提が成立する）ので、Obsidian 実装より一段シンプルになる。一方、Obsidian 側は「ブラウザを持っていない環境（モバイル含む）」でも動かす必要があるので Electron BrowserWindow まで自前で抱える。",[47,674,676],{"id":675},"_7-url-セレクタの再利用","7. URL / セレクタの再利用",[10,678,679,680,683],{},"実装方針として大事なのは、",[87,681,682],{},"Amazon の内部エンドポイント URL とセレクタは Obsidian プラグインの実装をそのまま使ってよい"," という点だ。具体的には以下：",[95,685,686,694,699,704,709,716,722,727,732,737,742],{},[98,687,688,690,691,374],{},[14,689,57],{}," （または ",[14,692,693],{},".com/.de/.fr/.es/.it/.in",[98,695,696],{},[14,697,698],{},"https://read.amazon.{tld}/notebook?library=list&token={next}",[98,700,701],{},[14,702,703],{},"https://read.amazon.{tld}/notebook?asin={ASIN}&contentLimitState={state}&token={token}",[98,705,706,707],{},"書籍ブロックセレクタ: ",[14,708,332],{},[98,710,711,712,715],{},"書籍 ID: ",[14,713,714],{},"div.id","（= ASIN）",[98,717,718,719,721],{},"最終アノテーション日: ",[14,720,369],{}," hidden input",[98,723,415,724],{},[14,725,726],{},".a-row.a-spacing-base[id]",[98,728,729,730],{},"位置: ",[14,731,426],{},[98,733,734,735],{},"ハイライト本文: ",[14,736,438],{},[98,738,739,740],{},"メモ本文: ",[14,741,444],{},[98,743,744,745,747,748,750],{},"色: ",[14,746,432],{}," のテキスト or ",[14,749,451],{}," の class",[10,752,753],{},"これらは 2026-06-18 時点で Chrome DevTools MCP 経由で実物観察した結果とも一致している。Amazon 側で UI を大きく変えない限り当面再利用可能と思われる。",[47,755,757],{"id":756},"_8-参考","8. 参考",[95,759,760,767,775,783,791,799,807],{},[98,761,762,763],{},"リポジトリ: ",[55,764,16],{"href":765,"rel":766},"https://github.com/hadynz/obsidian-kindle-plugin",[59],[98,768,769,770],{},"認証実装: ",[55,771,773],{"href":103,"rel":772},[59],[14,774,107],{},[98,776,777,778],{},"DOM ロード: ",[55,779,781],{"href":266,"rel":780},[59],[14,782,270],{},[98,784,785,786],{},"書籍リストスクレイピング: ",[55,787,789],{"href":314,"rel":788},[59],[14,790,318],{},[98,792,793,794],{},"ハイライトスクレイピング: ",[55,795,797],{"href":390,"rel":796},[59],[14,798,394],{},[98,800,801,802],{},"同期マネージャ: ",[55,803,805],{"href":472,"rel":804},[59],[14,806,476],{},[98,808,809,810],{},"差分マネージャ: ",[55,811,813],{"href":515,"rel":812},[59],[14,814,519],{},[816,817,818],"style",{},"html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":168,"searchDepth":182,"depth":182,"links":820},[821,822,823,824,829,833,834,835],{"id":49,"depth":182,"text":50},{"id":81,"depth":182,"text":82},{"id":150,"depth":182,"text":151},{"id":248,"depth":182,"text":249,"children":825},[826,827,828],{"id":260,"depth":205,"text":261},{"id":308,"depth":205,"text":309},{"id":384,"depth":205,"text":385},{"id":459,"depth":182,"text":460,"children":830},[831,832],{"id":466,"depth":205,"text":467},{"id":509,"depth":205,"text":510},{"id":552,"depth":182,"text":553},{"id":675,"depth":182,"text":676},{"id":756,"depth":182,"text":757},"dev","hadynz/obsidian-kindle-plugin が Amazon Kindle Cloud Reader のハイライト/メモを取り込む仕組みを実装ファイルから読み解いたメモ。Electron BrowserWindow による pass-through 認証、persistent partition による cookie 保持、cheerio による HTML スクレイピング、lastAnnotatedDate ベースの差分同期までを整理する。同じ仕組みを Chrome 拡張 + DevTools MCP 経由で実装する場合との差分も併記する。","md",{},true,null,"/obsidian-kindle-plugin-internals",false,"2026-06-18T00:00:00.000Z",{"title":5,"description":837},"2026-06/2026-06-18/obsidian-kindle-plugin-internals",[848,849,850,851,852,853,854,855],"Obsidian","Kindle","Amazon","Electron","スクレイピング","cheerio","ハイライト同期","リバースエンジニアリング","FEkWLmEzWSm1rC2Hjq0oiTrUbdmxmJPEPGdYDyJzlV8",[],"https://log.eurekapu.com/og/blog/obsidian-kindle-plugin-internals.png?v=2026-06-18T00%3A00%3A00.000Z&title=Obsidian%20Kindle%20Highlights%20%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A3%85%E3%83%A1%E3%83%A2%20%E2%80%94%20%E8%AA%8D%E8%A8%BC%E3%83%BB%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%83%BB%E5%B7%AE%E5%88%86%E5%90%8C%E6%9C%9F&author=Kei%20Komatsu&sig=86f1aff6785345a1",1782176330572]