.*?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 
2 |
3 | 
4 | 
5 | 
6 | 
7 |
8 | 
9 |
10 | **คำเตือน** ไม่ควรเปิดเว็บแพนด้าเศร้า ระหว่างที่ดาวน์โหลดมังงะเพราะจะทำให้รูปบางรูปอาจจะโหลดไม่สำเร็จ
11 |
12 | ## Community
13 | - [Discord](https://touno.io/s/ixj7)
14 |
15 | ## Overview
16 | โปรแกรมดาวน์โหลดมังงะจากแพนด้าเศร้าได้โดยไม่ต้องปั้ม Credits สามารถเพิ่มคิวได้เรื่อยๆ ตรวจจับ clipboard ได้จากการ copy ที่หน้าโปรแกรมได้ที่ละเรื่องหรือ หลายเรื่องพร้อมกันก็ได้
17 |
18 | 
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 | 
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 | [](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 |
--------------------------------------------------------------------------------