From 9a98977f66dc25c6af0ff99c2d902d3625549ee8 Mon Sep 17 00:00:00 2001 From: CanWePlsRapeTheShitOuttaPluralsight Date: Sat, 4 Apr 2020 09:21:23 -0700 Subject: [PATCH] Add 'pluralsight.py' Rape me senpai - Pluralsight probably --- pluralsight.py | 353 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 pluralsight.py diff --git a/pluralsight.py b/pluralsight.py new file mode 100644 index 0000000..5e83531 --- /dev/null +++ b/pluralsight.py @@ -0,0 +1,353 @@ +import os +import random +import re +import secrets +import string +import time +from sys import platform + +import requests +import youtube_dl +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.firefox.options import Options + +# region Global Constant(s) and Readonly Variable(s) + +# True/False to determine whether selenium instances will be visible or not (headless) +HIDE_SELENIUM_INSTANCES = False + +# The maximum number of courses to download from a single account +MAX_COURSE_DOWNLOAD_COUNT = 5 + +# Denotes Time.Sleep() duration in seconds +SLEEP_DURATION = 5 + +# Master Directory Path +MASTER_DIRECTORY = os.path.join(os.path.expanduser("~/Desktop"), "Pluralsight") + +# Path of the text file where pluralsight account details will be stored +ACCOUNT_FILE_PATH = os.path.join(MASTER_DIRECTORY, "ps.txt") + +# Path of the text file where pluralsight courses to be downloaded will be stored +COURSE_LINKS_FILE_PATH = os.path.join(MASTER_DIRECTORY, "c.txt") + +# Path of the directory where downloaded courses will be saved +SAVE_DIRECTORY_PATH = os.path.join(MASTER_DIRECTORY, "Courses") + +# Path of the archive text file used by Youtube-dl to keep track of downloaded videos +ARCHIVE_FILE_PATH = os.path.join(MASTER_DIRECTORY, "archive.txt") + +# Options for youtube-dl. For a complete list of options, check https://github.com/ytdl-org/youtube-dl/blob/3e4cedf9e8cd3157df2457df7274d0c842421945/youtube_dl/YoutubeDL.py#L137-L312 +ydl_options = { + 'writesubtitles': True, + 'nooverwrites': True, + 'download_archive': ARCHIVE_FILE_PATH, + 'sleep_interval': 20, + 'max_sleep_interval': 40, + # 'outtmpl': f"{SAVE_DIRECTORY_PATH}/%(playlist)s/%(chapter_number)s - %(chapter)s/%(playlist_index)s - %(title)s.%(ext)s" + # Windows Users should comment out the previous outtmpl and uncomment the following + # 'outtmpl': f"{save_directory_path}\\%(playlist)s\\%(chapter_number)s - %(chapter)s\\%(playlist_index)s - %(title)s.%(ext)s" +} + +if platform.startswith("win"): + ydl_options[ + 'outtmpl'] = f"{SAVE_DIRECTORY_PATH}\\%(playlist)s\\%(chapter_number)s - %(chapter)s\\%(playlist_index)s - %(title)s.%(ext)s" +else: + ydl_options[ + 'outtmpl'] = f"{SAVE_DIRECTORY_PATH}/%(playlist)s/%(chapter_number)s - %(chapter)s/%(playlist_index)s - %(title)s.%(ext)s" + + +# endregion + +class TempGmail: + """ + This class is used to generate random disposable gmails from https://freetempemails.com + and use them for registration purpose + """ + + def __init__(self, email_address: str): + self.email_address = email_address + + def get_email_id(self) -> object: + post_url = "https://gmailnator.com/mailbox/mailboxquery" + post_data = { + 'action': 'LoadMailList', + 'Email_address': self.email_address + } + + while True: + try: + time.sleep(1) + + response_text = requests.post(post_url, post_data).json()[0]['content'] + + result = re.findall('#(.*)\\">', response_text) + mail_id = result[0] + + return mail_id + + + except Exception as e: + pass + + def get_verification_link(self) -> str: + post_url = "https://gmailnator.com/mailbox/get_single_message/" + post_data = { + 'action': 'LoadMailList', + 'message_id': self.get_email_id(), + 'email': self.email_address.split("+")[0] + } + + response_data = requests.post(post_url, post_data).text + + soup = BeautifulSoup(response_data) + for link in soup.findAll('a', href=True): + if "https://app.pluralsight.com/id/forgotpassword/reset?token" in link['href']: + return link['href'] + + +class Pluralsight: + """ + This class handles the registration, verification and bootstrapping of new Pluralsight accounts + """ + + def __init__(self, email: str, password: str, is_headless: bool = True): + if is_headless: + options = Options() + options.add_argument("--headless") + self.driver = webdriver.Firefox(options=options) + else: + self.driver = webdriver.Firefox() + + self.email = email + self.password = password + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.driver.quit() + + @staticmethod + def get_name() -> str: + """ + Generate a random string to be used as first or last name + + Returns: + str: Generated string + """ + + letters = string.ascii_lowercase + + return ''.join(random.choice(letters) for _ in range(random.randint(5, 15))) + + def register(self) -> None: + """ + Registers new Pluralsight account + """ + + self.driver.get("https://www.pluralsight.com/offer/2020/free-april-month") + time.sleep(SLEEP_DURATION) + + accept_cookie_button_element = self.driver.find_element_by_class_name("cookie_notification--opt_in") + accept_cookie_button_element.click() + + time.sleep(1) + + sign_up_now_button_element = self.driver.find_element_by_xpath('//a[@data-aa-title="Free-April-Start-Now"]') + sign_up_now_button_element.click() + + time.sleep(1) + + email_input_element = self.driver.find_element_by_name("email") + firstname_input_element = self.driver.find_element_by_name("firstname") + lastname_input_element = self.driver.find_element_by_name("lastname") + tos_checkbox_element = self.driver.find_element_by_name("optInBox") + + email_input_element.send_keys(self.email) + firstname_input_element.send_keys(self.get_name()) + lastname_input_element.send_keys(self.get_name()) + tos_checkbox_element.click() + + time.sleep(SLEEP_DURATION) + + create_account_button_element = self.driver.find_element_by_xpath( + "//*[contains(text(), 'I agree, activate benefit')]") + create_account_button_element.click() + + time.sleep(30) + + cancel_button_element = self.driver.find_element_by_class_name("cancelButton---CKAut") + cancel_button_element.click() + + def set_password(self, verification_link: str) -> None: + """ + Sets password in the given verification link + Args: + verification_link: The verification link (as string) to set up password + """ + + self.driver.get(verification_link) + time.sleep(SLEEP_DURATION) + + password_input_element = self.driver.find_element_by_id("Password") + password_confirm_input_element = self.driver.find_element_by_id("PasswordConfirmation") + save_button_element = self.driver.find_element_by_class_name("psds-button--appearance-primary") + + password_input_element.send_keys(self.password) + password_confirm_input_element.send_keys(self.password) + + time.sleep(1) + + save_button_element.click() + + time.sleep(SLEEP_DURATION) + + def bootstrap(self): + """ + Bootstraps newly registered accounts to prevent 403 errors in youtube-dl + """ + + username_input_element = self.driver.find_element_by_id("Username") + password_input_element = self.driver.find_element_by_id("Password") + login_button_element = self.driver.find_element_by_id("login") + + username_input_element.send_keys(self.email) + password_input_element.send_keys(self.password) + + time.sleep(1) + + login_button_element.click() + + time.sleep(SLEEP_DURATION) + + cancel_button_element = self.driver.find_element_by_class_name("cancelButton---CKAut") + cancel_button_element.click() + + +def get_password(length: int = 30) -> str: + """ + Generates a random password using ascii letters and numerical digits + Args: + length: Length of the password, default is 30 + + Returns: Generated password as string + """ + + alphabet = string.ascii_letters + string.digits + password = ''.join(secrets.choice(alphabet) for _ in range(length)) + + return password + + +def generate_email(is_gmail: bool = True) -> str: + """ + Generates a new email + + Returns: Generated email string + """ + + gmailnator_gen_url = "https://gmailnator.com/index/indexquery" + post_data = { + 'action': 'GenerateEmail' + } + email = requests.post(gmailnator_gen_url, post_data).text + + return email + + +def create_pluralsight_account() -> None: + """ + Creates new Pluralsight account using Pluralsight and TempMail + """ + + try: + email = generate_email() + password = get_password() + + with Pluralsight(email=email, password=password, is_headless=HIDE_SELENIUM_INSTANCES) as ps: + ps.register() + + verification_link = TempGmail(email_address=email).get_verification_link() + + ps.set_password(verification_link=verification_link) + + time.sleep(SLEEP_DURATION) + + with open(ACCOUNT_FILE_PATH, 'w+') as account_file: + account_file.write(f"{email}\n") + account_file.write(f"{password}\n") + + except Exception as e: + print(f"ERROR OCCURRED!!\n\nDETAILS: {e.__str__()} | {e.__context__}") + + +def download_course(course_link: str, username: str, password: str) -> bool: + """ + Download the given course using the provided credential + + Args: + course_link: The link of the course to download + username: Username (Email) of the Pluralsight account to be used for download + password: Password of the Pluralsight account to be used for download + + Returns: True/False bool value denoting the success status of the download + """ + + try: + ydl_options['username'] = username + ydl_options['password'] = password + + with youtube_dl.YoutubeDL(ydl_options) as ydl: + ydl.download([course_link]) + + return True + except Exception as exception: + return False + + +def main(): + if not os.path.exists(COURSE_LINKS_FILE_PATH): + print(f"{COURSE_LINKS_FILE_PATH} NOT FOUND!") + return + + with open(COURSE_LINKS_FILE_PATH, 'r') as course_file: + course_list = [course.rstrip() for course in course_file.readlines()] + + download_count = 0 + for course in course_list: + try: + while True: + if not os.path.exists(ACCOUNT_FILE_PATH) or download_count > 5: + print("CREATING NEW PLURALSIGHT ACCOUNT") + + create_pluralsight_account() + download_count = 0 + + print("SUCCESS! NEW PLURALSIGHT ACCOUNT CREATED.") + + with open(ACCOUNT_FILE_PATH, 'r') as account_file: + lines = account_file.readlines() + email = lines[0].rstrip() + password = lines[1].rstrip() + + print(f"[{email}] DOWNLOADING COURSE: {course}") + + is_download_success = download_course(course, username=email, password=password) + + if not is_download_success: + os.remove(ACCOUNT_FILE_PATH) + continue + + print(f"[{email}] SUCCESSFULLY DOWNLOADED COURSE: {course}") + + break + except Exception as e: + print(e) + finally: + download_count += 1 + + +if __name__ == '__main__': + main()