DLsite Play Downloader

在浏览器完成DLsite Play漫画的下载、拼图和保存

  1. // ==UserScript==
  2. // @name DLsite Play Downloader
  3. // @namespace https://github.com/cpuopt/DLsite-Play-Downloader
  4. // @version 1.7.1
  5. // @description 在浏览器完成DLsite Play漫画的下载、拼图和保存
  6. // @author cpufan
  7. // @match https://play.dlsite.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=dlsite.com
  9. // @license MIT
  10. // @grant window.onurlchange
  11. // @grant GM_addStyle
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_deleteValue
  15. // @grant window.close
  16. // @grant GM_addValueChangeListener
  17. // @grant GM_removeValueChangeListener
  18. // @supportURL https://github.com/cpuopt/DLsite-Play-Downloader/issues
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. "use strict";
  23.  
  24. GM_addStyle(`
  25. .button-down{
  26. border: none;
  27. background-color: #007aff;
  28. color: white;
  29. padding-inline: 0.6rem;
  30. position: absolute;
  31. right: 0;
  32. height: 100%;
  33. z-index: 2;
  34. font-weight: bolder;
  35. transition: background-color .5s;
  36. }
  37. .button-down:hover{
  38. background-color: #000000;
  39. }
  40. .jpeg-button-down{
  41. margin-left: auto;
  42. border: none;
  43. background-color: #007aff;
  44. color: white;
  45. padding-inline: 0.6rem;
  46. z-index: 2;
  47. font-weight: bolder;
  48. transition: background-color .5s;
  49. }
  50. .jpeg-button-down:hover{
  51. background-color: #000000;
  52. }
  53. `);
  54. var mutationob;
  55. var pluginPanel;
  56. var dlsiteMangaDownloader;
  57. class MutationOb {
  58. observer;
  59.  
  60. constructor() {
  61. let self = this;
  62. this.observer = new MutationObserver(() => {
  63. let artwork = document.querySelector("ol[class^='_tree_'] > li[class^='_item_']:has(svg)");
  64. let jpegs = document.querySelectorAll("ol[class^='_tree_'] > li[class^='_item_']:has(img)");
  65. console.debug("触发监视器", artwork, jpegs);
  66. if (artwork != null && artwork.querySelector("button") == null) {
  67. self.haveArtwork(artwork);
  68. }
  69. if (jpegs.length > 0 && document.querySelector("div[class^='_worktree_'] ul").querySelector("button") == null) {
  70. self.haveJpegs(jpegs);
  71. }
  72. });
  73. }
  74. start() {
  75. const illustsDivNode = document.querySelector("body");
  76. console.debug(illustsDivNode);
  77. this.observer.observe(illustsDivNode, {
  78. attributes: false,
  79. childList: true,
  80. subtree: true,
  81. });
  82. console.debug("监视器启动");
  83. }
  84. stop() {
  85. this.observer.disconnect();
  86. console.debug("监视器停止");
  87. }
  88. haveArtwork(artwork) {
  89. this.stop();
  90. let button = new ArtworkDownloadButton("button-down", artwork);
  91. if (pluginPanel == undefined) {
  92. mutationob.start();
  93. }
  94. }
  95. haveJpegs(jpegs) {
  96. this.stop();
  97. let button = new JpegsDownloadButton("jpeg-button-down", document.querySelector("div[class^='_worktree_'] ul"));
  98. if (pluginPanel == undefined) {
  99. mutationob.start();
  100. }
  101. }
  102. }
  103.  
  104. // fetch拦截器 用于截获xml文件url
  105. class FetchInterceptor {
  106. static originalFetch = unsafeWindow.fetch;
  107.  
  108. static intercept() {
  109. const o = unsafeWindow.fetch;
  110. unsafeWindow.fetch = (...args) => {
  111. return new Promise((resolve, reject) => {
  112. let [resource, config] = args;
  113. // request interceptor starts
  114. console.log(resource, config);
  115.  
  116. if (/https:\/\/play.dl.dlsite.com\/csr\/api\/diazepam_hybrid.php\?mode=7&file=face.xml&reqtype=0&vm=\d&param=.*&time=\d+/.test(resource)) {
  117. FetchInterceptor.stop();
  118. GM_setValue("URLStyle", resource);
  119. console.debug(`成功获取到图片链接格式`, resource);
  120. GM_setValue("download", false);
  121. window.close();
  122. } else {
  123. console.debug("图片链接格式不匹配");
  124. GM_setValue("download", false);
  125.  
  126. // window.location.reload();
  127. }
  128.  
  129. // request interceptor ends
  130.  
  131. o(...args).then((response) => {
  132. console.log(response);
  133.  
  134. resolve(response);
  135. });
  136.  
  137. // response interceptor here
  138. });
  139. };
  140. }
  141.  
  142. static stop() {
  143. unsafeWindow.fetch = this.originalFetch;
  144. }
  145. }
  146. class DLsiteMangaDownloader {
  147. preffix;
  148. suffix;
  149. pageNum;
  150. faceScramble;
  151. HorBlock;
  152. VerBlock;
  153. Width;
  154. Height;
  155. urls;
  156. filename;
  157. outputBlobs = new Array();
  158.  
  159. constructor(URLStyle, filename) {
  160. this.filename = filename;
  161. // 解析url前缀和后缀
  162. let urlExample = URLStyle;
  163. let modeIndex = urlExample.search(/\?mode/);
  164. let reqtypeIndex = urlExample.search(/&reqtype/);
  165. this.preffix = urlExample.substring(0, modeIndex);
  166. this.suffix = urlExample.substring(reqtypeIndex);
  167. pluginPanel.addLog("解析链接完成");
  168. }
  169.  
  170. buildUrls(page) {
  171. let urls = new Array();
  172.  
  173. for (let i = 0; i < page; i++) {
  174. let xml = `${this.preffix}?mode=8&file=${i.toString().padStart(4, "0")}.xml${this.suffix}`;
  175. let bin = `${this.preffix}?mode=1&file=${i.toString().padStart(4, "0")}_0000.bin${this.suffix}`;
  176. urls.push({ xml, bin });
  177. }
  178.  
  179. return urls;
  180. }
  181.  
  182. async getFaceInfo() {
  183. const faceResponse = await fetch(`${this.preffix}?mode=7&file=face.xml${this.suffix}`, {
  184. method: "GET",
  185. headers: {
  186. Accept: "*/*",
  187. "Accept-Encoding": "gzip, deflate, br",
  188. "Accept-Language": "zh-CN,zh;q=0.9",
  189. },
  190. referrer: "https://play.dlsite.com/",
  191. credentials: "same-origin",
  192. });
  193. let doc = DLsiteMangaDownloader.parseText2Xml(await faceResponse.text());
  194. this.HorBlock = parseInt(doc.evaluate("//Scramble/Width", doc).iterateNext().textContent);
  195. this.VerBlock = parseInt(doc.evaluate("//Scramble/Height", doc).iterateNext().textContent);
  196. this.Width = parseInt(doc.evaluate("//ContentFrame/Width", doc).iterateNext().textContent);
  197. this.Height = parseInt(doc.evaluate("//ContentFrame/Height", doc).iterateNext().textContent);
  198. this.pageNum = parseInt(doc.evaluate("//TotalPage", doc).iterateNext().textContent);
  199. }
  200.  
  201. // 解析xml响应为document
  202. static parseText2Xml(text) {
  203. const parser = new DOMParser();
  204. const doc = parser.parseFromString(text, "application/xml");
  205. return doc;
  206. }
  207.  
  208. async makeImages() {
  209. let self = this;
  210. await this.getFaceInfo();
  211. this.urls = this.buildUrls(this.pageNum);
  212.  
  213. this.urls.forEach(async ({ xml, bin }, index) => {
  214. console.debug(`${index}\n${xml}\n${bin}`);
  215. let xmlResponse = await fetch(xml, {
  216. method: "GET",
  217. headers: {
  218. Accept: "*/*",
  219. "Accept-Encoding": "gzip, deflate, br",
  220. "Accept-Language": "zh-CN,zh;q=0.9",
  221. },
  222. referrer: "https://play.dlsite.com/",
  223. credentials: "same-origin",
  224. });
  225.  
  226. let binResponse = await fetch(bin, {
  227. method: "GET",
  228. headers: {
  229. Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
  230. "Accept-Encoding": "gzip, deflate, br",
  231. "Accept-Language": "zh-CN,zh;q=0.9",
  232. "Sec-Fetch-Dest": "image",
  233. },
  234. referrer: "https://play.dlsite.com/",
  235. credentials: "same-origin",
  236. });
  237.  
  238. if (xmlResponse.ok && binResponse.ok) {
  239. let doc = DLsiteMangaDownloader.parseText2Xml(await xmlResponse.text());
  240. let mateix = doc.evaluate("//Scramble", doc).iterateNext().textContent.split(",");
  241. const width = parseInt(doc.evaluate("//StepRect/Width", doc).iterateNext().textContent);
  242. const height = parseInt(doc.evaluate("//StepRect/Height", doc).iterateNext().textContent);
  243. let vector = new Array(mateix.length);
  244. mateix.forEach((num, index) => {
  245. vector[parseInt(num)] = index;
  246. });
  247. console.debug(index, mateix, vector);
  248. let image = await binResponse.blob();
  249.  
  250. self.imagePuzzle({
  251. index: index,
  252. vector: vector,
  253. blob: image,
  254. TocTitle: null,
  255. size: { width: width, height: height },
  256. });
  257. }
  258. });
  259. }
  260. imagePuzzle({ index, vector, blob, TocTitle, size }) {
  261. let self = this;
  262. let canvas = document.createElement("canvas");
  263. canvas.width = size.width;
  264. canvas.height = size.height;
  265. let HorBlock = this.HorBlock;
  266. let VerBlock = this.VerBlock;
  267. let sourceW = Math.trunc(size.width / (this.HorBlock * 8)) * 8;
  268. let sourceH = Math.trunc(size.height / (this.VerBlock * 8)) * 8;
  269. let ctx = canvas.getContext("2d");
  270. const img = new Image();
  271. img.src = URL.createObjectURL(blob);
  272. img.onload = function () {
  273. ctx.drawImage(img, 0, 0);
  274.  
  275. for (let [index, item] of vector.entries()) {
  276. let sourceX = sourceW * (index % HorBlock);
  277. let sourceY = sourceH * Math.trunc(index / VerBlock);
  278. let x = sourceW * (item % HorBlock);
  279. let y = sourceH * Math.trunc(item / VerBlock);
  280.  
  281. ctx.drawImage(img, sourceX, sourceY, sourceW, sourceH, x, y, sourceW, sourceH);
  282. }
  283. canvas.toBlob(
  284. function (blob) {
  285. self.outputBlobs.push({
  286. index: index,
  287. blob: blob,
  288. TocTitle: null,
  289. });
  290. pluginPanel.addLog(`已处理完成:${self.outputBlobs.length}/${self.urls.length}`);
  291.  
  292. if (self.outputBlobs.length == self.urls.length) {
  293. // 所有图片已经处理完成
  294. self.save(self.filename);
  295. pluginPanel.addLog("<b>下载已完成</b>");
  296. pluginPanel.addLog("提示:<b>为最大限度保持画质,图片以PNG储存</b>");
  297. pluginPanel.addLog("提示:<b>返回或切换页面即可关闭此窗口</b>");
  298. mutationob.start();
  299. }
  300. }
  301. // document.querySelector("#imgType").value,
  302. // 1.0
  303. );
  304. };
  305.  
  306. // document.body.appendChild(canvas);
  307. }
  308. save(mangaName) {
  309. let self = this;
  310. const fileStream = streamSaver.createWriteStream(`${mangaName}.zip`);
  311.  
  312. const readableZipStream = new ZIP({
  313. start(ctrl) {
  314. self.outputBlobs.forEach(({ index, blob, TocTitle }, _) => {
  315. let file = {
  316. // name: `${mangaName}/${(index + 1).toString().padStart(4, "0")}.jpg`,
  317. name: `${(index + 1).toString().padStart(4, "0")}.png`,
  318. stream: () => blob.stream(),
  319. };
  320. ctrl.enqueue(file);
  321. });
  322. ctrl.close();
  323. },
  324. });
  325.  
  326. // more optimized
  327. if (window.WritableStream && readableZipStream.pipeTo) {
  328. return readableZipStream.pipeTo(fileStream).then(() => console.debug("done writing"));
  329. }
  330.  
  331. // less optimized
  332. const writer = fileStream.getWriter();
  333. const reader = readableZipStream.getReader();
  334. const pump = () => reader.read().then((res) => (res.done ? writer.close() : writer.write(res.value).then(pump)));
  335.  
  336. pump();
  337. }
  338. }
  339. class ArtworkDownloadButton {
  340. button;
  341. constructor(className, father) {
  342. let button = document.createElement("button");
  343. button.className = className;
  344. button.innerText = "使用脚本下载";
  345. let Title = father.querySelector("div[class^='_text_'] > p[class*='_titleMedium_']").innerText;
  346. let Author = document.querySelector("div[class^='_contentMain_'] > p[class^='_text_'][class*='_onSurface_']").innerText.replace("/", " ");
  347. let Maker = document.querySelector("div[class^='_contentMain_'] > p[class^='_text_'][class*='_onSurfacePrimary_'] > a[class^='_link_']").innerText.replace("/", " ");
  348. button.addEventListener("click", (e) => {
  349. // 显示插件面板
  350. pluginPanel = new PluginPanel();
  351. mutationob.stop();
  352.  
  353. GM_setValue("download", true);
  354. GM_setValue("filename", `[${Author}] ${Title}`);
  355. console.debug("filename:\n", `[${Author}] ${Title}`);
  356. pluginPanel.addLog(`获取到标题:<b>${Title}</b>`);
  357. pluginPanel.addLog(`获取到作者:<b>${Author}</b>`);
  358. pluginPanel.addLog(`获取到出版商:<b>${Maker}</b>`);
  359. e.stopPropagation(); // 阻止冒泡
  360. GM_deleteValue("URLStyle");
  361.  
  362. // 事件监听器获取URLStyle
  363. var URLStylelistener = GM_addValueChangeListener("URLStyle", function (key, oldValue, newValue, remote) {
  364. console.debug(key + ":\n" + oldValue + "=>" + newValue);
  365. pluginPanel.addLog(`获取到URL格式:` + newValue);
  366. dlsiteMangaDownloader = new DLsiteMangaDownloader(newValue, `[${Maker}] [${Author}] ${Title}`);
  367. GM_removeValueChangeListener(URLStylelistener);
  368. dlsiteMangaDownloader.makeImages();
  369. });
  370.  
  371. pluginPanel.addLog("准备前往阅读器获取URL...");
  372. // 延迟两秒开启阅读器界面
  373. setTimeout(() => {
  374. father.click();
  375. }, 2000);
  376. });
  377. father.appendChild(button);
  378. this.button = button;
  379. }
  380. }
  381. class JpegsDownloadButton {
  382. button;
  383. constructor(className, father) {
  384. let button = document.createElement("button");
  385. button.className = className;
  386. button.innerText = "使用脚本下载";
  387. let Title = document.querySelector("div[class^='_info_'] > div[class*='_contentMain_'] > :nth-child(1)").innerText;
  388. let Author = document.querySelector("div[class^='_info_'] > div[class*='_contentMain_'] > :nth-child(2)").innerText.replace("/", " ");
  389. let Maker = document.querySelector("div[class^='_info_'] > div[class*='_contentMain_'] > :nth-child(3)").innerText.replace("/", " ");
  390. button.addEventListener("click", async (e) => {
  391. // 显示插件面板
  392. pluginPanel = new PluginPanel();
  393. mutationob.stop();
  394.  
  395. console.debug("filename:\n", `[${Author}] ${Title}`);
  396. pluginPanel.addLog(`获取到标题:<b>${Title}</b>`);
  397. pluginPanel.addLog(`获取到作者:<b>${Author}</b>`);
  398. pluginPanel.addLog(`获取到出版商:<b>${Maker}</b>`);
  399. e.stopPropagation(); // 阻止冒泡
  400.  
  401. const { url: downloadPrefix, cookies } = await getDownloadCredential();
  402. const [mangaName, downloadUrls] = await Promise.all([getMangaName(), getDownloadUrls(downloadPrefix)]);
  403. const downloadResults = await Promise.all(downloadUrls.map((value) => imagePuzzle(downloadPrefix, value)));
  404.  
  405. save(mangaName, downloadResults);
  406. });
  407. father.appendChild(button);
  408. this.button = button;
  409. }
  410. }
  411. class PluginPanel {
  412. element;
  413. title;
  414. hr;
  415. log;
  416.  
  417. constructor() {
  418. GM_addStyle(`
  419. .plugin-panel{
  420. border: solid #007aff 2px;
  421. border-radius: 1rem;
  422. display: block;
  423. box-sizing: border-box;
  424. width: 50rem;
  425. height: 30rem;
  426. margin: 0 auto;
  427. position: fixed;
  428. background-color: white;
  429. z-index: 5000;
  430. margin-left: 50%;
  431. margin-top: 50%;
  432. top: -15rem;
  433. left: -25rem;
  434. padding-block: 1rem;
  435. font-size: 1.6rem;
  436. box-shadow: 5px 5px 10px #ccc;
  437. }
  438. .plugin-panel-title{
  439. margin: 0 auto;
  440. text-align: center;
  441. }
  442. .plugin-panel-hr{
  443. margin-block: 0.2rem;
  444. }
  445. .plugin-panel-log{
  446. margin-inline: 2rem;
  447. margin-block: 0.5rem;
  448. height: 10rem;
  449. border: solid #000000a6 1.5px;
  450. box-sizing: border-box;
  451. font-size: 1.2rem;
  452. padding: 0.5rem;
  453. border-radius: 0.5rem;
  454. overflow: auto;
  455. }
  456. ::-webkit-scrollbar {
  457. display: none;
  458. }
  459. `);
  460. let element = document.createElement("div");
  461. element.className = "plugin-panel";
  462. document.body.appendChild(element);
  463. let title = document.createElement("div");
  464. title.className = "plugin-panel-title";
  465. title.innerText = "DLsite Play Downloader";
  466. element.appendChild(title);
  467. let hr = document.createElement("hr");
  468. hr.className = "plugin-panel-hr";
  469. element.appendChild(hr);
  470. let log = document.createElement("div");
  471. log.className = "plugin-panel-log";
  472. element.appendChild(log);
  473.  
  474. this.log = log;
  475. this.element = element;
  476. }
  477. addLog(text) {
  478. let oldHTML = this.log.innerHTML;
  479. this.log.innerHTML = oldHTML + `<p>${text}<p>`;
  480. this.log.scrollTop = this.log.scrollHeight;
  481. }
  482. destroy() {
  483. const element = document.querySelector("div.plugin-panel");
  484. if (element) {
  485. document.body.removeChild(element);
  486. }
  487. }
  488. }
  489.  
  490. if (!window.location.href.startsWith("https://play.dlsite.com/csr/")) {
  491. // 非漫画阅读器页面 再启动监视器
  492. mutationob = new MutationOb();
  493. mutationob.start();
  494. GM_deleteValue("URLStyle");
  495. // 加载StreamSaver和zip-stream
  496. let scripts = ["https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.js", "https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js", "https://jimmywarting.github.io/StreamSaver.js/examples/zip-stream.js", "https://unpkg.com/mersenne-twister@1.1.0/src/mersenne-twister.js"];
  497. scripts.forEach((url) => {
  498. let script = document.createElement("script");
  499. script.setAttribute("type", "text/javascript");
  500. script.src = url;
  501. document.documentElement.appendChild(script);
  502. });
  503. if (window.onurlchange === null) {
  504. // feature is supported
  505. window.addEventListener("urlchange", () => {
  506. mutationob.stop();
  507. if (pluginPanel != undefined && pluginPanel.element != undefined) {
  508. pluginPanel.element.remove();
  509. pluginPanel.destroy();
  510. }
  511. setTimeout(() => {
  512. mutationob.start();
  513. }, 100);
  514. });
  515. }
  516. }
  517. if (window.location.href.startsWith("https://play.dlsite.com/csr/") && GM_getValue("download")) {
  518. // 当前位于漫画阅读器
  519. console.debug("当前位于漫画阅读器", "下载状态:", GM_getValue("download"));
  520.  
  521. // 加载StreamSaver和zip-stream
  522. let scripts = ["https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.js", "https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js", "https://jimmywarting.github.io/StreamSaver.js/examples/zip-stream.js"];
  523. scripts.forEach((url) => {
  524. let script = document.createElement("script");
  525. script.setAttribute("type", "text/javascript");
  526. script.src = url;
  527. document.documentElement.appendChild(script);
  528. });
  529. FetchInterceptor.intercept();
  530. }
  531. async function getDownloadCredential() {
  532. const response = await fetch(`https://play.dl.dlsite.com/api/download/sign/cookie?workno=${location.href.match(/\/work\/(\w+)\//)[1]}`, {
  533. method: "GET",
  534. headers: {
  535. Accept: "*/*",
  536. "Accept-Encoding": "gzip, deflate, br",
  537. "Accept-Language": "zh-CN,zh;q=0.9",
  538. },
  539. referrer: "https://play.dlsite.com/",
  540. credentials: "include",
  541. });
  542. return await response.json();
  543. }
  544.  
  545. async function getDownloadUrls(prefix) {
  546. const response = await fetch(`${prefix}ziptree.json`, {
  547. referrer: "https://play.dlsite.com/",
  548. credentials: "include",
  549. });
  550. const zipTree = await response.json();
  551.  
  552. const result = [];
  553.  
  554. const travel = (fileObj, index, path) => {
  555. if (fileObj.type === "folder") {
  556. fileObj.children.forEach((child, index) => travel(child, index, fileObj.path));
  557. }
  558. if (fileObj.type === "file" && !fileObj.hashname.endsWith(".pdf")) {
  559. result.push({
  560. filename: `${path ? `${path}/` : ""}${fileObj.name}`,
  561. optimized: zipTree.playfile[fileObj.hashname].image.optimized,
  562. });
  563. }
  564. };
  565. zipTree.tree.forEach(travel);
  566. // console.log(result);
  567. return result;
  568. }
  569.  
  570. function getDecryptedImageData(optimized) {
  571. const qv = (t, s) => {
  572. // const MersenneTwister = unsafeWindow.module.exports;
  573. // const MersenneTwister = window.module.exports;
  574. const n = new MersenneTwister(t);
  575. for (let r = s.length - 1; r > 0; r--) {
  576. const o = Math.floor(n.random() * (r + 1));
  577. [s[r], s[o]] = [s[o], s[r]];
  578. }
  579. return s;
  580. },
  581. Ir = (t, s) => (t >= s ? t % s : t),
  582. Lr = (t, s) => (t >= s ? Math.floor(t / s) : 0);
  583. const n = {
  584. w: Math.ceil(optimized.width / 128),
  585. h: Math.ceil(optimized.height / 128),
  586. },
  587. r = parseInt(optimized.name.substring(5, 12), 16),
  588. i = qv(r, [...Array(n.w * n.h).keys()]).map((value, index) => ({
  589. sx: 128 * Ir(index, n.w),
  590. sy: 128 * Lr(index, n.w),
  591. dx: 128 * Ir(value, n.w),
  592. dy: 128 * Lr(value, n.w),
  593. }));
  594. return { sourceCropSize: 128, cropCount: n, coordinates: i };
  595. }
  596.  
  597. async function imagePuzzle(downloadPrefix, { filename, optimized }) {
  598. let canvas = document.createElement("canvas");
  599. canvas.width = optimized.width;
  600. canvas.height = optimized.height;
  601. let ctx = canvas.getContext("2d");
  602. let binResponse = await fetch(`${downloadPrefix}optimized/${optimized.name}`, {
  603. method: "GET",
  604. headers: {
  605. Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
  606. "Accept-Encoding": "gzip, deflate, br",
  607. "Accept-Language": "zh-CN,zh;q=0.9",
  608. "Sec-Fetch-Dest": "image",
  609. },
  610. referrer: "https://play.dlsite.com/",
  611. credentials: "include",
  612. });
  613. let blob = await binResponse.blob();
  614. const img = new Image();
  615. img.src = URL.createObjectURL(blob);
  616. return new Promise((resolve) => {
  617. img.onload = function () {
  618. const { sourceCropSize: sourceCropSize, cropCount: cropCount, coordinates: coordinates } = getDecryptedImageData(optimized),
  619. // g = isSpread && m === 1 ? t[0].width : 0,
  620. g = 0,
  621. // y = t[isSpread && m === 0 ? 1 : 0].height,
  622. // w = isSpread && optimized.height < y ? Math.round((y - optimized.height) / 2) : 0,
  623. w = 0,
  624. x = {
  625. w: img.width - optimized.width,
  626. h: img.height - optimized.height,
  627. };
  628. for (const coordinate of coordinates) {
  629. const k = coordinate.dx + sourceCropSize === sourceCropSize * cropCount.w ? sourceCropSize - x.w : sourceCropSize,
  630. O = coordinate.dy + sourceCropSize === sourceCropSize * cropCount.h ? sourceCropSize - x.h : sourceCropSize;
  631. ctx.drawImage(img, coordinate.sx, coordinate.sy, k, O, coordinate.dx + g, coordinate.dy + w, k, O);
  632. }
  633.  
  634. canvas.toBlob(function (blob) {
  635. resolve({ filename, blob });
  636. });
  637. };
  638. });
  639. }
  640. function save(mangaName, blobs) {
  641. const fileStream = streamSaver.createWriteStream(`${mangaName}.zip`);
  642.  
  643. const readableZipStream = new ZIP({
  644. start(ctrl) {
  645. blobs.forEach(({ blob, filename }, arrayIndex) => {
  646. let file = {
  647. // name: `${mangaName}/${(index + 1).toString().padStart(4, "0")}.jpg`,
  648. // name: `${(index + 1).toString().padStart(4, "0")}.png`,
  649. name: `${filename.split(".")[0]}.png`,
  650. stream: () => blob.stream(),
  651. };
  652. ctrl.enqueue(file);
  653. });
  654. ctrl.close();
  655. },
  656. });
  657.  
  658. // more optimized
  659. if (window.WritableStream && readableZipStream.pipeTo) {
  660. return readableZipStream.pipeTo(fileStream).then(() => {
  661. console.debug("done writing");
  662. pluginPanel.addLog("<b>下载已完成</b>");
  663. });
  664. }
  665.  
  666. // less optimized
  667. const writer = fileStream.getWriter();
  668. const reader = readableZipStream.getReader();
  669. const pump = () => reader.read().then((res) => (res.done ? writer.close() : writer.write(res.value).then(pump)));
  670.  
  671. pump();
  672. }
  673.  
  674. async function getMangaName() {
  675. const response = await fetch(`https://play.dlsite.com/api/work/${location.href.match(/\/work\/(\w+)\//)[1]}`, {
  676. referrer: "https://play.dlsite.com/",
  677. credentials: "include",
  678. });
  679. const result = await response.json();
  680. return result.name["ja_JP"];
  681. }
  682. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址