写真的保存

不知道说什么...

  1. // ==UserScript==
  2. // @name 写真的保存
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2
  5. // @description 不知道说什么...
  6. // @license MIT
  7. // @author kOda
  8. // @match https://everia.club/*
  9. // @match https://everiaeveria.b-cdn.net/*
  10. // @match https://www.everiaclub.com/*
  11. // @require https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=everia.club
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM_addElement
  15. // @grant GM_addStyle
  16. // ==/UserScript==
  17.  
  18. // 将请求到的子页面转化成DOM对象,在DOM对象上进行操作。
  19.  
  20. 'use strict';
  21.  
  22. // 时间单位 毫秒
  23. // 延时尽量别整太低,限制访问就不好了
  24. const CONFIG = {
  25. // 单图片保存后延时
  26. photoWait: 300,
  27. // 单写真页保存后延时
  28. pageWait: 3000,
  29. // 写真页的文件夹名是否简化
  30. cleanDirName: true,
  31. // 简单图片文件名
  32. simplePhotoName: false,
  33. };
  34.  
  35. $(unsafeWindow.document).ready(function() {
  36. CONFIG.iseveriaclub = unsafeWindow.location.host == "www.everiaclub.com";
  37.  
  38. var downBtn = document.createElement("button");
  39. var multiSaver = new MultiPhotoSaver(unsafeWindow.document);
  40.  
  41. downBtn.style = ` display: block;
  42. position: fixed;
  43. left: 10%;
  44. bottom: 15%;
  45. text-align: center;
  46. width: 10%;
  47. height: 4%; `;
  48. multiSaver.document.body.appendChild(downBtn);
  49.  
  50. // 检测当前页面为写真页或主页。
  51. if (CONFIG.isSenntakuPeeji) {
  52. downBtn.innerText = "下载本页全部写真";
  53. downBtn.addEventListener("click", () => {
  54. multiSaver.downStart().then(() => {
  55. console.log("本页已下载完毕!(●ˇ∀ˇ●)");
  56. downBtn.innerText = "已全部下载完成"
  57. })
  58. downBtn.disabled = true;
  59. })
  60. } else {
  61. downBtn.innerText = "下载至本地"
  62. downBtn.addEventListener("click", (e) => {
  63. unsafeWindow.showDirectoryPicker({mode: "readwrite"}).then((value) => {
  64. // 油猴处于沙盒时(无// @grant none)使用unsafeWindow访问网页
  65. new PhotoSaver(unsafeWindow.document, value).pageSave().then(() => {
  66. console.log(unsafeWindow.document.title + " 下载成功。");
  67. downBtn.innerText = "已全部下载完成";
  68. downBtn.disabled = true;
  69. })
  70. })
  71. })
  72. }
  73. })
  74.  
  75. // 用于保存单页写真
  76. class PhotoSaver {
  77. constructor(newDoc, relativeDir) {
  78. this.window = unsafeWindow;
  79. this.document = newDoc;
  80. this.topDir = relativeDir;
  81. this.index = 0;
  82. this.srcAttr = "";
  83. this.title = "";
  84. this.retryList = [];
  85. this.imgs = [];
  86.  
  87. this.init();
  88. }
  89.  
  90. init() {
  91. // https://everia.club/*
  92. // https://everiaeveria.b-cdn.net/*
  93. // 这两个网站写真页的有多个不同的网页结构,大概是更新了却没有把旧的去除。
  94. let selectors = ["body > div:nth-child(5) > div.mainleft > img", "#content > article > div.entry-content.clr > figure > figure > img", "#content > article > div.entry-content.clr > div > a > img"]
  95.  
  96. // 标题,用于文件夹名.
  97. this.title = this.document.title;
  98. this.srcAttr = CONFIG.iseveriaclub ? "data-original" : "data-src";
  99.  
  100. if (CONFIG.cleanDirName) {
  101. this.title = CONFIG.iseveriaclub ? this.title.split(", ").pop().split("-Everia club")[0] :
  102. this.title.split(", ").pop().split(" \u2013 EVERIA.CLUB")[0]
  103. }
  104.  
  105. for (let i = 0; i < selectors.length; i++) {
  106. this.imgs = this.document.querySelectorAll(selectors[i]);
  107. if (this.imgs.length) {
  108. if (!CONFIG.iseveriaclub) {
  109. // 2023 10月左右,部分页面除src外没有其它属性存储url。
  110. this.imgs[0].getAttribute(this.srcAttr) ? 0 :
  111. this.srcAttr = "src";
  112. }
  113. break;
  114. }
  115. }
  116. }
  117.  
  118. isExist() {
  119. return new Promise(function(exist, noexist) {
  120. this.topDir.getDirectoryHandle(this.title).then(() => noexist(this.title)).catch(() => exist(this));
  121. }.bind(this))
  122. }
  123.  
  124. getFileName(response, base) {
  125. // 这里用png只是随便选的,没什么深意。
  126. var temp = `${base}.png`;
  127.  
  128. if (!CONFIG.simplePhotoName) {
  129. // 这个网站的文件名妹法从url上获取,但在返回头有着原文件名。
  130. if (response.finalUrl.includes("blogger.googleusercontent.com")) temp = response.responseHeaders.split("filename=\"").pop().split("\"")[0];
  131. else temp = decodeURIComponent(decodeURIComponent(response.finalUrl.split("/").pop()));
  132. }
  133.  
  134. return temp;
  135. }
  136.  
  137. pageSave() {
  138. return new Promise(function(saveSuccess, saveFail) {
  139. this.topDir.getDirectoryHandle(this.title, {create: true})
  140. .then(function(saveDir) {
  141. var timer = 0;
  142. var currentList = [];
  143. this.retryList.push(saveDir);
  144.  
  145. var temp2 = function() {
  146. if (this.index == this.imgs.length) {
  147. clearTimeout(timer);
  148.  
  149. this.retryList.push(currentList);
  150. this.saveRetry(saveSuccess, saveFail);
  151.  
  152. return;
  153. }
  154.  
  155. var url = this.imgs[this.index].getAttribute(this.srcAttr)
  156. this.photoSave(url, this.index, saveDir).then(() => {
  157. timer = setTimeout(temp2, CONFIG.photoWait)
  158. }).catch(function(e) {
  159. console.error(e)
  160.  
  161. currentList.push([url, this.index])
  162. timer = setTimeout(temp2, 1)
  163. }.bind(this)).finally(() => this.index++)
  164. }.bind(this)
  165.  
  166. temp2();
  167. }.bind(this), (e) => saveFail(e));
  168. }.bind(this))
  169. }
  170.  
  171. saveRetry(retrySuccess, retryFail) {
  172. // 目前只处理因网络波动而造成下载失败的图片
  173. if (this.retryList[1].length) {
  174. var tryCount = 0;
  175. var index = 0;
  176. var timer = 0;
  177.  
  178. console.log(`开始重新下载:`)
  179. console.log(this.retryList);
  180.  
  181. var temp3 = function () {
  182. if (index == this.retryList[1].length) {
  183. console.log(`重下完成,大成功!`)
  184. clearTimeout(timer);
  185. retrySuccess();
  186. return
  187. }
  188.  
  189. var args = this.retryList[1][index];
  190. this.photoSave(args[0], args[1], this.retryList[0]).then(() => {
  191. tryCount = 0;
  192. console.log(`${args[0]} 重下成功!`)
  193. timer = setTimeout(temp3, CONFIG.photoWait)
  194. }).catch((error) => {
  195. if (tryCount++ > 2) {
  196. tryCount = 0;
  197. console.log(`${args[0]} \n 编号为:${args[1]} \n重下失败.... 建议自行下载。`)
  198. timer = setTimeout(temp3, 1);
  199. }
  200.  
  201. index--;
  202. timer = setTimeout(temp3, CONFIG.photoWait);
  203. }).finally(() => index++)
  204. }.bind(this)
  205.  
  206. temp3();
  207. } else {
  208. retrySuccess();
  209. }
  210. }
  211.  
  212. photoSave(url, index, saveDir) {
  213. return new Promise(function(resolve, reject) {
  214. GM_xmlhttpRequest({
  215. method: "GET",
  216. url: url,
  217. responseType: "blob",
  218. timeout: 5000,
  219. ontimeout: (e) => reject(new Error("Timeout!")),
  220. onload: function(response) {
  221. var name = this.getFileName(response, String(index))
  222.  
  223. saveDir.getFileHandle(name, {create: true})
  224. .then((value) => {
  225. return value.createWritable()})
  226. .then((writer) => {
  227. console.log(`保存图片:${name}`)
  228. writer.write({
  229. type: "write",
  230. data: response.response
  231. })
  232. writer.close().then(function() {
  233. resolve()
  234. });
  235. }).catch((e) => reject(e))
  236. }.bind(this)
  237. });
  238. }.bind(this))
  239. }
  240. }
  241.  
  242. class MultiPhotoSaver {
  243. constructor(newDoc) {
  244. this.document = newDoc;
  245. this.existedDirs = [];
  246. this.photoSelector = "";
  247. this.photoLinks = [];
  248.  
  249. this.init();
  250. }
  251.  
  252. // initialization
  253. init() {
  254. // 指向写真页的a标签的选择器
  255. // https://www.everiaclub.com: .leftp a(search、tags)
  256. // https://everia.club/ .search-entry-inner .thumbnail-link(search) .blog-entry-inner div > a(tags)
  257. // https://everiaeveria.b-cdn.net
  258.  
  259. // 就两三个选择器,懒得再去做什么判断了
  260. if (!CONFIG.iseveriaclub) {
  261. let t1 = this.document.querySelectorAll(".search-entry-inner .thumbnail-link");
  262. let t2 = this.document.querySelectorAll(".blog-entry-inner div > a");
  263.  
  264. t1.length ? this.photoLinks = t1 : this.photoLinks = t2;
  265. } else {
  266. this.photoLinks = this.document.querySelectorAll(".leftp > a");
  267. }
  268.  
  269. this.photoLinks.length ? CONFIG.isSenntakuPeeji = true : 0;
  270. }
  271.  
  272. downStart() {
  273. return new Promise(function(resolve, reject) {
  274. unsafeWindow.showDirectoryPicker({mode: "readwrite"}).then(function(picker) {
  275. var index = 0;
  276. var retryCount = 0;
  277. var timer = 0;
  278.  
  279. var saverHand = function (photoDoc) {
  280. new PhotoSaver(photoDoc, picker).isExist().then((saver) => {saver.pageSave()
  281. .then(() => {
  282. console.log(photoDoc.title + " 下载成功。");
  283. timer = setTimeout(temp1, CONFIG.pageWait);
  284. }, (error) => {
  285. console.log(photoDoc.title + " 下载失败了");
  286. console.error(error)
  287. })
  288. }, function(fileName) {
  289. console.log(`${fileName} 已存在`)
  290. this.existedDirs.push(fileName);
  291. timer = setTimeout(temp1, 1);
  292. }.bind(this));
  293. }.bind(this)
  294.  
  295. // 为避免ip封禁
  296. // 使用该函数进行请求延时处理。
  297. var temp1 = function () {
  298. if (index == this.photoLinks.length) {
  299. if (this.existedDirs.length) {
  300. console.log("以下写真已存在,如需下载,请先删除同名文件夹...")
  301. console.log(this.existedDirs)
  302. }
  303.  
  304. resolve();
  305. clearTimeout(timer);
  306. return;
  307. }
  308.  
  309. GM_xmlhttpRequest({
  310. method: "GET",
  311. url: this.photoLinks[index++].href,
  312. timeout: 5000,
  313. ontimeout: function(error) {
  314. if (retryCount++ > 2) {
  315. console.log("重试失败...")
  316. console.log("IP可能被限制")
  317. console.log("请您在一段时间重试。")
  318. }
  319. console.log("请求文件超时,自动重试...");
  320. temp1();
  321. },
  322. onload: function(response) {
  323. retryCount = 0;
  324. saverHand(new DOMParser().parseFromString(response.response, "text/html"));
  325. }
  326. });
  327. }.bind(this)
  328.  
  329. temp1();
  330. }.bind(this));
  331. }.bind(this))
  332. }
  333. }