Sleazy Fork is available in English.

Pixiv Downloader

一键下载Pixiv各页面原图。支持多图下载,动图下载,按作品标签下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webp | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。

2023-04-10 일자. 최신 버전을 확인하세요.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
  1. // ==UserScript==
  2. // @name Pixiv Downloader
  3. // @namespace https://greasyfork.org/zh-CN/scripts/432150
  4. // @version 0.7.3
  5. // @description:en Download the original images of Pixiv pages with one click. Supports:multiple illustrations, ugoira(animation), and batch downloads of artists' work. Ugoira support format conversion: Gif | Apng | Webp | Webm. The downloaded images will be saved in a separate folder named after the artist (you need to adjust the tampermonkey "Download" setting to "Browser API"). A record of downloaded images is kept.
  6. // @description 一键下载Pixiv各页面原图。支持多图下载,动图下载,按作品标签下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webp | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。
  7. // @description:zh-TW 一鍵下載Pixiv各頁面原圖。支持多圖下載,動圖下載,按作品標籤下載,畫師作品批次下載。動圖支持格式轉換:Gif | Apng | Webp | Webm。下載的圖片將保存到以畫師名命名的單獨文件夾(需要調整tampermonkey“下載”設置為“瀏覽器API”)。保留已下載圖片的紀錄。
  8. // @author ruaruarua
  9. // @match https://www.pixiv.net/*
  10. // @icon https://www.pixiv.net/favicon.ico
  11. // @noframes
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_download
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_info
  17. // @grant GM_registerMenuCommand
  18. // @connect i.pximg.net
  19. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js
  21. // @require https://greasyfork.org/scripts/455256-toanimatedwebp/code/toAnimatedWebp.js?version=1120088
  22. // ==/UserScript==
  23. (function () {
  24. 'use strict';
  25.  
  26. const style = `
  27. @property --pdl-progress {
  28. syntax: '<percentage>';
  29. inherits: true;
  30. initial-value: 0%;
  31. }
  32. @keyframes pdl_loading {
  33. 100% {
  34. transform: translate(-50%, -50%) rotate(360deg);
  35. }
  36. }
  37. [data-theme="dark"] .pdl-btn-all::before {
  38. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  39. }
  40. [data-theme="dark"] .pdl-btn-main,
  41. [data-theme="dark"] .pdl-btn-all:hover::before {
  42. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23D6D6D6' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  43. }
  44. [data-theme="dark"] .pdl-stop::before {
  45. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  46. }
  47. [data-theme="dark"] .pdl-stop:hover::before {
  48. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23D6D6D6' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  49. }
  50. [data-theme="dark"] .pdl-wrap input:not(:checked):hover {
  51. background-color: rgba(155, 155, 155);
  52. }
  53. [data-theme="dark"] .pdl-btn.pdl-tag{
  54. background-color: rgba(255, 255, 255, 0.4);
  55. }
  56. [data-theme="dark"] .pdl-btn.pdl-modal-tag {
  57. background-color: rgba(255, 255, 255, 0.4);
  58. }
  59. [data-theme="dark"] .pdl-btn.pdl-modal-tag:hover {
  60. background-color: rgba(255, 255, 255, 0.6);
  61. }
  62. [data-theme="dark"] .pdl-wrap:hover,
  63. [data-theme="dark"] .pdl-stop.pdl-stop:hover,
  64. [data-theme="dark"] .pdl-btn-all.pdl-btn-all:hover {
  65. color: rgb(214, 214, 214);
  66. }
  67. [data-theme="dark"] .pdl-dialog {
  68. background-color: rgb(31, 31, 31);
  69. }
  70. [data-theme="dark"] .pdl-dialog-footer button {
  71. background-color: rgb(245, 245, 245);
  72. }
  73. .pdl-btn {
  74. position: relative;
  75. border-top-right-radius: 8px;
  76. background: no-repeat center/85%;
  77. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%233C3C3C' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  78. color: #01b468;
  79. display: inline-block;
  80. font-size: 13px;
  81. font-weight: bold;
  82. height: 32px;
  83. line-height: 32px;
  84. margin: 0;
  85. overflow: hidden;
  86. padding: 0;
  87. border: none;
  88. text-decoration: none!important;
  89. text-align: center;
  90. text-overflow: ellipsis;
  91. user-select: none;
  92. white-space: nowrap;
  93. width: 32px;
  94. z-index: 1;
  95. cursor: pointer;
  96. }
  97. .pdl-btn-main {
  98. margin: 0 0 0 10px;
  99. }
  100. .pdl-btn-sub {
  101. bottom: 0;
  102. background-color: rgba(255, 255, 255, .5);
  103. left: 0;
  104. position: absolute;
  105. }
  106. .pdl-btn-sub.artworks{
  107. position: sticky;
  108. top: 40px;
  109. border-radius: 4px;
  110. }
  111. .pdl-btn-sub.presentation{
  112. position: fixed;
  113. top: 50px;
  114. right: 16px;
  115. border-radius: 8px;
  116. left: auto;
  117. }
  118. .pdl-btn-sub-bookmark.pdl-btn-sub-bookmark {
  119. left: auto;
  120. right: 0;
  121. bottom: 34px;
  122. border-radius: 8px;
  123. border-top-right-radius: 0px;
  124. border-bottom-right-radius: 0px;
  125. }
  126. .pdl-error.pdl-error {
  127. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23EA0000' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  128. }
  129. .pdl-complete.pdl-complete {
  130. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%2301B468' d='M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z'%3E%3C/path%3E %3C/svg%3E");
  131. }
  132. .pdl-progress.pdl-progress {
  133. background-image: none;
  134. cursor: default;
  135. }
  136. .pdl-progress:after{
  137. content: '';
  138. display: inline-block;
  139. position: absolute;
  140. top: 50%;
  141. left: 50%;
  142. width: 27px;
  143. height: 27px;
  144. transform: translate(-50%, -50%);
  145. -webkit-mask: radial-gradient(transparent, transparent 54%, #000 57%, #000);
  146. mask: radial-gradient(transparent, transparent 54%, #000 57%, #000);
  147. border-radius: 50%;
  148. }
  149. .pdl-progress:not(:empty):after {
  150. background: conic-gradient(#01B468 0, #01B468 var(--pdl-progress), transparent var(--pdl-progress), transparent);
  151. transition: --pdl-progress .2s ease;
  152. }
  153. .pdl-progress:empty:after {
  154. background: conic-gradient(#01B468 0, #01B468 25%, #01B46833 25%, #01B46833);
  155. animation: 1.5s infinite linear pdl_loading;
  156. }
  157. .pdl-nav-placeholder {
  158. flex-grow: 1;
  159. height: 42px;
  160. line-height: 42px;
  161. text-align: right;
  162. font-weight: bold;
  163. font-size: 16px;
  164. color: rgb(133, 133, 133);
  165. border-top: 4px solid transparent;
  166. cursor: default;
  167. white-space: nowrap;
  168. }
  169. .pdl-btn-all.pdl-btn-all,
  170. .pdl-stop.pdl-stop {
  171. background-color: transparent;
  172. border: none;
  173. padding: 0 10px;
  174. }
  175. .pdl-btn-all.pdl-btn-all:hover,
  176. .pdl-stop.pdl-stop:hover {
  177. color: rgb(31, 31, 31);
  178. }
  179. .pdl-btn-all::before,
  180. .pdl-stop::before {
  181. content: '';
  182. height: 24px;
  183. width: 24px;
  184. transition: background-image 0.2s ease 0s;
  185. background: no-repeat center/85%;
  186. }
  187. .pdl-btn-all::before {
  188. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  189. }
  190. .pdl-stop::before {
  191. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  192. }
  193. .pdl-btn-all:hover::before{
  194. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  195. }
  196. .pdl-stop:hover::before {
  197. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  198. }
  199. .pdl-hide {
  200. display: none!important;
  201. }
  202. .pdl-wrap {
  203. text-align: right;
  204. padding-right: 24px;
  205. font-weight: bold;
  206. font-size: 14px;
  207. line-height: 14px;
  208. color: rgb(133, 133, 133);
  209. transition: color 0.2s ease 0s;
  210. }
  211. .pdl-wrap:hover {
  212. color: rgb(31, 31, 31);
  213. }
  214. .pdl-wrap label {
  215. padding-left: 8px;
  216. cursor: pointer;
  217. }
  218. .pdl-wrap input {
  219. vertical-align: top;
  220. appearance: none;
  221. position: relative;
  222. box-sizing: border-box;
  223. width: 28px;
  224. border: 2px solid transparent;
  225. cursor: pointer;
  226. border-radius: 14px;
  227. height: 14px;
  228. background-color: rgba(133, 133, 133);
  229. transition: background-color 0.2s ease 0s, box-shadow 0.2s ease 0s;
  230. }
  231. .pdl-wrap input:hover {
  232. background-color: rgba(31, 31, 31);
  233. }
  234. .pdl-wrap input::after {
  235. content: "";
  236. position: absolute;
  237. display: block;
  238. top: 0px;
  239. left: 0px;
  240. width: 10px;
  241. height: 10px;
  242. transform: translateX(0px);
  243. background-color: rgb(255, 255, 255);
  244. border-radius: 10px;
  245. transition: transform 0.2s ease 0s;
  246. }
  247. .pdl-wrap input:checked {
  248. background-color: rgb(0, 150, 250);
  249. }
  250. .pdl-wrap input:checked::after {
  251. transform: translateX(14px);
  252. }
  253. .pdl-wrap-artworks {
  254. position: absolute;
  255. right: 8px;
  256. top: 0px;
  257. bottom: 0px;
  258. margin-top: 40px;
  259. }
  260. .pdl-modal * {
  261. font-family: 'win-bug-omega, system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif';
  262. line-height: 1.15;
  263. }
  264. .pdl-modal {
  265. position: fixed;
  266. top: 0;
  267. left: 0;
  268. width: 100%;
  269. height: 100%;
  270. display: flex;
  271. z-index: 99;
  272. background-color: rgba(0, 0, 0, 0.32);
  273. user-select: none;
  274. }
  275. .pdl-dialog {
  276. position: relative;
  277. background-color: #fff;
  278. border-radius: 24px;
  279. margin: auto;
  280. padding: 20px 40px 30px 40px;
  281. max-width: 720px;
  282. min-width: 500px;
  283. font-size: 16px;
  284. }
  285. .pdl-dialog-header > h3 {
  286. font-weight: bold;
  287. font-size: 1.17em;
  288. margin: 1em 0;
  289. }
  290. .pdl-dialog p {
  291. margin: 1em 0px;
  292. overflow-wrap: break-word;
  293. }
  294. .pdl-dialog-close {
  295. position: absolute;
  296. top: 10px;
  297. right: 10px;
  298. margin: 0;
  299. padding: 0;
  300. width: 25px;
  301. height: 25px;
  302. border: none;
  303. cursor: pointer;
  304. border-radius: 50%;
  305. background-color: transparent;
  306. transform: rotate(45deg);
  307. transition: 0.25s background-color;
  308. background: linear-gradient(rgb(125, 125, 125) 0%, rgb(125, 125, 125) 100%) center/18px 2px no-repeat,
  309. linear-gradient(rgb(125, 125, 125) 0%, rgb(125, 125, 125) 100%) center/2px 18px no-repeat;
  310. }
  311. .pdl-dialog-close:hover {
  312. background-color: rgba(0, 0, 0, 0.05);
  313. }
  314. .pdl-dialog-content {
  315. user-select: text;
  316. }
  317. .pdl-btn.pdl-tag {
  318. height: auto;
  319. border-top-right-radius: 4px;
  320. border-bottom-right-radius: 4px;
  321. left: -1px;
  322. background-color: rgba(0, 0, 0, 0.12);
  323. transition: background-image 0.5s;
  324. }
  325. .pdl-btn.pdl-tag.pdl-tag-hide,
  326. .pdl-btn.pdl-modal-tag.pdl-tag-hide{
  327. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3C/svg%3E");
  328. pointer-events: none;
  329. }
  330. .pdl-btn.pdl-modal-tag {
  331. position: absolute;
  332. right: 65px;
  333. top: 6px;
  334. background-origin: content-box;
  335. border-radius: 4px;
  336. padding: 5px;
  337. width: 42px;
  338. height: 50px;
  339. background-color: rgba(0,0,0,0.04);
  340. transition: .25s background-color;
  341. }
  342. .pdl-btn.pdl-modal-tag:not(.pdl-tag-hide):hover {
  343. background-color: rgba(0,0,0,0.12);
  344. }
  345. `;
  346. function addStyle() {
  347. const sty = document.createElement('style');
  348. sty.innerHTML = style;
  349. document.head.appendChild(sty);
  350. }
  351.  
  352. function debugLog(...msgs) {
  353. }
  354.  
  355. const defaultSettings = {
  356. version: "0.7.3",
  357. ugoriaFormat: "zip",
  358. folderPattern: "pixiv/{artist}",
  359. filenamePattern: "{artist}_{title}_{id}_p{page}",
  360. tagLang: "ja",
  361. showMsg: true,
  362. log: false,
  363. };
  364. const regexp = {
  365. artworksPage: /artworks\/(\d+)$/,
  366. userPage: /users\/(\d+)/,
  367. bookmarkPage: /users\/\d+\/bookmarks\/artworks/,
  368. userPageTags:
  369. /users\/\d+\/(artworks|illustrations|manga|bookmarks(?!artworks))/,
  370. ppSearchPage: /\/tags\/.*\/(artworks|illustrations|manga)/,
  371. suscribePage: /bookmark_new_illust/,
  372. activityHref: /illust_id=(\d+)/,
  373. originSrcPageNum: /(?<=_p)\d+/,
  374. };
  375. const artworkType = {
  376. ILLUSTS: 0,
  377. MANGA: 1,
  378. UGOIRA: 2,
  379. };
  380. const depsUrls = {
  381. gifWorker:
  382. "https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js",
  383. pako: "https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.4/pako.min.js",
  384. upng: "https://cdnjs.cloudflare.com/ajax/libs/upng-js/2.1.0/UPNG.min.js",
  385. };
  386. const creditCode = `<img style="display: block; margin: 1em auto; width: 200px"
  387. src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAADzWSURBVHhe7Z0HnBRF9sdrMyxZoiCegKCCOZ3xzKcCgp56CoqKohj+6pmznBGMGM+cQDALBgwneuZ4nieGU5KKIoqAIrCEDf1/3zddQ29vd09P2oD++BQzW93TXV316uWqLvhZUCowv8P8+OOP5uOPPzY9evQwvXr1MoWFhe6RYDiOY2bPnm2+/vprs+WWW5p27dq5R37bWCEwNTU1K6WDfvNYvny5M2LECKd58+bOjjvu6Hz77bfukXB88803zuabb+60aNHCueqqq5xVq1a5R+JB+t79tmZBnms5U7IqQWdrHhYvXmymTZvm/hWNlStXmpkzZxohMOVCcK9UKCkpMT/88INZtmyZqa6uNgUFBe6RaHzxxRdm6dKl7l9rJKqieX0Txi+//GKGDh1qhKOYs846y1RVRc+fsrIy0759eyWOyspK2Ll7JBxrr722ef31182ECRPMyJEjTXFxsXskHOeee67p27ev+b//+79Y92iqWGMJCy4yb9481YNeeOGFlBwINbNbt26qV8G94g567969lYDXWmsttyYatIM2iWqLGuLWrnlYYwmrdevWyoHATz/9ZL777jv9HgYIqmPHjqaoqEg5FmIUAsg1xo4dax588EFz/fXXm/Lycrd2zUOTIiy4CDqNKMluTThatmxpNt54YxVPiMXvv/8+JaF07dpVCYvrw1nywVHatm1rDjvsMLU64+hk6G60Jc4zNyY0GcJCPI0ePdoMGjTIiAWmXCUKDNr+++9vNthgA7PeeuupmEs1kB06dFDCgqDgcg0tqrj/zTffbAYPHmzGjRuXUk9sVJDGL5OZ3OghHMfZZpttYDmO6DXOl19+6R4JhwyEI7PdmTNnjn5PhX/9619Oq1at9B4nn3yyuiDiQvrR/ZY7VFRUOMJFtT177723s2TJEvdI44b0xZImw7FQjvv166cc5auvvjJTp05VMREFzu3UqZPp3r27fk8FxJQ9LxXH4t633Xab2XrrrVVfSsVBM0GzZs3MlVdeaf70pz+Z4cOHm+bNm7tHmgCaCscC77//vtOmTRudwZtuuqkjg+8eiQYz/7PPPnNk8N2aYMyePdvp3LmzXn+PPfZwfv31V/dIXcyfP9/Zbrvt9Ny+ffsqV4xC3DYEAU4lhOz+1fjRpDiWtNdstdVWZt9999W/P/30UzNlyhT9ngrXXHON2XHHHdWHFAUh2mRYZtGiRZEcCwcnliOAy6XyYV133XXahlGjRrk18dGiRYuU4aVGh8bAscTaiz0jZ8yYoSEUabrqWmLxuUfCIaJEzxcCcObNm+fW1gXtsHrcH/7wh0iO+MUXXzg9evTQc//4xz86QojukbrgulZXEmPCrQ0HoaFrr73WkYngiBUcW3+jD9PRC/OFRsGx3nvvPXPUUUeZ++67L5aesv7665tjjjlGvwuRmXvuuUe/R0EGXj/Ri15++WX9HgS87zvttJN+h3NFcSFcHzhhgYjPSP1HjAJ1kwARsfoZhaefflq565gxY8xTTz0Vyy1BKOqGG25QVwb6Z4OjoTnWgQce6EjHafD3zTffdGuj8c477ziiaMfiFkBEpiNEo+eLEhzKHeEMIt6cCy+8UC3EKC767rvvOq1bt9ZrCqG7tcE47rjj9DzKs88+69YGg/tvttlmei5t5j5xMHPmTGejjTbSvtxnn30y0uVyhUbBsbB40FGYccOGDdMwTCrgm8IaYyb/73//M//+97/dI8HgfOl09XSjR4VxAOrx2F922WVm1113jeQUePJFIdfv1sMfBvxjOGy32GILs+2227q1wXj44YfNl19+qd+HDBlixEjR76lAG0jd6dmzp+qhcazgvKKhORbWkrDv5Iw++OCDI60xi0cffVR1LazEl156ya0NBj6s//znP8ot0tFZoiDErHqTEIxytygsXLjQefHFF9UqTKVL3nXXXcq9hRidV155Ja22LliwQP176HQNCThWAYQlM7NBg1b//e9/zX777adcoFWrVkaIxuy99956LIxr4IV+5plnVNfBw17fPh70tY8++kitVThFrjgEz/PEE08ohxswYECsjInGBumTpQ3OsZiRzOKLLrrIKSkpUa5Fot3PP//snpFbYOkxo9PhBL8jPcCxGlzHgiPhozn99NONKPJah2ed2GBcEGT+8MMPk7E0eTb99IPcKeJuf//732OnxdQnPvnkkzUnAbChOZYX0qnO3/72N+eBBx5QX04criImv3PEEUeolYjfB+4X9jtSj+WRndLSUmfatGlubV288MILzvHHH+98+umnbk1d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"
  388. />`;
  389.  
  390. const handleWorker = `
  391. let webpApi = {};
  392. Module.onRuntimeInitialized = () => {
  393. webpApi = {
  394. init: Module.cwrap('init', '', ['number', 'number', 'number']),
  395. createBuffer: Module.cwrap('createBuffer', 'number', ['number']),
  396. addFrame: Module.cwrap('addFrame', 'number', ['number', 'number', 'number']),
  397. generate: Module.cwrap('generate', 'number', []),
  398. freeResult: Module.cwrap('freeResult', '', []),
  399. getResultPointer: Module.cwrap('getResultPointer', 'number', []),
  400. getResultSize: Module.cwrap('getResultSize', 'number', []),
  401. };
  402.  
  403. postMessage('ok');
  404. };
  405.  
  406. onmessage = (evt) => {
  407. const { dataURLs, delays, lossless = 1, quality = 75, method = 4} = evt.data;
  408. webpApi.init(lossless, quality, method);
  409. dataURLs.forEach((dataURL, idx) => {
  410. const base64 = dataURL.split(',')[1];
  411. const binStr = atob(base64);
  412. const u8a = new Uint8Array(binStr.length);
  413. let p = binStr.length;
  414. while (p) {
  415. p--;
  416. u8a[p] = binStr.codePointAt(p);
  417. }
  418.  
  419. const pointer = webpApi.createBuffer(u8a.length);
  420. Module.HEAPU8.set(u8a, pointer);
  421. webpApi.addFrame(pointer, u8a.length, delays[idx]);
  422. postMessage(idx);
  423. });
  424.  
  425. webpApi.generate();
  426. const resultPointer = webpApi.getResultPointer();
  427. const resultSize = webpApi.getResultSize();
  428. const result = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
  429. postMessage(result);
  430. webpApi.freeResult();
  431. };`;
  432.  
  433. function initialDeps(urls) {
  434. return Promise.all([
  435. _getGifWS(urls.gifWorker),
  436. _getApngWS(urls.pako, urls.upng),
  437. _getWebpWS(),
  438. ]).then(([gif, apng, webp]) => {
  439. this._deps.gif = URL.createObjectURL(new Blob([gif], { type: 'text/javascript' }));
  440. this._deps.apng = URL.createObjectURL(new Blob([apng], { type: 'text/javascript' }));
  441. this._deps.webp = URL.createObjectURL(new Blob([webp], { type: 'text/javascript' }));
  442. return this;
  443. });
  444. }
  445. function _fetchDeps(url) {
  446. return fetch(url)
  447. .then((res) => {
  448. if (res.ok) return res.text();
  449. throw new Error(res.status + res.statusText);
  450. })
  451. .catch((err) => {
  452. console.log('[Pixiv Downloader]Fetch dependency failed.', url, err);
  453. return '';
  454. });
  455. }
  456. async function _getGifWS(url) {
  457. let gifWS;
  458. if (!(gifWS = await GM_getValue('gifWS'))) {
  459. gifWS = await _fetchDeps(url);
  460. if (!gifWS) throw new Error('[Pixiv Downloader]Can not fetch gif worker script.');
  461. GM_setValue('gifWS', gifWS);
  462. }
  463. return gifWS;
  464. }
  465. async function _getApngWS(pakoUrl, upngUrl) {
  466. let apngWS;
  467. if (!(apngWS = await GM_getValue('apngWS'))) {
  468. let pako = _fetchDeps(pakoUrl);
  469. let upng = _fetchDeps(upngUrl);
  470. pako = await pako;
  471. upng = await upng;
  472. if (!pako || !upng) throw new Error('[Pixiv Downloader]Can not fetch apng script.');
  473. upng = upng.replace('window.UPNG', 'UPNG').replace('window.pako', 'pako');
  474. const workerEvt = `onmessage = (evt) => {
  475. const {data, width, height, delay } = evt.data;
  476. const png = UPNG.encode(data, width, height, 0, delay, {loop: 0});
  477. if (!png) console.log('Convert Apng failed.');
  478. postMessage(png);
  479. };`;
  480. apngWS = workerEvt + pako + upng;
  481. GM_setValue('apngWS', apngWS);
  482. }
  483. return apngWS;
  484. }
  485. function _getWebpWS() {
  486. return workerChunk + handleWorker;
  487. }
  488. function _createImgElements(zip) {
  489. const eles = [];
  490. zip.forEach((relativePath, file) => {
  491. eles.push(
  492. new Promise((resolve) => {
  493. const image = new Image();
  494. image.onload = () => {
  495. resolve(image);
  496. };
  497. file.async('blob').then((blob) => void (image.src = URL.createObjectURL(blob)));
  498. })
  499. );
  500. });
  501. return Promise.all(eles);
  502. }
  503. function createInstance() {
  504. const zip = new JSZip();
  505. const freeApngWorkers = [];
  506. const freeWebpWorkers = [];
  507. const MAX_CONVERT = 2;
  508. let queue = [];
  509. let active = [];
  510. let isStop = false;
  511. const convertTo = {
  512. webp: (frames, convertMeta) => {
  513. return new Promise((resolve, reject) => {
  514. let worker;
  515. let reuse = false;
  516. if (freeWebpWorkers.length) {
  517. worker = freeWebpWorkers.shift();
  518. reuse = true;
  519. } else {
  520. worker = new Worker(this._deps.webp);
  521. }
  522. convertMeta.abort = convertMeta._baseAbort.bind(null, () => {
  523. reject('[Info]Convert stop manually, reject when convert webp. ' + convertMeta.id);
  524. worker.terminate();
  525. });
  526. const workerLoad = new Promise((resolve) => {
  527. if (reuse) return resolve();
  528. worker.onmessage = (evt) => {
  529. if (evt.data === 'ok') {
  530. resolve();
  531. }
  532. };
  533. });
  534. let dataURLs = [];
  535. let canvas = document.createElement('canvas');
  536. const width = (canvas.width = frames[0].naturalWidth);
  537. const height = (canvas.height = frames[0].naturalHeight);
  538. const context = canvas.getContext('2d', { willReadFrequently: true });
  539. const delays = convertMeta.framesInfo.map((frameInfo) => {
  540. return Number(frameInfo.delay);
  541. });
  542. dataURLs = frames.map((frame, idx) => {
  543. if (convertMeta.isAborted)
  544. throw '[Info]Convert stop manually when converting image to webp. ' + convertMeta.id;
  545. context.clearRect(0, 0, width, height);
  546. context.drawImage(frame, 0, 0, width, height);
  547. const dataURL = canvas.toDataURL('image/webp', 1);
  548. if (typeof convertMeta.onProgress === 'function') {
  549. debugLog('[Info]Webp convert phrase 1:', convertMeta.id);
  550. convertMeta.onProgress((idx / frames.length) * 0.5, 'webp');
  551. }
  552. return dataURL;
  553. });
  554. workerLoad.then(() => {
  555. worker.onmessage = (evt) => {
  556. if (typeof evt.data !== 'object') {
  557. if (typeof convertMeta.onProgress === 'function') {
  558. debugLog('[Info]Webp convert phrase 2:', convertMeta.id, evt.data);
  559. convertMeta.onProgress(0.5 + (evt.data / frames.length) * 0.5, 'webp');
  560. }
  561. } else {
  562. freeWebpWorkers.push(worker);
  563. resolve(new Blob([evt.data], { type: 'image/webp' }));
  564. }
  565. };
  566. worker.postMessage({ dataURLs, delays });
  567. });
  568. });
  569. },
  570. gif: (frames, convertMeta) => {
  571. return new Promise((resolve, reject) => {
  572. let gif = new GIF({
  573. workers: 2,
  574. quality: 10,
  575. workerScript: this._deps.gif,
  576. });
  577. convertMeta.abort = convertMeta._baseAbort.bind(null, gif.abort.bind(gif));
  578. debugLog('[Info]Start convert:', convertMeta.id);
  579. frames.forEach((frame, i) => {
  580. gif.addFrame(frame, { delay: convertMeta.framesInfo[i].delay });
  581. });
  582. gif.on(
  583. 'progress',
  584. (() => {
  585. const type = 'gif';
  586. return (progress) => {
  587. debugLog('[Info]Convert progress:', convertMeta.id);
  588. if (typeof convertMeta.onProgress === 'function')
  589. convertMeta.onProgress(progress, type);
  590. };
  591. })()
  592. );
  593. gif.on('finished', (gifBlob) => {
  594. gif = null;
  595. resolve(gifBlob);
  596. });
  597. gif.on('abort', () => {
  598. gif = null;
  599. reject('[Info]Convert stop: abort. ' + convertMeta.id);
  600. });
  601. gif.render();
  602. });
  603. },
  604. png: (frames, convertMeta) => {
  605. return new Promise((resolve, reject) => {
  606. let canvas = document.createElement('canvas');
  607. const width = (canvas.width = frames[0].naturalWidth);
  608. const height = (canvas.height = frames[0].naturalHeight);
  609. const context = canvas.getContext('2d', { willReadFrequently: true });
  610. const data = [];
  611. const delay = convertMeta.framesInfo.map((frameInfo) => {
  612. return Number(frameInfo.delay);
  613. });
  614. frames.forEach((frame) => {
  615. if (convertMeta.isAborted)
  616. throw '[Info]Convert stop manually, reject when drawImage. ' + convertMeta.id;
  617. context.clearRect(0, 0, width, height);
  618. context.drawImage(frame, 0, 0, width, height);
  619. data.push(context.getImageData(0, 0, width, height).data);
  620. });
  621. canvas = null;
  622. debugLog('[Info]Start convert:', convertMeta.id);
  623. let worker;
  624. if (freeApngWorkers.length) {
  625. worker = freeApngWorkers.shift();
  626. } else {
  627. worker = new Worker(this._deps.apng);
  628. }
  629. convertMeta.abort = convertMeta._baseAbort.bind(null, () => {
  630. reject('[Info]Convert stop manually, reject when convert apng. ' + convertMeta.id);
  631. worker.terminate();
  632. });
  633. worker.onmessage = function (e) {
  634. freeApngWorkers.push(worker);
  635. if (!e.data) {
  636. return reject('[Error]apng data is null. ' + convertMeta.id);
  637. }
  638. const pngBlob = new Blob([e.data], { type: 'image/png' });
  639. resolve(pngBlob);
  640. };
  641. const cfg = { data, width, height, delay };
  642. worker.postMessage(cfg);
  643. });
  644. },
  645. webm: (frames, convertMeta) => {
  646. return new Promise((resolve, reject) => {
  647. let canvas = document.createElement('canvas');
  648. const width = (canvas.width = frames[0].naturalWidth);
  649. const height = (canvas.height = frames[0].naturalHeight);
  650. const context = canvas.getContext('2d');
  651. const stream = canvas.captureStream();
  652. const recorder = new MediaRecorder(stream, {
  653. mimeType: 'video/webm',
  654. videoBitsPerSecond: 80000000,
  655. });
  656. const delay = convertMeta.framesInfo.map((frame) => {
  657. return Number(frame.delay);
  658. });
  659. let data = [];
  660. let frame = 0;
  661. const displayFrame = () => {
  662. context.clearRect(0, 0, width, height);
  663. context.drawImage(frames[frame], 0, 0);
  664. if (convertMeta.isAborted) {
  665. return recorder.stop();
  666. }
  667. setTimeout(() => {
  668. if (typeof convertMeta.onProgress === 'function')
  669. convertMeta.onProgress((frame + 1) / frames.length, 'webm');
  670. if (frame === frames.length - 1) {
  671. return recorder.stop();
  672. } else {
  673. frame++;
  674. }
  675. displayFrame();
  676. }, delay[frame]);
  677. };
  678. recorder.ondataavailable = (event) => {
  679. if (event.data && event.data.size) {
  680. data.push(event.data);
  681. }
  682. };
  683. recorder.onstop = () => {
  684. canvas = null;
  685. if (convertMeta.isAborted) {
  686. return reject(
  687. '[info]Convert stop manually, reject when convert webm.' + convertMeta.id
  688. );
  689. }
  690. resolve(new Blob(data, { type: 'video/webm' }));
  691. };
  692. displayFrame();
  693. recorder.start();
  694. });
  695. },
  696. };
  697. const convert = (convertMeta) => {
  698. const { id, data, convertResolve, convertReject } = convertMeta;
  699. let frames;
  700. active.push(convertMeta);
  701. if (typeof convertMeta.onProgress === 'function') convertMeta.onProgress(0, 'zip');
  702. zip
  703. .folder(id)
  704. .loadAsync(data)
  705. .then(_createImgElements)
  706. .then((imgEles) => {
  707. zip.remove(id);
  708. frames = imgEles;
  709. if (convertMeta.isAborted) throw '[Info]Convert stop manually, reject when unzip. ' + id;
  710. return convertTo[convertMeta.format](frames, convertMeta);
  711. })
  712. .then(convertResolve)
  713. .catch(convertReject)
  714. .finally(() => {
  715. frames.forEach((frame) => URL.revokeObjectURL(frame.src));
  716. frames = null;
  717. active.splice(active.indexOf(convertMeta), 1);
  718. if (queue.length) convert(queue.shift());
  719. });
  720. };
  721. return {
  722. add: (convertMeta) => {
  723. debugLog('[Info]Converter add', convertMeta.id);
  724. return new Promise((convertResolve, convertReject) => {
  725. convertMeta.isAborted = false;
  726. convertMeta.convertResolve = convertResolve;
  727. convertMeta.convertReject = convertReject;
  728. convertMeta._baseAbort = (callBack) => {
  729. if (typeof callBack === 'function') callBack();
  730. convertMeta.isAborted = true;
  731. };
  732. convertMeta.abort = convertMeta._baseAbort;
  733. queue.push(convertMeta);
  734. while (active.length < MAX_CONVERT && queue.length && !isStop) {
  735. convert(queue.shift());
  736. }
  737. });
  738. },
  739. del: (metas) => {
  740. if (!metas.length) return;
  741. isStop = true;
  742. active = active.filter((convertMeta) => {
  743. if (metas.find((meta) => meta.id === convertMeta.id)) {
  744. convertMeta.abort();
  745. } else {
  746. return true;
  747. }
  748. });
  749. queue = queue.filter((convertMeta) => !metas.find((meta) => meta.id === convertMeta.id));
  750. isStop = false;
  751. while (active.length < MAX_CONVERT && queue.length) {
  752. convert(queue.shift());
  753. }
  754. },
  755. };
  756. }
  757. const createConverter = {
  758. _deps: {
  759. gif: '',
  760. apng: '',
  761. webp: '',
  762. },
  763. initialDeps,
  764. createInstance,
  765. };
  766.  
  767. function getSettings() {
  768. let settings;
  769. if (!localStorage.pdlSetting) {
  770. settings = defaultSettings;
  771. saveSettings(settings);
  772. } else {
  773. settings = JSON.parse(localStorage.pdlSetting);
  774. if (settings.version !== defaultSettings.version) {
  775. settings.version = defaultSettings.version;
  776. settings.showMsg = true;
  777. for (const key in defaultSettings) {
  778. if (!(key in settings)) {
  779. settings[key] = defaultSettings[key];
  780. }
  781. }
  782. saveSettings(settings);
  783. }
  784. }
  785. return settings;
  786. }
  787. function saveSettings(settingObj) {
  788. settingObj = settingObj || settings;
  789. localStorage.pdlSetting = JSON.stringify(settingObj);
  790. }
  791. function upgradeSettings(key, value) {
  792. if (key in settings) {
  793. if (settings[key] === value) return;
  794. settings[key] = value;
  795. saveSettings();
  796. }
  797. }
  798. const settings = getSettings();
  799. function setFormatFactory(format) {
  800. return () => {
  801. if (settings.ugoriaFormat !== format) {
  802. upgradeSettings('ugoriaFormat', format);
  803. }
  804. };
  805. }
  806.  
  807. function sleep(delay) {
  808. return new Promise((resolve) => {
  809. setTimeout(resolve, delay);
  810. });
  811. }
  812. function getSelfId() {
  813. return document.querySelector('#qualtrics_user-id')?.textContent;
  814. }
  815. const env = {
  816. isViolentmonkey: GM_info.scriptHandler === 'Violentmonkey',
  817. isBlobDlAvaliable: !(
  818. navigator.userAgent.includes('Firefox') &&
  819. GM_info.scriptHandler === 'Tampermonkey' &&
  820. parseFloat(GM_info.version) > 4.17
  821. ),
  822. isSupportSubpath: GM_info.downloadMode && GM_info.downloadMode === 'browser',
  823. };
  824.  
  825. const _isNeedConvert = (meta) => {
  826. return meta.illustType === artworkType.UGOIRA && settings.ugoriaFormat !== 'zip';
  827. };
  828. const _saveWithoutSubpath = (blob, meta) => {
  829. const dlEle = document.createElement('a');
  830. dlEle.href = URL.createObjectURL(blob);
  831. dlEle.download = meta.path;
  832. dlEle.click();
  833. URL.revokeObjectURL(dlEle.href);
  834. meta.resolve(meta);
  835. };
  836. const _saveWithSubpath = (blob, meta) => {
  837. const imgUrl = URL.createObjectURL(blob);
  838. const request = {
  839. url: imgUrl,
  840. name: meta.path,
  841. onerror: (error) => {
  842. console.log('[pixiv downloader]Error when saving', meta.path);
  843. URL.revokeObjectURL(imgUrl);
  844. meta.reject && meta.reject(error);
  845. },
  846. onload: () => {
  847. if (typeof meta.onLoad === 'function') meta.onLoad();
  848. URL.revokeObjectURL(imgUrl);
  849. meta.resolve(meta);
  850. },
  851. };
  852. meta.abort = GM_download(request).abort;
  853. };
  854. function createDownloader(converter) {
  855. const MAX_DOWNLOAD = 5;
  856. const MAX_RETRY = 3;
  857. let isStop = false;
  858. let queue = [];
  859. let active = [];
  860. let save;
  861. if (env.isBlobDlAvaliable && env.isSupportSubpath) {
  862. save = _saveWithSubpath;
  863. } else {
  864. debugLog('[Info]scriptHandler:', GM_info.scriptHandler, GM_info.version);
  865. save = _saveWithoutSubpath;
  866. }
  867. const download = (meta) => {
  868. debugLog('[Info]Start download:', meta.path);
  869. active.push(meta);
  870. let abortObj;
  871. if ((!env.isBlobDlAvaliable || env.isViolentmonkey) && !_isNeedConvert(meta)) {
  872. abortObj = GM_download({
  873. url: meta.src,
  874. name: meta.path,
  875. headers: {
  876. referer: 'https://www.pixiv.net',
  877. },
  878. ontimeout: errHandler.bind(null, meta),
  879. onerror: errHandler.bind(null, meta),
  880. onload: () => {
  881. debugLog('[Info]Download complete', meta.path);
  882. if (typeof meta.onLoad === 'function') meta.onLoad();
  883. active.splice(active.indexOf(meta), 1);
  884. if (queue.length && !isStop) download(queue.shift());
  885. meta.resolve(meta);
  886. },
  887. });
  888. } else {
  889. const request = {
  890. url: meta.src,
  891. timeout: 20000,
  892. method: 'GET',
  893. headers: {
  894. referer: 'https://www.pixiv.net',
  895. },
  896. responseType: 'blob',
  897. ontimeout: errHandler.bind(null, meta),
  898. onprogress: (e) => {
  899. if (e.lengthComputable && typeof meta.onProgress === 'function') {
  900. meta.onProgress(e.loaded / e.total);
  901. }
  902. },
  903. onload: (e) => {
  904. debugLog('[Info]Download complete', meta.id);
  905. if (!meta.state) return debugLog('[Warning]But download was canceled.', meta.id);
  906. if (_isNeedConvert(meta)) {
  907. const convertMeta = {
  908. id: meta.id,
  909. data: e.response,
  910. format: settings.ugoriaFormat,
  911. framesInfo: meta.ugoiraMeta.frames,
  912. onProgress: meta.onProgress,
  913. };
  914. converter.add(convertMeta).then((blob) => {
  915. save(blob, meta);
  916. }, meta.reject);
  917. } else {
  918. save(e.response, meta);
  919. }
  920. active.splice(active.indexOf(meta), 1);
  921. if (queue.length && !isStop) download(queue.shift());
  922. },
  923. onerror: errHandler.bind(null, meta),
  924. };
  925. abortObj = GM_xmlhttpRequest(request);
  926. }
  927. meta.abort = () => {
  928. meta.state = 0;
  929. abortObj.abort();
  930. meta.reject('[Warning]xhr abort manually. ' + meta.id);
  931. };
  932. };
  933. const add = (metas) => {
  934. if (metas.length < 1) return;
  935. const promises = [];
  936. metas.forEach((meta) => {
  937. promises.push(
  938. new Promise((resolve, reject) => {
  939. meta.state = 1;
  940. meta.resolve = resolve;
  941. meta.reject = reject;
  942. })
  943. );
  944. });
  945. queue = queue.concat(metas);
  946. while (active.length < MAX_DOWNLOAD && queue.length && !isStop) {
  947. download(queue.shift());
  948. }
  949. return Promise.all(promises);
  950. };
  951. const del = (metas) => {
  952. if (!metas.length) return;
  953. isStop = true;
  954. active = active.filter((meta) => {
  955. if (metas.includes(meta)) {
  956. meta.abort();
  957. } else {
  958. return true;
  959. }
  960. });
  961. queue = queue.filter((meta) => !metas.includes(meta));
  962. isStop = false;
  963. while (active.length < MAX_DOWNLOAD && queue.length) {
  964. download(queue.shift());
  965. }
  966. };
  967. const errHandler = (meta) => {
  968. debugLog('[Error]xmlhttpRequest timeout:', meta.src);
  969. if (!meta.retries) {
  970. meta.retries = 1;
  971. } else {
  972. meta.retries++;
  973. }
  974. if (meta.retries > MAX_RETRY) {
  975. meta.reject('[Error]xmlhttpRequest failed: ' + meta.src);
  976. console.log('[pixiv downloader]Network error:', meta.path, meta.src);
  977. active.splice(active.indexOf(meta), 1);
  978. if (queue.length && !isStop) download(queue.shift());
  979. } else {
  980. debugLog('[Warning]retry xhr:', meta.retries, meta.src);
  981. download(meta);
  982. }
  983. };
  984. return {
  985. add: add,
  986. del: del,
  987. };
  988. }
  989.  
  990. function createParser() {
  991. const replaceInvalidChar = (string) => {
  992. if (!string) return;
  993. const temp = document.createElement('div');
  994. temp.innerHTML = string;
  995. return temp.textContent
  996. .trim()
  997. .replace(/^\.|\.$/g, '')
  998. .replace(/[\u200b-\u200f\uFEFF\u202a-\u202e\\/:*?|]/g, '')
  999. .replace(/"/g, "'")
  1000. .replace(/</g, '﹤')
  1001. .replace(/>/g, '﹥');
  1002. };
  1003. const getFilePath = ({ user, userId, title, tags, illustId, page, ext }) => {
  1004. const path =
  1005. settings.folderPattern && env.isSupportSubpath
  1006. ? settings.folderPattern + '/' + settings.filenamePattern
  1007. : settings.filenamePattern;
  1008. return (
  1009. path
  1010. .replaceAll('{artist}', user)
  1011. .replaceAll('{artistID}', userId)
  1012. .replaceAll('{title}', title)
  1013. .replaceAll('{tags}', tags)
  1014. .replaceAll('{page}', page)
  1015. .replaceAll('{id}', illustId) + ext
  1016. );
  1017. };
  1018. const makeTagsStr = (prev, cur, index, tagsArr) => {
  1019. const tag = settings.tagLang === 'jp' ? cur.tag : cur.translation?.['en'] || cur.tag;
  1020. if (index < tagsArr.length - 1) {
  1021. return prev + tag + '_';
  1022. } else {
  1023. return prev + tag;
  1024. }
  1025. };
  1026. const getData = async (url) => {
  1027. const res = await fetch(url);
  1028. if (!res.ok) throw new Error('[Error]fail to fetch:' + url + ', code:' + res.status);
  1029. const data = await res.json();
  1030. if (data.error) throw new Error('[Error]json return error.' + data.message);
  1031. return data;
  1032. };
  1033. const fetchJson = async (url) => {
  1034. let json;
  1035. let retry = 0;
  1036. do {
  1037. try {
  1038. debugLog('[Info]fetch url:', url);
  1039. json = await getData(url);
  1040. } catch (error) {
  1041. retry++;
  1042. if (retry === 3) throw error;
  1043. sleep(3000);
  1044. }
  1045. } while (!json);
  1046. return json;
  1047. };
  1048. const parseByIllust = async (illustId) => {
  1049. let params = '';
  1050. if (settings.tagLang !== 'jp') params = '?lang=' + settings.tagLang;
  1051. const res = await fetch('https://www.pixiv.net/artworks/' + illustId + params);
  1052. if (!res.ok) throw new Error(res.status);
  1053. const htmlText = await res.text();
  1054. const matchText = htmlText.match(/"meta-preload-data" content='(.*)'>/);
  1055. if (!matchText) throw new Error('[Error]Fail to parse preload data.');
  1056. const preloadData = JSON.parse(htmlText.match(/"meta-preload-data" content='(.*)'>/)[1]);
  1057. const illustInfo = preloadData.illust[illustId];
  1058. const user = replaceInvalidChar(illustInfo.userName) || 'userId-' + illustInfo.userId;
  1059. const title = replaceInvalidChar(illustInfo.illustTitle) || 'illustId-' + illustInfo.illustId;
  1060. const tags = replaceInvalidChar(illustInfo.tags.tags.reduce(makeTagsStr, ''));
  1061. const illustType = illustInfo.illustType;
  1062. let metas = [];
  1063. const pathInfo = {
  1064. user,
  1065. title,
  1066. tags,
  1067. illustId,
  1068. userId: illustInfo.userId,
  1069. ext: '',
  1070. page: 0,
  1071. };
  1072. if (illustType === artworkType.ILLUSTS || illustType === artworkType.MANGA) {
  1073. const firstImgSrc = illustInfo.urls.original;
  1074. const srcPrefix = firstImgSrc.slice(0, firstImgSrc.indexOf('_') + 2);
  1075. const srcSuffix = firstImgSrc.slice(-4);
  1076. pathInfo.ext = srcSuffix;
  1077. for (let i = 0; i < illustInfo.pageCount; i++) {
  1078. pathInfo.page = i;
  1079. metas.push({
  1080. id: illustId,
  1081. illustType: illustType,
  1082. path: getFilePath(pathInfo),
  1083. src: srcPrefix + i + srcSuffix,
  1084. });
  1085. }
  1086. }
  1087. if (illustType === artworkType.UGOIRA) {
  1088. const ugoira = await fetchJson(
  1089. 'https://www.pixiv.net/ajax/illust/' + illustId + '/ugoira_meta'
  1090. );
  1091. pathInfo.ext = '.' + settings.ugoriaFormat;
  1092. metas.push({
  1093. id: illustId,
  1094. illustType: illustType,
  1095. path: getFilePath(pathInfo),
  1096. src: ugoira.body.originalSrc,
  1097. ugoiraMeta: ugoira.body,
  1098. });
  1099. }
  1100. return metas;
  1101. };
  1102. function _filterBookmarks(works) {
  1103. const unavaliable = [];
  1104. function filterFn(work) {
  1105. if (work.isBookmarkable) {
  1106. return true;
  1107. } else {
  1108. unavaliable.push(work.id);
  1109. }
  1110. }
  1111. const avaliable = works.filter(filterFn).map((work) => work.id);
  1112. return { avaliable, unavaliable };
  1113. }
  1114. async function* generateIds(userId, category, tag = '', rest = 'show') {
  1115. let requestUrl;
  1116. if (tag || category === 'bookmarks') {
  1117. const OFFSET = 48;
  1118. if (category !== 'bookmarks') {
  1119. requestUrl = `https://www.pixiv.net/ajax/user/${userId}/${category}/tag?tag=${tag}&offset=0&limit=${OFFSET}&lang=ja`;
  1120. } else {
  1121. requestUrl = `https://www.pixiv.net/ajax/user/${userId}/illusts/bookmarks?tag=${tag}&offset=0&limit=${OFFSET}&rest=${rest}&lang=ja`;
  1122. }
  1123. let head = 0;
  1124. const firstPageData = await fetchJson(requestUrl);
  1125. const total = firstPageData.body.total;
  1126. yield total;
  1127. yield _filterBookmarks(firstPageData.body.works);
  1128. head += OFFSET;
  1129. while (head < total) {
  1130. const data = await fetchJson(requestUrl.replace('offset=0', 'offset=' + head));
  1131. head += OFFSET;
  1132. await sleep(3000);
  1133. yield _filterBookmarks(data.body.works);
  1134. }
  1135. } else {
  1136. requestUrl = 'https://www.pixiv.net/ajax/user/' + userId + '/profile/all';
  1137. const profile = await fetchJson(requestUrl);
  1138. let illustIds;
  1139. if (category !== 'both') {
  1140. illustIds = Reflect.ownKeys(profile.body[category]);
  1141. } else {
  1142. illustIds = Reflect.ownKeys(profile.body.illusts).concat(
  1143. Reflect.ownKeys(profile.body.manga)
  1144. );
  1145. }
  1146. yield illustIds.length;
  1147. yield { avaliable: illustIds, unavaliable: [] };
  1148. }
  1149. }
  1150. return {
  1151. id: parseByIllust,
  1152. generateIds,
  1153. };
  1154. }
  1155.  
  1156. let converter;
  1157. let downloader;
  1158. let parser;
  1159. async function initial() {
  1160. converter = await createConverter
  1161. .initialDeps(depsUrls)
  1162. .then((createConverter) => createConverter.createInstance());
  1163. parser = createParser();
  1164. downloader = createDownloader(converter);
  1165. }
  1166.  
  1167. const lang =
  1168. document.documentElement.getAttribute("lang").toLowerCase() || "en";
  1169. const i18nLib = {
  1170. en: {
  1171. illusts: "Illusts",
  1172. manga: "Manga",
  1173. illusts_manga: "Illusts & Manga",
  1174. bookmarks: "Bookmarks",
  1175. bookmarks_public: "Public",
  1176. bookmarks_private: "Private",
  1177. exclude_downloaded: "Exclude downloaded",
  1178. stop: "Stop",
  1179. edit_filename: "Edit filename",
  1180. clear_history: "Clear history",
  1181. clear_history_tips: "Do you really want to clear history?",
  1182. feedback: "Feedback",
  1183. modal_cancel: "Cancel",
  1184. modal_confirm: "OK",
  1185. tags_lang: "Tags language: ",
  1186. tags_tips: "{artist}, {artistID}, {title}, {id}, {page}, {tags}",
  1187. tags_tips2:
  1188. 'Note: Tags language may not be the language you selected, <a href="https://crowdin.com/project/pixiv-tags" target="_blank">some tags without translations</a> may still be in other languages.',
  1189. folder: "Folder:",
  1190. folder_tips: "I don't need subfolder",
  1191. folder_tips2:
  1192. "If you don't need a subfolder, just leave the folder name blank",
  1193. folder_vm_tips: "VM doesn't support",
  1194. folder_api_tips: "Need Browser Api",
  1195. filename: "FileName:",
  1196. filename_tips: "Your Name?",
  1197. },
  1198. "zh-cn": {
  1199. illusts: "插画",
  1200. manga: "漫画",
  1201. illusts_manga: "插画 & 漫画",
  1202. bookmarks: "收藏",
  1203. bookmarks_public: "公开",
  1204. bookmarks_private: "不公开",
  1205. exclude_downloaded: "排除已下载图片",
  1206. stop: "停止",
  1207. edit_filename: "编辑文件名",
  1208. clear_history: "清除下载历史",
  1209. clear_history_tips: "真的要清除历史吗?",
  1210. feedback: "有问题or想建议?这里反馈",
  1211. modal_cancel: "取消",
  1212. modal_confirm: "确认",
  1213. tags_lang: "标签语言:",
  1214. tags_tips:
  1215. "{artist}:作者, {artistID}:作者ID, {title}:作品标题, {id}:作品pixiv ID, {page}:页码, {tags}:作品标签。",
  1216. tags_tips2:
  1217. '请注意:标签翻译不一定是你选择的语言,部分<a href="https://crowdin.com/project/pixiv-tags" target="_blank">无对应语言翻译的标签</a>仍可能是其他语言。',
  1218. folder: "文件夹名:",
  1219. folder_tips: "我不想保存到子文件夹",
  1220. folder_tips2: "如果不想保存到画师目录,文件夹名留空即可。",
  1221. folder_vm_tips: "Violentmonkey不支持",
  1222. folder_api_tips: "需要Browser Api",
  1223. filename: "文件名:",
  1224. filename_tips: "你的名字?",
  1225. },
  1226. };
  1227. i18nLib.en = Object.create(
  1228. i18nLib["zh-cn"],
  1229. Object.getOwnPropertyDescriptors(i18nLib.en)
  1230. );
  1231. i18nLib.ja = Object.create(i18nLib.en);
  1232. i18nLib.ko = Object.create(i18nLib.en);
  1233. i18nLib["zh-tw"] = Object.create(i18nLib["zh-cn"]);
  1234. i18nLib.zh = i18nLib["zh-cn"];
  1235. const i18n = (key) =>
  1236. i18nLib[lang]?.[key] || `i18n[${lang}][${key}] not found`;
  1237. const modalHtml = {
  1238. upgradeMsgTitle: `<h3>Pixiv Downloader ${defaultSettings.version}</h3>`,
  1239. upgradeMsgContent: `<p>增加导出 / 导入下载记录的功能。</p>`,
  1240. modalCreditFooter: `<style>.pdl-dialog-footer {
  1241. position: relative;
  1242. font-size: 12px;
  1243. }</style><details style="margin-top: 1.5em;">
  1244. <summary style="display: inline-block; list-style: none; cursor: pointer; color: rgb(0, 0, 238); text-decoration: underline">脚本还行?请我喝杯可乐吧!</summary>
  1245. ${creditCode}
  1246. <p style="text-align: center">愿你每天都能找到对的色图,就像我每天都能喝到香草味可乐</p>
  1247. </details>`,
  1248. modalFeedback: `<a target="_blank" style="position: absolute; right: 0px; top: 0px; color: rgb(0, 0, 238); text-decoration: underline" href="https://greasyfork.org/zh-CN/scripts/432150-pixiv-downloader/feedback">${i18n(
  1249. "feedback"
  1250. )}</a>`,
  1251. filePathSettingTitle: `<h3>${i18n("edit_filename")}</h3>`,
  1252. filePathSettingContent: `<style>.pdl-dialog-content input[type="text"] {height: auto; padding: 0.5em; line-height: 1.5; margin: 0.6em 0 0.3em 0; font-size: 16px;}.pdl-dialog-content a{color: rgb(0, 0, 238); text-decoration: underline;} .tags-option label,.tags-option input {cursor: pointer;}</style><div style="display: flex; gap: 20px; justify-content: space-between;">
  1253. <div>
  1254. <label style="display: block; cursor: default;" for="pdlfolder">${i18n(
  1255. "folder"
  1256. )}</label>
  1257. <input type="text" id="pdlfolder" style="width: 200px;" maxlength='100'>
  1258. </div>
  1259. <div>
  1260. <label style="display: block; cursor: default;" for="pdlfilename">${i18n(
  1261. "filename"
  1262. )}</label>
  1263. <input type="text" id="pdlfilename" style="width: 300px;" placeholder="${i18n(
  1264. "filename_tips"
  1265. )}" required maxlength='100'>
  1266. </div>
  1267. </div>
  1268. <div class="tags-option" style="margin: 0.7em 0;">
  1269. <span>${i18n("tags_lang")}</span>
  1270. <input type="radio" name="lang" id="lang_ja" value="ja"/>
  1271. <label for="lang_ja">日本語(default)</label>
  1272. <input type="radio" name="lang" id="lang_zh" value="zh" />
  1273. <label for="lang_zh">简中</label>
  1274. <input type="radio" name="lang" id="lang_zh_tw" value="zh_tw" />
  1275. <label for="lang_zh_tw">繁中</label>
  1276. <input type="radio" name="lang" id="lang_en" value="en" />
  1277. <label for="lang_en">English</label>
  1278. </div>
  1279. <p style="font-size: 14px; margin: 0.5em 0">
  1280. ${i18n("tags_tips")}
  1281. </p>
  1282. <p style="font-size: 14px; margin: 0.5em 0">${i18n("folder_tips2")}</p>
  1283. <p style="font-size: 14px; margin: 0.5em 0">${i18n("tags_tips2")}</p>
  1284. </div>`,
  1285. modalOperationBar: `<style>
  1286. .pdl-dialog-footer button {
  1287. font-size: 16px;
  1288. background-color: transparent;
  1289. border: 1px solid;
  1290. color: rgb(125,125,125);
  1291. border-radius: 5px;
  1292. padding: 0.5em 1.5em;
  1293. cursor: pointer;
  1294. transition: .2s opacity;
  1295. line-height: 1.15;
  1296. }
  1297. .pdl-dialog-footer button:hover{
  1298. opacity: 0.7;
  1299. }
  1300. </style>
  1301. <div style="display: flex; justify-content: flex-end; margin-top: 1.5em; gap: 1.5em;">
  1302. <button id="pdlcancel">${i18n(
  1303. "modal_cancel"
  1304. )}</button><button id="pdlconfirm" style="border-color: #01b468; background-color: #01b468; color: #fff;">${i18n(
  1305. "modal_confirm"
  1306. )}</button></div>`,
  1307. };
  1308.  
  1309. function add(pixivId) {
  1310. this._records.add(pixivId);
  1311. localStorage.setItem(`pdlTemp-${pixivId}`, "");
  1312. }
  1313. function has(pixivId) {
  1314. return this._records.has(pixivId);
  1315. }
  1316. function getHistory() {
  1317. const storage = localStorage.pixivDownloader || "[]";
  1318. return new Set(JSON.parse(storage));
  1319. }
  1320. function updateHistory() {
  1321. Object.keys(localStorage).forEach((key) => {
  1322. const matchResult = /pdlTemp-(\d+)/.exec(key);
  1323. if (matchResult) {
  1324. this._records.add(matchResult[1]);
  1325. localStorage.removeItem(matchResult[0]);
  1326. }
  1327. });
  1328. this.saveHistory();
  1329. }
  1330. function clearHistory() {
  1331. const isConfirm = confirm(i18n("clear_history_tips"));
  1332. if (!isConfirm) return;
  1333. this.updateHistory();
  1334. this._records = new Set();
  1335. localStorage.pixivDownloader = "[]";
  1336. }
  1337. function saveHistory(historyArr) {
  1338. if (historyArr instanceof Array) {
  1339. localStorage.pixivDownloader = JSON.stringify(historyArr);
  1340. } else {
  1341. localStorage.pixivDownloader = JSON.stringify([...this._records]);
  1342. }
  1343. }
  1344. const pixivHistory = {
  1345. _records: getHistory(),
  1346. add,
  1347. has,
  1348. updateHistory,
  1349. saveHistory,
  1350. clearHistory,
  1351. };
  1352.  
  1353. function handleDownload(pdlBtn, illustId) {
  1354. let pageCount,
  1355. pageComplete = 0;
  1356. const onProgress = (progress = 0, type = null) => {
  1357. if (pageCount > 1) return;
  1358. progress = Math.floor(progress * 100);
  1359. switch (type) {
  1360. case null:
  1361. pdlBtn.style.setProperty('--pdl-progress', progress + '%');
  1362. case 'gif':
  1363. case 'webm':
  1364. case 'webp':
  1365. pdlBtn.textContent = progress;
  1366. break;
  1367. case 'zip':
  1368. pdlBtn.textContent = '';
  1369. break;
  1370. }
  1371. };
  1372. const onLoad = function () {
  1373. if (pageCount < 2) return;
  1374. const progress = Math.floor((++pageComplete / pageCount) * 100);
  1375. pdlBtn.textContent = progress;
  1376. pdlBtn.style.setProperty('--pdl-progress', progress + '%');
  1377. };
  1378. pdlBtn.classList.add('pdl-progress');
  1379. parser
  1380. .id(illustId)
  1381. .then((metas) => {
  1382. let shouldDownloadPage;
  1383. if ((shouldDownloadPage = pdlBtn.getAttribute('should-download'))) {
  1384. metas = [metas[shouldDownloadPage]];
  1385. }
  1386. pageCount = metas.length;
  1387. metas.forEach((meta) => {
  1388. meta.onProgress = onProgress;
  1389. meta.onLoad = onLoad;
  1390. });
  1391. return downloader.add(metas);
  1392. })
  1393. .then(() => {
  1394. pixivHistory.add(illustId);
  1395. pdlBtn.classList.remove('pdl-error');
  1396. pdlBtn.classList.add('pdl-complete');
  1397. })
  1398. .catch((err) => {
  1399. if (err) console.log(err);
  1400. pdlBtn.classList.remove('pdl-complete');
  1401. pdlBtn.classList.add('pdl-error');
  1402. })
  1403. .finally(() => {
  1404. pdlBtn.innerHTML = '';
  1405. pdlBtn.style.removeProperty('--pdl-progress');
  1406. pdlBtn.classList.remove('pdl-progress');
  1407. });
  1408. }
  1409. function changeDlbarDisplay() {
  1410. document.querySelectorAll('nav [pdl-userid]').forEach((ele) => {
  1411. ele.classList.toggle('pdl-hide');
  1412. });
  1413. document.querySelectorAll('section [pdl-userid]').forEach((ele) => {
  1414. ele.classList.toggle('pdl-tag-hide');
  1415. });
  1416. }
  1417. let isDownloading = false;
  1418. let dlBarRef = {};
  1419. async function downloadByIds(idsGenerators, abortBtn, isExcludeDled, updataStatus, onProgressCB) {
  1420. if (!(idsGenerators instanceof Array)) idsGenerators = [idsGenerators];
  1421. let resolve;
  1422. const done = new Promise((r) => {
  1423. resolve = r;
  1424. });
  1425. const abort = (msg) => {
  1426. resolve();
  1427. return Promise.reject(msg);
  1428. };
  1429. let total = 0,
  1430. completed = 0,
  1431. failed = [],
  1432. unavaliable = [];
  1433. let isCanceled = false;
  1434. let metasRecord = [];
  1435. let tooManyRequests = false;
  1436. if (isExcludeDled) pixivHistory.updateHistory();
  1437. abortBtn.onclick = () => {
  1438. isCanceled = true;
  1439. abortBtn.onclick = null;
  1440. if (metasRecord.length) {
  1441. downloader.del(metasRecord);
  1442. converter.del(metasRecord);
  1443. metasRecord = [];
  1444. }
  1445. };
  1446. const afterEach = (illustId) => {
  1447. onProgressCB({
  1448. illustId,
  1449. total,
  1450. completed,
  1451. });
  1452. if (completed === total - failed.length - unavaliable.length) {
  1453. resolve({ failed, unavaliable });
  1454. }
  1455. };
  1456. total = await idsGenerators.reduce(async (prev, cur, index) => {
  1457. const count = (await cur.next()).value;
  1458. return (await prev) + count;
  1459. }, 0);
  1460. if (total === 0) {
  1461. resolve();
  1462. throw 'No Works.';
  1463. }
  1464. updataStatus('Downloading...');
  1465. try {
  1466. for (const idsGenerator of idsGenerators) {
  1467. if (isCanceled) return abort(`Stopped. ${completed} / ${total}`);
  1468. for await (const ids of idsGenerator) {
  1469. debugLog('[Info]ids:', ids);
  1470. if (isCanceled) return done;
  1471. if (ids.unavaliable.length) {
  1472. unavaliable.push(...ids.unavaliable);
  1473. debugLog('[Info]unavaliable ids:', unavaliable.length);
  1474. }
  1475. for (const id of ids.avaliable) {
  1476. if (isCanceled) return abort(`Stopped. ${completed} / ${total}`);
  1477. if (isExcludeDled && pixivHistory.has(id)) {
  1478. total--;
  1479. afterEach(id);
  1480. continue;
  1481. }
  1482. if (tooManyRequests) {
  1483. updataStatus('Too many requests, wait 30s');
  1484. console.log('[Pixiv Downloader]Too many requests, wait 30s');
  1485. await sleep(30000);
  1486. tooManyRequests = false;
  1487. updataStatus('Downloading...');
  1488. }
  1489. parser
  1490. .id(id)
  1491. .then((metas) => {
  1492. if (isCanceled) {
  1493. throw '[Warning]Download stop manually: ' + metas[0].id;
  1494. }
  1495. metasRecord = metasRecord.concat(metas);
  1496. return downloader.add(metas);
  1497. })
  1498. .then(
  1499. (metas) => {
  1500. pixivHistory.add(id);
  1501. if (!isCanceled) {
  1502. metasRecord = metasRecord.filter((meta) => !metas.includes(meta));
  1503. completed++;
  1504. afterEach(id);
  1505. }
  1506. },
  1507. (reason) => {
  1508. if (!isCanceled) {
  1509. if (reason.message && reason.message === '429') tooManyRequests = true;
  1510. if (reason.message && reason.message === '[Error]Fail to parse preload data.') {
  1511. unavaliable.push(id);
  1512. } else {
  1513. failed.push(id);
  1514. }
  1515. afterEach(id);
  1516. }
  1517. }
  1518. );
  1519. await sleep(600);
  1520. }
  1521. }
  1522. }
  1523. } catch (error) {
  1524. console.error(error);
  1525. return abort('Error, see console.');
  1526. }
  1527. return done;
  1528. }
  1529. function downloadWorks(evt) {
  1530. evt.preventDefault();
  1531. evt.stopPropagation();
  1532. if (isDownloading) return;
  1533. const btn = evt.target;
  1534. const userId = btn.getAttribute('pdl-userid');
  1535. const category = btn.getAttribute('category');
  1536. const tag = btn.getAttribute('tag') || undefined;
  1537. const rest = btn.getAttribute('rest') || undefined;
  1538. const isExcludeDled = dlBarRef.filter.checked;
  1539. function updataStatus(str) {
  1540. dlBarRef.statusBar.textContent = str;
  1541. }
  1542. function onProgressCB({ illustId, total, completed }) {
  1543. updataStatus(`Downloading: ${completed} / ${total}`);
  1544. }
  1545. let idsGenerators;
  1546. if (category === 'bookmarks' && rest === 'all') {
  1547. const idsShow = parser.generateIds(userId, category, tag, 'show');
  1548. const idsHide = parser.generateIds(userId, category, tag, 'hide');
  1549. idsGenerators = [idsShow, idsHide];
  1550. } else {
  1551. idsGenerators = parser.generateIds(userId, category, tag, rest);
  1552. }
  1553. function download(idsGenerators) {
  1554. isDownloading = true;
  1555. changeDlbarDisplay();
  1556. return downloadByIds(
  1557. idsGenerators,
  1558. dlBarRef.abortBtn,
  1559. isExcludeDled,
  1560. updataStatus,
  1561. onProgressCB
  1562. )
  1563. .then(
  1564. ({ failed, unavaliable }) => {
  1565. if (failed.length || unavaliable.length) {
  1566. updataStatus(`Failed: ${failed.length + unavaliable.length}. See console.`);
  1567. console.log('[Pixiv Downloader]Failed: ', failed.join(', '));
  1568. console.log('[Pixiv Downloader]Unavaliable: ', unavaliable.join(', '));
  1569. if (failed.length) return failed;
  1570. } else {
  1571. updataStatus('Complete');
  1572. }
  1573. },
  1574. (reason) => {
  1575. if (reason) updataStatus(reason);
  1576. }
  1577. )
  1578. .finally((failed) => {
  1579. changeDlbarDisplay();
  1580. isDownloading = false;
  1581. return failed;
  1582. });
  1583. }
  1584. download(idsGenerators).then((failed) => {
  1585. if (failed instanceof Array) {
  1586. const idsGenerator = [failed.length, { avaliable: failed, unavaliable: [] }][
  1587. Symbol.iterator
  1588. ]();
  1589. download(idsGenerator);
  1590. }
  1591. });
  1592. }
  1593.  
  1594. function createModal(
  1595. { header, content, footer = "" },
  1596. option = { closeOnClickModal: true }
  1597. ) {
  1598. const modal = document.createElement("div");
  1599. const dialog = document.createElement("div");
  1600. modal.classList.add("pdl-modal");
  1601. dialog.classList.add("pdl-dialog");
  1602. if (option.closeOnClickModal) {
  1603. dialog.onclick = (e) => {
  1604. e.stopPropagation();
  1605. };
  1606. modal.onclick = () => {
  1607. modal.remove();
  1608. };
  1609. }
  1610. dialog.innerHTML = ` <header class="pdl-dialog-header">${header}</header>
  1611. <div class="pdl-dialog-content">${content}</div>
  1612. <footer class="pdl-dialog-footer">${footer}</footer>`;
  1613. const closeBtn = document.createElement("button");
  1614. closeBtn.classList.add("pdl-dialog-close");
  1615. closeBtn.onclick = () => {
  1616. modal.remove();
  1617. };
  1618. dialog.insertBefore(closeBtn, dialog.firstChild);
  1619. modal.appendChild(dialog);
  1620. return modal;
  1621. }
  1622. function showUpgradeMsg() {
  1623. document.body.appendChild(
  1624. createModal({
  1625. header: modalHtml.upgradeMsgTitle,
  1626. content: modalHtml.upgradeMsgContent,
  1627. footer: modalHtml.modalCreditFooter + modalHtml.modalFeedback,
  1628. })
  1629. );
  1630. }
  1631. function showFilePathSetting() {
  1632. if (document.querySelector("#pdlfolder")) return;
  1633. const modal = createModal(
  1634. {
  1635. header: modalHtml.filePathSettingTitle,
  1636. content: modalHtml.filePathSettingContent,
  1637. footer: modalHtml.modalOperationBar,
  1638. },
  1639. { closeOnClickModal: false }
  1640. );
  1641. const folder = modal.querySelector("#pdlfolder");
  1642. const filename = modal.querySelector("#pdlfilename");
  1643. modal.querySelector("#pdlcancel").onclick = () => {
  1644. modal.remove();
  1645. };
  1646. modal.querySelector("#pdlconfirm").onclick = () => {
  1647. if (filename.value === "") return;
  1648. const folderPattern = folder.value
  1649. .split("/")
  1650. .map((path) =>
  1651. path
  1652. .trim()
  1653. .replace(/^\.+|\.+$|[\u200b-\u200f\uFEFF\u202a-\u202e\\:*?"|<>]/g, "")
  1654. )
  1655. .filter((path) => !!path)
  1656. .join("/");
  1657. const filenamePattern = filename.value
  1658. .trim()
  1659. .replace(/^\.+|[\u200b-\u200f\uFEFF\u202a-\u202e\\/:*?"|<>]/g, "");
  1660. if (filenamePattern === "") return;
  1661. upgradeSettings(
  1662. "tagLang",
  1663. modal.querySelector(".tags-option [name='lang']:checked").value
  1664. );
  1665. upgradeSettings("folderPattern", folderPattern);
  1666. upgradeSettings("filenamePattern", filenamePattern);
  1667. modal.remove();
  1668. };
  1669. modal.querySelector(
  1670. `.tags-option [value="${settings.tagLang}"]`
  1671. ).checked = true;
  1672. filename.value = settings.filenamePattern;
  1673. folder.value = !env.isSupportSubpath
  1674. ? folder.setAttribute("disabled", "") || ""
  1675. : settings.folderPattern;
  1676. folder.placeholder = env.isViolentmonkey
  1677. ? i18n("folder_vm_tips")
  1678. : !env.isSupportSubpath
  1679. ? i18n("folder_api_tips")
  1680. : i18n("folder_tips");
  1681. document.body.appendChild(modal);
  1682. }
  1683. function getIllustId(node) {
  1684. const isLinkToArtworksPage = regexp.artworksPage.exec(node.href);
  1685. if (isLinkToArtworksPage) {
  1686. if (
  1687. node.getAttribute("data-gtm-value") ||
  1688. node.classList.contains("gtm-illust-recommend-node-node") ||
  1689. node.classList.contains("gtm-discover-user-recommend-node") ||
  1690. node.classList.contains("work")
  1691. ) {
  1692. return isLinkToArtworksPage[1];
  1693. }
  1694. } else {
  1695. const isActivityThumb = regexp.activityHref.exec(node.href);
  1696. if (isActivityThumb && node.classList.contains("work")) {
  1697. return isActivityThumb[1];
  1698. }
  1699. }
  1700. return "";
  1701. }
  1702. function createPdlBtn(
  1703. attributes,
  1704. textContent = "",
  1705. { addEvent } = { addEvent: true }
  1706. ) {
  1707. const ele = document.createElement("button");
  1708. ele.textContent = textContent;
  1709. if (!attributes) return ele;
  1710. const { attrs, classList } = attributes;
  1711. if (classList && classList.length > 0) {
  1712. for (const cla of classList) {
  1713. ele.classList.add(cla);
  1714. }
  1715. }
  1716. if (attrs) {
  1717. for (const key in attrs) {
  1718. ele.setAttribute(key, attrs[key]);
  1719. }
  1720. }
  1721. if (addEvent) {
  1722. ele.addEventListener("click", (evt) => {
  1723. evt.preventDefault();
  1724. evt.stopPropagation();
  1725. const ele = evt.currentTarget;
  1726. if (!evt.currentTarget.classList.contains("pdl-progress")) {
  1727. handleDownload(ele, ele.getAttribute("pdl-id"));
  1728. }
  1729. });
  1730. }
  1731. return ele;
  1732. }
  1733. function createMainBtn(id) {
  1734. if (document.querySelector(".pdl-btn-main")) return;
  1735. const handleBar = document.querySelector("main section section");
  1736. if (handleBar) {
  1737. const pdlBtnWrap = handleBar.lastElementChild.cloneNode();
  1738. const attrs = {
  1739. attrs: { "pdl-id": id },
  1740. classList: ["pdl-btn", "pdl-btn-main"],
  1741. };
  1742. if (pixivHistory.has(id)) attrs.classList.push("pdl-complete");
  1743. pdlBtnWrap.appendChild(createPdlBtn(attrs));
  1744. handleBar.appendChild(pdlBtnWrap);
  1745. }
  1746. }
  1747. function createDownloadBar(userId) {
  1748. const nav = document.querySelector("nav");
  1749. if (!nav || document.querySelector(".pdl-nav-placeholder")) return;
  1750. const fragment = document.createDocumentFragment();
  1751. const placeholder = document.createElement("div");
  1752. placeholder.classList.add("pdl-nav-placeholder");
  1753. dlBarRef.statusBar = fragment.appendChild(placeholder);
  1754. const baseClasses = nav.querySelector("a:not([aria-current])").classList;
  1755. dlBarRef.abortBtn = fragment.appendChild(
  1756. createPdlBtn(
  1757. {
  1758. attrs: { "pdl-userId": userId },
  1759. classList: [...baseClasses, "pdl-stop", "pdl-hide"],
  1760. },
  1761. i18n("stop"),
  1762. { addEvent: false }
  1763. )
  1764. );
  1765. if (userId !== getSelfId()) {
  1766. if (
  1767. nav.querySelector("a[href$='illustrations']") &&
  1768. nav.querySelector("a[href$='manga']")
  1769. ) {
  1770. fragment.appendChild(
  1771. createPdlBtn(
  1772. {
  1773. attrs: { "pdl-userId": userId, category: "both" },
  1774. classList: [...baseClasses, "pdl-btn-all"],
  1775. },
  1776. i18n("illusts_manga"),
  1777. { addEvent: false }
  1778. )
  1779. );
  1780. fragment.appendChild(
  1781. createPdlBtn(
  1782. {
  1783. attrs: { "pdl-userid": userId, category: "illusts" },
  1784. classList: [...baseClasses, "pdl-btn-all"],
  1785. },
  1786. i18n("illusts"),
  1787. { addEvent: false }
  1788. )
  1789. );
  1790. fragment.appendChild(
  1791. createPdlBtn(
  1792. {
  1793. attrs: { "pdl-userid": userId, category: "manga" },
  1794. classList: [...baseClasses, "pdl-btn-all"],
  1795. },
  1796. i18n("manga"),
  1797. { addEvent: false }
  1798. )
  1799. );
  1800. } else if (nav.querySelector("a[href$='illustrations']")) {
  1801. fragment.appendChild(
  1802. createPdlBtn(
  1803. {
  1804. attrs: { "pdl-userid": userId, category: "illusts" },
  1805. classList: [...baseClasses, "pdl-btn-all"],
  1806. },
  1807. i18n("illusts"),
  1808. { addEvent: false }
  1809. )
  1810. );
  1811. } else if (nav.querySelector("a[href$='manga']")) {
  1812. fragment.appendChild(
  1813. createPdlBtn(
  1814. {
  1815. attrs: { "pdl-userid": userId, category: "manga" },
  1816. classList: [...baseClasses, "pdl-btn-all"],
  1817. },
  1818. i18n("manga"),
  1819. { addEvent: false }
  1820. )
  1821. );
  1822. }
  1823. if (nav.querySelector("a[href*='bookmarks']")) {
  1824. fragment.appendChild(
  1825. createPdlBtn(
  1826. {
  1827. attrs: { "pdl-userid": userId, category: "bookmarks" },
  1828. classList: [...baseClasses, "pdl-btn-all"],
  1829. },
  1830. i18n("bookmarks"),
  1831. { addEvent: false }
  1832. )
  1833. );
  1834. }
  1835. } else {
  1836. if (nav.querySelector("a[href*='bookmarks']")) {
  1837. fragment.appendChild(
  1838. createPdlBtn(
  1839. {
  1840. attrs: { "pdl-userid": userId, category: "bookmarks", rest: "all" },
  1841. classList: [...baseClasses, "pdl-btn-all"],
  1842. },
  1843. i18n("bookmarks"),
  1844. { addEvent: false }
  1845. )
  1846. );
  1847. fragment.appendChild(
  1848. createPdlBtn(
  1849. {
  1850. attrs: {
  1851. "pdl-userid": userId,
  1852. category: "bookmarks",
  1853. rest: "show",
  1854. },
  1855. classList: [...baseClasses, "pdl-btn-all"],
  1856. },
  1857. i18n("bookmarks_public"),
  1858. { addEvent: false }
  1859. )
  1860. );
  1861. fragment.appendChild(
  1862. createPdlBtn(
  1863. {
  1864. attrs: {
  1865. "pdl-userid": userId,
  1866. category: "bookmarks",
  1867. rest: "hide",
  1868. },
  1869. classList: [...baseClasses, "pdl-btn-all"],
  1870. },
  1871. i18n("bookmarks_private"),
  1872. { addEvent: false }
  1873. )
  1874. );
  1875. }
  1876. }
  1877. fragment.querySelectorAll(".pdl-btn-all").forEach((node) => {
  1878. node.addEventListener("click", downloadWorks);
  1879. });
  1880. const wrapper = document.createElement("div");
  1881. const checkbox = document.createElement("input");
  1882. const label = document.createElement("label");
  1883. wrapper.classList.add("pdl-wrap");
  1884. checkbox.id = "pdl-filter";
  1885. checkbox.type = "checkbox";
  1886. label.setAttribute("for", "pdl-filter");
  1887. label.textContent = i18n("exclude_downloaded");
  1888. wrapper.appendChild(checkbox);
  1889. wrapper.appendChild(label);
  1890. dlBarRef.filter = checkbox;
  1891. nav.parentElement.insertBefore(wrapper, nav);
  1892. nav.appendChild(fragment);
  1893. }
  1894. function createSubBtn(nodes) {
  1895. const isBookmarkPage = regexp.bookmarkPage.test(location.pathname);
  1896. nodes.forEach((e) => {
  1897. if (e.childElementCount !== 0) {
  1898. const illustId = getIllustId(e);
  1899. if (illustId) {
  1900. const attrs = {
  1901. attrs: { "pdl-id": illustId },
  1902. classList: ["pdl-btn", "pdl-btn-sub"],
  1903. };
  1904. if (pixivHistory.has(illustId)) attrs.classList.push("pdl-complete");
  1905. if (isBookmarkPage) attrs.classList.push("pdl-btn-sub-bookmark");
  1906. e.appendChild(createPdlBtn(attrs));
  1907. }
  1908. }
  1909. });
  1910. }
  1911. function createMultyWorksBtn(id) {
  1912. const works = document.querySelectorAll("[role='presentation'] > a");
  1913. if (works.length < 2) return;
  1914. const containers = Array.from(works).map(
  1915. (node) => node.parentElement.parentElement
  1916. );
  1917. if (containers[0].querySelector(".pdl-btn")) return;
  1918. containers.forEach((node, idx) => {
  1919. const wrapper = document.createElement("div");
  1920. wrapper.classList.add("pdl-wrap-artworks");
  1921. const attrs = {
  1922. attrs: { "pdl-id": id, "should-download": idx },
  1923. classList: ["pdl-btn", "pdl-btn-sub", "artworks"],
  1924. };
  1925. wrapper.appendChild(createPdlBtn(attrs));
  1926. node.appendChild(wrapper);
  1927. });
  1928. }
  1929. const createPresentationBtn = (() => {
  1930. let observer, btn;
  1931. function cb(mutationList) {
  1932. const newImg = mutationList[1]["addedNodes"][0];
  1933. const [pageNum] = regexp.originSrcPageNum.exec(newImg.src);
  1934. const containers = btn.parentElement;
  1935. const attrs = {
  1936. attrs: {
  1937. "pdl-id": btn.getAttribute("pdl-id"),
  1938. "should-download": pageNum,
  1939. },
  1940. classList: ["pdl-btn", "pdl-btn-sub", "presentation"],
  1941. };
  1942. btn.remove();
  1943. btn = createPdlBtn(attrs);
  1944. containers.appendChild(btn);
  1945. }
  1946. return (id) => {
  1947. const containers = document.querySelector(
  1948. "body > [role='presentation'] > div"
  1949. );
  1950. if (!containers) {
  1951. if (observer) {
  1952. observer.disconnect();
  1953. observer = null;
  1954. btn = null;
  1955. }
  1956. return;
  1957. }
  1958. if (containers.querySelector(".pdl-btn")) return;
  1959. const img = containers.querySelector("img");
  1960. const isOriginImg = regexp.originSrcPageNum.exec(img.src);
  1961. if (!isOriginImg) return;
  1962. const [pageNum] = isOriginImg;
  1963. const attrs = {
  1964. attrs: { "pdl-id": id, "should-download": pageNum },
  1965. classList: ["pdl-btn", "pdl-btn-sub", "presentation"],
  1966. };
  1967. btn = createPdlBtn(attrs);
  1968. containers.appendChild(btn);
  1969. observer = new MutationObserver(cb);
  1970. observer.observe(img.parentElement, { childList: true, subtree: true });
  1971. };
  1972. })();
  1973. function createPreviewModalBtn() {
  1974. const illustModalBtn = document.querySelectorAll(
  1975. ".gtm-manga-viewer-preview-modal-open"
  1976. );
  1977. const mangaModalBtn = document.querySelectorAll(
  1978. ".gtm-manga-viewer-open-preview"
  1979. );
  1980. let mangaViewerModalBtn = document.querySelectorAll(
  1981. ".gtm-manga-viewer-close-icon"
  1982. )?.[1];
  1983. if (!illustModalBtn.length && !mangaModalBtn.length) return;
  1984. const btns = [...illustModalBtn, ...mangaModalBtn];
  1985. if (mangaViewerModalBtn) btns.push(mangaViewerModalBtn);
  1986. btns.forEach((node) => {
  1987. node.addEventListener("click", handleModalClick);
  1988. });
  1989. }
  1990. function handleModalClick() {
  1991. const timer = setInterval(() => {
  1992. const ulList = document.querySelectorAll("ul");
  1993. const previewList = ulList[ulList.length - 1];
  1994. if (getComputedStyle(previewList).display !== "grid") return;
  1995. clearInterval(timer);
  1996. const [, id] = regexp.artworksPage.exec(location.pathname);
  1997. previewList.childNodes.forEach((node, idx) => {
  1998. node.style.position = "relative";
  1999. const attrs = {
  2000. attrs: { "pdl-id": id, "should-download": idx },
  2001. classList: ["pdl-btn", "pdl-btn-sub"],
  2002. };
  2003. node.appendChild(createPdlBtn(attrs));
  2004. });
  2005. }, 300);
  2006. }
  2007. function createTagsBtn(userId, category) {
  2008. const tagsEles = document.querySelectorAll(
  2009. "section> div:nth-child(2) > div > div"
  2010. );
  2011. if (!tagsEles.length) return;
  2012. if (category === "illustrations" || category === "artworks")
  2013. category = "illusts";
  2014. let rest = "show";
  2015. if (
  2016. userId === getSelfId() &&
  2017. category === "bookmarks" &&
  2018. location.search.includes("rest=hide")
  2019. )
  2020. rest = "hide";
  2021. tagsEles.forEach((ele) => {
  2022. if (ele.querySelector(".pdl-btn")) return;
  2023. let tag;
  2024. const tagLink = ele.querySelector("a");
  2025. if (!tagLink) return;
  2026. if (tagLink.getAttribute("status") !== "active") {
  2027. if (rest === "hide") {
  2028. tag = tagLink.href.slice(
  2029. tagLink.href.lastIndexOf("/") + 1,
  2030. tagLink.href.lastIndexOf("?")
  2031. );
  2032. } else {
  2033. tag = tagLink.href.slice(tagLink.href.lastIndexOf("/") + 1);
  2034. }
  2035. } else {
  2036. const tagTextEles = ele.querySelectorAll("div[title]");
  2037. tag = tagTextEles[tagTextEles.length - 1].getAttribute("title").slice(1);
  2038. }
  2039. const attrs = {
  2040. attrs: { "pdl-userId": userId, category, tag, rest },
  2041. classList: ["pdl-btn", "pdl-tag"],
  2042. };
  2043. if (isDownloading) attrs.classList.push("pdl-tag-hide");
  2044. const dlBtn = createPdlBtn(attrs, "", { addEvent: false });
  2045. if (
  2046. !(
  2047. tagLink.href.includes("bookmarks") &&
  2048. tagLink.getAttribute("status") !== "active"
  2049. )
  2050. ) {
  2051. dlBtn.style.backgroundColor = tagLink.getAttribute("color") + "80";
  2052. }
  2053. dlBtn.addEventListener("click", downloadWorks);
  2054. ele.appendChild(dlBtn);
  2055. });
  2056. let modalTagsEles;
  2057. let modal;
  2058. if (category === "bookmarks") {
  2059. modal = document.querySelector('div[role="presentation"]');
  2060. if (!modal) return;
  2061. modalTagsEles = modal.querySelectorAll("a");
  2062. } else {
  2063. const charcoalTokens = document.querySelectorAll(".charcoal-token");
  2064. modal = charcoalTokens[charcoalTokens.length - 1];
  2065. modalTagsEles = modal.querySelectorAll("a");
  2066. }
  2067. if (!regexp.userPageTags.exec(modalTagsEles[0]?.href)) return;
  2068. modalTagsEles.forEach((ele) => {
  2069. if (ele.querySelector(".pdl-btn")) return;
  2070. let tag;
  2071. if (rest === "hide") {
  2072. tag = ele.href.slice(
  2073. ele.href.lastIndexOf("/") + 1,
  2074. ele.href.lastIndexOf("?")
  2075. );
  2076. } else {
  2077. tag = ele.href.slice(ele.href.lastIndexOf("/") + 1);
  2078. }
  2079. const attrs = {
  2080. attrs: { "pdl-userId": userId, category, tag, rest },
  2081. classList: ["pdl-btn", "pdl-modal-tag"],
  2082. };
  2083. if (isDownloading) attrs.classList.push("pdl-tag-hide");
  2084. const dlBtn = createPdlBtn(attrs, "", { addEvent: false });
  2085. dlBtn.addEventListener("click", (evt) => {
  2086. modal.querySelector("svg").parentElement.click();
  2087. downloadWorks(evt);
  2088. });
  2089. ele.appendChild(dlBtn);
  2090. });
  2091. }
  2092. function compatPixivPreviewer(nodes) {
  2093. const isPpSearchPage = regexp.ppSearchPage.test(location.pathname);
  2094. if (!isPpSearchPage) return;
  2095. nodes.forEach((node) => {
  2096. const pdlEle = node.querySelector(".pdl-btn");
  2097. if (!pdlEle) return false;
  2098. pdlEle.remove();
  2099. });
  2100. }
  2101. let firstRun = true;
  2102. function observerCallback(records) {
  2103. const addedNodes = [];
  2104. records.forEach((record) => {
  2105. if (!record.addedNodes.length) return;
  2106. record.addedNodes.forEach((node) => {
  2107. if (
  2108. node.nodeType === Node.ELEMENT_NODE &&
  2109. node.tagName !== "BUTTON" &&
  2110. node.tagName !== "IMG"
  2111. ) {
  2112. addedNodes.push(node);
  2113. }
  2114. });
  2115. });
  2116. if (!addedNodes.length) {
  2117. return;
  2118. }
  2119. if (firstRun) {
  2120. createSubBtn(document.querySelectorAll("a"));
  2121. firstRun = false;
  2122. } else {
  2123. compatPixivPreviewer(addedNodes);
  2124. const thunmnails = addedNodes.reduce((prev, current) => {
  2125. return prev.concat(Array.from(current.querySelectorAll("a")));
  2126. }, []);
  2127. createSubBtn(thunmnails);
  2128. }
  2129. const isArtworksPage = regexp.artworksPage.exec(location.pathname);
  2130. const isUserPage = regexp.userPage.exec(location.pathname);
  2131. const isTagsPage = regexp.userPageTags.exec(location.pathname);
  2132. if (isArtworksPage) {
  2133. const id = isArtworksPage[1];
  2134. createMainBtn(id);
  2135. createMultyWorksBtn(id);
  2136. createPresentationBtn(id);
  2137. createPreviewModalBtn();
  2138. } else if (isUserPage) {
  2139. createDownloadBar(isUserPage[1]);
  2140. if (isTagsPage) {
  2141. createTagsBtn(isUserPage[1], isTagsPage[1]);
  2142. }
  2143. }
  2144. }
  2145.  
  2146. function getHistoryStr() {
  2147. return JSON.stringify([...pixivHistory._records]);
  2148. }
  2149. function createImportModal() {
  2150. if (document.querySelector("#pdlfolder")) return;
  2151. const html = {
  2152. header: "<h3>导入 / 导出</h3>",
  2153. content: `<p>
  2154. <p><label for="pdl-import">选择要导入的文件:</label></p>
  2155. <input type="file" id="pdl-import" accept=".txt">
  2156. </p>
  2157. <p>
  2158. <hr style="border-top:solid 1px grey">
  2159. <p>
  2160. <button id="pdl-output">导出记录</button>
  2161. </p>`,
  2162. };
  2163. const modal = createModal(html);
  2164. const file = modal.querySelector("#pdl-import");
  2165. file.addEventListener("change", () => {
  2166. const file = modal.querySelector("#pdl-import").files[0];
  2167. if (file && file.type === "text/plain") {
  2168. const reader = new FileReader();
  2169. reader.readAsText(file);
  2170. reader.onload = (evt) => {
  2171. const text = evt.target.result;
  2172. try {
  2173. const history = JSON.parse(text);
  2174. if (!(history instanceof Array)) throw new Error("Invalid file");
  2175. if (history.length) {
  2176. if (!history.every((id) => typeof id === "string"))
  2177. throw new Error("Invalid file");
  2178. }
  2179. pixivHistory.saveHistory(history);
  2180. location.reload();
  2181. } catch (error) {
  2182. alert(error.message);
  2183. }
  2184. };
  2185. } else {
  2186. alert("Invalid file");
  2187. }
  2188. });
  2189. const btn = modal.querySelector("#pdl-output");
  2190. btn.addEventListener("click", () => {
  2191. const dlEle = document.createElement("a");
  2192. const history = getHistoryStr();
  2193. dlEle.href = URL.createObjectURL(
  2194. new Blob([history], { type: "text/plain" })
  2195. );
  2196. dlEle.download = "Pixiv Downloader " + new Date().toLocaleString();
  2197. dlEle.click();
  2198. URL.revokeObjectURL(dlEle.href);
  2199. });
  2200. document.body.appendChild(modal);
  2201. }
  2202.  
  2203. addStyle();
  2204. pixivHistory.updateHistory();
  2205. GM_registerMenuCommand("Apng", setFormatFactory("png"), "a");
  2206. GM_registerMenuCommand("Gif", setFormatFactory("gif"), "g");
  2207. GM_registerMenuCommand("Zip", setFormatFactory("zip"), "z");
  2208. GM_registerMenuCommand("Webm", setFormatFactory("webm"), "w");
  2209. GM_registerMenuCommand("Webp", setFormatFactory("webp"), "p");
  2210. GM_registerMenuCommand(
  2211. i18n("clear_history"),
  2212. pixivHistory.clearHistory.bind(pixivHistory),
  2213. "c"
  2214. );
  2215. GM_registerMenuCommand(i18n("edit_filename"), showFilePathSetting, "e");
  2216. GM_registerMenuCommand("导入/导出", createImportModal, "i");
  2217. initial()
  2218. .then(() => {
  2219. if (settings.showMsg) {
  2220. showUpgradeMsg();
  2221. upgradeSettings("showMsg", false);
  2222. }
  2223. new MutationObserver(observerCallback).observe(document.body, {
  2224. childList: true,
  2225. subtree: true,
  2226. });
  2227. document.addEventListener("keydown", (e) => {
  2228. if (e.ctrlKey && e.key === "q") {
  2229. const pdlMainBtn = document.querySelector(".pdl-btn-main");
  2230. if (pdlMainBtn) {
  2231. e.preventDefault();
  2232. if (!e.repeat) {
  2233. pdlMainBtn.dispatchEvent(new MouseEvent("click"));
  2234. }
  2235. }
  2236. }
  2237. });
  2238. })
  2239. .catch(console.error);
  2240.  
  2241. })();