Goodreads Libby Results

Searches for the book you are looking at on Goodreads across all your libby libraries

// ==UserScript==
// @name					Goodreads Libby Results
// @namespace			https://github.com/Dylancyclone/goodreads-libby-userscript
// @version				1.2.1
// @description		Searches for the book you are looking at on Goodreads across all your libby libraries
// @author				Dylancyclone
// @match					https://libbyapp.com/interview/menu
// @include				/^https?://.*\.goodreads\.com/book/show.*$/
// @icon					https://www.google.com/s2/favicons?sz=64&domain=libbyapp.com
// @grant					GM.setValue
// @grant					GM.getValue
// @license				MIT
// ==/UserScript==
window.addEventListener(
	"load",
	function () {
		;(function () {
			"use strict"

			const syncLibraries = () => {
				// Grab libraries from libby and remove circular references
				let libraries = unsafeWindow.APP.libraries.all.map((library) => {
					return {
						baseKey: library.baseKey,
						_: { activeKey: library._.activeKey, name: library._.name },
					}
				})
				libraries = JSON.stringify(libraries)
				GM.setValue("libraries", libraries)
			}

			const createLibbyButton = () => {
				let builderDiv = document.createElement("div")
				builderDiv.innerHTML = `
					<div class="summary-list-action">
						<button class="summary-list-action-add-library halo" role="button" type="button">
								<span role="text">Save Libraries (userscript)</span>
						</button>
					</div>
				`.trim()
				let libbySyncButton = builderDiv.firstChild
				libbySyncButton.onclick = syncLibraries
				return libbySyncButton
			}

			const renderSearchResults = async (
				libraries,
				searchString,
				targetDiv,
				openInOverdrive
			) => {
				targetDiv.innerHTML = ""
				if (libraries.length === 0) {
					targetDiv.innerHTML = `No libraries found, please visit <a href="https://libbyapp.com/interview/menu" target="_blank">here</a> to sync your libraries.`
				}

				libraries.map((library) => {
					let libraryKey = library._.activeKey || library.baseKey
					let url = `https://thunder.api.overdrive.com/v2/libraries/${libraryKey}/media?query=${searchString}`
					fetch(url)
						.then((response) => response.json())
						.then((result) => {
							let ebookCount = result.items.filter(
								(item) => item.type.id === "ebook"
							).length
							let audiobookCount = result.items.filter(
								(item) => item.type.id === "audiobook"
							).length
							let link = openInOverdrive
								? `https://${library.baseKey}.overdrive.com/search?query=${searchString}`
								: `https://libbyapp.com/search/${library.baseKey}/search/query-${searchString}/page-1`
							targetDiv.innerHTML += `
								<tr>
									<td style="padding-right: 20px;">${library._.name}</td>
									<td>
										<a href="${link}" target="_blank">
											${ebookCount || "-"} 📕 / ${audiobookCount || "-"} 🎧
										</a>
									</td>
								</tr>`
						})
				})
			}

			const createGoodreadsResults = async () => {
				let builderDiv = document.createElement("div")

				let bookTitle = document
					.querySelector("[data-testid='bookTitle']")
					.innerHTML.trim()
				let bookAuthor = document
					.querySelector("[data-testid='name']")
					.innerHTML.trim()
				let searchString = encodeURIComponent(`${bookTitle} ${bookAuthor}`)
				let libraries = JSON.parse(await GM.getValue("libraries", "[]"))
				let openInOverdrive = JSON.parse(
					await GM.getValue("openInOverdrive", "false")
				)
				builderDiv.innerHTML = `
					<div style="
						background-color: #ececec;
						border: 1px solid black;
						margin-top: 25px;
						padding: 1em;"
					>
						<h3>Libby results</h3>
						<table id="libby-results">
							<tr>
								<th style="margin-right: 20px;">Library</th>
								<th>Results</th>
							</tr>
						</table>
						<div style="margin-top: 10px;">
							<p> Open links in: </p>
							<input type="radio" id="openInLibbyButton" name="openLinksIn" value="libby" ${
								!openInOverdrive ? "checked" : ""
							}>
							<label for="libby">Libby</label>
							<input type="radio" id="openInOverdriveButton" name="openLinksIn" value="overdrive" ${
								openInOverdrive ? "checked" : ""
							}>
							<label for="overdrive">Overdrive</label>
						</div>
					</div>
				`.trim()

				let libbyResults = builderDiv.querySelector("#libby-results")
				let openInLibbyButton = builderDiv.querySelector("#openInLibbyButton")
				openInLibbyButton.onclick = () => {
					GM.setValue("openInOverdrive", "false")
					renderSearchResults(libraries, searchString, libbyResults, false)
				}
				let openInOverdriveButton = builderDiv.querySelector(
					"#openInOverdriveButton"
				)
				openInOverdriveButton.onclick = () => {
					GM.setValue("openInOverdrive", "true")
					renderSearchResults(libraries, searchString, libbyResults, true)
				}

				renderSearchResults(
					libraries,
					searchString,
					libbyResults,
					openInOverdrive
				)

				return builderDiv.firstChild
			}

			/**
			 * Add the buttons
			 * Might outrun the rest of the dom,
			 * so keep retrying until the container is ready
			 */
			const addLibbyButton = () => {
				let container = document.getElementsByClassName("summary-list-section-actions")
				if (container && container[0]) {
					container[0].parentNode.insertBefore(
						createLibbyButton(),
						container[0].nextSibling
					)
				} else {
					setTimeout(addLibbyButton, 10)
				}
			}

			const addGoodreadsResults = async () => {
				let container = document.getElementsByClassName("BookDetails")
				if (container && container[0]) {
					createGoodreadsResults().then((goodreadsResults) => {
						let test = container[0].parentNode.insertBefore(
							goodreadsResults,
							container[0].nextSibling
						)
					})
				} else {
					setTimeout(addGoodreadsResults, 10)
				}
			}

			if (unsafeWindow.location.host == "libbyapp.com") {
				addLibbyButton()
			} else if (unsafeWindow.location.host == "www.goodreads.com") {
				addGoodreadsResults()
			}
		})()
	},
	false
)