Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""LICENSE
2Copyright 2015 Hermann Krumrey <hermann@krumreyh.com>
4This file is part of manga-dl.
6manga-dl is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 3 of the License, or
9(at your option) any later version.
11manga-dl is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
16You should have received a copy of the GNU General Public License
17along with manga-dl. If not, see <http://www.gnu.org/licenses/>.
18LICENSE"""
20import os
21import shutil
22import logging
23import cfscrape
24import requests
25from puffotter.os import makedirs
26from puffotter.print import pprint
27from typing import Callable, List
28from typing import Optional
29from subprocess import Popen, DEVNULL
32class Chapter:
33 """
34 Class that models a manga chapter
35 """
37 def __init__(
38 self,
39 url: str,
40 language: str,
41 series_name: str,
42 chapter_number: str,
43 destination_dir: str,
44 _format: str,
45 page_load_callback: Callable[['Chapter', str], List[str]],
46 title: Optional[str] = None,
47 group: Optional[str] = None
48 ):
49 """
50 Initializes the manga chapter
51 :param url: The URL used to fetch page image URLs
52 :param language: The language of the chapter
53 :param series_name: The name of the series
54 :param chapter_number: The chapter number of this chapter
55 :param destination_dir: The destination directory in which to store
56 downloaded files by default
57 :param _format: The format in which to store the chapter when
58 downloading by default
59 :param title: The title of the chapter
60 :param group: The group that scanlated this chapter
61 :param page_load_callback:
62 """
63 self.logger = logging.getLogger(self.__class__.__name__)
64 self.url = url
65 self.language = language
66 self.series_name = series_name
67 self.chapter_number = chapter_number
68 self.destination_dir = destination_dir
69 self.format = _format
70 self._page_load_callback = page_load_callback
71 self._pages = [] # type: List[str]
72 self._additional_urls = [] # type: List[str]
73 self._last_additional_urls = [] # type: List[str]
74 self.group = group
75 self.title = title
77 if self.chapter_number == "" or chapter_number == "0":
78 self.chapter_number = "0.0"
80 @property
81 def name(self) -> str:
82 """
83 :return: The name of the chapter
84 """
85 name = "{} - Chapter {}".format(self.series_name, self.chapter_number)
86 if self.title is not None and self.title != "":
87 name += " - " + self.title
88 if self.group is not None and self.group != "":
89 name += " ({})".format(self.group)
90 return name
92 @property
93 def pages(self) -> List[str]:
94 """
95 Lazy-loads the URLs of the chapter's page images
96 :return: The list of page images, in the correct order
97 """
98 new_urls = self._last_additional_urls != self._additional_urls
99 if len(self._pages) == 0 or new_urls:
100 self._pages = self._page_load_callback(self, self.url)
101 for url in self._additional_urls:
102 self._pages += self._page_load_callback(self, url)
103 self._last_additional_urls = list(self._additional_urls)
104 return self._pages
106 @property
107 def macro_chapter(self) -> int:
108 """
109 Calculates the 'macro' chapter number. For example:
110 12 -> 12
111 15.5 -> 15
112 EX4 -> 4
113 :return: The macro chapter number
114 """
115 macro = self.chapter_number.split(".")[0]
116 macro_num = ""
117 for char in macro:
118 if char.isnumeric():
119 macro_num += char
120 return int(macro_num)
122 @property
123 def micro_chapter(self) -> int:
124 """
125 Calculates the 'micro' chapter number. For example:
126 12 -> 0
127 15.5 -> 5
128 EX4 -> 0
129 :return: The micro chapter number
130 """
131 try:
132 micro = self.chapter_number.split(".")[1]
133 micro_num = ""
134 for char in micro:
135 if char.isnumeric():
136 micro_num += char
137 return int(micro_num)
138 except IndexError:
139 return 0
141 @property
142 def is_special(self) -> bool:
143 """
144 :return: Whether or not this is a 'special' chapter (Omake etc)
145 """
146 if "." in self.chapter_number or self.macro_chapter == 0:
147 return True
148 else:
149 try:
150 int(self.chapter_number)
151 return False
152 except ValueError:
153 return True
155 def add_additional_url(self, url: str):
156 """
157 Adds an additional URL.
158 Useful for multi-part chapters
159 :param url: The URL to add
160 :return: None
161 """
162 self._additional_urls.append(url)
164 def download(
165 self,
166 file_path_override: Optional[str] = None,
167 format_override: Optional[str] = None
168 ) -> str:
169 """
170 Downloads the chapter to a local file or directory
171 :param file_path_override: Overrides the automatically generated
172 destination file path
173 :param format_override: Overrides the class-wide format
174 :return: The path to the downloaded chapter file/directory
175 """
176 _format = self.format if format_override is None else format_override
178 tempdir = os.path.join("/tmp", self.name)
179 makedirs(tempdir, delete_before=True)
181 dest_path = os.path.join(self.destination_dir, self.name)
182 if file_path_override:
183 dest_path = file_path_override
184 if not dest_path.endswith("." + _format) and _format != "dir":
185 dest_path += "." + _format
187 makedirs(os.path.dirname(dest_path))
189 index_fill = len(str(len(self.pages)))
190 downloaded = []
192 for i, image_url in enumerate(self.pages):
194 cloudflare = False
195 if image_url.startswith("CF!"):
196 image_url = image_url[3:]
197 cloudflare = True
199 ext = image_url.rsplit(".", 1)[1]
200 filename = "{}.{}".format(str(i).zfill(index_fill), ext)
201 image_file = os.path.join(tempdir, filename)
203 pprint("{} Chapter {} ({}/{})".format(
204 self.series_name,
205 self.chapter_number,
206 i + 1,
207 len(self.pages)
208 ), fg="black", bg="lyellow", end="\r")
210 if cloudflare:
211 scraper = cfscrape.create_scraper()
212 content = scraper.get(image_url).content
213 with open(image_file, "wb") as f:
214 f.write(content)
215 else:
216 resp = requests.get(
217 image_url, headers={"User-Agent": "Mozilla/5.0"}
218 )
219 if resp.status_code >= 300:
220 self.logger.warning("Couldn't download image file {}"
221 .format(image_file))
222 else:
223 with open(image_file, "wb") as f:
224 f.write(resp.content)
226 downloaded.append(image_file)
228 print()
230 if len(downloaded) == 0:
231 self.logger.warning("Couldn't download chapter {}".format(self))
232 else:
233 if _format in ["cbz", "zip"]:
234 self.logger.debug("Zipping Files")
235 Popen(["zip", "-j", dest_path] + downloaded,
236 stdout=DEVNULL, stderr=DEVNULL).wait()
237 shutil.rmtree(tempdir)
238 elif _format == "dir":
239 os.rename(tempdir, dest_path)
240 else:
241 self.logger.warning("Invalid format {}".format(_format))
243 return dest_path
245 def __str__(self) -> str:
246 """
247 :return: The string representation of the object
248 """
249 return self.name
251 def __eq__(self, other: object) -> bool:
252 """
253 Checks for equality with other objects
254 :param other: The other object
255 :return: Whether or not the objects are the same
256 """
257 if not isinstance(other, Chapter):
258 return False
259 else:
260 return other.url == self.url