// ==UserScript==
// @name Neopets - Mark Books Read
// @version 2025-07-26
// @description Mark books read in: inventory, neopian shops, user shops, sdb, quick stock, trading post, auctions
// @author senerio
// @match *://*.neopets.com/books_read.phtml?pet_name=*
// @match *://*.neopets.com/moon/books_read.phtml?pet_name=*
// @match *://*.neopets.com/inventory.phtml
// @match *://*.neopets.com/objects.phtml?*obj_type=*
// @match *://*.neopets.com/browseshop.phtml?*owner=*
// @match *://*.neopets.com/safetydeposit.phtml*
// @match *://*.neopets.com/quickstock.phtml*
// @match *://*.neopets.com/island/tradingpost.phtml*
// @match *://*.neopets.com/auctions.phtml*
// @match *://*.neopets.com/genie.phtml*
// @match *://*.neopets.com/generalstore.phtml*
// @match *://items.jellyneo.net/search/*
// @connect itemdb.com.br
// @grant GM_xmlhttpRequest
// @run-at document-end
// @downloadURL https://www.scriptneo.com/scripts/download.php?id=20
// @updateURL https://www.scriptneo.com/scripts/download.php?id=20
// ==/UserScript==
//////////////////////////////////////////////////////
// INSTRUCTIONS
// 1. Set reader name
const petName = '';
// 2. Visit pet's books read pages (redo this step whenever you'd like to update)
// - https://www.neopets.com/books_read.phtml?pet_name=
// - https://www.neopets.com/moon/books_read.phtml?pet_name=
// 3. If you want to clear the saved data, click the reset button, top right of the books read pages
//////////////////////////////////////////////////////
// functions for books read pages ////////////////////
function itemdbGetItemName(array) { // SMH BOOKTASTIC BOOKS PAGE WHY NO BOOK NAME / SMH QUICK STOCK WHY NO ITEM IMG
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://itemdb.com.br/api/v1/items/many',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({
image_id: array
}),
onload: function (res) {
if (res.status === 200) {
const itemData = [];
Object.entries(JSON.parse(res.responseText)).forEach(([k,v]) => { itemData.push(v.name); });
resolve(itemData);
return console.log('[itemdb] Fetched item data');
}
else {
const msg = '[itemdb] Failed to fetch item data';
return console.error(msg, res);
}
},
onerror: function (err) {
reject(err);
}
});
});
}
const booksReadStorage = {
'key': 'np_booksread',
'get': function() {
return JSON.parse(localStorage?.getItem(this.key)) || {};
},
'set': function(arr) { // [ {books, key, append} ]
const books = booksReadStorage.get();
arr.forEach(i => {
if(!i.append) { books[i.key] = []; }
(books[i.key] ??= []).push(...i.books);
})
localStorage?.setItem(this.key, JSON.stringify(books));
},
'all': function() {
const books = this.get();
return [...books.neopian, ...books.booktastic];
}
}
const booksReadPage = {
'type': window.location.pathname.includes('moon') ? 'booktastic' : 'neopian',
'updateBookCount': () => {
const books = booksReadStorage.get();
$('#np_booksread_count').text(`(${books.neopian?.length || 0} Neopian, ${books.booktastic?.length || 0} Booktastic)`);
},
'deleteData': function() {
const arr = [{
books: [],
key: this.type,
append: false
}];
if(this.type == 'booktastic') {
arr.push({ ...arr[0], key: arr[0].key.concat('Img') });
}
booksReadStorage.set(arr);
this.updateBookCount();
},
'storeBooksOnPage': async function() {
const booksStored = booksReadStorage.get();
const bookRows = $(".content table tr:not(:first-child)")
if (bookRows.length == booksStored[this.type]?.length) { return; }
// proceed if needs updating
let booksOnPage = [];
const arr = []
if(this.type == 'neopian') {
bookRows.find('td:last-child').each((i,e) => {
booksOnPage.push( $(e).text().split(': \u00a0').at(0) );
})
booksReadStorage.set([{
books: booksOnPage,
key: 'neopian',
append: false
}]);
}
else if(this.type == 'booktastic') {
bookRows.find('img').each((i,e) => {
booksOnPage.push( $(e).attr('src').match(/.*\/(.*)\..*/).at(-1) );
})
const newBooks = booksOnPage.filter((i)=>{ return !booksStored.booktasticImg?.includes(i) });
booksReadStorage.set([
{
books: booksOnPage,
key: 'booktasticImg',
append: false
},
{
books: await itemdbGetItemName(newBooks),
key: 'booktastic',
append: true
}
]);
}
this.updateBookCount();
},
'displayInitialize': function() {
// delete data button (only for specific page)
$('.content').prepend(
$('⟲')
.click(this.deleteData.bind(this))
);
// counter
$('.content > b:first-of-type').after(` `);
this.updateBookCount();
}
}
// functions for pages with books to mark as read ////
function markBooks(elementsWithItemName, table = false) {
elementsWithItemName.each(function() {
const itemName = $(this);
const isRead = booksReadStorage.all().includes( itemName.text().split('(')[0].trim() )
if(isRead) {
itemName.css('text-decoration', 'line-through');
itemName.parent().css('opacity', '50%');
if(table) itemName.parent().parent().find('img').eq(0).css('opacity', '50%');
}
});
}
const pages = [
{
name: 'inventory',
pageMatcher: /inventory/,
itemNameObject: '.item-name'
},
{
name: 'neopian shop',
pageMatcher: /type=shop/,
itemNameObject: $('.item-name')
},
{
name: 'user shop',
pageMatcher: /browseshop/,
itemNameObject: $('a[href*=buy_item] + br + b')
},
{
name: 'sdb',
pageMatcher: /safetydeposit/,
itemNameObject: $('.content form>table').eq(1).find('tr:not(:first-child):not(:last-child) td:nth-child(2) > b'),
table: true
},
{
name: 'quick stock',
pageMatcher: /quickstock/,
itemNameObject: $('form[name=quickstock] tr:not(:nth-last-child(2)) td:first-child:not([colspan])'),
table: true
},
{
name: 'trading post',
pageMatcher: /tradingpost/,
itemNameObject: $('img[src*="/items/"]').parent()
},
{
name: 'auctions',
pageMatcher: /auctions|genie/,
itemNameObject: $('.content a[href*=auction_id]:not(:has(img))'),
table: true
},
{
name: 'general store',
pageMatcher: /generalstore/,
itemNameObject: $(".contentModule:has(td.contentModuleHeader:contains('Books')) .contentModuleContent .item-title"),
table: true
},
{
name: 'Jellyneo Search',
pageMatcher: /jellyneo.*search/,
itemNameObject: $('.jnflex-grid p a.no-link-icon:nth-of-type(2)'),
table: false
}
]
//////////////////////////////////////////////////////
const loc = window.location.href;
if(loc.match(/books_read/) && loc.includes(petName)) {
booksReadPage.displayInitialize();
booksReadPage.storeBooksOnPage();
}
else if(!loc.match(/books_read/) && localStorage.hasOwnProperty(booksReadStorage.key)) {
const page = pages.find((i) => {
return loc.match(i.pageMatcher)
});
if( ['inventory'].includes(page.name) ) { // for pages that fetch items with ajax call
$(document).on('ajaxSuccess', () => {
markBooks($(page.itemNameObject));
});
}
else {
markBooks(page.itemNameObject);
}
}