This commit is contained in:
cwx committed 2025-07-11 09:04:51 +08:00
1 parent 4874600a07
commit 3a45a11a65
7 files changed
+352 -70

No files matched your search

+121 -14
View File
@@ -1,20 +1,16 @@
import os, re, requests, hashlib
import xml.etree.ElementTree as ET
from xml.dom import minidom
from typing import List
import os
from lxml import etree
from src.config import XSD_FILE
from src.common.logging import setup_logging
import logging
from zipfile import ZipFile
from pathlib import Path
import re
import requests
from urllib.parse import urlparse
from PIL import Image
from concurrent.futures import ThreadPoolExecutor
import hashlib
from io import BytesIO
from dataclasses import dataclass
logger = setup_logging(__name__)
@@ -207,8 +203,12 @@ class ImageInfo:
class ComicInfo:
# ComicInfo.xml 中的选项
def __init__(self):
self._init_default_values()
def _init_default_values(self):
"""初始化默认值"""
self.Title: str = ""
"""标题"""
"""<h2>标题</h2>"""
self.Series: str = ""
self.Number: str = ""
self.Count: int = -1
@@ -245,11 +245,20 @@ class ComicInfo:
self.StoryArc: str = ""
self.SeriesGroup: str = ""
self.AgeRating: str = ""
self.Pages: List[ComicPageInfo] = []
self.Pages: List[ComicPageInfo] = []
def __init__(self, **kwargs):
values = kwargs.values()
self._init_default_values()
if len(values) > 0:
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
@dataclass
class ComicPageInfo:
# ComicInfo.xml 中的<Page>
def __init__(self):
def _init_default_values(self):
self.Image: int = -1
self.Type: str = "Story"
self.DoublePage: bool = False
@@ -257,8 +266,19 @@ class ComicPageInfo:
self.Key: str = ""
self.Bookmark: str = ""
self.ImageWidth: int = -1
self.ImageHeight: int = -1
self.ImageHeight: int = -1
def __init__(self):
self._init_default_values()
def __init__(self, **kwargs):
values = kwargs.values()
self._init_default_values()
if len(values) > 0:
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
def toString(self):
data = {}
def add(key, value):
@@ -314,7 +334,7 @@ class ComicInfoXml:
logger.debug(f"zip_file={zip_file} PageCount: {page_count}")
return page_count
def _parse_comicinfo(self, comic: ComicInfo, save_dir=None, xml_filename="ComicInfo.xml", xsd_filename="ComicInfo.xsd"):
def _parse_comicinfo(self, comic: ComicInfo, save_dir=None, xml_filename="ComicInfo.xml", xsd_filename=XSD_FILE) -> Path:
"""_summary_
Args:
@@ -368,12 +388,13 @@ class ComicInfoXml:
self._validate_xml_with_xsd_file(xml_filename, xsd_filename) # 将 JSON 转换为 XML
#xml_data = json_to_xml_with_declaration(json_data)
#print(xml_data)
return Path(xml_filename)
def _required_attributes(self):
"""
必需值,如果为空刚报错
"""
return ["Title", "Series", "Number", "PageCount", "Writer"]
return ["Title", "Series", "Number", "Writer", "PageCount", "Pages" ]
def _gen_pageinfo(self, image_names, save_dir):
""" 获取PageInfo数据
@@ -387,7 +408,92 @@ class ComicInfoXml:
# 图像属性 文件名 大小 长
pages.append(page)
return pages
def _xml_file_to_comicinfo(self, cbz_path=None, xml_file=None) -> ComicInfo:
""" 读取 CBZ 文件或 XML 文件中的 ComicInfo.xml 元数据,返回 ComicInfo 对象
Args:
以下参数任意一个都可以
cbz_path (_type_, optional): 任选择CBZ文件路径或XML文件路径. Defaults to None.
xml_file (_type_, optional): XML文件路径. Defaults to None
Returns:
ci: returns a ComicInfo object with the updated page count
"""
def xml_parse(xml_str) -> ComicInfo:
""" 解析 XML 字符串并提取指定字段的值
Args:
xml_str (_type_): xml文件内容字符串
keys (_type_): 需要提取的字段列表
Returns:
ci: returns a ComicInfo object with the extracted values
"""
import xmltodict
xml_dict = xmltodict.parse(xml_str)
# 解析 XML 元数据
metadata = {}
# 获取comicinfo.xml 字段
keys = ComicInfo().__dict__.keys()
for key in keys:
key_element = xml_dict.get("ComicInfo", {}).get(key, "")
if key == "Pages":
pages = []
page_list_element = xml_dict.get("ComicInfo", {}).get("Pages", []).get("Page", "")
for page_element in page_list_element:
pages.append(ComicPageInfo(**{ "Image": page_element['@Image'],
"ImageSize": int(page_element['@ImageSize']),
"Key": page_element['@Key'],
"ImageWidth": int(page_element['@ImageWidth']),
"ImageHeight": int(page_element['@ImageHeight'])}))
key_element = pages
if key_element is not None:
metadata[key] = key_element if key_element else ""
else:
metadata[key] = ""
return ComicInfo(**metadata)
def read_zip_file(zip_file_path):
"""读取 ZIP 文件并返回其内容"""
try:
with ZipFile(zip_file_path, 'r') as zip_ref:
# 获取 ZIP 文件中的所有文件名
file_list = zip_ref.namelist()
# 读取 ComicInfo.xml 文件内容
if 'ComicInfo.xml' in file_list:
with zip_ref.open('ComicInfo.xml') as xml_file:
return xml_file.read().decode('utf-8')
else:
raise FileNotFoundError("ComicInfo.xml not found in the ZIP file.")
except Exception as e:
print(f"处理 CBZ 文件时出错: {e}")
raise exit(f"处理CBZ出错")
if cbz_path is not None:
xml_content = read_zip_file(cbz_path)
elif xml_file is not None:
with open(xml_file, 'r', encoding='utf-8') as f:
xml_content = f.read()
else:
raise ValueError("请提供 cbz_path 或 xml_file 参数, 否则无法处理 XML 文件")
return xml_parse(xml_content)
def update_comicinfo_count(self, count, cbz_path: Path) -> Path:
""" 更新 ComicInfo.xml 中的 PageCount 字段
Args:
cbz_path (Path): CBZ 文件路径
xml_filename (str, optional): XML 文件名. Defaults to "ComicInfo.xml".
xsd_filename (str, optional): XSD 文件名. Defaults to "ComicInfo.xsd".
Returns:
pages: 返回更新后的页面信息列表
"""
# 读取 ComicInfo.xml 文件
comicinfo = self._xml_file_to_comicinfo(cbz_path=cbz_path)
comicinfo.Count = count
# 保存更新后的 ComicInfo.xml 文件
return self._parse_comicinfo(comicinfo, save_dir=os.path.dirname(cbz_path))
def scrapy_xml_by_json(self, json_data, save_dir=None, xsd_file=XSD_FILE):
""" 根据Json数据生成ComicInfo.xml
"""
@@ -395,6 +501,7 @@ class ComicInfoXml:
comic.Title = json_data.get("chapter", "")
comic.Series = json_data.get("name", "")
comic.Writer = json_data.get("author", "")
comic.Count = json_data.get("count", -1)
comic.AgeRating = json_data.get("age_rating", "")
comic.Tags = json_data.get("tags", "")
comic.Summary = json_data.get("description", "")
+14
View File
@@ -122,6 +122,14 @@ class MangaInfo(BaseModel):
list_value.append(val)
return FileNaming.chinese_file_name(",".join(list_value))
status: str
"""漫画状态"""
@field_validator('status', mode='before')
def validate_status(cls, v):
if isinstance(v, str):
return FileNaming.chinese_file_name(v)
return v
description: Optional[str] = None
"""漫画描述"""
@field_validator('description', mode='before')
@@ -235,11 +243,17 @@ class MangaItem(BaseModel):
filename_list = []
for image in cls.chapter_images:
filename_list.append(image.filename)
count = -1
if cls.info.status == "已完结" and len(cls.chapters) > 1:
# 本章节为最终章节刚添加Count字段
if cls.number > 0 and cls.number == len(cls.chapters):
count = len(cls.chapters)
return {
"name": cls.info.title,
"chapter": cls.chapter.title,
"author": cls.info.author,
"count" : count,
"tags": cls.info.tags,
"images": filename_list,
"description": cls.info.description,
+31 -1
View File
@@ -468,7 +468,37 @@ class CBZUtils:
if old_img > 0:
#os.remove(cbz_path)
print(f"remove cbz {cbz_path}")
def update_cbz_with_new_xml(self, cbz_path, new_xml_content, output_path=None):
"""将新生成的 ComicInfo.xml 更新到 CBZ 文件中"""
try:
# 默认输出路径为原文件路径(覆盖原文件)
if output_path is None:
output_path = cbz_path
# 创建临时文件处理覆盖操作
with NamedTemporaryFile(delete=False) as tmp:
tmp.close()
shutil.move(cbz_path, tmp.name)
# 读取原文件并替换 ComicInfo.xml
with ZipFile(tmp.name, 'r') as source_zip:
with ZipFile(output_path, 'w') as new_zip:
# 复制原文件(跳过旧 XML)
for item in source_zip.infolist():
if item.filename.lower() != 'comicinfo.xml':
new_zip.writestr(item, source_zip.read(item.filename))
# 添加新 XML
new_zip.writestr("ComicInfo.xml", new_xml_content)
os.remove(tmp.name) # 清理临时文件
return True
except Exception as e:
print(f"更新 CBZ 文件失败: {e}")
if os.path.exists(tmp.name):
shutil.move(tmp.name, cbz_path) # 恢复备份
raise exit(f"更新失败")
class ImageUtils:
@classmethod
+18 -1
View File
@@ -11,7 +11,7 @@ from src.common.item import Chapter, MangaItem, MangaInfo,CoverItem
from src.common.exceptions import SiteError, NetworkError, ParseError
from src.common.logging import setup_logging
from src.common.naming import DirectoryNaming,FileNaming
from src.common.ComicInfo import ComicInfo, ImageInfo
from src.common.ComicInfo import ComicInfo, ImageInfo, ComicInfoXml
logger = setup_logging(__name__)
@@ -88,10 +88,27 @@ class BaseSite(ABC):
try:
# result_type list[Chapter]
list_chapter = manga_info.get_list_chapter()
# 临时添加begin
# 获取最新章节
last_chapter = list_chapter[-1] if list_chapter else []
# 临时添加end
down_chapter = []
for chapter in list_chapter:
cbz_path = FileNaming.chapter_cbz(manga_info=manga_info,chapter=chapter)
if os.path.exists(cbz_path):
# 临时添加begin
if chapter.title == last_chapter.title and manga_info.status == "已完结":
# 如果是最新章节且漫画已完结,则不再下
ci = ComicInfoXml()._xml_file_to_comicinfo(cbz_path=cbz_path)
if ci.Count == "":
# 生成ComicInfo.xml
xml_path = ComicInfoXml().update_comicinfo_count(count=len(list_chapter), cbz_path=cbz_path)
# 更新ComicInfo.xml至CBZ文件中
CBZUtils().update_cbz_with_new_xml(cbz_path, xml_path.read_text(encoding="utf-8"))
# 更新完成后删除临时生成的ComicInfo.xml
xml_path.unlink()
logger.debug(f"更新 {cbz_path} 的 ComicInfo.xml Count完成")
# 临时添加end
logger.debug(f"{chapter.title} 章节已存在")
chapter.status = "downloaded"
down_chapter.append(chapter)
+3
View File
@@ -13,6 +13,9 @@ selectors:
author:
selector: '//div[@class="basis-3/5 text-sm sm:text-base"]//span[@class="text-foreground"]/text()'
index: 0
status:
selector: '//div[@class="basis-3/5 text-sm sm:text-base"]//span[@class="text-foreground"]/text()'
index: 1
description:
selector: '//div[@class="my-2 text-foreground text-sm sm:text-base"]/p/text()'
index: 1