def main(): config = Config().app_config() if Config().app_config_exists() else None cli = CLI() # Set up logging log = logging.getLogger('manga-dl') log_formatter = logging.Formatter("[%(asctime)s] %(levelname)s.%(name)s: %(message)s") # Set up our console logger if config and config.getboolean('Common', 'debug'): log.setLevel(logging.DEBUG) else: log.setLevel(logging.CRITICAL) console_logger = logging.StreamHandler() console_logger.setLevel(logging.DEBUG) console_logger.setFormatter(log_formatter) log.addHandler(console_logger) # If this is our first time running the application, run setup first try: if not config: cli.setup() cli.prompt() while True: print() cli.prompt(False) except KeyboardInterrupt: print('\nExiting\n') cli.exit()
def __init__(self): """ Initialize a new CLI instance """ self.config = Config() self.scraper_manager = ScraperManager() self.log = logging.getLogger('manga-dl.cli') if path.isfile(self.config.app_config_path): self.config = self.config.app_config() self.manga = Manga()
def __init__(self): """ Initialize a new Manga instance """ self.config = Config().app_config() self.log = logging.getLogger('manga-dl.manga') self._site_scrapers = ScraperManager().scrapers self.throttle = self.config.getint('Common', 'throttle', fallback=1) self.progress_widget = [Percentage(), ' ', Bar(), ' Page: ', SimpleProgress(), ' ', AdaptiveETA()] # Define the directory / filename templates self.manga_dir_template = self.config.get('Paths', 'manga_dir') self.series_dir_template = self.config.get('Paths', 'series_dir') self.chapter_dir_template = self.config.get('Paths', 'chapter_dir') self.page_filename_template = self.config.get('Paths', 'page_filename')
def _load(self): """ Attempt to load the requested Manga title """ manga_paths = Manga.natural_sort(os.listdir(self.manga_path)) # Loop through the manga directories and see if we can find a match for path_item in manga_paths: self.path = os.path.join(self.manga_path, path_item) if self.title.lower() == path_item.lower() and os.path.isdir(self.path): self.log.info('Match found: {dir}'.format(dir=path_item)) # Series matched, define the path and begin loading self.path = os.path.join(self.manga_path, path_item) # Load the series configuration file series_config_path = os.path.join(self.path, '.' + Config().app_config_file) if not os.path.isfile(series_config_path): continue self._series_config = ConfigParser() self._series_config.read(series_config_path) # Compile the regex patterns self.series_pattern = re.compile(self._series_config.get('Patterns', 'series_pattern', raw=True)) self.chapter_pattern = re.compile(self._series_config.get('Patterns', 'chapter_pattern', raw=True)) self.page_pattern = re.compile(self._series_config.get('Patterns', 'page_pattern', raw=True)) # Break on match break else: # Title was not found, abort loading raise MangaNotSavedError('Manga title "{manga}" could not be loaded from the filesystem' .format(manga=self.title)) # Successful match if we're still here, load all available chapters self._load_chapters()
def create_series(self, series): """ Create a new Manga series placeholder on the filesystem :param series: The meta instance of the Manga series :type series: SeriesMeta """ # Set up the manga directory self.log.debug('Formatting the manga directory path') manga_path = self.manga_dir_template self.log.debug('Manga path set: {path}'.format(path=manga_path)) # Set up the series directory self.log.debug('Formatting the series directory path') series_path = os.path.join(manga_path, self.series_dir_template.format(series=series.title)) self.log.debug('Series path set: {path}'.format(path=series_path)) if not os.path.isdir(series_path): self.log.debug('Creating series directory') os.makedirs(series_path, 0o755) # Escape our dir templates for regex parsing series_re_template = self.series_dir_template chapter_re_template = self.chapter_dir_template.replace('[', r'\[').replace(']', r'\]') page_re_template = self.page_filename_template # Format the pattern templates series_pattern = '^' + series_re_template.format(series=r'(?P<series>\.+)') + '$' chapter_pattern = '^' + chapter_re_template.format(chapter=r'(?P<chapter>\d+(\.\d)?)', title=r'(?P<title>.+)') + '$' page_pattern = '^' + page_re_template.format(page=r'(?P<page>\d+(\.\d)?)', ext=r'\w{3,4}') + '$' # Set up the series configuration config = ConfigParser() config.add_section('Patterns') config.set('Patterns', 'series_pattern', series_pattern) config.set('Patterns', 'chapter_pattern', chapter_pattern) config.set('Patterns', 'page_pattern', page_pattern) config.add_section('Common') config.set('Common', 'version', '0.1.0') # Write to and close the configuration file config_path = os.path.join(series_path, '.' + Config().app_config_file) # This series has already been created and configured if os.path.isfile(config_path): raise MangaAlreadyExistsError config_file = open(config_path, 'w') config.write(config_file) config_file.close() # If we're on Windows, make the configuration file hidden if platform.system() == 'Windows': p = os.popen('attrib +h ' + config_path) t = p.read() p.close()
def __init__(self, title): """ Initialize a new Manga Meta instance :param title: The title of the Manga series to load :type title: str """ self.log = logging.getLogger('manga-dl.manga-meta') self.title = title.strip() self.config = Config().app_config() self.manga_path = self.config.get('Paths', 'manga_dir') # Series configuration placeholders self._series_config = None self.chapter_pattern = None self.page_pattern = None # Manga metadata placeholders self.path = None self.chapters = OrderedDict() self._load()
class SeriesMeta: """ Manga Metadata """ def __init__(self, title): """ Initialize a new Manga Meta instance :param title: The title of the Manga series to load :type title: str """ self.log = logging.getLogger('manga-dl.manga-meta') self.title = title.strip() self.config = Config().app_config() self.manga_path = self.config.get('Paths', 'manga_dir') # Series configuration placeholders self._series_config = None self.chapter_pattern = None self.page_pattern = None # Manga metadata placeholders self.path = None self.chapters = OrderedDict() self._load() def _load(self): """ Attempt to load the requested Manga title """ manga_paths = Manga.natural_sort(os.listdir(self.manga_path)) # Loop through the manga directories and see if we can find a match for path_item in manga_paths: self.path = os.path.join(self.manga_path, path_item) if self.title.lower() == path_item.lower() and os.path.isdir(self.path): self.log.info('Match found: {dir}'.format(dir=path_item)) # Series matched, define the path and begin loading self.path = os.path.join(self.manga_path, path_item) # Load the series configuration file series_config_path = os.path.join(self.path, '.' + Config().app_config_file) if not os.path.isfile(series_config_path): continue self._series_config = ConfigParser() self._series_config.read(series_config_path) # Compile the regex patterns self.series_pattern = re.compile(self._series_config.get('Patterns', 'series_pattern', raw=True)) self.chapter_pattern = re.compile(self._series_config.get('Patterns', 'chapter_pattern', raw=True)) self.page_pattern = re.compile(self._series_config.get('Patterns', 'page_pattern', raw=True)) # Break on match break else: # Title was not found, abort loading raise MangaNotSavedError('Manga title "{manga}" could not be loaded from the filesystem' .format(manga=self.title)) # Successful match if we're still here, load all available chapters self._load_chapters() def _load_chapters(self): """ Load all available chapters for the volume """ series_path = Manga.natural_sort(os.listdir(self.path)) for path_item in series_path: match = self.chapter_pattern.match(path_item) if match: chapter_path = os.path.join(self.path, path_item) chapter = match.group('chapter') # chapter number title = match.group('title') # chapter title self.chapters[chapter] = ChapterMeta(chapter_path, chapter, title, self)
class Manga: """ Manga downloading and updating services """ def __init__(self): """ Initialize a new Manga instance """ self.config = Config().app_config() self.log = logging.getLogger('manga-dl.manga') self._site_scrapers = ScraperManager().scrapers self.throttle = self.config.getint('Common', 'throttle', fallback=1) self.progress_widget = [Percentage(), ' ', Bar(), ' Page: ', SimpleProgress(), ' ', AdaptiveETA()] # Define the directory / filename templates self.manga_dir_template = self.config.get('Paths', 'manga_dir') self.series_dir_template = self.config.get('Paths', 'series_dir') self.chapter_dir_template = self.config.get('Paths', 'chapter_dir') self.page_filename_template = self.config.get('Paths', 'page_filename') @staticmethod def natural_sort(l): """ Natural / human list sorting http://stackoverflow.com/a/4836734 :param l: List to sort :type l: list :return: A naturally sorted list :rtype : list """ convert = lambda text: int(text) if text.isdigit() else text.lower() alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] return sorted(l, key=alphanum_key) def search(self, title): """ Search for a given Manga title :param title: The name of the Manga series :type title: str :return: Ordered dictionary of mangopi metasite chapter instances :rtype : MetaChapter """ for name, site_class in self._site_scrapers.items(): self.log.info('Assigning site: ' + name) self.log.info('Searching for series: {title}'.format(title=title)) site = site_class() try: site.series = title except NoSearchResultsError: continue break else: raise NoSearchResultsError return site.series def create_series(self, series): """ Create a new Manga series placeholder on the filesystem :param series: The meta instance of the Manga series :type series: SeriesMeta """ # Set up the manga directory self.log.debug('Formatting the manga directory path') manga_path = self.manga_dir_template self.log.debug('Manga path set: {path}'.format(path=manga_path)) # Set up the series directory self.log.debug('Formatting the series directory path') series_path = os.path.join(manga_path, self.series_dir_template.format(series=series.title)) self.log.debug('Series path set: {path}'.format(path=series_path)) if not os.path.isdir(series_path): self.log.debug('Creating series directory') os.makedirs(series_path, 0o755) # Escape our dir templates for regex parsing series_re_template = self.series_dir_template chapter_re_template = self.chapter_dir_template.replace('[', r'\[').replace(']', r'\]') page_re_template = self.page_filename_template # Format the pattern templates series_pattern = '^' + series_re_template.format(series=r'(?P<series>\.+)') + '$' chapter_pattern = '^' + chapter_re_template.format(chapter=r'(?P<chapter>\d+(\.\d)?)', title=r'(?P<title>.+)') + '$' page_pattern = '^' + page_re_template.format(page=r'(?P<page>\d+(\.\d)?)', ext=r'\w{3,4}') + '$' # Set up the series configuration config = ConfigParser() config.add_section('Patterns') config.set('Patterns', 'series_pattern', series_pattern) config.set('Patterns', 'chapter_pattern', chapter_pattern) config.set('Patterns', 'page_pattern', page_pattern) config.add_section('Common') config.set('Common', 'version', '0.1.0') # Write to and close the configuration file config_path = os.path.join(series_path, '.' + Config().app_config_file) # This series has already been created and configured if os.path.isfile(config_path): raise MangaAlreadyExistsError config_file = open(config_path, 'w') config.write(config_file) config_file.close() # If we're on Windows, make the configuration file hidden if platform.system() == 'Windows': p = os.popen('attrib +h ' + config_path) t = p.read() p.close() def download_chapter(self, chapter, manga, overwriting=True): """ Download all pages in a chapter :param chapter: The chapter to download_chapter :type chapter: MetaSite.MetaChapter :param manga: The local Manga series :type manga: SeriesMeta :param overwriting: Overwrite existing pages :type overwriting: bool """ self.log.info('Downloading chapter {chapter}: {title}'.format(chapter=chapter.chapter, title=chapter.title)) # Output the formatted Chapter title to the console chapter_header = '\nChapter {chapter}: {title}'.format(chapter=chapter.chapter, title=chapter.title) chapter_header = colored.yellow(chapter_header, bold=True) puts(chapter_header) # Assign the page counts pages = chapter.pages page_count = len(pages) self.log.info('{num} pages found'.format(num=page_count)) # Set up the Chapter directory self.log.debug('Formatting chapter directory path') chapter_path = os.path.join(manga.path, self.chapter_dir_template.format(chapter=chapter.chapter, title=chapter.title)) self.log.debug('Chapter path set: {path}'.format(path=chapter_path)) if not os.path.isdir(chapter_path): self.log.debug('Creating chapter directory') os.makedirs(chapter_path, 0o755) # Set up the progress bar progress_bar = ProgressBar(page_count, self.progress_widget) progress_bar.start() for index, page in enumerate(pages.values(), 1): # Set the filename and path page_filename = self.page_filename_template.format(page=page.page, ext='jpg') self.log.debug('Page filename set: {filename}'.format(filename=page_filename)) page_path = os.path.join(chapter_path, page_filename) # If we're not overwriting and the file exists, skip it if not overwriting and os.path.exists(page_path): self.log.info('Skipping existing page ({page})'.format(page=page.page)) progress_bar.update(index) continue # Download and save the page image image = page.image if not image: self.log.warn('Page found but it has no image resource available') raise ImageResourceUnavailableError failures = 0 retry_throttle = 2 while True: try: request.urlretrieve(image.url, page_path) except ContentTooShortError: # If we've already tried this download several times, give up if failures >= 5: self.log.error('Unable to download a page after several attempts were made, giving up') raise # Increase our failure count and throttle, then try again failures += 1 sleep(retry_throttle) retry_throttle *= 2 self.log.warn('Page download failed partway through, waiting a couple seconds then trying again') break self.log.debug('Updating progress page number: {page_no}'.format(page_no=page.page)) progress_bar.update(index) sleep(self.throttle) puts() def update(self, chapter, manga, checking_pages=True): """ Download a chapter only if it doesn't already exist, and replace any missing pages in existing chapters :param chapter: The chapter to update :type chapter: MetaSite.MetaChapter :param manga: The local Manga series being updated :type manga: SeriesMeta """ # If we don't have this chapter yet, download_chapter it if chapter.chapter in manga.chapters and not checking_pages: self.log.info('Skipping existing chapter: ({no}) {title}'.format(no=chapter.chapter, title=chapter.title)) return self.download_chapter(chapter, manga, overwriting=False) def get(self, chapter): """ Retrieve local metadata on a given Manga chapter :param chapter: The chapter to retrieve :type chapter: MetaSite.MetaChapter :return: MetaChapter instance if it exists, otherwise None :rtype : object or None """ pass def all(self): """ Return all locally available Manga saves :return: List of MangaMeta instances :rtype : list of SeriesMeta """ manga_list = [] manga_paths = self.natural_sort(os.listdir(self.config.get('Paths', 'manga_dir'))) for path_item in manga_paths: try: manga_list.append(SeriesMeta(path_item)) except MangaNotSavedError: continue return manga_list
def setup(self, header=True): """ Run setup tasks for MangaDL :param header: Display the setup header prior to user prompts :type header: bool """ self.log.info('Running setup tasks') if header: puts( 'MangaDL appears to be running for the first time, initializing setup' ) # Manga directory manga_dir_default = path.join(path.expanduser('~'), 'Manga') manga_dir = prompt.query( '\nWhere would you like your Manga collection to be saved?', manga_dir_default) manga_dir = manga_dir.strip().rstrip('/') # Create the Manga directory if it doesn't exist if not path.exists(manga_dir): create_manga_dir = prompt.query( 'Directory does not exist, would you like to create it now?', 'Y') if create_manga_dir.lower().strip() in self.YES_RESPONSES: self.log.info('Setting up Manga directory') makedirs(manga_dir) puts('Manga directory created successfully') else: self.log.info( 'User refused to create manga directory, aborting setup') puts('Not creating Manga directory, setup aborted') # Sites while True: self.log.info('Prompting for Manga sites to enable') puts('\nWhich Manga websites would you like to enable?') sites = self.scraper_manager.scrapers sites_map = {} for key, site in enumerate(sites, 1): sites_map[key] = site puts('{key}. {site}'.format(key=key, site=site)) csv = ','.join( str(i) for i in range(1, len(sites) + 1)) # Generate a comma separated range list site_keys = prompt.query( 'Provide a comma separated list, highest priority first', csv) site_keys = site_keys.split(',') try: enabled_sites = [] for site_key in site_keys: site_key = int(site_key.strip()) self.log.info('Appending site: {site}'.format( site=sites_map[site_key])) enabled_sites.append(sites_map[site_key]) except (ValueError, IndexError): self.log.info('User provided invalid sites input') puts( 'Please provide a comma separated list of ID\'s from the above list' ) continue break # Synonyms puts( '\nMangaDL can attempt to search for known alternative names to Manga titles when no results are found' ) synonyms_enabled = prompt.query( 'Would you like to enable this functionality?', 'Y') synonyms_enabled = True if synonyms_enabled.lower().strip( ) in self.YES_RESPONSES else False # Paths series_dir = '{series}' chapter_dir = '[Chapter {chapter}] - {title}' page_filename = 'page-{page}.{ext}' # Development mode debug_mode = prompt.query('\nWould you like to enable debug mode?', 'N') debug_mode = True if debug_mode.lower().strip( ) in self.YES_RESPONSES else False # Define the configuration values config = { 'Paths': { 'manga_dir': manga_dir, 'series_dir': series_dir, 'chapter_dir': chapter_dir, 'page_filename': page_filename }, 'Common': { 'sites': ','.join(enabled_sites), 'synonyms': str(synonyms_enabled), 'debug': debug_mode, 'throttle': 1 } } Config().app_config_create(config) execl(sys.executable, *([sys.executable] + sys.argv))
class CLI: # Boolean responses YES_RESPONSES = ['y', 'yes', 'true'] NO_RESPONSES = ['n', 'no', 'false'] PROMPT_ACTIONS = { '1': 'download', '2': 'update', '3': 'create_pdf', '4': 'list', 's': 'setup', 'e': 'exit' } def __init__(self): """ Initialize a new CLI instance """ self.config = Config() self.scraper_manager = ScraperManager() self.log = logging.getLogger('manga-dl.cli') if path.isfile(self.config.app_config_path): self.config = self.config.app_config() self.manga = Manga() def _list_manga(self): """ List all tracked Manga series' :return: List of SeriesMeta instances :rtype : list of SeriesMeta """ manga_list = self.manga.all() if not manga_list: puts( 'No Manga titles have been downloaded yet, download something first!' ) raise NoMangaSavesError # Print our a list of available Manga saves puts() for key, manga in enumerate(manga_list, 1): puts('{key}. {title}'.format(key=key, title=manga.title)) puts() return manga_list def _manga_prompt(self, query='Which Manga title would you like to use?'): """ Prompt the user to select a saved Manga entry :param query: The prompt query message :type query: str :return: The metadata instance of the selected Manga :rtype : SeriesMeta :raises: NoMangaSavesError """ manga_list = self._list_manga() # Prompt the user for the Manga title to update while True: try: update_key = int(prompt.query(query)) local_manga = manga_list[update_key - 1] except (ValueError, IndexError): self.log.info('User provided invalid update input') puts( 'Invalid entry, please select a Manga entry from the above list' ) continue break return local_manga @staticmethod def print_header(): """ Prints the CLI header """ puts(""" ___ __ /\/\ __ _ _ __ __ _ __ _ / \/ / / \ / _` | '_ \ / _` |/ _` | / /\ / / / /\/\ \ (_| | | | | (_| | (_| |/ /_// /___ \/ \/\__,_|_| |_|\__, |\__,_/___,'\____/ |___/ """) def prompt(self, header=True): """ Prompt for an action :param header: Display the application header before the options :type header: bool :return : An action to perform :rtype : str """ if header: self.print_header() puts('1. Download new series') puts('2. Update existing series') puts('3. Create PDF\'s from existing series') puts('4. List all tracked series\'') puts('--------------------------------') puts('s. Re-run setup') puts('e. Exit\n') self.log.info('Prompting user for an action') action = prompt.query('What would you like to do?').lower() if action not in self.PROMPT_ACTIONS: self.log.info('User provided an invalid action response') puts( 'Invalid selection, please chose from one of the options listed above' ) return self.prompt(False) action = self.PROMPT_ACTIONS[action] action_method = getattr(self, action) action_method() def download(self): """ Download a new Manga title """ title = prompt.query('What is the title of the Manga series?').strip() puts() # Fetch all available chapters try: series = self.manga.search(title) except NoSearchResultsError: puts('No search results returned for {query}'.format( query=colored.blue(title, bold=True))) if prompt.query('Exit?', 'Y').lower().strip() in self.YES_RESPONSES: self.exit() return # Create the series try: self.manga.create_series(series) except MangaAlreadyExistsError: # Series already exists, prompt the user for confirmation to continue puts('This Manga has already been downloaded') continue_prompt = prompt.query( 'Do you still wish to continue and overwrite the series?', 'N') if continue_prompt.lower().strip() not in self.YES_RESPONSES: self.exit() # Print out the number of chapters to be downloaded chapter_count = len(series.chapters) puts('{count} chapters added to queue'.format(count=chapter_count)) # Loop through our chapters and download_chapter them for chapter_no, chapter in series.chapters.items(): manga = SeriesMeta(series.title) try: self.manga.download_chapter(chapter, manga) except ImageResourceUnavailableError: puts( 'A match was found, but no image resources for the pages appear to be available' ) puts( 'This probably means the Manga was licensed and has been removed' ) if prompt.query('Exit?', 'Y').lower().strip() in self.YES_RESPONSES: self.exit() return self.prompt() except AttributeError as e: self.log.warn( 'An exception was raised downloading this chapter', exc_info=e) puts( 'Chapter does not appear to have any readable pages, skipping' ) continue except Exception as e: self.log.error('Uncaught exception thrown', exc_info=e) response = prompt.query( 'An unknown error occurred trying to download this chapter. Continue?', 'Y') if response.lower().strip() in self.YES_RESPONSES: continue puts('Exiting') break def update(self): """ Update an existing Manga title """ try: local_manga = self._manga_prompt( 'Which Manga title would you like to update?') except NoMangaSavesError: return # Run a search query on the selected title try: remote_series = self.manga.search(local_manga.title) except NoSearchResultsError: return puts( 'No search results returned for {query} (the title may have been licensed or otherwise removed)' .format(query=colored.blue(local_manga.title, bold=True))) for remote_chapter in remote_series.chapters.values(): try: self.manga.update(remote_chapter, local_manga) except ImageResourceUnavailableError: puts( 'A match was found, but no image resources for the pages appear to be available' ) puts( 'This probably means the Manga was licensed and has been removed' ) if prompt.query('Exit?', 'Y').lower().strip() in self.YES_RESPONSES: self.exit() return self.prompt() except AttributeError as e: self.log.warn( 'An exception was raised downloading this chapter', exc_info=e) puts( 'Chapter does not appear to have any readable pages, skipping' ) continue except Exception as e: self.log.error('Uncaught exception thrown', exc_info=e) response = prompt.query( 'An unknown error occurred trying to download this chapter. Continue?', 'Y') if response.lower().strip() in self.YES_RESPONSES: continue puts('Exiting') break def create_pdf(self): """ Create PDFs for a Manga series """ try: self.log.debug( 'Prompting the user for a Manga to create a PDF from') manga = self._manga_prompt() except NoMangaSavesError: self.log.info('No Manga\'s available to create PDFs from') return # Prompt for chapter / series PDF creation while True: self.log.debug( 'Prompting the user for Chapter or Series PDF creation') puts( '\nWould you like to create individual PDF\'s for each chapter, or one for the entire series?' ) response = prompt.query('Chapter / Series?', 'CHAPTER').lower().strip() if response in ['c', 'chapter']: pdf_type = 'chapter' elif response in ['s', 'series']: pdf_type = 'series' else: puts( 'Invalid response, please enter in either "CHAPTER" or "SERIES"' ) continue break # Flip the order of the pages, so that the PDF can be read in reverse order reverse = prompt.query( '\nWould you like to add the pages in reverse order? (Manga will be read backwards)', 'N') reverse = True if reverse.lower().strip( ) in self.YES_RESPONSES else False page_paths = [] # If we're just creating one giant series PDF, do that now and return if pdf_type == 'series': self.log.info( 'Retrieving a list of paths to all pages in all chapters') for chapter in manga.chapters.values(): page_paths += [page.path for page in chapter.pages.values()] if reverse: page_paths.reverse() page_count = len(page_paths) chapter_count = len(manga.chapters) self.log.info('{num} pages queued'.format(num=page_count)) # Create the PDF and write it to a file pdf_header = '\nCreating a PDF for the entire {series} series, containing {chapter_count} chapters and ' \ '{page_count} pages' pdf_header = colored.yellow(pdf_header) puts( pdf_header.format(series=manga.title, chapter_count=chapter_count, page_count=page_count)) pdf = img2pdf.convert(page_paths) pdf_path = path.join(manga.path, 'PDF', manga.title + '.pdf') pdf_file = open(pdf_path, 'wb') pdf_file.write(pdf) pdf_file.close() puts('PDF created and saved successfully to {path}'.format( path=pdf_path)) return # Create individual PDFs for each chapter for chapter in manga.chapters.values(): pdf_header = '\nCreating a PDF for Chapter {chapter}: {title}' pdf_header = colored.yellow(pdf_header) puts( pdf_header.format(chapter=chapter.chapter, title=chapter.title)) page_paths = [page.path for page in chapter.pages.values()] if reverse: page_paths.reverse() pdf = img2pdf.convert(page_paths) pdf_filename = 'Chapter {chapter}: {title}'.format( chapter=chapter.chapter, title=chapter.title) pdf_path = path.join(manga.path, 'PDF', pdf_filename + '.pdf') makedirs(path.join(manga.path, 'PDF'), 0o755, True) pdf_file = open(pdf_path, 'wb') pdf_file.write(pdf) pdf_file.close() puts('PDF created and saved successfully to {path}'.format( path=pdf_path)) def list(self): """ List information on all tracked Manga's """ manga_list = self.manga.all() # Counters total_series_count = 0 total_chapter_count = 0 total_page_count = 0 for manga in manga_list: # Manga header manga_header = '\n{title}'.format(title=manga.title) manga_header = colored.yellow(manga_header) puts(manga_header) # Manga metadata manga_subheader = 'Chapters: {chapter_count}, Total pages: {page_count}' chapter_count = len(manga.chapters) page_count = sum( [len(chapter.pages) for chapter in manga.chapters.values()]) manga_subheader = manga_subheader.format( chapter_count=chapter_count, page_count=page_count) puts(manga_subheader) # Update total counters total_series_count += 1 total_chapter_count += chapter_count total_page_count += page_count total_counts = 'Series\': {series}, Chapters: {chapters}, Pages: {pages}' puts(colored.yellow('\nTotals:')) puts( total_counts.format(series=total_series_count, chapters=total_chapter_count, pages=total_page_count)) def setup(self, header=True): """ Run setup tasks for MangaDL :param header: Display the setup header prior to user prompts :type header: bool """ self.log.info('Running setup tasks') if header: puts( 'MangaDL appears to be running for the first time, initializing setup' ) # Manga directory manga_dir_default = path.join(path.expanduser('~'), 'Manga') manga_dir = prompt.query( '\nWhere would you like your Manga collection to be saved?', manga_dir_default) manga_dir = manga_dir.strip().rstrip('/') # Create the Manga directory if it doesn't exist if not path.exists(manga_dir): create_manga_dir = prompt.query( 'Directory does not exist, would you like to create it now?', 'Y') if create_manga_dir.lower().strip() in self.YES_RESPONSES: self.log.info('Setting up Manga directory') makedirs(manga_dir) puts('Manga directory created successfully') else: self.log.info( 'User refused to create manga directory, aborting setup') puts('Not creating Manga directory, setup aborted') # Sites while True: self.log.info('Prompting for Manga sites to enable') puts('\nWhich Manga websites would you like to enable?') sites = self.scraper_manager.scrapers sites_map = {} for key, site in enumerate(sites, 1): sites_map[key] = site puts('{key}. {site}'.format(key=key, site=site)) csv = ','.join( str(i) for i in range(1, len(sites) + 1)) # Generate a comma separated range list site_keys = prompt.query( 'Provide a comma separated list, highest priority first', csv) site_keys = site_keys.split(',') try: enabled_sites = [] for site_key in site_keys: site_key = int(site_key.strip()) self.log.info('Appending site: {site}'.format( site=sites_map[site_key])) enabled_sites.append(sites_map[site_key]) except (ValueError, IndexError): self.log.info('User provided invalid sites input') puts( 'Please provide a comma separated list of ID\'s from the above list' ) continue break # Synonyms puts( '\nMangaDL can attempt to search for known alternative names to Manga titles when no results are found' ) synonyms_enabled = prompt.query( 'Would you like to enable this functionality?', 'Y') synonyms_enabled = True if synonyms_enabled.lower().strip( ) in self.YES_RESPONSES else False # Paths series_dir = '{series}' chapter_dir = '[Chapter {chapter}] - {title}' page_filename = 'page-{page}.{ext}' # Development mode debug_mode = prompt.query('\nWould you like to enable debug mode?', 'N') debug_mode = True if debug_mode.lower().strip( ) in self.YES_RESPONSES else False # Define the configuration values config = { 'Paths': { 'manga_dir': manga_dir, 'series_dir': series_dir, 'chapter_dir': chapter_dir, 'page_filename': page_filename }, 'Common': { 'sites': ','.join(enabled_sites), 'synonyms': str(synonyms_enabled), 'debug': debug_mode, 'throttle': 1 } } Config().app_config_create(config) execl(sys.executable, *([sys.executable] + sys.argv)) @staticmethod def exit(): """ Exit the application """ sys.exit()