LibreGRAB

Download all the booty!

当前为 2025-02-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name LibreGRAB
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-02-22
  5. // @description Download all the booty!
  6. // @author HeronErin
  7. // @license MIT
  8. // @supportURL https://github.com/HeronErin/LibbyRip/issues
  9. // @match *://*.listen.libbyapp.com/*
  10. // @match *://*.listen.overdrive.com/*
  11. // @match *://*.read.libbyapp.com/?*
  12. // @match *://*.read.overdrive.com/?*
  13. // @run-at document-start
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=libbyapp.com
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  16. // @grant none
  17. // ==/UserScript==
  18.  
  19. (()=>{
  20.  
  21. let downloadElem;
  22. const CSS = `
  23. .pNav{
  24. background-color: red;
  25. width: 100%;
  26. display: flex;
  27. justify-content: space-between;
  28. }
  29. .pLink{
  30. color: blue;
  31. text-decoration-line: underline;
  32. padding: .25em;
  33. font-size: 1em;
  34. }
  35. .foldMenu{
  36. position: absolute;
  37. width: 100%;
  38. height: 0%;
  39. z-index: 1000;
  40.  
  41. background-color: grey;
  42.  
  43. overflow-x: hidden;
  44. overflow-y: scroll;
  45.  
  46. transition: height 0.3s
  47. }
  48. .active{
  49. height: 40%;
  50. border: double;
  51. }
  52. .pChapLabel{
  53. font-size: 2em;
  54. }`;
  55. /* =========================================
  56. BEGIN AUDIOBOOK SECTION!
  57. =========================================
  58. */
  59.  
  60. const audioBookNav = `
  61. <a class="pLink" id="chap"> <h1> View chapters </h1> </a>
  62. <a class="pLink" id="dow"> <h1> Download chapters </h1> </a>
  63. <a class="pLink" id="exp"> <h1> Export audiobook </h1> </a>
  64. `;
  65. const chaptersMenu = `
  66. <h2>This book contains {CHAPTERS} chapters.</h2>
  67. `;
  68. let chapterMenuElem;
  69.  
  70. function buildPirateUi(){
  71. // Create the nav
  72. let nav = document.createElement("div");
  73. nav.innerHTML = audioBookNav;
  74. nav.querySelector("#chap").onclick = viewChapters;
  75. nav.querySelector("#dow").onclick = downloadChapters;
  76. nav.querySelector("#exp").onclick = exportChapters;
  77. nav.classList.add("pNav");
  78. let pbar = document.querySelector(".nav-progress-bar");
  79. pbar.insertBefore(nav, pbar.children[1]);
  80.  
  81. // Create the chapters menu
  82. chapterMenuElem = document.createElement("div");
  83. chapterMenuElem.classList.add("foldMenu");
  84. chapterMenuElem.setAttribute("tabindex", "-1"); // Don't mess with tab key
  85. const urls = getUrls();
  86.  
  87. chapterMenuElem.innerHTML = chaptersMenu.replace("{CHAPTERS}", urls.length);
  88. document.body.appendChild(chapterMenuElem);
  89.  
  90. downloadElem = document.createElement("div");
  91. downloadElem.classList.add("foldMenu");
  92. downloadElem.setAttribute("tabindex", "-1"); // Don't mess with tab key
  93. document.body.appendChild(downloadElem);
  94.  
  95.  
  96. }
  97. function getUrls(){
  98. let ret = [];
  99.  
  100. // New libby version uses a special object for the encoded urls.
  101. // They use a much more complex alg for calculating the url, but it is exposed (by accedent)
  102. for (let spine of BIF.objects.spool.components){
  103. // Delete old fake value
  104. let old_whereabouts = spine["_whereabouts"];
  105. delete spine["_whereabouts"];
  106.  
  107. // Call the function to decode the true media path
  108. let true_whereabouts = spine._whereabouts();
  109.  
  110. // Reset to original value
  111. spine["_whereabouts"] = old_whereabouts;
  112.  
  113. let data = {
  114. url: location.origin + "/" + true_whereabouts,
  115. index : spine.meta["-odread-spine-position"],
  116. duration: spine.meta["audio-duration"],
  117. size: spine.meta["-odread-file-bytes"],
  118. type: spine.meta["media-type"]
  119. };
  120. ret.push(data);
  121. }
  122. return ret;
  123. }
  124. function paddy(num, padlen, padchar) {
  125. var pad_char = typeof padchar !== 'undefined' ? padchar : '0';
  126. var pad = new Array(1 + padlen).join(pad_char);
  127. return (pad + num).slice(-pad.length);
  128. }
  129. let firstChapClick = true;
  130. function viewChapters(){
  131. // Populate chapters ONLY after first viewing
  132. if (firstChapClick){
  133. firstChapClick = false;
  134. for (let url of getUrls()){
  135. let span = document.createElement("span");
  136. span.classList.add("pChapLabel")
  137. span.textContent = "#" + (1 + url.index);
  138.  
  139. let audio = document.createElement("audio");
  140. audio.setAttribute("controls", "");
  141. let source = document.createElement("source");
  142. source.setAttribute("src", url.url);
  143. source.setAttribute("type", url.type);
  144. audio.appendChild(source);
  145.  
  146. chapterMenuElem.appendChild(span);
  147. chapterMenuElem.appendChild(document.createElement("br"));
  148. chapterMenuElem.appendChild(audio);
  149. chapterMenuElem.appendChild(document.createElement("br"));
  150. }
  151. }
  152. if (chapterMenuElem.classList.contains("active"))
  153. chapterMenuElem.classList.remove("active")
  154. else
  155. chapterMenuElem.classList.add("active")
  156. }
  157. async function createMetadata(zip){
  158. let folder = zip.folder("metadata");
  159.  
  160. let spineToIndex = BIF.map.spine.map((x)=>x["-odread-original-path"]);
  161. let metadata = {
  162. title: BIF.map.title.main,
  163. description: BIF.map.description,
  164. coverUrl: BIF.root.querySelector("image").getAttribute("href"),
  165. creator: BIF.map.creator,
  166. spine: BIF.map.spine.map((x)=>{return {
  167. duration: x["audio-duration"],
  168. type: x["media-type"],
  169. bitrate: x["audio-bitrate"],
  170. }})
  171. };
  172. const response = await fetch(metadata.coverUrl);
  173. const blob = await response.blob();
  174. const csplit = metadata.coverUrl.split(".");
  175. folder.file("cover." + csplit[csplit.length-1], blob, { compression: "STORE" });
  176.  
  177. if (BIF.map.nav.toc != undefined){
  178. metadata.chapters = BIF.map.nav.toc.map((rChap)=>{
  179. return {
  180. title: rChap.title,
  181. spine: spineToIndex.indexOf(rChap.path.split("#")[0]),
  182. offset: 1*(rChap.path.split("#")[1] | 0)
  183. };
  184. });
  185. }
  186. folder.file("metadata.json", JSON.stringify(metadata, null, 2));
  187. }
  188.  
  189. let downloadState = -1;
  190. async function createAndDownloadZip(urls, addMeta) {
  191. const zip = new JSZip();
  192.  
  193. // Fetch all files and add them to the zip
  194. const fetchPromises = urls.map(async (url) => {
  195. const response = await fetch(url.url);
  196. const blob = await response.blob();
  197. const filename = "Chapter " + paddy(url.index + 1, 3) + ".mp3";
  198.  
  199. let partElem = document.createElement("div");
  200. partElem.textContent = "Download of "+ filename + " complete";
  201. downloadElem.appendChild(partElem);
  202. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  203.  
  204. downloadState += 1;
  205.  
  206. zip.file(filename, blob, { compression: "STORE" });
  207. });
  208. if (addMeta)
  209. fetchPromises.push(createMetadata(zip));
  210.  
  211. // Wait for all files to be fetched and added to the zip
  212. await Promise.all(fetchPromises);
  213.  
  214.  
  215. downloadElem.innerHTML += "<br><b>Downloads complete!</b> Now waiting for them to be assembled! (This might take a <b><i>minute</i></b>) <br>";
  216. downloadElem.innerHTML += "Zip progress: <b id='zipProg'>0</b>%";
  217.  
  218. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  219.  
  220. // Generate the zip file
  221. const zipBlob = await zip.generateAsync({
  222. type: 'blob',
  223. compression: "STORE",
  224. streamFiles: true,
  225. }, (meta)=>{
  226. if (meta.percent)
  227. downloadElem.querySelector("#zipProg").textContent = meta.percent.toFixed(2);
  228.  
  229. });
  230.  
  231. downloadElem.innerHTML += "Generated zip file! <br>"
  232. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  233.  
  234. // Create a download link for the zip file
  235. const downloadUrl = URL.createObjectURL(zipBlob);
  236.  
  237. downloadElem.innerHTML += "Generated zip file link! <br>"
  238. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  239.  
  240. const link = document.createElement('a');
  241. link.href = downloadUrl;
  242. link.download = BIF.map.title.main + '.zip';
  243. document.body.appendChild(link);
  244. link.click();
  245. link.remove();
  246.  
  247. downloadState = -1;
  248. downloadElem.innerHTML = ""
  249. downloadElem.classList.remove("active");
  250.  
  251. // Clean up the object URL
  252. setTimeout(() => URL.revokeObjectURL(downloadUrl), 100);
  253. }
  254. function downloadChapters(){
  255. if (downloadState != -1)
  256. return;
  257.  
  258. downloadState = 0;
  259. downloadElem.classList.add("active");
  260. downloadElem.innerHTML = "<b>Starting download</b><br>";
  261. createAndDownloadZip(getUrls()).then((p)=>{});
  262.  
  263. }
  264. function exportChapters(){
  265. if (downloadState != -1)
  266. return;
  267.  
  268. downloadState = 0;
  269. downloadElem.classList.add("active");
  270. downloadElem.innerHTML = "<b>Starting export</b><br>";
  271. createAndDownloadZip(getUrls(), true).then((p)=>{});
  272. }
  273.  
  274. // Main entry point for audiobooks
  275. function bifFoundAudiobook(){
  276. // New global style info
  277. let s = document.createElement("style");
  278. s.innerHTML = CSS;
  279. document.head.appendChild(s)
  280.  
  281. buildPirateUi();
  282. }
  283.  
  284.  
  285.  
  286. /* =========================================
  287. END AUDIOBOOK SECTION!
  288. =========================================
  289. */
  290.  
  291. /* =========================================
  292. BEGIN BOOK SECTION!
  293. =========================================
  294. */
  295. const bookNav = `
  296. <div style="text-align: center; width: 100%;">
  297. <a class="pLink" id="download"> <h1> Download EPUB </h1> </a>
  298. </div>
  299. `;
  300. window.pages = {};
  301.  
  302. // Libby used the bind method as a way to "safely" expose
  303. // the decryption module. THIS IS THEIR DOWNFALL.
  304. // As we can hook bind, allowing us to obtain the
  305. // decryption function
  306. const originalBind = Function.prototype.bind;
  307. Function.prototype.bind = function(...args) {
  308. const boundFn = originalBind.apply(this, args);
  309. boundFn.__boundArgs = args.slice(1); // Store bound arguments (excluding `this`)
  310. return boundFn;
  311. };
  312.  
  313.  
  314. async function waitForChapters(callback){
  315. let components = getBookComponents();
  316. // Force all the chapters to load in.
  317. components.forEach(page =>{
  318. if (undefined != window.pages[page.id]) return;
  319. page._loadContent({callback: ()=>{}})
  320. });
  321. // But its not instant, so we need to wait until they are all set (see: bifFound())
  322. while (components.filter((page)=>undefined==window.pages[page.id]).length){
  323. await new Promise(r => setTimeout(r, 100));
  324. callback();
  325. console.log(components.filter((page)=>undefined==window.pages[page.id]).length);
  326. }
  327. }
  328. function getBookComponents(){
  329. return BIF.objects.reader._.context.spine._.components.filter(p => "hidden" != (p.block || {}).behavior)
  330. }
  331. function truncate(path){
  332. return path.substring(path.lastIndexOf('/') + 1);
  333. }
  334. function goOneLevelUp(url) {
  335. let u = new URL(url);
  336. if (u.pathname === "/") return url; // Already at root
  337.  
  338. u.pathname = u.pathname.replace(/\/[^/]*\/?$/, "/");
  339. return u.toString();
  340. }
  341. function getFilenameFromURL(url) {
  342. const parsedUrl = new URL(url);
  343. const pathname = parsedUrl.pathname;
  344. return pathname.substring(pathname.lastIndexOf('/') + 1);
  345. }
  346. async function createContent(oebps, imgAssests){
  347.  
  348. let cssRegistry = {};
  349.  
  350. let components = getBookComponents();
  351. let totComp = components.length;
  352. downloadElem.innerHTML += `Gathering chapters <span id="chapAcc"> 0/${totComp} </span><br>`
  353. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  354.  
  355. let gc = 0;
  356. await waitForChapters(()=>{
  357. gc+=1;
  358. downloadElem.querySelector("span#chapAcc").innerHTML = ` ${components.filter((page)=>undefined!=window.pages[page.id]).length}/${totComp}`;
  359. });
  360.  
  361. downloadElem.innerHTML += `Chapter gathering complete<br>`
  362. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  363.  
  364. let idToIfram = {};
  365. components.forEach(c=>{
  366. // Nothing that can be done here...
  367. if (c.sheetBox.querySelector("iframe") == null){
  368. console.warn("!!!" + window.pages[c.id]);
  369. return;
  370. }
  371. idToIfram[c.id] = c.sheetBox.querySelector("iframe");
  372.  
  373. c.sheetBox.querySelector("iframe").contentWindow.document.querySelectorAll("link").forEach(link=>{
  374. cssRegistry[c.id] = cssRegistry[c.id] || [];
  375. cssRegistry[c.id].push(link.href);
  376.  
  377. if (imgAssests.includes(link.href)) return;
  378. imgAssests.push(link.href);
  379.  
  380.  
  381. });
  382. });
  383. let url = location.origin;
  384. for (let i of Object.keys(window.pages)){
  385. if (idToIfram[i])
  386. url = idToIfram[i].src;
  387. oebps.file(truncate(i), fixXhtml(url, window.pages[i], imgAssests, cssRegistry[i] || []));
  388. }
  389.  
  390. downloadElem.innerHTML += `Downloading assets <span id="assetGath"> 0/${imgAssests.length} </span><br>`
  391. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  392.  
  393.  
  394. gc = 0;
  395. await Promise.all(imgAssests.map(name=>(async function(){
  396. const response = await fetch(name.startsWith("http") ? name : location.origin + "/" + name);
  397. if (response.status != 200) {
  398. downloadElem.innerHTML += `<b>WARNING:</b> Could not fetch ${name}<br>`
  399. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  400. return;
  401. }
  402. const blob = await response.blob();
  403.  
  404. oebps.file(name.startsWith("http") ? getFilenameFromURL(name) : name, blob, { compression: "STORE" });
  405.  
  406. gc+=1;
  407. downloadElem.querySelector("span#assetGath").innerHTML = ` ${gc}/${imgAssests.length} `;
  408. })()));
  409. }
  410. function enforceEpubXHTML(url, htmlString, assetRegistry, links) {
  411. const parser = new DOMParser();
  412. const doc = parser.parseFromString(htmlString, 'text/html');
  413.  
  414. // Convert all elements to lowercase tag names
  415. const elements = doc.getElementsByTagName('*');
  416. for (let el of elements) {
  417. const newElement = doc.createElement(el.tagName.toLowerCase());
  418.  
  419. // Copy attributes to the new element
  420. for (let attr of el.attributes) {
  421. newElement.setAttribute(attr.name, attr.value);
  422. }
  423.  
  424. // Move child nodes to the new element
  425. while (el.firstChild) {
  426. newElement.appendChild(el.firstChild);
  427. }
  428.  
  429. // Replace old element with the new one
  430. el.parentNode.replaceChild(newElement, el);
  431. }
  432.  
  433. for (let el of elements) {
  434. if (el.tagName.toLowerCase() == "img" || el.tagName.toLowerCase() == "image"){
  435. let src = el.getAttribute("src") || el.getAttribute("xlink:href");
  436. if (!src) continue;
  437.  
  438. if (!(src.startsWith("http://") || src.startsWith("https://"))){
  439. src = (new URL(src, new URL(url))).toString();
  440. }
  441. if (!assetRegistry.includes(src))
  442. assetRegistry.push(src);
  443.  
  444. if (el.getAttribute("src"))
  445. el.setAttribute("src", truncate(src));
  446. if (el.getAttribute("xlink:href"))
  447. el.setAttribute("xlink:href", truncate(src));
  448. }
  449. }
  450.  
  451.  
  452. // Ensure the <head> element exists with a <title>
  453. let head = doc.querySelector('head');
  454. if (!head) {
  455. head = doc.createElement('head');
  456. doc.documentElement.insertBefore(head, doc.documentElement.firstChild);
  457. }
  458.  
  459. let title = head.querySelector('title');
  460. if (!title) {
  461. title = doc.createElement('title');
  462. title.textContent = BIF.map.title.main; // Default title
  463. head.appendChild(title);
  464. }
  465.  
  466. for (let link of links){
  467. let linkElement = doc.createElement('link');
  468. linkElement.setAttribute("href", link);
  469. linkElement.setAttribute("rel", "stylesheet");
  470. linkElement.setAttribute("type", "text/css");
  471. head.appendChild(linkElement);
  472. }
  473.  
  474. // Get the serialized XHTML string
  475. const serializer = new XMLSerializer();
  476. let xhtmlString = serializer.serializeToString(doc);
  477.  
  478. // Ensure proper namespaces (if not already present)
  479. if (!xhtmlString.includes('xmlns="http://www.w3.org/1999/xhtml"')) {
  480. xhtmlString = xhtmlString.replace('<html>', '<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xmlns:m="http://www.w3.org/1998/Math/MathML" xmlns:pls="http://www.w3.org/2005/01/pronunciation-lexicon" xmlns:ssml="http://www.w3.org/2001/10/synthesis" xmlns:svg="http://www.w3.org/2000/svg">');
  481. }
  482.  
  483. return xhtmlString;
  484. }
  485. function fixXhtml(url, html, assetRegistry, links){
  486. html = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
  487. ` + enforceEpubXHTML(url, `<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xmlns:m="http://www.w3.org/1998/Math/MathML" xmlns:pls="http://www.w3.org/2005/01/pronunciation-lexicon" xmlns:ssml="http://www.w3.org/2001/10/synthesis" xmlns:svg="http://www.w3.org/2000/svg">`
  488. + html + `</html>`, assetRegistry, links);
  489.  
  490.  
  491.  
  492. return html;
  493. }
  494. function getMimeTypeFromFileName(fileName) {
  495. const mimeTypes = {
  496. jpg: 'image/jpeg',
  497. jpeg: 'image/jpeg',
  498. png: 'image/png',
  499. gif: 'image/gif',
  500. bmp: 'image/bmp',
  501. webp: 'image/webp',
  502. mp4: 'video/mp4',
  503. mp3: 'audio/mp3',
  504. pdf: 'application/pdf',
  505. txt: 'text/plain',
  506. html: 'text/html',
  507. css: 'text/css',
  508. json: 'application/json',
  509. // Add more extensions as needed
  510. };
  511.  
  512. const ext = fileName.split('.').pop().toLowerCase();
  513. return mimeTypes[ext] || 'application/octet-stream';
  514. }
  515. function makePackage(oebps, assetRegistry){
  516. const doc = document.implementation.createDocument(
  517. 'http://www.idpf.org/2007/opf', // default namespace
  518. 'package', // root element name
  519. null // do not specify a doctype
  520. );
  521.  
  522. // Step 2: Set attributes for the root element
  523. const packageElement = doc.documentElement;
  524. packageElement.setAttribute('version', '2.0');
  525. packageElement.setAttribute('xml:lang', 'en');
  526. packageElement.setAttribute('unique-identifier', 'pub-identifier');
  527. packageElement.setAttribute('xmlns', 'http://www.idpf.org/2007/opf');
  528. packageElement.setAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
  529. packageElement.setAttribute('xmlns:dcterms', 'http://purl.org/dc/terms/');
  530.  
  531. // Step 3: Create and append child elements to the root
  532. const metadata = doc.createElementNS('http://www.idpf.org/2007/opf', 'metadata');
  533. packageElement.appendChild(metadata);
  534.  
  535. // Create child elements for metadata
  536. const dcIdentifier = doc.createElementNS('http://purl.org/dc/elements/1.1/', 'dc:identifier');
  537. dcIdentifier.setAttribute('id', 'pub-identifier');
  538. dcIdentifier.textContent = "" + BIF.map["-odread-buid"];
  539. metadata.appendChild(dcIdentifier);
  540.  
  541. if (BIF.map.language.length){
  542. const dcLanguage = doc.createElementNS('http://purl.org/dc/elements/1.1/', 'dc:language');
  543. dcLanguage.setAttribute('xsi:type', 'dcterms:RFC4646');
  544. dcLanguage.textContent = BIF.map.language[0];
  545. metadata.appendChild(dcLanguage);
  546. }
  547.  
  548. const metaIdentifier = doc.createElementNS('http://www.idpf.org/2007/opf', 'meta');
  549. metaIdentifier.setAttribute('id', 'meta-identifier');
  550. metaIdentifier.setAttribute('property', 'dcterms:identifier');
  551. metaIdentifier.textContent = "" + BIF.map["-odread-buid"];
  552. metadata.appendChild(metaIdentifier);
  553.  
  554. const dcTitle = doc.createElementNS('http://purl.org/dc/elements/1.1/', 'dc:title');
  555. dcTitle.setAttribute('id', 'pub-title');
  556. dcTitle.textContent = BIF.map.title.main;
  557. metadata.appendChild(dcTitle);
  558.  
  559. // Add other elements similarly...
  560.  
  561. // Step 4: Create the manifest, spine, guide, and other sections...
  562. const manifest = doc.createElementNS('http://www.idpf.org/2007/opf', 'manifest');
  563. packageElement.appendChild(manifest);
  564.  
  565. const spine = doc.createElementNS('http://www.idpf.org/2007/opf', 'spine');
  566. spine.setAttribute("toc", "ncx");
  567. packageElement.appendChild(spine);
  568.  
  569.  
  570. const item = doc.createElementNS('http://www.idpf.org/2007/opf', 'item');
  571. item.setAttribute('id', 'ncx');
  572. item.setAttribute('href', 'toc.ncx');
  573. item.setAttribute('media-type', 'application/x-dtbncx+xml');
  574. manifest.appendChild(item);
  575.  
  576.  
  577. // Generate out the manifest
  578. let components = getBookComponents();
  579. components.forEach(chapter =>{
  580. const item = doc.createElementNS('http://www.idpf.org/2007/opf', 'item');
  581. item.setAttribute('id', chapter.meta.id);
  582. item.setAttribute('href', truncate(chapter.meta.path));
  583. item.setAttribute('media-type', 'application/xhtml+xml');
  584. manifest.appendChild(item);
  585.  
  586.  
  587. const itemref = doc.createElementNS('http://www.idpf.org/2007/opf', 'itemref');
  588. itemref.setAttribute('idref', chapter.meta.id);
  589. itemref.setAttribute('linear', "yes");
  590. spine.appendChild(itemref);
  591. });
  592.  
  593. assetRegistry.forEach(asset => {
  594. const item = doc.createElementNS('http://www.idpf.org/2007/opf', 'item');
  595. let aname = asset.startsWith("http") ? getFilenameFromURL(asset) : asset;
  596. item.setAttribute('id', aname.split(".")[0]);
  597. item.setAttribute('href', aname);
  598. item.setAttribute('media-type', getMimeTypeFromFileName(aname));
  599. manifest.appendChild(item);
  600. });
  601.  
  602. // Step 5: Serialize the document to a string
  603. const serializer = new XMLSerializer();
  604. const xmlString = serializer.serializeToString(doc);
  605.  
  606. oebps.file("content.opf", `<?xml version="1.0" encoding="utf-8" standalone="no"?>\n` + xmlString);
  607. }
  608. function makeToc(oebps){
  609. // Step 1: Create the document with a default namespace
  610. const doc = document.implementation.createDocument(
  611. 'http://www.daisy.org/z3986/2005/ncx/', // default namespace
  612. 'ncx', // root element name
  613. null // do not specify a doctype
  614. );
  615.  
  616. // Step 2: Set attributes for the root element
  617. const ncxElement = doc.documentElement;
  618. ncxElement.setAttribute('version', '2005-1');
  619.  
  620. // Step 3: Create and append child elements to the root
  621. const head = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'head');
  622. ncxElement.appendChild(head);
  623.  
  624. const uidMeta = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'meta');
  625. uidMeta.setAttribute('name', 'dtb:uid');
  626. uidMeta.setAttribute('content', "" + BIF.map["-odread-buid"]);
  627. head.appendChild(uidMeta);
  628.  
  629. // Step 4: Create docTitle and add text
  630. const docTitle = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'docTitle');
  631. ncxElement.appendChild(docTitle);
  632.  
  633. const textElement = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'text');
  634. textElement.textContent = BIF.map.title.main;
  635. docTitle.appendChild(textElement);
  636.  
  637. // Step 5: Create navMap and append navPoint elements
  638. const navMap = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'navMap');
  639. ncxElement.appendChild(navMap);
  640.  
  641.  
  642. let components = getBookComponents();
  643.  
  644. components.forEach(chapter =>{
  645. // First navPoint
  646. const navPoint1 = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'navPoint');
  647. navPoint1.setAttribute('id', chapter.meta.id);
  648. navPoint1.setAttribute('playOrder', '' + (1+chapter.index));
  649. navMap.appendChild(navPoint1);
  650.  
  651. const navLabel1 = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'navLabel');
  652. navPoint1.appendChild(navLabel1);
  653.  
  654. const text1 = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'text');
  655. text1.textContent = BIF.map.title.main;
  656. navLabel1.appendChild(text1);
  657.  
  658. const content1 = doc.createElementNS('http://www.daisy.org/z3986/2005/ncx/', 'content');
  659. content1.setAttribute('src', truncate(chapter.meta.path));
  660. navPoint1.appendChild(content1);
  661. });
  662.  
  663.  
  664. // Step 6: Serialize the document to a string
  665. const serializer = new XMLSerializer();
  666. const xmlString = serializer.serializeToString(doc);
  667.  
  668. oebps.file("toc.ncx", `<?xml version="1.0" encoding="utf-8" standalone="no"?>\n` + xmlString);
  669. }
  670. async function downloadEPUB(){
  671. let imageAssets = new Array();
  672.  
  673.  
  674. const zip = new JSZip();
  675. zip.file("mimetype", "application/epub+zip", {compression: "STORE"});
  676. zip.folder("META-INF").file("container.xml", `<?xml version="1.0" encoding="UTF-8"?>
  677. <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  678. <rootfiles>
  679. <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
  680. </rootfiles>
  681. </container>
  682. `);
  683.  
  684. let oebps = zip.folder("OEBPS");
  685. await createContent(oebps, imageAssets);
  686.  
  687. makePackage(oebps, imageAssets);
  688. makeToc(oebps);
  689.  
  690.  
  691. downloadElem.innerHTML += "<br><b>Downloads complete!</b> Now waiting for them to be assembled! (This might take a <b><i>minute</i></b>) <br>";
  692. downloadElem.innerHTML += "Zip progress: <b id='zipProg'>0</b>%<br>";
  693.  
  694.  
  695. // Generate the zip file
  696. const zipBlob = await zip.generateAsync({
  697. type: 'blob',
  698. compression: "DEFLATE",
  699. streamFiles: true,
  700. }, (meta)=>{
  701. if (meta.percent)
  702. downloadElem.querySelector("#zipProg").textContent = meta.percent.toFixed(2);
  703.  
  704. });
  705.  
  706.  
  707. downloadElem.innerHTML += `EPUB generation complete! Starting download<br>`
  708. downloadElem.scrollTo(0, downloadElem.scrollHeight);
  709.  
  710. const downloadUrl = URL.createObjectURL(zipBlob);
  711. const link = document.createElement('a');
  712. link.href = downloadUrl;
  713. link.download = BIF.map.title.main + '.epub';
  714. link.click();
  715.  
  716.  
  717.  
  718. // Clean up the object URL
  719. setTimeout(() => URL.revokeObjectURL(downloadUrl), 100);
  720.  
  721. downloadState = -1;
  722. }
  723.  
  724. // Main entry point for audiobooks
  725. function bifFoundBook(){
  726. // New global style info
  727. let s = document.createElement("style");
  728. s.innerHTML = CSS;
  729. document.head.appendChild(s)
  730.  
  731. if (!window.__bif_cfc1){
  732. alert("Injection failed! __bif_cfc1 not found");
  733. return;
  734. }
  735. const old_crf1 = window.__bif_cfc1;
  736. window.__bif_cfc1 = (win, edata)=>{
  737. // If the bind hook succeeds, then the first element of bound args
  738. // will be the decryption function. So we just passivly build up an
  739. // index of the pages!
  740. pages[win.name] = old_crf1.__boundArgs[0](edata);
  741. return old_crf1(win, edata);
  742. };
  743.  
  744. buildBookPirateUi();
  745. }
  746.  
  747. function downloadEPUBBBtn(){
  748. if (downloadState != -1)
  749. return;
  750.  
  751. downloadState = 0;
  752. downloadElem.classList.add("active");
  753. downloadElem.innerHTML = "<b>Starting download</b><br>";
  754.  
  755. downloadEPUB().then(()=>{});
  756. }
  757. function buildBookPirateUi(){
  758. // Create the nav
  759. let nav = document.createElement("div");
  760. nav.innerHTML = bookNav;
  761. nav.querySelector("#download").onclick = downloadEPUBBBtn;
  762. nav.classList.add("pNav");
  763. let pbar = document.querySelector(".nav-progress-bar");
  764. pbar.insertBefore(nav, pbar.children[1]);
  765.  
  766.  
  767.  
  768. downloadElem = document.createElement("div");
  769. downloadElem.classList.add("foldMenu");
  770. downloadElem.setAttribute("tabindex", "-1"); // Don't mess with tab key
  771. document.body.appendChild(downloadElem);
  772. }
  773.  
  774. /* =========================================
  775. END BOOK SECTION!
  776. =========================================
  777. */
  778.  
  779. /* =========================================
  780. BEGIN INITIALIZER SECTION!
  781. =========================================
  782. */
  783.  
  784.  
  785. // The "BIF" contains all the info we need to download
  786. // stuff, so we wait until the page is loaded, and the
  787. // BIF is present, to inject the pirate menu.
  788. let intr = setInterval(()=>{
  789. if (window.BIF != undefined && document.querySelector(".nav-progress-bar") != undefined){
  790. clearInterval(intr);
  791. let mode = location.hostname.split(".")[1];
  792. if (mode == "listen"){
  793. bifFoundAudiobook();
  794. }else if (mode == "read"){
  795. bifFoundBook();
  796. }
  797. }
  798. }, 25);
  799. })();