├── .gitattributes ├── .gitignore ├── Epub.py ├── HbookerAPI ├── CryptoUtil.py ├── HttpUtil.py ├── UrlConstants.py └── __init__.py ├── README.md ├── book.py ├── bookshelf.py ├── cache.py ├── config.py ├── instance.py ├── list.txt ├── msg.py ├── run.py ├── template.epub └── token_parser.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | Cache/ 13 | Hbooker/ 14 | LocalCache/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | .idea/ 105 | HbookerAppNovelDownloader - Config.json 106 | Backup/ 107 | Books/ 108 | com.kuangxiangciweimao.novel_preferences.xml 109 | -------------------------------------------------------------------------------- /Epub.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from shutil import copyfile 3 | from instance import * 4 | import urllib.request 5 | import zipfile 6 | import codecs 7 | import time 8 | import msg 9 | import os 10 | import re 11 | 12 | image_get_retry = 10 13 | image_download_display_delay = 1 14 | last_image_dl_start_time = None 15 | 16 | 17 | def str_mid(string: str, left: str, right: str, start=None, end=None): 18 | pos1 = string.find(left, start, end) 19 | if pos1 > -1: 20 | pos2 = string.find(right, pos1 + len(left), end) 21 | if pos2 > -1: 22 | return string[pos1 + len(left): pos2] 23 | return '' 24 | 25 | 26 | def get_all_files(dir_path: str): 27 | result = list() 28 | for _name in os.listdir(dir_path): 29 | if os.path.isdir(dir_path + '/' + _name): 30 | result.extend(get_all_files(dir_path + '/' + _name)) 31 | else: 32 | result.append(dir_path + '/' + _name) 33 | return result 34 | 35 | 36 | def text_to_html_element_escape(text: str): 37 | return text.replace('&', '&').replace('<', '<').replace('>', '>') 38 | 39 | 40 | def html_element_to_text_unescape(element: str): 41 | return element.replace('&', '&').replace('<', '<').replace('>', '>') 42 | 43 | 44 | def backup_copy_add_suffix_if_exists_add_index(file_path: str, suffix: str): 45 | # fix_illegal_book_name_dir 46 | backup_dir = os.path.join(Vars.cfg.data['backup_dir'], re.sub('^(.+)\\.\\s*$', '\\1.', 47 | os.path.splitext(os.path.basename(file_path))[0])) 48 | file_basename = os.path.splitext(os.path.basename(file_path))[0] + ' ' + suffix 49 | file_ext = os.path.splitext(file_path)[1] 50 | if not os.path.isdir(backup_dir): 51 | os.makedirs(backup_dir) 52 | if os.path.exists(file_path): 53 | if os.path.exists(os.path.join(backup_dir, file_basename) + file_ext): 54 | index = 1 55 | while os.path.exists(os.path.join(backup_dir, file_basename) + ' ' + str(index) + file_ext): 56 | index += 1 57 | copyfile(file_path, os.path.join(backup_dir, file_basename) + ' ' + str(index) + file_ext) 58 | else: 59 | copyfile(file_path, os.path.join(backup_dir, file_basename) + file_ext) 60 | else: 61 | # 出現錯誤 62 | print("error: file dose not exists: " + file_path) 63 | 64 | 65 | def download_progress_reporthook(count, block_size, total_size): 66 | # this prints the image downloading progress 67 | # this is to show that the script is working 68 | # the timer is "not thread" safe, but working as intended 69 | # the timer is reset every time when a new download is triggered 70 | # it will print the progress if it's taking too long 71 | global last_image_dl_start_time 72 | if count == 0: 73 | last_image_dl_start_time = time.time() 74 | return 75 | duration = time.time() - last_image_dl_start_time 76 | if duration < image_download_display_delay: 77 | return 78 | progress_size = int(count * block_size) 79 | percent = int(progress_size * 100 / total_size) 80 | print("\rDownloading Image... %d%%, %d KB" % (percent, progress_size / 1024), end='') 81 | 82 | 83 | class EpubFile: 84 | _filepath = '' 85 | tempdir = '' 86 | _content_opf = '' 87 | _content_opf_manifest = '' 88 | _content_opf_spine = '' 89 | _chapter_format_manifest = '' 90 | _image_format_manifest = '' 91 | _chapter_format_spine = '' 92 | _toc_ncx = '' 93 | _toc_ncx_navMap = '' 94 | _chapter_format_navMap = '' 95 | _chapter_format = '' 96 | 97 | def __init__(self, filepath: str, tempdir: str, book_id: str, book_title: str, book_author: str, use_old_epub=True): 98 | self._filepath = filepath 99 | self.tempdir = tempdir 100 | if not os.path.isdir(tempdir): 101 | os.makedirs(tempdir) 102 | _template = zipfile.ZipFile(os.path.abspath(os.path.join(os.path.dirname(__file__), 'template.epub'))) 103 | self.cover_template = bytes(_template.read('OEBPS/Text/cover.xhtml')).decode() 104 | self._content_opf = bytes(_template.read('OEBPS/content.opf')).decode() 105 | self._chapter_format_manifest = str_mid(self._content_opf, '${chapter_format_manifest}={{{', '}}}') 106 | self._image_format_manifest = str_mid(self._content_opf, '${image_format_manifest}={{{', '}}}') 107 | self._chapter_format_spine = str_mid(self._content_opf, '${chapter_format_spine}={{{', '}}}') 108 | self._toc_ncx = bytes(_template.read('OEBPS/toc.ncx')).decode() 109 | self._chapter_format_navMap = str_mid(self._toc_ncx, '${chapter_format_navMap}={{{', '}}}') 110 | self._chapter_format = bytes(_template.read('OEBPS/Text/${chapter_format}.xhtml')).decode() 111 | if os.path.isfile(filepath) and use_old_epub: 112 | try: 113 | with zipfile.ZipFile(self._filepath, 'r', zipfile.ZIP_DEFLATED) as _file: 114 | try: 115 | self._content_opf = bytes(_file.read('OEBPS/content.opf')).decode() 116 | self._toc_ncx = bytes(_file.read('OEBPS/toc.ncx')).decode() 117 | _file.extractall(self.tempdir) 118 | except (KeyError, NameError, IOError): 119 | self._content_opf = bytes(_template.read('OEBPS/content.opf')).decode() 120 | self._toc_ncx = bytes(_template.read('OEBPS/toc.ncx')).decode() 121 | finally: 122 | self._content_opf_manifest = str_mid(self._content_opf, '', '') 123 | self._content_opf_spine = str_mid(self._content_opf, '', '') 124 | self._toc_ncx_navMap = str_mid(self._toc_ncx, '', '') 125 | _init = False 126 | except zipfile.BadZipFile: 127 | _init = True 128 | else: 129 | _init = True 130 | if _init: 131 | for _name in _template.namelist(): 132 | if _name.find('$') == -1: 133 | if not os.path.exists(tempdir + '/' + _name): 134 | _template.extract(_name, self.tempdir) 135 | self._content_opf = re.sub(r'\${.*?}={{{[\S\s]*?}}}[\r\n]*', '', self._content_opf) 136 | self._toc_ncx = re.sub(r'\${.*?}={{{[\S\s]*?}}}[\r\n]*', '', self._toc_ncx) 137 | self._content_opf_manifest = str_mid(self._content_opf, '', '') 138 | self._content_opf_spine = str_mid(self._content_opf, '', '') 139 | self._toc_ncx_navMap = str_mid(self._toc_ncx, '', '') 140 | self._content_opf = self._content_opf.replace('${book_id}', book_id) 141 | self._content_opf = self._content_opf.replace('${book_title}', text_to_html_element_escape(book_title)) 142 | self._content_opf = self._content_opf.replace('${book_author}', text_to_html_element_escape(book_author)) 143 | self._toc_ncx = self._toc_ncx.replace('${book_id}', book_id) 144 | self._toc_ncx = self._toc_ncx.replace('${book_title}', text_to_html_element_escape(book_title)) 145 | self._toc_ncx = self._toc_ncx.replace('${book_author}', text_to_html_element_escape(book_author)) 146 | _template.close() 147 | 148 | def _add_manifest_chapter(self, chapter_id: str): 149 | if self._content_opf_manifest.find('id="' + chapter_id + '.xhtml"') == -1: 150 | _before = self._content_opf_manifest 151 | self._content_opf_manifest += self._chapter_format_manifest.replace('${chapter_id}', chapter_id) + '\r\n' 152 | self._content_opf = self._content_opf.replace( 153 | '' + _before + '', 154 | '' + self._content_opf_manifest + '', 1) 155 | 156 | def _add_manifest_image(self, filename: str): 157 | if self._content_opf_manifest.find('id="' + filename + '"') == -1: 158 | _before = self._content_opf_manifest 159 | if filename.endswith('.png'): 160 | _media_type = 'image/png' 161 | else: 162 | _media_type = 'image/jpeg' 163 | self._content_opf_manifest += self._image_format_manifest.replace('${filename}', filename) \ 164 | .replace('${media_type}', _media_type) + '\r\n' 165 | self._content_opf = self._content_opf.replace( 166 | '' + _before + '', 167 | '' + self._content_opf_manifest + '', 1) 168 | 169 | def _add_spine(self, chapter_id: str): 170 | # noinspection SpellCheckingInspection 171 | if self._content_opf_spine.find('idref="' + chapter_id + '.xhtml"') == -1: 172 | _before = self._content_opf_spine 173 | self._content_opf_spine += self._chapter_format_spine.replace('${chapter_id}', chapter_id) + '\r\n' 174 | self._content_opf = self._content_opf.replace( 175 | '' + _before + '', 176 | '' + self._content_opf_spine + '', 1) 177 | 178 | def add_nav_map(self, chapter_index: str, chapter_id: str, chapter_title: str): 179 | if self._toc_ncx_navMap.find('id="' + chapter_id) == -1: 180 | _before = self._toc_ncx_navMap 181 | self._toc_ncx_navMap += self._chapter_format_navMap.replace('${chapter_id}', chapter_id) \ 182 | .replace('${chapter_index}', chapter_index) \ 183 | .replace('${chapter_title}', chapter_title) + '\r\n' 184 | self._toc_ncx = self._toc_ncx.replace( 185 | '' + _before + '', 186 | '' + self._toc_ncx_navMap + '', 1) 187 | 188 | def _save(self): 189 | with codecs.open(self.tempdir + '/OEBPS/content.opf', 'w', 'utf-8') as _file: 190 | _file.write(self._content_opf) 191 | with codecs.open(self.tempdir + '/OEBPS/toc.ncx', 'w', 'utf-8') as _file: 192 | _file.write(self._toc_ncx) 193 | 194 | def set_cover(self, url: str): 195 | image_path = self.tempdir + '/OEBPS/Images/' + url[url.rfind('/') + 1:] 196 | if os.path.exists(image_path): 197 | if os.path.getsize(image_path) != 0: 198 | return 199 | for retry in range(image_get_retry): 200 | try: 201 | urllib.request.urlretrieve(url, image_path, download_progress_reporthook) 202 | copyfile(image_path, self.tempdir + '/OEBPS/Images/cover.jpg') 203 | return 204 | except OSError as e: 205 | if retry != image_get_retry - 1: 206 | print(msg.m('cover_dl_rt') + str(retry + 1) + ' / ' + str(image_get_retry) + ', ' + str(e) + '\n' + 207 | url) 208 | time.sleep(0.5 * retry) 209 | else: 210 | print(msg.m('cover_dl_f') + str(e) + '\n' + url) 211 | with open(image_path, 'wb'): 212 | pass 213 | 214 | def export(self): 215 | self._save() 216 | with zipfile.ZipFile(self._filepath, 'w', zipfile.ZIP_DEFLATED) as _file: 217 | _result = get_all_files(self.tempdir) 218 | counter = 0 219 | print() 220 | for _name in _result: 221 | _file.write(_name, _name.replace(self.tempdir + '/', '')) 222 | counter += 1 223 | print('\r' + str(counter) + ' / ' + str(len(_result)), end='') 224 | print() 225 | 226 | def add_image(self, filename: str, url: str): 227 | image_path = self.tempdir + '/OEBPS/Images/' + filename 228 | if os.path.exists(image_path): 229 | if os.path.getsize(image_path) != 0: 230 | return 231 | for retry in range(image_get_retry): 232 | try: 233 | urllib.request.urlretrieve(url, image_path, download_progress_reporthook) 234 | return 235 | except OSError as e: 236 | if retry != image_get_retry - 1: 237 | print(msg.m('image_dl_rt') + str(retry + 1) + ' / ' + str(image_get_retry) + ', ' + str(e) + '\n' + 238 | url) 239 | time.sleep(0.5 * retry) 240 | else: 241 | print(msg.m('image_dl_f') + str(e) + '\n' + url) 242 | with open(image_path, 'wb'): 243 | pass 244 | 245 | def add_chapter(self, chapter_id: str, division_name: str, chapter_title: str, chapter_content: str, 246 | division_index, chapter_order): 247 | f_name = division_index.rjust(4, "0") + '-' + str(chapter_order).rjust(6, "0") + '-' + chapter_id 248 | _data = self._chapter_format.replace( 249 | '${chapter_title}', '' + text_to_html_element_escape(division_name) + ' : ' + 250 | text_to_html_element_escape(chapter_title) + '') \ 251 | .replace('${chapter_content}', '

' + text_to_html_element_escape(chapter_title) + '

\r\n' + 252 | chapter_content) 253 | # 改動插圖連結辨識方式,以適應另一種連結的結構 254 | for _img in re.findall(r'))[/>]?", 264 | "\\1/>", _data) 265 | _data = re.sub( 266 | f"())[/>]?", 267 | "\\1/>", _data) 268 | with codecs.open(self.tempdir + '/OEBPS/Text/' + f_name + '.xhtml', 'w', 'utf-8') as _file: 269 | _file.write(_data) 270 | 271 | def download_book_write_chapter(self, division_chapter_list): 272 | order_count = 2 273 | with codecs.open(os.path.splitext(self._filepath)[0] + ".txt", 'w', 'utf-8') as _file: 274 | with codecs.open(self.tempdir + '/OEBPS/Text/cover.xhtml', 'r', 'utf-8') as _cover_xhtml: 275 | cover = str(_cover_xhtml.read()) 276 | cover = str_mid(cover, '

', '') 277 | cover = cover.replace('

', '').replace('

', '').replace('

', '').replace('

', ''). \ 278 | replace('

', '').replace('

', '').replace('

', '') 279 | cover = html_element_to_text_unescape(cover) 280 | _file.write(cover + '\r\n') 281 | for filename in sorted(os.listdir(self.tempdir + '/OEBPS/Text/')): 282 | if filename.find('$') > -1 or filename == 'cover.xhtml': 283 | continue 284 | f_name = os.path.splitext(filename)[0] 285 | self._add_manifest_chapter(f_name) 286 | self._add_spine(f_name) 287 | with codecs.open(self.tempdir + '/OEBPS/Text/' + filename, 'r', 'utf-8') as _file_xhtml: 288 | _data_chapter = re.sub(r'

.*?

', '', _file_xhtml.read()) 289 | division_and_chapter_file = str_mid(_data_chapter, "", "") 290 | if division_and_chapter_file == '': 291 | division_name = '' 292 | chapter_title = '' 293 | for division_list_name in division_chapter_list: 294 | for chapter in division_chapter_list[division_list_name]: 295 | if chapter['chapter_id'] == os.path.splitext(filename)[0].split("-")[2]: 296 | division_name = division_list_name 297 | chapter_title = chapter['chapter_title'] 298 | break 299 | else: 300 | continue 301 | break 302 | if division_name == '' and chapter_title == '': 303 | division_and_chapter_file = text_to_html_element_escape(filename) 304 | else: 305 | division_and_chapter_file = text_to_html_element_escape(division_name + ' : ' + chapter_title) 306 | self.add_nav_map(str(order_count), f_name, division_and_chapter_file) 307 | for _a in re.findall(r'章节链接', _data_chapter): 308 | _data_chapter = _data_chapter.replace(_a, '章节链接:' + str_mid(_a, '', _data_chapter): 310 | _data_chapter = _data_chapter.replace(_img, '图片:"' + str_mid(_img, "alt='", "'") + '",' + '位置:"' 311 | + str_mid(_img, '', '', _data_chapter) 313 | _data_chapter = re.sub(r'[\r\n]+', '\r\n\r\n', _data_chapter) 314 | _data_chapter = html_element_to_text_unescape(_data_chapter) 315 | _file.write(_data_chapter) 316 | order_count += 1 317 | for filename in sorted(os.listdir(self.tempdir + '/OEBPS/Images/')): 318 | self._add_manifest_image(filename) 319 | self.export() 320 | self.make_backup() 321 | 322 | def make_backup(self): 323 | if Vars.cfg.data['do_backup']: 324 | date = str(datetime.now().date()) 325 | backup_copy_add_suffix_if_exists_add_index(self._filepath, date) 326 | backup_copy_add_suffix_if_exists_add_index(os.path.splitext(self._filepath)[0] + '.txt', date) 327 | 328 | def make_cover_text(self, book_name: str, author_name: str, book_description: str, update_time: str, book_id: str): 329 | text = '

' + text_to_html_element_escape(book_name) + '

\r\n

作者: ' + \ 330 | text_to_html_element_escape(author_name) + '

\r\n

更新時間: ' + update_time + \ 331 | '

\r\n

Book ID: ' + book_id + '

\r\n

簡介:

\r\n

' + \ 332 | re.sub('\r\n', '

\r\n

', text_to_html_element_escape(book_description) + '

') 333 | text = re.sub('\r\n', text + '\r\n\r\n', self.cover_template) 334 | with codecs.open(self.tempdir + '/OEBPS/Text/cover.xhtml', 'w', 'utf-8') as _file: 335 | _file.write(text) 336 | -------------------------------------------------------------------------------- /HbookerAPI/CryptoUtil.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | import base64 3 | import hashlib 4 | 5 | iv = b'\0' * 16 6 | 7 | 8 | def encrypt(text, key): 9 | aes_key = hashlib.sha256(key.encode('utf-8')).digest() 10 | aes = AES.new(aes_key, AES.MODE_CFB, iv) 11 | return base64.b64encode(aes.encrypt(text)) 12 | 13 | 14 | def decrypt(encrypted, key='zG2nSeEfSHfvTCHy5LCcqtBbQehKNLXn'): 15 | aes_key = hashlib.sha256(key.encode('utf-8')).digest() 16 | aes = AES.new(aes_key, AES.MODE_CBC, iv) 17 | return pkcs7un_padding(aes.decrypt(base64.b64decode(encrypted))) 18 | 19 | 20 | def pkcs7un_padding(data): 21 | length = len(data) 22 | un_padding = ord(chr(data[length - 1])) 23 | return data[0:length - un_padding] 24 | 25 | 26 | if __name__ == '__main__': 27 | print('decrypt,key=\t', decrypt( 28 | 'c1SR02T7X+xmq37zfs0U8NAj73eedAs3tnXMQKDNUPlI2vcaNRXpKA3JktMoffp3EYPCsvCjzeCJUynjDISbNP4D5HjaCp6tMrOsBBfQzVI=')) 29 | test = b'{"code":200001,"tip":"\\u7f3a\\u5c11\\u767b\\u5f55\\u5fc5\\u9700\\u53c2\\u6570"}\x08\x08\x08\x08\x08\x08' \ 30 | b'\x08\x08 ' 31 | print('pkcs7unpadding=\t', pkcs7un_padding(test)) 32 | -------------------------------------------------------------------------------- /HbookerAPI/HttpUtil.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | import sys 4 | 5 | headers = {'User-Agent': 'Android'} 6 | maxRetry = 10 7 | requests_timeout = 20 8 | 9 | 10 | def get(url, params=None, retry=maxRetry, **kwargs): 11 | for count in range(retry): 12 | try: 13 | return str(requests.get(url, params=params, headers=headers, **kwargs, timeout=requests_timeout).text) 14 | except requests.exceptions.RequestException as e: 15 | print("\nGet Error Retry: " + str(e) + '\n' + url) 16 | time.sleep(1 * count) 17 | except Exception as e: 18 | print(repr(e)) 19 | break 20 | print("\nGet Failed, Terminating......") 21 | sys.exit(1) 22 | 23 | 24 | def post(url, data=None, retry=maxRetry, **kwargs): 25 | for count in range(retry): 26 | try: 27 | return str(requests.post(url, data, headers=headers, **kwargs, timeout=requests_timeout).text) 28 | except requests.exceptions.RequestException as e: 29 | print("\nPost Error Retry: " + str(e) + '\n' + url) 30 | time.sleep(1 * count) 31 | except Exception as e: 32 | print(repr(e)) 33 | break 34 | print("\nPost Failed, Terminating......") 35 | sys.exit(1) 36 | -------------------------------------------------------------------------------- /HbookerAPI/UrlConstants.py: -------------------------------------------------------------------------------- 1 | ABOUT_URL = "https://app.hbooker.com/setting/view_about" 2 | ADD_BBS_COMMENT = "bbs/add_bbs_comment" 3 | ADD_BBS_COMMENT_REPLY = "bbs/add_bbs_comment_reply" 4 | ADD_BLIST_REVIEW = "booklist/add_review" 5 | ADD_BLIST_REVIEW_COMMENT = "booklist/add_review_comment" 6 | ADD_BLITREVIEW_COMMENT_REPLY = "booklist/add_review_comment_reply" 7 | ADD_BOOK = "booklist/add_booklist_book" 8 | ADD_REVIEW = "book/add_review" 9 | ADD_REVIEW_COMMENT = "book/add_review_comment" 10 | ADD_REVIEW_COMMENT_REPLY = "book/add_review_comment_reply" 11 | ADD_TSUKKOMI = "chapter/add_tsukkomi" 12 | ALIPAY_URL = "recharge/alipay_order" 13 | ALL_MISSION_LIST = "task/get_all_task_list" 14 | ALl_COUPONLIST = "recharge/get_all_platform_recharge_coupon_list" 15 | AUTO_BUY_LIST = "bookshelf/get_auto_buy_pub_book_list" 16 | BBS_ADD_BBS = "bbs/add_bbs" 17 | BBS_GET_BBS_COMMENT_LIST = "bbs/get_bbs_comment_list" 18 | BBS_GET_BBS_LIST = "bbs/get_bbs_list" 19 | BBS_LIKE_BBS = "bbs/like_bbs" 20 | BBS_UNLIKE_BBS = "bbs/unlike_bbs" 21 | BIND_THIRD = "reader/oauth_bind" 22 | BOOKCITY_DIS_DATA = "bookcity/get_discount_list" 23 | BOOKCITY_GET_BOOK_LIST = "bookcity/get_filter_book_list" 24 | BOOKCITY_GET_BOOK_LIST_BY_ID = "book/get_book_list" 25 | BOOKCITY_GET_BOOK_LIST_BY_MODULE = "bookcity/get_module_more_list" 26 | BOOKCITY_GET_CAROUSEL = "bookcity/get_carousel" 27 | BOOKCITY_GET_CATEGORY_BOOK_LIST = "bookcity/get_category_book_list" 28 | BOOKCITY_GET_FILTER_LIST = "bookcity/get_filter_search_book_list" 29 | BOOKCITY_GET_Fans_RANK_LIST = "reader/get_consume_most" 30 | BOOKCITY_GET_HPMEPAGE_INFO = "reader/get_homepage_info" 31 | BOOKCITY_GET_RANK_lIST = "bookcity/get_rank_book_list" 32 | BOOKCITY_GET_SEARCH_BOOKNAME = "bookcity/get_search_book_list_by_bookname" 33 | BOOKCITY_GET_SEARCH_BOOK_LIST = "bookcity/get_search_book_list" 34 | BOOKCITY_GET_SEARCH_KEYS = "bookcity/get_search_keys" 35 | BOOKCITY_GET_TAG_BOOK_LIST = "bookcity/get_tag_book_list" 36 | BOOKCITY_GET_UP_BOOK_LIST = "reader/get_up_book_list" 37 | BOOKCITY_RECOMMEND_DATA = "bookcity/get_index_list" 38 | BOOKCITY_RECOMMEND_LIST = "bookcity/get_recommend_list" 39 | BOOKCITY_VIP_DATA = "bookcity/get_vip_list" 40 | BOOKLIST_ADD_SHARE = "booklist/add_booklist_share_num" 41 | BOOKLIST_LIKE_REVIEW = "booklist/like_review" 42 | BOOKLIST_UNLIKE_REVIEW = "booklist/unlike_review" 43 | BOOKSHELF_ADD_SHELF = "bookshelf/add_shelf" 44 | BOOKSHELF_DELETE_SHELF_BOOK = "bookshelf/delete_shelf_book" 45 | BOOKSHELF_FAVOR = "bookshelf/favor" 46 | BOOKSHELF_GET_SHELF_BOOK_LIST = "bookshelf/get_shelf_book_list" 47 | BOOKSHELF_GET_SHELF_LIST = "bookshelf/get_shelf_list" 48 | BOOKSHELF_MOVE_TO_SHELF = "bookshelf/move_to_shelf" 49 | BOOK_BUY = "book/buy" 50 | BOOK_BUY_MULTI = "chapter/buy_multi" 51 | BOOK_GET_INFO_BY_ID = "book/get_info_by_id" 52 | BOOK_GET_PRICE = "book/get_price" 53 | BOOK_GIVE_RECOMMEND = "book/give_recommend" 54 | BOOK_GIVE_YP = "book/give_yp" 55 | BOOK_ITEM_REWARD = "book/give_reward_prop" 56 | BOOK_LIKE_REVIEW = "book/like_review" 57 | BOOK_REWARD = "book/reward" 58 | BOOK_UNLIKE_REVIEW = "book/unlike_review" 59 | BUYMALL = "prop/buy_prop" 60 | BUY_BLIST = "bookcity/buy_book_list" 61 | BUY_PROP = "prop/buy_prop" 62 | CAN_ADDTAG_BOOK = "book/can_add_tag" 63 | CAN_ADD_BOOKLIST = "booklist/can_add_booklist" 64 | CHANGE_PHONE = "https://app.hbooker.com/signup/change_bind_phone_num" 65 | CHAPTER_BUY = "chapter/buy" 66 | CHAPTER_DOWNLOAD = "chapter/get_chapter_info" 67 | CHAPTER_LIKE_TSUKKOMI = "chapter/like_tsukkomi" 68 | CHAPTER_UNLIKE_TSUKKOMI = "chapter/unlike_tsukkomi" 69 | CREATE_BOOKLIST = "booklist/add_booklist" 70 | DELETE_BOOK = "booklist/delete_booklist_book" 71 | DELETE_BOOKLIST = "booklist/delete_booklist" 72 | DELETE_MSG = "reader/del_chat_record" 73 | DISFAVOR_BOOKLIST = "booklist/disfavor_booklist" 74 | EDIT_BOOK = "booklist/edit_booklist_book" 75 | EDIT_BOOKLIST = "booklist/edit_booklist" 76 | EXP_URL = "https://app.hbooker.com/reader/get_grade" 77 | FAVOR_BOOKLIST = "booklist/favor_booklist" 78 | GET_ALL_PERMISISSION = "chapter/get_chapter_permission_list" 79 | GET_ALL_TAG = "book/get_official_tag_list" 80 | GET_AUTOBUY = "bookshelf/get_is_auto_buy_pub" 81 | GET_BBS_COMMENT_REPLY_LIST = "bbs/get_bbs_comment_reply_list" 82 | GET_BLIST_REVIEW_COMMENT_LIST = "booklist/get_review_comment_list" 83 | GET_BLIST_REVIEW_LIST = "booklist/get_review_list" 84 | GET_BOOKLISTS = "bookcity/get_book_lists" 85 | GET_BOOKLISTS_DETAIL = "bookcity/get_booklist_detail" 86 | GET_BOOK_HEI = "book/unlike_tag" 87 | GET_BOOK_HOT_TAGLIST = "book/get_book_hot_tag_list" 88 | GET_BOOK_TAGLIST = "book/get_book_tag_list" 89 | GET_BOOK_ZAN = "book/like_tag" 90 | GET_BOUNS_HLB = "recharge/get_bonus_hlb" 91 | GET_BUY_MALLLIST = "prop/get_prop_buy_record_list" 92 | GET_CHAPTER_UPDATE = "https://app.hbooker.com/chapter/get_updated_chapter_by_division_id" 93 | GET_CHAPTER_COMMAND = "chapter/get_chapter_command" 94 | GET_CPT_IFM = "chapter/get_cpt_ifm" 95 | GET_DECORATION = "reader/get_decoration_list_by_type" 96 | GET_DETAIL_BOOKLISTS = "bookcity/get_book_correlation_lists" 97 | GET_DIVISION_LIST = "https://app.hbooker.com/book/get_division_list" 98 | GET_DIVISION_LIST_NEW = "https://app.hbooker.com/chapter/get_updated_chapter_by_division_new" 99 | GET_DOWNURL = "setting/get_resource_download_url" 100 | GET_FANSVALUES = "book/get_book_fans_value" 101 | GET_FAVOR_BOOKLIST = "booklist/get_favor_booklist" 102 | GET_HIS_LIST = "setting/get_activity_list" 103 | GET_MALLLIST = "prop/get_mall_prop_list" 104 | GET_MONEYBAG = "reader/get_wallet_info" 105 | GET_MONEY_FANS_LIST = "book/get_book_fans_list" 106 | GET_MSG_AT = "reader/get_message_at_list" 107 | GET_MSG_COMMENT = "reader/get_message_comment_list" 108 | GET_MY_BOOKLIST = "booklist/get_my_booklist" 109 | GET_PARAGRAPH_TSUKKOMI_LIST = "chapter/get_paragraph_tsukkomi_list" 110 | GET_PARAGRAPH_TSUKKOMI_LIST_NEW = "chapter/get_paragraph_tsukkomi_list_new" 111 | GET_PAYPLID = "recharge/get_paypal_cid" 112 | GET_REVIEW_BLIST_COMMENT_REPLY_LIST = "booklist/get_review_comment_reply_list" 113 | GET_REVIEW_COMMENT_LIST = "book/get_review_comment_list" 114 | GET_REVIEW_COMMENT_REPLY_LIST = "book/get_review_comment_reply_list" 115 | GET_REVIEW_LIST = "book/get_review_list" 116 | GET_REWARDLIST = "prop/get_reward_list" 117 | GET_REWARD_MONEY__LIST = "book/get_book_operate_list" 118 | GET_SEARCH_TAG = "bookcity/get_hot_key_list" 119 | GET_SHARE_INFO = "book/get_share_info" 120 | GET_TSUKKOMI_LIST = "chapter/get_tsukkomi_list" 121 | GET_TSUKKOMI_LIST_SHORT = "chapter/get_short_tsukkomi_list" 122 | GET_TSUKKOMI_NUM = "chapter/get_tsukkomi_num" 123 | GET_UNREAD = "reader/get_unread_num" 124 | GET_UPTIME_LIST = "bookshelf/get_uptime_list" 125 | GET_USER_DECORATION = "reader/get_reader_decoration_list_by_type" 126 | GIFT_HLB_URL = "https://app.hbooker.com/setting/view_gift_hlb" 127 | GIVE_BLADE = "book/give_blade" 128 | GUIDE_URL = "https://app.hbooker.com/setting/help_center_ios" 129 | HOW_TO_GET_YP_URL = "https://app.hbooker.com/setting/view_yp" 130 | INFORM = "reader/inform" 131 | IS_AUTO_BUY_PUB = "bookshelf/set_is_auto_buy_pub" 132 | LEVEL_URL = "https://app.hbooker.com/reader/get_grade_rule" 133 | META_GET_META_DATA = "meta/get_meta_data" 134 | MOD_SHELF_NAME = "bookshelf/mod_shelf_name" 135 | MONEY_FANS_URL = "https://app.hbooker.com/setting/top_rich_detail" 136 | MSG_CHAT_LIST = "reader/get_chat_list_by_reader" 137 | MSG_READER_LIST = "reader/get_message_reader_list" 138 | MY_ATTE_LIST = "reader/get_following_list" 139 | MY_BIND = "signup/check_bind" 140 | MY_CONSUMNE_RECORD = "reader/get_consume_record_list" 141 | MY_DETAILS_INFO = "reader/get_my_info" 142 | MY_FANS_LIST = "reader/get_followed_list" 143 | MY_FOOT_BBS = "reader/get_reader_bbs_list" 144 | MY_FOOT_REVIEW = "reader/get_reader_review_list" 145 | MY_FOOT_TSUKKOLIST = "reader/get_reader_tsukkomi_list" 146 | MY_GETSMSCODE = "signup/send_verify_code" 147 | MY_GET_RECHAR_LIST = "recharge/get_recharge_coupon_list" 148 | MY_GIFTHLB_RECORD = "reader/get_gift_hlb_record_list" 149 | MY_GIFT_CODE = "reader/use_gift_code" 150 | MY_ITEM = "prop/get_active_mall_prop_list" 151 | MY_MESSAGE_LIST = "reader/get_message_list" 152 | MY_MISSION_BASE = "reader/get_personal_task_bonus" 153 | MY_MISSION_LIST = "reader/get_task_list" 154 | MY_MISSION_REWARD = "reader/get_daily_task_bonus" 155 | MY_MODIFY_PWD = "signup/modify_passwd" 156 | MY_MOD_INFO = "reader/mod_my_info" 157 | MY_PAY_RECORD = "reader/get_recharge_record_list" 158 | MY_PSON_HOME_DATA = "reader/get_homepage_info" 159 | MY_PSON_PROP_DATA = "reader/get_prop_info" 160 | MY_RECH_RECORD = "recharge/get_recharge_record_list" 161 | MY_SETTING_UPDATE = "setting/get_version" 162 | MY_SHARE = "reader/personal_task_shared" 163 | MY_SIGN_LOGIN = "signup/login" 164 | MY_USE_RECORD = "prop/get_prop_use_record_list" 165 | MY_VERIFY_RECH = "recharge/verify_recharge" 166 | PAYPAL_NOFITY = "recharge/paypal_notify" 167 | READER_FOLLOW = "reader/follow" 168 | READER_GET_BBS_LIST = "reader/get_reader_bbs_list" 169 | READER_GET_CHAPTER_LIST = "chapter/get_chapter_list" 170 | READER_GET_DYNAMIC_LIST = "reader/get_dynamic_list" 171 | READER_GET_REVIEW_LIST = "reader/get_reader_review_list" 172 | READER_GET_TSUKKOMI_LIST = "reader/get_reader_tsukkomi_list" 173 | READER_SEND_MESSAGE = "reader/send_message" 174 | READER_UNFOLLOW = "reader/unfollow" 175 | RECHARGE_ORDER = "recharge/recharge_order" 176 | SAVE_REANTIME = "reader/add_readbook" 177 | SAVE_RECORD = "bookshelf/set_last_read_chapter" 178 | SEND_SCREEN = "chapter/screenshot" 179 | SETTING_APP_FEEDBACK = "setting/app_feedback" 180 | SET_DAOPIAN = "https://app.hbooker.com/setting/daopian_des" 181 | SET_DEFRIEND = "reader/defriend" 182 | SET_FANSLEVEL = "https://app.hbooker.com/setting/fans_level_des" 183 | SET_ISREAD = "reader/set_is_read_comment" 184 | SET_ISREAD_AT = "reader/set_is_read_at" 185 | SET_UN_DEFRIEND = "reader/un_defriend" 186 | SET_USER_DECORATION = "reader/set_decoration_use" 187 | SHELF_TUIJIAN = "https://app.hbooker.com/bookshelf/get_bookself_reommend_list" 188 | SIGNUP_AUTO_LOGIN = "signup/auto_login" 189 | SIGNUP_AUTO_REQ = "signup/auto_reg" 190 | SIGN_RECORD = "task/get_sign_record" 191 | SING_RECORD_TASK = "reader/get_task_bonus_with_sign_recommend" 192 | START_PAGE = "setting/get_startpage_url" 193 | SYS_MSG = "reader/get_message_sys_list" 194 | THIRD_SWITCH = "setting/thired_party_switch" 195 | UNBIND_THIRD = "reader/oauth_unbind" 196 | URL_ORDER = "recharge/ipay_order" 197 | WEB_SITE = "https://app.hbooker.com/" 198 | -------------------------------------------------------------------------------- /HbookerAPI/__init__.py: -------------------------------------------------------------------------------- 1 | from HbookerAPI import HttpUtil, CryptoUtil, UrlConstants 2 | import json 3 | 4 | # 某些帳號要'device_token': 'ciweimao_'才能登入 5 | common_params = {'account': None, 'login_token': None, 'app_version': '2.7.039'} 6 | # common_params = {'account': None, 'login_token': None, 'app_version': '2.9.290', 'device_token': 'ciweimao_'} 7 | 8 | 9 | def set_common_params(params): 10 | common_params.update(params) 11 | 12 | 13 | def get(api_url, params=None, **kwargs): 14 | if params is None: 15 | params = common_params 16 | if params is not None: 17 | params.update(common_params) 18 | api_url = api_url.replace(UrlConstants.WEB_SITE, '') 19 | # print('get', UrlConstants.WEB_SITE + api_url, params) 20 | try: 21 | return json.loads(CryptoUtil.decrypt(HttpUtil.get(UrlConstants.WEB_SITE + api_url, params=params, **kwargs))) 22 | except Exception as error: 23 | print("get error:", error) 24 | 25 | 26 | def post(api_url, data=None, **kwargs): 27 | if data is None: 28 | data = common_params 29 | if data is not None: 30 | data.update(common_params) 31 | api_url = api_url.replace(UrlConstants.WEB_SITE, '') 32 | # print('post', UrlConstants.WEB_SITE + api_url, data) 33 | try: 34 | return json.loads(CryptoUtil.decrypt(HttpUtil.post(UrlConstants.WEB_SITE + api_url, data=data, **kwargs))) 35 | except Exception as error: 36 | print("post error:", error) 37 | 38 | 39 | class SignUp: 40 | @staticmethod 41 | def login(login_name, passwd): 42 | data = {'login_name': login_name, 'passwd': passwd} 43 | return post(UrlConstants.MY_SIGN_LOGIN, data) 44 | 45 | 46 | class BookShelf: 47 | @staticmethod 48 | def get_shelf_list(): 49 | return post(UrlConstants.BOOKSHELF_GET_SHELF_LIST) 50 | 51 | @staticmethod 52 | def get_shelf_book_list(shelf_id, last_mod_time='0', direction='prev'): 53 | data = {'shelf_id': shelf_id, 'last_mod_time': last_mod_time, 'direction': direction} 54 | return post(UrlConstants.BOOKSHELF_GET_SHELF_BOOK_LIST, data) 55 | 56 | 57 | class Book: 58 | @staticmethod 59 | def get_division_list(book_id): 60 | data = {'book_id': book_id} 61 | return post(UrlConstants.GET_DIVISION_LIST, data) 62 | 63 | @staticmethod 64 | def get_updated_chapter_by_division_new(book_id: str): 65 | return post(UrlConstants.GET_DIVISION_LIST_NEW, {'book_id': book_id}) 66 | 67 | @staticmethod 68 | def get_chapter_update(division_id, last_update_time='0'): 69 | data = {'division_id': division_id, 'last_update_time': last_update_time} 70 | return post(UrlConstants.GET_CHAPTER_UPDATE, data) 71 | 72 | @staticmethod 73 | def get_info_by_id(book_id): 74 | data = {'book_id': book_id, 'recommend': '', 'carousel_position': '', 'tab_type': '', 'module_id': ''} 75 | return post(UrlConstants.BOOK_GET_INFO_BY_ID, data) 76 | 77 | 78 | class Chapter: 79 | @staticmethod 80 | def get_chapter_command(chapter_id): 81 | data = {'chapter_id': chapter_id} 82 | return post(UrlConstants.GET_CHAPTER_COMMAND, data) 83 | 84 | @staticmethod 85 | def get_cpt_ifm(chapter_id, chapter_command): 86 | data = {'chapter_id': chapter_id, 'chapter_command': chapter_command} 87 | return post(UrlConstants.GET_CPT_IFM, data) 88 | 89 | 90 | class CheckIn: 91 | @staticmethod 92 | def get_check_in_records(): 93 | return post(UrlConstants.SIGN_RECORD, {}) 94 | 95 | @staticmethod 96 | def do_check_in(): 97 | return post(UrlConstants.SING_RECORD_TASK, {'task_type': 1}) 98 | 99 | 100 | class CheckAppVersion: 101 | @staticmethod 102 | def get_version(): 103 | return post(UrlConstants.MY_SETTING_UPDATE) 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HbookerAppNovelDownloader 2 | ## 使用前請仔細閱讀!! 3 | ## 介紹 4 | * HbookerAppNovelDownloader 5 | * 基於https://github.com/hang333/HbookerAppNovelDownloader 6 | * 模仿 刺猬貓 / 歡樂書客 android app版內核的小說下載器 7 | * 此程序目的是: 8 | 為了預防書籍/章節哪天遭屏蔽或下架(特別是已購買的書籍/章節),讓讀者有預先下載書籍到本地保存選擇。 9 | * 此程序不是"**破解**"看書,付費章節依然需要付費購買後才能獲取章節 10 | * 支持免費與已付費章節下載 11 | * 支持書籍內插圖下載 12 | * 支持書籍的章節更新/更新檢查 13 | * 若刪除Cache中的檔案,下次下載時缺失的部分會被重新下載 14 | * 支持導出文件格式:txt、epub,文件將會被下載至HBooker目錄下 15 | 16 | * 書籍/章節下架後然會無法下載 17 | * 不要大規模宣傳此程式,請低調個人使用。 18 | ### 下載書籍僅個人閱讀,禁止分享上傳 19 | ## 免責聲明: 20 | * **使用此工具可能導致封號** 21 | * **請自行評估風險是否使用此程序** 22 | * **若發生封號等事件,後果自負,這裡不負責** 23 | * **用戶帳號與密碼會報保存在HbookerAppNovelDownloader - Config.json中,小心勿洩漏** 24 | * **若帳號密碼寫洩漏(請自行檢查代碼,自行評估),後果自負,這裡不負責** 25 | * **使用此程式造成的任何意外接不負責** 26 | 27 | ### 警告: 此版本的站存檔先前版本的暫存不相容,請移移除(備分舊版EPUB)後再執行下載 28 | * 此此處站存主要是針對暫存`./Cache/OEBPS/Text/.xhtml`的警告。 29 | 其他暫存檔案**因該**沒有影響。 30 | * 原因在於此版建立匯出書籍EPUB的過程有極大改動。 31 | * 若未移除舊版的暫存直接執行,會導致章節順序混亂、目錄標題缺失卷名等問題,甚至出現崩潰。 32 | * 若有保存現在已屏蔽章節想要加入,需要人工進行編輯,改成現在的檔名與格式後才能加入站存使用。 33 | 34 | ## 需求環境 35 | * Python3 36 | * requests 37 | * pycryptodome或pycrypto 38 | ## 使用法法 39 | - 類似控制台的操作 40 | * 內部指令說明 : 41 | ``` 42 | h | help --- 顯示說明 (此訊息) 43 | m | message --- 切換提示訊息 (繁體/簡體) 44 | q | quit --- 退出腳本 45 | i | import_token --- 以匯入token方式登入帳號 46 | l | login <手機號/郵箱/用戶名> <密碼> --- 登錄歡樂書客帳號 !!!已失效!!! 47 | version --- 從網路獲取現在版本號,詢問是否刷新版本號 (輸入完整單字) 48 | 注:<用戶名>空格<密碼>,<用戶名>與<密碼>不能含有空格。 49 | t | task --- 執行每日簽到,領代幣 (啟動時自動執行,無異常不需再次執行) 50 | s | shelf --- 刷新並顯示當前書架列表 (啟動時會自動刷新1次) 51 | s <書架編號> | shelf <書架編號> --- 選擇與切換書架 52 | b | book --- 刷新並顯示當前書架的書籍列表 53 | b <書籍編號/書籍ID> | book <書籍編號/書籍ID> --- 選擇書籍 54 | d | download --- 下載當前書籍(book時選擇的書籍) 55 | d <書籍編號/書籍ID> | download <書籍編號/書籍ID> --- 下載指定ID書籍 56 | ds <書架編號> | downloadshelf <書架編號> --- 下載整個書架 57 | u | update --- 下載"list.txt"書單之中所列書籍 58 | u | update --- 下載指定書單中所列書籍 59 | ``` 60 | 61 | 注: 輸入指令開頭字母即可 62 | ## 基本使用流程 63 | * 首先執行 64 | `py run.py` 65 | 確認仔細閱讀且同意README.md中之敘述事物,如果同意,輸入 66 | `yes` 67 | 登入帳號,token 與 account 輸入 68 | `i` 69 | 進入匯入模式 提供2種匯入方式 70 | 71 | 1 . 由xml檔案方式匯入 需從手機APP獲取 `/data/data/com.kuangxiangciweimao.novel/shared_prefs/com.kuangxiangciweimao.novel_preferences.xml` 72 | 後放置於工作目錄後選擇第`1`選項 73 | 獲取`com.kuangxiangciweimao.novel_preferences.xml`提供2方案 74 | 75 | 對於root android 可嘗試 76 | ``` 77 | adb shell 78 | su 79 | chmod 777 /data/data/com.kuangxiangciweimao.novel/shared_prefs/com.kuangxiangciweimao.novel_preferences.xml 80 | exit 81 | exit 82 | adb pull /data/data/com.kuangxiangciweimao.novel/shared_prefs/com.kuangxiangciweimao.novel_preferences.xml 83 | ``` 84 | 非root android 可嘗試 85 | ``` 86 | 備份data/data/com.kuangxiangciweimao.novel 87 | adb backup -f data -noapk com.kuangxiangciweimao.novel 88 | 使用https://github.com/nelenkov/android-backup-extractor 將data轉成tar 89 | java -jar .\abe.jar unpack data data.tar 90 | 再用7z或其他壓縮軟件解壓所 91 | "C:\Program Files\7-Zip\7z.exe" x .\data.tar 92 | 後獲取檔案xml 93 | "apps\com.kuangxiangciweimao.novel\sp\com.kuangxiangciweimao.novel_preferences.xml" 94 | ``` 95 | 2 . 利用抓包軟體讀取payload中的`login_token`與 `account` 後手動輸入 96 | * login_token 為32位數16進位字串 97 | * account 類似`书客123456789`字串 98 | 99 | * 選擇書籍 100 | * 方法1: 根據用戶架`選擇書架` 101 | `shelf <書架編號>` 102 | 根據該`書架中書籍邊號`選擇書籍 103 | `book <書架中書籍邊號>` 104 | * 方法2: 根據`書籍ID` `選擇書籍` 105 | `book <書籍ID>` 106 | * <書籍ID> 可以從網站該書頁的書籍網址 107 | 範例網址: www.ciweimao.com/book/123456789 108 | 範例網址尾部的"`123456789`" (9位數字)為`<書籍ID>` 109 | * 也可以在官方APP內書頁點選 "分享" -> "複製連結" 取得連結 110 | * 書籍選擇後可開始下載書籍,輸入: 111 | `download` 112 | * 指定編號 + 下載: 113 | `download <書架中書籍編號>` 114 | 可跳過`book <書架中書籍邊號>`指令,但必須已選書架(法1) 115 | * 指定書籍ID + 下載 116 | `download <書籍ID>` 117 | 可以跳過`選擇書籍`步驟,直接進行下載,但須預先知道ID 118 | 119 | ## 其他功能 120 | ### 下載整個書架 (若書架長,不建議使用)輸入 121 | `downloadshelf <書架編號>` 122 | 可用縮寫代替`ds <書架編號>` 123 | 若省略`<書架編號>`,則會下載當前書架 124 | ### 下載書單 125 | 可以根據**書單**中所列出的書籍進行下載,預設書單 "list.txt",輸入: 126 | `update <書單位置>` 127 | 若省略`<書單位置>`則會下載預設`"list.txt"`中所列的書籍 128 | * 書單語法 根據Regex :``^\s*([0-9]{9}).*$``進行辨認 129 | * 規則概要 : 130 | * 每行只能有1本書 131 | * 書籍開頭只能是<空白>或<書籍ID_9位數> 132 | * <書籍ID_9位數>後的會被忽略 133 | * 若不符合規則 --> 會被忽略 134 | * 可在刻意"**不符合規則**"書自前插入任意符號來停用該書下載,不必刪除該行 135 | * 可自由添加"**不符合規則**"的文字來進行分類管理或註釋,增加閱讀辨識度 136 | * 提供參考範例 "list.txt" 137 | ### 自動簽到能(每日代幣) 138 | 登入後每當啟動時會行簽到嘗試,若已簽到則不會進行簽到 139 | 可手動執行,輸入: 140 | `task` 或 `t` 141 | ### 自動化 142 | 最初執行時可**添加參數**,執行完畢後自動退出,只能之行單相指令,參數與以上指令相同 143 | * 例: 若要 下載特定書籍 : 144 | `py run.py d <書籍ID>` 145 | * 例: 若要 下載 / 更新 書單 : 146 | `py run.py u <書單位置>` 147 | * 例: 只簽到 148 | `py run.py t` 149 | * 注: 此方式執行的`簽到`成功後會立刻結束,不會獲取刷新書架。 150 | * 若想要完全自動化,可使用Windows的工作排程器`task scheduler`或Unix的`cron job`等類似工具,設定`定時執行`執行。 151 | ###介面訊息繁/簡切換 152 | * 可以切換提示訊息繁體/簡體, 153 | 輸入`m` 154 | * **僅介面提示訊息**,與匯出的書籍無關 155 | ###可更黨檔案保存位置 156 | * 可以編輯`HbookerAppNovelDownloader - Config.json`檔案中的以下的"`./XXX/`"來改變檔案保存位置 157 | * 書籍匯出位置: "output_dir": "`./Hbooker/`" 158 | * 書籍備份匯出位置: "backup_dir": "`./Hbooker/`" 159 | * 暫存檔案位置: "cache_dir": "`./Cache/`" 160 | ### 可以選擇是否會在書籍匯出時生成備份 161 | * 可以編輯`HbookerAppNovelDownloader - Config.json`檔案中的以下的 `"do_backup":` `true` 162 | * 啟用`"do_backup":` `true` 163 | * 停用`"do_backup":` `false` 164 | ###更新版本號功能 165 | * 可以透過網路獲取目前版本號 166 | 輸入`version` 167 | * 獲取候群問使否刷新版本號 168 | * 獲取輸入`yes`可確認,其他取消 169 | * (目前對於版本號的作用有待觀察) 170 | * 另外可手動改版本號 171 | 編輯`HbookerAppNovelDownloader - Config.json`中的`"current_app_version":` "`2.7.039`" 172 | 173 | ## 與原版差異/改動 174 | * \+ **簽到功能(領代幣),(自動簽到)**。 175 | * \+ **下載章節與獲取書籍目錄時使用多工,加快下載速度(取代原download)**, 176 | 預設平行下載上限為`32`,可在Config中更改。 177 | 注:根據觀察官方APP書籍下載速度,因該也是有進類似平行下載,因該不會封號。 178 | * \+ 登入失效會自動嘗試重新登入。 179 | * \+ **使用者帳號與密碼會以明文方式保存在Config中**,小心誤洩漏。 180 | * 改動config.json名稱與儲存位置至 ./HbookerAppNovelDownloader.json。 181 | * **更改書籍章節排序方式**,從依照index(因該是章節公開順序),改成依照app中書籍目錄分眷章節排序。 182 | * \+ **增加命令行界面(command line interface),方便自動化**。 183 | * \- 移除了下載單卷功能(認為此功能多餘)。 184 | * \- 指令介面中移除config相關指令。 185 | * \+ 在EPUB封面添加了書籍名稱、作者名、書籍簡介、更新時間。 186 | * \+ 將封面加入EPUB目錄。 187 | * \+ 取代原"update"更興功能,改成讀取使用者自建立的書單,進行書單下載。 188 | * \+ 增價下載帳號中整個書架功能。 189 | * \- 不建立書籍.json。 190 | * 更改提示文字。 191 | * \+ 增加更多出錯重試與改變出錯處裡。 192 | * \+ 遇到`未訂閱的付費章節`時,不會結束下載立刻匯出書籍,而會跳過該章,直到下載所有可被下載的`已付費`與`免費章節`後才匯出書籍。 193 | * \+ 從章節目錄獲取章節資訊,得知那些章節已付費可被下載,以及那些章節被屏蔽無法下載,根據章節狀態執行下載,減少非必要的伺服器下載訪問。 194 | 因只會嘗試下載可下載的章節,沒有必定會失敗的下載嘗試,**理論上因該會減小封號機率**,較原版會嘗試訪下載問未購買章節與屏蔽章節,我認為算是可疑行為。 195 | 章節狀態分為(已下載\未下載)、(免費/已訂閱/未訂閱)、(屏蔽),根據狀態判斷是否要嘗試下載章節。 196 | * \+ 改變"`章節已下載`"判定方式,現在根據暫存中使否存在該章節對應的檔案(`xxxx-yyyyyy-zzzzzzzzz.xhtm`)。 197 | - 如要重新下載某書或某章節,刪除暫存內的該書或該章(建議刪除單章),再次執行下載即可。 198 | * \+ 改變"`已下載章節`"的存檔名稱為`xxxx`-`yyyyyy`-`zzzzzzzzz`.xhtml。 199 | 注:`xxxx`: 分卷號、`yyyyyy`: 分卷章節編號(順序)、`zzzzzzzzz`: 章節id 200 | 例:某書的`第2卷`-`第5章`會檔名會是`0002`-`000005`-123456789.xhtml 201 | * 章節存檔內的`\章節名\`改成`\捲名 : 章節名\`對閱讀無引響 202 | * \+ 匯出EPUB目錄中所列的`章節名`更改`捲名 : 章節名` 203 | * \+ 匯出EPUB與TXT的章節排方式根據"已下載章節"檔名排序 204 | * 移動`template.epub`至主目錄下。 205 | * 不會自動解壓保存書籍EPUB至Cache暫存,若需要需手動執行。 206 | * \+ 書籍匯出時會進行備份,加上日期後綴,如有需要會再增加後編號綴數字(此功能意義不大)。 207 | * \+ 書籍下載時會顯示3組數字 : `` / `` / `` 208 | 分別代表 : `該次執行實際下載章節數` / `目前已經處理章節數(進度)` / `該書總章節數` 209 | * 當``等於``時,代表下載處完成,請等待書籍匯出。 210 | * 若下載完成時``為`0`,則代表沒有下載任何章節。 211 | * 因習慣所以使用繁體中文顯示。 212 | 213 | ## 其他事項 214 | * 預設平行下載上限為`32`,可在Config中更改,編輯Config中`"max_concurrent_downloads": 32`的數字即可更改,下次啟動後生效。 215 | **若值設過高,可能會提高封號風險。** 216 | * 可在Config手動更改帳號密碼,編輯在Config中的 217 | `user_account": "YOUR_ACCOUNT"`的`YOUR_ACCOUNT`以及 218 | `"user_password": "YOUR_PASSWORD"`的`YOUR_PASSWORD` 219 | 下次啟動後生效,建議使用login指令比較實際 220 | * 若某章節下載後有改動、修正等,"需要刷新",需要手動進入該書籍暫存檔,找到該節對應檔案,進行刪除或改名,後再次執行下載以重新下載 221 | * 進入`Cache\《該書籍名》\OEBPS\Text\`尋找到對應章節檔案進行`刪除`或`重新命名` 222 | * 例: `0001-000002-123456789.xhtml`改名為`0001-000002-123456789-任意後綴.xhtml` 223 | **重要: 重新命名時只能加上`後綴`,且必須用"`-`"號隔開,否則會導致錯誤。** 224 | * 圖片可能會被刪除等原因無法下載,下載時會進行錯圖片下載錯誤提示,忽略即可。 225 | * 有時會因為等待圖片重試下載,增價閜下時間,請耐心等待完成訊息。 226 | * 若章節因屏蔽無法下載,會建立空檔案當作標記 (檔案大小為0),該檔案匯出EPUB後會造成該章頁面無法顯示(當作標記)。 227 | * 若個人有保存缺失的章節,可自行依照xhtml格式放入,取代空章節。 228 | * 若發現某些章節被屏蔽,導致以後無法下載,建議保存該章節檔案(不要刪除)。 229 | * 若未購買章節,則會進行告知,不會建立空檔檔案。 230 | * 若下載中途出現問題,導致停止運行,請閱讀錯誤原因,大部分錯誤重新執行下載幾可排除。 231 | 注: 少數狀況下可能導致章節不全(機率極小),需要手動刪除該章,重新下載。 232 | * 若有需要EPUB黨可被解壓縮還原成暫存檔。 233 | * 在Windows上可建立捷徑,添加參數,點擊可執行特定功能。 234 | * 可使用`pyinstaller`打包凍結封包程執行檔方便使用 : 235 | `pyinstaller --noconfirm --onefile --console --add-data "template.epub;." "run.py" --name "HbookerAppNovelDownloader"` 236 | -------------------------------------------------------------------------------- /book.py: -------------------------------------------------------------------------------- 1 | from Epub import * 2 | import HbookerAPI 3 | import threading 4 | import cache 5 | import queue 6 | import msg 7 | import os 8 | 9 | 10 | class Book: 11 | index = None 12 | book_id = None 13 | book_name = None 14 | author_name = None 15 | cover = None 16 | book_info = None 17 | last_chapter_info = None 18 | division_list = None 19 | chapter_list = None 20 | division_chapter_list = None 21 | epub = None 22 | config = None 23 | file_path = None 24 | 25 | def __init__(self, index, book_info): 26 | self.index = index 27 | self.book_info = book_info 28 | self.book_id = book_info['book_id'] 29 | self.book_name = book_info['book_name'] 30 | self.author_name = book_info['author_name'] 31 | self.cover = book_info['cover'].replace(' ', '') 32 | self.last_chapter_info = book_info['last_chapter_info'] 33 | self.division_list = [] 34 | self.chapter_list = [] 35 | self.division_chapter_list = {} 36 | self.get_chapter_catalog_mt_dl_lock = threading.Lock() 37 | self.concurrent_download_queue = queue.Queue() 38 | for item in range(Vars.cfg.data['max_concurrent_downloads']): 39 | self.concurrent_download_queue.put(item) 40 | self.process_finished_count = 0 41 | self.downloaded_count = 0 42 | # self.config = Config(Vars.cfg.data['cache_dir'] + 'book-' + self.fix_illegal_book_name() + '.json', 43 | # Vars.cfg.data['cache_dir']) 44 | # self.config.load() 45 | 46 | def get_division_list(self): 47 | print(msg.m('get_div'), end='') 48 | response = HbookerAPI.Book.get_updated_chapter_by_division_new(self.book_id) 49 | if response.get('code') == '100000': 50 | self.division_list = response['data']['chapter_list'] 51 | cache.save_cache(f"{self.book_id}_chapter_list.json", self.division_list) 52 | else: 53 | print(msg.m('failed_get_div'), response.get('tip')) 54 | division_list = cache.test_division_list(self.book_id) 55 | if division_list: 56 | self.division_list = division_list 57 | else: 58 | exit(1) 59 | 60 | def show_division_list(self): 61 | print('\r', end='') 62 | for chapter_list in self.division_list: 63 | print(msg.m('show_div_index') + chapter_list['division_index'].rjust(3, " ") + msg.m('show_div_total') + 64 | str(len(self.division_chapter_list[chapter_list['division_name']])).rjust(4, " ") + 65 | msg.m('show_div_name') + chapter_list['division_name']) 66 | 67 | def get_chapter_catalog(self): 68 | print(msg.m('get_chap')) 69 | self.chapter_list.clear() 70 | for chapter_list in self.division_list: 71 | self.chapter_list.extend(chapter_list['chapter_list']) 72 | self.division_chapter_list[chapter_list['division_name']] = chapter_list['chapter_list'] 73 | print("\r", end="") 74 | 75 | def show_latest_chapter(self): 76 | print(msg.m('show_last_chap_s_index'), self.chapter_list[-1]['chapter_index'], msg.m('show_last_chap_uptime'), 77 | self.last_chapter_info['uptime'], msg.m('show_last_chap_name'), self.chapter_list[-1]['chapter_title']) 78 | 79 | def fix_illegal_book_name(self): 80 | # replace illegal characters with full-width counterparts 81 | return self.book_name.replace('<', '<').replace('>', '>').replace(':', ':').replace('"', '“') \ 82 | .replace('/', '╱').replace('|', '|').replace('?', '?').replace('*', '*') 83 | 84 | def fix_illegal_book_name_dir(self): 85 | # remove spaces at the end 86 | # if last character is '.' replace with full-width '.' 87 | # replace illegal characters with full-width counterparts 88 | return re.sub('^(.+)\\.\\s*$', '\\1.', self.book_name).replace('<', '<').replace('>', '>').replace(':', ':') \ 89 | .replace('"', '“').replace('/', '╱').replace('|', '|').replace('?', '?').replace('*', '*') 90 | 91 | def show_chapter_list_order_division(self): 92 | for division in self.division_list: 93 | chapter_order = 1 94 | print(msg.m('show_chap_list_index'), division['division_index'], ',:', division['division_name']) 95 | for chapter_info in self.division_chapter_list[division['division_name']]: 96 | print(' ' + chapter_info['chapter_index'] + ', ' + division['division_index'] + "-" + 97 | str(chapter_order) + "-" + str(chapter_info['chapter_id']) + ' ' + division['division_name'] + 98 | ':' + chapter_info['chapter_title']) 99 | chapter_order += 1 100 | 101 | def download_book_multi_thread(self): 102 | self.file_path = os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir(), 103 | self.fix_illegal_book_name()) + '.epub' 104 | self.epub = EpubFile(self.file_path, os.path.join(Vars.cfg.data['cache_dir'], self.fix_illegal_book_name_dir()), 105 | self.book_id, self.book_name, self.author_name, use_old_epub=False) 106 | self.epub.set_cover(self.cover) 107 | threads = [] 108 | # for every chapter in order of division 109 | for division in self.division_list: 110 | chapter_order = 1 111 | # print('分卷編號:', division['division_index'], ', 分卷名:', division['division_name']) 112 | for chapter_info in self.division_chapter_list[division['division_name']]: 113 | # print("ch_info_index: ", chapter_info['chapter_index'], " ch_info_id", chapter_info['chapter_id']) 114 | if chapter_info['is_valid'] == '0': 115 | # 處理屏蔽章節 116 | self.process_finished_count += 1 117 | f_name = division['division_index'].rjust(4, "0") + '-' + str(chapter_order).rjust(6, "0") + '-' + \ 118 | chapter_info['chapter_id'] 119 | if os.path.exists(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml'): 120 | if os.path.getsize(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml') == 0: 121 | # self.add_download_finished_count() 122 | print('\r' + chapter_info['chapter_index'].rjust(5, "0") + ', ' + division[ 123 | 'division_index'].rjust(4, "0") + "-" + 124 | str(chapter_order).rjust(6, "0") + "-" + str(chapter_info['chapter_id']) + 125 | msg.m('dl_chap_block_e') + division['division_name'] + ':' + chapter_info[ 126 | 'chapter_title'] + 127 | "\n" + str(self.downloaded_count) + ' / ' + str( 128 | self.process_finished_count) + " / " + str( 129 | len(self.chapter_list)), end=' ') 130 | else: 131 | print('\r' + chapter_info['chapter_index'].rjust(5, "0") + ', ' + division[ 132 | 'division_index'].rjust(4, "0") + "-" + 133 | str(chapter_order).rjust(6, "0") + "-" + str(chapter_info['chapter_id']) + 134 | msg.m('dl_chap_block_c') + division['division_name'] + ':' + chapter_info[ 135 | 'chapter_title'] + 136 | "\n" + str(self.downloaded_count) + ' / ' + str( 137 | self.process_finished_count) + " / " + str( 138 | len(self.chapter_list)), end=' ') 139 | else: 140 | # 如無檔案 建立空檔 141 | with codecs.open(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml', 'w', 'utf-8') as _file: 142 | pass 143 | print('\r' + chapter_info['chapter_index'].rjust(5, "0") + ', ' + division[ 144 | 'division_index'].rjust(4, "0") + "-" + 145 | str(chapter_order).rjust(6, "0") + "-" + str(chapter_info['chapter_id']) + 146 | msg.m('dl_chap_block_e') + division['division_name'] + ':' + chapter_info[ 147 | 'chapter_title'] + 148 | "\n" + str(self.downloaded_count) + ' / ' + str( 149 | self.process_finished_count) + " / " + str( 150 | len(self.chapter_list)), end=' ') 151 | 152 | elif chapter_info['auth_access'] == '0': 153 | # 跳過未購買章節 154 | self.process_finished_count += 1 155 | print( 156 | '\r' + chapter_info['chapter_index'].rjust(5, "0") + ', ' + 157 | division['division_index'].rjust(4, "0") + '-' + str(chapter_order).rjust(6, "0") + '-' + 158 | chapter_info['chapter_id'] + msg.m('dl_chap_not_paid') + division['division_name'] + ':' + 159 | chapter_info['chapter_title'] + "\n" + str(self.downloaded_count) + ' / ' + 160 | str(self.process_finished_count) + " / " + str(len(self.chapter_list)), end=' ') 161 | else: 162 | download_thread = threading.Thread(target=self.download_book_get_chapter, 163 | args=(chapter_info['chapter_index'], chapter_info['chapter_id'], 164 | division['division_index'], chapter_order,)) 165 | threads.append(download_thread) 166 | download_thread.start() 167 | chapter_order += 1 168 | for thread in threads: 169 | thread.join() 170 | print(msg.m('dl_fin'), end='') 171 | # bookshelf description is not available 172 | if self.book_info.get("description") is None: 173 | if Vars.cfg.data.get('force_book_description'): 174 | Vars.current_book = Book(None, HbookerAPI.Book.get_info_by_id(self.book_id)['data']['book_info']) 175 | self.book_info['description'] = Vars.current_book.book_info['description'] 176 | else: 177 | self.book_info["description"] = "" 178 | if self.downloaded_count == 0: 179 | if os.path.exists(self.epub.tempdir + '/OEBPS/Text'): 180 | text_mod_time = os.path.getmtime(self.epub.tempdir + '/OEBPS/Text') 181 | else: 182 | text_mod_time = 0 183 | if os.path.exists(os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir(), 184 | self.fix_illegal_book_name()) + '.epub'): 185 | epub_mod_time = os.path.getmtime( 186 | os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir(), 187 | self.fix_illegal_book_name()) + '.epub') 188 | else: 189 | epub_mod_time = 0 190 | if text_mod_time >= epub_mod_time: 191 | print(msg.m('expo_s'), end='') 192 | if not os.path.isdir(os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir())): 193 | os.makedirs(os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir())) 194 | self.epub.make_cover_text(self.book_info['book_name'], self.book_info['author_name'], 195 | self.book_info['description'], self.book_info['uptime'], self.book_id) 196 | self.epub.download_book_write_chapter(self.division_chapter_list) 197 | # self.config.data['book_info'] = self.book_info 198 | # self.config.data['division_chapter_list'] = self.division_chapter_list 199 | # self.config.save() 200 | print('\r' + msg.m('expo_e')) 201 | else: 202 | print(msg.m('expo_no')) 203 | else: 204 | print(msg.m('expo_s'), end='') 205 | if not os.path.isdir(Vars.cfg.data['output_dir']): 206 | os.makedirs(Vars.cfg.data['output_dir']) 207 | if not os.path.isdir(os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir())): 208 | os.makedirs(os.path.join(Vars.cfg.data['output_dir'], self.fix_illegal_book_name_dir())) 209 | self.epub.make_cover_text(self.book_info['book_name'], self.book_info['author_name'], 210 | self.book_info['description'], self.book_info['uptime'], self.book_id) 211 | self.epub.download_book_write_chapter(self.division_chapter_list) 212 | # self.config.data['book_info'] = self.book_info 213 | # self.config.data['division_chapter_list'] = self.division_chapter_list 214 | # self.config.save() 215 | print('\r' + msg.m('expo_e')) 216 | self.process_finished_count = 0 217 | self.downloaded_count = 0 218 | 219 | def download_book_get_chapter(self, chapter_index, chapter_id, division_index, chapter_order): 220 | division_name = None 221 | for division in self.division_list: 222 | if division['division_index'] == division_index: 223 | division_name = division['division_name'] 224 | chapter_title = None 225 | if division_name is not None: 226 | chapter_title = self.division_chapter_list[division_name][chapter_order - 1]['chapter_title'] 227 | f_name = division_index.rjust(4, "0") + '-' + str(chapter_order).rjust(6, "0") + '-' + chapter_id 228 | if os.path.exists(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml'): 229 | if os.path.getsize(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml') == 0: 230 | # 章節檔案大小為0 重新下載 231 | if chapter_title == "该章节未审核通过": 232 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + '-' + 233 | str(chapter_order).rjust(6, "0") + '-' + chapter_id + " 分辨屏蔽章節下載: 標題 #1 " + 234 | division_name + ':' + chapter_title + "\n" + 235 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 236 | len(self.chapter_list)), end=' ') 237 | return False 238 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + "-" + 239 | str(chapter_order).rjust(6, "0") + "-" + str(chapter_id) + 240 | msg.m('dl_0_chap_re_dl') + division_name + ':' + chapter_title + 241 | "\n" + str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 242 | len(self.chapter_list)), end=' ') 243 | else: 244 | # 章節已經下載過 跳過 245 | self.add_process_finished_count() 246 | if chapter_title == "该章节未审核通过": 247 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + "-" + 248 | str(chapter_order).rjust(6, "0") + "-" + chapter_id + 249 | msg.m( 250 | 'dl_chap_block_c') + division_name + ':' + chapter_title + " : 分辨屏蔽章節下載: 標題 #2 " + 251 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + 252 | str(len(self.chapter_list)), end=' ') 253 | else: 254 | print('\r' + str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 255 | len(self.chapter_list)), end=' ') 256 | self.get_chapter_catalog_mt_dl_lock.release() 257 | return True 258 | 259 | q_item = self.concurrent_download_queue.get() 260 | response = HbookerAPI.Chapter.get_chapter_command(chapter_id) 261 | if response.get('code') == '100000': 262 | chapter_command = response['data']['command'] 263 | response2 = HbookerAPI.Chapter.get_cpt_ifm(chapter_id, chapter_command) 264 | self.concurrent_download_queue.put(q_item) 265 | if response2.get('code') == '100000' and response2['data']['chapter_info'].get('chapter_title') is not None: 266 | if response2['data']['chapter_info']['auth_access'] == '1': 267 | content = HbookerAPI.CryptoUtil.decrypt(response2['data']['chapter_info']['txt_content'], 268 | chapter_command).decode('utf-8') 269 | if content[-1] == '\n': 270 | content = content[:-1] 271 | content = content.replace('\n', '

\r\n

') 272 | 273 | if content == "本章节内容未审核通过 " \ 274 | " ": 275 | # 分辨屏蔽章節下載 (2021/07/13) 276 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + '-' + 277 | str(chapter_order).rjust(6, "0") + '-' + chapter_id + " 分辨屏蔽章節下載: 內容 #3 " + 278 | division_name + ':' + chapter_title + "\n" + 279 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 280 | len(self.chapter_list)), end=' ') 281 | with codecs.open(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml', 'w', 'utf-8') as _file: 282 | pass 283 | return False 284 | if response2['data']['chapter_info']['chapter_title'] == "该章节未审核通过#Ejxt" or \ 285 | chapter_title == "该章节未审核通过": 286 | # 分辨屏蔽章節下載 (2021/07/13) 287 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + '-' + 288 | str(chapter_order).rjust(6, "0") + '-' + chapter_id + " 分辨屏蔽章節下載: 標題 #4 " + 289 | division_name + ':' + chapter_title + "\n" + 290 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 291 | len(self.chapter_list)), end=' ') 292 | with codecs.open(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml', 'w', 'utf-8') as _file: 293 | pass 294 | return False 295 | 296 | # 下載成功 297 | author_say = response2['data']['chapter_info']['author_say'].replace('\r', '') 298 | author_say = author_say.replace('\n', '

\r\n

') 299 | # version above 2.9.225 chapter_title from 300 | # self.division_chapter_list[division_name][chapter_order - 1]['chapter_title'] 301 | # and 302 | # response2['data']['chapter_info']['chapter_title'] 303 | # are different 304 | # response2['data']['chapter_info']['chapter_title'] has string #[a-zA-Z0-9]{4} ending 305 | # print(chapter_title, response2['data']['chapter_info']['chapter_title']) 306 | if chapter_title is None or chapter_title == '': 307 | print("debug: self.division_chapter_list[division_name][chapter_order - 1]['chapter_title'] " 308 | "is None or blank") 309 | chapter_title = response2['data']['chapter_info']['chapter_title'] 310 | # change fail safe, old method 311 | self.epub.add_chapter(chapter_id, division_name, 312 | chapter_title, '

' + content + 313 | '

\r\n

' + author_say + '

', division_index, chapter_order) 314 | self.add_process_finished_count() 315 | self.downloaded_count += 1 316 | print('\r' + str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 317 | len(self.chapter_list)), end=' ') 318 | self.get_chapter_catalog_mt_dl_lock.release() 319 | return True 320 | else: 321 | # 異常狀況,態異常,已訂閱但無法下載。 322 | self.add_process_finished_count() 323 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + '-' + 324 | str(chapter_order).rjust(6, "0") + '-' + chapter_id + msg.m('dl_error_paid_stat_conflict') + 325 | division_name + ':' + chapter_title + "\n" + 326 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 327 | len(self.chapter_list)), end=' ') 328 | self.get_chapter_catalog_mt_dl_lock.release() 329 | return False 330 | else: 331 | # 章節下載異常,請再次嘗試下載 332 | with codecs.open(self.epub.tempdir + '/OEBPS/Text/' + f_name + '.xhtml', 'w', 'utf-8') as _file: 333 | pass 334 | self.add_process_finished_count() 335 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + '-' + 336 | str(chapter_order).rjust(6, "0") + '-' + chapter_id + 337 | msg.m('dl_error_chap_get_failed_1') + division_name + ':' + chapter_title + "\n" + 338 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 339 | len(self.chapter_list)), end=' ') 340 | self.get_chapter_catalog_mt_dl_lock.release() 341 | return True 342 | else: 343 | self.concurrent_download_queue.put(q_item) 344 | self.add_process_finished_count() 345 | print('\r' + chapter_index.rjust(5, "0") + ', ' + division_index.rjust(4, "0") + '-' + 346 | str(chapter_order).rjust(6, "0") + '-' + chapter_id + msg.m('dl_error_chap_get_failed_2') + 347 | division_name + ':' + chapter_title + "\n" + 348 | str(self.downloaded_count) + ' / ' + str(self.process_finished_count) + " / " + str( 349 | len(self.chapter_list)), end=' ') 350 | self.get_chapter_catalog_mt_dl_lock.release() 351 | return False 352 | 353 | def add_process_finished_count(self): 354 | self.get_chapter_catalog_mt_dl_lock.acquire() 355 | self.process_finished_count += 1 356 | -------------------------------------------------------------------------------- /bookshelf.py: -------------------------------------------------------------------------------- 1 | from book import * 2 | import msg 3 | 4 | BookShelfList = [] 5 | 6 | 7 | def get_bookshelf_by_index(shelf_index): 8 | for shelf in BookShelfList: 9 | if shelf.shelf_index == shelf_index: 10 | return shelf 11 | return None 12 | 13 | 14 | class BookShelf: 15 | shelf_id = None 16 | reader_id = None 17 | shelf_name = None 18 | shelf_index = None 19 | book_limit = None 20 | BookList = None 21 | 22 | def __init__(self, data): 23 | self.shelf_id = data['shelf_id'] 24 | self.reader_id = data['reader_id'] 25 | self.shelf_name = data['shelf_name'] 26 | self.shelf_index = data['shelf_index'] 27 | self.book_limit = data['book_limit'] 28 | self.BookList = [] 29 | 30 | def show_info(self): 31 | print(msg.m('shelf_index'), self.shelf_index, msg.m('shelf_name'), self.shelf_name) 32 | 33 | def get_book_list(self): 34 | response = HbookerAPI.BookShelf.get_shelf_book_list(self.shelf_id) 35 | if response.get('code') == '100000': 36 | self.BookList.clear() 37 | i = 1 38 | for data in response['data']['book_list']: 39 | self.BookList.append(Book(str(i), data['book_info'])) 40 | i += 1 41 | 42 | def show_book_list(self): 43 | for book in self.BookList: 44 | print('《', book.book_name, msg.m('show_list_author'), book.author_name, msg.m('show_list_book_index'), book.index, msg.m('show_list_book_id'), book.book_id) 45 | print(msg.m('show_list_uptime'), book.last_chapter_info['uptime'], msg.m('show_list_last_chap'), book.last_chapter_info['chapter_title'], '\n') 46 | print() 47 | 48 | def get_book(self, index): 49 | for book in self.BookList: 50 | if book.index == index: 51 | return book 52 | return None 53 | -------------------------------------------------------------------------------- /cache.py: -------------------------------------------------------------------------------- 1 | from instance import * 2 | import book 3 | 4 | 5 | def save_cache(file_name: str, response: dict) -> None: 6 | if Vars.cfg.data.get('backups_local_cache'): 7 | if not os.path.exists(Vars.cfg.data['local_cache_dir']): 8 | os.mkdir(Vars.cfg.data['local_cache_dir']) 9 | with open(f"{Vars.cfg.data['local_cache_dir']}/{file_name}", 'w', encoding='utf-8') as book_info_file: 10 | json.dump(response, book_info_file, ensure_ascii=False, indent=4) 11 | # else: 12 | # print("未开启本地缓存备份,已跳过本地缓存备份步骤, 请在配置文件中开启本地缓存备份。") 13 | 14 | 15 | def load_cache(file_name: str) -> dict: 16 | local_cache_dir = f"{Vars.cfg.data['local_cache_dir']}/{file_name}" 17 | if os.path.exists(local_cache_dir) and \ 18 | os.path.getsize(local_cache_dir) > 0: 19 | with open(local_cache_dir, 'r', encoding='utf-8') as book_info_file: 20 | return json.load(book_info_file) 21 | else: 22 | print(f'\n{file_name} not found.') 23 | 24 | 25 | def test_division_list(book_id: str): 26 | division_list = load_cache(f"{book_id}_chapter_list.json") 27 | if isinstance(division_list, dict): 28 | print("服务器无法获取目录信息,从本地缓存中获取目录信息成功。") 29 | return division_list 30 | print("服务器无法获取目录信息,已尝试从本地缓存中获取,但未找到。") 31 | return None 32 | 33 | 34 | def test_cache_and_init_object(book_id: str) -> bool: 35 | Vars.current_book = load_cache(f"{book_id}.json") 36 | if not isinstance(Vars.current_book, dict): 37 | print("服务器无法获取书籍信息,已尝试从本地缓存中获取,但未找到。") 38 | return False 39 | Vars.current_book = book.Book(None, Vars.current_book) 40 | print("服务器无法获取书籍信息,从本地缓存中获取书籍信息成功。") 41 | return True 42 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | class Config: 6 | file_path = None 7 | dir_path = None 8 | data = None 9 | 10 | def __init__(self, file_path, dir_path): 11 | self.file_path = file_path 12 | self.dir_path = dir_path 13 | self.data = {} 14 | 15 | def load(self): 16 | try: 17 | with open(self.file_path, 'r', encoding='utf-8') as f: 18 | self.data = json.load(f) or {} 19 | except FileNotFoundError: 20 | try: 21 | if not os.path.isdir(self.dir_path): 22 | os.makedirs(self.dir_path) 23 | with open(self.file_path, 'w'): 24 | pass 25 | except Exception as e: 26 | print('error: ', e) 27 | print('error: while creating config file: ' + self.file_path) 28 | except Exception as e: 29 | print('error: ', e) 30 | print('error: while reading config file: ' + self.file_path) 31 | 32 | def save(self): 33 | try: 34 | if not os.path.isdir(self.dir_path): 35 | os.makedirs(self.dir_path) 36 | with open(self.file_path, 'w') as f: 37 | json.dump(self.data, f, sort_keys=True, indent=4) 38 | except Exception as e: 39 | print('error: ', e) 40 | print('error: while saving config file: ' + self.file_path) 41 | -------------------------------------------------------------------------------- /instance.py: -------------------------------------------------------------------------------- 1 | from config import * 2 | 3 | 4 | class Vars: 5 | cfg = Config('HbookerAppNovelDownloader - Config.json', os.getcwd()) 6 | current_bookshelf = None 7 | current_book = None 8 | 9 | 10 | def get(prompt, default=None): 11 | while True: 12 | ret = input(prompt) 13 | if ret != '': 14 | return ret 15 | elif default is not None: 16 | return default 17 | -------------------------------------------------------------------------------- /list.txt: -------------------------------------------------------------------------------- 1 | 此文件為書單範例 --- ^\s*([0-9]{9}).*$ 2 | 作者:红萌吃货 ---可添加註釋 進行分類 ---忽略 3 | 100019018 我家师傅被宅女拐走了 ---開頭有空格 ---執行下載 4 | #100001814 天才女仆伪娘 ---因為開頭的"#",並非空格或數字 ---不進行下載(忽略) 5 | 6 | -100126316 身为剑圣的我想要当魔王 作者:飞扬的 ---因為開頭有"-",並非空格或數字 ---不進行下載 7 | # 100021919 网游之羽化 伪娘王 ---因為開頭的"#",並非空格或數字 ---不進行下載 8 | 9 | 免費書 ---可添加註釋 ---被忽略 10 | 100036935 迦勒底的次元搞事旅途 作者:只是野生的搞事咕哒 ---開頭可加入空格 ---執行下載 11 | 100013438 刀剑神域之模糊的境界线 作者:绯雪千夜 ---執行下載 12 | 13 | 每行開頭的"空白類"無影響 14 | "9位數書籍ID"的文字才會觸發執行下載 15 | 如ID前出現非數字非空白則會忽略 16 | ID後可以隨意添加文字無影響 17 | 若ID超過9位數則只取前9位 -------------------------------------------------------------------------------- /msg.py: -------------------------------------------------------------------------------- 1 | lang = {} 2 | _TC = { 3 | # msg 4 | 'lang': "提示訊息切換為繁體中文", 5 | # run 6 | 'read_readme': """!!匯出書籍僅供自己保存閱讀禁止傳播!! !!使用前請仔細閱讀README.md!! 7 | 8 | !!匯出書籍僅供自己保存閱讀禁止傳播!! !!使用前請仔細閱讀README.md!! 9 | 10 | !!匯出書籍僅供自己保存閱讀禁止傳播!! !!使用前請仔細閱讀README.md!!\n\n""", 11 | 'agree_terms': """是否同意匯出書籍僅供自己保存閱讀,不傳播給他人 12 | 是否已仔細閱讀且同意README.md中敘述事物 13 | 如果兩者回答皆為\"是\",請輸入英文 \"yes\" 後按Enter建,如果不同意請關閉此程式\n""", 14 | 'not_login_pl_login': "未登入,請先登入", 15 | 'input_correct_var': "請輸入正確的參數", 16 | 'login_success_user': "登錄成功, 用戶暱稱為: ", 17 | 'login_method_change_message': """現在登入需要校驗,因此改為使用Token方式登入 18 | 選擇匯入方式 19 | 1) 匯入com.kuangxiangciweimao.novel_preferences.xml 20 | 2) 手動輸入""", 21 | 'import_token_complete': 'Token已匯入', 22 | 'error_response': "Error: ", 23 | 'picked_shelf_s': "選擇書架: \"", 24 | 'picked_shelf_e': "\"", 25 | 'not_picked_shelf': "未選擇書架", 26 | 'check_in_success_got': "簽到成功, 獲得: ", 27 | 'check_in_xp': " 經驗, ", 28 | 'check_in_token': " 代幣, ", 29 | 'check_in_recommend': " 推薦票\n", 30 | 'check_in_no_redo': "任務已完成,請勿重複簽到\n", 31 | 'check_in_no_certification': "此帳戶未實名認證,請先綁定手機\n", 32 | 'check_in_failed': "簽到失敗:\n", 33 | 'check_in_already': "已簽到\n", 34 | 'check_in_token_failed': "登入過期或失效,自動嘗試重新登入", 35 | 'check_in_re_login_retry_check_in': "成功重新登入,再次執行簽到", 36 | 'check_in_error_1': "簽到失敗,特殊原因,請手動處理:\n", 37 | 'check_in_re_login_failed': "重新登入失敗,請手動處理\n", 38 | 'check_in_error_2': "簽到失敗,特殊原因,請手動處理:\n", 39 | 'check_in_error_day_not_found': "日期異常,未找本日對應簽到記錄,不進行簽到嘗試,可能是因為裝置時間不準確\n", 40 | 'failed_get_book_info_index': "獲取書籍信息失敗, shelf_index:", 41 | 'failed_get_book_info_id': "獲取書籍信息失敗, book_id:", 42 | 'not_picked_book': "未選擇書籍", 43 | 'start_book_dl': "開始下載書籍...\n", 44 | 'app_update_info': "獲取更新訊息 : \n", 45 | 'current_version_var': "目前版本號 : ", 46 | 'get_app_version_var': "獲取版本號 : ", 47 | 'confirm_change_version_var': "是否使用\"獲取版本號\"替換\"目前版本號\"\n確定:輸入\"yes\", 取消:輸入其他字符", 48 | 'confirm_msg': "確認", 49 | 'cancel_msg': "取消", 50 | # shelf 51 | 'shelf_index': "書架编號:", 52 | 'shelf_name': ", 書架名:", 53 | 'show_list_author': "》作者:", 54 | 'show_list_book_index': "\n書籍编號:", 55 | 'show_list_book_id': ", 書籍ID:", 56 | 'show_list_uptime': "更新時間:", 57 | 'show_list_last_chap': "\n最新章節:", 58 | # book 59 | 'show_div_index': "分卷編號:", 60 | 'show_div_total': " 共:", 61 | 'show_div_name': " 章 分卷名: ", 62 | 'failed_get_chap': "章節獲取失敗: ", 63 | 'get_div': "正在獲取書籍分卷...", 64 | 'get_chap': "正在獲取書籍目錄...", 65 | 'failed_get_div': "分捲獲取失敗: ", 66 | 'show_last_chap_s_index': " 最新章節, 編號: ", 67 | 'show_last_chap_uptime': ", 更新時間:", 68 | 'show_last_chap_name': "\n 章節:", 69 | 'show_chap_list_index': "分卷:", 70 | 'dl_chap_block_e': ".xhtml,章節屏蔽無法下載,以空檔案標記。\n", 71 | 'dl_chap_block_c': ".xhtml,章節屏蔽無法下載,使用本地檔案。\n", 72 | 'dl_chap_not_paid': ",該章節未訂閱無法下載。\n", 73 | 'dl_fin': "\n\n下載完畢...", 74 | 'expo_s': "匯出書籍...", 75 | 'expo_e': "匯出完成\n\n", 76 | 'expo_no': "書籍無更新\n\n", 77 | 'dl_0_chap_re_dl': ".xhtml,發現缺失章節(空檔案),重新下載。\n", 78 | 'dl_error_paid_stat_conflict': ",錯誤,該章節訂授權態異常無法下載,請再次嘗試下載。!!!!!\n", 79 | 'dl_error_chap_get_failed_1': ".xhtml,錯誤,章節下載異常,請重新嘗試下載,章節缺失以空檔案標記!!!!!\n", 80 | 'dl_error_chap_get_failed_2': ".xhtml,錯誤,章節下載異常,請重新嘗試下載!!!!!\n", 81 | # epub 82 | 'cover_dl_rt': "下載封面圖片失敗,重試: ", 83 | 'cover_dl_f': "下載封面圖片失敗,放棄: ", 84 | 'image_dl_rt': "下載圖片失敗,重試: ", 85 | 'image_dl_f': "下載圖片失敗,放棄: ", 86 | # help 87 | 'help_msg': """HbookerAppNovelDownloader 刺蝟貓 / 歡樂書客 小說下載器 88 | 請閱讀README.md 89 | 指令(指令輸入字首即可): 90 | h | help\t\t\t\t\t\t--- 顯示說明 (此訊息) 91 | m | message\t\t\t\t\t\t--- 切換提示訊息 (繁體/簡體) 92 | q | quit\t\t\t\t\t\t--- 退出腳本 93 | version\t\t\t\t\t\t\t--- 從網路獲取現在版本號,詢問是否刷新版本號 (輸入完整單字) 94 | i | import_token\t\t\t\t--- 以匯入token方式登入帳號 95 | l | login <手機號/郵箱/用戶名> <密碼>\t\t\t--- 登錄歡樂書客帳號!!!已失效!!! 96 | t | task\t\t\t\t\t\t--- 執行每日簽到,領代幣 (啟動時自動執行,無異常不需再次執行) 97 | s | shelf\t\t\t\t\t\t--- 刷新並顯示當前書架列表 (啟動時會自動刷新1次) 98 | s <書架編號> | shelf <書架編號>\t\t\t\t--- 選擇與切換書架 99 | b | book\t\t\t\t\t\t--- 刷新並顯示當前書架的書籍列表 100 | b <書籍編號/書籍ID> | book <書籍編號/書籍ID>\t\t--- 選擇書籍 101 | d | download\t\t\t\t\t\t--- 下載當前書籍(book時選擇的書籍) 102 | d <書籍編號/書籍ID> | download <書籍編號/書籍ID>\t--- 下載指定ID書籍 103 | ds <書架編號> | download_shelf <書架編號> \t\t--- 下載整個書架 104 | u | update\t\t\t\t\t\t--- 下載"list.txt"書單之中所列書籍 105 | u | update \t\t\t--- 下載指定書單中所列書籍 106 | """ 107 | } 108 | # 下載指定檔案"list_path.txt"中的所有書籍 109 | _SC = { 110 | # msg 111 | 'lang': "提示讯息切换为简体中文", 112 | # run 113 | 'read_readme': """!!汇出书籍仅供自己保存阅读禁止传播!! !!使用前请仔细阅读README.md!! 114 | 115 | !!汇出书籍仅供自己保存阅读禁止传播!! !!使用前请仔细阅读README.md!! 116 | 117 | !!汇出书籍仅供自己保存阅读禁止传播!! !!使用前请仔细阅读README.md!!\n\n""", 118 | 'agree_terms': """是否同意汇出书籍仅供自己保存阅读,不传播给他人 119 | 是否已仔细阅读且同意README.md中叙述事物 120 | 如果两者回答皆为\"是\",请输入英文 \"yes\" 后按Enter建,如果不同意请关闭此程式\n""", 121 | 'not_login_pl_login': "未登入,请先登入", 122 | 'input_correct_var': "请输入正确的参数", 123 | 'login_success_user': "登录成功, 用户暱称为: ", 124 | 'login_method_change_message': """现在登入需要校验,因此改为使用Token方式登入 125 | 选择汇入方式 126 | 1) 汇入com.kuangxiangciweimao.novel_preferences.xml 127 | 2) 手动输入""", 128 | 'import_token_complete': 'Token已汇入', 129 | 'error_response': "Error: ", 130 | 'picked_shelf_s': "选择书架: \"", 131 | 'picked_shelf_e': "\"", 132 | 'not_picked_shelf': "未选择书架", 133 | 'check_in_success_got': "签到成功, 获得: ", 134 | 'check_in_xp': " 经验, ", 135 | 'check_in_token': " 代币, ", 136 | 'check_in_recommend': " 推荐票\n", 137 | 'check_in_no_redo': "任务已完成,请勿重複签到\n", 138 | 'check_in_no_certification': "此账户未实名认证,请先绑定手机\n", 139 | 'check_in_failed': "签到失败:\n", 140 | 'check_in_already': "已签到\n", 141 | 'check_in_token_failed': "登入过期或失效,自动尝试重新登入", 142 | 'check_in_re_login_retry_check_in': "成功重新登入,再次执行签到", 143 | 'check_in_error_1': "签到失败,特殊原因,请手动处理:\n", 144 | 'check_in_re_login_failed': "重新登入失败,请手动处理\n", 145 | 'check_in_error_2': "签到失败,特殊原因,请手动处理:\n", 146 | 'check_in_error_day_not_found': "日期异常,未找本日对应签到记录,不进行签到尝试,可能是因为装置时间不准确\n", 147 | 'failed_get_book_info_index': "获取书籍信息失败, shelf_index:", 148 | 'failed_get_book_info_id': "获取书籍信息失败, book_id:", 149 | 'not_picked_book': "未选择书籍", 150 | 'start_book_dl': "开始下载书籍...\n", 151 | 'app_update_info': "获取更新讯息 : \n", 152 | 'current_version_var': "目前版本号 : ", 153 | 'get_app_version_var': "获取版本号 : ", 154 | 'confirm_change_version_var': "是否使用\"获取版本号\"替换\"目前版本号\"\n确定:输入\"yes\", 取消:输入其他字符", 155 | 'confirm_msg': "确认", 156 | 'cancel_msg': "取消", 157 | # shelf 158 | 'shelf_index': "书架编号:", 159 | 'shelf_name': ", 书架名:", 160 | 'show_list_author': "》作者:", 161 | 'show_list_book_index': "\n书籍编号:", 162 | 'show_list_book_id': ", 书籍ID:", 163 | 'show_list_uptime': "更新时间:", 164 | 'show_list_last_chap': "\n最新章节:", 165 | # book 166 | 'show_div_index': "分卷编号:", 167 | 'show_div_total': " 共:", 168 | 'show_div_name': " 章 分卷名: ", 169 | 'failed_get_chap': "章节获取失败: ", 170 | 'get_div': "正在获取书籍分卷...", 171 | 'get_chap': "正在获取书籍目录...", 172 | 'failed_get_div': "分捲获取失败: ", 173 | 'show_last_chap_s_index': " 最新章节, 编号: ", 174 | 'show_last_chap_uptime': ", 更新时间:", 175 | 'show_last_chap_name': "\n 章节:", 176 | 'show_chap_list_index': "分卷:", 177 | 'dl_chap_block_e': ".xhtml,章节屏蔽无法下载,以空档案标记。\n", 178 | 'dl_chap_block_c': ".xhtml,章节屏蔽无法下载,使用本地档案。\n", 179 | 'dl_chap_not_paid': ",该章节未订阅无法下载。\n", 180 | 'dl_fin': "\n\n下载完毕...", 181 | 'expo_s': "汇出书籍...", 182 | 'expo_e': "汇出完成\n\n", 183 | 'expo_no': "书籍无更新\n\n", 184 | 'dl_0_chap_re_dl': ".xhtml,發现缺失章节(空档案),重新下载。\n", 185 | 'dl_error_paid_stat_conflict': ",错误,该章节订授权态异常无法下载,请再次尝试下载。!!!!!\n", 186 | 'dl_error_chap_get_failed_1': ".xhtml,错误,章节下载异常,请重新尝试下载,章节缺失以空档案标记!!!!!\n", 187 | 'dl_error_chap_get_failed_2': ".xhtml,错误,章节下载异常,请重新尝试下载!!!!!\n", 188 | # epub 189 | 'cover_dl_rt': "下载封面图片失败,重试: ", 190 | 'cover_dl_f': "下载封面图片失败,放弃: ", 191 | 'image_dl_rt': "下载图片失败,重试: ", 192 | 'image_dl_f': "下载图片失败,放弃: ", 193 | # help 194 | 'help_msg': """HbookerAppNovelDownloader 刺蝟猫 / 欢乐书客 小说下载器 195 | 请阅读README.md 196 | 指令(指令输入字首即可): 197 | h | help\t\t\t\t\t\t--- 显示说明 (显示此讯息) 198 | m | message\t\t\t\t\t\t--- 切换提示讯息 (繁体/简体) 199 | q | quit\t\t\t\t\t\t--- 退出脚本 200 | version\t\t\t\t\t\t\t--- 从网路获取现在版本号,询问是否刷新版本号 (输入完整单字) 201 | i | import_token\t\t\t\t--- 以匯入token方式登入帳號 202 | l | login <手機號/郵箱/用戶名> <密碼>\t\t\t--- 登錄歡樂書客帳號!!!已失效!!! 203 | t | task\t\t\t\t\t\t--- 执行每日签到,领代币 (启动时自动执行,无异常不需再次执行) 204 | s | shelf\t\t\t\t\t\t--- 刷新并显示当前书架列表 (启动时会自动刷新1次) 205 | s <书架编号> | shelf <书架编号>\t\t\t\t--- 选择与切换书架 206 | b | book\t\t\t\t\t\t--- 刷新并显示当前书架的书籍列表 207 | b <书籍编号/书籍ID> | book <书籍编号/书籍ID>\t\t--- 选择书籍 208 | d | download\t\t\t\t\t\t--- 下载当前书籍(book时选择的书籍) 209 | d <书籍编号/书籍ID> | download <书籍编号/书籍ID>\t--- 下载指定ID书籍 210 | ds <书架编号> | download_shelf <书架编号> \t\t--- 下载整个书架 211 | u | update\t\t\t\t\t\t--- 下载"list.txt"书单之中所列书籍 212 | u | update \t\t\t--- 下载指定书单中所列书籍 213 | """ 214 | } 215 | 216 | 217 | def set_message_lang(tc: bool = False): 218 | global lang 219 | if tc: 220 | lang = _TC 221 | else: 222 | lang = _SC 223 | 224 | 225 | def m(key: str = ''): 226 | message = lang.get(key) 227 | if message is not None: 228 | return str(message) 229 | else: 230 | return '' 231 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from bookshelf import * 2 | from instance import * 3 | import HbookerAPI 4 | import datetime 5 | import msg 6 | import sys 7 | import re 8 | import token_parser 9 | import cache 10 | 11 | default_current_app_version = "2.9.290" 12 | 13 | 14 | def refresh_bookshelf_list(): 15 | response = HbookerAPI.BookShelf.get_shelf_list() 16 | if response.get('code') == '100000': 17 | BookShelfList.clear() 18 | for shelf in response['data']['shelf_list']: 19 | BookShelfList.append(BookShelf(shelf)) 20 | else: 21 | print(msg.m('error_response') + str(response)) 22 | for shelf in BookShelfList: 23 | shelf.show_info() 24 | if len(BookShelfList) == 1: 25 | shell_bookshelf(['', '1']) 26 | 27 | 28 | def shell_login(inputs): 29 | print('!!!此登入法已失效!!!保留測試用 20220609 ') 30 | if len(inputs) == 1 and Vars.cfg.data.get('user_account') is not None and \ 31 | Vars.cfg.data.get('user_password') is not None: 32 | pass 33 | elif len(inputs) >= 3: 34 | Vars.cfg.data['user_account'] = inputs[1] 35 | Vars.cfg.data['user_password'] = inputs[2] 36 | else: 37 | print(msg.m('input_correct_var')) 38 | return False 39 | response = HbookerAPI.SignUp.login(Vars.cfg.data['user_account'], Vars.cfg.data['user_password']) 40 | if response.get('code') == '100000': 41 | Vars.cfg.data['reader_name'] = response['data']['reader_info']['reader_name'] 42 | Vars.cfg.data['user_code'] = response['data']['user_code'] 43 | Vars.cfg.data['common_params'] = {'login_token': response['data']['login_token'], 44 | 'account': response['data']['reader_info']['account']} 45 | Vars.cfg.save() 46 | HbookerAPI.set_common_params(Vars.cfg.data['common_params']) 47 | print(msg.m('login_success_user') + Vars.cfg.data['reader_name']) 48 | return True 49 | else: 50 | # print("response logon: " + str(response)) 51 | print(response) 52 | return False 53 | 54 | 55 | def shell_bookshelf(inputs): 56 | if len(inputs) >= 2: 57 | if not BookShelfList: 58 | refresh_bookshelf_list() 59 | Vars.current_bookshelf = get_bookshelf_by_index(inputs[1]) 60 | if Vars.current_bookshelf is None: 61 | print(msg.m('input_correct_var')) 62 | else: 63 | print(msg.m('picked_shelf_s') + Vars.current_bookshelf.shelf_name + msg.m('picked_shelf_e')) 64 | Vars.current_bookshelf.get_book_list() 65 | Vars.current_bookshelf.show_book_list() 66 | else: 67 | refresh_bookshelf_list() 68 | 69 | 70 | def shell_select_books(inputs): 71 | if len(inputs) >= 2: 72 | Vars.current_book = None 73 | if Vars.current_bookshelf is not None and not re.match('^[0-9]{9,}$', inputs[1]): 74 | Vars.current_book = Vars.current_bookshelf.get_book(inputs[1]) 75 | if Vars.current_book is None: 76 | print(msg.m('failed_get_book_info_index'), inputs[1]) 77 | return 78 | elif re.match('^[0-9]{9,}$', inputs[1]): 79 | Vars.current_book = HbookerAPI.Book.get_info_by_id(inputs[1]) 80 | if Vars.current_book.get('code') == '100000': 81 | Vars.current_book = Book(None, Vars.current_book['data']['book_info']) 82 | else: 83 | # test local cache and init a book.Book 84 | if not cache.test_cache_and_init_object(inputs[1]): 85 | return 86 | else: 87 | print('input', inputs[1], 'not a book ID, ID should be a 9 digit number') 88 | return 89 | # update book info 90 | cache.save_cache(f"{Vars.current_book.book_id}.json", Vars.current_book.book_info) 91 | 92 | print('《' + Vars.current_book.book_name + '》') 93 | Vars.current_book.get_division_list() 94 | Vars.current_book.get_chapter_catalog() 95 | # with open('test/book info.json', 'w') as f: 96 | # json.dump({'book_info': response['data']['book_info'], 'division_list': Book.division_list}, f, indent=4) 97 | if len(inputs) < 3: 98 | Vars.current_book.show_division_list() 99 | Vars.current_book.show_latest_chapter() 100 | server_time = (datetime.datetime.now(tz=datetime.timezone.utc) + 101 | datetime.timedelta(hours=8)).replace(tzinfo=None) 102 | last_update_time = \ 103 | datetime.datetime.strptime(Vars.current_book.last_chapter_info['uptime'], '%Y-%m-%d %H:%M:%S') 104 | up_ago_time = server_time - last_update_time 105 | print(' last update ' + str(up_ago_time.days) + ' days ago') 106 | else: 107 | if Vars.current_book is not None: 108 | Vars.current_book.show_chapter_list_order_division() 109 | elif Vars.current_bookshelf is None: 110 | print(msg.m('not_picked_shelf')) 111 | else: 112 | Vars.current_bookshelf.get_book_list() 113 | Vars.current_bookshelf.show_book_list() 114 | 115 | 116 | def shell_download_book(inputs): 117 | if len(inputs) > 1: 118 | shell_select_books(inputs) 119 | if Vars.current_book is None: 120 | print(msg.m('not_picked_book')) 121 | return 122 | print(msg.m('start_book_dl')) 123 | Vars.current_book.download_book_multi_thread() 124 | 125 | 126 | def shell_download_list(inputs): 127 | if len(inputs) >= 2: 128 | list_file = inputs[1] 129 | else: 130 | list_file = 'list.txt' 131 | try: 132 | list_file_input = open(list_file, 'r', encoding='utf-8') 133 | except OSError: 134 | print(OSError) 135 | return 136 | list_lines = list_file_input.readlines() 137 | for line in list_lines: 138 | if re.match("^\\s*([0-9]{9}).*$", line): 139 | book_id = re.sub("^\\s*([0-9]{9}).*$\\n?", "\\1", line) 140 | print("Book ID: " + book_id + " ", end='') 141 | shell_download_book(['', book_id, '']) 142 | 143 | 144 | def shell_download_shelf(inputs): 145 | if len(inputs) >= 2: 146 | shell_bookshelf(inputs) 147 | if Vars.current_bookshelf is not None: 148 | for bookshelf_index, book in enumerate(Vars.current_bookshelf.BookList, start=1): 149 | # shell_download_book(['', book.book_id]) 150 | shell_download_book(['', str(bookshelf_index)]) # check if bookshelf_index is correct and download 151 | else: 152 | print(msg.m('not_picked_shelf')) 153 | 154 | 155 | def check_in_today(): 156 | # if Vars.cfg.data.get('user_account') is None or Vars.cfg.data.get('user_account') == "" \ 157 | # or Vars.cfg.data.get('user_password') is None or Vars.cfg.data.get('user_password') == "": 158 | # print(msg.m('not_login_pl_login')) 159 | # return False 160 | check_in_records = HbookerAPI.CheckIn.get_check_in_records() 161 | if check_in_records.get('code') == '100000': 162 | if check_in_today_do(check_in_records): 163 | return True 164 | else: 165 | return False 166 | elif check_in_records.get('code') == '200001': 167 | # {'code': '200001', 'tip': '缺少登录必需参数'} 168 | # {'code': '310002', 'tip': '此账户未实名认证,请先绑定手机'} 169 | # {'code': '240001', 'tip': '注册超过24小时的用户才能签到哦~'} 170 | print(check_in_records) 171 | print(msg.m('not_login_pl_login')) 172 | return False 173 | # elif check_in_records.get('code') == '200100': 174 | # # {'code': '200100', 'tip': '登录状态过期,请重新登录'} 175 | # print(msg.m('check_in_token_failed')) 176 | # if shell_login(['']): 177 | # print(msg.m('check_in_re_login_retry_check_in')) 178 | # check_in_records = HbookerAPI.CheckIn.get_check_in_records() 179 | # if check_in_records.get('code') == '100000': 180 | # if check_in_today_do(check_in_records): 181 | # return True 182 | # else: 183 | # return False 184 | # else: 185 | # print(msg.m('check_in_error_1') + str(check_in_records) + '\n') 186 | # return False 187 | # else: 188 | # print(msg.m('check_in_re_login_failed')) 189 | # return False 190 | else: 191 | print(msg.m('check_in_error_2') + str(check_in_records) + '\n') 192 | return False 193 | 194 | 195 | def check_in_today_do(check_in_records): 196 | # UTC+8 197 | server_time = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(hours=8) 198 | print(str(server_time.date()) + " " + str(server_time.hour).rjust(2, '0') + ":" 199 | + str(server_time.minute).rjust(2, '0')) 200 | today = str(server_time.date()) 201 | for record in check_in_records['data']['sign_record_list']: 202 | if record['date'] == today: 203 | if record['is_signed'] == '0': 204 | check_in = HbookerAPI.CheckIn.do_check_in() 205 | if check_in.get('code') == '100000': 206 | check_in_exp = check_in.get('data').get('bonus').get('exp') 207 | check_in_hlb = check_in.get('data').get('bonus').get('hlb') 208 | check_in_recommend = check_in.get('data').get('bonus').get('recommend') 209 | print(msg.m('check_in_success_got') + str(check_in_exp) + msg.m('check_in_xp') + str(check_in_hlb) 210 | + msg.m('check_in_token') + str(check_in_recommend) + msg.m('check_in_recommend')) 211 | if check_in_exp is None or check_in_hlb is None or check_in_recommend is None: 212 | print('debug : check\n' + str(check_in)) # debug 213 | return True 214 | elif check_in.get('code') == '340001': 215 | print(msg.m('check_in_no_redo')) 216 | return True 217 | elif check_in.get('code') == '310002': 218 | print(msg.m('check_in_failed') + msg.m('check_in_no_certification')) 219 | return True 220 | else: 221 | print(msg.m('check_in_failed') + str(check_in) + '\n') 222 | return False 223 | else: 224 | print(msg.m('check_in_already')) 225 | return True 226 | # 日期異常,未找本日對應簽到記錄,不進行簽到嘗試 227 | print(msg.m('check_in_error_day_not_found') + str(check_in_records)) 228 | return False 229 | 230 | 231 | def agreed_read_readme(): 232 | if Vars.cfg.data.get('agreed_to_readme') != 'yes': 233 | print(msg.m('read_readme')) 234 | print(msg.m('agree_terms')) 235 | 236 | confirm = get('>').strip() 237 | if confirm == 'yes': 238 | Vars.cfg.data['agreed_to_readme'] = 'yes' 239 | Vars.cfg.save() 240 | else: 241 | sys.exit() 242 | 243 | 244 | def shell_switch_message_charter_set(): 245 | if Vars.cfg.data['interface_traditional_chinese']: 246 | Vars.cfg.data['interface_traditional_chinese'] = False 247 | pass 248 | else: 249 | Vars.cfg.data['interface_traditional_chinese'] = True 250 | Vars.cfg.save() 251 | msg.set_message_lang(Vars.cfg.data['interface_traditional_chinese']) 252 | print(msg.m('lang')) 253 | 254 | 255 | def setup_config(): 256 | Vars.cfg.load() 257 | config_change = False 258 | if type(Vars.cfg.data.get('interface_traditional_chinese')) is not bool: 259 | Vars.cfg.data['interface_traditional_chinese'] = False 260 | msg.set_message_lang() 261 | config_change = True 262 | msg.set_message_lang(Vars.cfg.data['interface_traditional_chinese']) 263 | 264 | if type(Vars.cfg.data.get('cache_dir')) is not str or Vars.cfg.data.get('cache_dir') == "": 265 | Vars.cfg.data['cache_dir'] = "./Cache/" 266 | config_change = True 267 | 268 | if type(Vars.cfg.data.get('output_dir')) is not str or Vars.cfg.data.get('output_dir') == "": 269 | Vars.cfg.data['output_dir'] = "./Hbooker/" 270 | config_change = True 271 | 272 | if type(Vars.cfg.data.get('local_cache_dir')) is not str or Vars.cfg.data.get('local_cache_dir') == "": 273 | Vars.cfg.data['local_cache_dir'] = "./LocalCache/" 274 | config_change = True 275 | 276 | if not isinstance(Vars.cfg.data.get('backups_local_cache'), bool): 277 | Vars.cfg.data['backups_local_cache'] = True 278 | config_change = True 279 | 280 | if type(Vars.cfg.data.get('do_backup')) is not bool: 281 | Vars.cfg.data['do_backup'] = True 282 | config_change = True 283 | 284 | if type(Vars.cfg.data.get('backup_dir')) is not str or Vars.cfg.data.get('backup_dir') == "": 285 | Vars.cfg.data['backup_dir'] = "./Hbooker/" 286 | config_change = True 287 | 288 | if not isinstance(Vars.cfg.data.get('force_book_description'), bool): 289 | Vars.cfg.data['force_book_description'] = True 290 | config_change = True 291 | 292 | if type(Vars.cfg.data.get('max_concurrent_downloads')) is not int or \ 293 | Vars.cfg.data.get('max_concurrent_downloads') < 1: 294 | Vars.cfg.data['max_concurrent_downloads'] = 8 295 | config_change = True 296 | 297 | if type(Vars.cfg.data.get('current_app_version')) is not str: 298 | Vars.cfg.data['current_app_version'] = default_current_app_version 299 | config_change = True 300 | HbookerAPI.common_params['app_version'] = Vars.cfg.data['current_app_version'] 301 | 302 | # if type(Vars.cfg.data.get('export_epub')) is not bool: 303 | # Vars.cfg.data['export_txt'] = True 304 | # config_change = True 305 | # 306 | # if type(Vars.cfg.data.get('export_txt')) is not bool: 307 | # Vars.cfg.data['export_txt'] = True 308 | # config_change = True 309 | 310 | if config_change: 311 | Vars.cfg.save() 312 | 313 | 314 | def get_app_update_version_info(): 315 | response = (HbookerAPI.CheckAppVersion.get_version()) 316 | if response.get('code') == '100000': 317 | android_version = response.get('data').get('android_version') 318 | print(msg.m('app_update_info') + str(response)) 319 | print(msg.m('current_version_var') + HbookerAPI.common_params['app_version']) 320 | print(msg.m('get_app_version_var') + android_version) 321 | 322 | print(msg.m('confirm_change_version_var')) 323 | confirm = get('>').strip() 324 | if confirm == 'yes': 325 | print(msg.m('confirm_msg')) 326 | HbookerAPI.common_params['app_version'] = android_version 327 | Vars.cfg.data['current_app_version'] = android_version 328 | Vars.cfg.save() 329 | else: 330 | print(msg.m('cancel_msg')) 331 | print(msg.m('current_version_var') + HbookerAPI.common_params['app_version']) 332 | else: 333 | print("error response: " + str(response)) 334 | 335 | 336 | def import_token(): 337 | print(msg.m('login_method_change_message')) 338 | import_method = input('1/2: ') 339 | if import_method == '1': 340 | user_token = token_parser.token_from_novel_preferences_xml() 341 | Vars.cfg.data['reader_name'] = user_token.get('reader_name') 342 | Vars.cfg.data['user_code'] = user_token.get('user_code') 343 | Vars.cfg.data['common_params'] = {'login_token': user_token.get('login_token'), 344 | 'account': user_token.get('account')} 345 | Vars.cfg.save() 346 | HbookerAPI.set_common_params(Vars.cfg.data['common_params']) 347 | if HbookerAPI.common_params.get('device_token') is not None: 348 | HbookerAPI.common_params.pop('device_token') 349 | print(user_token) 350 | else: 351 | user_token = token_parser.token_from_input() 352 | Vars.cfg.data['common_params'] = {'login_token': user_token.get('login_token'), 353 | 'account': user_token.get('account')} 354 | Vars.cfg.save() 355 | HbookerAPI.set_common_params(Vars.cfg.data['common_params']) 356 | print(user_token) 357 | print(msg.m('import_token_complete')) 358 | token_test() 359 | 360 | 361 | def toggle_token_device(): 362 | if type(Vars.cfg.data.get('common_params')) is dict: 363 | if 'device_token' in Vars.cfg.data.get('common_params'): 364 | Vars.cfg.data['common_params'].pop('device_token') 365 | HbookerAPI.common_params.pop('device_token') 366 | print('remove device_token\': \'ciweimao_') 367 | else: 368 | Vars.cfg.data['common_params'].update({'device_token': 'ciweimao_'}) 369 | HbookerAPI.common_params.update({'device_token': 'ciweimao_'}) 370 | print('add device_token\': \'ciweimao_') 371 | Vars.cfg.save() 372 | 373 | 374 | def token_test(): 375 | login_in_test = HbookerAPI.CheckIn.get_check_in_records() 376 | if login_in_test.get('code') == '100000': 377 | print('login success') 378 | return 379 | elif login_in_test.get('code') == '200100': 380 | toggle_token_device() 381 | else: 382 | return 383 | 384 | login_in_test = HbookerAPI.CheckIn.get_check_in_records() 385 | if login_in_test.get('code') != '100000': 386 | print(msg.m('not_login_pl_login')) 387 | 388 | 389 | def shell(): 390 | if Vars.cfg.data.get('common_params') is not None: 391 | HbookerAPI.set_common_params(Vars.cfg.data['common_params']) 392 | if len(sys.argv) > 1: 393 | if str(sys.argv[1]).startswith('t'): 394 | if check_in_today(): 395 | sys.exit() 396 | else: 397 | sys.exit(5) 398 | # loop = True 399 | # inputs = [''] 400 | else: 401 | check_in_today() 402 | loop = False 403 | inputs = sys.argv[1:] 404 | else: 405 | check_in_today() 406 | loop = True 407 | print(msg.m('help_msg')) 408 | refresh_bookshelf_list() 409 | inputs = re.split('\\s+', get('>').strip()) 410 | else: 411 | loop = True 412 | save = False 413 | if Vars.cfg.data.get('user_account') is None or Vars.cfg.data.get('user_account') is not str: 414 | Vars.cfg.data['user_account'] = "" 415 | save = True 416 | if Vars.cfg.data.get('user_password') is None or Vars.cfg.data.get('user_password') is not str: 417 | Vars.cfg.data['user_password'] = "" 418 | save = True 419 | if save: 420 | Vars.cfg.save() 421 | print(msg.m('help_msg')) 422 | print(msg.m('not_login_pl_login')) 423 | if len(sys.argv) > 1: 424 | inputs = sys.argv[1:] 425 | else: 426 | inputs = re.split('\\s+', get('>').strip()) 427 | while True: 428 | if inputs[0].startswith('q'): 429 | sys.exit() 430 | elif inputs[0].startswith('l'): 431 | shell_login(inputs) 432 | check_in_today() 433 | elif inputs[0].startswith('i'): 434 | import_token() 435 | check_in_today() 436 | elif inputs[0].startswith('s'): 437 | shell_bookshelf(inputs) 438 | elif inputs[0].startswith('b'): 439 | shell_select_books(inputs) 440 | elif inputs[0].startswith('ds') or inputs[0].startswith('downloads'): 441 | shell_download_shelf(inputs) 442 | elif inputs[0].startswith('d'): 443 | shell_download_book(inputs) 444 | elif inputs[0].startswith('u'): 445 | shell_download_list(inputs) 446 | elif inputs[0].startswith('t'): 447 | check_in_today() 448 | elif inputs[0].startswith('m'): 449 | shell_switch_message_charter_set() 450 | elif inputs[0].startswith('version'): 451 | get_app_update_version_info() 452 | # elif inputs[0].startswith('p'): 453 | # toggle_token_device() 454 | else: 455 | print(msg.m('help_msg')) 456 | if loop is False: 457 | break 458 | inputs = re.split('\\s+', get('>').strip()) 459 | 460 | 461 | if __name__ == "__main__": 462 | setup_config() 463 | 464 | agreed_read_readme() 465 | 466 | shell() 467 | -------------------------------------------------------------------------------- /template.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hang333/HbookerAppNovelDownloader/0cf8925cde842f31c2bb47ac4c6241518bbe5f72/template.epub -------------------------------------------------------------------------------- /token_parser.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree 2 | import json 3 | 4 | 5 | def token_from_novel_preferences_xml(file: str='com.kuangxiangciweimao.novel_preferences.xml'): 6 | # parse token and other user info from xml file used by com.kuangxiangciweimao.novel 7 | # the file can be found at 8 | # com.kuangxiangciweimao.novel\shared_prefs\com.kuangxiangciweimao.novel_preferences.xml 9 | logined_user = json.loads(xml.etree.cElementTree.parse(file).getroot().find('string[@name="LoginedUser"]').text) 10 | return {'reader_name': logined_user.get('readerInfo').get('reader_name'), 11 | 'user_code': logined_user.get('userCode'), 12 | 'account': logined_user.get('readerInfo').get('account'), 13 | 'login_token': logined_user.get('loginToken')} 14 | 15 | 16 | def token_from_input(token: str = None, account: str = None): 17 | # used to input token and account manually 18 | data = {} 19 | if account is None: 20 | account = input('input account (例:书客123456789):') 21 | if token is None: 22 | token = input('input login_token (32 digit hex code):') 23 | return {'account': account, 'login_token': token} 24 | 25 | 26 | if __name__ == '__main__': 27 | print(token_from_novel_preferences_xml()) 28 | print(token_from_input()) 29 | --------------------------------------------------------------------------------