Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/hitomi-db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
continue-on-error: true
run: |
uv run python -m utils.website.hitomi.scape_dataset \
--db-path assets/hitomi.db \
--db-path __temp/hitomi.db \
--workers 2 \
--max-retries 5 \
--timeout 15
Expand All @@ -47,20 +47,20 @@ jobs:
if: steps.scrape.outcome == 'failure'
run: |
uv run python -m utils.website.hitomi.scape_dataset \
--db-path assets/hitomi.db \
--db-path __temp/hitomi.db \
--workers 1 \
--max-retries 8 \
--timeout 20 \
--only-failed

- name: Quality gate
run: uv run python .github/scripts/hitomi_db_gate.py assets/hitomi.db
run: uv run python .github/scripts/hitomi_db_gate.py __temp/hitomi.db

- name: Generate manifest
run: |
uv run python .github/scripts/generate_hitomi_manifest.py \
assets/hitomi.db \
--output assets/hitomi-manifest.json
__temp/hitomi.db \
--output __temp/hitomi-manifest.json

- name: Upload to release
env:
Expand All @@ -74,13 +74,13 @@ jobs:
gh release delete-asset "$TAG" hitomi.db --yes 2>/dev/null || true
gh release delete-asset "$TAG" hitomi-manifest.json --yes 2>/dev/null || true
gh release upload "$TAG" \
assets/hitomi.db \
assets/hitomi-manifest.json
__temp/hitomi.db \
__temp/hitomi-manifest.json
else
gh release create "$TAG" \
--title "Preset Assets" \
--notes "Auto-managed preset assets (hitomi.db, etc.)" \
--latest=false \
assets/hitomi.db \
assets/hitomi-manifest.json
__temp/hitomi.db \
__temp/hitomi-manifest.json
fi
36 changes: 36 additions & 0 deletions ComicSpider/spiders/nhentai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from utils.website import NhentaiUtils

from .basecomicspider import BaseComicSpider2, font_color


class NhentaiSpider(BaseComicSpider2):
custom_settings = {
"DOWNLOADER_MIDDLEWARES": {
"ComicSpider.middlewares.ComicDlAllProxyMiddleware": 5,
"ComicSpider.middlewares.UAMiddleware": 6,
"ComicSpider.middlewares.RefererMiddleware": 10,
}
}
name = "nhentai"
num_of_row = 4
domain = NhentaiUtils.domain
search_url_head = NhentaiUtils.search_url_head
turn_page_info = NhentaiUtils.turn_page_info
book_id_url = NhentaiUtils.gallery_api_url_template
mappings = dict(NhentaiUtils.mappings)

@property
def ua(self):
return dict(NhentaiUtils.headers)

def frame_section(self, response):
detail = self.spider_site_runtime.parser.parse_book(response.text)
book = response.meta.get("book") or detail
if book is not detail:
self.spider_site_runtime.parser.apply_detail(book, detail)
frame_results = self.spider_site_runtime.parser.build_page_image_map(book)
if not frame_results:
raise ValueError("nhentai gallery detail produced empty image map")
self.say("📢" + font_color(" 这本已经扔进任务了", cls="theme-tip"))
return frame_results
2 changes: 2 additions & 0 deletions GUI/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ def apply_translations(self):
self.chooseBox.setItemText(8, _translate("MainWindow", "8、h-comic🔞"))
self.chooseBox.addItem("")
self.chooseBox.setItemText(9, _translate("MainWindow", "9、jestful"))
self.chooseBox.addItem("")
self.chooseBox.setItemText(10, _translate("MainWindow", "10、nhentai🔞"))
self.chooseBox.setCurrentIndex(0)
self.domainBtn = TransparentToolButton(QIcon(':/main/publish.svg'), self)
self.domainBtn.setVisible(False)
Expand Down
118 changes: 90 additions & 28 deletions GUI/manager/async_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
from dataclasses import dataclass, field
import inspect
import math
import time
import traceback
from typing import Any, Callable, Dict, List, Optional, Tuple
Expand Down Expand Up @@ -33,6 +34,86 @@ def summarize_error_message(message: object, *, max_length: int = 180) -> str:
has_hidden_detail = clipped or len(text.splitlines()) > 1
return f"{first_line}。详情见日志" if has_hidden_detail else first_line


class AsyncTaskProgressReporter:
def __init__(self, emit: Callable[[str], None], *, throttle_seconds: float = 0.2, min_percent_step: int = 1):
self._emit = emit
self._throttle_seconds = throttle_seconds
self._min_percent_step = min_percent_step
self._label = ""
self._bytes_downloaded = 0
self._total_bytes: Optional[int] = None
self._last_emit_at = 0.0
self._last_percent = -1

def __call__(self, message: str):
self._emit(message)

def download_reset(self, *, label: Optional[str] = None):
self._label = label or self._label
self._bytes_downloaded = 0
self._total_bytes = None
self._last_emit_at = 0.0
self._last_percent = -1

def download_start(self, *, label: str, total_bytes: Optional[int] = None):
self.download_reset(label=label)
self._total_bytes = total_bytes if total_bytes is not None and total_bytes > 0 else None
if self._total_bytes is None:
self._emit(f"{self._label} 0B")
return
self._emit(f"{self._label} 0% {self._format_ratio(0, self._total_bytes)}")

def download_advance(self, chunk_size: int, *, label: Optional[str] = None, total_bytes: Optional[int] = None):
if chunk_size < 0:
raise ValueError("chunk_size must be >= 0")
if label:
self._label = label
if total_bytes is not None:
self._total_bytes = total_bytes if total_bytes > 0 else None
self._bytes_downloaded += chunk_size
now = time.monotonic()
if self._total_bytes is None:
if now - self._last_emit_at < self._throttle_seconds:
return
self._last_emit_at = now
self._emit(f"{self._label} {self._format_bytes(self._bytes_downloaded)}")
return

percent = min(100, math.floor((self._bytes_downloaded * 100) / self._total_bytes))
should_emit = percent >= self._last_percent + self._min_percent_step or now - self._last_emit_at >= self._throttle_seconds
if not should_emit:
return
self._last_emit_at = now
self._last_percent = percent
self._emit(f"{self._label} {percent}% {self._format_ratio(self._bytes_downloaded, self._total_bytes)}")

def download_finish(self, *, label: Optional[str] = None):
if label:
self._label = label
if self._total_bytes is None:
self._emit(f"{self._label} done {self._format_bytes(self._bytes_downloaded)}")
return
done_bytes = max(self._bytes_downloaded, self._total_bytes)
self._emit(f"{self._label} done {self._format_bytes(done_bytes)}")

@staticmethod
def _format_bytes(size_bytes: int) -> str:
if size_bytes < 1024:
return f"{size_bytes}B"
if size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f}K"
return f"{size_bytes / 1024 / 1024:.1f}M"

@classmethod
def _format_ratio(cls, downloaded_bytes: int, total_bytes: int) -> str:
if total_bytes < 1024:
return f"{downloaded_bytes}/{total_bytes}B"
if total_bytes < 1024 * 1024:
return f"{downloaded_bytes / 1024:.1f}/{total_bytes / 1024:.1f}K"
return f"{downloaded_bytes / 1024 / 1024:.1f}/{total_bytes / 1024 / 1024:.1f}M"


class AsyncTaskThread(QThread):
success_signal = Signal(object)
error_signal = Signal(str)
Expand All @@ -52,7 +133,7 @@ def run(self):
return
code_obj = getattr(self.task_func, "__code__", None)
if code_obj is not None and "progress_callback" in code_obj.co_varnames:
self.kwargs["progress_callback"] = self.emit_progress
self.kwargs["progress_callback"] = AsyncTaskProgressReporter(self.emit_progress)
result = self.task_func(*self.args, **self.kwargs)
if inspect.isawaitable(result):
result = asyncio.run(result)
Expand Down Expand Up @@ -201,13 +282,8 @@ def _show(self, factory: Callable[..., Optional[InfoBar]], title: str, content:
if self._gui is None:
return None
infobar = factory(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=duration,
parent=self._gui,
title=title, content=content, orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP,
duration=duration, parent=self._gui,
)
if infobar is None:
return None
Expand Down Expand Up @@ -263,11 +339,7 @@ def execute_task(self, task_id: str, config: TaskConfig) -> bool:
)
if config.show_tooltip:
self._tooltip_stack.show(
task_id,
config.tooltip_title,
config.tooltip_content,
config.tooltip_position,
config.tooltip_parent,
task_id, config.tooltip_title, config.tooltip_content, config.tooltip_position, config.tooltip_parent
)
thread.start()
return True
Expand Down Expand Up @@ -301,21 +373,11 @@ def execute_simple_task(
return self.execute_task(
task_id,
TaskConfig(
task_func=task_func,
success_callback=success_callback,
error_callback=error_callback,
progress_callback=progress_callback,
tooltip_title=tooltip_title,
tooltip_content=tooltip_content,
show_success_info=show_success_info,
show_error_info=show_error_info,
success_message=success_message,
auto_hide_tooltip=auto_hide_tooltip,
show_tooltip=show_tooltip,
tooltip_position=tooltip_position,
tooltip_parent=tooltip_parent,
args=args,
kwargs=kwargs,
task_func=task_func, success_callback=success_callback, error_callback=error_callback,
progress_callback=progress_callback, tooltip_title=tooltip_title, tooltip_content=tooltip_content,
show_success_info=show_success_info, show_error_info=show_error_info, success_message=success_message,
auto_hide_tooltip=auto_hide_tooltip, show_tooltip=show_tooltip, tooltip_position=tooltip_position,
tooltip_parent=tooltip_parent, args=args, kwargs=kwargs,
),
)

Expand Down
Loading
Loading