Skip to content

Instantly share code, notes, and snippets.

@jakecraige
Last active July 29, 2026 14:10
Show Gist options
  • Select an option

  • Save jakecraige/309f337692bd11a22df8bb8cdbe032e9 to your computer and use it in GitHub Desktop.

Select an option

Save jakecraige/309f337692bd11a22df8bb8cdbe032e9 to your computer and use it in GitHub Desktop.
A replacement for obsidian-book-search-plugin for direct use with Obsidian QuickAdd
/**
* This is an updated version of the example QuickAdd book search script, made to allow the user to choose a particular book
* from the results that come back from the api.
*
* Much of it's implementation details are directly from https://github.com/anpigon/obsidian-book-search-plugin, with some additions from https://github.com/anpigon/obsidian-book-search-plugin/pull/158/changes
* I created this to avoid another third-party dependency, fix a few issues I had with it, and to be able to update it if needed given the
* upstream project isn't actively maintained.
*
* Changes Made:
* - Less configuration options
* - Force URL links to use HTTPS (This is so that Notebook Navigator will load them)
* - Escape double quotes fields to stop breaking YAML properties. This happened often with descriptions containing quotes.
*
* Usage: (Basically just this: https://quickadd.obsidian.guide/docs/Examples/Macro_BookFinder/#installation)
* 1. Create a QuickAdd Macro
* 2. Add this script as a QuickAdd User Script.
* 3. Go into the settings for the user script and configure accordingly.
* 4. Create a template as step 2 of the Macro. See "function createBookItem()" for the list of options available to be used in templates.
*
* Cheers, Jake Craige (@jakecraige).
* License: MIT
*/
const notice = (msg) => new Notice(msg, 5000);
const log = (msg) => console.log('book-search:', msg);
const GOOGLE_BOOKS_API_URL = "https://www.googleapis.com/books/v1/volumes";
const ISBN_RE = /^(97[89])?\d{9}[\dX]$/i;
let QuickAdd;
module.exports = {
entry: start,
settings: {
name: "Book Search",
author: "Jake Craige",
options: {
"File Name Format": {
type: "format",
defaultValue: "{{VALUE:title}} - {{VALUE:author}} (book)",
placeholder: "{{VALUE:title}} - {{VALUE:author}}",
description: "Template format for name of file to be created, all book fields are available for use."
},
"Google Books API Key": {
type: "input", // idk why but secret is not working for me as of 7/26/2026
placeholder: "Google API key",
description: "Your Google API key with Books API access."
},
"Max Results": {
type: "text",
defaultValue: "20",
placeholder: "Number",
description: "Maximum number of results to return from Google API."
}
}
}
}
function formatList(list) {
if (!list) { return '' };
return list.length > 1 ? list.map(item => item.trim()).join(', ') : list[0];
}
function yamlStringEscape(value) {
if (!value) { return ''; }
return value.replaceAll("\"", "\\\"");
}
function extractISBNs(industryIdentifiers) {
if (!industryIdentifiers) { return {} };
return industryIdentifiers.reduce((result, item) => {
const isbnType = item.type === 'ISBN_10' ? 'isbn10' : 'isbn13';
result[isbnType] = item.identifier.trim();
return result;
},
{}
);
}
function extractBasicBookInfo(item) {
return {
title: yamlStringEscape(item.title),
subtitle: yamlStringEscape(item.subtitle),
author: yamlStringEscape(formatList(item.authors)),
authors: item.authors?.map(yamlStringEscape),
category: yamlStringEscape(formatList(item.categories)),
categories: item.categories?.map(yamlStringEscape),
publisher: yamlStringEscape(item.publisher),
publishDate: yamlStringEscape(item.publishedDate || ''),
totalPage: item.pageCount,
coverUrl: item.imageLinks ? item.imageLinks.thumbnail.replace("http://", "https://").replace('&edge=curl', '') : '',
coverSmallUrl: item.imageLinks ? item.imageLinks.smallThumbnail.replace("http://", "https://").replace('&edge=curl', '') : '',
description: yamlStringEscape(item.description),
link: item.canonicalVolumeLink || item.infoLink,
previewLink: item.previewLink,
};
}
function createBookItem(item) {
log(item);
const book = {
fileName: '',
title: '',
subtitle: '',
author: '',
authors: [],
category: '',
categories: [],
publisher: '',
publishDate: '',
totalPage: '',
coverUrl: '',
coverSmallUrl: '',
description: '',
link: '',
previewLink: '',
isbn10: '',
isbn13: '',
...extractBasicBookInfo(item),
...extractISBNs(item.industryIdentifiers),
};
return book;
}
function createBookSubtitle(book) {
const publisher = book.publisher ? `, ${book.publisher}` : '';
const publishDate = book.publishDate ? ` (${book.publishDate})` : '';
const totalPage = book.totalPage ? `, p${book.totalPage}` : '';
const subtitle = `${book.author}${publisher}${publishDate}${totalPage}`;
return subtitle;
}
async function promptUserToChooseBook(books) {
const selected = await QuickAdd.quickAddApi.suggester(
books.map(b => (b?.title ?? "No Title") + " | " + createBookSubtitle(b)),
books,
'Choose the book',
true, // allowCustomInput
{
renderItem: (value, el) => {
const book = value || {};
el.addClass('book-suggestion-item');
el.setAttr('style', 'display:flex;align-items:center;margin-bottom:10px;');
const coverImageUrl = book.coverSmallUrl || book.coverUrl;
if (coverImageUrl) {
const imgEl = el.createEl('img', {
cls: 'book-cover-image',
attr: {
src: coverImageUrl,
alt: `Cover Image for ${book.title}`,
},
});
imgEl.setAttr('style', 'max-width:100px;max-height:100px;border-radius:3px;object-fit:cover;margin-right:10px');
}
const textContainer = el.createEl('div', { cls: 'book-text-info' });
textContainer.setAttr('style', 'flex-grow:1;')
const titleEl = textContainer.createEl('div', { text: book?.title ?? 'No Title Provided' });
titleEl.addClass('book-title-text');
titleEl.setAttr('style', 'font-weight:bold');
const subtitleText = createBookSubtitle(book);
const subtitleEl = textContainer.createEl('small', { text: subtitleText });
subtitleEl.addClass('book-subtitle-text');
}
}
);
return selected;
}
const dummyBooks = [
{
author: "Neil Postman",
title: "Amusing Ourselves To Death",
publisher: "Penguin",
publishDate: "2012",
totalPage: 125,
coverUrl: "http://books.google.com/books/content?id=oup6iagfox8C&printsec=frontcover&img=1&zoom=1&source=gbs_api"
},
{
author: "Neil Postman",
title: "Amusing Ourselves To Death",
publisher: "Viking Adult",
publishDate: "2012",
totalPage: 125,
coverUrl: "http://books.google.com/books/content?id=dV0NAQAAMAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
}
];
async function start(params, settings) {
QuickAdd = params;
// Used for testing purposes.
// promptUserToChooseBook(dummyBooks);
// return;
const fileNameFormat = settings["File Name Format"];
const maxResults = settings["Max Results"];
const apiKey = settings["Google Books API Key"];
if (!apiKey) {
new Notice("Please configure your API key in the script settings");
throw new Error("API key not configured");
}
let clipBoardContents = await QuickAdd.quickAddApi.utility.getClipboard();
const q = await QuickAdd.quickAddApi.inputPrompt(
"Search by keyword or ISBN: ", clipBoardContents, clipBoardContents // clipBoardContents is added once as the prompt text and once as the default value
);
if (!q) {
notice("No keyword provided.");
throw new Error("No keyword entered.");
}
let encodedQ;
if (ISBN_RE.test(q.replace(/-/g, ''))) {
encodedQ = `isbn:${encodeURIComponent(q)}`;
} else {
encodedQ = encodeURIComponent(q);
}
log('encodedQ:' + encodedQ);
const finalURL = GOOGLE_BOOKS_API_URL + "?q=" + encodedQ + "&maxResults="+maxResults+"&printType=books&key=" + apiKey;
const response = await fetch(finalURL);
const bookDesc = await response.json();
// The Google Books API omits `items` entirely when a title yields no matches.
if (!bookDesc.items || bookDesc.items.length === 0) {
notice("No results found for: " + q);
throw new Error("No results found for: " + q);
}
const books = bookDesc.items.map(({volumeInfo}) => createBookItem(volumeInfo));
log(books);
const selectedBook = await promptUserToChooseBook(books);
log(selectedBook);
notice("Chose: " + selectedBook.title);
const formattedFileName = await QuickAdd.quickAddApi.format(fileNameFormat, selectedBook);
log("formattedFileName: " + formattedFileName);
QuickAdd.variables = {
...selectedBook,
fileName: replaceIllegalFileNameCharactersInString(formattedFileName)
};
}
function replaceIllegalFileNameCharactersInString(string) {
return string.replace(/[\\,#%&\{\}\/*<>?$\'\":@]*/g, "");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment