def _select_should_userid_and_version_be_saved(self, userid, version):
        """
        Asks the user if the userid and version should be saved in the configuration
        """

        print('')
        Log.info('The user id and version number of Moodle are downloaded' +
                 ' at the beginning of each run of the downloader.' +
                 ' Since this data rarely changes, it can be saved in the' +
                 ' configuration.')

        Log.critical(
            f'Your user id is `{userid}` and the moodle version is `{version}`'
        )

        print('')

        save_userid_and_version = cutie.prompt_yes_or_no(
            Log.special_str(
                'Do you want to store the user id and version number of Moodle in the configuration?'
            ),
            default_is_yes=False,
        )

        if save_userid_and_version:
            Log.warning('Remember to delete the version number from the' +
                        ' configuration once Moodle has been updated' +
                        ' and then run the configurator again!')

            self.config_helper.set_property('userid', userid)
            self.config_helper.set_property('version', version)

        self.section_seperator()
예제 #2
0
    def _select_should_download_linked_files(self):
        """
        Asks the user if linked files should be downloaded
        """
        download_linked_files = self.config_helper.get_download_linked_files()

        self.section_seperator()
        Log.info('In Moodle courses the teacher can also link to external' +
                 ' files. This can be audio, video, text or anything else.' +
                 ' In particular, the teacher can link to Youtube videos.')
        Log.debug('To download videos correctly you have to install ffmpeg. ')

        Log.error('These files can increase the download volume considerably.')

        Log.info('If you want to filter the external links by their domain,' +
                 ' you can manually set a whitelist and a blacklist' +
                 ' (https://github.com/C0D3D3V/Moodle-Downloader-2/' +
                 'wiki/Download-(external)-linked-files' +
                 ' for more details).')
        Log.warning(
            'Please note that the size of the external files is determined during the download, so the total size'
            + ' changes during the download.')
        print('')

        download_linked_files = cutie.prompt_yes_or_no(
            Log.special_str(
                'Would you like to download linked files of the courses you have selected?'
            ),
            default_is_yes=download_linked_files,
        )

        self.config_helper.set_property('download_linked_files',
                                        download_linked_files)
예제 #3
0
    def notify_about_failed_downloads(self, failed_downloads: [URLTarget]):
        if len(failed_downloads) > 0:
            print('')
            Log.warning(
                'Error while trying to download files, look at the log for more details. List of failed downloads:'
            )
            print('')

        RESET_SEQ = '\033[0m'
        COLOR_SEQ = '\033[1;%dm'

        BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)

        for url_target in failed_downloads:
            print(
                (COLOR_SEQ % CYAN)
                + url_target.file.content_filename
                + RESET_SEQ
                + (COLOR_SEQ % RED)
                + '\n\t'
                + str(url_target.error)
                + RESET_SEQ
            )

        print('')
예제 #4
0
    def _select_should_load_default_filename_character_map(self):
        """
        Asks the user if the default filename character map should be loaded
        """
        filename_character_map = self.config_helper.get_filename_character_map(
        )

        if os.name != 'nt':
            self.section_seperator()

            Log.info(
                'On Windows many characters are forbidden in filenames and paths, if you want, these characters can be'
                + ' automatically removed from filenames.')

            Log.warning(
                'If you want to view the downloaded files on Windows this is important!'
            )

            print('Current filename character map: {}'.format(
                filename_character_map))
            Log.special(
                'Do you want to load the default filename character map for Windows?'
            )

            choices = [
                'No, leave it as it was.',
                'No, load the default linux filename character map.',
                'Yes, load the default windows filename character map.',
            ]

            print('[Confirm your selection with the Enter key]')
            print('')

            selected_map = cutie.select(options=choices)

            if selected_map == 0:
                return
            elif selected_map == 1:
                self.config_helper.set_default_filename_character_map(False)
            elif selected_map == 2:
                self.config_helper.set_default_filename_character_map(True)
        else:
            if filename_character_map != ConfigHelper.windows_map:

                self.section_seperator()
                Log.warning(
                    'Warning: Your current filename character map does not match the standard Windows'
                    + ' filename character map!')
                print('Current filename character map: {}'.format(
                    filename_character_map))
                load_default_map = cutie.prompt_yes_or_no(
                    Log.special_str(
                        'Do you want to load the default filename character map for Windows?'
                    ),
                    default_is_yes=False,
                )
                if load_default_map:
                    self.config_helper.set_default_filename_character_map(True)
    def _log_failures(self):
        """
        Logs errors if any have occurred.
        """
        print('')
        if len(self.report['failure']) > 0:
            Log.warning(
                'Error while trying to download files, look at the log for more details.'
            )

        for url_target in self.report['failure']:
            Log.error('%s\t%s' %
                      (url_target.file.content_filename, url_target.error))
    def check_and_fetch_cookies(self, privatetoken: str, userid: str) -> bool:
        if os.path.exists(self.cookies_path):
            # test if still logged in.

            if self.test_cookies(self.moodle_test_url):
                return True

            warning_msg = 'Moodle cookie has expired, an attempt is made to generate a new cookie.'
            logging.warning(warning_msg)
            Log.warning('\r' + warning_msg + '\033[K')

        if privatetoken is None:
            error_msg = (
                'Moodle Cookies are not retrieved because no private token is set.'
                +
                ' To set a private token, use the `--new-token` option (if necessary also with `--sso`).'
            )
            logging.warning(error_msg)
            Log.error('\r' + error_msg + '\033[K')
            self.delete_cookie_file()
            return False

        autologin_key = self.fetch_autologin_key(privatetoken)

        if autologin_key is None:
            error_msg = 'Failed to download autologin key!'
            logging.debug(error_msg)
            print('')
            Log.error(error_msg)
            self.delete_cookie_file()
            return False

        print('\rDownloading cookies\033[K', end='')

        post_data = {'key': autologin_key.get('key', ''), 'userid': userid}
        url = autologin_key.get('autologinurl', '')

        cookies_response, cookies_session = self.request_helper.post_URL(
            url, post_data, self.cookies_path)

        moodle_test_url = cookies_response.url

        if self.test_cookies(moodle_test_url):
            return True
        else:
            error_msg = 'Failed to generate cookies!'
            logging.debug(error_msg)
            print('')
            Log.error(error_msg)
            self.delete_cookie_file()
            return False
예제 #7
0
    def _select_should_download_submissions(self):
        """
        Asks the user if submissions should be downloaded
        """
        download_submissions = self.config_helper.get_download_submissions()

        self.section_seperator()
        Log.info('Submissions are files that you or a teacher have uploaded' +
                 ' to your assignments. Moodle does not provide an' +
                 ' interface for downloading information from all' +
                 ' submissions to a course at once.')
        Log.warning(
            'Therefore, it may be slow to monitor changes to submissions.')
        print('')

        download_submissions = cutie.prompt_yes_or_no(
            Log.special_str(
                'Do you want to download submissions of your assignments?'),
            default_is_yes=download_submissions,
        )

        self.config_helper.set_property('download_submissions',
                                        download_submissions)
예제 #8
0
    def filter_courses(
        changes: [Course],
        config_helper: ConfigHelper,
        cookie_handler: CookieHandler = None,
        courses_list: [Course] = None,
    ) -> [Course]:
        """
        Filters the changes course list from courses that
        should not get downloaded
        @param config_helper: ConfigHelper to obtain all the diffrent filter configs
        @param cookie_handler: CookieHandler to check if the cookie is valid
        @param courses_list: A list of all courses that are available online
        @return: filtered changes course list
        """

        download_course_ids = config_helper.get_download_course_ids()
        download_public_course_ids = config_helper.get_download_public_course_ids()
        dont_download_course_ids = config_helper.get_dont_download_course_ids()
        download_submissions = config_helper.get_download_submissions()
        download_descriptions = config_helper.get_download_descriptions()
        download_links_in_descriptions = config_helper.get_download_links_in_descriptions()
        download_databases = config_helper.get_download_databases()
        download_quizzes = config_helper.get_download_quizzes()
        download_lessons = config_helper.get_download_lessons()
        download_workshops = config_helper.get_download_workshops()
        exclude_file_extensions = config_helper.get_exclude_file_extensions()
        download_also_with_cookie = config_helper.get_download_also_with_cookie()
        if cookie_handler is not None:
            download_also_with_cookie = cookie_handler.test_cookies()

        filtered_changes = []

        for course in changes:
            if not ResultsHandler.should_download_course(
                course.id, download_course_ids + download_public_course_ids, dont_download_course_ids
            ):
                # Filter courses that should not be downloaded
                continue

            if courses_list is not None:
                not_online = True
                # Filter courses that are not available online
                for online_course in courses_list:
                    if online_course.id == course.id:
                        not_online = False
                        break
                if not_online:
                    Log.warning(f'The Moodle course with id {course.id} is no longer available online.')
                    logging.warning('The Moodle course with id %d is no longer available online.', course.id)
                    continue

            course_files = []
            for file in course.files:
                # Filter Files based on options
                if (
                    # Filter Assignment Submission Files
                    (download_submissions or (not (file.module_modname.endswith('assign') and file.deleted)))
                    # Filter Description Files (except the forum posts)
                    and (
                        download_descriptions
                        or file.content_type != 'description'
                        or (file.module_modname == 'forum' and file.content_type == 'description' and file.content_filename != 'Forum intro')
                    )
                    # Filter Database Files
                    and (download_databases or file.content_type != 'database_file')
                    # Filter Quiz Files
                    and (download_quizzes or (not (file.module_modname.endswith('quiz') and file.deleted)))
                    # Filter Lesson Files
                    and (download_lessons or (not (file.module_modname.endswith('lesson') and file.deleted)))
                    # Filter Workshops Files
                    and (download_workshops or (not (file.module_modname.endswith('workshop') and file.deleted)))
                    # Filter Files that requiere a Cookie
                    and (download_also_with_cookie or (not file.module_modname.startswith('cookie_mod-')))
                    # Exclude files whose file extension is blacklisted
                    and (not (determine_ext(file.content_filename) in exclude_file_extensions))
                    # Exclude files that are in excluded sections
                    and (ResultsHandler.should_download_section(file.section_id, course.excluded_sections))
                ):
                    course_files.append(file)
            course.files = course_files

            # Filter Description URLs
            course_files = []
            for file in course.files:
                if not file.content_type == 'description-url':
                    course_files.append(file)

                elif download_links_in_descriptions:
                    add_description_url = True
                    for test_file in course.files:
                        if file.content_fileurl == test_file.content_fileurl:
                            if test_file.content_type != 'description-url':
                                # If a URL in a description also exists as a real link in the course,
                                # then ignore this URL
                                add_description_url = False
                                break
                            elif file.module_id > test_file.module_id:
                                # Always use the link from the older description.
                                add_description_url = False
                                break

                    if add_description_url:
                        course_files.append(file)
            course.files = course_files

            if len(course.files) > 0:
                filtered_changes.append(course)

        return filtered_changes
예제 #9
0
    def interactively_acquire_sso_token(self, use_stored_url: bool = False) -> str:
        """
        Walks the user through the receiving of a SSO token for the
        Moodle-System and saves it.
        @return: The Token for Moodle.
        """
        if not use_stored_url:

            moodle_url = input('URL of Moodle:   ')

            moodle_uri = urlparse(moodle_url)

            moodle_domain, moodle_path = self._split_moodle_uri(moodle_uri)

        else:
            moodle_domain = self.config_helper.get_moodle_domain()
            moodle_path = self.config_helper.get_moodle_path()

        use_http = self.config_helper.get_use_http()
        scheme = 'https://'
        if use_http:
            scheme = 'http://'

        atomatic_procedure_warning = (
            'Between version 3.81 and 3.82 a change was added to'
            + ' Moodle so that automatic copying of the SSO token'
            + ' might not work.'
        )
        try:
            version = RequestHelper(
                moodle_domain,
                moodle_path,
                skip_cert_verify=self.skip_cert_verify,
                use_http=use_http,
            ).get_simple_moodle_version()

            if StrictVersion(version) > StrictVersion("3.8.1"):
                Log.warning(atomatic_procedure_warning + '\nYou can still try it, your version is: ' + str(version))

        except ConnectionError:
            Log.warning(
                atomatic_procedure_warning
                + '\nThe version of your Moodle could not be detected, you can still try the automatic procedure.'
            )

        print(' If you want to copy the login-token manual, you will be guided through the manual copy process.')
        do_automatic = cutie.prompt_yes_or_no('Do you want to try to receive the SSO token automatically?')

        print('Please log into Moodle on this computer and then visit the following address in your web browser: ')

        if do_automatic:
            print(
                scheme
                + moodle_domain
                + moodle_path
                + 'admin/tool/mobile/launch.php?service='
                + 'moodle_mobile_app&passport=12345&'
                + 'urlscheme=http%3A%2F%2Flocalhost'
            )
            moodle_token = sso_token_receiver.receive_token()
        else:
            print(
                scheme
                + moodle_domain
                + moodle_path
                + 'admin/tool/mobile/launch.php?service='
                + 'moodle_mobile_app&passport=12345&urlscheme=moodledownloader'
            )

            print(
                'If you open the link in the browser, no web page should'
                + ' load, instead an error will occur. Open the'
                + ' developer console (press F12) and go to the Network Tab,'
                + ' if there is no error, reload the web page.'
            )

            print(
                'Copy the link address of the website that could not be'
                + ' loaded (right click, then click on Copy, then click'
                + ' on copy link address).'
            )

            print(
                'The script expects a URL that looks something like this:'
                + '`moodledownloader://token=$apptoken`.'
                + ' Where $apptoken looks random. In reality it is a Base64'
                + ' encoded hash and the token we need to access moodle.'
            )

            token_address = input('Then insert the address here:   ')

            moodle_token, moodle_privatetoken = sso_token_receiver.extract_token(token_address)
            if moodle_token is None:
                raise ValueError('Invalid URL!')

        # Saves the created token and the successful Moodle parameters.
        self.config_helper.set_property('token', moodle_token)
        if moodle_privatetoken is not None:
            self.config_helper.set_property('privatetoken', moodle_privatetoken)
        self.config_helper.set_property('moodle_domain', moodle_domain)
        self.config_helper.set_property('moodle_path', moodle_path)

        return moodle_token
예제 #10
0
def run_main(
    storage_path,
    verbose=False,
    skip_cert_verify=False,
    ignore_ytdl_errors=False,
    without_downloading_files=False,
    log_responses=False,
):

    log_formatter = logging.Formatter(
        '%(asctime)s  %(levelname)s  {%(module)s}  %(message)s',
        '%Y-%m-%d %H:%M:%S')
    log_file = os.path.join(storage_path, 'MoodleDownloader.log')
    log_handler = RotatingFileHandler(log_file,
                                      mode='a',
                                      maxBytes=1 * 1024 * 1024,
                                      backupCount=2,
                                      encoding='utf-8',
                                      delay=0)

    log_handler.setFormatter(log_formatter)
    if verbose:
        log_handler.setLevel(logging.DEBUG)
    else:
        log_handler.setLevel(logging.INFO)

    app_log = logging.getLogger()
    if verbose:
        app_log.setLevel(logging.DEBUG)
    else:
        app_log.setLevel(logging.INFO)
    app_log.addHandler(log_handler)

    logging.info('--- moodle-dl started ---------------------')
    Log.info('Moodle Downloader starting...')
    if verbose:
        logging.debug('moodle-dl version: %s', __version__)
        logging.debug('python version: %s',
                      ".".join(map(str, sys.version_info[:3])))
        ffmpeg_available = which('ffmpeg') is not None
        logging.debug('Is ffmpeg available: %s', ffmpeg_available)

    if IS_DEBUG:
        logging.info('Debug-Mode detected. Errors will be re-risen.')
        app_log.addHandler(ReRaiseOnError())

    try:
        msg_load_config = 'Loading config...'
        logging.debug(msg_load_config)
        Log.debug(msg_load_config)

        config = ConfigHelper(storage_path)
        config.load()
    except BaseException as e:
        logging.error(
            'Error while trying to load the Configuration! %s Exiting...',
            e,
            extra={'exception': e})
        Log.error('Error while trying to load the Configuration!')
        sys.exit(1)

    r_client = False
    try:
        sentry_dsn = config.get_property('sentry_dsn')
        if sentry_dsn:
            sentry_sdk.init(sentry_dsn)
    except BaseException:
        pass

    mail_service = MailService(config)
    tg_service = TelegramService(config)
    xmpp_service = XmppService(config)
    console_service = ConsoleService(config)

    PathTools.restricted_filenames = config.get_restricted_filenames()

    try:
        if not IS_DEBUG:
            process_lock.lock(storage_path)

        moodle = MoodleService(config, storage_path, skip_cert_verify,
                               log_responses)

        msg_checking_for_changes = 'Checking for changes for the configured Moodle-Account....'
        logging.debug(msg_checking_for_changes)
        Log.debug(msg_checking_for_changes)
        changed_courses = moodle.fetch_state()

        if log_responses:
            msg_responses_logged = (
                "All JSON-responses from Moodle have been written to the responses.log file. Exiting..."
            )
            logging.debug(msg_responses_logged)
            Log.success(msg_responses_logged)
            process_lock.unlock(storage_path)
            return

        msg_start_downloading = 'Start downloading changed files...'
        logging.debug(msg_start_downloading)
        Log.debug(msg_start_downloading)

        if without_downloading_files:
            downloader = FakeDownloadService(changed_courses, moodle,
                                             storage_path)
        else:
            downloader = DownloadService(changed_courses, moodle, storage_path,
                                         skip_cert_verify, ignore_ytdl_errors)
        downloader.run()
        failed_downloads = downloader.get_failed_url_targets()

        changed_courses_to_notify = moodle.recorder.changes_to_notify()

        if len(changed_courses_to_notify) > 0:
            console_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)
            mail_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)
            tg_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)
            xmpp_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)

            moodle.recorder.notified(changed_courses_to_notify)

        else:
            msg_no_changes = 'No changes found for the configured Moodle-Account.'
            logging.info(msg_no_changes)
            Log.warning(msg_no_changes)

        if len(failed_downloads) > 0:
            console_service.notify_about_failed_downloads(failed_downloads)
            mail_service.notify_about_failed_downloads(failed_downloads)
            tg_service.notify_about_failed_downloads(failed_downloads)
            xmpp_service.notify_about_failed_downloads(failed_downloads)

        process_lock.unlock(storage_path)

        logging.debug('All done. Exiting...')
        Log.success('All done. Exiting..')
    except BaseException as e:
        print('\n')
        if not isinstance(e, process_lock.LockError):
            process_lock.unlock(storage_path)

        error_formatted = traceback.format_exc()
        logging.error(error_formatted, extra={'exception': e})

        if r_client:
            sentry_sdk.capture_exception(e)

        if verbose:
            Log.critical('Exception:\n%s' % (error_formatted))

        short_error = str(e)
        if not short_error or short_error.isspace():
            short_error = traceback.format_exc(limit=1)

        console_service.notify_about_error(short_error)
        mail_service.notify_about_error(short_error)
        tg_service.notify_about_error(short_error)
        xmpp_service.notify_about_error(short_error)

        logging.debug('Exception-Handling completed. Exiting...')

        sys.exit(1)
    def interactively_manage_database(self):
        RESET_SEQ = '\033[0m'
        COLOR_SEQ = '\033[1;%dm'

        BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)

        stored_files = self.state_recorder.get_stored_files()

        stored_files = MoodleService.filter_courses(stored_files, self.config_helper)

        if len(stored_files) <= 0:
            return

        course_options = []
        courses = []
        for course in stored_files:
            for course_file in course.files:
                if not os.path.exists(course_file.saved_to):
                    course_options.append(COLOR_SEQ % BLUE + course.fullname + RESET_SEQ)
                    courses.append(course)
                    break

        print(
            'This management tool will navigate you through a menu to'
            + ' selectively remove file entries from the database so'
            + ' that these files can be downloaded again.'
        )

        Log.warning(
            'Only files that are missing locally but stored in the local'
            + ' database are displayed in this tool. If a file is not missing'
            + ' from a course, it will not be listed here at all.  Also, only'
            + ' courses that are selected for download are displayed.'
        )

        Log.critical(
            'For more complicated operations on the database a DB browser for SQLite'
            + ' is advantageous (https://sqlitebrowser.org/).'
        )

        if not courses:
            print('No files are missing locally but stored in the local database. Nothing to do.')
            return

        print('Choose one of the courses:')
        print('[Confirm your selection with the Enter key]')
        print('')
        selected_course_id = cutie.select(options=course_options)

        selected_course = courses[selected_course_id]

        section_options = []
        sections = []

        # Add the option to select all sections
        section_options.append(COLOR_SEQ % MAGENTA + '[All sections]' + RESET_SEQ)
        sections.append(None)  # Add None at index 0 to avoid index shifting

        for course_file in selected_course.files:
            if not os.path.exists(course_file.saved_to) and (course_file.section_name not in sections):
                section_options.append(COLOR_SEQ % MAGENTA + course_file.section_name + RESET_SEQ)
                sections.append(course_file.section_name)

        print('From which sections you want to select files?')
        print('[You can select with the space bar and confirm your selection with the enter key]')
        print('')

        selected_sections_ids = cutie.select_multiple(options=section_options, minimal_count=1)

        selected_sections = []
        for selected_sections_id in selected_sections_ids:
            if selected_sections_id == 0:
                selected_sections = sections[1:]
                break
            elif (selected_sections_id) < len(sections):
                selected_sections.append(sections[selected_sections_id])

        file_options = []
        files = []

        # Add the option to select all files
        file_options.append(COLOR_SEQ % CYAN + '[All files]' + RESET_SEQ)
        files.append(None)  # Add None at index 0 to avoid index shifting

        for course_file in selected_course.files:
            if not os.path.exists(course_file.saved_to) and (course_file.section_name in selected_sections):
                file_options.append(COLOR_SEQ % CYAN + course_file.content_filename + RESET_SEQ)
                files.append(course_file)

        print('Which of the files should be removed form the database, so that they will be redownloaded?')
        print('[You can select with the space bar and confirm your selection with the enter key]')
        print('')
        selected_files = cutie.select_multiple(options=file_options)

        files_to_delete = []
        for file_index in selected_files:
            if file_index == 0:  # If all files is selected
                for file_to_delete in files[1:]:  # Ignore the first element of the array set as None
                    if isinstance(file_to_delete, File):
                        files_to_delete.append(file_to_delete)

                break

            elif file_index < len(files) and isinstance(files[file_index], File):
                files_to_delete.append(files[file_index])

        self.state_recorder.batch_delete_files_from_db(files_to_delete)
예제 #12
0
def run_init(storage_path, use_sso=False, skip_cert_verify=False):
    config = ConfigHelper(storage_path)

    if config.is_present():
        do_override_input = cutie.prompt_yes_or_no(Log.error_str('Do you want to override the existing config?'))

        if not do_override_input:
            sys.exit(0)

    MailService(config).interactively_configure()
    TelegramService(config).interactively_configure()

    do_sentry = cutie.prompt_yes_or_no('Do you want to configure Error Reporting via Sentry?')
    if do_sentry:
        sentry_dsn = input('Please enter your Sentry DSN:   ')
        config.set_property('sentry_dsn', sentry_dsn)

    moodle = MoodleService(config, storage_path, skip_cert_verify)

    if use_sso:
        moodle.interactively_acquire_sso_token()
    else:
        moodle.interactively_acquire_token()

    if os.name != 'nt':
        Log.info(
            'On Windows many characters are forbidden in filenames and paths, if you want, these characters can be'
            + ' automatically removed from filenames.'
        )

        Log.warning('If you want to view the downloaded files on Windows this is important!')

        default_windows_map = cutie.prompt_yes_or_no(
            'Do you want to load the default filename character map for windows?'
        )
        if default_windows_map:
            config.set_default_filename_character_map(True)
        else:
            config.set_default_filename_character_map(False)
    else:
        config.set_default_filename_character_map(True)

    Log.success('Configuration finished and saved!')

    if os.name != 'nt':
        if storage_path == '.':
            Log.info(
                '  To set a cron-job for this program on your Unix-System:\n'
                + '    1. `crontab -e`\n'
                + '    2. Add `*/15 * * * * cd %s && python3 %smain.py`\n'
                % (os.getcwd(), os.path.join(os.path.dirname(os.path.realpath(__file__)), ''))
                + '    3. Save and you\'re done!'
            )
        else:
            Log.info(
                '  To set a cron-job for this program on your Unix-System:\n'
                + '    1. `crontab -e`\n'
                + '    2. Add `*/15 * * * *'
                + ' cd %s && python3 %smain.py --path %s`\n'
                % (os.getcwd(), os.path.join(os.path.dirname(os.path.realpath(__file__)), ''), storage_path)
                + '    3. Save and you\'re done!'
            )

    print('')

    Log.info('You can always do the additional configuration later with the --config option.')

    do_config = cutie.prompt_yes_or_no('Do you want to make additional configurations now?')

    if do_config:
        run_configure(storage_path, skip_cert_verify)

    print('')
    Log.success('All set and ready to go!')
    def interactively_add_all_visible_courses(self):
        """
        Guides the user through the process of adding all visible courses
        to the list of courses to download in the configuration
        """

        token = self.config_helper.get_token()
        moodle_domain = self.config_helper.get_moodle_domain()
        moodle_path = self.config_helper.get_moodle_path()
        use_http = self.config_helper.get_use_http()

        request_helper = RequestHelper(moodle_domain,
                                       moodle_path,
                                       token,
                                       self.skip_cert_verify,
                                       use_http=use_http)
        first_contact_handler = FirstContactHandler(request_helper)

        print('')
        Log.info(
            'It is possible to automatically complete the moodle-dl configuration'
            +
            ' with all the courses you can see on your moodle. These are either'
            + ' courses to which you have the appropriate rights to see the' +
            ' course or the course is visible without enrollment.')

        Log.critical(
            'This process can take several minutes for large Moodels, as is common at'
            + ' large universities. Timeout is set to 20 minutes.')

        print('')

        add_all_visible_courses = cutie.prompt_yes_or_no(
            Log.special_str(
                'Do you want to add all visible courses of your Moodle to the configuration?'
            ),
            default_is_yes=False,
        )

        if not add_all_visible_courses:
            return
        else:
            Log.warning(
                'Please wait for the result, this may take several minutes.' +
                ' In addition to adding the courses to the configuration,' +
                ' it will also create an `all_courses.json` file with all' +
                ' the courses available on your Moodle.')

        courses = []
        all_visible_courses = []
        try:
            userid, version = self.config_helper.get_userid_and_version()
            if userid is None or version is None:
                userid, version = first_contact_handler.fetch_userid_and_version(
                )
            else:
                first_contact_handler.version = version

            courses = first_contact_handler.fetch_courses(userid)
            log_all_courses_to = str(
                Path(self.storage_path) / 'all_courses.json')
            all_visible_courses = first_contact_handler.fetch_all_visible_courses(
                log_all_courses_to)

        except (RequestRejectedError, ValueError, RuntimeError,
                ConnectionError) as error:
            Log.error(
                'Error while communicating with the Moodle System! (%s)' %
                (error))
            sys.exit(1)

        # Filter out courses the user is enroled in
        filtered_all_courses = []
        for visible_course in all_visible_courses:
            add_to_final_list = True
            for course in courses:
                if visible_course.id == course.id:
                    add_to_final_list = False
                    break
            if add_to_final_list:
                filtered_all_courses.append(visible_course)

        # Update Public Courses IDs
        download_public_course_ids = self.config_helper.get_download_public_course_ids(
        )
        # Update Course settings List for all new Courses
        options_of_courses = self.config_helper.get_options_of_courses()
        for course in filtered_all_courses:
            current_course_settings = options_of_courses.get(
                str(course.id), None)

            # create default settings
            if current_course_settings is None:
                current_course_settings = {
                    'original_name': course.fullname,
                    'overwrite_name_with': None,
                    'create_directory_structure': True,
                }

                options_of_courses.update(
                    {str(course.id): current_course_settings})

            if course.id not in download_public_course_ids:
                download_public_course_ids.append(course.id)

        self.config_helper.set_property('options_of_courses',
                                        options_of_courses)
        self.config_helper.set_property('download_public_course_ids',
                                        download_public_course_ids)