Last active
March 3, 2025 11:52
-
-
Save uroybd/03339046aca52f0aefdc533ab852c683 to your computer and use it in GitHub Desktop.
A Tampermonkey script to get annotations from boox cloud
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 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