├── .npmrc ├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── codeql.yml ├── .eslintignore ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── docs ├── app-items.png ├── sample-app.jpg ├── 1639646197537.gif ├── beta-cmd-loader.png ├── _config.yml └── index.md ├── resources ├── icon.ico ├── 128x128.png ├── 16x16.png ├── 256x256.png ├── 32x32.png ├── 64x64.png └── icon.icns ├── .prettierignore ├── dev-app-update.yml ├── .editorconfig ├── src ├── main │ ├── plugins │ │ ├── lib │ │ │ ├── parse-image.js │ │ │ ├── config.js │ │ │ ├── request.js │ │ │ └── parse-manga.js │ │ ├── ex │ │ │ ├── logout.js │ │ │ ├── gallery.js │ │ │ └── login.js │ │ ├── config.js │ │ ├── events.js │ │ └── ehentai.js │ └── index.js ├── renderer │ ├── src │ │ ├── main.js │ │ ├── components │ │ │ └── Versions.svelte │ │ ├── App.svelte │ │ └── assets │ │ │ ├── base.css │ │ │ ├── wavy-lines.svg │ │ │ └── main.css │ └── index.html └── preload │ └── index.js ├── .prettierrc.yaml ├── .eslintrc.cjs ├── SECURITY.md ├── electron.vite.config.mjs ├── _config.yml ├── CONTRIBUTING.md ├── LICENSE ├── electron-builder.yml ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dvgamerr 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/app-items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/docs/app-items.png -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /docs/sample-app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/docs/sample-app.jpg -------------------------------------------------------------------------------- /resources/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/128x128.png -------------------------------------------------------------------------------- /resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/16x16.png -------------------------------------------------------------------------------- /resources/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/256x256.png -------------------------------------------------------------------------------- /resources/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/32x32.png -------------------------------------------------------------------------------- /resources/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/64x64.png -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /docs/1639646197537.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/docs/1639646197537.gif -------------------------------------------------------------------------------- /docs/beta-cmd-loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvgamerr-app/hentai-downloader/HEAD/docs/beta-cmd-loader.png -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://example.com/auto-updates 3 | updaterCacheDirName: hentai-downloader-updater 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/main/plugins/lib/parse-image.js: -------------------------------------------------------------------------------- 1 | export default html => { 2 | let gdtm = html.match(/gdtm".*?/ig) || [] 3 | return gdtm.map(item => /href="(?.*?)"/ig.exec(item).groups.link) 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import App from './App.svelte' 4 | 5 | const app = new App({ 6 | target: document.getElementById('app') 7 | }) 8 | 9 | export default app 10 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | plugins: 6 | - prettier-plugin-svelte 7 | overrides: 8 | - files: '*.svelte' 9 | options: 10 | parser: svelte 11 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | 2 | title: Hentai Downloader 3 | description: how to download manga from exhentai.org or e-hentai.org 4 | google_analytics: UA-81630553-1 5 | show_downloads: true 6 | theme: jekyll-theme-minimal 7 | 8 | gems: 9 | - jekyll-mentions 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '00:00' 8 | commit-message: 9 | prefix: fix 10 | prefix-development: chore 11 | include: scope 12 | -------------------------------------------------------------------------------- /src/main/plugins/ex/logout.js: -------------------------------------------------------------------------------- 1 | import request from '../lib/request' 2 | 3 | export default async () => { 4 | if (!global._appToken.step03) return 5 | 6 | console.log(`[FORUMS] logout-get: ${global._appToken.step03}`) 7 | let html = await request('GET', global._appToken.step03) 8 | return /redirectwrap[\W\w]*?

Thanks/ig.test(html) 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/src/components/Versions.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | extraFileExtensions: ['.svelte'] 4 | }, 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:svelte/recommended', 8 | '@electron-toolkit', 9 | '@electron-toolkit/eslint-config-prettier' 10 | ], 11 | rules: { 12 | 'svelte/no-unused-svelte-ignore': 'off' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[svelte]": { 12 | "editor.defaultFormatter": "svelte.svelte-vscode" 13 | }, 14 | "svelte.enable-ts-plugin": true, 15 | "eslint.validate": [ 16 | "javascript", 17 | "javascriptreact", 18 | "svelte" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 2.2.0 | :white_check_mark: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Use this section to tell people how to report a vulnerability. 15 | 16 | Tell them where to go, how often they can expect to get an update on a 17 | reported vulnerability, what to expect if the vulnerability is accepted or 18 | declined, etc. 19 | -------------------------------------------------------------------------------- /electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | export default defineConfig({ 5 | main: { 6 | plugins: [externalizeDepsPlugin()] 7 | }, 8 | preload: { 9 | plugins: [externalizeDepsPlugin()] 10 | }, 11 | renderer: { 12 | plugins: [ 13 | svelte({ 14 | compilerOptions: { 15 | // keep old `new Component({ target })` API working (Svelte 4 behavior) 16 | compatibility: { componentApi: 4 } 17 | } 18 | }) 19 | ] 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/preload/index.js: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | // Custom APIs for renderer 5 | const api = {} 6 | 7 | // Use `contextBridge` APIs to expose Electron APIs to 8 | // renderer only if context isolation is enabled, otherwise 9 | // just add to the DOM global. 10 | if (process.contextIsolated) { 11 | try { 12 | contextBridge.exposeInMainWorld('electron', electronAPI) 13 | contextBridge.exposeInMainWorld('api', api) 14 | } catch (error) { 15 | console.error(error) 16 | } 17 | } else { 18 | window.electron = electronAPI 19 | window.api = api 20 | } 21 | -------------------------------------------------------------------------------- /src/main/plugins/config.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | 3 | const DevMode = process.env.NODE_ENV === 'development' 4 | const endpoint = DevMode ? 'http://localhost:3000' : 'https://opensource.api-v2.touno.io/' 5 | const token = 'JJpeNu1VAXuHk505.app-exhentai' 6 | export default { 7 | DevMode, 8 | endpoint, 9 | token, 10 | api: (data) => { 11 | try { 12 | return request.defaults({ 13 | method: 'POST', 14 | baseUrl: endpoint, 15 | timeout: 5000, 16 | json: true, 17 | headers: { 'X-Token': token, 'X-Access': +new Date() } 18 | })(data) 19 | } catch (ex) { 20 | return () => ({ }) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/main/plugins/lib/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const tough = require('tough-cookie') 3 | const dir = './config' 4 | 5 | const fileCookie = `${dir}/.cookie` 6 | const fileToken = `${dir}/.token` 7 | 8 | if (!fs.existsSync(dir)) fs.mkdirSync(dir) 9 | 10 | export const saveCookie = (jar) => { 11 | fs.writeFileSync(fileCookie, JSON.stringify(jar.serializeSync())) 12 | } 13 | 14 | export const loadCookie = () => { 15 | const cookie = new tough.CookieJar() 16 | // if (fs.existsSync(fileCookie)) { 17 | // cookie.jar = tough.CookieJar.deserializeSync(JSON.parse(fs.readFileSync(fileCookie))) 18 | // } 19 | return cookie 20 | } 21 | 22 | export const loadToken = () => { 23 | return fs.existsSync(fileToken) ? tough.CookieJar.deserializeSync(JSON.parse(fs.readFileSync(fileToken))) : {} 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/App.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
Powered by electron-vite
8 |
9 | Build an Electron app with 10 | Svelte 11 |
12 |

Please try pressing F12 to open the devTool

13 |
14 | 17 |
18 | 19 | Send IPC 20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hentai Downloader v3.0.0 8 | 9 | 13 | 14 | 15 | 16 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: Hentai Downloader 2 | repo: dvgamerr-app/hentai-downloader 3 | version: v2.2.1 4 | author: 5 | username: dvgamerr 6 | name: Kananek T. 7 | website: https://dvgamerr.app/ 8 | 9 | description: > 10 | ExHentai exhentai.org, e-hentai.org images gallery download to folder. 11 | 12 | # Build settings 13 | cronitor_token: a08df15d735c0c7518246129767fd319 14 | 15 | # theme: dvgamerr/minima-lite 16 | baseurl: /hentai-downloader 17 | remote_theme: dvgamerr/minima-lite 18 | 19 | plugins: 20 | - jekyll-feed 21 | - jekyll-seo-tag 22 | 23 | minima: 24 | date_format: "%-d %b %Y" 25 | social_links: 26 | - { platform: devto, user_url: "https://dev.to/dvgamerr" } 27 | - { platform: facebook, user_url: "https://www.facebook.com/dvgamerr" } 28 | - { platform: x, user_url: "https://x.com/dvgamerr" } 29 | - { platform: youtube, user_url: "https://www.youtube.com/channel/UCbMNge3pQHkb3avJvUj4hLA" } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2018 Touno™ 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. -------------------------------------------------------------------------------- /src/main/plugins/ex/gallery.js: -------------------------------------------------------------------------------- 1 | import request from '../lib/request' 2 | 3 | export default async (uri) => { 4 | try { 5 | uri = uri instanceof URL ? uri : new URL(uri.trim().replace(/&/g, '&')) 6 | 7 | if (!/\/\w{1}\/\d{1,8}\/[0-9a-f]+?\//ig.test(uri.pathname)) throw new Error(`Key missing, or incorrect key provided.`) 8 | if (/^ex/i.test(uri.hostname) && global._appToken.name) throw new Error(`Session not login.`) 9 | 10 | let html = await request('GET', uri.toString()) 11 | if (!/DOCTYPE.html.PUBLIC/ig.test(html)) throw new Error(html) 12 | let [ , warnMe ] = /Never Warn Me Again/ig.exec(html) || [] 13 | if (warnMe) { 14 | // throw new Error('Never Warn Me Again') 15 | html = await request('GET', warnMe[1], {}, { 'referer': `${uri.protocol}//${uri.hostname}` }) 16 | } 17 | return html 18 | } catch (ex) { 19 | if (ex.statusCode === 404) { 20 | if (ex.error) { 21 | // logs('hentai-downloader', `*rare*: https://${uri.hostname}${uri.pathname}`) 22 | throw new Error('This gallery has been removed or is unavailable.') 23 | } else { 24 | // logs('hentai-downloader', `*error*: ${link}\n${ex.name.toString()}`) 25 | throw new Error(ex.name) 26 | } 27 | } else { 28 | // logs('hentai-downloader', `*error*: ${link}\n${ex.toString()}`) 29 | throw ex 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: hentai-downloader 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | asarUnpack: 12 | - resources/** 13 | win: 14 | executableName: hentai-downloader 15 | nsis: 16 | artifactName: ${name}-${version}-setup.${ext} 17 | shortcutName: ${productName} 18 | uninstallDisplayName: ${productName} 19 | createDesktopShortcut: always 20 | mac: 21 | entitlementsInherit: build/entitlements.mac.plist 22 | extendInfo: 23 | - NSCameraUsageDescription: Application requests access to the device's camera. 24 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 25 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 26 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 27 | notarize: false 28 | dmg: 29 | artifactName: ${name}-${version}.${ext} 30 | linux: 31 | target: 32 | - AppImage 33 | - snap 34 | - deb 35 | maintainer: electronjs.org 36 | category: Utility 37 | appImage: 38 | artifactName: ${name}-${version}.${ext} 39 | npmRebuild: false 40 | publish: 41 | provider: generic 42 | url: https://example.com/auto-updates 43 | -------------------------------------------------------------------------------- /src/renderer/src/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ev-c-white: #ffffff; 3 | --ev-c-white-soft: #f8f8f8; 4 | --ev-c-white-mute: #f2f2f2; 5 | 6 | --ev-c-black: #1b1b1f; 7 | --ev-c-black-soft: #222222; 8 | --ev-c-black-mute: #282828; 9 | 10 | --ev-c-gray-1: #515c67; 11 | --ev-c-gray-2: #414853; 12 | --ev-c-gray-3: #32363f; 13 | 14 | --ev-c-text-1: rgba(255, 255, 245, 0.86); 15 | --ev-c-text-2: rgba(235, 235, 245, 0.6); 16 | --ev-c-text-3: rgba(235, 235, 245, 0.38); 17 | 18 | --ev-button-alt-border: transparent; 19 | --ev-button-alt-text: var(--ev-c-text-1); 20 | --ev-button-alt-bg: var(--ev-c-gray-3); 21 | --ev-button-alt-hover-border: transparent; 22 | --ev-button-alt-hover-text: var(--ev-c-text-1); 23 | --ev-button-alt-hover-bg: var(--ev-c-gray-2); 24 | } 25 | 26 | :root { 27 | --color-background: var(--ev-c-black); 28 | --color-background-soft: var(--ev-c-black-soft); 29 | --color-background-mute: var(--ev-c-black-mute); 30 | 31 | --color-text: var(--ev-c-text-1); 32 | } 33 | 34 | *, 35 | *::before, 36 | *::after { 37 | box-sizing: border-box; 38 | margin: 0; 39 | font-weight: normal; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | body { 47 | min-height: 100vh; 48 | color: var(--color-text); 49 | background: var(--color-background); 50 | line-height: 1.6; 51 | font-family: 52 | Inter, 53 | -apple-system, 54 | BlinkMacSystemFont, 55 | 'Segoe UI', 56 | Roboto, 57 | Oxygen, 58 | Ubuntu, 59 | Cantarell, 60 | 'Fira Sans', 61 | 'Droid Sans', 62 | 'Helvetica Neue', 63 | sans-serif; 64 | text-rendering: optimizeLegibility; 65 | -webkit-font-smoothing: antialiased; 66 | -moz-osx-font-smoothing: grayscale; 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/assets/wavy-lines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Hentai Downloader `2.2.0` 2 | 3 | ![Travis](https://img.shields.io/travis/touno-io/hentai-downloader.svg?style=flat-square) 4 | ![GitHub top language](https://img.shields.io/github/languages/top/badges/shields.svg?style=flat-square) 5 | ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/touno-io/hentai-downloader/master.svg?style=flat-square) 6 | ![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square) 7 | 8 | ## Overview 9 | โปรแกรมดาวน์โหลดมังงะจาก `e-hentai.org` ได้โดยไม่ต้องปั้ม Credits และสามารถเพิ่มคิวได้เรื่อยๆ และตรวจจับ clipboard ได้จากการ copy และ paste ที่หน้าโปรแกรม ในรูปแบบ บรรทัดละเรื่อง 10 | 11 | **ข้อจำกัด** 12 | - ระบบจะเริ่มดาวน์โหลดทีละเรื่องที่ละไฟล์เท่านั้น (เป็นข้อจำกัดของตัวเว็บไซต์เอง) 13 | 14 | ### Features 15 | - auto-update version 16 | - `exhentai.org` download manga. 17 | - addon login for `exhentai.org` rare. 18 | - save last user queue. 19 | - list history user page. 20 | 21 | ### How to use 22 | - copy link จากเว็บ `e-hentai.org` ใส่ในช่อง url แล้วกด Enter 23 | - ถ้าจะเพิ่ม Queue ก็แค่ copy link มาใส่เพิ่ม 24 | - หรือ copy ลิ้งแบบหลายบรรทัดมาแล้ว paste ที่หน้าโปรแกรม **ตัวอย่างแบบหลายบรรทัด** 25 | 26 | ``` 27 | https://e-hentai.org/g/1165997/855db7b447/ 28 | https://e-hentai.org/g/1161024/4cf43275bc/ 29 | https://e-hentai.org/g/1160960/81818b89fe/ 30 | https://e-hentai.org/g/1132634/ddc026cba5/ 31 | https://e-hentai.org/g/1109336/bec482d462/ 32 | https://e-hentai.org/g/1215542/a443972290/ 33 | ``` 34 | 35 | - กดปุ่ม folder เพื่อเลือก path ที่ต้องการจะ save ไฟล์ลง 36 | - กดปุ่ม download เพื่อเริ่มดาวน์โหลดมังงะ 37 | - **คำเตือน** ไม่ควรเปิดเว็บ `e-hentai.org` ระหว่างที่ดาวน์โหลดมังงะเพราะจะทำให้รูปบางรูปอาจจะโหลดไม่สำเร็จ 38 | 39 | ![Icon][icons] 40 | 41 | 42 | ![app][app-items] 43 | 44 | [icons]: https://raw.githubusercontent.com/unhax/ghentai-downloader/master/build/icons/256x256.png 45 | [app-items]: https://raw.githubusercontent.com/unhax/ghentai-downloader/master/docs/app-items.png 46 | 47 | ### License 48 | MIT © 2018 Touno™ 49 | -------------------------------------------------------------------------------- /src/main/plugins/ex/login.js: -------------------------------------------------------------------------------- 1 | import request from '../lib/request' 2 | 3 | const fs = require('fs') 4 | 5 | export default async (username, password) => { 6 | const step00 = `https://forums.e-hentai.org/index.php?act=Login&CODE=00` 7 | const header01 = { 8 | 'accept-encoding': 'gzip, deflate, br', 9 | 'cache-control': 'max-age=0', 10 | 'content-type': 'application/x-www-form-urlencoded' 11 | } 12 | 13 | console.log(`[FORUMS] login-form: ${step00}`) 14 | let html = await request('GET', step00) 15 | let [ , step01 ] = /form action="(.*?)"/ig.exec(html) || [] 16 | if (!step01) throw new Error(html) 17 | 18 | console.log(`[FORUMS] login-post: ${step01}`) 19 | html = await request('POST', step01, { 20 | referer: `https://${new URL(step01).hostname}/`, 21 | b: '', 22 | bt: '', 23 | UserName: username, 24 | PassWord: password, 25 | CookieDate: 1 26 | }, header01) 27 | 28 | let [ , step02 ] = /redirectfoot.*?href="(.*?)"/ig.exec(html) || [] 29 | let [ , nickname ] = /You.are.now.logged.in.as:(.*?)(.*?)<[\W\w]*?src='(?.*?)'[\W\w]*Member Group:(?.*?)<[\W\w]*?Joined:(?[\W\w]*?) { 9 | // uri = uri.trim().replace(/&/g, '&') 10 | // let base = uri instanceof URL ? uri : new URL(uri) 11 | // let referer = `https://${base.hostname}/` 12 | // let httpHeaders = Object.assign({ 13 | // 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 14 | // 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 15 | // 'accept-language': 'th-TH,th;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6,ru;q=0.5,zh-TW;q=0.4,zh;q=0.3', 16 | // 'cache-control': 'no-cache', 17 | // 'pragma': 'no-cache', 18 | // ':authority': base.hostname, 19 | // ':scheme': 'https', 20 | // 'referer': referer 21 | // }, addHeaders) 22 | // // let request = xhr.defaults({ timeout: 5000, jar: getCookie() }) 23 | // return axios(Object.assign({ 24 | // url: uri, 25 | // method: method || 'GET', 26 | // withCredentials: true, 27 | // timeout: 5000 28 | // }, options)).then((res) => { 29 | // return jarCookieCheck().then(() => res) 30 | // }) 31 | 32 | // // return new Promise((resolve, reject) => { 33 | // // const callback = (error, res, body) => { 34 | // // if (error) { 35 | // // return reject(error) 36 | // // } 37 | // // const { statusCode } = res 38 | // // if (([ 200, 302 ]).indexOf(statusCode) < 0) return reject(statusCode) 39 | 40 | // // // setCookie(referer, headers) 41 | // // resolve(body) 42 | // // } 43 | 44 | // // const options = { 45 | // // url: uri, 46 | // // method: method, 47 | // // header: httpHeaders, 48 | // // formData: data 49 | // // } 50 | // // request(options, callback) 51 | 52 | // // }) 53 | // } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hentai-downloader", 3 | "title": "Hentai Downloader", 4 | "version": "3.0.0", 5 | "description": "Manga Hentai in e-hentai.org and exhentai.org downloader.", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/touno-io/hentai-downloader.git" 9 | }, 10 | "keywords": [ 11 | "exhentai", 12 | "e-hentai", 13 | "hentai", 14 | "downloader", 15 | "manga", 16 | "dark", 17 | "doujin" 18 | ], 19 | "bugs": { 20 | "url": "https://github.com/touno-io/hentai-downloader/issues" 21 | }, 22 | "homepage": "https://github.com/touno-io/hentai-downloader#readme", 23 | "author": { 24 | "name": "Kananek Thongkam", 25 | "email": "info.dvgamer@gmail.com" 26 | }, 27 | "license": "MIT", 28 | "main": "./out/main/index.js", 29 | "scripts": { 30 | "format": "prettier --plugin prettier-plugin-svelte --write .", 31 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 32 | "start": "electron-vite preview", 33 | "dev": "electron-vite dev", 34 | "build": "electron-vite build", 35 | "postinstall": "electron-builder install-app-deps", 36 | "build:unpack": "npm run build && electron-builder --dir", 37 | "build:win": "npm run build && electron-builder --win", 38 | "build:mac": "npm run build && electron-builder --mac", 39 | "build:linux": "npm run build && electron-builder --linux" 40 | }, 41 | "dependencies": { 42 | "@electron-toolkit/preload": "^3.0.2", 43 | "@electron-toolkit/utils": "4.0.0", 44 | "electron-updater": "^6.6.2" 45 | }, 46 | "devDependencies": { 47 | "@electron-toolkit/eslint-config": "2.1.0", 48 | "@electron-toolkit/eslint-config-prettier": "3.0.0", 49 | "@sveltejs/vite-plugin-svelte": "6.1.2", 50 | "electron": "37.3.0", 51 | "electron-builder": "26.0.19", 52 | "electron-log": "^5.4.2", 53 | "electron-settings": "5.0.0", 54 | "electron-vite": "4.0.0", 55 | "eslint": "^9.33.0", 56 | "eslint-plugin-svelte": "3.11.0", 57 | "prettier": "^3.6.2", 58 | "prettier-plugin-svelte": "^3.4.0", 59 | "sass": "^1.90.0", 60 | "svelte": "5.38.1", 61 | "svelte-preprocess": "6.0.3", 62 | "svelte-routing": "^2.13.0", 63 | "vite": "6.3.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '44 15 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /src/main/plugins/lib/parse-manga.js: -------------------------------------------------------------------------------- 1 | import parseImage from './parse-image' 2 | 3 | export default (uri, html) => { 4 | uri = uri instanceof URL ? uri : new URL(uri.trim().replace(/&/g, '&')) 5 | const [ gallery ] = /\/\w{1}\/\d{1,8}\/[0-9a-f]+?\//ig.exec(uri.pathname) || [] 6 | uri = new URL(`${uri.protocol}//${uri.hostname}${gallery}`) 7 | 8 | const [ , name ] = /
.*?gn">(.*?)<\/.*?gj">(.*?)<\/.*?<\/div>/ig.exec(html) || [] 9 | const [ , language ] = /Language:.*?class="gdt2">(.*?)&/ig.exec(html) || [] 10 | const [ , size ] = /File Size:.*?class="gdt2">(.*?)(.*?).page/ig.exec(html) || [] 12 | const [ , cover ] = /
.*?url\((.*?)\)/ig.exec(html) || [] 13 | 14 | if (!name) throw new Error('manga.name is undefined.') 15 | if (!language) throw new Error('manga.language is undefined.') 16 | if (!size) throw new Error('manga.size is undefined.') 17 | if (!length) throw new Error('manga.page is undefined.') 18 | if (!cover) throw new Error('manga.cover is undefined.') 19 | 20 | return { 21 | ref: gallery, 22 | url: uri.toString(), 23 | name: name.trim(), 24 | cover: cover.trim(), 25 | language: language.trim(), 26 | size: size.trim(), 27 | page: length.trim(), 28 | items: parseImage(html) 29 | } 30 | // // let config = settings.get('config') 31 | // // exHentaiHistory('exhentai/manga', { 32 | // // user_id: config.user_id, 33 | // // name: manga.name, 34 | // // link: manga.url, 35 | // // cover: manga.cover, 36 | // // language: manga.language, 37 | // // size: manga.size, 38 | // // page: manga.page 39 | // // }) 40 | // // slack(baseUrl.host, manga) 41 | // getImage(manga, res) 42 | // console.log(manga) 43 | // let totalPage = Math.ceil(manga.page / manga.items.length) 44 | // emit.send('INIT_MANGA', { page: 1, total: totalPage }) 45 | // if (manga.items.length !== manga.page) { 46 | // let all = [] 47 | // for (let i = 1; i < totalPage; i++) { 48 | // all.push(() => { 49 | // emit.send('INIT_MANGA', { page: i + 1, total: totalPage }) 50 | // return request(`${link}?p=${i}`).then((res) => getImage(manga, res)) 51 | // }) 52 | // } 53 | // return async.series(all).then(() => { 54 | // // request({ 55 | // // url: link, 56 | // // header: { 57 | // // 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 58 | // // 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 59 | // // 'accept-language': 'th-TH,th;q=0.8,en-US;q=0.6,en;q=0.4,ja;q=0.2', 60 | // // 'cache-control': 'no-cache', 61 | // // 'pragma': 'no-cache', 62 | // // 'referer': `https://${baseUrl.hostname}/`, 63 | // // 'upgrade-insecure-requests': '1' 64 | // // } 65 | // // }) 66 | // if (manga.items.length !== parseInt(manga.page)) throw new Error(`manga.items is '${manga.items.length}' and length is '${manga.page}'`) 67 | // return manga 68 | // }).then(() => { 69 | // return exHentaiHistory('/exhentai', {}) 70 | // }) 71 | // } else { 72 | // exHentaiHistory('/exhentai', {}) 73 | // return manga 74 | // } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hentai Downloader ![Version](https://img.shields.io/github/package-json/v/touno-io/hentai-downloader?label=latest&style=flat-square) 2 | 3 | ![Build (main)](https://img.shields.io/github/workflow/status/touno-io/hentai-downloader/Multiplatform%20Build/main?style=flat-square) 4 | ![last commit (main)](https://img.shields.io/github/last-commit/touno-io/hentai-downloader/main.svg?style=flat-square) 5 | ![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square) 6 | ![Node](https://img.shields.io/badge/node-10.20.1-green?style=flat-square) 7 | 8 | ![app](./docs/1639646197537.gif) 9 | 10 | **คำเตือน** ไม่ควรเปิดเว็บแพนด้าเศร้า ระหว่างที่ดาวน์โหลดมังงะเพราะจะทำให้รูปบางรูปอาจจะโหลดไม่สำเร็จ 11 | 12 | ## Community 13 | - [Discord](https://touno.io/s/ixj7) 14 | 15 | ## Overview 16 | โปรแกรมดาวน์โหลดมังงะจากแพนด้าเศร้าได้โดยไม่ต้องปั้ม Credits สามารถเพิ่มคิวได้เรื่อยๆ ตรวจจับ clipboard ได้จากการ copy ที่หน้าโปรแกรมได้ที่ละเรื่องหรือ หลายเรื่องพร้อมกันก็ได้ 17 | 18 | ![Icon](./build/icons/256x256.png) 19 | 20 | **ข้อจำกัด** 21 | - ระบบจะดาวน์โหลดทีละเรื่องที่ละไฟล์เท่านั้น `(เป็นข้อจำกัดของตัวเว็บไซต์เอง และ ป้องกันการโดนแบน)` 22 | - ระบบไม่ตั้งชื่อตามชื่อไฟล์เดิมที่ download แต่จะตั้งชื่อใหม่เป็นเลขหน้าแทน 23 | 24 | ### Features 25 | - รับรอง `exhentai.org` และ `e-hentai.org` เท่านั้น 26 | - แก้ไฟล์เสียได้จาก redownload 27 | - บันทึก queue ให้ หากยังไม่ได้ download 28 | 29 | ### วิธิใช้ 30 | - copy link จากเว็บ `e-hentai.org` ใส่ในช่อง url แล้วกด Enter 31 | - ถ้าจะเพิ่ม Queue ก็แค่ copy link มาใส่เพิ่ม 32 | - หรือ copy ลิ้งแบบหลายบรรทัดมาแล้ว paste ที่หน้าโปรแกรม **ตัวอย่างแบบหลายบรรทัด** 33 | 34 | ``` 35 | https://e-hentai.org/g/1161024/4cf43275bc/ 36 | https://e-hentai.org/g/1160960/81818b89fe/ 37 | https://e-hentai.org/g/1132634/ddc026cba5/ 38 | https://e-hentai.org/g/1109336/bec482d462/ 39 | ``` 40 | 41 | - กดปุ่ม folder เพื่อเลือก path ที่ต้องการจะ save ไฟล์ลง 42 | - กดปุ่ม download เพื่อเริ่มดาวน์โหลดมังงะ 43 | 44 | ## What's Changed 45 | - แก้ปัญหาโหลดไฟล์ไม่สมบูรณ์ด้วยการลบไฟล์ที่มีปัญหาทิ้ง แล้วกด Download อีกครั้ง โปรแกรมจะโหลดใหม่จะไฟล์ที่ขาดไปให้ใหม่ 46 | - `exhentai.org` via `cookie` download supported. 47 | 48 | ![image](https://user-images.githubusercontent.com/10203425/146333356-656c53de-37ee-4118-b9c3-6ddc7ca7caf9.png) 49 | 50 | - Awaly on Top with try icon. 51 | - Watch clipboard and parse manga. 52 | - New Button Join `Discord Community` 53 | - New Button Link `Donate` 54 | - New Button `Setting` and `History` 55 | 56 | ### Changelog `v2.2.0` 57 | - Addon script join your session exhentai.org. 58 | - Update UI 59 | - Update electron `v3` to `v11` 60 | - Change request-promise module to axios module. 61 | - Fixed cookie jar to tough-cookie. 62 | - Fixed headers sending with SSL 63 | - Fixed SSL Verify with download images. 64 | - Fixed cookie show error from UI 65 | - Fixed Can't load `ex` and `e-` at the same time. 66 | 67 | ## Project Setup 68 | 69 | ### Install 70 | 71 | ```bash 72 | $ pnpm install 73 | ``` 74 | 75 | ### Development 76 | 77 | ```bash 78 | $ pnpm dev 79 | ``` 80 | 81 | ### Build 82 | 83 | ```bash 84 | # For windows 85 | $ pnpm build:win 86 | 87 | # For macOS 88 | $ pnpm build:mac 89 | 90 | # For Linux 91 | $ pnpm build:linux 92 | ``` 93 | 94 | ## If you need help You can join Discord. 95 | 96 | [![Join Us?](https://discordapp.com/api/guilds/475720106471849996/widget.png?style=banner2)](https://discord.gg/QDccF497Mw) 97 | 98 | 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [dvgamerr@gmail.com]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version v2.2.0, 71 | available at [https://github.com/touno-io/hentai-downloader/releases/tag/v2.2.0][version] 72 | 73 | [homepage]: https://touno-io.github.io/hentai-downloader/ 74 | [version]: https://github.com/touno-io/hentai-downloader/releases/tag/v2.2.0 75 | -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | body { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | overflow: hidden; 8 | background-image: url('./wavy-lines.svg'); 9 | background-size: cover; 10 | user-select: none; 11 | } 12 | 13 | code { 14 | font-weight: 600; 15 | padding: 3px 5px; 16 | border-radius: 2px; 17 | background-color: var(--color-background-mute); 18 | font-family: 19 | ui-monospace, 20 | SFMono-Regular, 21 | SF Mono, 22 | Menlo, 23 | Consolas, 24 | Liberation Mono, 25 | monospace; 26 | font-size: 85%; 27 | } 28 | 29 | #app { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | margin-bottom: 80px; 35 | } 36 | 37 | .logo { 38 | margin-bottom: 20px; 39 | -webkit-user-drag: none; 40 | height: 128px; 41 | width: 128px; 42 | will-change: filter; 43 | transition: filter 300ms; 44 | } 45 | 46 | .logo:hover { 47 | filter: drop-shadow(0 0 1.2em #6988e6aa); 48 | } 49 | 50 | .creator { 51 | font-size: 14px; 52 | line-height: 16px; 53 | color: var(--ev-c-text-2); 54 | font-weight: 600; 55 | margin-bottom: 10px; 56 | } 57 | 58 | .text { 59 | font-size: 28px; 60 | color: var(--ev-c-text-1); 61 | font-weight: 700; 62 | line-height: 32px; 63 | text-align: center; 64 | margin: 0 10px; 65 | padding: 16px 0; 66 | } 67 | 68 | .tip { 69 | font-size: 16px; 70 | line-height: 24px; 71 | color: var(--ev-c-text-2); 72 | font-weight: 600; 73 | } 74 | 75 | .svelte { 76 | background: -webkit-linear-gradient(315deg, #ff3e00 35%, #647eff); 77 | background-clip: text; 78 | -webkit-background-clip: text; 79 | -webkit-text-fill-color: transparent; 80 | font-weight: 700; 81 | } 82 | 83 | .actions { 84 | display: flex; 85 | padding-top: 32px; 86 | margin: -6px; 87 | flex-wrap: wrap; 88 | justify-content: flex-start; 89 | } 90 | 91 | .action { 92 | flex-shrink: 0; 93 | padding: 6px; 94 | } 95 | 96 | .action a { 97 | cursor: pointer; 98 | text-decoration: none; 99 | display: inline-block; 100 | border: 1px solid transparent; 101 | text-align: center; 102 | font-weight: 600; 103 | white-space: nowrap; 104 | border-radius: 20px; 105 | padding: 0 20px; 106 | line-height: 38px; 107 | font-size: 14px; 108 | border-color: var(--ev-button-alt-border); 109 | color: var(--ev-button-alt-text); 110 | background-color: var(--ev-button-alt-bg); 111 | } 112 | 113 | .action a:hover { 114 | border-color: var(--ev-button-alt-hover-border); 115 | color: var(--ev-button-alt-hover-text); 116 | background-color: var(--ev-button-alt-hover-bg); 117 | } 118 | 119 | .versions { 120 | position: absolute; 121 | bottom: 30px; 122 | margin: 0 auto; 123 | padding: 15px 0; 124 | font-family: 'Menlo', 'Lucida Console', monospace; 125 | display: inline-flex; 126 | overflow: hidden; 127 | align-items: center; 128 | border-radius: 22px; 129 | background-color: #202127; 130 | backdrop-filter: blur(24px); 131 | } 132 | 133 | .versions li { 134 | display: block; 135 | float: left; 136 | border-right: 1px solid var(--ev-c-gray-1); 137 | padding: 0 20px; 138 | font-size: 14px; 139 | line-height: 14px; 140 | opacity: 0.8; 141 | &:last-child { 142 | border: none; 143 | } 144 | } 145 | 146 | @media (max-width: 720px) { 147 | .text { 148 | font-size: 20px; 149 | } 150 | } 151 | 152 | @media (max-width: 620px) { 153 | .versions { 154 | display: none; 155 | } 156 | } 157 | 158 | @media (max-width: 350px) { 159 | .tip, 160 | .actions { 161 | display: none; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Tray, Menu, shell, screen } from 'electron' 2 | import { join } from 'path' 3 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 4 | import settings from 'electron-settings' 5 | import log from 'electron-log/main' 6 | import icon from '../../resources/256x256.png?asset' 7 | import icoTray from '../../resources/16x16.png?asset' 8 | const { name } = '../../package.json?asset' 9 | 10 | // Optional, initialize the log for any renderer process 11 | log.initialize() 12 | 13 | const onClick = (e) => { 14 | console.log(e) 15 | } 16 | 17 | settings.configure({ fileName: `${name}.json`, prettify: true }) 18 | log.log('settings-loaded', settings.SettingsObject) 19 | log.log('env:', process.env.NODE_ENV) 20 | log.log('App:', app.getName()) 21 | 22 | const mainApp = { 23 | url: `file://${__dirname}/index.html`, 24 | // // eslint-disable-next-line no-undef 25 | // url: MAIN_WINDOW_WEBPACK_ENTRY, 26 | win: null, 27 | tray: null, 28 | config: {}, 29 | width: 600, 30 | height: 380 31 | } 32 | 33 | function createWindow() { 34 | mainApp.config = { 35 | width: mainApp.width, 36 | height: mainApp.height, 37 | minWidth: mainApp.width, 38 | minHeight: mainApp.height, 39 | maxWidth: mainApp.width, 40 | maxHeight: mainApp.height, 41 | title: app.getName(), 42 | show: true, 43 | movable: false, 44 | resizable: false, 45 | alwaysOnTop: true, 46 | skipTaskbar: false, 47 | transparent: false, 48 | autoHideMenuBar: true, 49 | ...(process.platform === 'linux' ? { icon } : {}), 50 | webPreferences: { 51 | preload: join(__dirname, '../preload/index.js'), 52 | sandbox: false 53 | } 54 | } 55 | 56 | const getPosition = () => { 57 | const padding = 10 58 | if (settings.getSync('ontop', false)) { 59 | const screenSize = screen.getPrimaryDisplay().workAreaSize 60 | if (mainApp.win) { 61 | ;[mainApp.width, mainApp.height] = mainApp.win.getSize() 62 | log.log('screenSize', screenSize, 'width', mainApp.width, 'height', mainApp.height) 63 | } 64 | return { 65 | x: screenSize.width - mainApp.width - padding, 66 | y: screenSize.height - mainApp.height - padding 67 | } 68 | } else { 69 | return settings.getSync('position') 70 | } 71 | } 72 | 73 | mainApp.win = new BrowserWindow(Object.assign(mainApp.config, getPosition())) 74 | mainApp.win.loadURL(mainApp.url) 75 | mainApp.win.setMenu(null) 76 | mainApp.win.setSkipTaskbar(settings.getSync('ontop', false)) 77 | mainApp.win.setAlwaysOnTop(settings.getSync('ontop', false)) 78 | mainApp.win.setMovable(!settings.getSync('ontop', false)) 79 | mainApp.win.setMinimizable(false) 80 | 81 | mainApp.tray = new Tray(icoTray) 82 | let hideWindow = false 83 | 84 | const contextMenu = Menu.buildFromTemplate([ 85 | { 86 | label: 'Always On Top', 87 | sublabel: 'and lock window mode.', 88 | type: 'checkbox', 89 | checked: settings.getSync('ontop', false), 90 | click: (menuItem) => { 91 | settings.set('ontop', menuItem.checked) 92 | mainApp.win.setAlwaysOnTop(menuItem.checked) 93 | mainApp.win.setMovable(!menuItem.checked) 94 | mainApp.win.setSkipTaskbar(menuItem.checked) 95 | // mainApp.win.set 96 | const position = getPosition() 97 | if (position) mainApp.win.setPosition(position.x, position.y) 98 | } 99 | }, 100 | { type: 'separator' }, 101 | { 102 | label: 'Watch Clipboard', 103 | type: 'checkbox', 104 | role: 'toggle-clipboard', 105 | checked: settings.getSync('clipboard', false), 106 | click: onClick 107 | }, 108 | { 109 | label: 'Auto Download', 110 | type: 'checkbox', 111 | role: 'auto-dl', 112 | visible: false, 113 | checked: false, 114 | click: onClick 115 | }, 116 | { label: 'Exit', role: 'quit' } 117 | ]) 118 | 119 | mainApp.tray.setContextMenu(contextMenu) 120 | mainApp.tray.setToolTip('Hentai Downloader 2.2.0') 121 | mainApp.tray.on('click', () => { 122 | if (hideWindow) { 123 | mainApp.win.show() 124 | } else { 125 | mainApp.win.hide() 126 | } 127 | }) 128 | 129 | const savePosition = () => { 130 | let [x, y] = mainApp.win.getPosition() 131 | const { width, height } = screen.getPrimaryDisplay().workAreaSize 132 | const [winWidth, winHeight] = mainApp.win.getSize() 133 | if (x < 0) x = 0 134 | if (x > width - winWidth) x = width - winWidth 135 | if (y < 0) y = 0 136 | if (y > height - winHeight) y = height - winHeight 137 | settings.set('position', { x, y }) 138 | log.log({ x, y }) 139 | } 140 | let moveId = null 141 | mainApp.win.on('moved', () => { 142 | if (settings.getSync('ontop', false)) return 143 | if (moveId) clearTimeout(moveId) 144 | moveId = setTimeout(savePosition, 200) 145 | }) 146 | mainApp.win.on('move', () => { 147 | if (settings.getSync('ontop', false)) return 148 | if (moveId) clearTimeout(moveId) 149 | moveId = setTimeout(savePosition, 200) 150 | }) 151 | 152 | mainApp.win.on('show', () => { 153 | hideWindow = !hideWindow 154 | }) 155 | 156 | mainApp.win.on('hide', () => { 157 | hideWindow = !hideWindow 158 | }) 159 | 160 | mainApp.win.on('closed', () => { 161 | mainApp.tray.destroy() 162 | delete mainApp.win 163 | }) 164 | 165 | mainApp.win.on('ready-to-show', () => { 166 | mainApp.win.show() 167 | }) 168 | 169 | mainApp.win.webContents.setWindowOpenHandler((details) => { 170 | shell.openExternal(details.url) 171 | return { action: 'deny' } 172 | }) 173 | 174 | // HMR for renderer base on electron-vite cli. 175 | // Load the remote URL for development or the local html file for production. 176 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 177 | mainApp.win.loadURL(process.env['ELECTRON_RENDERER_URL']) 178 | } else { 179 | mainApp.win.loadFile(join(__dirname, '../renderer/index.html')) 180 | } 181 | } 182 | 183 | // This method will be called when Electron has finished 184 | // initialization and is ready to create browser windows. 185 | // Some APIs can only be used after this event occurs. 186 | app.whenReady().then(() => { 187 | // Set app user model id for windows 188 | electronApp.setAppUserModelId('com.electron') 189 | 190 | // Default open or close DevTools by F12 in development 191 | // and ignore CommandOrControl + R in production. 192 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 193 | app.on('browser-window-created', (_, window) => { 194 | optimizer.watchWindowShortcuts(window) 195 | }) 196 | 197 | createWindow() 198 | 199 | app.on('activate', function () { 200 | // On macOS it's common to re-create a window in the app when the 201 | // dock icon is clicked and there are no other windows open. 202 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 203 | }) 204 | }) 205 | 206 | // Quit when all windows are closed, except on macOS. There, it's common 207 | // for applications and their menu bar to stay active until the user quits 208 | // explicitly with Cmd + Q. 209 | app.on('window-all-closed', () => { 210 | if (process.platform !== 'darwin') { 211 | app.quit() 212 | mainApp.tray.destroy() 213 | } 214 | }) 215 | 216 | // In this file you can include the rest of your app"s specific main process 217 | // code. You can also put them in separate files and require them here. 218 | -------------------------------------------------------------------------------- /src/main/plugins/events.js: -------------------------------------------------------------------------------- 1 | import { app, clipboard, dialog, ipcMain, ipcRenderer } from 'electron' 2 | import settings from 'electron-settings' 3 | import * as hentai from './ehentai.js' 4 | 5 | let config = settings.get('directory') 6 | if (!settings.get('directory')) { 7 | console.log('directory:', config) 8 | settings.set('directory', app.getPath('downloads')) 9 | } 10 | 11 | const onWatchClipboard = (e) => { 12 | let data = null 13 | setInterval(() => { 14 | if (!settings.get('clipboard', false)) return 15 | 16 | const text = clipboard.readText() 17 | if (data !== text) { 18 | e.sender.send('CLIPBOARD', text) 19 | data = text 20 | } 21 | }, 300) 22 | } 23 | 24 | export function onClick (menuItem) { 25 | if (menuItem.role === 'toggle-clipboard') { 26 | settings.set('clipboard', menuItem.checked) 27 | } 28 | } 29 | 30 | export function initMain (mainWindow) { 31 | ipcMain.on('CLIPBOARD', function (e) { 32 | onWatchClipboard(e) 33 | }) 34 | 35 | ipcMain.on('CANCEL', function (e) { 36 | hentai.cancel() 37 | e.sender.send('CANCEL', null) 38 | }) 39 | 40 | ipcMain.on('CHANGE_DIRECTORY', async (e) => { 41 | 42 | const fileNames = dialog.showOpenDialogSync(mainWindow, { 43 | properties: ['openDirectory'] 44 | }) 45 | if (fileNames.length > 0) settings.set('directory', fileNames[0]) 46 | e.sender.send('CHANGE_DIRECTORY', fileNames) 47 | }) 48 | ipcMain.on('URL_VERIFY', function (e, url) { 49 | hentai.parseHentai(url, e.sender).then(async manga => { 50 | // Request Send Manga 51 | // await touno.api({ 52 | // url: '/exhentai', 53 | // data: {} 54 | // }) 55 | 56 | e.sender.send('URL_VERIFY', { error: false, data: manga }) 57 | }).catch(ex => { 58 | e.sender.send('URL_VERIFY', { error: ex.toString(), data: {} }) 59 | }) 60 | }) 61 | ipcMain.on('DOWNLOAD_BEGIN', function (e, sender) { 62 | hentai.download(sender.manga, sender.directory, e.sender).then(() => { 63 | e.sender.send('DOWNLOAD_COMPLATE') 64 | }).catch(e => { 65 | console.log('DOWNLOAD_COMPLATE', e) 66 | }) 67 | }) 68 | ipcMain.on('LOGIN', function (e, cookieString) { 69 | settings.set('cookie', cookieString) 70 | console.log(`Login: ${cookieString}`) 71 | const result = { 72 | success: false, 73 | igneous: null, 74 | ipb_member_id: null, 75 | ipb_pass_hash: null 76 | } 77 | 78 | settings.delete('igneous') 79 | settings.delete('ipb_member_id') 80 | settings.delete('ipb_pass_hash') 81 | 82 | let countCookie = 0 83 | for (const cookie of cookieString.split(';')) { 84 | const [ key, value ] = cookie.split('=') 85 | 86 | if (key.trim() == 'igneous') { 87 | result.igneous = value.trim() 88 | settings.set('igneous', value.trim()) 89 | countCookie++ 90 | } 91 | 92 | else if (key.trim() == 'ipb_member_id') { 93 | result.ipb_member_id = value.trim() 94 | settings.set('ipb_member_id', value.trim()) 95 | countCookie++ 96 | } 97 | 98 | else if (key.trim() == 'ipb_pass_hash') { 99 | result.ipb_pass_hash = value.trim() 100 | settings.set('ipb_pass_hash', value.trim()) 101 | countCookie++ 102 | } 103 | } 104 | if (countCookie == 3) result.success = true 105 | e.sender.send('LOGIN', result) 106 | 107 | // if (igneous !== '') { 108 | // console.log('igneous', igneous) 109 | // hentai.login(igneous).then(() => { 110 | // // let getName = /You are now logged in as:(.*?)
{ 116 | // // e.sender.send('SESSION', data) 117 | // // }).catch(ex => { 118 | // // console.log(ex) 119 | // // e.sender.send('SESSION', null) 120 | // // }) 121 | // e.sender.send('LOGIN', { success: true, igneous }) 122 | // // } else { 123 | // // let message = /"errorwrap"[\w\W]*?

(.*?) { 127 | // console.log(ex) 128 | // e.sender.send('LOGIN', { success: false, message: ex.message }) 129 | // }) 130 | // } else { 131 | // e.sender.send('LOGIN', { success: false, message: 'This field is empty.' }) 132 | // } 133 | }) 134 | } 135 | export const client = { 136 | config: {}, 137 | install: Vue => { 138 | Vue.mixin({ 139 | methods: { 140 | clearCookie: () => { 141 | settings.delete('cookie') 142 | settings.delete('igneous') 143 | settings.delete('ipb_member_id') 144 | settings.delete('ipb_pass_hash') 145 | }, 146 | GetToggleClipboard: () => { 147 | return settings.get('clipboard', false) 148 | }, 149 | ConfigLoaded: () => { 150 | return Object.assign(settings.get('config') || {}, { 151 | directory: settings.get('directory'), 152 | igneous: settings.get('igneous'), 153 | cookie: settings.get('cookie') 154 | }) 155 | }, 156 | ConfigSaved: config => { 157 | console.log('ConfigSaved :: ', config) 158 | settings.set('config', Object.assign(settings.get('config'), config)) 159 | }, 160 | ExUser: () => { 161 | return new Promise((resolve) => { 162 | console.log('ipc-send::CHANGE_DIRECTORY') 163 | ipcRenderer.send('CHANGE_DIRECTORY') 164 | ipcRenderer.once('CHANGE_DIRECTORY', (e, dir) => { 165 | console.log('ipc-once::CHANGE_DIRECTORY:', dir) 166 | resolve(dir ? dir[0] : '') 167 | }) 168 | }) 169 | }, 170 | CANCEL: () => { 171 | return new Promise((resolve) => { 172 | console.log('ipc-remove::CANCEL') 173 | ipcRenderer.send('CANCEL') 174 | ipcRenderer.removeAllListeners('INIT_MANGA') 175 | ipcRenderer.removeAllListeners('URL_VERIFY') 176 | ipcRenderer.removeAllListeners('DOWNLOAD_WATCH') 177 | ipcRenderer.removeAllListeners('DOWNLOAD_COMPLATE') 178 | ipcRenderer.removeAllListeners('LOGIN') 179 | resolve() 180 | }) 181 | }, 182 | CHANGE_DIRECTORY: () => { 183 | return new Promise((resolve) => { 184 | console.log('ipc-send::CHANGE_DIRECTORY') 185 | ipcRenderer.send('CHANGE_DIRECTORY') 186 | ipcRenderer.once('CHANGE_DIRECTORY', (e, dir) => { 187 | console.log('ipc-once::CHANGE_DIRECTORY:', dir) 188 | resolve(dir ? dir[0] : '') 189 | }) 190 | }) 191 | }, 192 | URL_VERIFY: url => { 193 | return new Promise((resolve) => { 194 | ipcRenderer.once('URL_VERIFY', (e, res) => { 195 | console.log('ipc-once::URL_VERIFY:', res) 196 | resolve(res) 197 | }) 198 | console.log('ipc-send::URL_VERIFY:', url) 199 | ipcRenderer.send('URL_VERIFY', url) 200 | }) 201 | }, 202 | INIT_MANGA: callback => { 203 | ipcRenderer.removeAllListeners('INIT_MANGA') 204 | ipcRenderer.on('INIT_MANGA', (e, sender) => { 205 | console.log('ipc-on::INIT_MANGA', sender) 206 | callback(sender) 207 | }) 208 | }, 209 | DOWNLOAD: (manga, events) => { 210 | return new Promise((resolve) => { 211 | ipcRenderer.removeAllListeners('DOWNLOAD_WATCH') 212 | ipcRenderer.removeAllListeners('DOWNLOAD_COMPLATE') 213 | ipcRenderer.on('DOWNLOAD_WATCH', (e, manga) => { 214 | console.log('ipc-on::DOWNLOAD_WATCH:', manga) 215 | return events(e, manga) 216 | }) 217 | ipcRenderer.on('DOWNLOAD_COMPLATE', () => { 218 | console.log('ipc-on::DOWNLOAD_COMPLATE') 219 | resolve() 220 | }) 221 | console.log('ipc-send::DOWNLOAD_BEGIN:', manga) 222 | ipcRenderer.send('DOWNLOAD_BEGIN', manga) 223 | }) 224 | }, 225 | LOGIN: (cookie) => { 226 | return new Promise((resolve) => { 227 | ipcRenderer.removeAllListeners('LOGIN') 228 | ipcRenderer.on('LOGIN', (e, data) => { 229 | console.log('ipc-on::LOGIN') 230 | resolve(data) 231 | }) 232 | console.log('ipc-send::LOGIN') 233 | ipcRenderer.send('LOGIN', cookie) 234 | }) 235 | }, 236 | CLIPBOARD: (vm) => { 237 | console.log('ipc-on::CLIPBOARD', vm) 238 | ipcRenderer.removeAllListeners('CLIPBOARD') 239 | ipcRenderer.send('CLIPBOARD') 240 | ipcRenderer.on('CLIPBOARD', async (e, data) => { 241 | console.log('ipc-on::CLIPBOARD', data) 242 | await vm.onPasteClipboard(data) 243 | }) 244 | } 245 | }, 246 | created () { 247 | // ipcRenderer.send('LOGIN') 248 | // console.log('created `vue-mbos.js`mixin.') 249 | } 250 | }) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/plugins/ehentai.js: -------------------------------------------------------------------------------- 1 | import { powerSaveBlocker } from 'electron' 2 | import URL from 'url-parse' 3 | import fs, { existsSync } from 'fs' 4 | import path from 'path' 5 | import moment from 'moment' 6 | import settings from 'electron-settings' 7 | import axios from 'axios' 8 | import https from 'https' 9 | 10 | import cookieSupport from 'axios-cookiejar-support' 11 | import * as cfg from './lib/config' 12 | 13 | cookieSupport(axios) 14 | 15 | // At request level 16 | const agent = new https.Agent({ rejectUnauthorized: false }) 17 | 18 | let cancelDownload = false 19 | let saveBlockerId = null 20 | let jarCookie = cfg.loadCookie() 21 | 22 | console.log('cookieSupport:', jarCookie) 23 | 24 | const reqHentai = async (link, method, options = {}) => { 25 | options.headers = Object.assign({ 26 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36', 27 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 28 | 'Accept-Encoding': 'gzip, deflate, br', 29 | 'accept-language': 'en-US,en;q=0.9,th;q=0.8,ja;q=0.7', 30 | 'cache-control': 'no-cache', 31 | "sec-ch-ua": `" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"`, 32 | "sec-ch-ua-mobile": "?0", 33 | "sec-ch-ua-platform": "Windows", 34 | "Sec-Fetch-Dest": "document", 35 | "Sec-Fetch-Mode": "navigate", 36 | "Sec-Fetch-Site": "same-origin", 37 | "Sec-Fetch-User": "?1", 38 | "Upgrade-Insecure-Requests": "1", 39 | "Connection": "keep-alive", 40 | }, options.headers) 41 | 42 | wLog(`URL REQUEST: ${link}`) 43 | await jarCookieBuild(new URL(link).hostname) 44 | return axios(Object.assign({ 45 | url: link, 46 | method: method || 'GET', 47 | jar: jarCookie, 48 | withCredentials: true, 49 | httpsAgent: agent, 50 | timeout: 5000 51 | }, options)).then((res) => { 52 | return jarCookieCheck().then(() => res) 53 | }).then((res) => { 54 | wLog(`URL RESPONSE: ${res.status} body: ${res.data.length}`) 55 | return res.data 56 | }).catch((ex) => { 57 | wError(ex) 58 | if (ex.response) { 59 | console.log('EX.RESPONSE', ex.response) 60 | const unavailable = /

(.*?)<\/p>/ig.exec(ex.response.data) 61 | return unavailable[1] || unavailable || ex.response.status 62 | } else { 63 | console.log('EX', ex) 64 | return ex.message || ex 65 | } 66 | }) 67 | 68 | // return new Promise((resolve, reject) => { 69 | // wLog(`URL REQUEST: ${link}`) 70 | // axios(Object.assign({ 71 | // url: link, 72 | // method: method || 'GET', 73 | // jar: jarCookie, 74 | // timeout: 5000 75 | // }, options), (error, res, body) => { 76 | // jarCookieCheck().then(() => { 77 | // if (error) { 78 | // reject(new Error(error)) 79 | // return 80 | // } 81 | // let { statusCode } = res 82 | // wLog(`URL RESPONSE: ${statusCode} body: ${body.length}`) 83 | // if (statusCode === 302 || statusCode === 200) { 84 | // resolve(body) 85 | // } else { 86 | // reject(new Error(statusCode)) 87 | // } 88 | // }) 89 | // }) 90 | // }) 91 | } 92 | 93 | const blockCookie = (path, name, ex = false) => new Promise((resolve, reject) => { 94 | if (!jarCookie) resolve() 95 | 96 | jarCookie.store.removeCookie(!ex ? 'e-hentai.org' : 'exhentai.org', path, name, (err) => { 97 | 98 | if (err) { 99 | console.error('jarCookie::removeCookie:', err) 100 | return reject(err) 101 | } 102 | resolve() 103 | }) 104 | }) 105 | 106 | const getCookie = (name, ex = false) => new Promise((resolve, reject) => { 107 | if (!jarCookie) return resolve(null) 108 | 109 | // console.log('getCookie', name) 110 | jarCookie.store.findCookie(!ex ? 'e-hentai.org' : 'exhentai.org', '/', name, (err, cookie) => { 111 | if (err) { 112 | console.error('jarCookie::findCookie:', err) 113 | return reject(err) 114 | } 115 | resolve(cookie) 116 | }) 117 | }) 118 | 119 | // const pushCookie = (cookie) => new Promise((resolve, reject) => { 120 | // if (!cookie || !jarCookie) return resolve() 121 | 122 | // jarCookie.store.putCookie(cookie, (err) => { 123 | // if (err) { 124 | // console.error('jarCookie::putCookie:', err) 125 | // return reject(err) 126 | // } 127 | // resolve() 128 | // }) 129 | // }) 130 | export const setCookie = (path, value, domain = 'e-hentai.org') => new Promise((resolve, reject) => { 131 | if (!jarCookie) return resolve() 132 | 133 | // console.log('setCookie:', value) 134 | jarCookie.setCookie(`${value}; Path=${path}; Domain=${domain}`, `http://${domain}/`, {}, (err) => { 135 | if (err) { 136 | console.error('jarCookie::setCookie:', err) 137 | return reject(err) 138 | } 139 | resolve() 140 | }) 141 | }) 142 | const jarCookieBuild = async (hostname) => { 143 | // let memberId = await getCookie('ipb_member_id') 144 | // if (memberId) { 145 | // const exMemberId = await getCookie('ipb_member_id', true) 146 | // if (!exMemberId) { 147 | // memberId = memberId.clone() 148 | // memberId.domain = 'exhentai.org' 149 | // pushCookie(memberId) 150 | // } 151 | // if (!settings.get('igneous')) { 152 | // throw new Error('Please join your browser session.') 153 | // } 154 | // } else { 155 | // settings.set('config', {}) 156 | // } 157 | 158 | // let passHash = await getCookie('ipb_pass_hash') 159 | // if (passHash) { 160 | // passHash = passHash.clone() 161 | // passHash.domain = 'exhentai.org' 162 | // pushCookie(passHash) 163 | // } 164 | if (!settings.get('igneous') && hostname == 'exhentai.org') { 165 | throw new Error('Please join your browser session.') 166 | } 167 | 168 | jarCookie.removeAllCookiesSync() 169 | await setCookie('/', 'nw=1', hostname) 170 | if (settings.get('igneous')) { 171 | await setCookie('/', `igneous=${settings.get('igneous')}`, hostname) 172 | await setCookie('/', `ipb_member_id=${settings.get('ipb_member_id')}`, hostname) 173 | await setCookie('/', `ipb_pass_hash=${settings.get('ipb_pass_hash')}`, hostname) 174 | } 175 | } 176 | 177 | const jarCookieCheck = async () => { 178 | await blockCookie('/', 'sk') 179 | await blockCookie('/', 'sk', true) 180 | 181 | await blockCookie('/s/', 'skipserver') 182 | await blockCookie('/', 'yay', true) 183 | cfg.saveCookie(jarCookie) 184 | 185 | // console.group('Cookie Check List') 186 | // const idx = jarCookie.store.idx 187 | // if (Object.keys(idx).length > 0) { 188 | // for (const domain in idx) { 189 | // for (const router in idx[domain]) { 190 | // for (const cookie in idx[domain][router]) { 191 | // console.log(' -', domain, cookie, ':', idx[domain][router][cookie].value) 192 | // } 193 | // } 194 | // } 195 | // } 196 | // console.groupEnd('Cookie Check List') 197 | } 198 | // console.log('development:', touno.DevMode) 199 | const wError = (...msg) => { 200 | fs.appendFileSync(`./${moment().format('YYYY-MM-DD')}-error.log`, `${moment().format('HH:mm:ss.SSS')} ${msg.join(' ')}\n`) 201 | } 202 | const wLog = (...msg) => { 203 | fs.appendFileSync(`./${moment().format('YYYY-MM-DD')}.log`, `${moment().format('HH:mm:ss.SSS')} ${msg.join(' ')}\n`) 204 | } 205 | 206 | const exHentaiHistory = (uri, data) => { 207 | return { 208 | url: uri, 209 | data: data 210 | } 211 | } 212 | 213 | let getFilename = (index, total) => { 214 | return `${Math.pow(10, (total.toString().length - index.toString().length) + 1).toString().substr(2, 10) + index}` 215 | } 216 | 217 | const getExtension = (res) => { 218 | switch (res.headers['content-type']) { 219 | case 'jpg': 220 | case 'image/jpg': 221 | case 'image/jpeg': return 'jpg' 222 | case 'image/png': return 'png' 223 | case 'image/gif': return 'gif' 224 | } 225 | } 226 | let getImage = async (res, manga, l, index, directory, emit) => { 227 | let image = /id="img".*?src="(.*?)"/ig.exec(res)[1] 228 | let nl = /return nl\('(.*?)'\)/ig.exec(res)[1] 229 | let filename = getFilename(index + 1, manga.page) 230 | 231 | let name = manga.name.replace(/[/\\|.:?<>"]/ig, '') 232 | let dir = path.join(directory, name) 233 | if (!fs.existsSync(dir)) fs.mkdirSync(dir) 234 | 235 | let nRetry = 0 236 | let isSuccess = false 237 | do { 238 | if (cancelDownload) { throw new Error('User Cancel Request by button.') } 239 | 240 | let resImage = null 241 | if (nRetry > 0) { 242 | wLog('Retry::', nRetry) 243 | let link = manga.items[index] 244 | manga.items[index] = `${link}${link.indexOf('?') > -1 ? '&' : '?'}nl=${nl}` 245 | res = await reqHentai(manga.items[index]) 246 | nl = /return nl\('(.*?)'\)/ig.exec(res)[1] 247 | image = /id="img".*?src="(.*?)"/ig.exec(res)[1] 248 | } 249 | emit.send('DOWNLOAD_WATCH', { index: l, current: filename, total: parseInt(manga.page) }) 250 | wLog(`Downloading... -- '${(index + 1)}.jpg' of ${manga.page} files -->`) 251 | try { 252 | const response = await axios({ 253 | url: image, 254 | method: 'GET', 255 | responseType: 'stream', 256 | jar: jarCookie, 257 | withCredentials: true, 258 | httpsAgent: agent, 259 | timeout: 10000, 260 | headers: { 261 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36', 262 | 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', 263 | 'Accept-Encoding': 'gzip, deflate, br', 264 | 'Accept-Language': 'en-US,en;q=0.9,th;q=0.8,ja;q=0.7', 265 | 'Strict-Transport-Security': 'max-age=15552000; includeSubDomains; preload', 266 | 'referer': `https://${new URL(manga.url).hostname}/`, 267 | "sec-ch-ua": `" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"`, 268 | "sec-ch-ua-mobile": "?0", 269 | "sec-ch-ua-platform": "Windows", 270 | "Sec-Fetch-Dest": "image", 271 | "Sec-Fetch-Mode": "no-cors", 272 | "Sec-Fetch-Site": "cross-site" 273 | } 274 | }) 275 | // console.log('response', response) 276 | isSuccess = true 277 | resImage = response.data 278 | // // clearTimeout(cancelTime) 279 | const extensions = getExtension(response) 280 | if (extensions) wLog(index + 1, '--> ', response.statusCode, response.headers['content-type']) 281 | 282 | const asyncWriterImage = (timeout = 30) => new Promise((resolve, reject) => { 283 | let cancelTime = setTimeout(() => { 284 | writer.close() 285 | // resImage.close() 286 | reject(new Error('Operation canceled.')) 287 | }, timeout * 1000) 288 | resImage.on('error', ex => { 289 | clearTimeout(cancelTime) 290 | writer.close() 291 | // resImage.close() 292 | reject(new Error(`Download:: ${ex.toString()}`)) 293 | }) 294 | writer.on('error', (ex) => { 295 | clearTimeout(cancelTime) 296 | writer.close() 297 | // resImage.close() 298 | reject(new Error(`Writer:: ${ex.toString()}`)) 299 | }) 300 | 301 | writer.on('finish', () => { 302 | clearTimeout(cancelTime) 303 | writer.close() 304 | // resImage.close() 305 | resolve() 306 | }) 307 | }) 308 | 309 | const writer = fs.createWriteStream(`${dir}/${filename}.${extensions}`) 310 | resImage.pipe(writer) 311 | await asyncWriterImage() 312 | 313 | let success = parseInt(manga.page) === index + 1 314 | emit.send('DOWNLOAD_WATCH', { index: l, current: filename, total: parseInt(manga.page), finish: success }) 315 | if (success) { 316 | let config = settings.get('config') || { user_id: 'guest' } 317 | let items = fs.readdirSync(dir) 318 | wLog('Complate -- Read', manga.page, 'files, and in directory', items.length, 'files') 319 | wLog('---------------------') 320 | 321 | exHentaiHistory('exhentai/manga', { 322 | user_id: config.user_id, 323 | name: manga.name, 324 | link: manga.url, 325 | cover: manga.cover, 326 | language: manga.language, 327 | size: manga.size, 328 | page: manga.page, 329 | completed: true 330 | }) 331 | } 332 | } catch (ex) { 333 | nRetry++ 334 | wLog('getImage::', manga.items[index]) 335 | wError('getImage::', index, ex.message) 336 | wError('getImage::', index, ex.stack) 337 | } finally { 338 | wLog('getImage::', manga.items[index]) 339 | } 340 | } while (nRetry < 3 && !isSuccess) 341 | 342 | } 343 | 344 | export const cancel = () => { 345 | cancelDownload = true 346 | } 347 | 348 | export const download = async (list, directory, emit) => { 349 | cancelDownload = false 350 | const delay = (timeout = 1000) => new Promise(resolve => { 351 | const id = setTimeout(() => { 352 | clearTimeout(id) 353 | resolve() 354 | }, timeout) 355 | }) 356 | 357 | saveBlockerId = powerSaveBlocker.start('prevent-display-sleep') 358 | let imgTotal = 0 359 | try { 360 | let iManga = 0 361 | for await (const manga of list) { 362 | if (cancelDownload) { throw new Error('User Cancel Request by button.') } 363 | if (manga.error) continue 364 | 365 | let iImage = 0 366 | for await (const imageUrl of manga.items) { 367 | const filename = getFilename(iImage + 1, manga.page) 368 | const name = manga.name.replace(/[/\\|.:?<>"]/ig, '') 369 | const exisFile = ext => existsSync(`${path.join(directory, name)}/${filename}.${ext}`) 370 | if (!exisFile('jpg') && !exisFile('png') && !exisFile('gif')) { 371 | let res = await reqHentai(imageUrl) 372 | await getImage(res, manga, iManga, iImage, directory, emit) 373 | } else { 374 | await delay(400) 375 | emit.send('DOWNLOAD_WATCH', { index: iManga, current: filename, total: parseInt(manga.page), finish: parseInt(manga.page) === iImage + 1 }) 376 | } 377 | iImage++ 378 | imgTotal++ 379 | } 380 | iManga++ 381 | } 382 | } catch (ex) { 383 | wError(`*error*: ${ex.toString()}`) 384 | } finally { 385 | wLog('hentai-downloader', `*downloading request* \`${imgTotal}\` time`) 386 | if (powerSaveBlocker.isStarted(saveBlockerId)) powerSaveBlocker.stop(saveBlockerId) 387 | saveBlockerId = null 388 | } 389 | 390 | // for (let l = 0; l < list.length; l++) { 391 | // let manga = list[l] 392 | // if (manga.error) continue 393 | // for (let i = 0; i < manga.items.length; i++) { 394 | // let filename = getFilename(i + 1, manga.page) 395 | // let name = manga.name.replace(/[/\\|.:?<>"]/ig, '') 396 | // let dir = path.join(directory, name) 397 | // if (!fs.existsSync(`${dir}/${filename}.jpg`) && !fs.existsSync(`${dir}/${filename}.png`) && !fs.existsSync(`${dir}/${filename}.gif`)) { 398 | // all.push(() => new Promise(async (resolve, reject) => { 399 | // await jarCookieBuild() 400 | // let res = await request(manga.items[i], { jar: jarCookie }) 401 | // await jarCookieCheck() 402 | // await getImage(res, manga, l, i, resolve, directory, emit) 403 | // })) 404 | // } else { 405 | // emit.send('DOWNLOAD_WATCH', { index: l, current: filename, total: parseInt(manga.page), finish: parseInt(manga.page) === i + 1 }) 406 | // } 407 | // } 408 | // } 409 | } 410 | 411 | const validateURL = (link) => { 412 | const baseUrl = new URL(link.trim()) 413 | if (!/\/\w{1}\/\d{1,8}\/[0-9a-f]+?\//ig.test(baseUrl.pathname)) { 414 | throw new Error(`Key missing, or incorrect key provided.`) 415 | } else { 416 | let [fixed] = /\/\w{1}\/\d{1,8}\/[0-9a-f]+?\//ig.exec(baseUrl.pathname) 417 | return `https://${baseUrl.hostname}${fixed}` 418 | } 419 | } 420 | 421 | let getManga = async (link, raw, emit) => { 422 | const baseUrl = new URL(link) 423 | let [fixed] = /\/\w{1}\/\d{1,8}\/[0-9a-f]+?\//ig.exec(baseUrl.pathname) 424 | 425 | let name = /

.*?gn">(.*?)<\/.*?gj">(.*?)<\/.*?<\/div>/ig.exec(raw) 426 | let language = /Language:.*?class="gdt2">(.*?)&/ig.exec(raw) 427 | let size = /File Size:.*?class="gdt2">(.*?)(.*?).page/ig.exec(raw) 429 | let cover = /
.*?url\((.*?)\)/ig.exec(raw) 430 | 431 | if (!name) throw new Error('manga.name is not found') 432 | if (!language) throw new Error('manga.language is not found') 433 | if (!size) throw new Error('manga.size is not found') 434 | if (!length) throw new Error('manga.page is not found') 435 | if (!cover) throw new Error('manga.page is not found') 436 | 437 | let manga = { 438 | ref: fixed, 439 | url: link, 440 | name: name[1], 441 | cover: cover[1], 442 | language: (language[1] || '').trim(), 443 | size: size[1], 444 | page: length[1], 445 | items: [] 446 | } 447 | const fetchImage = (manga, raw) => { 448 | for (const gdt of raw.match(/(gdtm|gdtl)".*?/ig)) { 449 | manga.items.push(/(gdtm|gdtl)".*?/i.exec(gdt)[2]) 450 | } 451 | } 452 | // slack(baseUrl.host, manga) 453 | fetchImage(manga, raw) 454 | 455 | console.log('------- manga -------') 456 | console.dir(manga) 457 | let config = settings.get('config') || { user_id: 'guest' } 458 | console.log('------- config -------') 459 | console.dir(config) 460 | exHentaiHistory('exhentai/manga', Object.assign(manga, { 461 | user_id: config.user_id 462 | })) 463 | 464 | const totalPage = Math.ceil(manga.page / manga.items.length) 465 | emit.send('INIT_MANGA', { page: 1, total: totalPage }) 466 | 467 | console.log('Recheck NextPage:', manga.items.length, manga.page) 468 | if (manga.items.length !== manga.page) { 469 | for (let i = 1; i < totalPage; i++) { 470 | emit.send('INIT_MANGA', { page: i + 1, total: totalPage }) 471 | raw = await reqHentai(`${link}?p=${i}`) 472 | fetchImage(manga, raw) 473 | } 474 | if (manga.items.length !== parseInt(manga.page)) throw new Error(`manga.items is '${manga.items.length}' and length is '${manga.page}'`) 475 | return manga 476 | } else { 477 | return manga 478 | } 479 | } 480 | 481 | export function parseHentai (link, emit) { 482 | cancelDownload = false 483 | return (async () => { 484 | link = validateURL(link) 485 | console.log('reqHentai', link) 486 | const hostname = new URL(link).hostname 487 | 488 | await setCookie('/', 'nw=1', hostname) 489 | let res = await reqHentai(link, 'GET', { 490 | headers: { 491 | 'pragma': 'no-cache', 492 | 'referer': `https://${hostname}/` 493 | } 494 | }) 495 | if (!/DOCTYPE.html.PUBLIC/ig.test(res)) throw new Error(res) 496 | let warnMe = /Never Warn Me Again/ig.exec(res) 497 | if (warnMe) throw new Error('Never Warn Me Again') 498 | console.log('getManga') 499 | return getManga(link, res, emit) 500 | })().catch(ex => { 501 | if (ex.response) { 502 | const res = ex.response.toJSON() 503 | console.log('Error:', res) 504 | const baseUrl = new URL(link.trim()) 505 | wLog(`This gallery has been removed: https://${baseUrl.hostname}${baseUrl.pathname}`) 506 | throw new Error('This gallery has been removed or is unavailable.') 507 | } else { 508 | wError(`*error*: ${link}\n${ex.toString()}`) 509 | throw ex 510 | } 511 | }) 512 | } 513 | 514 | export async function login (username, password) { 515 | let res1 = await reqHentai('https://forums.e-hentai.org/index.php?act=Login&CODE=01', 'POST', { 516 | header: { 'referer': 'https://forums.e-hentai.org/index.php' }, 517 | form: { 518 | referer: 'https://forums.e-hentai.org/index.php', 519 | CookieDate: 1, 520 | b: 'd', 521 | bt: '1-1', 522 | UserName: username.trim(), 523 | PassWord: password.trim(), 524 | ipb_login_submit: 'Login!' 525 | }, 526 | resolveWithFullResponse: true 527 | }) 528 | return res1 529 | } 530 | 531 | export const cookie = getCookie 532 | export const reload = jarCookieCheck 533 | --------------------------------------------------------------------------------