Google Books Preview Downloader

Add link to download Google Books preview pages into a ZIP file including a book viewer HTML application for use with a web browser (use web browser's zoom feature to zoom in/out pages).

  1. // ==UserScript==
  2. // @name Google Books Preview Downloader
  3. // @namespace https://greasyfork.org/en/users/85671-jcunews
  4. // @version 1.0.3
  5. // @license AGPLv3
  6. // @author jcunews
  7. // @description Add link to download Google Books preview pages into a ZIP file including a book viewer HTML application for use with a web browser (use web browser's zoom feature to zoom in/out pages).
  8. // @include /^https:\/\/books\.google\.com\/books\?.*$/
  9. // @include /^https:\/\/books\.google\.com?\.[a-z]{2}\/books\?.*$/
  10. // @include /^https:\/\/books\.google\.[a-z]{2}\/books\?.*$/
  11. // @require https://cdn.jsdelivr.net/gh/nindogo/tiny_zip_js@4fa0ae770e32bac5181c83d8c5c6a9f59f853e12/tiny_zip.js
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. ((em, dd, dp, dt, ic, dl, rl, pi, pc, dc, tz, ns, fe, ou, z) => {
  16. function cl(e) {
  17. clearInterval(dt);
  18. if (fe && (fe !== 1)) alert(em);
  19. dp.remove();
  20. document.body.style.pointerEvents = ""
  21. }
  22. function fer(e) {
  23. fe = e;
  24. if (--dc === 0) cl(e)
  25. }
  26. function html(s, e) {
  27. (e = document.createElement("DIV")).textContent = s;
  28. return e.innerHTML
  29. }
  30. function gi(ii, ti, si, ct) {
  31. if (fe) return;
  32. dc++;
  33. fetch(ii.page[si].src + "&w=" + dd[1].max_resolution_image_width).then(r => ((ct = r.headers.get("content-type")), r.arrayBuffer())).then((a, b) => {
  34. dc--;
  35. tz.add(ns[ti] = `${("000" + (ti + 1)).substr(-4)}-${ii.page[si].pid}${
  36. dd[0].page[ti].title ? " " + dd[0].page[ti].title.replace(/[\*:\\\|\<\>\/\?]/, "-") : ""
  37. }.${ct ? ct.match(/\/(.*)/)[1].replace("jpeg", "jpg") : "png"}`, new Uint8Array(a));
  38. if (++pc === dl) {
  39. (new Blob([`<!DOCTYPE html>
  40. <html>
  41. <head>
  42. <meta charset=utf-8 />
  43. <!--
  44. Downloaded on $(dltime) from: $(gburl)
  45.  
  46. Using Google Books Preview Downloader v1.0.1.
  47. https://greasyfork.org/en/users/85671-jcunews
  48. -->
  49. <title>$(title)</title>
  50. <style>
  51. body{
  52. margin:1.7vw 0 0 0;background:#bbb;overflow:hidden;text-align:center;
  53. font-family:sans-serif;font-size:1.2vw;
  54. }
  55. button,input{
  56. box-sizing:border-box;border-width:.1vw;padding:0 .25vw;height:1.45vw;
  57. font:inherit;line-height:1vw;
  58. }
  59. #tnav{ display:none; }
  60. #panel{
  61. position:fixed;left:0;top:0;right:0;border-bottom:.1vw solid #000;
  62. box-sizing:border-box;height:1.7vw;background:#ddd;
  63. }
  64. #lnav{
  65. position:absolute;z-index:1;left:1.0vw;top:.1vw;box-sizing:border-box;
  66. border:.1vw solid #55f;border-radius:.3vw;;padding:0 .3vw;
  67. line-height:1.3vw;cursor:pointer;
  68. }
  69. #tnav:checked+#panel #lnav{
  70. border-color:transparent;background:#55f;color:#fff;
  71. }
  72. #title{
  73. position:absolute;left:6vw;width:36vw;height:1.6vw;overflow:hidden;
  74. text-align:left;white-space:nowrap;text-overflow:ellipsis;
  75. }
  76. #pnc{ display:inline-block;margin-top:.1vw;vertical-align:top; }
  77. #pnc button{ margin:0 1vw;width:1.6vw; }
  78. #pn{
  79. vertical-align:top;border-width:.1vw;padding-right:0;width:4vw;
  80. text-align:right;
  81. }
  82. #vgb{ position:absolute;right:.3vw;text-decoration:none }
  83. #nav{
  84. position:fixed;left:0;top:1.7vw;bottom:0;box-sizing:border-box;
  85. border-right:.1vw solid #000;padding-bottom:.4vw;width:6vw;
  86. overflow-y:scroll;background:#ddd;counter-reset:pg;
  87. }
  88. #tnav:not(:checked)~#nav{ display:none; }
  89. #nav a{
  90. display:inline-block;margin-top:.4vw;box-sizing:border-box;
  91. border:.1vw solid #000;width:4vw;background:#fff;
  92. text-decoration:none!important;counter-increment:a;
  93. }
  94. #nav a:after{
  95. display:block;color:#000;font-size:1vw;line-height:1vw;
  96. content:counter(a);
  97. }
  98. #nav a:hover:after{ background:#bbf; }
  99. #nav img{
  100. vertical-align:top;width:100%;object-fit:contain;object-position:0 0;
  101. }
  102. #pages{
  103. position:fixed;left:6vw;top:1.7vw;right:0;bottom:0;padding-bottom:.4vw;
  104. overflow:auto;
  105. }
  106. #tnav:not(:checked)~#pages{ left:0; }
  107. #pages img{
  108. vertical-align:top;margin:.4vw 0 0 .4vw;border:.1vw solid #000;
  109. box-sizing:border-box;width:calc(100% - .8vw);background:#fff;
  110. object-fit:contain;object-position:0 0;
  111. }
  112. </style>
  113. <style id=css></style>
  114. </head>
  115. <body>
  116. <input id=tnav type=checkbox checked />
  117. <div id=panel>
  118. <label id=lnav for=tnav>Nav</label>
  119. <div id=title title="$(title2)">$(title2)</div>
  120. <div id=pnc>
  121. <button id=pp>&lt;</button>
  122. Page <input id=pn type=number value=1 min=1 max=$(pagemax)/>
  123. <button id=np>&gt;</button>
  124. </div>
  125. <a id=vgb href="$(gburl)">View in Google Books</a>
  126. </div>
  127. <div id=nav></div>
  128. <div id=pages></div>
  129. <script>
  130. a = b = "";
  131. "${ns.join("|")}".split("|").forEach((n, i) => {
  132. a += \`<a href=#p$[i + 1]><img src="$[n]" /></a>\`;
  133. b += \`<img id=p$[i + 1] src="$[n]" />\`
  134. });
  135. nav.innerHTML = a;
  136. nav.querySelectorAll("A").forEach((e, i) => e.page = i + 1);
  137. pages.innerHTML = b;
  138. (ps = Array.from(pages.children)).forEach((e, i) => e.page = i + 1);
  139. (tnav.onchange = onresize = () => setTimeout(() => {
  140. css.innerHTML = \`#pages img{width:calc($[pages.offsetWidth - pages.offsetTop * 2]px * $[devicePixelRatio])}\`
  141. }, 0))();
  142. pn.oninput = e => (e = nav.querySelector(\`a[href="#p$[pn.value]"]\`)) && e.click();
  143. pages.onscroll = (vh, st, pm) => {
  144. vh = pages.offsetHeight / 2;
  145. st = pages.scrollTop;
  146. pm = ps[0].offsetTop;
  147. ps.some(e => {
  148. if ((st >= (e.offsetTop - pm - vh)) && (st < (e.offsetTop + e.offsetHeight - vh))) {
  149. pn.value = e.page;
  150. return true
  151. }
  152. })
  153. };
  154. addEventListener("keydown", ev => {
  155. if (!ev.altKey && !ev.ctrlKey && !ev.shiftKey) switch (ev.key) {
  156. case "ArrowLeft":
  157. pp.click()
  158. break;
  159. case "ArrowRight":
  160. np.click()
  161. break;
  162. }
  163. });
  164. addEventListener("click", (ev, e) => {
  165. switch ((e = ev.target).id) {
  166. case "pp":
  167. pn.oninput(pn.value = pn.value > 1 ? parseInt(pn.value) - 1 : 1);
  168. break;
  169. case "np":
  170. if (document.querySelector("#p" + (e = (p = parseInt(pn.value)) + 1))) {
  171. pn.oninput(pn.value = e);
  172. if (parseInt(pn.value) <= e) pn.oninput(pn.value = e + 2)
  173. }
  174. break;
  175. default:
  176. if (e.matches("#nav *")) {
  177. if (!e.href) e = e.parentNode;
  178. document.querySelector(e.hash).scrollIntoView();
  179. pages.scrollBy(0, -pages.children[0].offsetTop);
  180. ev.preventDefault(pn.value = e.page)
  181. }
  182. }
  183. onfocus()
  184. });
  185. onfocus = () => {
  186. if (!document.activeElement || (document.activeElement.id !== "pn")) pages.focus()
  187. };
  188. onload = () => setTimeout(() => pages.onscroll(onresize(pages.focus())), 0)
  189. </script>
  190. </body>
  191. </html>`.replace(/\$\(dltime\)/g, (new Date).toGMTString()
  192. ).replace(/\$\(gburl\)/g, location.href.match(/^[^#]+/)[0]
  193. ).replace(/\$\(title\)/,
  194. b = `${dd[1].title}${(si = document.querySelector('.gb-volume-title span')?.textContent?.trim()) ? ", " + si : ""
  195. }${dd[1].publisher ? ", " + dd[1].publisher : ""}, ${dd[1].volume_id}`
  196. ).replace(/\$\(title2\)/g, html(b)
  197. ).replace(/\$\(pagemax\)/, dd[0].page.length
  198. ).replace(/\$\[([^\]]+)\]/g, "${$1}"
  199. )], {type: "text/html"})).arrayBuffer().then(a => {
  200. tz.add("viewer.html", new Uint8Array(a));
  201. if (ou) URL.revokeObjectURL(ou);
  202. (ct = document.createElement("A")).href = ou = URL.createObjectURL(tz.generate());
  203. ct.download = `${b}.zip`;
  204. ct.style.display = "none";
  205. document.body.appendChild(ct).click();
  206. ct.remove();
  207. cl()
  208. })
  209. }
  210. }).catch(fer)
  211. }
  212. function nx(av) {
  213. try {
  214. if (!confirm(
  215. `Book preview has ${dd[0].page.length} page(s).
  216. Rough estimated download size is ${
  217. (() => {
  218. if (av >= 1024*1048576) {
  219. return parseFloat((av / 1024*1048576).toFixed(2)) + "GB"
  220. } else if (av >= 1048576) {
  221. return parseFloat((av / 1048576).toFixed(2)) + "MB"
  222. } else return parseFloat((av / 1024).toFixed(2)) + "KB"
  223. })()} based only from ${rl.length} already loaded page(s).
  224. \nProceed with the download?`)) return;
  225. ic = document.createElement("DIV");
  226. dl = dd[0].page.length;
  227. rl = Math.ceil(dl / 4);
  228. pi = 0;
  229. pc = 0;
  230. dc = 0;
  231. tz = new tiny_zip;
  232. ns = [];
  233. document.body.style.pointerEvents = "none";
  234. dpp.textContent = `Retrieving pages (1/${dl})...`;
  235. document.documentElement.append(dp);
  236. dt = setInterval(() => (dpp.textContent = `Retrieving pages (${pc}/${dl})...`), 100);
  237. for (let ri = 0; ri < rl; ri++) {
  238. if (fe) break;
  239. let i = ri;
  240. dc++;
  241. fetch(`/books?id=${dd[1].volume_id}&pg=${dd[0].page[i * 4].pid}&jscmd=click3`).then(r => r.json()).then((j, d, k, x, y) => {
  242. dc--;
  243. if (fe) return;
  244. gi(j, i * 4, 0);
  245. z++;
  246. d = 1;
  247. if (i) { x = 2; y = 5
  248. } else { x = 1; y = 4 }
  249. while ((x < y) && (pi < dl)) {
  250. if ((k = i * 4 + d) >= dl) break;
  251. gi(j, k, x);
  252. d++;
  253. x++
  254. }
  255. }).catch(fer)
  256. }
  257. if (fe) cl()
  258. } catch(z) {
  259. alert(em, cl())
  260. }
  261. }
  262. em = "Failed to retrieve data due to site changes.";
  263. if (dd = document.querySelector(".gback+script")) try {
  264. (dp = document.createElement("DIV")).id = "gbpdujs";
  265. dp.innerHTML = `<style>
  266. #gbpdujs{all:revert;position:fixed;z-index:999;left:0;top:0;right:0;bottom:0;background:#0007}
  267. #gbpdujspop{
  268. position:absolute;left:50%;top:50%;transform:translate(-50%);
  269. border:.2em solid #00b;border-radius:.5em;padding:.5em 1em;background:#fff;font-family:sans-serif;
  270. }
  271. </style><div id="gbpdujspop"></div>`;
  272. dpp = dp.lastChild;
  273. dd = JSON.parse("[" + dd.text.match(/_OC_Run\((.*)\);/)[1] + "]");
  274. if (!(ic = document.querySelector(".menu_content p"))) throw 0;
  275. ic.insertAdjacentHTML("afterend", '<p><a href="javascript:void(0)" class="gb-left-nav-link"><span>Download preview pages</span></a></p>');
  276. ic.nextElementSibling.onclick = function() {
  277. this.style.pointerEvents = "none";
  278. if (!(rl = Array.from(viewport.querySelectorAll('img[src*="/books/content"]')).splice(0, 3)).length) return alert(em);
  279. dl = 0;
  280. (function gii(this_, i) {
  281. fetch(rl[i].src, {method: "HEAD"}).then(r => {
  282. dl += parseInt(r.headers.get("content-length"));
  283. if (++i < rl.length) {
  284. gii(this_, i)
  285. } else {
  286. this_.style.pointerEvents = "";
  287. nx(dl / rl.length * dd[0].page.length)
  288. }
  289. }).catch(e => {
  290. this_.style.pointerEvents = "";
  291. fer(e)
  292. })
  293. })(this, 0)
  294. }
  295. } catch(z) {
  296. alert(em)
  297. }
  298. })()