From 65c8c3ad18d6e3283e40eff2529107cf9a251918 Mon Sep 17 00:00:00 2001 From: quantran-novobi Date: Fri, 17 Sep 2021 19:10:41 +0700 Subject: [PATCH] Update version 2.0.0 --- README.md | 67 +++--- hako2epub.py | 521 ++++++++++++++++++++++++++++++------------- images/demo.png | Bin 0 -> 22250 bytes images/exec_demo.png | Bin 21313 -> 0 bytes 4 files changed, 403 insertions(+), 185 deletions(-) create mode 100644 images/demo.png delete mode 100644 images/exec_demo.png diff --git a/README.md b/README.md index e7d8ed8..db2b258 100644 --- a/README.md +++ b/README.md @@ -45,88 +45,91 @@ A tool to download light novels on [ln.hako.re](https://ln.hako.re) as the epub **_Notes:_** * _This tool is a personal standalone project, it does not have any related to [ln.hako.re](https://ln.hako.re) administrators._ -* _If possible, please support the website, original light novel, and light novel translation authors._ +* _If possible, please support the original light novel, hako website, and light novel translation authors._ * _This tool is for non-commercial purpose only._ ### Features +* Working with [docln.net](https://docln.net/). * Support images. * Support navigation and table of contents. +* Notes are shown directly in the light novel content. * Download all/single volume of a light novel. +* Download specific chapters of a light novel. * Update all/single downloaded light novel. * Update new volumes. * Update new chapters. - * Update new chapters of a single volume. -* Notes are shown directly in the light novel content.  -* Working with [docln.net](https://docln.net/). +* Support multiprocessing to speed up. +* Auto get current downloaded light novel in the directory. +* Auto checking the new tool version. ## Getting Started -For normal user, there is a single execution file [here](https://github.com/quantrancse/hako2epub/releases). Run and follow the instructions. +For normal user, download the execution file below. Run and follow the instructions. + +**Windows**: [hako2epub.exe ~ 14MB]() ### Prerequisites -* python 3.6.8 +* python 3.9.6 * ebooklib +* requests * bs4 * pillow +* tqdm +* questionary * argparse ```sh -pip install ebooklib bs4 pillow argparse +pip install ebooklib requests bs4 pillow argparse tqdm questionary ``` -**_Notes:_** _I only tested on python 3.6.8_ ### Usage -```bash -usage: hako2epub.py [-h] [-v ln_url] [-u [ln_url]] [-uv ln_url] [ln_url] +```text +usage: hako2epub.py [-h] [-c ln_url] [-u [ln_url]] [ln_url] + +A tool to download light novels on https://ln.hako.re as the epub file format for offline reading. positional arguments: - ln_url url to the ln homepage + ln_url url to the light novel page optional arguments: -h, --help show this help message and exit - -v ln_url, --volume ln_url - download single volume + -c ln_url, --chapter ln_url + download specific chapters of a light novel -u [ln_url], --update [ln_url] - update all/single ln - -uv ln_url, --updatevol ln_url - update single volume + update all/single light novel ``` * Download a light novel ```sh python hako2epub.py light_novel_url ``` -* Download a single volume in a light novel +* Download specific chapters of light novel ```sh -python hako2epub.py -v light_novel_url +python hako2epub.py -c light_novel_url ``` -* Update all light novels +* Update all downloaded light novels ```sh python hako2epub.py -u ``` -* Update a single light novel +* Update a single downloaded light novel ```sh python hako2epub.py -u light_novel_url ``` -* Update a single volume in a light novel -```sh -python hako2epub.py -uv light_novel_url -``` ### Notes * Light novel will be downloaded into the same folder as the program. * Downloaded information will be saved into `ln_info.json` file located in the same folder as the program. -* If you change anything (like move the epub files away), try to change the `ln_info.json` too. -* Or try not to change anything ^^ +* If you download specific chapters of a light novel, please enter the full name of the chapter in the "from ... to ..." prompt. +* If you update the volume which contains specific chapters, only new chapters after the current latest chapter will be added. +* Try to keep the program and `ln_info.json` file at the same folder with your downloaded light novels for efficiently management. ## Screenshots -![Demo](images/exec_demo.png) +![Demo](images/demo.png) ## Issues * I only tested on some of my favorite light novels. -* Images may crash on some epub readers. -* Sometime can not get image from some image hosts. +* Sometime can not get images from some image hosts. ## Contributing @@ -147,13 +150,13 @@ Distributed under the MIT License. See [LICENSE][license-url] for more informati ## Contact -* **Author** - [@quantrancse](https://www.facebook.com/quantrancse) +* **Author** - [@quantrancse](https://quantrancse.github.io) ## Acknowledgements * [EbookLib](https://github.com/aerkalov/ebooklib) -[python-shield]: https://img.shields.io/badge/python-3.6.8-brightgreen?style=flat-square -[license-shield]: https://img.shields.io/github/license/quantrancse/nettruyen-downloader?style=flat-square +[python-shield]: https://img.shields.io/badge/python-3.9.6-brightgreen?style=flat-square +[license-shield]: https://img.shields.io/github/license/quantrancse/hako2epub?style=flat-square [license-url]: https://github.com/quantrancse/hako2epub/blob/master/LICENSE diff --git a/hako2epub.py b/hako2epub.py index 5004893..7447de4 100644 --- a/hako2epub.py +++ b/hako2epub.py @@ -2,20 +2,62 @@ import json import re from io import BytesIO +from multiprocessing.dummy import Pool as ThreadPool from os import mkdir -from os.path import isdir, isfile +from os.path import isdir, isfile, join +import questionary import requests +import tqdm from bs4 import BeautifulSoup from ebooklib import epub from PIL import Image +LINE_SIZE = 80 +THREAD_NUM = 8 HEADERS = { 'user-agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36')} +tool_version = '2.0.0' bs4_html_parser = 'html.parser' +def print_format(name='', info='', info_style='bold fg:orange', prefix='! '): + questionary.print(prefix, style='bold fg:gray', end='') + questionary.print(name, style='bold fg:white', end='') + questionary.print(info, style=info_style) + + +def check_for_tool_updates(): + try: + release_api = 'https://api.github.com/repos/quantrancse/hako2epub/releases/latest' + response = requests.get( + release_api, headers=HEADERS, timeout=10).json() + latest_release = response['tag_name'][1:] + if tool_version != latest_release: + print_format('Current tool version: ', + tool_version, info_style='bold fg:red') + print_format('Latest tool version: ', latest_release, + info_style='bold fg:green') + print_format('Please upgrade the tool at: ', + 'https://github.com/quantrancse/hako2epub', info_style='bold fg:cyan') + print('-' * LINE_SIZE) + except Exception: + print('Something was wrong. Can not get the tool latest update!') + + +class pcolors: + HEADER = '\033[95m' + OKORANGE = '\033[93m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + class Utils(): def re_url(self, ln_url, url): @@ -29,13 +71,22 @@ def re_url(self, ln_url, url): def format_text(self, text): return text.strip().replace('\n', '') + def format_name(self, name): + special_char = ['?', '!', '.', ':', '\\', '/', '<', '>', '|', '*'] + for char in special_char: + name = name.replace(char, '') + name = name.replace(' ', '-') + if len(name) > 100: + name = name[:100] + return name + def get_image(self, image_url): if 'imgur.com' in image_url and '.' not in image_url[-5:]: image_url += '.jpg' try: image = Image.open(requests.get( image_url, headers=HEADERS, stream=True, timeout=10).raw).convert('RGB') - except BaseException as e: # NOSONAR + except Exception: print("Can not get image: " + image_url) return image @@ -49,52 +100,90 @@ def check_update(self, ln_url='all', mode=''): try: if isfile(self.ln_info_json_file): - with open(self.ln_info_json_file, 'r', encoding='utf-8') as read_file: - save_file = json.load(read_file) + with open(self.ln_info_json_file, 'r', encoding='utf-8') as readfile: + save_file = json.load(readfile) for old_ln in save_file.get('ln_list'): - if ln_url == 'all' or ln_url == old_ln.get('ln_url'): - self.check_update_ln(old_ln, mode) + if ln_url == 'all': + self.check_update_ln(old_ln) + elif ln_url == old_ln.get('ln_url'): + self.check_update_ln(old_ln, 'updatevol') else: print('Can not find ln_info.json file!') - except BaseException as e: # NOSONAR + except Exception: print('Error: Can not process ln_info.json!') - raise e + print('--------------------') - def check_update_ln(self, old_ln, mode): - print('Checking update: ' + old_ln.get('ln_name')) + def check_update_ln(self, old_ln, mode=''): + print_format('Checking update: ', old_ln.get('ln_name')) old_ln_url = old_ln.get('ln_url') try: request = requests.get(old_ln_url, headers=HEADERS, timeout=10) soup = BeautifulSoup(request.text, bs4_html_parser) new_ln = LNInfo() - new_ln = new_ln.get_ln_info(old_ln_url, soup, 'default') + new_ln = new_ln.get_ln_info(old_ln_url, soup, 'update') if mode == 'updatevol': - self.updatevol_ln(old_ln, new_ln) + self.update_volume_ln(old_ln, new_ln) else: self.update_ln(old_ln, new_ln) + print( + f'Update {pcolors.OKORANGE}{old_ln.get("ln_name")}{pcolors.ENDC}: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + print('--------------------') + except Exception: + print( + f'Update {old_ln.get("ln_name")}: [{pcolors.FAIL} FAIL {pcolors.ENDC}]') + print('Error: Can not check light novel info!') + print('--------------------') + + def update_volume_ln(self, old_ln, new_ln): + old_volume_list = [volume_item.get('vol_name') + for volume_item in old_ln.get('vol_list')] + new_volume_list = [volume_item.name + for volume_item in new_ln.volume_list] + + existed_prefix = 'Existed: ' + new_prefix = 'New: ' + + volume_titles = [ + existed_prefix + volume_name for volume_name in old_volume_list] + all_existed_volumes = 'All existed volumes (%s volumes)' % str( + len(old_volume_list)) + + all_volumes = '' + + if old_volume_list != new_volume_list: + new_volume_titles = [ + new_prefix + volume_name for volume_name in new_volume_list if volume_name not in old_volume_list] + volume_titles += new_volume_titles + all_volumes = 'All volumes (%s volumes)' % str(len(volume_titles)) + volume_titles.insert(0, all_existed_volumes) + volume_titles.insert( + 0, questionary.Choice(all_volumes, checked=True)) + else: + volume_titles.insert(0, questionary.Choice( + all_existed_volumes, checked=True)) - print('Done...\n') - except BaseException as e: # NOSONAR - print('Error: Can not check ln info!') - - def updatevol_ln(self, old_ln, new_ln): - volume_titles = [vol_item.get('vol_name') - for vol_item in old_ln.get('vol_list')] - - print('Select a volume to update:\n') - for i, volume_title in enumerate(volume_titles): - print(str(i) + ': ' + volume_title + '\n') + selected_volumes = questionary.checkbox( + 'Select volumes to update:', choices=volume_titles).ask() - try: - selected_volume = int(input('Enter volume number: ')) - for volume in new_ln.volume_list: - if volume.name == old_ln.get('vol_list')[selected_volume].get('vol_name'): - self.update_new_chapter(new_ln, volume, old_ln) - except BaseException as e: - print('Invalid input number.') - raise e + if selected_volumes: + if all_volumes in selected_volumes: + self.update_ln(old_ln, new_ln) + elif all_existed_volumes in selected_volumes: + for volume in new_ln.volume_list: + if volume.name in old_volume_list: + self.update_new_chapter(new_ln, volume, old_ln) + else: + new_volumes_name = [ + volume[len(new_prefix):] for volume in selected_volumes if new_prefix in volume] + old_volumes_name = [ + volume[len(existed_prefix):] for volume in selected_volumes if existed_prefix in volume] + for volume in new_ln.volume_list: + if volume.name in old_volumes_name: + self.update_new_chapter(new_ln, volume, old_ln) + elif volume.name in new_volumes_name: + self.update_new_volume(new_ln, volume) def update_ln(self, old_ln, new_ln): old_ln_vol_list = [vol.get('vol_name') @@ -107,28 +196,47 @@ def update_ln(self, old_ln, new_ln): self.update_new_chapter(new_ln, volume, old_ln) def update_new_volume(self, new_ln, volume): + print_format('Updating volume: ', volume.name, + info_style='bold fg:cyan') new_ln.volume_list = [volume] epub_engine = EpubEngine() epub_engine.create_epub(new_ln) + print( + f'Updating volume {pcolors.OKCYAN}{volume.name}{pcolors.ENDC}: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + print('--------------------') def update_new_chapter(self, new_ln, volume, old_ln): - for vol in old_ln.get('vol_list'): - if volume.name == vol.get('vol_name'): - print('Checking volume: ' + volume.name) - volume_chapter_list = list(volume.chapter_list.keys()) - for chapter in volume_chapter_list: - if chapter in vol.get('chapter_list'): + print_format('Checking volume: ', volume.name, + info_style='bold fg:cyan') + for old_volume in old_ln.get('vol_list'): + if volume.name == old_volume.get('vol_name'): + + new_ln_chapter_list = list(volume.chapter_list.keys()) + old_ln_chapter_list = old_volume.get('chapter_list') + volume_chapter_list = new_ln_chapter_list[new_ln_chapter_list.index( + old_ln_chapter_list[0]):] + + for chapter in new_ln_chapter_list: + if chapter in old_ln_chapter_list or chapter not in volume_chapter_list: volume.chapter_list.pop(chapter, None) + if volume.chapter_list: - print('Updating volume: ' + volume.name) + print_format('Updating volume: ', volume.name, + info_style='bold fg:cyan') epub_engine = EpubEngine() epub_engine.update_epub(new_ln, volume) + print( + f'Updating {pcolors.OKCYAN}{volume.name}{pcolors.ENDC}: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + + print( + f'Checking volume {pcolors.OKCYAN}{volume.name}{pcolors.ENDC}: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + print('--------------------') def update_json(self, ln): # NOSONAR try: - print('Updating ln_info.json...') - with open(self.ln_info_json_file, 'r', encoding='utf-8') as read_file: - save_file = json.load(read_file) + print('Updating ln_info.json...', end='\r') + with open(self.ln_info_json_file, 'r', encoding='utf-8') as readfile: + save_file = json.load(readfile) ln_url_list = [ln_item.get('ln_url') for ln_item in save_file.get('ln_list')] @@ -174,13 +282,20 @@ def update_json(self, ln): # NOSONAR with open(self.ln_info_json_file, 'w', encoding='utf-8') as outfile: json.dump(save_file, outfile, indent=4, ensure_ascii=False) - except BaseException as e: + + print( + f'Updating ln_info.json: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + print('--------------------') + + except Exception: + print( + f'Updating ln_info.json: [{pcolors.FAIL} FAIL {pcolors.ENDC}]') print('Error: Can not update ln_info.json!') - raise e + print('--------------------') def create_json(self, ln): try: - print('Creating ln_info.json...') + print('Creating ln_info.json...', end='\r') ln_list = {} current_ln = {} @@ -202,9 +317,15 @@ def create_json(self, ln): with open(self.ln_info_json_file, 'w', encoding='utf-8') as outfile: json.dump(ln_list, outfile, indent=4, ensure_ascii=False) - except BaseException as e: + + print( + f'Creating ln_info.json: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + print('--------------------') + except Exception: + print( + f'Creating ln_info.json: [{pcolors.FAIL} FAIL {pcolors.ENDC}]') print('Error: Can not create ln_info.json!') - raise e + print('--------------------') class EpubEngine(): @@ -212,21 +333,22 @@ class EpubEngine(): def __init__(self): self.ln_info_json_file = 'ln_info.json' - def format_name(self, name): - return name.replace(' ', '-').replace('?', '').replace('!', '') - def make_cover_image(self): try: - print('Making cover image...') + print('Making cover image...', end='\r') img = Utils().get_image(self.volume.cover_img) b = BytesIO() img.save(b, 'jpeg') b_img = b.getvalue() cover_image = epub.EpubItem( file_name='cover_image.jpeg', media_type='image/jpeg', content=b_img) + print( + f'Making cover image: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') return cover_image - except BaseException as e: # NOSONAR + except Exception: + print(f'Making cover image: [{pcolors.FAIL} FAIL {pcolors.ENDC}]') print('Error: Can not get cover image!') + print('--------------------') return None def set_metadata(self, title, author, lang='vi'): @@ -235,7 +357,7 @@ def set_metadata(self, title, author, lang='vi'): self.book.add_author(author) def make_intro_page(self): - print('Making intro page...') + print('Making intro page...', end='\r') source_url = self.volume.url github_url = 'https://github.com/quantrancse/hako2epub' @@ -274,6 +396,8 @@ def make_intro_page(self): Generated by hako2epub ''' % (source_url, source_url, github_url) + print(f'Making intro page: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + return epub.EpubHtml( uid='intro', file_name='intro.xhtml', @@ -282,37 +406,62 @@ def make_intro_page(self): ) def make_chapter(self, i=0): + chapter_urls_index = [] + for i, chapter in enumerate(self.volume.chapter_list.keys(), i): + chapter_urls_index.append((i, self.volume.chapter_list[chapter])) + + pool = ThreadPool(THREAD_NUM) + contents = [] try: - print('Making chapter contents...') - for i, chapter in enumerate(self.volume.chapter_list.keys(), i): - chapter_url = self.volume.chapter_list[chapter] - request = requests.get( - chapter_url, headers=HEADERS, timeout=10) - soup = BeautifulSoup(request.text, bs4_html_parser) + contents = list(tqdm.tqdm(pool.imap_unordered(self.make_chapter_content, chapter_urls_index), total=len( + chapter_urls_index), desc='Making chapter contents: ')) + contents.sort(key=lambda x: x[0]) + contents = [content[1] for content in contents] + except Exception: + pass + pool.close() + pool.join() + + for content in contents: + self.book.add_item(content) + self.book.spine.append(content) + self.book.toc.append(content) + + def make_chapter_content(self, chapter_list): + try: + i = chapter_list[0] + chapter_url = chapter_list[1] + + request = requests.get( + chapter_url, headers=HEADERS, timeout=10) + soup = BeautifulSoup(request.text, bs4_html_parser) + + xhtml_file = 'chap_%s.xhtml' % str(i + 1) + + chapter_title = soup.find('div', 'title-top').find('h4').text + chapter_content = '''

%s

''' % ( + chapter_title) + chapter_content += self.make_image( + soup.find('div', id='chapter-content'), i + 1) + + note_list = self.get_chapter_content_note(soup) + chapter_content = self.replace_chapter_content_note( + chapter_content, note_list) + + content = epub.EpubHtml( + uid=str(i + 1), + title=chapter_title, + file_name=xhtml_file, + content=chapter_content + ) + + return (i, content) - xhtml_file = 'chap_%s.xhtml' % str(i + 1) - - chapter_title = soup.find('div', 'title-top').find('h4').text - chapter_content = '''

%s

''' % ( - chapter_title) - chapter_content += self.make_image( - soup.find('div', id='chapter-content'), i + 1) - - note_list = self.get_chapter_content_note(soup) - chapter_content = self.replace_chapter_content_note( - chapter_content, note_list) - - content = epub.EpubHtml( - uid=str(i + 1), - title=chapter_title, - file_name=xhtml_file, - content=chapter_content - ) - self.book.add_item(content) - self.book.spine.append(content) - self.book.toc.append(content) - except BaseException as e: # NOSONAR - print('Error: Can not get chapter content!') + except Exception: + print( + f'Making chapter contents: [{pcolors.FAIL} FAIL {pcolors.ENDC}]') + print('Error: Can not get chapter contents! ' + chapter_url) + print('--------------------') def make_image(self, chapter_content, chapter_id): img_tags = chapter_content.findAll('img') @@ -338,8 +487,9 @@ def make_image(self, chapter_content, chapter_id): img_old_path = 'src="' + img_url img_new_path = 'style="display: block;margin-left: auto;margin-right: auto;" src="' + img_path content = content.replace(img_old_path, img_new_path) - except BaseException as e: # NOSONAR + except Exception: print('Error: Can not get chapter images! ' + img_url) + print('--------------------') return content def get_chapter_content_note(self, soup): @@ -365,8 +515,9 @@ def bind_epub_book(self): try: self.book.set_cover('cover.jpeg', requests.get( self.volume.cover_img, headers=HEADERS, stream=True, timeout=10).content) - except BaseException as e: # NOSONAR + except Exception: print('Error: Can not set cover image!') + print('--------------------') self.book.spine = ['cover', intro_page, 'nav'] @@ -374,42 +525,47 @@ def bind_epub_book(self): self.book.add_item(epub.EpubNcx()) self.book.add_item(epub.EpubNav()) - epub_name = self.format_name( - self.volume.name + '-' + self.ln.name + '.epub') + epub_name = Utils().format_name(self.volume.name + '-' + self.ln.name) + '.epub' self.set_metadata(epub_name, self.ln.author) - epub_folder = self.format_name(self.ln.name) + epub_folder = Utils().format_name(self.ln.name) if not isdir(epub_folder): mkdir(epub_folder) - epub_path = epub_folder + '/' + epub_name + epub_path = join(epub_folder, epub_name) + try: epub.write_epub(epub_path, self.book, {}) - except BaseException as e: # NOSONAR + except Exception: print('Error: Can not write epub file!') + print('--------------------') def create_epub(self, ln): self.ln = ln for volume in ln.volume_list: - print('Processing volume: ' + volume.name) + print_format('Processing volume: ', volume.name, + info_style='bold fg:cyan') self.book = epub.EpubBook() self.volume = volume self.bind_epub_book() - print('Done volume: ' + volume.name + '\n') + print( + f'Processing {pcolors.OKCYAN}{volume.name}{pcolors.ENDC}: [{pcolors.OKGREEN} DONE {pcolors.ENDC}]') + print('--------------------') self.save_json(ln) def update_epub(self, ln, volume): - epub_name = self.format_name(volume.name + '-' + ln.name + '.epub') - epub_folder = self.format_name(ln.name) + epub_name = Utils().format_name(volume.name + '-' + ln.name) + '.epub' + epub_folder = Utils().format_name(ln.name) epub_path = epub_folder + '/' + epub_name try: self.book = epub.read_epub(epub_path) - except BaseException as e: # NOSONAR + except Exception: print('Error: Can not read epub file!') + print('--------------------') chap_name_list = [chap.file_name for chap in self.book.get_items( - )if chap.file_name.startswith('chap')] + ) if chap.file_name.startswith('chap')] self.ln = ln self.volume = volume @@ -423,8 +579,9 @@ def update_epub(self, ln, volume): try: epub.write_epub(epub_path, self.book, {}) - except BaseException as e: # NOSONAR + except Exception: print('Error: Can not write epub file!') + print('--------------------') self.save_json(ln) @@ -487,8 +644,7 @@ def get_ln(self, ln_url, soup, mode): self.get_ln_info(ln_url, soup, mode) self.create_ln_epub() - def get_ln_info(self, ln_url, soup, mode): - print('Getting LN Info...\n') + def get_ln_info(self, ln_url, soup, mode=''): self.set_ln_url(ln_url) self.set_ln_name(soup) self.set_ln_series_info(soup) @@ -526,61 +682,118 @@ def set_ln_summary(self, soup): def set_ln_fact_item(self, soup): self.fact_item = str(soup.find('div', 'fact-item')) - def set_ln_volume(self, soup, mode): + def set_ln_volume(self, soup, mode=''): get_volume_section = soup.findAll('section', 'volume-list') self.num_vol = len(get_volume_section) + volume_titles = [] + for volume_section in get_volume_section: + volume_titles.append(Utils().format_text( + volume_section.find('span', 'sect-title').text)) + volume_urls = [] for volume_section in get_volume_section: volume_url = Utils().re_url(self.url, volume_section.find( 'div', 'volume-cover').find('a').get('href')) volume_urls.append(volume_url) - try: - if mode == 'default': - for volume_url in volume_urls: - request = requests.get( - volume_url, headers=HEADERS, timeout=10) - soup = BeautifulSoup(request.text, bs4_html_parser) - - self.volume_list.append(Volume(volume_url, soup)) - - elif mode == 'volume': - get_volume_section = soup.findAll('section', 'volume-list') - volume_titles = [] - - for volume_section in get_volume_section: - volume_titles.append(Utils().format_text(volume_section.find( - 'span', 'sect-title').text).replace(':', '')) - - print('Select a volume:\n') - for i, volume_title in enumerate(volume_titles): - print(str(i) + ': ' + volume_title + '\n') - - try: - selected_volume = int(input('Enter volume number: ')) - print('\n') - except BaseException as e: - print('Invalid volume number.') - raise e - - if selected_volume in range(len(volume_urls)): - request = requests.get( - volume_urls[selected_volume], headers=HEADERS, timeout=10) - soup = BeautifulSoup(request.text, bs4_html_parser) - - self.volume_list.append( - Volume(volume_urls[selected_volume], soup)) + volume_info = dict(zip(volume_titles, volume_urls)) - except BaseException as e: # NOSONAR - print('Error: Can not get volume info!') + if mode == 'update': + self.set_ln_volume_list(volume_info.values()) + elif mode == 'chapter': + print_format('Novel: ', self.name) + selected_volume = questionary.select( + 'Select volumes to download:', choices=volume_titles, use_shortcuts=True).ask() + self.set_ln_volume_list([volume_info[selected_volume]]) + self.set_ln_volume_chapter_list() + else: + print_format('Novel: ', self.name) + all_volumes = 'All volumes (%s volumes)' % str(self.num_vol) + volume_titles.insert( + 0, questionary.Choice(all_volumes, checked=True)) + + selected_volumes = questionary.checkbox( + 'Select volumes to download:', choices=volume_titles).ask() + + if all_volumes in selected_volumes: + self.set_ln_volume_list(volume_info.values()) + elif selected_volumes: + self.set_ln_volume_list( + [volume_info[volume_title] for volume_title in selected_volumes]) + + def set_ln_volume_chapter_list(self): + chapter_name_list = list(self.volume_list[0].chapter_list.keys()) + from_chapter = questionary.text('Enter from chapter name:').ask() + end_chapter = questionary.text('Enter to chapter name:').ask() + + if from_chapter not in chapter_name_list or end_chapter not in chapter_name_list: + print('Invalid input chapter!') + self.volume_list = [] + else: + from_chapter_index = chapter_name_list.index(from_chapter) + end_chapter_index = chapter_name_list.index(end_chapter) + if end_chapter_index < from_chapter_index: + from_chapter_index, end_chapter_index = end_chapter_index, from_chapter_index + + selected_chapters = chapter_name_list[from_chapter_index:end_chapter_index+1] + self.volume_list[0].chapter_list = { + chapter_name: self.volume_list[0].chapter_list[chapter_name] for chapter_name in selected_chapters} + + def set_ln_volume_list(self, volume_urls): + for volume_url in volume_urls: + try: + request = requests.get(volume_url, headers=HEADERS, timeout=10) + soup = BeautifulSoup(request.text, bs4_html_parser) + self.volume_list.append(Volume(volume_url, soup)) + except Exception: + print('Error: Can not get volume info!' + volume_url) + print('--------------------') class Engine(): def __init__(self): super().__init__() self.current_ln = LNInfo() + self.ln_info_json_file = 'ln_info.json' + + def update_current_json(self): # NOSONAR + try: + if isfile(self.ln_info_json_file): + with open(self.ln_info_json_file, 'r', encoding='utf-8') as readfile: + current_json = json.load(readfile) + + new_json = current_json + + for old_ln in current_json.get('ln_list'): + ln_name = old_ln.get('ln_name') + epub_folder = Utils().format_name(ln_name) + if not isdir(epub_folder): + new_json['ln_list'] = [ln for ln in current_json.get( + 'ln_list') if ln.get('ln_name') != ln_name] + else: + new_vol_list = old_ln.get('vol_list') + for current_vol in old_ln.get('vol_list'): + current_vol_name = current_vol.get('vol_name') + epub_name = Utils().format_name(current_vol_name + '-' + ln_name) + '.epub' + epub_path = join(epub_folder, epub_name) + if not isfile(epub_path): + new_vol_list = [vol for vol in new_vol_list if vol.get( + 'vol_name') != current_vol_name] + for ln in new_json['ln_list']: + if old_ln.get('ln_url') == ln.get('ln_url'): + ln['vol_list'] = new_vol_list + + with open(self.ln_info_json_file, 'w', encoding='utf-8') as outfile: + json.dump(new_json, outfile, indent=4, ensure_ascii=False) + + readfile.close() + outfile.close() + + except Exception: + print('Error: Can not process ln_info.json!') + print('--------------------') def check_valid_url(self, url): if not any(substr in url for substr in ['ln.hako.re/truyen/', 'docln.net/truyen/']): @@ -590,11 +803,10 @@ def check_valid_url(self, url): return True def start(self, ln_url, mode): + self.update_current_json() if ln_url and self.check_valid_url(ln_url): if mode == 'update': UpdateLN().check_update(ln_url) - elif mode == 'updatevol': - UpdateLN().check_update(ln_url, 'updatevol') else: try: request = requests.get(ln_url, headers=HEADERS, timeout=10) @@ -603,32 +815,35 @@ def start(self, ln_url, mode): print('Invalid url. Please try again.') else: self.current_ln.get_ln(ln_url, soup, mode) - except BaseException as e: - print('Error: Can not check url!') - raise e - else: + except Exception: + print('Error: Can not check light novel url!') + print('--------------------') + elif mode == 'update_all': UpdateLN().check_update() if __name__ == '__main__': - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + description='A tool to download light novels on https://ln.hako.re as the epub file format for offline reading.') + parser.add_argument('-v', '--version', action='version', version='hako2epub v%s' % tool_version) parser.add_argument('ln_url', type=str, nargs='?', default='', - help='url to the ln homepage') - parser.add_argument('-v', '--volume', metavar='ln_url', type=str, - help='download single volume') + help='url to the light novel page') + parser.add_argument('-c', '--chapter', type=str, metavar='ln_url', + help='download specific chapters of a light novel') parser.add_argument('-u', '--update', type=str, metavar='ln_url', nargs='?', default=argparse.SUPPRESS, - help='update all/single ln') - parser.add_argument('-uv', '--updatevol', type=str, metavar='ln_url', - help='update single volume') + help='update all/single light novel') args = parser.parse_args() engine = Engine() + + check_for_tool_updates() - if args.volume: - engine.start(args.volume, 'volume') - elif args.updatevol: - engine.start(args.updatevol, 'updatevol') + if args.chapter: + engine.start(args.chapter, 'chapter') elif 'update' in args: - engine.start(args.update, 'update') + if args.update: + engine.start(args.update, 'update') + else: + engine.start(None, 'update_all') else: engine.start(args.ln_url, 'default') diff --git a/images/demo.png b/images/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..8d782f3243a02d50f2a6a8de4f5ade6e0049e6f1 GIT binary patch literal 22250 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 literal 0 HcmV?d00001 diff --git a/images/exec_demo.png b/images/exec_demo.png deleted file mode 100644 index a7f0ac0938b52795455387c58fbe8e6b1a5e3333..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21313 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