Skip to content

Commit 93fff88

Browse files
authored
Merge-feat: add nhentai
2 parents 3115220 + ed83255 commit 93fff88

23 files changed

Lines changed: 1336 additions & 470 deletions

File tree

.github/workflows/hitomi-db.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
continue-on-error: true
3838
run: |
3939
uv run python -m utils.website.hitomi.scape_dataset \
40-
--db-path assets/hitomi.db \
40+
--db-path __temp/hitomi.db \
4141
--workers 2 \
4242
--max-retries 5 \
4343
--timeout 15
@@ -47,20 +47,20 @@ jobs:
4747
if: steps.scrape.outcome == 'failure'
4848
run: |
4949
uv run python -m utils.website.hitomi.scape_dataset \
50-
--db-path assets/hitomi.db \
50+
--db-path __temp/hitomi.db \
5151
--workers 1 \
5252
--max-retries 8 \
5353
--timeout 20 \
5454
--only-failed
5555
5656
- name: Quality gate
57-
run: uv run python .github/scripts/hitomi_db_gate.py assets/hitomi.db
57+
run: uv run python .github/scripts/hitomi_db_gate.py __temp/hitomi.db
5858

5959
- name: Generate manifest
6060
run: |
6161
uv run python .github/scripts/generate_hitomi_manifest.py \
62-
assets/hitomi.db \
63-
--output assets/hitomi-manifest.json
62+
__temp/hitomi.db \
63+
--output __temp/hitomi-manifest.json
6464
6565
- name: Upload to release
6666
env:
@@ -74,13 +74,13 @@ jobs:
7474
gh release delete-asset "$TAG" hitomi.db --yes 2>/dev/null || true
7575
gh release delete-asset "$TAG" hitomi-manifest.json --yes 2>/dev/null || true
7676
gh release upload "$TAG" \
77-
assets/hitomi.db \
78-
assets/hitomi-manifest.json
77+
__temp/hitomi.db \
78+
__temp/hitomi-manifest.json
7979
else
8080
gh release create "$TAG" \
8181
--title "Preset Assets" \
8282
--notes "Auto-managed preset assets (hitomi.db, etc.)" \
8383
--latest=false \
84-
assets/hitomi.db \
85-
assets/hitomi-manifest.json
84+
__temp/hitomi.db \
85+
__temp/hitomi-manifest.json
8686
fi

ComicSpider/spiders/nhentai.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
from utils.website import NhentaiUtils
3+
4+
from .basecomicspider import BaseComicSpider2, font_color
5+
6+
7+
class NhentaiSpider(BaseComicSpider2):
8+
custom_settings = {
9+
"DOWNLOADER_MIDDLEWARES": {
10+
"ComicSpider.middlewares.ComicDlAllProxyMiddleware": 5,
11+
"ComicSpider.middlewares.UAMiddleware": 6,
12+
"ComicSpider.middlewares.RefererMiddleware": 10,
13+
}
14+
}
15+
name = "nhentai"
16+
num_of_row = 4
17+
domain = NhentaiUtils.domain
18+
search_url_head = NhentaiUtils.search_url_head
19+
turn_page_info = NhentaiUtils.turn_page_info
20+
book_id_url = NhentaiUtils.gallery_api_url_template
21+
mappings = dict(NhentaiUtils.mappings)
22+
23+
@property
24+
def ua(self):
25+
return dict(NhentaiUtils.headers)
26+
27+
def frame_section(self, response):
28+
detail = self.spider_site_runtime.parser.parse_book(response.text)
29+
book = response.meta.get("book") or detail
30+
if book is not detail:
31+
self.spider_site_runtime.parser.apply_detail(book, detail)
32+
frame_results = self.spider_site_runtime.parser.build_page_image_map(book)
33+
if not frame_results:
34+
raise ValueError("nhentai gallery detail produced empty image map")
35+
self.say("📢" + font_color(" 这本已经扔进任务了", cls="theme-tip"))
36+
return frame_results

GUI/mainwindow.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def apply_translations(self):
6262
self.chooseBox.setItemText(8, _translate("MainWindow", "8、h-comic🔞"))
6363
self.chooseBox.addItem("")
6464
self.chooseBox.setItemText(9, _translate("MainWindow", "9、jestful"))
65+
self.chooseBox.addItem("")
66+
self.chooseBox.setItemText(10, _translate("MainWindow", "10、nhentai🔞"))
6567
self.chooseBox.setCurrentIndex(0)
6668
self.domainBtn = TransparentToolButton(QIcon(':/main/publish.svg'), self)
6769
self.domainBtn.setVisible(False)

GUI/manager/async_task.py

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
from dataclasses import dataclass, field
77
import inspect
8+
import math
89
import time
910
import traceback
1011
from typing import Any, Callable, Dict, List, Optional, Tuple
@@ -33,6 +34,86 @@ def summarize_error_message(message: object, *, max_length: int = 180) -> str:
3334
has_hidden_detail = clipped or len(text.splitlines()) > 1
3435
return f"{first_line}。详情见日志" if has_hidden_detail else first_line
3536

37+
38+
class AsyncTaskProgressReporter:
39+
def __init__(self, emit: Callable[[str], None], *, throttle_seconds: float = 0.2, min_percent_step: int = 1):
40+
self._emit = emit
41+
self._throttle_seconds = throttle_seconds
42+
self._min_percent_step = min_percent_step
43+
self._label = ""
44+
self._bytes_downloaded = 0
45+
self._total_bytes: Optional[int] = None
46+
self._last_emit_at = 0.0
47+
self._last_percent = -1
48+
49+
def __call__(self, message: str):
50+
self._emit(message)
51+
52+
def download_reset(self, *, label: Optional[str] = None):
53+
self._label = label or self._label
54+
self._bytes_downloaded = 0
55+
self._total_bytes = None
56+
self._last_emit_at = 0.0
57+
self._last_percent = -1
58+
59+
def download_start(self, *, label: str, total_bytes: Optional[int] = None):
60+
self.download_reset(label=label)
61+
self._total_bytes = total_bytes if total_bytes is not None and total_bytes > 0 else None
62+
if self._total_bytes is None:
63+
self._emit(f"{self._label} 0B")
64+
return
65+
self._emit(f"{self._label} 0% {self._format_ratio(0, self._total_bytes)}")
66+
67+
def download_advance(self, chunk_size: int, *, label: Optional[str] = None, total_bytes: Optional[int] = None):
68+
if chunk_size < 0:
69+
raise ValueError("chunk_size must be >= 0")
70+
if label:
71+
self._label = label
72+
if total_bytes is not None:
73+
self._total_bytes = total_bytes if total_bytes > 0 else None
74+
self._bytes_downloaded += chunk_size
75+
now = time.monotonic()
76+
if self._total_bytes is None:
77+
if now - self._last_emit_at < self._throttle_seconds:
78+
return
79+
self._last_emit_at = now
80+
self._emit(f"{self._label} {self._format_bytes(self._bytes_downloaded)}")
81+
return
82+
83+
percent = min(100, math.floor((self._bytes_downloaded * 100) / self._total_bytes))
84+
should_emit = percent >= self._last_percent + self._min_percent_step or now - self._last_emit_at >= self._throttle_seconds
85+
if not should_emit:
86+
return
87+
self._last_emit_at = now
88+
self._last_percent = percent
89+
self._emit(f"{self._label} {percent}% {self._format_ratio(self._bytes_downloaded, self._total_bytes)}")
90+
91+
def download_finish(self, *, label: Optional[str] = None):
92+
if label:
93+
self._label = label
94+
if self._total_bytes is None:
95+
self._emit(f"{self._label} done {self._format_bytes(self._bytes_downloaded)}")
96+
return
97+
done_bytes = max(self._bytes_downloaded, self._total_bytes)
98+
self._emit(f"{self._label} done {self._format_bytes(done_bytes)}")
99+
100+
@staticmethod
101+
def _format_bytes(size_bytes: int) -> str:
102+
if size_bytes < 1024:
103+
return f"{size_bytes}B"
104+
if size_bytes < 1024 * 1024:
105+
return f"{size_bytes / 1024:.1f}K"
106+
return f"{size_bytes / 1024 / 1024:.1f}M"
107+
108+
@classmethod
109+
def _format_ratio(cls, downloaded_bytes: int, total_bytes: int) -> str:
110+
if total_bytes < 1024:
111+
return f"{downloaded_bytes}/{total_bytes}B"
112+
if total_bytes < 1024 * 1024:
113+
return f"{downloaded_bytes / 1024:.1f}/{total_bytes / 1024:.1f}K"
114+
return f"{downloaded_bytes / 1024 / 1024:.1f}/{total_bytes / 1024 / 1024:.1f}M"
115+
116+
36117
class AsyncTaskThread(QThread):
37118
success_signal = Signal(object)
38119
error_signal = Signal(str)
@@ -52,7 +133,7 @@ def run(self):
52133
return
53134
code_obj = getattr(self.task_func, "__code__", None)
54135
if code_obj is not None and "progress_callback" in code_obj.co_varnames:
55-
self.kwargs["progress_callback"] = self.emit_progress
136+
self.kwargs["progress_callback"] = AsyncTaskProgressReporter(self.emit_progress)
56137
result = self.task_func(*self.args, **self.kwargs)
57138
if inspect.isawaitable(result):
58139
result = asyncio.run(result)
@@ -201,13 +282,8 @@ def _show(self, factory: Callable[..., Optional[InfoBar]], title: str, content:
201282
if self._gui is None:
202283
return None
203284
infobar = factory(
204-
title=title,
205-
content=content,
206-
orient=Qt.Horizontal,
207-
isClosable=True,
208-
position=InfoBarPosition.TOP,
209-
duration=duration,
210-
parent=self._gui,
285+
title=title, content=content, orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP,
286+
duration=duration, parent=self._gui,
211287
)
212288
if infobar is None:
213289
return None
@@ -263,11 +339,7 @@ def execute_task(self, task_id: str, config: TaskConfig) -> bool:
263339
)
264340
if config.show_tooltip:
265341
self._tooltip_stack.show(
266-
task_id,
267-
config.tooltip_title,
268-
config.tooltip_content,
269-
config.tooltip_position,
270-
config.tooltip_parent,
342+
task_id, config.tooltip_title, config.tooltip_content, config.tooltip_position, config.tooltip_parent
271343
)
272344
thread.start()
273345
return True
@@ -301,21 +373,11 @@ def execute_simple_task(
301373
return self.execute_task(
302374
task_id,
303375
TaskConfig(
304-
task_func=task_func,
305-
success_callback=success_callback,
306-
error_callback=error_callback,
307-
progress_callback=progress_callback,
308-
tooltip_title=tooltip_title,
309-
tooltip_content=tooltip_content,
310-
show_success_info=show_success_info,
311-
show_error_info=show_error_info,
312-
success_message=success_message,
313-
auto_hide_tooltip=auto_hide_tooltip,
314-
show_tooltip=show_tooltip,
315-
tooltip_position=tooltip_position,
316-
tooltip_parent=tooltip_parent,
317-
args=args,
318-
kwargs=kwargs,
376+
task_func=task_func, success_callback=success_callback, error_callback=error_callback,
377+
progress_callback=progress_callback, tooltip_title=tooltip_title, tooltip_content=tooltip_content,
378+
show_success_info=show_success_info, show_error_info=show_error_info, success_message=success_message,
379+
auto_hide_tooltip=auto_hide_tooltip, show_tooltip=show_tooltip, tooltip_position=tooltip_position,
380+
tooltip_parent=tooltip_parent, args=args, kwargs=kwargs,
319381
),
320382
)
321383

0 commit comments

Comments
 (0)