from pathlib import Path from typing import Dict, Type, Optional from src.config import BASE_IMAGES_DIR from src.sites.base import BaseSite from src.sites.configs.rouman import RoumanSite from src.common.utils import MangaDownloader, CBZUtils, MangaUtils from src.common.naming import DirectoryNaming, FileNaming from src.common.exceptions import MangaException from src.common.item import MangaItem, MangaInfo from src.common.logging import setup_logging from src.common.ComicInfo import ComicInfoXml logger = setup_logging(__name__) class MangaManager: """漫画下载管理器""" SITE_MAP: Dict[str, Type[BaseSite]] = { # 'manhuagui.com': ManhuaguiSite, 'roum26.xyz': RoumanSite, 'rouman5.com': RoumanSite, # 在这里添加更多网站支持 } def __init__(self, base_dir: Path = BASE_IMAGES_DIR): self.downloader = MangaDownloader(base_dir) def get_site_handler(self, url: str) -> Optional[Type[BaseSite]]: """根据URL获取对应的网站处理器""" for domain, handler in self.SITE_MAP.items(): if domain in url: return handler return None async def process_manga( self, url: str, volume_num: int = 1, status_callback = None ): """处理漫画下载""" # 获取网站处理器 site_handler = self.get_site_handler(url) if not site_handler: raise MangaException(f"不支持的网站: {url}") async with site_handler() as site: # 下载整部漫画 async for result in site.download_manga(url): if result['type'] == 'info': manga_info = result['data'] logger.debug(f"漫画信息: {manga_info}") # 使用 MangaItem 保存数据 manga_item = MangaItem(info=manga_info, chapters=[]) manga_name = manga_info.title # 创建命名策略 #self.manga_path = NamingStrategy.manga_volume_path( # manga_name, # volume_num=volume_num #) #self.manga_filename = NamingStrategy.custom_manga_filename( # prefix="page", # digits=3 #) elif result['type'] == 'chapters': chapters = result['data'] total = 0 for chapter in chapters: if not chapter.status == "downloaded": total += 1 total_chapters = total logger.info(f"找到 {total_chapters} 个章节") manga_item.chapters.extend(chapters) # 添加章节到 MangaItem yield { 'type': 'progress', 'total_chapters': total_chapters } elif result['type'] == 'cover': await self.downloader.download_cover(manga_info) yield { 'type': 'cover_complete', 'item': manga_item } elif result['type'] == 'chapter': manga_item = result['item'] chapter = manga_item.chapter # 生成章节图像工作目录 chapter_dir = DirectoryNaming.chapter_images_dir(manga_info, chapter) DirectoryNaming.ensure_dir(chapter_dir) try: # 下载章节 download_result = await self.downloader.download_chapter( manga_item, #filename_generator=self.manga_filename, #path_generator=self.manga_path, status_callback=status_callback ) # 章节下载完成后处理流程 start # 下载完成后生成 ComicInfo.xml if int(download_result['success']) == int(download_result['total']): cbz_path = FileNaming.chapter_cbz(manga_info, chapter) # 解密图片 CBZUtils(cbz_path)._image_deScrambleByPath(chapter_dir) ComicInfoXml().scrapy_xml_by_json(manga_item.get_comic_info_json(), chapter_dir) # 打包成 CBZ 文件 CBZUtils(cbz_path).create_cbz(chapter_dir, clear_chapter=True) # 章节下载完成后处理流程 end yield { 'type': 'chapter_complete', 'chapter': chapter, 'result': download_result } except Exception as e: logger.error(f"下载章节 {chapter['title']} 失败: {str(e)}") yield { 'type': 'chapter_error', 'chapter': chapter, 'error': str(e) } elif result['type'] == 'error': logger.error(f"错误: {result['error']}") yield { 'type': 'error', 'error': result['error'] } @staticmethod def print_progress(status): """打印下载进度""" progress_bar_length = 30 # 进度条长度 progress = int(status.progress * progress_bar_length) bar = '#' * progress + '-' * (progress_bar_length - progress) print(f"\r下载进度: |{bar}| {status.current}/{status.total} " f"({status.progress:.1%})", end="") async def download_list_manga(self, manga_url: str): # 获取网站处理器 list_site_handler = self.get_site_handler(manga_url) if not list_site_handler: raise MangaException(f"不支持的网站: {manga_url}") async with list_site_handler() as site: manga_list = await site.get_manga_list(manga_url) for title,url,updated_at in zip(manga_list.title, manga_list.url, manga_list.updated_at): title = FileNaming.chinese_file_name(title) save_manga = MangaUtils().search_manga(title) updated = None if save_manga != None: updated = save_manga.get('updated_at', None) if updated != None and updated_at == updated: updated = save_manga.get('updated_at', None) logger.debug(f"{save_manga} 已存在") else: logger.info(f"开始下载 漫画: {title}") logger.info(f"{url}") await self.download_manga(str(url), title = title, updated_at = updated_at ) @classmethod async def download_manga(cls, url: str, title: str = None, updated_at: str = None, save_dir: Path = BASE_IMAGES_DIR): """下载漫画""" manager = MangaManager(save_dir) try: total_chapters = 0 completed_chapters = 0 success_chapters = 0 async for result in manager.process_manga(url, status_callback=cls.print_progress): if result['type'] == 'progress': total_chapters = result['total_chapters'] logger.info(f"开始下载,共 {total_chapters} 章") elif result['type'] == 'chapter_complete': completed_chapters += 1 chapter_result = result['result'] if chapter_result['failed']: logger.warning( f"章节 {result['chapter']} 完成: " f"{chapter_result['success']}/{chapter_result['total']} 张图片成功, " f"{chapter_result['failed']} 张失败" ) else: success_chapters += 1 logger.info(f"\n章节 {result['chapter']} 完成") if title != None: print(f"\n {title}") print(f"总进度: {completed_chapters}/{total_chapters} 章") elif result['type'] == 'chapter_error': logger.error(f"章节 {result['chapter']} 下载失败: {result['error']}") elif result['type'] == 'error': logger.error(f"下载出错: {result['error']}") # 全部下载完成 if int(total_chapters) == int(success_chapters) and title != None and updated_at != None: MangaUtils().add_manga(title, updated_at=updated_at) logger.info(f"全部完成 {title}, {updated_at}") except MangaException as e: logger.error(f"下载失败: {str(e)}") except Exception as e: logger.error(f"未知错误: {str(e)}")