-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathbookbuyer.user.js
286 lines (248 loc) · 9.07 KB
/
bookbuyer.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// ==UserScript==
// @name BookBuyer
// @author JonDerThan
// @namespace JonDerThan.github.com
// @version 1.2.2
// @description Allows for quick searching of goodread books on an arbitrary website.
// @match https://www.goodreads.com/*
// @iconURL https://raw.githubusercontent.com/JonDerThan/bookbuyer/main/src/bookbuyer-favicon.png
// @source https://github.com/JonDerThan/bookbuyer
//
// ==/UserScript==
"use strict"
// ---------- CONFIGURATION START ---------------------------------------------
// Use any site that offers a search feature and search for `SEARCH`.
// Consider the example site https://www.amazon.com/s?k=SEARCH&browser=chrome&ref=nav_bar.
// The `SEARCH_SITE` variable is everything left to the `?`. With this example,
// this would be `https://www.amazon.com/s`. The `SEARCH_PARAM` variable is
// found before the `SEARCH`, in the example this would just be `k`.
let SEARCH_SITE = ""
let SEARCH_PARAM = ""
// Whether to use the favicon of the search site as link images.
let USE_SEARCH_SITE_FAVICON = false
// Whether to use the DuckDuckGo image proxy for the favicons.
let USE_DDG_PROXY_FAV = false
// Whether to include the author's name in the search.
let INCL_AUTHOR = false
// You can include search parameters of your search site. If your configured
// search site offers a filter for content choice for example, you'll notice
// that this filter will get added to the URL if you enable it, e.g.
// `content=book_fiction` will be part of the url. You can add this filter to
// the add-on, similar to the examples below.
let searchParams = [
// ["sort", "newest"],
// ["lang", "en"],
// ["lang", "de"],
// ["content", "book_nonfiction"],
// ["content", "book_fiction"],
// ["ext", "epub"],
// ["ext", "pdf"],
]
// ---------- CONFIGURATION END -----------------------------------------------
let ICON_URL = "https://raw.githubusercontent.com/JonDerThan/bookbuyer/main/src/bookbuyer-favicon.png"
let SEARCH_HOSTNAME = ""
// Check whether the script is currently executed as an add-on.
function isAddon() {
return typeof chrome !== "undefined" && typeof chrome.runtime !== "undefined"
}
function parseSettings(data) {
if (!Object.prototype.hasOwnProperty.call(data, "settings")) return
const settings = data.settings
// Parse the add-on settings.
INCL_AUTHOR = settings.findIndex(s => s[0] === "incl_author") != -1
USE_SEARCH_SITE_FAVICON = settings
.findIndex(s => s[0] === "use_search_site_fav") != -1
USE_DDG_PROXY_FAV = settings
.findIndex(s => s[0] === "use_ddg_proxy_fav") != -1
SEARCH_SITE = settings.find(s => s[0] === "search_site")
if (SEARCH_SITE) SEARCH_SITE = SEARCH_SITE[1]
SEARCH_PARAM = settings.find(s => s[0] === "search_param")
if (SEARCH_PARAM) SEARCH_PARAM = SEARCH_PARAM[1]
// Parse the site parameters.
searchParams = settings.filter(s =>
s[0] !== "incl_author"
&& s[0] !== "use_search_site_fav"
&& s[0] !== "use_ddg_proxy_fav"
&& s[0] !== "search_site"
&& s[0] !== "search_param"
&& s[0] !== "lang" // the lang field is parsed below
&& s[1]
)
const lang = settings.find(s => s[0] == "lang")
if (lang) searchParams.push(...lang[1]
.split(",")
.map(l => l.trim())
.filter(l => l)
.map(l => [ "lang", l ])
)
}
function getURL(search, author) {
let httpGetList = searchParams
.map(s => encodeURIComponent(s[0]) + "=" + encodeURIComponent(s[1]))
.join("&")
if (httpGetList.length > 0) httpGetList += "&"
if (INCL_AUTHOR && author) search += " " + fmtAuthor(author)
httpGetList += SEARCH_PARAM + "=" + encodeURIComponent(search)
return `${SEARCH_SITE}?${httpGetList}`
}
function getIconURL() {
const DDG_IMG_PROXY = "https://external-content.duckduckgo.com/iu/?u="
if (!USE_SEARCH_SITE_FAVICON)
return ICON_URL
try {
let url = new URL(SEARCH_SITE)
url = url.origin + "/favicon.ico"
if (USE_DDG_PROXY_FAV)
url = DDG_IMG_PROXY + encodeURIComponent(url)
return url
}
catch (e) {
console.error(e)
return ICON_URL
}
}
function findBookElems() {
let bookElems = []
// finds books on most pages, basically just gets the links to book pages
let elems = document.querySelectorAll("a[href*='/book/show/']")
for (let i = 0; i < elems.length; i++) {
let elem = elems[i]
let author = isBookElem(elem)
if (author == null) continue
bookElems.push([getTitle(elem), author, elem])
}
// find book title on books page - "book/show/"
elems = document.getElementsByClassName("Text__title1")
for (let i = 0; i < elems.length; i++) {
if (elems[i].getAttribute("data-testid") !== "bookTitle") continue
const title = elems[i].innerText
let author = document
.querySelector("a.ContributorLink[href*='/author/show/']")
if (author) author = author.innerText
else author = ""
bookElems.push([title, author, elems[i]])
break
}
return bookElems
}
// Returns `null` if elem isn't a book element, returns the authors name if it
// is. Can return `""` if the element is a book but no author was found.
function isBookElem(elem) {
const BOOK_HREF_REGEX = /\/book\/show\/[^#]+$/
const DONT_MATCH = /Continue reading/
if (!BOOK_HREF_REGEX.test(elem.href)) return null
if (!elem.innerText.length) return null
if (DONT_MATCH.test(elem.innerText)) return null
let author = null
while (elem) {
if (elem.classList.contains("MoreEditions")) return null
if (!author) author = getAuthorChild(elem)
elem = elem.parentElement
}
return author || ""
}
// Given an element, search the children for a link to an author. Returns
// `null` if none was found, else returns the authors name.
function getAuthorChild(elem) {
let a = elem.querySelector("a[href*='/author/show/']")
if (a && a.innerText.length > 0)
return a.innerText
else
return null
}
// Get the title to of the book element. Only return the title and exclude the
// series this book belongs to e.g. "(Harry Potter, #5)".
function getTitle(elem) {
const TITLE = /^(.+?)(?: \(.+, #\d+(?:-\d+)?\))?$/
const matches = elem.innerText.match(TITLE)
return matches[1]
}
// Author's name may be in a format "LASTNAME, FIRSTNAME". This function always
// returns "FIRSTNAME LASTNAME".
function fmtAuthor(author) {
if (!author.includes(","))
return author
return author.split(",").reverse().join(" ").trim()
}
function createLink(title, author) {
// create img
let img = document.createElement("img")
img.setAttribute("src", getIconURL())
let alt = `Search for "${title}" on ${SEARCH_HOSTNAME}`
img.setAttribute("alt", alt)
img.setAttribute("title", alt)
img.style = "height: 1.2em;"
// create a
const url = getURL(title, author)
let a = document.createElement("a")
a.setAttribute("href", url)
a.setAttribute("target", "_blank")
a.style.setProperty("margin", ".25em")
a.appendChild(img)
return a
}
let pendingChecks = [-1, -1, -1, -1]
function refreshPendingChecks(func) {
// clear out remaining ones:
pendingChecks.forEach(clearTimeout)
// set new ones
pendingChecks[0] = setTimeout(func, 1000)
pendingChecks[1] = setTimeout(func, 2000)
pendingChecks[2] = setTimeout(func, 3000)
pendingChecks[3] = setTimeout(func, 5000)
}
function injectLinks() {
let elems = findBookElems()
elems = elems.filter((elem) => !elem[2].innerHTML.includes(SEARCH_SITE))
elems.forEach((elem) => {
let a = createLink(elem[0], elem[1])
elem[2].appendChild(a)
})
if (elems.length > 0) pendingChecks.forEach(clearTimeout)
}
async function main() {
if (!SEARCH_SITE || !SEARCH_PARAM) {
alert("The BookBuyer add-on can only work if the search site is configured!")
if (isAddon())
window.open(chrome.runtime.getURL("options.html"))
else
window.open("https://github.com/JonDerThan/bookbuyer#userscript-users")
return
}
SEARCH_HOSTNAME = (new URL(SEARCH_SITE)).hostname
let lastScroll = 0
addEventListener("scroll", (e) => {
// checked less than .5s ago
if (e.timeStamp - lastScroll < 500) return
lastScroll = e.timeStamp
refreshPendingChecks(injectLinks)
})
injectLinks()
}
// Run as an add-on.
if (isAddon()) {
ICON_URL = chrome.runtime.getURL("bookbuyer-favicon.png")
// FIXME: Despite documentation saying otherwise, apparently Firefox supports
// both the callback AND the promises API for the `chrome.storage.sync.get`
// API, when run with manifest v2. As per the docs, with v2, only the
// callback API should be supported. Since the Chrome version is run with v3
// anyways (where the promises API is adopted), this works in both Firefox
// and Chrome. Since the Firefox version should switch over to v3 soon, this
// undocumented version is kept. Remove this note when manifest v3 is used
// for the Firefox version too.
// Note: Seemingly this only applies to content scripts. In the settings page
// only the callback API is supported.
chrome.storage.sync.get("settings")
.then(data => {
parseSettings(data)
main()
})
.catch(e => {
console.error("error loading settings: " + e)
main()
})
}
// Run as a userscript.
else {
main()
}