Hide keyboard shortcuts

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> 

3 

4This file is part of manga-dl. 

5 

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. 

10 

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. 

15 

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""" 

19 

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 

30 

31 

32class Chapter: 

33 """ 

34 Class that models a manga chapter 

35 """ 

36 

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 

76 

77 if self.chapter_number == "" or chapter_number == "0": 

78 self.chapter_number = "0.0" 

79 

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 

91 

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 

105 

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) 

121 

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 

140 

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 

154 

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) 

163 

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 

177 

178 tempdir = os.path.join("/tmp", self.name) 

179 makedirs(tempdir, delete_before=True) 

180 

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 

186 

187 makedirs(os.path.dirname(dest_path)) 

188 

189 index_fill = len(str(len(self.pages))) 

190 downloaded = [] 

191 

192 for i, image_url in enumerate(self.pages): 

193 

194 cloudflare = False 

195 if image_url.startswith("CF!"): 

196 image_url = image_url[3:] 

197 cloudflare = True 

198 

199 ext = image_url.rsplit(".", 1)[1] 

200 filename = "{}.{}".format(str(i).zfill(index_fill), ext) 

201 image_file = os.path.join(tempdir, filename) 

202 

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") 

209 

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) 

225 

226 downloaded.append(image_file) 

227 

228 print() 

229 

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)) 

242 

243 return dest_path 

244 

245 def __str__(self) -> str: 

246 """ 

247 :return: The string representation of the object 

248 """ 

249 return self.name 

250 

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