Esempio n. 1
0
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()
Esempio n. 2
0
 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()
Esempio n. 3
0
    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')
Esempio n. 4
0
    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()
Esempio n. 5
0
    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()
Esempio n. 6
0
    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()
Esempio n. 7
0
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)
Esempio n. 8
0
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
Esempio n. 9
0
    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))
Esempio n. 10
0
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()