Skip to content

Instantly share code, notes, and snippets.

@uroybd
Last active March 3, 2025 11:52
Show Gist options
  • Save uroybd/03339046aca52f0aefdc533ab852c683 to your computer and use it in GitHub Desktop.
Save uroybd/03339046aca52f0aefdc533ab852c683 to your computer and use it in GitHub Desktop.
A Tampermonkey script to get annotations from boox cloud
// ==UserScript==
// @name Boox Annotations
// @namespace http://tampermonkey.net/
// @version 2025-02-26
// @description try to take over the world!
// @author You
// @include https://push.boox.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=mozilla.org
// @grant GM.registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
async function getDBSpec() {
const databases = await window.indexedDB.databases();
return databases.find(db => db.name.startsWith("_pouch") && db.name.endsWith("-library"))
}
function loadFromIndexedDB(storeName){
return new Promise(
function(resolve, reject) {
var dbRequest = indexedDB.open(storeName);
dbRequest.onerror = function(event) {
reject(Error("Error text"));
};
dbRequest.onupgradeneeded = function(event) {
// Objectstore does not exist. Nothing to load
event.target.transaction.abort();
reject(Error('Not found'));
};
dbRequest.onsuccess = function(event) {
var content = [];
var database = event.target.result;
database.transaction("by-sequence").objectStore("by-sequence").openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
content.push(cursor.value);
cursor.continue();
} else {
console.log("No more entries!");
resolve(content);
}
};
};
}
);
}
async function getContent() {
const db = await getDBSpec();
const content = await loadFromIndexedDB(db.name);
return content
}
var bookCommands = [];
var content = [];
function selectBook(content) {
let books = content.filter((item) => item.progress != undefined && item.title != undefined && item.title != null)
books = books.filter((item, index) => {
return books.findIndex((otherItem) => otherItem.uniqueId == item.uniqueId && otherItem.updatedAt > item.updatedAt) == -1
})
// Create a modal with the list of books:
const modal = document.createElement("div");
// Position it to center
modal.style.position = "fixed"
modal.style.top = "50%"
modal.style.left = "50%"
modal.style.transform = "translate(-50%, -50%)"
modal.style.padding = "20px"
modal.style.backgroundColor = "white"
modal.style.zIndex = "9999"
modal.style.border = "1px solid black"
modal.style.borderRadius = "10px"
// Add a title
const title = document.createElement("h3")
title.textContent = "Select a book"
modal.appendChild(title)
// Add the list of books
const list = document.createElement("ul")
// Style it without bullets
list.style.listStyle = "none"
books.forEach((book) => {
const btn = document.createElement("li")
btn.textContent = book.title
btn.addEventListener("click", () => {
document.body.removeChild(modal)
const ann = getAnnotations(book)
download(ann)
})
// Style it like buttons
btn.style.cursor = "pointer"
btn.style.border = "1px solid green"
btn.style.borderRadius = "10px"
btn.style.padding = "10px"
btn.style.marginBottom = "10px"
list.appendChild(btn)
})
modal.appendChild(list)
document.body.appendChild(modal)
// Add a close button
const close = document.createElement("button")
close.textContent = "Close"
modal.appendChild(close)
close.addEventListener("click", () => {
document.body.removeChild(modal)
})
}
function integerToColorHex(num) {
num >>>= 0;
var b = num & 0xFF,
g = (num & 0xFF00) >>> 8,
r = (num & 0xFF0000) >>> 16
return "#" + ("00" + r.toString(16)).substr(-2) + ("00" + g.toString(16)).substr(-2) + ("00" + b.toString(16)).substr(-2);
}
function getAnnotations(book) {
let annotations = content.filter((item) => item.documentId == book.uniqueId && item.status == 0 && item.pageNumber != undefined && item.color != undefined)
annotations.sort((a, b) => {
const pageDiff = a.pageNumber - b.pageNumber
if (pageDiff == 0) {
return a.createdAt - b.createdAt
}
return pageDiff
})
// Remove duplicate entries from annotiations. First, match by uniqueId, then keep the one with the highest updatedA
annotations = annotations.filter((item, index) => {
return annotations.findIndex((otherItem) => otherItem.uniqueId == item.uniqueId && otherItem.updatedAt > item.updatedAt) == -1
})
const rdata = {
title: book.title,
authors: book.authors,
format: book.type,
pageNumber: parseInt(book.progress.split("/")[1]),
annotations: annotations.map((item) => {
return {
quote: item.quote,
note: item.note,
pageNumber: item.pageNumber,
chapter: item.chapter,
createdAt: item.createdAt,
color: integerToColorHex(item.color)
}
})
}
return rdata
}
const formatedTimestamp = ()=> {
const d = new Date()
const date = d.toISOString().split('T')[0];
const time = d.toTimeString().split(' ')[0].replace(/:/g, '-');
return `${date}-${time}`
}
function download(annotations) {
// Create blob and download
const blob = new Blob([JSON.stringify(annotations, null, 4)], { type: "application/json" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = formatedTimestamp(new Date()) + "-annotations.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
console.log(annotations)
}
GM.registerMenuCommand("Download Book's Annotations", async (event) => {
content = await getContent()
selectBook(content)
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment