├── assets ├── home.png ├── icon.png ├── logo.png ├── buttons.png ├── config.png ├── gallery.png ├── fetching.png ├── compressing.png ├── downloading.png └── recommendations.png ├── LICENSE ├── README.md └── nhentai-konnichiwa.user.js /assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/home.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/buttons.png -------------------------------------------------------------------------------- /assets/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/config.png -------------------------------------------------------------------------------- /assets/gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/gallery.png -------------------------------------------------------------------------------- /assets/fetching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/fetching.png -------------------------------------------------------------------------------- /assets/compressing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/compressing.png -------------------------------------------------------------------------------- /assets/downloading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/downloading.png -------------------------------------------------------------------------------- /assets/recommendations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/HEAD/assets/recommendations.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 naiymu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | CodeFactor 5 |

6 |

NHentai Konnichiwa

7 |

8 | A simple usercript for downloading galleries from NHentai and mirrors 9 |

10 | 11 | ## Description 12 | Userscript to download galleries from NHentai. It has features to be used with 13 | [konnichiwa](https://github.com/naiymu/konnichiwa). NHentai started using 14 | Cloudflare protection and now you can't reliably download hentai with the 15 | download tab in konnichiwa. To make things easier for yourself if you use 16 | konnichiwa, you could use this userscript. 17 | 18 | If you don't use konnichiwa, you can still use this without losing any 19 | functionality. 20 | 21 | This script downloads a single zip file with separate directories for all 22 | selected galleries inside it. The name of the zip file is the number of 23 | milliseconds since January 1, 1970. 24 | 25 | Supports only 26 | [Tampermonkey](https://www.tampermonkey.net/) 27 | and 28 | [Violentmonkey](https://violentmonkey.github.io/). 29 | 30 | ## Install 31 | - [Install](https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/master/nhentai-konnichiwa.user.js) from *github.com* 32 | - [Install](https://greasyfork.org/scripts/446488-nhentai-konnichiwa/code/NHentai%20Konnichiwa.user.js) from *greasyfork.org* 33 | - [Install](https://sleazyfork.org/scripts/446488-nhentai-konnichiwa/code/NHentai%20Konnichiwa.user.js) from *sleazyfork.org* 34 | 35 | ## Using with konnichiwa 36 | 37 | For using with konnichiwa, extract the zip file, move the json file to the 38 | `scripts/modification` directory and run the command: 39 | ``` 40 | php refresh_db.php [JSON-FILE-NAME] 41 | ``` 42 | This adds all the data to the database. Now move the downloaded directories 43 | to your holy directory. 44 | 45 | ## Features 46 | ### Checkboxes at the top left corner of every gallery on any page 47 | 48 | On the individual gallery page. 49 | 50 |

51 | 52 |

53 | 54 | On the homepage, search and similar pages. 55 | 56 |

57 | 58 |

59 | 60 | On the recommendations section. 61 | 62 |

63 | 64 |

65 | 66 | ### Buttons at the bottom-left corner of the page 67 |

68 | 69 |

70 | 71 | - **Download button**: Download all selected galleries. The Download button changes state depending on the stage of download. 72 | If the data is being `fetched`, it looks like this: 73 |

74 | 75 |

76 | If the data is being `downloaded`, it looks like this: 77 |

78 | 79 |

80 | If the data is being `compressed`, it looks like this: 81 |

82 | 83 |

84 | 85 | - **Config button**: Change configuration options. 86 | 87 | - **Check-all checkbox**: Select all galleries on the page. Note: Since there 88 | are checkboxes on the recommendation ("More like this") section, if you check 89 | this on a gallery page, it's recommendations will get selected as well. 90 | 91 | ### Configuration options 92 | 93 |

94 | 95 |

96 | 97 | - **Download batch size**: Number of downloads to run simultaneously. 98 | 99 | - *Minimum*: 1 100 | - *Maximum*: 50 101 | 102 | Default is 10. If you make it too high, a lot of downloads will fail. 103 | 104 | - **Compression Level**: The level of compression for the final zip. 105 | - *Minumum*: 0 (No compression. Fastest.) 106 | - *Maximum*: 9 (Maximum compression. Slowest.) 107 | 108 | - **Title format**: The galleries are downloaded in their own separate 109 | directories. This option is for specifying the name of those directories. 110 | There are four available options: 111 | 112 | - *pretty* `default` 113 | - *english* 114 | - *japanese* 115 | - *id* 116 | 117 | - **Filename to prepend**: The text that gets added before every filename. If no 118 | value is specified, The default filename is `{page}.{ext}`. 119 | 120 | - **Filename separator**: The character to put between given *filename* (if any) 121 | and `{page}`. So, if this value is set to `Underscore` and the filename is 122 | set to `myFileName`, the files will be saved as `myFileName_{page}.{ext}`. The 123 | symbols `/`, `\`, `|`, `:` and `;` will also be replaced by this separator, 124 | both in the title and the `Filename to prepend` text provided. 125 | There are three available options: 126 | 127 | - *Space* `default` 128 | - *Hyphen* 129 | - *Underscore* 130 | 131 | - **Save JSON**: This is mainly for use with konnichiwa. This outputs a file in 132 | the format specified 133 | [here](https://github.com/naiymu/konnichiwa#with-the-refresh_db-script). There 134 | are three available options: 135 | 136 | - *Don't save* `default` 137 | - *Save as JSON file* 138 | - *Copy to clipboard* 139 | 140 | *Save as JSON file* will save the content to a file with the same name as 141 | the zip file. You can then run `refresh_db.php` script from konnichiwa on 142 | this file to add all the downloaded galleries to your database. 143 | 144 | - **Include groups in authors**: If the *Save JSON* option is set, the output 145 | has a key `authors`. This decides if the 'Groups' in NHentai metadata will be 146 | added to its value. 147 | 148 | - **Button orientation**: The three buttons (download, config and check-all) are 149 | by default aligned vertically. But if you want you can change this. There are 150 | two available options: 151 | 152 | - Vertical `default` 153 | - Horizontal 154 | 155 | - **Open galleries in new tab**: Galleries are opened in new tab by default. 156 | You can turn this off if you want. But as a result, you will be prompted for 157 | confirmation if you click on a gallery link. 158 | 159 | - **Auto restart downloads**: Downlads are automatically restarted on page 160 | change by default. You can turn this off if you want. 161 | 162 | - **Cancel downloads**: You can choose to cancel ongoing downloads by clicking 163 | this. 164 | 165 | ## Suggestions 166 | Ongoing downloads will restart if you change the page. You can, of course, turn 167 | this off. It's still recommended to stay on the page while a download is 168 | ongoing. 169 | 170 | ## Supported sites 171 | - nhentai.net 172 | - nhentai.xxx 173 | - nyahentai.red 174 | - nhentai.to 175 | - nhentai.website 176 | -------------------------------------------------------------------------------- /nhentai-konnichiwa.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name NHentai Konnichiwa 3 | // @author naiymu 4 | // @version 1.1.12 5 | // @license MIT; https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/main/LICENSE 6 | // @namespace https://github.com/naiymu/nhentai-konnichiwa 7 | // @homepage https://github.com/naiymu/nhentai-konnichiwa 8 | // @downloadURL https://github.com/naiymu/nhentai-konnichiwa/raw/main/nhentai-konnichiwa.user.js 9 | // @updateURL https://github.com/naiymu/nhentai-konnichiwa/raw/main/nhentai-konnichiwa.user.js 10 | // @supportURL https://github.com/naiymu/nhentai-konnichiwa/issues 11 | // @description A simple usercript for downloading doujinshi from NHentai and mirrors 12 | // @match https://nhentai.net/* 13 | // @match https://nhentai.xxx/* 14 | // @match https://nyahentai.red/* 15 | // @match https://nhentai.to/* 16 | // @match https://nhentai.website/* 17 | // @exclude /https:\/\/n.*hentai.(red|net|xxx|to|website)\/g\/[0-9]*\/[0-9]+\/?$/ 18 | // @connect nhentai.xxx 19 | // @connect cdn.nload.xyz 20 | // @connect i3.nhentai.net 21 | // @connect cdn.nhentai.xxx 22 | // @connect nhentai.com 23 | // @connect t.dogehls.xyz 24 | // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.0/jszip.min.js 25 | // @require https://unpkg.com/comlink@4.3.1/dist/umd/comlink.min.js 26 | // @grant GM_addStyle 27 | // @grant GM_setClipboard 28 | // @grant GM_xmlhttpRequest 29 | // @grant GM_setValue 30 | // @grant GM_getValue 31 | // @run-at document-end 32 | // @icon https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/main/assets/icon.png 33 | // ==/UserScript== 34 | 35 | GM_addStyle ( 36 | ` 37 | .relative { 38 | position: relative !important; 39 | } 40 | 41 | .download-check { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | cursor: pointer; 46 | height: 20px; 47 | width: 20px; 48 | accent-color: #ed2553; 49 | } 50 | 51 | .download-check:focus, 52 | .download-check:hover { 53 | box-shadow: 0 0 10px 0 #ed2553; 54 | } 55 | 56 | .red-box { 57 | position: relative; 58 | background-color: #ed2553; 59 | color: white; 60 | border: none; 61 | outline: none; 62 | font-size: 16px; 63 | width: 80px; 64 | height: 35px; 65 | border-radius: 5px; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | text-align: center; 70 | cursor: pointer; 71 | } 72 | 73 | .percent { 74 | position: absolute; 75 | top: 50%; 76 | left: 50%; 77 | transform: translate(-50%,-50%); 78 | } 79 | 80 | .downloading-span, 81 | .compressing-span { 82 | display: inline-block; 83 | position: absolute; 84 | top: 0; 85 | left: 0; 86 | width: 50px; 87 | height: 100%; 88 | border-radius: 5px; 89 | } 90 | 91 | .downloading-span { 92 | background-color: #03c03c; 93 | } 94 | 95 | .compressing-span { 96 | background-color: #0047ab; 97 | } 98 | 99 | .red-box:hover { 100 | background-color: #4d4d4d; 101 | } 102 | 103 | .red-box>i, 104 | .red-box>.download-check { 105 | margin-right: 5px; 106 | } 107 | 108 | .download-check-all { 109 | width: 20px; 110 | height: 20px; 111 | cursor: pointer; 112 | accent-color: #1f1f1f; 113 | } 114 | 115 | .red-box>.download-check:focus, 116 | .red-box>.download-check:hover { 117 | box-shadow: none; 118 | } 119 | 120 | .red-box:disabled { 121 | background-color: #1f1f1f; 122 | cursor: default; 123 | } 124 | 125 | .download-div { 126 | position: fixed; 127 | bottom: 0; 128 | left: 0; 129 | display: flex; 130 | flex-direction: column; 131 | gap: 5px; 132 | } 133 | 134 | .div-horizontal { 135 | flex-direction: row !important; 136 | } 137 | 138 | .fa-spinner, 139 | .fa-circle-notch { 140 | animation: spin 1.5s infinite linear; 141 | } 142 | 143 | @keyframes spin { 144 | 100% {transform: rotate(359deg)}; 145 | } 146 | 147 | .config-wrapper { 148 | position: fixed; 149 | top: 0; 150 | left: 0; 151 | width: 100%; 152 | height: 100%; 153 | background-color: rgba(0, 0, 0, 0.8); 154 | z-index: 999999; 155 | visibility: hidden; 156 | } 157 | 158 | .visible { 159 | visibility: visible; 160 | } 161 | 162 | .config-div { 163 | position: absolute; 164 | top: 50%; 165 | left: 50%; 166 | width: 100%; 167 | height: 100%; 168 | max-width: 850px; 169 | max-height: 550px; 170 | padding: 50px; 171 | transform: translate(-50%, -50%); 172 | background-color: #1f1f1f; 173 | border-radius: 5px; 174 | display: flex; 175 | flex-direction: column; 176 | align-items: flex-start; 177 | justify-content: center; 178 | overflow: scroll; 179 | } 180 | 181 | .config-element-div { 182 | width: 100%; 183 | display: inline-grid; 184 | grid: auto / 200px auto; 185 | margin-bottom: 15px 186 | } 187 | 188 | .config-element-div>input { 189 | color: #000 !important; 190 | } 191 | 192 | .config-element-div>label { 193 | grid-column-start: 1; 194 | } 195 | 196 | .config-element-div>input[type='checkbox'] { 197 | justify-self: left; 198 | } 199 | 200 | .config-element-div>*:not(label) { 201 | grid-column-start: 2; 202 | border-radius: 0; 203 | border: none; 204 | background-color: #d9d9d9 !important; 205 | } 206 | 207 | .config-btn-div { 208 | display: flex; 209 | flex-direction: row; 210 | gap: 10px; 211 | align-items: center; 212 | } 213 | 214 | .config-reset:hover { 215 | cursor: pointer; 216 | color: #ed2553; 217 | } 218 | 219 | .heading { 220 | width: 100%; 221 | border-bottom: solid 1px #d9d9d9; 222 | text-align: left; 223 | } 224 | ` 225 | ); 226 | 227 | const netMediaUrl = "https://i3.nhentai.net/galleries/"; 228 | const btnStates = { 229 | enabled: "", 230 | fetching: "", 231 | downloading: "", 232 | config: "", 233 | }; 234 | const titleFormats = { 235 | PR: "pretty", 236 | EN: "english", 237 | JP: "japanese", 238 | ID: "id", 239 | }; 240 | const saveJSONModes = { 241 | NO: "Don't save", 242 | FI: "Save as JSON file", 243 | CB: "Copy to clipboard", 244 | }; 245 | const fileNameSeps = { 246 | SP: "Space", 247 | HY: "Hyphen", 248 | US: "Underscore", 249 | }; 250 | const btnOrientations = { 251 | VR: "Vertical", 252 | HR: "Horizontal", 253 | }; 254 | const CONFIG = { 255 | simulN: { 256 | label: 'Download batch size', 257 | type: 'int', 258 | min: 1, 259 | max: 50, 260 | default: 10, 261 | }, 262 | compressionLevel: { 263 | label: 'Compression level', 264 | type: 'int', 265 | min: 0, 266 | max: 9, 267 | default: 0, 268 | }, 269 | titleFormat: { 270 | label: 'Title format', 271 | type: 'select', 272 | options: [titleFormats.PR,titleFormats.EN,titleFormats.JP, 273 | titleFormats.ID], 274 | default: titleFormats.PR, 275 | }, 276 | fileNamePrep: { 277 | label: 'Filename to prepend', 278 | type: 'text', 279 | size: 30, 280 | default: "", 281 | }, 282 | fileNameSep: { 283 | label: 'Filename separator', 284 | type: 'select', 285 | options: [fileNameSeps.SP,fileNameSeps.HY,fileNameSeps.US], 286 | default: fileNameSeps.SP, 287 | }, 288 | saveJSONMode: { 289 | label: 'Save JSON', 290 | type: 'select', 291 | options: [saveJSONModes.NO,saveJSONModes.FI,saveJSONModes.CB], 292 | default: saveJSONModes.NO, 293 | }, 294 | includeGroups: { 295 | label: 'Include groups in authors', 296 | type: 'checkbox', 297 | default: false, 298 | }, 299 | btnOrientation: { 300 | label: 'Button orientation', 301 | type: 'select', 302 | options: [btnOrientations.VR, btnOrientations.HR], 303 | default: btnOrientations.VR, 304 | }, 305 | openInNewTab: { 306 | label: 'Open galleries in new tab', 307 | type: 'checkbox', 308 | default: true, 309 | }, 310 | autorestart: { 311 | label: 'Auto restart downloads', 312 | type: 'checkbox', 313 | default: true, 314 | } 315 | }; 316 | 317 | const WORKER_THREAD_NUM = ((navigator && navigator.hardwareConcurrency) || 2) - 1; 318 | 319 | class JSZipWorkerPool { 320 | constructor() { 321 | this.pool = []; 322 | this.WORKER_URL = URL.createObjectURL( 323 | new Blob( 324 | [ 325 | `importScripts( 326 | 'https://unpkg.com/comlink/dist/umd/comlink.min.js', 327 | 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js' 328 | ); 329 | class JSZipWorker { 330 | constructor() { 331 | this.zip = new JSZip; 332 | } 333 | file(title, name, {data:data}) { 334 | this.zip.folder(title).file(name, data); 335 | } 336 | async generateAsync(options, onUpdate) { 337 | const data = await this.zip.generateAsync(options, onUpdate); 338 | const url = URL.createObjectURL(data); 339 | return Comlink.transfer({url:url}); 340 | } 341 | } 342 | Comlink.expose(JSZipWorker);` 343 | ], 344 | {type: 'text/javascript'} 345 | ) 346 | ); 347 | for(let id=0; id idle); 361 | if(!worker) throw new Error('No available JSZip worker'); 362 | worker.idle = false; 363 | if(!worker.JSZip) worker.JSZip = this.createWorker(); 364 | const zip = await new worker.JSZip(); 365 | for(const {title, name, data} of files) { 366 | await zip.file(title, name, Comlink.transfer({data}, [data])); 367 | } 368 | return zip 369 | .generateAsync( 370 | options, 371 | Comlink.proxy((data) => onUpdate({workerId: worker.id, ...data})) 372 | ) 373 | .then(({url}) => { 374 | worker.idle = true; 375 | return url; 376 | }); 377 | } 378 | } 379 | 380 | const jsZipPool = new JSZipWorkerPool(); 381 | 382 | class JSZip { 383 | constructor() { 384 | this.files = []; 385 | } 386 | file(title, name, data) { 387 | this.files.push({title, name, data}); 388 | } 389 | generateAsync(options, onUpdate) { 390 | return jsZipPool.generateAsync(this.files, options, onUpdate); 391 | } 392 | } 393 | 394 | // nhentai.net API URL 395 | const netAPI = "https://nhentai.net/api/gallery/"; 396 | // nhentai.xxx page URL 397 | const xxxPage = "https://nhentai.xxx/g/"; 398 | // If we are on nhentai.net 399 | const onNET = location.hostname == 'nhentai.net'; 400 | // DOM parser for later 401 | var parser = new DOMParser(); 402 | // Saved config options 403 | var configOptions = JSON.parse(GM_getValue('configOptions') || '{}'); 404 | // Buttons and Divs 405 | var downloadDiv, 406 | downloadBtn, 407 | downloadPercent, 408 | downloadingSpan, 409 | compressingSpan, 410 | configWrapper, 411 | configDiv; 412 | // Download info 413 | var downloading = false, 414 | cancelled = false, 415 | total = 0, 416 | downloaded = 0, 417 | currentDownloads = 0, 418 | queue = [], 419 | info = JSON.parse(sessionStorage.getItem('info') || '[]'); 420 | // Final zip 421 | var zip; 422 | 423 | function disableButton(btnState) { 424 | downloadingSpan.style.width = 0; 425 | compressingSpan.style.width = 0; 426 | downloadBtn.disabled = true; 427 | downloadPercent.innerHTML = btnState; 428 | } 429 | 430 | function enableButton() { 431 | downloadingSpan.style.width = 0; 432 | compressingSpan.style.width = 0; 433 | downloadBtn.disabled = false; 434 | downloadPercent.innerHTML = btnStates.enabled; 435 | } 436 | 437 | function createNode(element, classes=[]) { 438 | var node = document.createElement(element); 439 | for(let cls of classes) { 440 | node.classList.add(cls); 441 | } 442 | return node; 443 | } 444 | 445 | function createLabel(id, text) { 446 | var label = createNode('label'); 447 | label.innerHTML = text; 448 | label.setAttribute('for', id); 449 | return label; 450 | } 451 | 452 | function getExtension(type) { 453 | switch(type) { 454 | case 'j': return '.jpg'; 455 | case 'p': return '.png'; 456 | case 'g': return '.gif'; 457 | case 'w': return '.webp'; 458 | } 459 | } 460 | 461 | function saveConfig(reset=false) { 462 | const oldOpenInNewTab = configOptions.openInNewTab; 463 | for(const [key, value] of Object.entries(CONFIG)) { 464 | var element = document.getElementById(`config-${key}`); 465 | var configValue; 466 | switch(value.type) { 467 | case 'checkbox': 468 | configValue = element.checked; 469 | break; 470 | case 'int': 471 | configValue = element.value; 472 | if(configValue > value.max) { 473 | configValue = value.max; 474 | } 475 | if(configValue < value.min) { 476 | configValue = value.min; 477 | } 478 | break; 479 | default: 480 | configValue = element.value; 481 | } 482 | configValue = reset ? value.default : configValue; 483 | configOptions[key] = configValue; 484 | element.value = configValue; 485 | if(value.type == 'checkbox') element.checked = configValue; 486 | } 487 | if(configOptions.btnOrientation == btnOrientations.HR) { 488 | downloadDiv.classList.add('div-horizontal'); 489 | } 490 | else { 491 | downloadDiv.classList.remove('div-horizontal'); 492 | } 493 | GM_setValue('configOptions', JSON.stringify(configOptions)); 494 | if(configOptions.openInNewTab != oldOpenInNewTab) { 495 | var gLinks = document.querySelectorAll('.gallery > a, .gallerythumb'); 496 | for(let a of gLinks) { 497 | if(configOptions.openInNewTab) { 498 | a.setAttribute('target', '_blank'); 499 | } 500 | else { 501 | a.removeAttribute('target'); 502 | } 503 | } 504 | } 505 | } 506 | 507 | function addConfigMenu() { 508 | var saveConfigBtn, closeConfigBtn, cancelAnchor, resetConfigAnchor; 509 | 510 | var heading = createNode('h3', ['heading']); 511 | heading.innerHTML = "nhentai-konnichiwa"; 512 | configDiv.appendChild(heading); 513 | 514 | for(const [key, value] of Object.entries(CONFIG)) { 515 | var div = createNode('div', ['config-element-div']); 516 | var element, id, label; 517 | switch(value.type) { 518 | case 'select': 519 | element = createNode('select'); 520 | for(let i=0; i { 576 | saveConfig(); 577 | }); 578 | 579 | closeConfigBtn = createNode('button', ['red-box']); 580 | closeConfigBtn.innerHTML = "Close"; 581 | closeConfigBtn.addEventListener('click', () => { 582 | configWrapper.classList.remove('visible'); 583 | }); 584 | 585 | cancelAnchor = createNode('a', ['config-reset']); 586 | cancelAnchor.innerHTML = "Cancel downloads"; 587 | cancelAnchor.addEventListener('click', () => { 588 | cancelDownload(); 589 | }); 590 | 591 | resetConfigAnchor = createNode('a', ['config-reset']); 592 | resetConfigAnchor.innerHTML = 'Reset to default'; 593 | resetConfigAnchor.addEventListener('click', () => { 594 | saveConfig(true); 595 | }); 596 | 597 | btnDiv.appendChild(saveConfigBtn); 598 | btnDiv.appendChild(closeConfigBtn); 599 | btnDiv.appendChild(cancelAnchor); 600 | btnDiv.appendChild(resetConfigAnchor); 601 | 602 | configDiv.appendChild(btnDiv); 603 | } 604 | 605 | (async function() { 606 | 'use strict'; 607 | 608 | window.addEventListener('beforeunload', (e) => { 609 | if(downloading) { 610 | e.preventDefault(); 611 | return ''; 612 | } 613 | }); 614 | 615 | var aList, configBtn, checkAllDiv, checkAll; 616 | 617 | configWrapper = createNode('div', ['config-wrapper']); 618 | configDiv = createNode('div', ['config-div']); 619 | configWrapper.addEventListener('click', () => { 620 | if(event.target != configWrapper) { 621 | return; 622 | } 623 | configWrapper.classList.remove('visible'); 624 | }); 625 | configWrapper.appendChild(configDiv); 626 | addConfigMenu(); 627 | 628 | aList = document.querySelectorAll(".gallery > a, #cover > a"); 629 | for(let a of aList) { 630 | if(configOptions.openInNewTab) a.setAttribute('target', '_blank'); 631 | var ref = a.href; 632 | var parent = a.parentElement; 633 | var code; 634 | code = ref.split("/g/")[1]; 635 | if(code.endsWith("/")) { 636 | code = code.split("/")[0]; 637 | } 638 | var check = createNode('input', ['download-check']); 639 | check.type = 'checkbox'; 640 | check.value = code; 641 | parent.classList.add("relative"); 642 | parent.appendChild(check); 643 | } 644 | if(configOptions.openInNewTab) { 645 | aList = document.getElementsByClassName("gallerythumb"); 646 | for(let a of aList) { 647 | a.setAttribute('target', '_blank'); 648 | } 649 | } 650 | let classes = ['download-div']; 651 | if(configOptions.btnOrientation == btnOrientations.HR) { 652 | classes.push('div-horizontal'); 653 | } 654 | downloadDiv = createNode('div', classes); 655 | 656 | downloadBtn = createNode('button', ['red-box']); 657 | downloadPercent = createNode('span', ['percent']); 658 | downloadingSpan = createNode('span', ['downloading-span']); 659 | compressingSpan = createNode('span', ['compressing-span']); 660 | enableButton(); 661 | downloadBtn.appendChild(downloadingSpan); 662 | downloadBtn.appendChild(compressingSpan); 663 | downloadBtn.appendChild(downloadPercent); 664 | 665 | configBtn = createNode('button', ['red-box']); 666 | configBtn.innerHTML = btnStates.config; 667 | configBtn.addEventListener("click", (event) => { 668 | configWrapper.classList.add('visible'); 669 | }); 670 | 671 | checkAllDiv = createNode('div', ['red-box', 'relative']); 672 | checkAll = createNode('input', ['download-check-all']); 673 | checkAll.type = 'checkbox'; 674 | checkAll.addEventListener('change', () => { 675 | let toCheck = checkAll.checked; 676 | let boxes = document.querySelectorAll('.download-check'); 677 | for(let box of boxes) { 678 | if(box === checkAll) { 679 | continue; 680 | } 681 | box.checked = toCheck; 682 | } 683 | }); 684 | checkAllDiv.appendChild(checkAll); 685 | 686 | downloadBtn.addEventListener("click", async () => { 687 | var checked = document.querySelectorAll(".download-check:checked"); 688 | if(checked.length > 0) { 689 | disableButton(btnStates.fetching); 690 | } 691 | else { 692 | return; 693 | } 694 | zip = await new JSZip(); 695 | switch(configOptions.fileNameSep) { 696 | case fileNameSeps.SP: configOptions.fileNameSep = " "; break; 697 | case fileNameSeps.HY: configOptions.fileNameSep = "-"; break; 698 | case fileNameSeps.US: configOptions.fileNameSep = "_"; break; 699 | } 700 | for(let c of checked) { 701 | c.checked = false; 702 | } 703 | for(const c of checked) { 704 | var code = c.getAttribute("value"); 705 | await addInfo(code); 706 | sessionStorage.setItem('info', JSON.stringify(info)); 707 | } 708 | startDownload(); 709 | }); 710 | 711 | downloadDiv.appendChild(downloadBtn); 712 | downloadDiv.appendChild(configBtn); 713 | downloadDiv.appendChild(checkAllDiv); 714 | 715 | document.body.appendChild(downloadDiv); 716 | document.body.appendChild(configWrapper); 717 | 718 | if(info.length > 0) { 719 | if(configOptions.autorestart) { 720 | queue = []; 721 | startDownload(); 722 | } 723 | else { 724 | info = []; 725 | sessionStorage.removeItem('info'); 726 | } 727 | } 728 | })(); 729 | 730 | function startDownload() { 731 | downloading = true; 732 | cancelled = false; 733 | currentDownloads = 0; 734 | downloaded = 0; 735 | zip = new JSZip(); 736 | populateQueue(); 737 | total = queue.length; 738 | disableButton(btnStates.downloading); 739 | downloadQueue(); 740 | } 741 | 742 | function cancelDownload() { 743 | info = []; 744 | queue = []; 745 | sessionStorage.removeItem('info'); 746 | currentDownloads = 0; 747 | downloading = false; 748 | cancelled = true; 749 | enableButton(); 750 | } 751 | 752 | function cleanString(string) { 753 | string = string.replace(/(\.+$)|(^\.+)|(\|)/g, ''); 754 | string = string.replace(/\\\/\:\;/g, configOptions.fileNameSep); 755 | string = string.replace(/\s\s+/, ' '); 756 | string = string.trim(); 757 | return string; 758 | } 759 | 760 | async function makeGetRequest(url, code = null) { 761 | return new Promise((resolve, reject) => { 762 | if(onNET) { 763 | fetch(url, { 764 | method: 'GET', 765 | mode: 'same-origin', 766 | credentials: 'same-origin', 767 | headers: { 768 | 'Content-Type': 'application/json' 769 | }, 770 | referrerPolicy: 'same-origin', 771 | }) 772 | .then(response => resolve(response.json())); 773 | } 774 | else { 775 | GM_xmlhttpRequest({ 776 | method: "GET", 777 | url: url, 778 | onload: (response) => { 779 | resolve(parseXXXResponse(response, code)); 780 | }, 781 | onerror: (error) => { 782 | reject(error); 783 | } 784 | }); 785 | } 786 | }); 787 | } 788 | 789 | function addInfoNET(obj, code) { 790 | var title; 791 | if(configOptions.titleFormat == 'id') { 792 | title = `${obj.id}`; 793 | } 794 | else { 795 | title = obj.title[configOptions.titleFormat]; 796 | title = cleanString(title); 797 | } 798 | var pages = obj.num_pages; 799 | var tagList = obj.tags; 800 | var artists = []; 801 | var tags = []; 802 | for(let i=0; i el.title === title); 814 | if(constTitleExists) { 815 | title += " - "+code; 816 | } 817 | var fileNamePrep = configOptions.fileNamePrep; 818 | fileNamePrep = cleanString(fileNamePrep); 819 | var namePrep = ""; 820 | if(fileNamePrep != "") { 821 | namePrep = fileNamePrep + configOptions.fileNameSep; 822 | } 823 | var coverExtension = getExtension(obj.images.pages[0].t); 824 | info.push({ 825 | code: code, 826 | title: title, 827 | artists: artists, 828 | tags: tags, 829 | pages: pages, 830 | mediaUrl: mediaUrl, 831 | namePrep: namePrep, 832 | coverExtension: coverExtension, 833 | pagesInfo: obj.images.pages, 834 | }); 835 | } 836 | 837 | async function addInfo(code) { 838 | var apiUrl, obj; 839 | if(onNET) { 840 | apiUrl = netAPI + code; 841 | obj = await makeGetRequest(apiUrl); 842 | addInfoNET(obj, code); 843 | } 844 | else { 845 | apiUrl = xxxPage + code; 846 | obj = await makeGetRequest(apiUrl, code); 847 | info.push(obj); 848 | } 849 | } 850 | 851 | function parseXXXResponse(response, code) { 852 | var htmlDoc = parser.parseFromString(response.responseText, 853 | 'text/html'); 854 | var title, artists = [], tags = [], pages, mediaUrl, pagesInfo = [], coverExtension; 855 | var titleTemplate = string => { 856 | return `${string}.title > span`; 857 | } 858 | const cleanRegex = /(\[[^\]]*\])|(\([^)]*\))|(\{[^}]*\})|([\.\|\~]*)/g; 859 | switch(configOptions.titleFormat) { 860 | case 'english': 861 | title = htmlDoc.querySelector(titleTemplate('h1')); 862 | title = title.textContent; 863 | break; 864 | case 'japanese': 865 | title = htmlDoc.querySelector(titleTemplate('h2')); 866 | title = title.textContent; 867 | break; 868 | case 'pretty': 869 | default: 870 | title = htmlDoc.querySelector(titleTemplate('h1')); 871 | title = title.textContent; 872 | title = title.replace(cleanRegex, ''); 873 | title = title.replace(/\s\s+/g, ' '); 874 | title = title.trim(); 875 | title = title.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 876 | } 877 | var tagTemplate = string => { 878 | return `a[href*='${string}/'] > span.name`; 879 | } 880 | var artistSpans = htmlDoc.querySelectorAll(tagTemplate('artist')); 881 | for(var artist of artistSpans) { 882 | artists.push(artist.textContent); 883 | } 884 | if(configOptions.includeGroups) { 885 | var groupSpans = htmlDoc.querySelectorAll(tagTemplate('group')); 886 | for(var group of groupSpans) { 887 | artists.push(group.textContent); 888 | } 889 | } 890 | var tagSpans = htmlDoc.querySelectorAll(tagTemplate('tag')); 891 | for(var tag of tagSpans) { 892 | tags.push(tag.textContent); 893 | } 894 | pages = htmlDoc.querySelector(".tag[href*='#'] > span.name"); 895 | pages = parseInt(pages.textContent); 896 | var fileNamePrep = configOptions.fileNamePrep; 897 | fileNamePrep = cleanString(fileNamePrep); 898 | var namePrep = ""; 899 | if(fileNamePrep != "") { 900 | namePrep = fileNamePrep + configOptions.fileNameSep; 901 | } 902 | var thumbs = htmlDoc.querySelectorAll("a.gallerythumb > img"); 903 | mediaUrl = thumbs[0].src; 904 | mediaUrl = mediaUrl.substring(0, mediaUrl.lastIndexOf("/")+1); 905 | for(var thumb of thumbs) { 906 | var extension = thumb.src.split('.').pop().charAt(0); 907 | pagesInfo.push({t: extension}); 908 | } 909 | coverExtension = getExtension(pagesInfo[0].t); 910 | var obj = { 911 | code: code, 912 | title: title, 913 | artists: artists, 914 | tags: tags, 915 | pages: pages, 916 | mediaUrl: mediaUrl, 917 | namePrep: namePrep, 918 | coverExtension: coverExtension, 919 | pagesInfo: pagesInfo, 920 | } 921 | return obj; 922 | } 923 | 924 | async function addToQueue(item, mediaId=null) { 925 | var pages = item.pages; 926 | var title = item.title; 927 | var namePrep = item.namePrep; 928 | var mediaUrl = item.mediaUrl; 929 | for(let i=0; i 0) { 951 | if(currentDownloads >= configOptions.simulN) { 952 | await sleep(125); 953 | continue; 954 | } 955 | var item = queue.shift(); 956 | download(item); 957 | } 958 | } 959 | 960 | function saveJSON(fileName) { 961 | var data = []; 962 | for(var item of info) { 963 | data.push({ 964 | dirName: item.title, 965 | dirCover: `${item.namePrep}1${item.coverExtension}`, 966 | authors: item.artists, 967 | tags: item.tags, 968 | }); 969 | } 970 | var fileContent = { 971 | 'directories': data 972 | }; 973 | fileContent = JSON.stringify(fileContent, null, 2); 974 | if(configOptions.saveJSONMode == saveJSONModes.CB) { 975 | GM_setClipboard(fileContent, 'text'); 976 | return; 977 | } 978 | fileContent = new TextEncoder().encode(fileContent); 979 | zip.file('', fileName, fileContent.buffer); 980 | } 981 | 982 | function generateZip() { 983 | var dateName = Date.now(); 984 | var compressionType = configOptions.compressionLevel == 0 985 | ? 'STORE' 986 | : 'DEFLATE'; 987 | var zipName = `${dateName}.zip`; 988 | var jsonName = `${dateName}.json`; 989 | if(configOptions.saveJSONMode != saveJSONModes.NO) { 990 | saveJSON(jsonName); 991 | } 992 | zip.generateAsync( 993 | { 994 | type: 'blob', 995 | compression: compressionType, 996 | compressionOptions: { 997 | level: configOptions.compressionLevel, 998 | }}, 999 | ({workerId, percent, currentFile}) => { 1000 | var fraction = percent / 100; 1001 | if(fraction == 1) { 1002 | enableButton(); 1003 | return; 1004 | } 1005 | downloadPercent.innerHTML = `${percent.toFixed(2)}%`; 1006 | compressingSpan.style.width = fraction * downloadBtn.offsetWidth + 'px'; 1007 | } 1008 | ) 1009 | .then((url) => { 1010 | var a = createNode('a'); 1011 | a.download = zipName; 1012 | a.href = url; 1013 | a.click(); 1014 | }) 1015 | .then(() => { 1016 | info = []; 1017 | sessionStorage.removeItem('info'); 1018 | downloading = false; 1019 | }); 1020 | } 1021 | 1022 | function download(item) { 1023 | currentDownloads++; 1024 | const fileName = `${item.namePrep}${item.page}${item.extension}`; 1025 | GM_xmlhttpRequest({ 1026 | method: 'GET', 1027 | url: item.url, 1028 | responseType: 'arraybuffer', 1029 | onload: (response) => { 1030 | if(cancelled) return; 1031 | var data = response.response; 1032 | zip.file(item.title, fileName, data); 1033 | 1034 | currentDownloads--; 1035 | 1036 | downloaded++; 1037 | var fraction = downloaded/total; 1038 | downloadPercent.innerHTML = `${(fraction * 100).toFixed(2)}%`; 1039 | downloadingSpan.style.width = fraction * downloadBtn.offsetWidth + 'px'; 1040 | 1041 | if(queue.length == 0 && currentDownloads <= 0) { 1042 | disableButton(btnStates.downloading); 1043 | generateZip(); 1044 | } 1045 | }, 1046 | onerror: (error) => { 1047 | currentDownloads--; 1048 | console.warn(`Could not download '${item.title}' - page ${item.page}`); 1049 | } 1050 | }); 1051 | } 1052 | 1053 | function sleep(ms) { 1054 | return new Promise((resolve) => setTimeout(resolve, ms)); 1055 | } 1056 | --------------------------------------------------------------------------------