Skip to content

Commit

Permalink
🐛✨ 修复下载问题以及其他小功能
Browse files Browse the repository at this point in the history
1. 修复了下载问题,现在可以正常下载了
2. 递归读取子文件夹内的漫画,感谢@spr-equinox
3. 新增下载画质和大小选项(未时装)
4. 新增命名样式选项(未时装)
5. 新增哈希值校验选项(未时装)
6. 更新了user-agent
  • Loading branch information
shadlc committed Nov 14, 2024
1 parent bae7f5f commit 5dce5af
Show file tree
Hide file tree
Showing 16 changed files with 1,427 additions and 878 deletions.
1,380 changes: 737 additions & 643 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ qrcode = "^7.4.2"
qt-material = "^2.14"
requests = "^2.31.0"
retrying = "^1.3.4"
pycryptodome = "^3.21.0"

[tool.poetry.group.dev.dependencies]
auto-py-to-exe = "^2.42.0"
Expand Down
2 changes: 1 addition & 1 deletion src/BiliQrCode.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, mainGUI: MainGUI) -> None:
self.qrcode_key = None
self.close_flag = False
self.headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"origin": "https://manga.bilibili.com",
}

Expand Down
9 changes: 6 additions & 3 deletions src/Comic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@
class Comic:
"""单本漫画 综合信息类"""

def __init__(self, comic_id: int, mainGUI: MainGUI) -> None:
def __init__(self, comic_id: int, mainGUI: MainGUI, save_path: str = "") -> None:
self.mainGUI = mainGUI
self.comic_id = comic_id
self.save_path = mainGUI.getConfig("save_path")
if save_path == "":
self.save_path = mainGUI.getConfig("save_path")
else:
self.save_path = save_path
self.num_thread = mainGUI.getConfig("num_thread")
self.num_downloaded = 0
self.episodes = []
Expand All @@ -38,7 +41,7 @@ def __init__(self, comic_id: int, mainGUI: MainGUI) -> None:
"https://manga.bilibili.com/twirp/comic.v1.Comic/ComicDetail?device=pc&platform=web"
)
self.headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"origin": "https://manga.bilibili.com",
"referer": f"https://manga.bilibili.com/detail/mc{comic_id}?from=manga_homepage",
"cookie": f"SESSDATA={mainGUI.getConfig('cookie')}",
Expand Down
10 changes: 8 additions & 2 deletions src/DownloadManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
该模块包含了一个下载管理器类,用于管理漫画下载任务的创建、更新和删除等操作
"""

import re
import time
from concurrent.futures import ThreadPoolExecutor

Expand Down Expand Up @@ -151,11 +152,16 @@ def __thread__EpisodeTask(self, curr_id: int, epi: Episode) -> None:
if self.terminated:
epi.clear(imgs_path)
return
if img.get("token") is not None:
if img.get("token"):
img_url = f"{img['url']}?token={img['token']}"
elif img.get("complete_url"):
img_url = img["complete_url"]
else:
img_url = img["url"]
img_path = epi.downloadImg(index, img_url)
img_url = re.sub(r"@[^?]*\?", "", img_url)
match_cpx = re.search(r"[?&]cpx=([^&]*)", img_url)
cpx = match_cpx.group(1) if match_cpx else ""
img_path = epi.downloadImg(index, img_url, cpx)
if img_path is None:
self.reportError(curr_id)
epi.clearAfterSave(imgs_path)
Expand Down
70 changes: 56 additions & 14 deletions src/Episode.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

from __future__ import annotations

import base64
import glob
import json
import os
import re
import shutil
from typing import TYPE_CHECKING
from urllib.parse import unquote
from zipfile import ZipFile, ZIP_DEFLATED

import piexif
Expand All @@ -30,6 +32,7 @@
__copyright__,
__version__,
isCheckSumValid,
AES_CBCDecrypt,
logger,
myStrFilter,
)
Expand Down Expand Up @@ -94,7 +97,7 @@ def __init__(
self.title = re.sub(r"^([0-9\-\.]+)$", r"第\1话", self.title)

self.headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"origin": "https://manga.bilibili.com",
"referer": f"https://manga.bilibili.com/detail/mc{comic_id}/{self.id}?from=manga_homepage",
"cookie": f"SESSDATA={mainGUI.getConfig('cookie')}",
Expand Down Expand Up @@ -511,12 +514,13 @@ def _() -> None:

############################################################

def downloadImg(self, index: int, img_url: str) -> str:
"""根据 url 和 token 下载图片
def downloadImg(self, index: int, img_url: str, cpx: str) -> str:
"""根据 url 和 cpx 下载图片
Args:
index (int): 章节中图片的序号
img_url (str): 图片的合法 url
cpx (str): 图片的加密密钥
Returns:
str: 图片的保存路径
Expand All @@ -525,12 +529,14 @@ def downloadImg(self, index: int, img_url: str) -> str:
# ?###########################################################
# ? 下载图片
@retry(stop_max_delay=MAX_RETRY_LARGE, wait_exponential_multiplier=RETRY_WAIT_EX)
def _() -> bytes:
def _() -> list[bytes, str, bool]:
try:
if img_url.find("token") != -1:
res = requests.get(img_url, timeout=TIMEOUT_LARGE)
else:
elif img_url:
res = requests.get(img_url, headers=self.headers, timeout=TIMEOUT_LARGE)
else:
raise requests.RequestException

except requests.RequestException as e:
logger.warning(
Expand All @@ -543,17 +549,12 @@ def _() -> bytes:
f"状态码:{res.status_code}, 理由: {res.reason} 重试中..."
)
raise requests.HTTPError()
isValid, md5 = isCheckSumValid(res.headers["Etag"], res.content)
if not isValid:
logger.warning(
f"《{self.comic_name}》章节:{self.title} - {index} - {img_url} - 下载内容Checksum不正确! 重试中...\n"
f"\t{res.headers['Etag']}{md5}"
)
raise requests.HTTPError()
return res.content
md5 = res.headers["Etag"]
hit_encrypt = not res.headers.get("content-type").startswith("image/")
return res.content, md5, hit_encrypt

try:
img = _()
img, md5, hit_encrypt = _()
except requests.RequestException as e:
logger.error(
f"《{self.comic_name}》章节:{self.title} - {index} - {img_url} 重复下载图片多次后失败!\n{e}"
Expand All @@ -564,6 +565,47 @@ def _() -> bytes:
)
return None

# ?###########################################################
# ? 解密图片
@retry(stop_max_attempt_number=1)
def _() -> None:
nonlocal img
if not cpx or not hit_encrypt or not img:
return
cpx_text = unquote(cpx)
cpx_char = base64.b64decode(cpx_text)
iv = cpx_char[60:76]
img_flag = img[0]
if img_flag == 0:
raise ValueError("图片文件读取异常!")
data_length = int.from_bytes(img[1: 5])
key = img[data_length + 5:]
content = img[5:data_length + 5]
head = AES_CBCDecrypt(content[0:20496], key, iv)
img = head + content[20496:]

try:
_()
isValid, img_md5 = isCheckSumValid(md5, img)
if not isValid:
logger.warning(
f"《{self.comic_name}》章节:{self.title} - {index} - {img_url} - 下载内容Checksum不正确! 重试中...\n"
f"\t{md5}{img_md5}"
)
raise requests.HTTPError()
except OSError as e:
logger.error(
f"《{self.comic_name}》章节:{self.title} - {index} - {img_url} - {path_to_save} - 处理图片失败!\n{e}"
)
logger.exception(e)
self.mainGUI.signal_message_box.emit(
f"《{self.comic_name}》章节:{self.title} - {index} - 处理图片失败!\n"
f"已暂时跳过此章节, 并删除所有缓存文件!\n"
f"请重新尝试或者重启软件!\n\n"
f"更多详细信息请查看日志文件, 或联系开发者!"
)
raise e

# ?###########################################################
# ? 保存图片
img_format = img_url.split(".")[-1].split("?")[0].lower().replace("&append=", "")
Expand Down
2 changes: 1 addition & 1 deletion src/SearchComic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, comic_name: str, sessdata: str) -> None:
"https://manga.bilibili.com/twirp/comic.v1.Comic/Search?device=pc&platform=web"
)
self.headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"origin": "https://manga.bilibili.com",
"referer": "https://manga.bilibili.com/search?from=manga_homepage",
"cookie": f"SESSDATA={sessdata}",
Expand Down
31 changes: 28 additions & 3 deletions src/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import base64
import ctypes
import hashlib
import logging
Expand All @@ -13,6 +14,8 @@
from logging.handlers import TimedRotatingFileHandler
from sys import platform
from typing import TYPE_CHECKING
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES

import requests
from PySide6.QtCore import Qt, QUrl
Expand Down Expand Up @@ -145,14 +148,36 @@ def sizeToBytes(size_str: str) -> int:
############################################################


def isCheckSumValid(etag, content) -> tuple[bool, str]:
def isCheckSumValid(etag: str, content) -> tuple[bool, str]:
"""判断MD5是否有效
Returns:
tuple[bool, str]: (是否有效, MD5)
"""
md5 = hashlib.md5(content).hexdigest()
return etag == md5, md5
md5 = base64.b64encode(hashlib.md5(content).digest()).decode()
if md5 == etag:
return md5 == etag, etag
else:
md5 = hashlib.md5(content).hexdigest()
return md5 == etag, etag

############################################################


def AES_CBCDecrypt(cipher_data: bytes, key: bytes, iv: bytes) -> bytes:
"""使用AES-CBC进行解密
Args:
cipher_data (str): 加密数据
key (str): 密钥
iv (str): 初始化向量
Returns:
bytes: 解密数据
"""
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(cipher.decrypt(cipher_data), AES.block_size)
return decrypted_data


############################################################
Expand Down
27 changes: 16 additions & 11 deletions src/ui/MangaUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ def updateMyLibrary(self, notice: bool = False) -> bool:
self.updateMyLibrarySingle,
comic_id,
comic_info["comic_path"],
comic_info["root_path"],
)
for comic_id, comic_info in self.mainGUI.my_library.items()
)
Expand Down Expand Up @@ -274,15 +275,16 @@ def updateMyLibraryWatcher(self, futures: list, notice: bool) -> None:
self.mainGUI.label_myLibrary_tip.setText("(右键打开文件夹)")

############################################################
def updateMyLibrarySingle(self, comic_id: int, comic_path: str) -> int | None:
def updateMyLibrarySingle(self, comic_id: int, comic_path: str, root_path: str) -> int | None:
"""添加单个漫画到我的库存
Args:
comic_id (int): 漫画ID
comic_path (str): 漫画保存路径
root_path (str): 漫画保存路径上级
"""

comic = Comic(comic_id, self.mainGUI)
comic = Comic(comic_id, self.mainGUI, root_path)
data = comic.getComicInfo()
# ? 获取漫画信息失败直接跳过
if not data:
Expand Down Expand Up @@ -547,6 +549,7 @@ def getEpisodeList(self, comic: Comic) -> None:
background = QColor(0, 255, 0, 50)
if not epi.isAvailable():
flags = Qt.ItemFlag.NoItemFlags
background = QColor(0, 0, 0, 40)
else:
num_unlocked += 1

Expand Down Expand Up @@ -922,15 +925,17 @@ def get_meta_dict(self, path: str) -> dict:

meta_dict = {}
try:
for item in os.listdir(path):
if os.path.exists(os.path.join(path, item, "元数据.json")):
with open(os.path.join(path, item, "元数据.json"), "r", encoding="utf-8") as f:
comic_path = os.path.join(path, item)
data = json.load(f)
meta_dict[data["id"]] = {
"comic_name": data["title"],
"comic_path": comic_path,
}
for dirpath, dirs, files in os.walk(path):
for item in dirs:
if os.path.exists(os.path.join(dirpath, item, "元数据.json")):
with open(os.path.join(dirpath, item, "元数据.json"), "r", encoding="utf-8") as f:
comic_path = os.path.join(dirpath, item)
data = json.load(f)
meta_dict[data["id"]] = {
"comic_name": data["title"],
"comic_path": comic_path,
"root_path": path,
}
except (OSError, ValueError) as e:
logger.error(f"读取元数据时发生错误\n {e}")
return meta_dict
Loading

0 comments on commit 5dce5af

Please sign in to comment.