175 lines
6.6 KiB
Python
175 lines
6.6 KiB
Python
import asyncio
|
|
import tempfile
|
|
import xml.etree.ElementTree as XML
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
import aiohttp
|
|
import questionary
|
|
from aiohttp import ClientError
|
|
from rich import print, status
|
|
from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TextColumn
|
|
|
|
from packager import zip_files
|
|
from sites import Provider
|
|
|
|
|
|
class Manga:
|
|
def __init__(self, provider: Provider, cached: list[float] = []) -> None:
|
|
self.site = provider
|
|
self.cached = cached
|
|
|
|
spinner = status.Status("Fetching manga info")
|
|
spinner.start()
|
|
self.info, self.chapters = self.site.get_mediainfo()
|
|
spinner.stop()
|
|
|
|
self.slug = self.slugify(self.info.get("series")) # type: ignore
|
|
|
|
@staticmethod
|
|
def slugify(string: str) -> str:
|
|
string = "".join(x for x in string if not x in '<>:"/\\|?*')
|
|
return str(string)
|
|
|
|
def _generateComicInfo(self, chapter: dict, dir: Path):
|
|
filename = dir.joinpath("ComicInfo.xml")
|
|
ComicInfo = XML.Element("ComicInfo")
|
|
|
|
XML.SubElement(ComicInfo, "Series").text = self.info.get("series")
|
|
XML.SubElement(ComicInfo, "Writer").text = ",".join(self.info.get("writer", [])) # type: ignore
|
|
XML.SubElement(ComicInfo, "Penciller").text = ",".join(self.info.get("penciller", [])) # type: ignore
|
|
XML.SubElement(ComicInfo, "Genre").text = ",".join(self.info.get("genre", [])) # type: ignore
|
|
XML.SubElement(ComicInfo, "Summary").text = self.info.get("summary")
|
|
XML.SubElement(ComicInfo, "Number").text = str(chapter.get("nr"))
|
|
XML.SubElement(ComicInfo, "Title").text = chapter.get("title")
|
|
XML.SubElement(ComicInfo, "LanguageISO").text = self.info.get("languageISO")
|
|
XML.SubElement(ComicInfo, "PageCount").text = str(len(chapter.get("images"))) # type: ignore
|
|
XML.SubElement(ComicInfo, "ScanInformation").text = self.info.get(
|
|
"scanInformation"
|
|
)
|
|
|
|
tree = XML.ElementTree(ComicInfo)
|
|
|
|
XML.indent(tree, space="\t", level=0)
|
|
tree.write(str(filename), encoding="utf-8")
|
|
|
|
return filename
|
|
|
|
@staticmethod
|
|
def printMangaInfo(info: dict):
|
|
print(info)
|
|
|
|
def choose_chapters(self, auto):
|
|
choices = [
|
|
questionary.Choice(
|
|
f"{chapter.get('nr'):g}: {chapter.get('title')}",
|
|
chapter,
|
|
checked=chapter.get("nr") not in self.cached,
|
|
)
|
|
for chapter in self.chapters
|
|
]
|
|
self.chapters = (
|
|
questionary.checkbox("Choose which chapters to download", choices)
|
|
.skip_if(
|
|
auto,
|
|
[
|
|
chapter
|
|
for chapter in self.chapters
|
|
if chapter.get("nr") not in self.cached
|
|
],
|
|
)
|
|
.ask()
|
|
)
|
|
|
|
def download(self, save_dir: Path):
|
|
if not self.chapters:
|
|
print("No new [orange3]Chapter[/] were found...")
|
|
return
|
|
|
|
async def process_all_chapters():
|
|
|
|
chapter_sem = asyncio.Semaphore(8)
|
|
image_sem = asyncio.Semaphore(40)
|
|
|
|
connector = aiohttp.TCPConnector(limit=100, ttl_dns_cache=300)
|
|
async with aiohttp.ClientSession(
|
|
headers=self.site.headers, connector=connector
|
|
) as session:
|
|
with Progress(
|
|
TextColumn("[progress.description]{task.description}"),
|
|
MofNCompleteColumn(),
|
|
SpinnerColumn(),
|
|
) as p:
|
|
task = p.add_task(
|
|
f"[bold]Processing [orange3]{self.info.get('series')}[/]",
|
|
total=len(self.chapters),
|
|
)
|
|
|
|
async def download_and_zip(chapter):
|
|
async with chapter_sem:
|
|
chapter_nr = chapter.get("nr")
|
|
chapter_temp = Path(temp_dir) / f"ch_{chapter_nr:g}"
|
|
chapter_temp.mkdir(parents=True, exist_ok=True)
|
|
info_file = self._generateComicInfo(chapter, chapter_temp)
|
|
img_urls = chapter.get("images", [])
|
|
img_files = await self._async_download_images_internal(
|
|
img_urls, chapter_temp, session, image_sem
|
|
)
|
|
|
|
all_files = [info_file] + [f for f in img_files if f]
|
|
zip_files(self.slug, chapter, all_files, save_dir)
|
|
|
|
self.cached.append(chapter_nr)
|
|
p.update(task, advance=1)
|
|
|
|
await asyncio.gather(
|
|
*(download_and_zip(ch) for ch in self.chapters)
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="manga_") as temp_dir:
|
|
asyncio.run(process_all_chapters())
|
|
|
|
async def _async_download_images_internal(self, urls, temp_dir, session, semaphore):
|
|
"""Worker to manage the image download tasks for a single chapter."""
|
|
|
|
async def bounded_download(url):
|
|
async with semaphore:
|
|
return await self.download_image(
|
|
session, url, temp_dir, self.site.headers
|
|
)
|
|
|
|
tasks = [bounded_download(url) for url in urls]
|
|
return await asyncio.gather(*tasks)
|
|
|
|
@staticmethod
|
|
async def download_image(
|
|
session: aiohttp.ClientSession,
|
|
url: str,
|
|
temp_dir: Path,
|
|
headers: dict,
|
|
retries=3,
|
|
timeout=10,
|
|
):
|
|
|
|
filename = temp_dir.joinpath(Path(url).name)
|
|
|
|
for attempt in range(1, retries + 1):
|
|
try:
|
|
async with session.get(url, headers=headers, timeout=timeout) as response: # type: ignore
|
|
if response.status == 200:
|
|
async with aiofiles.open(filename, "wb") as f:
|
|
await f.write(await response.read())
|
|
return filename
|
|
else:
|
|
raise ClientError(f"Bad status {response.status} for {url}")
|
|
except (
|
|
asyncio.TimeoutError,
|
|
ClientError,
|
|
aiohttp.ClientConnectorError,
|
|
) as e:
|
|
if attempt < retries:
|
|
await asyncio.sleep(2**attempt)
|
|
else:
|
|
print(f"Failed to download {url}: {e}")
|
|
return None
|