add feature to download a whole volume. and add doc
Some checks failed
ci/woodpecker/push/tests Pipeline failed
Some checks failed
ci/woodpecker/push/tests Pipeline failed
This commit is contained in:
parent
162cb59d28
commit
e3d46e48c4
5 changed files with 107 additions and 48 deletions
41
README.md
41
README.md
|
@ -97,6 +97,47 @@ options:
|
|||
./downloads/mangatitle/chaptertitle(.cbz)
|
||||
```
|
||||
|
||||
### Select chapters to download
|
||||
|
||||
> With the option `-c "all"` you download every chapter available in the selected language
|
||||
|
||||
To download specific chapters you can use the option `-c` or `--chapters`. That you don't have to specify all chapters
|
||||
individually, the script has some logic to fill in the blanks.
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
# if you want to download chapters 1 to 5
|
||||
python3 manga-dlp -u <url> -c 1-5
|
||||
|
||||
# if you want to download chapters 1 and 5
|
||||
python3 manga-dlp -u <url> -c 1,5
|
||||
```
|
||||
|
||||
If you use `--forcevol` it's the same, just with the volume number
|
||||
|
||||
```sh
|
||||
# if you want to download chapters 1:1 to 1:5
|
||||
python3 manga-dlp -u <url> -c 1:1-1:5
|
||||
|
||||
# if you want to download chapters 1:1 and 1:5
|
||||
python3 manga-dlp -u <url> -c 1:1,1:5
|
||||
|
||||
# to download the whole volume 1
|
||||
python3 manga-dlp -u <url> -c 1:
|
||||
```
|
||||
|
||||
And a combination of all
|
||||
|
||||
```sh
|
||||
# if you want to download chapters 1 to 5 and 9
|
||||
python3 manga-dlp -u <url> -c 1-5,9
|
||||
|
||||
# with --forcevol
|
||||
# if you want to download chapters 1:1 to 1:5 and 9, also the whole volume 4
|
||||
python3 manga-dlp -u <url> -c 1:1-1:5,1:9,4:
|
||||
```
|
||||
|
||||
### Read list of links from file
|
||||
|
||||
With the option `--read` you can specify a file with links to multiple mangas. They will be parsed from top to bottom
|
||||
|
|
|
@ -11,7 +11,7 @@ class Mangadex:
|
|||
img_base_url = "https://uploads.mangadex.org"
|
||||
|
||||
# get infos to initiate class
|
||||
def __init__(self, url_uuid, language, forcevol, verbose):
|
||||
def __init__(self, url_uuid: str, language: str, forcevol: bool, verbose: bool):
|
||||
# static info
|
||||
self.url_uuid = url_uuid
|
||||
self.language = language
|
||||
|
@ -31,7 +31,7 @@ class Mangadex:
|
|||
self.chapter_list = self.create_chapter_list()
|
||||
|
||||
# make initial request
|
||||
def get_manga_data(self):
|
||||
def get_manga_data(self) -> requests:
|
||||
if self.verbose:
|
||||
print(f"INFO: Getting manga data for: {self.manga_uuid}")
|
||||
counter = 1
|
||||
|
@ -58,7 +58,7 @@ class Mangadex:
|
|||
return manga_data
|
||||
|
||||
# get the uuid for the manga
|
||||
def get_manga_uuid(self):
|
||||
def get_manga_uuid(self) -> str:
|
||||
# isolate id from url
|
||||
uuid_regex = re.compile(
|
||||
"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"
|
||||
|
@ -71,7 +71,7 @@ class Mangadex:
|
|||
return manga_uuid
|
||||
|
||||
# get the title of the manga (and fix the filename)
|
||||
def get_manga_title(self):
|
||||
def get_manga_title(self) -> str:
|
||||
if self.verbose:
|
||||
print(f"INFO: Getting manga title for: {self.manga_uuid}")
|
||||
manga_data = self.manga_data.json()
|
||||
|
@ -90,7 +90,7 @@ class Mangadex:
|
|||
return utils.fix_name(title)
|
||||
|
||||
# check if chapters are available in requested language
|
||||
def check_chapter_lang(self):
|
||||
def check_chapter_lang(self) -> int:
|
||||
if self.verbose:
|
||||
print(
|
||||
f"INFO: Checking for chapters in specified language for: {self.manga_uuid}"
|
||||
|
@ -113,7 +113,7 @@ class Mangadex:
|
|||
return total_chapters
|
||||
|
||||
# get chapter data like name, uuid etc
|
||||
def get_chapter_data(self):
|
||||
def get_chapter_data(self) -> dict:
|
||||
if self.verbose:
|
||||
print(f"INFO: Getting chapter data for: {self.manga_uuid}")
|
||||
api_sorting = "order[chapter]=asc&order[volume]=asc"
|
||||
|
@ -172,7 +172,7 @@ class Mangadex:
|
|||
return chapter_data
|
||||
|
||||
# get images for the chapter (mangadex@home)
|
||||
def get_chapter_images(self, chapter, wait_time):
|
||||
def get_chapter_images(self, chapter: str, wait_time: float) -> list:
|
||||
if self.verbose:
|
||||
print(f"INFO: Getting chapter images for: {self.manga_uuid}")
|
||||
athome_url = f"{self.api_base_url}/at-home/server"
|
||||
|
@ -205,7 +205,7 @@ class Mangadex:
|
|||
# check if result is ok
|
||||
else:
|
||||
if api_error:
|
||||
return None
|
||||
return []
|
||||
|
||||
chapter_hash = api_data["chapter"]["hash"]
|
||||
chapter_img_data = api_data["chapter"]["data"]
|
||||
|
@ -219,7 +219,7 @@ class Mangadex:
|
|||
return image_urls
|
||||
|
||||
# create list of chapters
|
||||
def create_chapter_list(self):
|
||||
def create_chapter_list(self) -> list:
|
||||
if self.verbose:
|
||||
print(f"INFO: Creating chapter list for: {self.manga_uuid}")
|
||||
chapter_list = []
|
||||
|
@ -235,7 +235,7 @@ class Mangadex:
|
|||
return chapter_list
|
||||
|
||||
# create easy to access chapter infos
|
||||
def get_chapter_infos(self, chapter):
|
||||
def get_chapter_infos(self, chapter: str) -> dict:
|
||||
if self.verbose:
|
||||
print(
|
||||
f"INFO: Getting chapter infos for: {self.manga_chapter_data[chapter][0]}"
|
||||
|
|
|
@ -31,8 +31,8 @@ class MangaDLP:
|
|||
self,
|
||||
url_uuid: str = "",
|
||||
language: str = "en",
|
||||
chapters: str = None,
|
||||
readlist: str = "",
|
||||
chapters: str = "",
|
||||
readlist: str = None,
|
||||
list_chapters: bool = False,
|
||||
file_format: str = "cbz",
|
||||
forcevol: bool = False,
|
||||
|
@ -52,7 +52,7 @@ class MangaDLP:
|
|||
self.download_wait = download_wait
|
||||
self.verbose = verbose
|
||||
|
||||
def __main__(self):
|
||||
def __main__(self) -> None:
|
||||
# additional stuff
|
||||
# set manga format suffix
|
||||
if self.file_format and "." not in self.file_format:
|
||||
|
@ -78,7 +78,7 @@ class MangaDLP:
|
|||
# start flow
|
||||
self.get_manga()
|
||||
|
||||
def pre_checks(self):
|
||||
def pre_checks(self) -> None:
|
||||
# prechecks userinput/options
|
||||
if not self.list_chapters and self.chapters is None:
|
||||
# no chapters to download were given
|
||||
|
@ -96,15 +96,17 @@ class MangaDLP:
|
|||
if self.url_uuid and self.readlist:
|
||||
print(f'ERR: You can only use "-u" or "--read". Dont specify both')
|
||||
exit(1)
|
||||
# when chapters are not being listed
|
||||
if not self.list_chapters:
|
||||
# if forcevol is used, but didn't specify a volume in the chapters selected
|
||||
if self.forcevol and ":" not in self.chapters:
|
||||
print(f"ERR: You need to specify the volume if you use --forcevol.")
|
||||
print(f"ERR: You need to specify the volume if you use --forcevol")
|
||||
exit(1)
|
||||
# if forcevol is not used, but a volume is specified
|
||||
if not self.forcevol and ":" in self.chapters:
|
||||
print(f"ERR: Don't specify the volume without --forcevol")
|
||||
exit(1)
|
||||
|
||||
# check the api which needs to be used
|
||||
def check_api(self, url_uuid):
|
||||
def check_api(self, url_uuid: str) -> type:
|
||||
# apis to check
|
||||
api_mangadex = re.compile("mangadex.org")
|
||||
api_mangadex2 = re.compile(
|
||||
|
@ -126,7 +128,7 @@ class MangaDLP:
|
|||
raise ValueError
|
||||
|
||||
# read in the list of links from a file
|
||||
def readin_list(self, readlist):
|
||||
def readin_list(self, readlist: str) -> list:
|
||||
list_file = Path(readlist)
|
||||
try:
|
||||
url_str = list_file.read_text()
|
||||
|
@ -137,7 +139,7 @@ class MangaDLP:
|
|||
return url_list
|
||||
|
||||
# once called per manga
|
||||
def get_manga(self):
|
||||
def get_manga(self) -> None:
|
||||
# create empty skipped chapters list
|
||||
skipped_chapters = []
|
||||
error_chapters = []
|
||||
|
@ -159,7 +161,9 @@ class MangaDLP:
|
|||
if self.chapters.lower() == "all":
|
||||
chapters_to_download = self.manga_chapter_list
|
||||
else:
|
||||
chapters_to_download = utils.get_chapter_list(self.chapters)
|
||||
chapters_to_download = utils.get_chapter_list(
|
||||
self.chapters, self.manga_chapter_list
|
||||
)
|
||||
|
||||
# show chapters to download
|
||||
print(f"INFO: Chapters selected:\n{', '.join(chapters_to_download)}")
|
||||
|
@ -192,7 +196,7 @@ class MangaDLP:
|
|||
print(f"{print_divider}\n")
|
||||
|
||||
# once called per chapter
|
||||
def get_chapter(self, chapter) -> dict:
|
||||
def get_chapter(self, chapter: str) -> dict:
|
||||
# get chapter infos
|
||||
chapter_infos = self.api.get_chapter_infos(chapter)
|
||||
|
||||
|
@ -283,7 +287,7 @@ class MangaDLP:
|
|||
return {"chapter_path": chapter_path}
|
||||
|
||||
# create an archive of the chapter if needed
|
||||
def archive_chapter(self, chapter_path):
|
||||
def archive_chapter(self, chapter_path: Path) -> dict:
|
||||
print(f"INFO: Creating '{self.file_format}' archive")
|
||||
try:
|
||||
# check if image folder is existing
|
||||
|
|
|
@ -7,7 +7,9 @@ import mangadlp.utils as utils
|
|||
|
||||
|
||||
# download images
|
||||
def download_chapter(image_urls, chapter_path, download_wait, verbose):
|
||||
def download_chapter(
|
||||
image_urls: list, chapter_path: str, download_wait: float, verbose: bool
|
||||
) -> None:
|
||||
total_img = len(image_urls)
|
||||
for img_num, img in enumerate(image_urls, 1):
|
||||
# set image path
|
||||
|
@ -22,6 +24,9 @@ def download_chapter(image_urls, chapter_path, download_wait, verbose):
|
|||
while counter <= 3:
|
||||
try:
|
||||
r = requests.get(img, stream=True)
|
||||
if r.status_code != 200:
|
||||
print(f"ERR: Request for image {img} failed, retrying")
|
||||
raise ConnectionError
|
||||
except KeyboardInterrupt:
|
||||
print("ERR: Stopping")
|
||||
exit(1)
|
||||
|
@ -29,13 +34,9 @@ def download_chapter(image_urls, chapter_path, download_wait, verbose):
|
|||
if counter >= 3:
|
||||
print("ERR: Maybe the MangaDex Servers are down?")
|
||||
raise ConnectionError
|
||||
print(f"ERR: Request for image {img} failed, retrying")
|
||||
sleep(download_wait)
|
||||
counter += 1
|
||||
else:
|
||||
if r.status_code != 200:
|
||||
print(f"ERR: Image {img} could not be downloaded. Retrying")
|
||||
continue
|
||||
break
|
||||
|
||||
# write image
|
||||
|
@ -49,6 +50,3 @@ def download_chapter(image_urls, chapter_path, download_wait, verbose):
|
|||
|
||||
img_num += 1
|
||||
sleep(download_wait)
|
||||
|
||||
# if every image was downloaded and written successfully
|
||||
return True
|
||||
|
|
|
@ -4,7 +4,7 @@ from zipfile import ZipFile
|
|||
|
||||
|
||||
# create an archive of the chapter images
|
||||
def make_archive(chapter_path, file_format):
|
||||
def make_archive(chapter_path: Path, file_format: str) -> None:
|
||||
zip_path = Path(f"{chapter_path}.zip")
|
||||
try:
|
||||
# create zip
|
||||
|
@ -17,7 +17,7 @@ def make_archive(chapter_path, file_format):
|
|||
raise IOError
|
||||
|
||||
|
||||
def make_pdf(chapter_path):
|
||||
def make_pdf(chapter_path: Path) -> None:
|
||||
try:
|
||||
import img2pdf
|
||||
except:
|
||||
|
@ -36,24 +36,38 @@ def make_pdf(chapter_path):
|
|||
|
||||
|
||||
# create a list of chapters
|
||||
def get_chapter_list(chapters):
|
||||
def get_chapter_list(chapters: str, available_chapters: list = None) -> list:
|
||||
chapter_list = []
|
||||
for chapter in chapters.split(","):
|
||||
# check if chapter list is with volumes and ranges
|
||||
if "-" in chapter and ":" in chapter:
|
||||
# split chapters and volumes apart for list generation
|
||||
lower = chapter.split("-")[0].split(":")
|
||||
upper = chapter.split("-")[1].split(":")
|
||||
lower_num = chapter.split("-")[0].split(":")
|
||||
upper_num = chapter.split("-")[1].split(":")
|
||||
vol = lower_num[0]
|
||||
chap_beg = int(lower_num[1])
|
||||
chap_end = int(upper_num[1])
|
||||
# generate range inbetween start and end --> 1:1-1:3 == 1:1,1:2,1:3
|
||||
for n in range(int(lower[1]), int(upper[1]) + 1):
|
||||
chapter_list.append(str(f"{lower[0]}:{n}"))
|
||||
for chap in range(chap_beg, chap_end + 1):
|
||||
chapter_list.append(str(f"{vol}:{chap}"))
|
||||
# no volumes, just chapter ranges
|
||||
elif "-" in chapter:
|
||||
lower = chapter.split("-")[0]
|
||||
upper = chapter.split("-")[1]
|
||||
lower_num = int(chapter.split("-")[0])
|
||||
upper_num = int(chapter.split("-")[1])
|
||||
# generate range inbetween start and end --> 1-3 == 1,2,3
|
||||
for n in range(int(lower), int(upper) + 1):
|
||||
chapter_list.append(str(n))
|
||||
for chap in range(lower_num, upper_num + 1):
|
||||
chapter_list.append(str(chap))
|
||||
# check if full volume should be downloaded
|
||||
elif ":" in chapter:
|
||||
vol = chapter.split(":")[0]
|
||||
chap = chapter.split(":")[1]
|
||||
# select all chapters from the volume --> 1: == 1:1,1:2,1:3...
|
||||
if vol and not chap:
|
||||
regex = re.compile(f"{vol}:[0-9]{{1,4}}")
|
||||
vol_list = [n for n in available_chapters if regex.match(n)]
|
||||
chapter_list.extend(vol_list)
|
||||
else:
|
||||
chapter_list.append(chapter)
|
||||
# single chapters without a range given
|
||||
else:
|
||||
chapter_list.append(chapter)
|
||||
|
@ -62,7 +76,7 @@ def get_chapter_list(chapters):
|
|||
|
||||
|
||||
# remove illegal characters etc
|
||||
def fix_name(filename):
|
||||
def fix_name(filename: str) -> str:
|
||||
# remove illegal characters
|
||||
filename = re.sub(r'[/\\<>:;|?*!@"]', "", filename)
|
||||
# remove multiple dots
|
||||
|
@ -76,7 +90,9 @@ def fix_name(filename):
|
|||
|
||||
|
||||
# create name for chapter
|
||||
def get_filename(chapter_name, chapter_vol, chapter_num, forcevol):
|
||||
def get_filename(
|
||||
chapter_name: str, chapter_vol: str, chapter_num: str, forcevol: bool
|
||||
) -> str:
|
||||
# if chapter is a oneshot
|
||||
if chapter_name == "Oneshot" or chapter_num == "Oneshot":
|
||||
return "Oneshot"
|
||||
|
@ -96,7 +112,7 @@ def get_filename(chapter_name, chapter_vol, chapter_num, forcevol):
|
|||
)
|
||||
|
||||
|
||||
def progress_bar(progress, total):
|
||||
def progress_bar(progress: float, total: float) -> None:
|
||||
percent = int(progress / (int(total) / 100))
|
||||
bar_length = 50
|
||||
bar_progress = int(progress / (int(total) / bar_length))
|
||||
|
|
Loading…
Reference in a new issue