Last active
January 28, 2026 21:39
-
-
Save edwinhu/100a4adf3665aa10831a20357d340721 to your computer and use it in GitHub Desktop.
Sync VitalSource Bookshelf highlights to Readwise
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name VitalSource to Readwise Sync | |
| // @namespace https://github.com/edwinhu | |
| // @version 1.3.2 | |
| // @description Sync VitalSource Bookshelf highlights to Readwise | |
| // @author Edwin Hu | |
| // @match https://*.vitalsource.com/* | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_xmlhttpRequest | |
| // @connect jigsaw.vitalsource.com | |
| // @connect readwise.io | |
| // @run-at document-end | |
| // @noframes | |
| // @updateURL https://gist.githubusercontent.com/edwinhu/100a4adf3665aa10831a20357d340721/raw/vitalsource-readwise-sync.user.js | |
| // @downloadURL https://gist.githubusercontent.com/edwinhu/100a4adf3665aa10831a20357d340721/raw/vitalsource-readwise-sync.user.js | |
| // ==/UserScript== | |
| (function() { | |
| "use strict"; | |
| // Get token from Tampermonkey storage or prompt user | |
| function getToken() { | |
| var token = GM_getValue("READWISE_TOKEN"); | |
| if (!token) { | |
| token = prompt("Enter your Readwise API token (from https://readwise.io/access_token):"); | |
| if (token) GM_setValue("READWISE_TOKEN", token); | |
| } | |
| return token; | |
| } | |
| // Show notification | |
| function notify(msg, isError) { | |
| var existing = document.getElementById("vs-readwise-notify"); | |
| if (existing) existing.remove(); | |
| var div = document.createElement("div"); | |
| div.id = "vs-readwise-notify"; | |
| div.style.cssText = "position:fixed;top:50px;left:50%;transform:translateX(-50%);padding:12px 24px;background:" + (isError ? "#ef4444" : "#10b981") + ";color:white;border-radius:8px;z-index:999999;font-family:system-ui;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"; | |
| div.textContent = msg; | |
| document.body.appendChild(div); | |
| setTimeout(function() { div.remove(); }, 5000); | |
| } | |
| // Make GM_xmlhttpRequest return a promise | |
| function gmFetch(url, options) { | |
| return new Promise(function(resolve, reject) { | |
| GM_xmlhttpRequest({ | |
| method: options.method || "GET", | |
| url: url, | |
| headers: options.headers || {}, | |
| data: options.body || null, | |
| withCredentials: true, | |
| onload: function(response) { | |
| resolve({ | |
| ok: response.status >= 200 && response.status < 300, | |
| status: response.status, | |
| json: function() { return Promise.resolve(JSON.parse(response.responseText)); }, | |
| text: function() { return Promise.resolve(response.responseText); } | |
| }); | |
| }, | |
| onerror: function(err) { | |
| reject(new Error("Network error")); | |
| } | |
| }); | |
| }); | |
| } | |
| // Main sync function | |
| async function syncToReadwise() { | |
| var token = getToken(); | |
| if (!token) { | |
| notify("No Readwise token provided", true); | |
| return; | |
| } | |
| var btn = document.getElementById("vs-sync-btn"); | |
| btn.textContent = "Syncing..."; | |
| btn.disabled = true; | |
| try { | |
| // Get ISBN from URL | |
| var isbn = location.href.split("/books/")[1].split("/")[0]; | |
| // Fetch book info | |
| var bookResp = await gmFetch("https://jigsaw.vitalsource.com/books/" + isbn, { | |
| headers: { "Accept": "application/json" } | |
| }); | |
| var book = await bookResp.json(); | |
| // Fetch highlights | |
| var hlResp = await gmFetch("https://jigsaw.vitalsource.com/books/" + isbn + "/highlights", { | |
| headers: { "Accept": "application/json" } | |
| }); | |
| var hlData = await hlResp.json(); | |
| if (!hlData.highlights || hlData.highlights.length === 0) { | |
| notify("No highlights found in this book", true); | |
| btn.textContent = "Sync to Readwise"; | |
| btn.disabled = false; | |
| return; | |
| } | |
| // Format for Readwise | |
| var highlights = hlData.highlights.map(function(h) { | |
| return { | |
| text: h.selectedText, | |
| title: book.title, | |
| author: book.author, | |
| source_type: "vitalsource", | |
| category: "books", | |
| note: h.chapterTitle ? ("Chapter: " + h.chapterTitle + (h.noteText ? " | " + h.noteText : "")) : h.noteText, | |
| highlighted_at: h.lastModifiedAt ? new Date(h.lastModifiedAt).toISOString() : null | |
| }; | |
| }); | |
| // Send to Readwise | |
| var resp = await gmFetch("https://readwise.io/api/v2/highlights/", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": "Token " + token, | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify({ highlights: highlights }) | |
| }); | |
| if (resp.ok) { | |
| notify("Synced " + highlights.length + " highlights to Readwise!"); | |
| } else { | |
| var err = await resp.text(); | |
| notify("Readwise error: " + err, true); | |
| } | |
| } catch (e) { | |
| notify("Error: " + e.message, true); | |
| } | |
| btn.textContent = "Sync to Readwise"; | |
| btn.disabled = false; | |
| } | |
| // Add button on VitalSource reader pages | |
| if (window.location.hostname.endsWith("vitalsource.com") && window.location.pathname.includes("/books/")) { | |
| setTimeout(function() { | |
| // Remove any old buttons and check for duplicates | |
| var oldBtns = document.querySelectorAll("#vs-sync-btn, #readwise-sync-btn, [id*='readwise'][id*='btn']"); | |
| oldBtns.forEach(function(b) { b.remove(); }); | |
| var button = document.createElement("button"); | |
| button.id = "vs-sync-btn"; | |
| button.textContent = "Sync to Readwise"; | |
| button.style.cssText = "position:fixed;top:12px;left:50%;transform:translateX(-50%);padding:8px 14px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(102,126,234,0.4);z-index:999999;font-family:-apple-system,BlinkMacSystemFont,sans-serif;"; | |
| button.onmouseenter = function() { button.style.transform = "translateX(-50%) scale(1.05)"; }; | |
| button.onmouseleave = function() { button.style.transform = "translateX(-50%) scale(1)"; }; | |
| button.onclick = syncToReadwise; | |
| document.body.appendChild(button); | |
| }, 2000); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment