Kemono 下載工具

一鍵下載圖片 (壓縮下載/單圖下載) , 頁面數據創建 json 下載 , 一鍵開啟當前所有帖子

Version au 17/08/2023. Voir la dernière version.

  1. // ==UserScript==
  2. // @name Kemono 下載工具
  3. // @name:zh-TW Kemono 下載工具
  4. // @name:zh-CN Kemono 下载工具
  5. // @name:ja Kemono ダウンロードツール
  6. // @name:en Kemono DownloadTool
  7. // @version 0.0.9
  8. // @author HentiSaru
  9. // @description 一鍵下載圖片 (壓縮下載/單圖下載) , 頁面數據創建 json 下載 , 一鍵開啟當前所有帖子
  10. // @description:zh-TW 一鍵下載圖片 (壓縮下載/單圖下載) , 頁面數據創建 json 下載 , 一鍵開啟當前所有帖子
  11. // @description:zh-CN 一键下载图片 (压缩下载/单图下载) , 页面数据创建 json 下载 , 一键开启当前所有帖子
  12. // @description:ja 画像をワンクリックでダウンロード(圧縮ダウンロード/単一画像ダウンロード)、ページデータを作成してjsonでダウンロード、現在のすべての投稿をワンクリックで開く
  13. // @description:en One-click download of images (compressed download/single image download), create page data for json download, one-click open all current posts
  14.  
  15. // @match *://kemono.su/*
  16. // @match *://*.kemono.su/*
  17. // @match *://kemono.party/*
  18. // @match *://*.kemono.party/*
  19. // @icon https://cdn-icons-png.flaticon.com/512/2381/2381981.png
  20.  
  21. // @license MIT
  22. // @namespace https://greasyfork.org/users/989635
  23.  
  24. // @run-at document-end
  25. // @grant GM_addStyle
  26. // @grant GM_setValue
  27. // @grant GM_getValue
  28. // @grant GM_download
  29. // @grant GM_addElement
  30. // @grant GM_notification
  31. // @grant GM_xmlhttpRequest
  32. // @grant GM_registerMenuCommand
  33.  
  34. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
  35. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
  36. // ==/UserScript==
  37.  
  38. const regex = /^https:\/\/[^/]+/, pattern = /^(https?:\/\/)?(www\.)?kemono\..+\/.+\/user\/.+\/post\/.+$/, language = display_language(navigator.language);
  39. var CompressMode = GM_getValue("壓縮下載", []),
  40. parser = new DOMParser(),
  41. url = window.location.href.match(regex),
  42. dict = {},
  43. Pages=0,
  44. OriginalTitle = document.title,
  45. ModeDisplay;
  46. let observer = new MutationObserver((mutationsList, observer) => {
  47. for (const mutation of mutationsList) {
  48. if (mutation.type === "childList") {
  49. if (pattern.test(window.location.href) && !document.querySelector("#DBExist")) {ButtonCreation()}
  50. }
  51. }
  52. });
  53. (function() {
  54. if (pattern.test(window.location.href)) {
  55. observer.observe(document.body, {childList: true, subtree: true});
  56. }
  57. GM_registerMenuCommand(language[0], function() {DownloadModeSwitch()}, "C")
  58. GM_registerMenuCommand(language[1], function() {
  59. const section = document.querySelector("section");
  60. if (section) {
  61. GetPageData(section);
  62. }
  63. }, "J")
  64. GM_registerMenuCommand(language[2], function() {OpenData()}, "O")
  65. })();
  66.  
  67. async function DownloadModeSwitch() {
  68. if (GM_getValue("壓縮下載", [])){
  69. GM_setValue("壓縮下載", false);
  70. GM_notification({
  71. title: language[3],
  72. text: language[6],
  73. timeout: 1500
  74. });
  75. } else {
  76. GM_setValue("壓縮下載", true);
  77. GM_notification({
  78. title: language[3],
  79. text: language[4],
  80. timeout: 1500
  81. });
  82. }
  83. location.reload();
  84. }
  85.  
  86. async function ButtonCreation() {
  87. GM_addStyle(`
  88. .File_Span {
  89. padding: 1rem;
  90. font-size: 20% !important;
  91. }
  92. .Download_Button {
  93. color: hsl(0, 0%, 45%);
  94. padding: 6px;
  95. border-radius: 8px;
  96. border: 2px solid rgba(59, 62, 68, 0.7);
  97. background-color: rgba(29, 31, 32, 0.8);
  98. font-family: Arial, sans-serif;
  99. }
  100. .Download_Button:hover {
  101. color: hsl(0, 0%, 95%);
  102. background-color: hsl(0, 0%, 45%);
  103. font-family: Arial, sans-serif;
  104. }
  105. .Download_Button:disabled {
  106. color: hsl(0, 0%, 95%);
  107. background-color: hsl(0, 0%, 45%);
  108. cursor: default;
  109. }
  110. `);
  111. let download_button;
  112. try {
  113. const Files = document.querySelectorAll("div.post__body h2")
  114. const spanElement = GM_addElement(Files[Files.length - 1], "span", {class: "File_Span"});
  115. download_button = GM_addElement(spanElement, "button", {
  116. class: "Download_Button",
  117. id: "DBExist"
  118. });
  119. if (CompressMode) {
  120. ModeDisplay = language[5];
  121. } else {
  122. ModeDisplay = language[7];
  123. }
  124. download_button.textContent = ModeDisplay;
  125. download_button.addEventListener("click", function() {
  126. DownloadTrigger(download_button);
  127. });
  128. } catch {
  129. download_button.textContent = language[9];
  130. download_button.disabled = true;
  131. }
  132. }
  133.  
  134. function IllegalFilter(Name) {
  135. return Name.replace(/[\/\?<>\\:\*\|":]/g, '');
  136. }
  137. function Conversion(Name) {
  138. return Name.replace(/[\[\]]/g, '');
  139. }
  140. function GetExtension(link) {
  141. const match = link.match(/\.([^.]+)$/);
  142. if (match) {return match[1].toLowerCase()}
  143. return "png";
  144. }
  145.  
  146. function DownloadTrigger(button) {
  147. let data = new Map(), link;
  148. let interval = setInterval(function() {
  149. let imgdata = document.querySelectorAll("div.post__files a");
  150. let title = document.querySelector("h1.post__title").textContent.trim();
  151. let user = document.querySelector("a.post__user-name").textContent.trim();
  152. if (imgdata && title && user) {
  153. imgdata.forEach((files, index) => {
  154. link = files.href || files.querySelector("img").src;
  155. data.set(index, link.split("?f=")[0]);
  156. });
  157. if (CompressMode) {
  158. ZipDownload(`[${user}] ${title}`, data, button);
  159. } else {
  160. ImageDownload(`[${user}] ${title}`, data, button)
  161. }
  162. button.textContent = language[8];
  163. button.disabled = true;
  164. clearInterval(interval);
  165. }
  166. }, 500);
  167. }
  168.  
  169. async function ZipDownload(Folder, ImgData, Button) {
  170. const zip = new JSZip(),
  171. File = Conversion(Folder),
  172. Total = ImgData.size,
  173. name = IllegalFilter(Folder.split(" ")[1]);
  174. let pool = [], poolSize = 5, progress = 1, mantissa, link, extension, BackgroundWork;
  175. function Request(index, retry) {
  176. link = ImgData.get(index);
  177. extension = GetExtension(link);
  178. return new Promise((resolve) => {
  179. GM_xmlhttpRequest({
  180. method: "GET",
  181. url: link,
  182. responseType: "blob",
  183. headers : {"user-agent": navigator.userAgent},
  184. onload: response => {
  185. if (response.status === 200 && response.response instanceof Blob && response.response.size > 0) {
  186. mantissa = (index + 1).toString().padStart(3, '0');
  187. zip.file(`${File}/${name}_${mantissa}.${extension}`, response.response);
  188. document.title = `[${progress}/${Total}]`;
  189. Button.textContent = `${language[10]} [${progress}/${Total}]`;
  190. progress++;
  191. resolve();
  192. } else {
  193. if (retry > 0) {
  194. Request(index, retry-1);
  195. console.log(`[${retry}] : ${link}`);
  196. } else {
  197. console.log(`[error] : ${link}`);
  198. resolve();
  199. }
  200. }
  201. },
  202. onerror: error => {
  203. if (retry > 0) {
  204. Request(index, retry-1);
  205. console.log(`[${retry}] : ${link}`);
  206. } else {
  207. console.log(`[error] : ${link}`);
  208. resolve();
  209. }
  210. }
  211. });
  212. });
  213. }
  214. for (let i = 0; i < Total; i++) {
  215. let promise = Request(i, 10);
  216. pool.push(promise);
  217. if (pool.length >= poolSize) {
  218. await Promise.allSettled(pool);
  219. pool = [];
  220. }
  221. }
  222. if (pool.length > 0) {await Promise.allSettled(pool)}
  223. Compression();
  224. async function Compression() {
  225. if (typeof(Worker) !== "undefined" && typeof(BackgroundWork) === "undefined") {
  226. BackgroundWork = new Worker(BackgroundCreation());
  227. BackgroundWork.postMessage([
  228. await zip.generateAsync({ // 不await 無法序列化, 不太會 Worker
  229. type: "blob",
  230. compression: "DEFLATE",
  231. compressionOptions: {
  232. level: 5 // 壓縮級別,範圍從 0(無壓縮)到 9(最大壓縮)
  233. }
  234. }, (progress) => {
  235. document.title = `${progress.percent.toFixed(1)} %`;
  236. Button.textContent = `${language[11]}: ${progress.percent.toFixed(1)} %`;
  237. }).then(zip => {
  238. saveAs(zip, `${Folder}.zip`);
  239. Button.textContent = language[13];
  240. document.title = OriginalTitle;
  241. setTimeout(() => {Button.textContent = ModeDisplay}, 4000);
  242. Button.disabled = false;
  243. }).catch(result => {
  244. Button.textContent = language[12];
  245. document.title = OriginalTitle;
  246. setTimeout(() => {Button.textContent = ModeDisplay}, 6000);
  247. Button.disabled = false;
  248. })
  249. ]);
  250. }
  251. }
  252. }
  253.  
  254. async function ImageDownload(Folder, ImgData, Button) {
  255. const name = IllegalFilter(Folder.split(" ")[1]), Total = ImgData.size;
  256. let progress = 1, link, extension;
  257. for (let i = 0; i < Total; i++) {
  258. link = ImgData.get(i);
  259. extension = GetExtension(link);
  260. GM_download({
  261. url: link,
  262. name: `${name}_${(progress+i).toString().padStart(3, '0')}.${extension}`,
  263. onload: () => {
  264. document.title = `[${progress}/${Total}]`;
  265. Button.textContent = `${language[10]} [${progress}/${Total}]`;
  266. progress++;
  267. },
  268. onerror: () => {
  269. i--;
  270. }
  271. });
  272. }
  273. Button.textContent = language[13];
  274. setTimeout(() => {Button.textContent = ModeDisplay}, 4000);
  275. document.title = OriginalTitle;
  276. Button.disabled = false;
  277. }
  278.  
  279. async function GetPageData(section) {
  280. const menu = section.querySelector("a.pagination-button-after-current");
  281. const item = section.querySelectorAll(".card-list__items article");
  282. let title, link;
  283. item.forEach(card => {
  284. title = card.querySelector(".post-card__header").textContent.trim()
  285. link = card.querySelector("a").href
  286. dict[`${link}`] = title;
  287. })
  288. try { // 當沒有下一頁連結就會發生例外
  289. let NextPage = menu.href;
  290. if (NextPage) {
  291. Pages++;
  292. GM_notification({
  293. title: language[14],
  294. text: `${language[15]} : ${Pages}`,
  295. image: "https://cdn-icons-png.flaticon.com/512/2582/2582087.png",
  296. timeout: 800
  297. });
  298. GM_xmlhttpRequest({
  299. method: "GET",
  300. url: NextPage,
  301. nocache: false,
  302. ontimeout: 8000,
  303. onload: response => {
  304. const DOM = parser.parseFromString(response.responseText, "text/html");
  305. GetPageData(DOM.querySelector("section"));
  306. }
  307. });
  308. }
  309. } catch {
  310. try {
  311. // 進行簡單排序
  312. Object.keys(dict).sort();
  313. const author = document.querySelector('span[itemprop="name"]').textContent;
  314. const json = document.createElement("a");
  315. json.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(dict, null, 4));
  316. json.download = `${author}.json`;
  317. json.click();
  318. json.remove();
  319. GM_notification({
  320. title: language[16],
  321. text: language[17],
  322. image: "https://cdn-icons-png.flaticon.com/512/2582/2582087.png",
  323. timeout: 2000
  324. });
  325. } catch {
  326. alert(language[18]);
  327. }
  328. }
  329. }
  330.  
  331. async function OpenData() {
  332. try {
  333. let content = document.querySelector('.card-list__items').querySelectorAll('article.post-card');
  334. content.forEach(function(content) {
  335. let link = content.querySelector('a').getAttribute('href');
  336. setTimeout(() => {
  337. window.open("https://kemono.party" + link , "_blank");
  338. }, 300);
  339. });
  340. } catch {
  341. alert(language[19]);
  342. }
  343. }
  344.  
  345. function display_language(language) {
  346. let display = {
  347. "zh-TW": [
  348. "🔁 切換下載模式",
  349. "📑 獲取所有帖子 Json 數據",
  350. "📃 開啟當前頁面所有帖子",
  351. "模式切換",
  352. "壓縮下載模式",
  353. "壓縮下載",
  354. "單圖下載模式",
  355. "單圖下載",
  356. "開始下載",
  357. "無法下載",
  358. "下載進度",
  359. "封裝進度",
  360. "壓縮封裝失敗",
  361. "下載完成",
  362. "數據處理中",
  363. "當前處理頁數",
  364. "數據處理完成",
  365. "Json 數據下載",
  366. "錯誤的請求頁面",
  367. "錯誤的開啟頁面"
  368. ],
  369. "zh-CN": [
  370. "🔁 切换下载模式",
  371. "📑 获取所有帖子 Json 数据",
  372. "📃 打开当前页面所有帖子",
  373. "模式切换",
  374. "压缩下载模式",
  375. "压缩下载",
  376. "单图下载模式",
  377. "单图下载",
  378. "开始下载",
  379. "无法下载",
  380. "下载进度",
  381. "封装进度",
  382. "压缩封装失败",
  383. "下载完成",
  384. "数据处理中",
  385. "当前处理页数",
  386. "数据处理完成",
  387. "Json 数据下载",
  388. "错误的请求页面",
  389. "错误的打开页面"
  390. ],
  391. "ja": [
  392. '🔁 ダウンロードモードの切り替え',
  393. '📑 すべての投稿のJsonデータを取得する',
  394. '📃 現在のページのすべての投稿を開く',
  395. 'モード切り替え',
  396. '圧縮ダウンロードモード',
  397. '圧縮ダウンロード',
  398. 'シングル画像ダウンロードモード',
  399. 'シングル画像ダウンロード',
  400. 'ダウンロードを開始する',
  401. 'ダウンロードできません',
  402. 'ダウンロードの進行状況',
  403. 'パッケージング中',
  404. '圧縮パッケージングに失敗しました',
  405. 'ダウンロードが完了しました',
  406. 'データ処理中',
  407. '現在の処理ページ数',
  408. 'データ処理が完了しました',
  409. 'Jsonデータのダウンロード',
  410. '間違ったリクエストページ',
  411. '間違ったページを開く'
  412. ],
  413. "en": [
  414. '🔁 Switch download mode',
  415. '📑 Get all post Json data',
  416. '📃 Open all posts on the current page',
  417. 'Mode switch',
  418. 'Compressed download mode',
  419. 'Compressed download',
  420. 'Single image download mode',
  421. 'Single image download',
  422. 'Start downloading',
  423. 'Unable to download',
  424. 'Download progress',
  425. 'Packaging',
  426. 'Compression packaging failed',
  427. 'Download completed',
  428. 'Data processing',
  429. 'Current processing page number',
  430. 'Data processing completed',
  431. 'Json data download',
  432. 'Wrong request page',
  433. 'Wrong page to open'
  434. ]
  435. };
  436. return display[language] || display["en"];
  437. }
  438.  
  439. function BackgroundCreation() {
  440. let blob = new Blob([""], {type: "application/javascript"});
  441. return URL.createObjectURL(blob);
  442. }