Skip to content

Instantly share code, notes, and snippets.

@edwinhu
Last active January 28, 2026 21:39
Show Gist options
  • Select an option

  • Save edwinhu/100a4adf3665aa10831a20357d340721 to your computer and use it in GitHub Desktop.

Select an option

Save edwinhu/100a4adf3665aa10831a20357d340721 to your computer and use it in GitHub Desktop.
Sync VitalSource Bookshelf highlights to Readwise
// ==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