Last active
July 29, 2026 14:10
-
-
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 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
| /** | |
| * 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