From 8ec2ca11e27ddaaa38443a169275cb2e73a7e4fb Mon Sep 17 00:00:00 2001
From: jlarini <41792441+jlarini@users.noreply.github.com>
Date: Mon, 17 May 2021 20:21:46 -0300
Subject: [PATCH 01/19] FreebiesGlobal Included "--freebiesglobal"
---
udemy_enroller/cli.py | 75 ++++++++++++---
udemy_enroller/runner.py | 11 ++-
udemy_enroller/scrapers/comidoc.py | 10 +-
udemy_enroller/scrapers/freebiesglobal.py | 110 ++++++++++++++++++++++
4 files changed, 189 insertions(+), 17 deletions(-)
create mode 100644 udemy_enroller/scrapers/freebiesglobal.py
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index 8f5f308..ea22c48 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -23,31 +23,49 @@ def enable_debug_logging() -> None:
def determine_if_scraper_enabled(
- tutorialbar_enabled: bool,
- discudemy_enabled: bool,
- coursevania_enabled: bool,
-) -> Tuple[bool, bool, bool]:
+ freebiesglobal_enabled: bool,
+ comidoc_enabled: bool,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
+) -> Tuple[bool, bool, bool, bool, bool]:
"""
Determine what scrapers should be enabled and disabled
:return: tuple containing boolean of what scrapers should run
"""
- if not tutorialbar_enabled and not discudemy_enabled and not coursevania_enabled:
+ if not freebiesglobal_enabled \
+ and not comidoc_enabled \
+ and not tutorialbar_enabled \
+ and not discudemy_enabled \
+ and not coursevania_enabled:
# Set all to True
- tutorialbar_enabled, discudemy_enabled, coursevania_enabled = True, True, True
- return tutorialbar_enabled, discudemy_enabled, coursevania_enabled
+ freebiesglobal_enabled, \
+ comidoc_enabled, \
+ tutorialbar_enabled, \
+ discudemy_enabled, \
+ coursevania_enabled = True, True, True, True, True
+
+ return freebiesglobal_enabled, \
+ comidoc_enabled, \
+ tutorialbar_enabled, \
+ discudemy_enabled, \
+ coursevania_enabled
def run(
- tutorialbar_enabled: bool,
- discudemy_enabled: bool,
- coursevania_enabled: bool,
- max_pages: Union[int, None],
- delete_settings: bool,
+ freebiesglobal_enabled: bool,
+ comidoc_enabled: bool,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
+ max_pages: Union[int, None],
+ delete_settings: bool,
):
"""
Run the udemy enroller script
-
+ :param bool freebiesglobal_enabled:
+ :param bool comidoc_enabled:
:param bool tutorialbar_enabled:
:param bool discudemy_enabled:
:param bool coursevania_enabled:
@@ -57,7 +75,13 @@ def run(
"""
settings = Settings(delete_settings)
redeem_courses(
- settings, tutorialbar_enabled, discudemy_enabled, coursevania_enabled, max_pages
+ settings,
+ freebiesglobal_enabled,
+ comidoc_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages
)
@@ -69,6 +93,19 @@ def parse_args() -> Namespace:
"""
parser = argparse.ArgumentParser(description="Udemy Enroller")
+ parser.add_argument(
+ "--freebiesglobal",
+ action="store_true",
+ default=False,
+ help="Run freebiesglobal scraper",
+ )
+
+ parser.add_argument(
+ "--comidoc",
+ action="store_true",
+ default=False,
+ help="Run comidoc scraper",
+ )
parser.add_argument(
"--tutorialbar",
action="store_true",
@@ -116,13 +153,21 @@ def main():
if args.debug:
enable_debug_logging()
(
+ freebiesglobal_enabled,
+ comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
) = determine_if_scraper_enabled(
- args.tutorialbar, args.discudemy, args.coursevania
+ args.freebiesglobal,
+ args.comidoc,
+ args.tutorialbar,
+ args.discudemy,
+ args.coursevania
)
run(
+ freebiesglobal_enabled,
+ comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index de43b76..c5601a5 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -55,6 +55,8 @@ def _redeem_courses(
def redeem_courses(
settings: Settings,
+ freebiesglobal_enabled: bool,
+ comidoc_enabled: bool,
tutorialbar_enabled: bool,
discudemy_enabled: bool,
coursevania_enabled: bool,
@@ -64,6 +66,8 @@ def redeem_courses(
Wrapper of _redeem_courses so we always close browser on completion
:param Settings settings: Core settings used for Udemy
+ :param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run
+ :param bool comidoc_enabled: Boolean signifying if comidoc scraper should run
:param bool tutorialbar_enabled: Boolean signifying if tutorialbar scraper should run
:param bool discudemy_enabled: Boolean signifying if discudemy scraper should run
:param bool coursevania_enabled: Boolean signifying if coursevania scraper should run
@@ -72,7 +76,12 @@ def redeem_courses(
"""
try:
scrapers = ScraperManager(
- tutorialbar_enabled, discudemy_enabled, coursevania_enabled, max_pages
+ freebiesglobal_enabled,
+ comidoc_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages
)
_redeem_courses(settings, scrapers)
except Exception as e:
diff --git a/udemy_enroller/scrapers/comidoc.py b/udemy_enroller/scrapers/comidoc.py
index 2685d4b..9168043 100644
--- a/udemy_enroller/scrapers/comidoc.py
+++ b/udemy_enroller/scrapers/comidoc.py
@@ -44,11 +44,19 @@ async def get_links(self) -> List:
:return: List of udemy course urls
"""
+
comidoc_links = []
self.current_page += 1
coupons_data = await get(f"{self.DOMAIN}/coupons?page={self.current_page}")
+
+
soup = BeautifulSoup(coupons_data.decode("utf-8"), "html.parser")
- for course_card in soup.find_all("div", class_="MuiPaper-root"):
+
+ with open("output1.html", "w", encoding='utf-8') as file:
+ file.write(str(soup))
+
+
+ for course_card in soup.find_all("div", class_="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"):
all_links = course_card.find_all("a")
if len(all_links) == 2:
comidoc_links.append(f"{self.DOMAIN}{all_links[1].get('href')}")
diff --git a/udemy_enroller/scrapers/freebiesglobal.py b/udemy_enroller/scrapers/freebiesglobal.py
new file mode 100644
index 0000000..268e759
--- /dev/null
+++ b/udemy_enroller/scrapers/freebiesglobal.py
@@ -0,0 +1,110 @@
+import asyncio
+import logging
+from typing import List
+
+from bs4 import BeautifulSoup
+
+from udemy_enroller.http import get
+from udemy_enroller.scrapers.base_scraper import BaseScraper
+
+logger = logging.getLogger("udemy_enroller")
+
+
+class FreebiesglobalScraper(BaseScraper):
+ """
+ Contains any logic related to scraping of data from Freebiesglobal.com
+ """
+
+ DOMAIN = "https://freebiesglobal.com"
+
+ def __init__(self, enabled, max_pages=None):
+ super().__init__()
+ self.scraper_name = "freebiesglobal"
+ if not enabled:
+ self.set_state_disabled()
+ self.max_pages = max_pages
+
+ @BaseScraper.time_run
+ async def run(self) -> List:
+ """
+ Called to gather the udemy links
+
+ :return: List of udemy course links
+ """
+ links = await self.get_links()
+ logger.info(
+ f"Page: {self.current_page} of {self.last_page} scraped from freebiesglobal.com"
+ )
+ self.max_pages_reached()
+ return links
+
+ async def get_links(self) -> List:
+ """
+ Scrape udemy links from freebiesglobal.com
+
+ :return: List of udemy course urls
+ """
+ freebiesglobal_links = []
+ self.current_page += 1
+ coupons_data = await get(f"{self.DOMAIN}/dealstore/udemy/page/{self.current_page}")
+ soup = BeautifulSoup(coupons_data.decode("utf-8"), "html.parser")
+
+ for course_card in soup.find_all("a", class_="img-centered-flex rh-flex-center-align rh-flex-justify-center"):
+ url_end = course_card["href"].split("/")[-1]
+ freebiesglobal_links.append(f"{self.DOMAIN}/{url_end}")
+
+ links = await self.gather_udemy_course_links(freebiesglobal_links)
+
+ for counter, course in enumerate(links):
+ logger.debug(f"Received Link {counter + 1} : {course}")
+
+ self.last_page = self._get_last_page(soup)
+
+ return links
+
+ @classmethod
+ async def get_udemy_course_link(cls, url: str) -> str:
+ """
+ Gets the udemy course link
+
+ :param str url: The url to scrape data from
+ :return: Coupon link of the udemy course
+ """
+
+ data = await get(url)
+ soup = BeautifulSoup(data.decode("utf-8"), "html.parser")
+ for link in soup.find_all("a", class_="re_track_btn"):
+ udemy_link = cls.validate_coupon_url(link["href"])
+
+ if udemy_link is not None:
+ return udemy_link
+
+ async def gather_udemy_course_links(self, courses: List[str]):
+ """
+ Async fetching of the udemy course links from discudemy.com
+
+ :param list courses: A list of discudemy.com course links we want to fetch the udemy links for
+ :return: list of udemy links
+ """
+ return [
+ link
+ for link in await asyncio.gather(*map(self.get_udemy_course_link, courses))
+ if link is not None
+ ]
+
+ @staticmethod
+ def _get_last_page(soup: BeautifulSoup) -> int:
+ """
+ Extract the last page number to scrape
+
+ :param soup:
+ :return: The last page number to scrape
+ """
+
+ return max(
+ [
+ int(i.text)
+ for i in soup.find("ul", class_="page-numbers").find_all("li")
+ if i.text.isdigit()
+ ]
+ )
From 19d9878fb18193fcbdd2c3cbd063c2486f7b3b2f Mon Sep 17 00:00:00 2001
From: jlarini <41792441+jlarini@users.noreply.github.com>
Date: Mon, 17 May 2021 20:22:57 -0300
Subject: [PATCH 02/19] FreebiesGlobal Included "--freebiesglobal"
---
udemy_enroller/scrapers/manager.py | 18 ++++++++++++++++-
udemy_enroller/udemy.py | 32 ++++++++++++++++++------------
2 files changed, 36 insertions(+), 14 deletions(-)
diff --git a/udemy_enroller/scrapers/manager.py b/udemy_enroller/scrapers/manager.py
index ae54e45..e5e740e 100644
--- a/udemy_enroller/scrapers/manager.py
+++ b/udemy_enroller/scrapers/manager.py
@@ -2,6 +2,8 @@
from functools import reduce
from typing import List
+from udemy_enroller.scrapers.freebiesglobal import FreebiesglobalScraper
+from udemy_enroller.scrapers.comidoc import ComidocScraper
from udemy_enroller.scrapers.coursevania import CoursevaniaScraper
from udemy_enroller.scrapers.discudemy import DiscUdemyScraper
from udemy_enroller.scrapers.tutorialbar import TutorialBarScraper
@@ -9,8 +11,20 @@
class ScraperManager:
def __init__(
- self, tutorialbar_enabled, discudemy_enabled, coursevania_enabled, max_pages
+ self,
+ freebiesglobal_enabled,
+ comidoc_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages
):
+ self.freebiesglobal_scraper = FreebiesglobalScraper(
+ freebiesglobal_enabled, max_pages=max_pages
+ )
+ self.comidoc_scraper = ComidocScraper(
+ comidoc_enabled, max_pages=max_pages
+ )
self.tutorialbar_scraper = TutorialBarScraper(
tutorialbar_enabled, max_pages=max_pages
)
@@ -21,6 +35,8 @@ def __init__(
coursevania_enabled, max_pages=max_pages
)
self._scrapers = (
+ self.freebiesglobal_scraper,
+ self.comidoc_scraper,
self.tutorialbar_scraper,
self.discudemy_scraper,
self.coursevania_scraper,
diff --git a/udemy_enroller/udemy.py b/udemy_enroller/udemy.py
index d318303..7113b96 100644
--- a/udemy_enroller/udemy.py
+++ b/udemy_enroller/udemy.py
@@ -49,7 +49,7 @@ class UdemyActions:
HEADERS = {
"origin": "https://www.udemy.com",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 "
- "Safari/537.36",
+ "Safari/537.36",
"accept": "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br",
"content-type": "application/json;charset=UTF-8",
@@ -123,6 +123,7 @@ def login(self, retry=False) -> None:
try:
self._enrolled_course_info = self.load_my_courses()
+
user_details = self.load_user_details()
# Extract the users currency info needed for checkout
self._currency = user_details["Config"]["price_country"]["currency"]
@@ -152,7 +153,7 @@ def load_my_courses(self) -> List:
logger.info("Loading existing course details")
all_courses = list()
page_size = 100
-
+ # return all_courses
my_courses = self.my_courses(1, page_size)
all_courses.extend(my_courses["results"])
total_pages = my_courses["count"] // page_size
@@ -162,6 +163,11 @@ def load_my_courses(self) -> List:
all_courses.extend(my_courses["results"])
time.sleep(1)
logger.info(f"Currently enrolled in {len(all_courses)} courses")
+
+ for counter, course in enumerate(all_courses):
+ with open("Courses.txt", "a") as file:
+ file.write(f"{counter}\t==>\t{course[counter]}\n")
+
return all_courses
def load_user_details(self):
@@ -209,9 +215,9 @@ def is_coupon_valid(self, course_id: int, coupon_code: str) -> bool:
)
coupon_valid = False
if not bool(
- coupon_details["price_text"]["data"]["pricing_result"]["list_price"][
- "amount"
- ]
+ coupon_details["price_text"]["data"]["pricing_result"]["list_price"][
+ "amount"
+ ]
):
logger.debug("Skipping course as it is always FREE")
coupon_valid = False
@@ -243,9 +249,9 @@ def is_preferred_category(self, course_details: Dict) -> bool:
is_preferred_category = True
if (
- course_details["primary_category"]["title"] not in self.settings.categories
- and course_details["primary_subcategory"]["title"]
- not in self.settings.categories
+ course_details["primary_category"]["title"] not in self.settings.categories
+ and course_details["primary_subcategory"]["title"]
+ not in self.settings.categories
):
logger.debug("Skipping course as it does not have a wanted category")
is_preferred_category = False
@@ -332,11 +338,11 @@ def _get_course_id(self, url: str) -> int:
return int(soup.find("body")["data-clp-course-id"])
def _checkout(
- self,
- course_id: int,
- coupon_code: str,
- course_identifier: str,
- retry: bool = False,
+ self,
+ course_id: int,
+ coupon_code: str,
+ course_identifier: str,
+ retry: bool = False,
) -> str:
"""
Checkout process for the course and coupon provided
From f5227bfa98845ec6b03e2dcc2c9de6183d48ee90 Mon Sep 17 00:00:00 2001
From: jlarini <41792441+jlarini@users.noreply.github.com>
Date: Thu, 20 May 2021 16:05:00 -0300
Subject: [PATCH 03/19] FreebiesGlobal Included "--freebiesglobal"
---
README.md | 57 +++++++----
udemy_enroller/cli.py | 25 ++---
udemy_enroller/runner.py | 53 +++++++---
udemy_enroller/scrapers/comidoc.py | 115 ----------------------
udemy_enroller/scrapers/freebiesglobal.py | 2 +-
udemy_enroller/scrapers/manager.py | 6 --
udemy_enroller/udemy.py | 19 ++--
7 files changed, 99 insertions(+), 178 deletions(-)
delete mode 100644 udemy_enroller/scrapers/comidoc.py
diff --git a/README.md b/README.md
index 00b3f8e..9286b82 100644
--- a/README.md
+++ b/README.md
@@ -9,34 +9,35 @@ Do you want to LEARN NEW STUFF for FREE? Don't worry, with the power of
web-scraping and automation, this script will find the necessary Udemy Coupons
& enroll you to PAID UDEMY COURSES, ABSOLUTELY FREE!
-**NOTE: THIS PROJECT IS NOT AFFILIATED WITH UDEMY.**
+##NOTE: THIS PROJECT IS NOT AFFILIATED WITH UDEMY.##
-**NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.**
+##NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.##
The code scrapes course links and coupons from:
- [tutorialbar.com](https://tutorialbar.com)
- [discudemy.com](https://discudemy.com)
- [coursevania.com](https://coursevania.com)
+ - [freebiesglobal.com](https://freebiesglobal.com) -> _New_
In case of any bugs or issues, please open an issue in github.
-Also, don't forget to **Fork & Star the repository if you like it!**
+Also, don't forget to ##Fork & Star the repository if you like it!##
-***We are also on [GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)***
+###We are also on [GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)###
-**_Video Proof:_**
+##_Video Proof:_##
[![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/IW8CCtv2k2A/0.jpg)](https://www.youtube.com/watch?v=IW8CCtv2k2A "GET PAID UDEMY Courses for FREE, Automatically with this Python Script!")
---
-## **_Disclaimer & WARNINGS:_**
+## ##_Disclaimer & WARNINGS:_##
-1. **Use** this ONLY for **Educational Purposes!** By using this code you agree
- that **I'm not responsible for any kind of trouble** caused by the code. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.**
-2. **Make sure web-scraping is legal in your region.**
-3. This is **NOT a hacking script**, i.e., it can't enroll you for a specific
+1. ##Use## this ONLY for ##Educational Purposes!## By using this code you agree
+ that ##I'm not responsible for any kind of trouble## caused by the code. ##THIS PROJECT IS NOT AFFILIATED WITH UDEMY.##
+2. ##Make sure web-scraping is legal in your region.##
+3. This is ##NOT a hacking script##, i.e., it can't enroll you for a specific
course! Instead it finds courses that provide coupon links to make the
transaction free and then LEGALLY enroll you to the course!
@@ -46,9 +47,9 @@ Also, don't forget to **Fork & Star the repository if you like it!**
### How to Install the Requirements?
-**Required Python version:** [Python 3.8+](https://www.python.org/downloads/)
+##Required Python version:## [Python 3.8+](https://www.python.org/downloads/)
-**You must have pip or poetry installed. Please look up how to install them in your OS.**
+##You must have pip or poetry installed. Please look up how to install them in your OS.##
Download a release of this project or clone the repository then navigate to the
folder where you placed the files on. Type `pip install -r requirements.txt` to
@@ -66,18 +67,19 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
- Run the script and the cli will guide you through the settings required
- If you decide to save the settings they will be stored in your home directory:
-**Windows**:
+##Windows##:
C:/Users/CurrentUserName/.udemy_enroller
-**Linux**:
+##Linux##:
/home/username/.udemy_enroller
- **The values in settings.yaml should be in the same language as the site you are browsing on**
+ ##The values in settings.yaml should be in the same language as the site you are browsing on##
2 . The script can be passed arguments:
- `--help`: View full list of arguments available
- `--discudemy`: Run the discudemy scraper only
- `--coursevania`: Run the coursevania scraper only
- `--tutorialbar`: Run the tutorialbar scraper only
+- `--freebiesglobal`: Run the freebiesglobal scraper only _[New]_
- `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5)
- `--delete-settings`: Delete existing settings file
- `--debug`: Enable debug logging
@@ -85,13 +87,32 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
3 . Run the script in terminal like so:
- `udemy_enroller`
-4 . The bot starts scraping the course links from the first **All Courses** page
-on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com) and starts
+4 . The bot starts scraping the course links from the first ##All Courses## page
+on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts
enrolling you to Udemy courses. After it has enrolled you to courses from the
first page, it then moves to the next site page and the cycle continues.
- Stop the script by pressing ctrl+c in terminal to stop the enrollment process.
+5. _[New]_ After each scrap how many courses was found is shown.
+
+Total courses this time: 54
+
+6. _[New]_ At the end of process a detailed result is shown:
+
+
+ ####################################
+ # RESULTS #
+ ####################################
+ # New Enrolled Courses: 0000 #
+ # Already Enrolled Courses: 0204 #
+ # Expired: 0003 #
+ # Other Languages: 0021 #
+ # Other Categories 0081 #
+ ####################################
+ # Total Courses Scraped: 0309 #
+ ####################################
+
---
## FAQs
@@ -120,7 +141,7 @@ Daily, at least once! I've painstakingly amassed over 4000
courses in the last four years! And out of those 4000, I've only paid for 4 of
these courses.
-So, a mere **0.001%** of courses are **actually paid** in my collection!
+So, a mere ##0.001%## of courses are ##actually paid## in my collection!
Thankfully, you can get more than what I gathered in 4 years, in a matter of
weeks! ๐๐ป
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index ea22c48..7923b19 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -24,30 +24,26 @@ def enable_debug_logging() -> None:
def determine_if_scraper_enabled(
freebiesglobal_enabled: bool,
- comidoc_enabled: bool,
tutorialbar_enabled: bool,
discudemy_enabled: bool,
coursevania_enabled: bool,
-) -> Tuple[bool, bool, bool, bool, bool]:
+) -> tuple[bool, bool, bool, bool]:
"""
Determine what scrapers should be enabled and disabled
:return: tuple containing boolean of what scrapers should run
"""
if not freebiesglobal_enabled \
- and not comidoc_enabled \
and not tutorialbar_enabled \
and not discudemy_enabled \
and not coursevania_enabled:
# Set all to True
freebiesglobal_enabled, \
- comidoc_enabled, \
tutorialbar_enabled, \
discudemy_enabled, \
- coursevania_enabled = True, True, True, True, True
+ coursevania_enabled = True, True, True, True
return freebiesglobal_enabled, \
- comidoc_enabled, \
tutorialbar_enabled, \
discudemy_enabled, \
coursevania_enabled
@@ -55,7 +51,6 @@ def determine_if_scraper_enabled(
def run(
freebiesglobal_enabled: bool,
- comidoc_enabled: bool,
tutorialbar_enabled: bool,
discudemy_enabled: bool,
coursevania_enabled: bool,
@@ -65,7 +60,6 @@ def run(
"""
Run the udemy enroller script
:param bool freebiesglobal_enabled:
- :param bool comidoc_enabled:
:param bool tutorialbar_enabled:
:param bool discudemy_enabled:
:param bool coursevania_enabled:
@@ -77,7 +71,6 @@ def run(
redeem_courses(
settings,
freebiesglobal_enabled,
- comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
@@ -100,42 +93,41 @@ def parse_args() -> Namespace:
help="Run freebiesglobal scraper",
)
- parser.add_argument(
- "--comidoc",
- action="store_true",
- default=False,
- help="Run comidoc scraper",
- )
parser.add_argument(
"--tutorialbar",
action="store_true",
default=False,
help="Run tutorialbar scraper",
)
+
parser.add_argument(
"--discudemy",
action="store_true",
default=False,
help="Run discudemy scraper",
)
+
parser.add_argument(
"--coursevania",
action="store_true",
default=False,
help="Run coursevania scraper",
)
+
parser.add_argument(
"--max-pages",
type=int,
default=5,
help=f"Max pages to scrape from sites (if pagination exists) (Default is 5)",
)
+
parser.add_argument(
"--delete-settings",
action="store_true",
default=False,
help="Delete any existing settings file",
)
+
parser.add_argument(
"--debug",
action="store_true",
@@ -154,20 +146,17 @@ def main():
enable_debug_logging()
(
freebiesglobal_enabled,
- comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
) = determine_if_scraper_enabled(
args.freebiesglobal,
- args.comidoc,
args.tutorialbar,
args.discudemy,
args.coursevania
)
run(
freebiesglobal_enabled,
- comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index c5601a5..ecdfba7 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -10,8 +10,14 @@
def _redeem_courses(
- settings: Settings,
- scrapers: ScraperManager,
+ settings: Settings,
+ scrapers: ScraperManager,
+ counter_enroled: int = 0,
+ counter_already_enroled: int = 0,
+ counter_expired: int = 0,
+ counter_other_languages: int = 0,
+ counter_other_categories: int = 0,
+ counter_total: int = 0
) -> None:
"""
Method to scrape courses from tutorialbar.com and enroll in them on udemy
@@ -26,18 +32,30 @@ def _redeem_courses(
while True:
udemy_course_links = loop.run_until_complete(scrapers.run())
-
+ logger.info(f"Total courses this time: {len(udemy_course_links)}")
if udemy_course_links:
for course_link in udemy_course_links:
+ counter_total += 1
try:
status = udemy_actions.enroll(course_link)
+
if status == UdemyStatus.ENROLLED.value:
+ counter_enroled += 1
# Try to avoid udemy throttling by sleeping for 1-5 seconds
sleep_time = random.choice(range(1, 5))
logger.debug(
f"Sleeping for {sleep_time} seconds between enrolments"
)
time.sleep(sleep_time)
+ elif status == UdemyStatus.ALREADY_ENROLLED.value:
+ counter_already_enroled += 1
+ elif status == UdemyStatus.EXPIRED.value:
+ counter_expired += 1
+ elif status == UdemyStatus.UNWANTED_LANGUAGE.value:
+ counter_other_languages += 1
+ elif status == UdemyStatus.UNWANTED_CATEGORY.value:
+ counter_other_categories += 1
+
except KeyboardInterrupt:
logger.error("Exiting the script")
return
@@ -48,26 +66,36 @@ def _redeem_courses(
logger.info("We have attempted to subscribe to 1 udemy course")
logger.info("Ending test")
return
+
else:
- logger.info("All scrapers complete")
+ logger.info("All scrapers complete\n\n")
+ logger.info("\t####################################")
+ logger.info("\t# RESULTS #")
+ logger.info("\t####################################")
+ logger.info(f"\t# New Enrolled Courses: {counter_enroled:04} #")
+ logger.info(f"\t# Already Enrolled Courses: {counter_already_enroled:04} #")
+ logger.info(f"\t# Expired: {counter_expired:04} #")
+ logger.info(f"\t# Other Languages: {counter_other_languages:04} #")
+ logger.info(f"\t# Other Categories: {counter_other_categories:04} #")
+ logger.info("\t####################################")
+ logger.info(f"\t# Total Courses Scraped: {counter_total:04} #")
+ logger.info("\t####################################")
return
def redeem_courses(
- settings: Settings,
- freebiesglobal_enabled: bool,
- comidoc_enabled: bool,
- tutorialbar_enabled: bool,
- discudemy_enabled: bool,
- coursevania_enabled: bool,
- max_pages: Union[int, None],
+ settings: Settings,
+ freebiesglobal_enabled: bool,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
+ max_pages: Union[int, None],
) -> None:
"""
Wrapper of _redeem_courses so we always close browser on completion
:param Settings settings: Core settings used for Udemy
:param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run
- :param bool comidoc_enabled: Boolean signifying if comidoc scraper should run
:param bool tutorialbar_enabled: Boolean signifying if tutorialbar scraper should run
:param bool discudemy_enabled: Boolean signifying if discudemy scraper should run
:param bool coursevania_enabled: Boolean signifying if coursevania scraper should run
@@ -77,7 +105,6 @@ def redeem_courses(
try:
scrapers = ScraperManager(
freebiesglobal_enabled,
- comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
diff --git a/udemy_enroller/scrapers/comidoc.py b/udemy_enroller/scrapers/comidoc.py
deleted file mode 100644
index 9168043..0000000
--- a/udemy_enroller/scrapers/comidoc.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import asyncio
-import logging
-from typing import List
-
-from bs4 import BeautifulSoup
-
-from udemy_enroller.http import get
-from udemy_enroller.scrapers.base_scraper import BaseScraper
-
-logger = logging.getLogger("udemy_enroller")
-
-
-class ComidocScraper(BaseScraper):
- """
- Contains any logic related to scraping of data from comidoc.net
- """
-
- DOMAIN = "https://comidoc.net"
-
- def __init__(self, enabled, max_pages=None):
- super().__init__()
- self.scraper_name = "comidoc"
- if not enabled:
- self.set_state_disabled()
- self.max_pages = max_pages
-
- @BaseScraper.time_run
- async def run(self) -> List:
- """
- Called to gather the udemy links
-
- :return: List of udemy course links
- """
- links = await self.get_links()
- logger.info(
- f"Page: {self.current_page} of {self.last_page} scraped from comidoc.net"
- )
- self.max_pages_reached()
- return links
-
- async def get_links(self) -> List:
- """
- Scrape udemy links from comidoc.net
-
- :return: List of udemy course urls
- """
-
- comidoc_links = []
- self.current_page += 1
- coupons_data = await get(f"{self.DOMAIN}/coupons?page={self.current_page}")
-
-
- soup = BeautifulSoup(coupons_data.decode("utf-8"), "html.parser")
-
- with open("output1.html", "w", encoding='utf-8') as file:
- file.write(str(soup))
-
-
- for course_card in soup.find_all("div", class_="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"):
- all_links = course_card.find_all("a")
- if len(all_links) == 2:
- comidoc_links.append(f"{self.DOMAIN}{all_links[1].get('href')}")
-
- links = await self.gather_udemy_course_links(comidoc_links)
- self.last_page = self._get_last_page(soup)
-
- return links
-
- @classmethod
- async def get_udemy_course_link(cls, url: str) -> str:
- """
- Gets the udemy course link
-
- :param str url: The url to scrape data from
- :return: Coupon link of the udemy course
- """
-
- data = await get(url)
- soup = BeautifulSoup(data.decode("utf-8"), "html.parser")
- for link in soup.find_all("a", href=True):
- udemy_link = cls.validate_coupon_url(link["href"])
- if udemy_link is not None:
- return udemy_link
-
- async def gather_udemy_course_links(self, courses: List[str]):
- """
- Async fetching of the udemy course links from comidoc.net
-
- :param list courses: A list of comidoc.net course links we want to fetch the udemy links for
- :return: list of udemy links
- """
- return [
- link
- for link in await asyncio.gather(*map(self.get_udemy_course_link, courses))
- if link is not None
- ]
-
- @staticmethod
- def _get_last_page(soup: BeautifulSoup) -> int:
- """
- Extract the last page number to scrape
-
- :param soup:
- :return: The last page number to scrape
- """
- all_pages = []
- for page_link in soup.find("ul", class_="MuiPagination-ul").find_all("li"):
- pagination = page_link.find("a")
-
- if pagination:
- page_number = pagination["aria-label"].split()[-1]
- if page_number.isdigit():
- all_pages.append(int(page_number))
-
- return max(all_pages)
diff --git a/udemy_enroller/scrapers/freebiesglobal.py b/udemy_enroller/scrapers/freebiesglobal.py
index 268e759..aeef783 100644
--- a/udemy_enroller/scrapers/freebiesglobal.py
+++ b/udemy_enroller/scrapers/freebiesglobal.py
@@ -81,7 +81,7 @@ async def get_udemy_course_link(cls, url: str) -> str:
async def gather_udemy_course_links(self, courses: List[str]):
"""
- Async fetching of the udemy course links from discudemy.com
+ Async fetching of the udemy course links from freebiesglobal.com
:param list courses: A list of discudemy.com course links we want to fetch the udemy links for
:return: list of udemy links
diff --git a/udemy_enroller/scrapers/manager.py b/udemy_enroller/scrapers/manager.py
index e5e740e..046c564 100644
--- a/udemy_enroller/scrapers/manager.py
+++ b/udemy_enroller/scrapers/manager.py
@@ -3,7 +3,6 @@
from typing import List
from udemy_enroller.scrapers.freebiesglobal import FreebiesglobalScraper
-from udemy_enroller.scrapers.comidoc import ComidocScraper
from udemy_enroller.scrapers.coursevania import CoursevaniaScraper
from udemy_enroller.scrapers.discudemy import DiscUdemyScraper
from udemy_enroller.scrapers.tutorialbar import TutorialBarScraper
@@ -13,7 +12,6 @@ class ScraperManager:
def __init__(
self,
freebiesglobal_enabled,
- comidoc_enabled,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
@@ -22,9 +20,6 @@ def __init__(
self.freebiesglobal_scraper = FreebiesglobalScraper(
freebiesglobal_enabled, max_pages=max_pages
)
- self.comidoc_scraper = ComidocScraper(
- comidoc_enabled, max_pages=max_pages
- )
self.tutorialbar_scraper = TutorialBarScraper(
tutorialbar_enabled, max_pages=max_pages
)
@@ -36,7 +31,6 @@ def __init__(
)
self._scrapers = (
self.freebiesglobal_scraper,
- self.comidoc_scraper,
self.tutorialbar_scraper,
self.discudemy_scraper,
self.coursevania_scraper,
diff --git a/udemy_enroller/udemy.py b/udemy_enroller/udemy.py
index 7113b96..654a3cf 100644
--- a/udemy_enroller/udemy.py
+++ b/udemy_enroller/udemy.py
@@ -68,6 +68,8 @@ def __init__(self, settings: Settings, cookie_file_name: str = ".cookie"):
self._all_course_ids = []
self._currency_symbol = None
self._currency = None
+ self.counter_enroled: int = 0
+ self.counter_already_enroled: int = 0
def login(self, retry=False) -> None:
"""
@@ -153,7 +155,7 @@ def load_my_courses(self) -> List:
logger.info("Loading existing course details")
all_courses = list()
page_size = 100
- # return all_courses
+
my_courses = self.my_courses(1, page_size)
all_courses.extend(my_courses["results"])
total_pages = my_courses["count"] // page_size
@@ -164,9 +166,9 @@ def load_my_courses(self) -> List:
time.sleep(1)
logger.info(f"Currently enrolled in {len(all_courses)} courses")
- for counter, course in enumerate(all_courses):
- with open("Courses.txt", "a") as file:
- file.write(f"{counter}\t==>\t{course[counter]}\n")
+ # for counter, course in enumerate(all_courses):
+ # with open("Courses.txt", "a") as file:
+ # file.write(f"{counter}\t==>\t{course[counter]}\n")
return all_courses
@@ -306,7 +308,9 @@ def enroll(self, course_link: str) -> str:
course_identifier = course_details.get("title", url)
if self.is_enrolled(course_id):
- logger.info(f"Already enrolled in: {course_identifier}")
+ self.counter_already_enroled += 1
+
+ logger.info(f"Already enrolled in: {course_identifier} --> {self.counter_already_enroled}")
return UdemyStatus.ALREADY_ENROLLED.value
if self.user_has_preferences:
@@ -342,7 +346,7 @@ def _checkout(
course_id: int,
coupon_code: str,
course_identifier: str,
- retry: bool = False,
+ retry: bool = False
) -> str:
"""
Checkout process for the course and coupon provided
@@ -370,7 +374,8 @@ def _checkout(
else:
result = checkout_result.json()
if result["status"] == "succeeded":
- logger.info(f"Successfully enrolled: {course_identifier}")
+ self.counter_enroled += 1
+ logger.info(f"Successfully enrolled: {course_identifier} --> {self.counter_enroled}")
self._add_enrolled_course(course_id)
return UdemyStatus.ENROLLED.value
elif result["status"] == "failed":
From fcdda0fef2c3cc4a470bbb99459409278a05b1c7 Mon Sep 17 00:00:00 2001
From: jlarini <41792441+jlarini@users.noreply.github.com>
Date: Thu, 20 May 2021 16:25:41 -0300
Subject: [PATCH 04/19] FreebiesGlobal Included "--freebiesglobal"
---
README.md | 20 +++++++++-----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 9286b82..9780202 100644
--- a/README.md
+++ b/README.md
@@ -101,17 +101,15 @@ Total courses this time: 54
6. _[New]_ At the end of process a detailed result is shown:
- ####################################
- # RESULTS #
- ####################################
- # New Enrolled Courses: 0000 #
- # Already Enrolled Courses: 0204 #
- # Expired: 0003 #
- # Other Languages: 0021 #
- # Other Categories 0081 #
- ####################################
- # Total Courses Scraped: 0309 #
- ####################################
+ RESULTS
+
+ New Enrolled Courses: 0000
+ Already Enrolled Courses: 0204
+ Expired: 0003
+ Other Languages: 0021
+ Other Categories 0081
+ Total Courses Scraped: 0309
+
---
From 942d0840b2175248172b088597a9cea06fb8f944 Mon Sep 17 00:00:00 2001
From: jlarini <41792441+jlarini@users.noreply.github.com>
Date: Thu, 20 May 2021 16:40:26 -0300
Subject: [PATCH 05/19] Correct Readme format.
---
README.md | 70 +++++++++++++++++++++++++++----------------------------
1 file changed, 35 insertions(+), 35 deletions(-)
diff --git a/README.md b/README.md
index 9780202..f49f8e5 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,17 @@
[![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com)
[![forthebadge](https://forthebadge.com/images/badges/it-works-why.svg)](https://forthebadge.com)
-# ALPHA IS A PRE DEVELOPMENT BRANCH, DO NOT EXPECT USER FACING ISSUES TO BE ADDRESSED IN THIS BRANCH!
+* ALPHA IS A PRE DEVELOPMENT BRANCH, DO NOT EXPECT USER FACING ISSUES TO BE ADDRESSED IN THIS BRANCH!
-# Udemy Coupon Grabber & Course Enroller: Grab FREE Coupons!
+* Udemy Coupon Grabber & Course Enroller: Grab FREE Coupons!
Do you want to LEARN NEW STUFF for FREE? Don't worry, with the power of
web-scraping and automation, this script will find the necessary Udemy Coupons
& enroll you to PAID UDEMY COURSES, ABSOLUTELY FREE!
-##NOTE: THIS PROJECT IS NOT AFFILIATED WITH UDEMY.##
+**NOTE: THIS PROJECT IS NOT AFFILIATED WITH UDEMY.**
-##NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.##
+**NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.**
The code scrapes course links and coupons from:
- [tutorialbar.com](https://tutorialbar.com)
@@ -21,35 +21,35 @@ The code scrapes course links and coupons from:
In case of any bugs or issues, please open an issue in github.
-Also, don't forget to ##Fork & Star the repository if you like it!##
+Also, don't forget to **Fork & Star the repository if you like it!**
-###We are also on [GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)###
+***We are also on [GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)***
-##_Video Proof:_##
+**_Video Proof:_**
[![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/IW8CCtv2k2A/0.jpg)](https://www.youtube.com/watch?v=IW8CCtv2k2A "GET PAID UDEMY Courses for FREE, Automatically with this Python Script!")
---
-## ##_Disclaimer & WARNINGS:_##
+** **_Disclaimer & WARNINGS:_**
-1. ##Use## this ONLY for ##Educational Purposes!## By using this code you agree
- that ##I'm not responsible for any kind of trouble## caused by the code. ##THIS PROJECT IS NOT AFFILIATED WITH UDEMY.##
-2. ##Make sure web-scraping is legal in your region.##
-3. This is ##NOT a hacking script##, i.e., it can't enroll you for a specific
+1. **Use** this ONLY for **Educational Purposes!** By using this code you agree
+ that **I'm not responsible for any kind of trouble** caused by the code. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.**
+2. **Make sure web-scraping is legal in your region.**
+3. This is **NOT a hacking script**, i.e., it can't enroll you for a specific
course! Instead it finds courses that provide coupon links to make the
transaction free and then LEGALLY enroll you to the course!
---
-## Requirements:
+** Requirements:
-### How to Install the Requirements?
+*** How to Install the Requirements?
-##Required Python version:## [Python 3.8+](https://www.python.org/downloads/)
+**Required Python version:** [Python 3.8+](https://www.python.org/downloads/)
-##You must have pip or poetry installed. Please look up how to install them in your OS.##
+**You must have pip or poetry installed. Please look up how to install them in your OS.**
Download a release of this project or clone the repository then navigate to the
folder where you placed the files on. Type `pip install -r requirements.txt` to
@@ -57,7 +57,7 @@ get all the requirements installed in one go. Similar instructions applies for p
---
-## Instructions
+** Instructions
Props to Davidd Sargent for making a super simple video tutorial. If you prefer written instructions then continue reading further, else click on the image below for a quick video tutorial:
@@ -67,12 +67,12 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
- Run the script and the cli will guide you through the settings required
- If you decide to save the settings they will be stored in your home directory:
-##Windows##:
+**Windows**:
C:/Users/CurrentUserName/.udemy_enroller
-##Linux##:
+**Linux**:
/home/username/.udemy_enroller
- ##The values in settings.yaml should be in the same language as the site you are browsing on##
+ **The values in settings.yaml should be in the same language as the site you are browsing on**
2 . The script can be passed arguments:
- `--help`: View full list of arguments available
@@ -87,7 +87,7 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
3 . Run the script in terminal like so:
- `udemy_enroller`
-4 . The bot starts scraping the course links from the first ##All Courses## page
+4 . The bot starts scraping the course links from the first **All Courses** page
on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts
enrolling you to Udemy courses. After it has enrolled you to courses from the
first page, it then moves to the next site page and the cycle continues.
@@ -115,7 +115,7 @@ Total courses this time: 54
## FAQs
-### 1. Can I get a specific course for free with this script?
+*** 1. Can I get a specific course for free with this script?
Unfortunately no, but let me assure you that you may be lucky enough to get a
particular course for free when the instructor posts its coupon code in order
@@ -125,7 +125,7 @@ I made this course after completing a
[Python automation course](https://www.udemy.com/course/automate/) and selenium,
which of course I got for free! :)
-### 2. How does the bot work?
+*** 2. How does the bot work?
The bot retrieves coupon links from Tutorial Bar, DiscUdemy and Coursevania's lists to cut the prices and
then uses REST requests to authenticate and enroll to the
@@ -133,40 +133,40 @@ courses. Think of it this way: Epic Games & other clients like Steam provide you
a handful of games each week, for free; Only in this case, we need a coupon code
to make those courses free.
-### 3. How frequently should you run the script?
+*** 3. How frequently should you run the script?
Daily, at least once! I've painstakingly amassed over 4000
courses in the last four years! And out of those 4000, I've only paid for 4 of
these courses.
-So, a mere ##0.001%## of courses are ##actually paid## in my collection!
+So, a mere **0.001%** of courses are **actually paid** in my collection!
Thankfully, you can get more than what I gathered in 4 years, in a matter of
weeks! ๐๐ป
-### 4. Why did I create this?
+*** 4. Why did I create this?
It used to be my daily habit to redeem courses and it was an extremely tedious
task that took around 15 minutes, for 10 courses. And then I suddenly got the
idea to automate it, after I found the automation course mentioned above. I bet,
it will save your precious time too! :)
-### 5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?
+*** 5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?
Since we are heavily dependent on a third-party site to retrieve coupons links,
there may be issues when the site is down. Needless to mention the connectivity
issues too. If everything is working fine, you can see the courses being
retrieved in the Python console/shell, which may take a while.
-### 6. Which is the best way to run the script?
+*** 6. Which is the best way to run the script?
It is recommended to run the script using your terminal and system python.
-### 7. Which branch to commit against?
+*** 7. Which branch to commit against?
Pull request should be made on "develop" branch.
-### 8. What's the roadmap?
+*** 8. What's the roadmap?
Take a look at our
[Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1)
@@ -174,27 +174,27 @@ and help us on what you want or talk to us about your proposed changes.
---
-## Support & Maintenance Notice
+** Support & Maintenance Notice
By using this repo/script, you agree that the authors and contributors are under no obligation to provide support for the script and can discontinue it's development, as and when necessary, without prior notice.
---
-## Supporters
+** Supporters
-### Jetbrains
+*** Jetbrains
[![JetBrains](https://i.imgur.com/h2R018M.jpg)](https://jetbrains.com/?from=udemy-free-course-enroller)
Thanks to [JetBrains](https://jetbrains.com/?from=udemy-free-course-enroller) for supporting us. They are the maker of world class IDE and developer tooling. If you think their product might help you, please support them.
-### GitBook
+*** GitBook
[![GitBook](https://i.imgur.com/OkuB14I.jpg)](https://gitbook.com)
Thanks to [GitBook](https://gitbook.com) for supporting us. GitBook is the best place to track personal notes and ideas for teams. If you think their product might help you, please support them.
-### GitLab
+*** GitLab
[![GitLab](https://i.imgur.com/aUWtSn4.png)](https://gitlab.com)
From 2af91b3455ef519d532141c41fb74f09ae5c11ed Mon Sep 17 00:00:00 2001
From: "Restyled.io"
Date: Tue, 14 Sep 2021 20:25:43 +0000
Subject: [PATCH 06/19] style(black): apply code style
---
udemy_enroller/cli.py | 57 ++++++++++++-----------
udemy_enroller/runner.py | 44 +++++++++--------
udemy_enroller/scrapers/freebiesglobal.py | 8 +++-
udemy_enroller/scrapers/manager.py | 10 ++--
udemy_enroller/udemy.py | 32 +++++++------
5 files changed, 84 insertions(+), 67 deletions(-)
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index 7923b19..a7db228 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -23,39 +23,45 @@ def enable_debug_logging() -> None:
def determine_if_scraper_enabled(
- freebiesglobal_enabled: bool,
- tutorialbar_enabled: bool,
- discudemy_enabled: bool,
- coursevania_enabled: bool,
+ freebiesglobal_enabled: bool,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
) -> tuple[bool, bool, bool, bool]:
"""
Determine what scrapers should be enabled and disabled
:return: tuple containing boolean of what scrapers should run
"""
- if not freebiesglobal_enabled \
- and not tutorialbar_enabled \
- and not discudemy_enabled \
- and not coursevania_enabled:
+ if (
+ not freebiesglobal_enabled
+ and not tutorialbar_enabled
+ and not discudemy_enabled
+ and not coursevania_enabled
+ ):
# Set all to True
- freebiesglobal_enabled, \
- tutorialbar_enabled, \
- discudemy_enabled, \
- coursevania_enabled = True, True, True, True
+ (
+ freebiesglobal_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ ) = (True, True, True, True)
- return freebiesglobal_enabled, \
- tutorialbar_enabled, \
- discudemy_enabled, \
- coursevania_enabled
+ return (
+ freebiesglobal_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ )
def run(
- freebiesglobal_enabled: bool,
- tutorialbar_enabled: bool,
- discudemy_enabled: bool,
- coursevania_enabled: bool,
- max_pages: Union[int, None],
- delete_settings: bool,
+ freebiesglobal_enabled: bool,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
+ max_pages: Union[int, None],
+ delete_settings: bool,
):
"""
Run the udemy enroller script
@@ -74,7 +80,7 @@ def run(
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
- max_pages
+ max_pages,
)
@@ -150,10 +156,7 @@ def main():
discudemy_enabled,
coursevania_enabled,
) = determine_if_scraper_enabled(
- args.freebiesglobal,
- args.tutorialbar,
- args.discudemy,
- args.coursevania
+ args.freebiesglobal, args.tutorialbar, args.discudemy, args.coursevania
)
run(
freebiesglobal_enabled,
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index 23b11e0..16bc785 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -10,14 +10,14 @@
def _redeem_courses(
- settings: Settings,
- scrapers: ScraperManager,
- counter_enroled: int = 0,
- counter_already_enroled: int = 0,
- counter_expired: int = 0,
- counter_other_languages: int = 0,
- counter_other_categories: int = 0,
- counter_total: int = 0
+ settings: Settings,
+ scrapers: ScraperManager,
+ counter_enroled: int = 0,
+ counter_already_enroled: int = 0,
+ counter_expired: int = 0,
+ counter_other_languages: int = 0,
+ counter_other_categories: int = 0,
+ counter_total: int = 0,
) -> None:
"""
Method to scrape courses from tutorialbar.com and enroll in them on udemy
@@ -73,14 +73,20 @@ def _redeem_courses(
logger.info("\t# RESULTS #")
logger.info("\t####################################")
logger.info(f"\t# New Enrolled Courses: {counter_enroled:04} #")
- logger.info(f"\t# Already Enrolled Courses: {counter_already_enroled:04} #")
+ logger.info(
+ f"\t# Already Enrolled Courses: {counter_already_enroled:04} #"
+ )
logger.info(f"\t# Expired: {counter_expired:04} #")
- logger.info(f"\t# Other Languages: {counter_other_languages:04} #")
- logger.info(f"\t# Other Categories: {counter_other_categories:04} #")
+ logger.info(
+ f"\t# Other Languages: {counter_other_languages:04} #"
+ )
+ logger.info(
+ f"\t# Other Categories: {counter_other_categories:04} #"
+ )
logger.info("\t####################################")
logger.info(f"\t# Total Courses Scraped: {counter_total:04} #")
logger.info("\t####################################")
-
+
udemy_actions.stats.table()
logger.info("All scrapers complete")
@@ -88,12 +94,12 @@ def _redeem_courses(
def redeem_courses(
- settings: Settings,
- freebiesglobal_enabled: bool,
- tutorialbar_enabled: bool,
- discudemy_enabled: bool,
- coursevania_enabled: bool,
- max_pages: Union[int, None],
+ settings: Settings,
+ freebiesglobal_enabled: bool,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
+ max_pages: Union[int, None],
) -> None:
"""
Wrapper of _redeem_courses so we always close browser on completion
@@ -112,7 +118,7 @@ def redeem_courses(
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
- max_pages
+ max_pages,
)
_redeem_courses(settings, scrapers)
except Exception as e:
diff --git a/udemy_enroller/scrapers/freebiesglobal.py b/udemy_enroller/scrapers/freebiesglobal.py
index aeef783..f95cd7f 100644
--- a/udemy_enroller/scrapers/freebiesglobal.py
+++ b/udemy_enroller/scrapers/freebiesglobal.py
@@ -46,10 +46,14 @@ async def get_links(self) -> List:
"""
freebiesglobal_links = []
self.current_page += 1
- coupons_data = await get(f"{self.DOMAIN}/dealstore/udemy/page/{self.current_page}")
+ coupons_data = await get(
+ f"{self.DOMAIN}/dealstore/udemy/page/{self.current_page}"
+ )
soup = BeautifulSoup(coupons_data.decode("utf-8"), "html.parser")
- for course_card in soup.find_all("a", class_="img-centered-flex rh-flex-center-align rh-flex-justify-center"):
+ for course_card in soup.find_all(
+ "a", class_="img-centered-flex rh-flex-center-align rh-flex-justify-center"
+ ):
url_end = course_card["href"].split("/")[-1]
freebiesglobal_links.append(f"{self.DOMAIN}/{url_end}")
diff --git a/udemy_enroller/scrapers/manager.py b/udemy_enroller/scrapers/manager.py
index 046c564..aa48b27 100644
--- a/udemy_enroller/scrapers/manager.py
+++ b/udemy_enroller/scrapers/manager.py
@@ -11,11 +11,11 @@
class ScraperManager:
def __init__(
self,
- freebiesglobal_enabled,
- tutorialbar_enabled,
- discudemy_enabled,
- coursevania_enabled,
- max_pages
+ freebiesglobal_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages,
):
self.freebiesglobal_scraper = FreebiesglobalScraper(
freebiesglobal_enabled, max_pages=max_pages
diff --git a/udemy_enroller/udemy.py b/udemy_enroller/udemy.py
index c73fd5f..14db34a 100644
--- a/udemy_enroller/udemy.py
+++ b/udemy_enroller/udemy.py
@@ -98,7 +98,7 @@ class UdemyActions:
HEADERS = {
"origin": "https://www.udemy.com",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 "
- "Safari/537.36",
+ "Safari/537.36",
"accept": "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br",
"content-type": "application/json;charset=UTF-8",
@@ -287,9 +287,9 @@ def is_coupon_valid(
)
coupon_valid = False
if not bool(
- coupon_details["price_text"]["data"]["pricing_result"]["list_price"][
- "amount"
- ]
+ coupon_details["price_text"]["data"]["pricing_result"]["list_price"][
+ "amount"
+ ]
):
logger.debug(f"Skipping course '{course_identifier}' as it is always FREE")
coupon_valid = False
@@ -334,9 +334,9 @@ def is_preferred_category(
is_preferred_category = True
if (
- course_details["primary_category"]["title"] not in self.settings.categories
- and course_details["primary_subcategory"]["title"]
- not in self.settings.categories
+ course_details["primary_category"]["title"] not in self.settings.categories
+ and course_details["primary_subcategory"]["title"]
+ not in self.settings.categories
):
logger.debug(
f"Skipping course '{course_identifier}' as it does not have a wanted category"
@@ -393,7 +393,9 @@ def enroll(self, course_link: str) -> str:
if self.is_enrolled(course_id):
self.counter_already_enroled += 1
- logger.info(f"Already enrolled in: {course_identifier} --> {self.counter_already_enroled}")
+ logger.info(
+ f"Already enrolled in: {course_identifier} --> {self.counter_already_enroled}"
+ )
return UdemyStatus.ALREADY_ENROLLED.value
@@ -434,11 +436,11 @@ def _get_course_id(self, url: str) -> int:
return int(soup.find("body")["data-clp-course-id"])
def _checkout(
- self,
- course_id: int,
- coupon_code: str,
- course_identifier: str,
- retry: bool = False
+ self,
+ course_id: int,
+ coupon_code: str,
+ course_identifier: str,
+ retry: bool = False,
) -> str:
"""
Checkout process for the course and coupon provided
@@ -468,7 +470,9 @@ def _checkout(
if result["status"] == "succeeded":
self.counter_enroled += 1
- logger.info(f"Successfully enrolled: {course_identifier} --> {self.counter_enroled}")
+ logger.info(
+ f"Successfully enrolled: {course_identifier} --> {self.counter_enroled}"
+ )
self._add_enrolled_course(course_id)
self.stats.enrolled += 1
From 1db2d6f4c230e99f64eef759aed2d5b94e8a1c8a Mon Sep 17 00:00:00 2001
From: "Restyled.io"
Date: Tue, 14 Sep 2021 20:25:51 +0000
Subject: [PATCH 07/19] style(isort): apply code style
---
udemy_enroller/scrapers/manager.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/udemy_enroller/scrapers/manager.py b/udemy_enroller/scrapers/manager.py
index aa48b27..d03e467 100644
--- a/udemy_enroller/scrapers/manager.py
+++ b/udemy_enroller/scrapers/manager.py
@@ -2,9 +2,9 @@
from functools import reduce
from typing import List
-from udemy_enroller.scrapers.freebiesglobal import FreebiesglobalScraper
from udemy_enroller.scrapers.coursevania import CoursevaniaScraper
from udemy_enroller.scrapers.discudemy import DiscUdemyScraper
+from udemy_enroller.scrapers.freebiesglobal import FreebiesglobalScraper
from udemy_enroller.scrapers.tutorialbar import TutorialBarScraper
From 5a25dd4840ffb920b03ecb074f210471a6fe47ab Mon Sep 17 00:00:00 2001
From: Sayedul Sayem
Date: Sun, 3 Oct 2021 11:34:04 +0600
Subject: [PATCH 08/19] Fix Incorrect video replaced in README file
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index e229b60..285eded 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Also, don't forget to **Fork & Star the repository if you like it!**
**_Video Proof:_**
-[![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/IW8CCtv2k2A/0.jpg)](https://www.youtube.com/watch?v=IW8CCtv2k2A "GET PAID UDEMY Courses for FREE, Automatically with this Python Script!")
+[![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/tdLsVoraMxw/0.jpg)](https://www.youtube.com/watch?v=tdLsVoraMxw "GET Udemy Courses for FREE with Python | 2 Minute Tuesday")
---
From 7477800b878ea58be7af936a594f214cdfade24c Mon Sep 17 00:00:00 2001
From: cullzie
Date: Mon, 4 Oct 2021 13:21:59 +0100
Subject: [PATCH 09/19] Removing un-needed statistics duplication
---
README.md | 27 +++++++------
udemy_enroller/cli.py | 2 +-
udemy_enroller/runner.py | 47 ++---------------------
udemy_enroller/scrapers/freebiesglobal.py | 2 +-
udemy_enroller/udemy.py | 27 +++----------
5 files changed, 23 insertions(+), 82 deletions(-)
diff --git a/README.md b/README.md
index 7657c9f..426e7d3 100644
--- a/README.md
+++ b/README.md
@@ -96,27 +96,26 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
- `udemy_enroller`
4 . The bot starts scraping the course links from the first **All Courses** page
-on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts
+on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all), [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts
enrolling you to Udemy courses. After it has enrolled you to courses from the
first page, it then moves to the next site page and the cycle continues.
- Stop the script by pressing ctrl+c in terminal to stop the enrollment process.
-5. _[New]_ After each scrap how many courses was found is shown.
+5 . _[New]_ At the end of process a detailed result is shown:
-Total courses this time: 54
+```
+================== Run Statistics ==================
-6. _[New]_ At the end of process a detailed result is shown:
-
-
- RESULTS
-
- New Enrolled Courses: 0000
- Already Enrolled Courses: 0204
- Expired: 0003
- Other Languages: 0021
- Other Categories 0081
- Total Courses Scraped: 0309
+Enrolled: 56
+Unwanted Category: 0
+Unwanted Language: 1
+Already Claimed: 93
+Expired: 7
+Total Enrolments: 1705
+Savings: โฌ2674.44
+================== Run Statistics ==================
+```
---
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index a7db228..37d931b 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -27,7 +27,7 @@ def determine_if_scraper_enabled(
tutorialbar_enabled: bool,
discudemy_enabled: bool,
coursevania_enabled: bool,
-) -> tuple[bool, bool, bool, bool]:
+) -> Tuple[bool, bool, bool, bool]:
"""
Determine what scrapers should be enabled and disabled
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index 16bc785..3275173 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -9,18 +9,9 @@
logger = get_logger()
-def _redeem_courses(
- settings: Settings,
- scrapers: ScraperManager,
- counter_enroled: int = 0,
- counter_already_enroled: int = 0,
- counter_expired: int = 0,
- counter_other_languages: int = 0,
- counter_other_categories: int = 0,
- counter_total: int = 0,
-) -> None:
+def _redeem_courses(settings: Settings, scrapers: ScraperManager) -> None:
"""
- Method to scrape courses from tutorialbar.com and enroll in them on udemy
+ Method to scrape courses from the supported sites and enroll in them on udemy
:param Settings settings: Core settings used for Udemy
:param ScraperManager scrapers:
@@ -35,27 +26,16 @@ def _redeem_courses(
logger.info(f"Total courses this time: {len(udemy_course_links)}")
if udemy_course_links:
for course_link in udemy_course_links:
- counter_total += 1
try:
status = udemy_actions.enroll(course_link)
if status == UdemyStatus.ENROLLED.value:
- counter_enroled += 1
# Try to avoid udemy throttling by sleeping for 1-5 seconds
sleep_time = random.choice(range(1, 5))
logger.debug(
f"Sleeping for {sleep_time} seconds between enrolments"
)
time.sleep(sleep_time)
- elif status == UdemyStatus.ALREADY_ENROLLED.value:
- counter_already_enroled += 1
- elif status == UdemyStatus.EXPIRED.value:
- counter_expired += 1
- elif status == UdemyStatus.UNWANTED_LANGUAGE.value:
- counter_other_languages += 1
- elif status == UdemyStatus.UNWANTED_CATEGORY.value:
- counter_other_categories += 1
-
except KeyboardInterrupt:
logger.error("Exiting the script")
return
@@ -66,30 +46,9 @@ def _redeem_courses(
logger.info("We have attempted to subscribe to 1 udemy course")
logger.info("Ending test")
return
-
else:
- logger.info("All scrapers complete\n\n")
- logger.info("\t####################################")
- logger.info("\t# RESULTS #")
- logger.info("\t####################################")
- logger.info(f"\t# New Enrolled Courses: {counter_enroled:04} #")
- logger.info(
- f"\t# Already Enrolled Courses: {counter_already_enroled:04} #"
- )
- logger.info(f"\t# Expired: {counter_expired:04} #")
- logger.info(
- f"\t# Other Languages: {counter_other_languages:04} #"
- )
- logger.info(
- f"\t# Other Categories: {counter_other_categories:04} #"
- )
- logger.info("\t####################################")
- logger.info(f"\t# Total Courses Scraped: {counter_total:04} #")
- logger.info("\t####################################")
-
udemy_actions.stats.table()
logger.info("All scrapers complete")
-
return
@@ -102,7 +61,7 @@ def redeem_courses(
max_pages: Union[int, None],
) -> None:
"""
- Wrapper of _redeem_courses so we always close browser on completion
+ Wrapper of _redeem_courses which catches unhandled exceptions
:param Settings settings: Core settings used for Udemy
:param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run
diff --git a/udemy_enroller/scrapers/freebiesglobal.py b/udemy_enroller/scrapers/freebiesglobal.py
index f95cd7f..a911696 100644
--- a/udemy_enroller/scrapers/freebiesglobal.py
+++ b/udemy_enroller/scrapers/freebiesglobal.py
@@ -87,7 +87,7 @@ async def gather_udemy_course_links(self, courses: List[str]):
"""
Async fetching of the udemy course links from freebiesglobal.com
- :param list courses: A list of discudemy.com course links we want to fetch the udemy links for
+ :param list courses: A list of freebiesglobal.com course links we want to fetch the udemy links for
:return: list of udemy links
"""
return [
diff --git a/udemy_enroller/udemy.py b/udemy_enroller/udemy.py
index 14db34a..25df61d 100644
--- a/udemy_enroller/udemy.py
+++ b/udemy_enroller/udemy.py
@@ -52,7 +52,7 @@ def savings(self):
return sum(self.prices) or 0
def table(self):
- logger.info("==================Run Statistics==================")
+ logger.info("================== Run Statistics ==================")
logger.info(f"Enrolled: {self.enrolled}")
logger.info(f"Unwanted Category: {self.unwanted_category}")
logger.info(f"Unwanted Language: {self.unwanted_language}")
@@ -62,7 +62,7 @@ def table(self):
logger.info(
f"Savings: {self.currency_symbol}{self.savings():.2f}"
)
- logger.info("==================Run Statistics==================")
+ logger.info("================== Run Statistics ==================")
class UdemyStatus(Enum):
@@ -118,9 +118,6 @@ def __init__(self, settings: Settings, cookie_file_name: str = ".cookie"):
self._currency_symbol = None
self._currency = None
- self.counter_enroled: int = 0
- self.counter_already_enroled: int = 0
-
self.stats = RunStatistics()
def login(self, retry=False) -> None:
@@ -230,11 +227,6 @@ def load_my_courses(self) -> List:
all_courses.extend(my_courses["results"])
time.sleep(1)
logger.info(f"Currently enrolled in {len(all_courses)} courses")
-
- # for counter, course in enumerate(all_courses):
- # with open("Courses.txt", "a") as file:
- # file.write(f"{counter}\t==>\t{course[counter]}\n")
-
return all_courses
@format_requests
@@ -391,12 +383,8 @@ def enroll(self, course_link: str) -> str:
course_identifier = course_details.get("title", url)
if self.is_enrolled(course_id):
- self.counter_already_enroled += 1
-
- logger.info(
- f"Already enrolled in: {course_identifier} --> {self.counter_already_enroled}"
- )
-
+ logger.info(f"Already enrolled in: '{course_identifier}'")
+ self.stats.already_enrolled += 1
return UdemyStatus.ALREADY_ENROLLED.value
if self.user_has_preferences:
@@ -468,12 +456,7 @@ def _checkout(
else:
result = checkout_result.json()
if result["status"] == "succeeded":
-
- self.counter_enroled += 1
- logger.info(
- f"Successfully enrolled: {course_identifier} --> {self.counter_enroled}"
- )
-
+ logger.info(f"Successfully enrolled: '{course_identifier}'")
self._add_enrolled_course(course_id)
self.stats.enrolled += 1
return UdemyStatus.ENROLLED.value
From d7eed6fd20feae556e8c2071e3389934edc591b8 Mon Sep 17 00:00:00 2001
From: cullzie
Date: Tue, 5 Oct 2021 13:26:46 +0100
Subject: [PATCH 10/19] Add delete-cookie option
---
README.md | 1 +
tests/core/test_settings.py | 8 ++++----
udemy_enroller/cli.py | 12 +++++++++++-
udemy_enroller/settings.py | 23 ++++++++++++++++++++---
udemy_enroller/udemy.py | 9 ++++++---
5 files changed, 42 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 426e7d3..c4cfbdf 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,7 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
- `--freebiesglobal`: Run the freebiesglobal scraper only _[New]_
- `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5)
- `--delete-settings`: Delete existing settings file
+- `--delete-cookie`: Delete the cookie file if it exists _[New]_
- `--debug`: Enable debug logging
diff --git a/tests/core/test_settings.py b/tests/core/test_settings.py
index d0f2056..417bf24 100644
--- a/tests/core/test_settings.py
+++ b/tests/core/test_settings.py
@@ -82,7 +82,7 @@ def test_settings(
):
with mock.patch("getpass.getpass", return_value=password):
settings_path = os.path.join(get_app_dir(), f"test_tmp/{file_name}")
- settings = Settings(False, settings_path)
+ settings = Settings(settings_path=settings_path)
assert settings.email == email
assert settings.password == password
assert settings.zip_code == zip_code
@@ -114,7 +114,7 @@ def test_settings(
else categories
)
# Load settings just created
- Settings(False, settings_path)
+ Settings(settings_path=settings_path)
@pytest.mark.parametrize(
@@ -224,10 +224,10 @@ def test_load_existing_settings(
):
with mock.patch("getpass.getpass", return_value=password):
settings_path = f"test_tmp/{file_name}"
- Settings(False, settings_path)
+ Settings(settings_path=settings_path)
# Load existing settings
- settings = Settings(False, settings_path)
+ settings = Settings(settings_path=settings_path)
if should_save_email.upper() == "Y":
assert settings.email == email
else:
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index 37d931b..7e29147 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -62,6 +62,7 @@ def run(
coursevania_enabled: bool,
max_pages: Union[int, None],
delete_settings: bool,
+ delete_cookie: bool,
):
"""
Run the udemy enroller script
@@ -71,9 +72,10 @@ def run(
:param bool coursevania_enabled:
:param int max_pages: Max pages to scrape from sites (if pagination exists)
:param bool delete_settings: Determines if we should delete old settings file
+ :param bool delete_cookie: Determines if we should delete the cookie file
:return:
"""
- settings = Settings(delete_settings)
+ settings = Settings(delete_settings, delete_cookie)
redeem_courses(
settings,
freebiesglobal_enabled,
@@ -134,6 +136,13 @@ def parse_args() -> Namespace:
help="Delete any existing settings file",
)
+ parser.add_argument(
+ "--delete-cookie",
+ action="store_true",
+ default=False,
+ help="Delete existing cookie file",
+ )
+
parser.add_argument(
"--debug",
action="store_true",
@@ -165,4 +174,5 @@ def main():
coursevania_enabled,
args.max_pages,
args.delete_settings,
+ args.delete_cookie,
)
diff --git a/udemy_enroller/settings.py b/udemy_enroller/settings.py
index 987a9a4..747d023 100644
--- a/udemy_enroller/settings.py
+++ b/udemy_enroller/settings.py
@@ -16,7 +16,9 @@ class Settings:
Contains all logic related to the scripts settings
"""
- def __init__(self, delete_settings, settings_path="settings.yaml"):
+ def __init__(
+ self, delete_settings=False, delete_cookie=False, settings_path="settings.yaml"
+ ):
self.email = None
self.password = None
self.zip_code = None
@@ -24,11 +26,14 @@ def __init__(self, delete_settings, settings_path="settings.yaml"):
self.categories = []
self._settings_path = os.path.join(get_app_dir(), settings_path)
+ self._cookies_path = os.path.join(get_app_dir(), ".cookie")
self._should_store_email = False
self._should_store_password = False
self.is_ci_build = strtobool(os.environ.get("CI_TEST", "False"))
if delete_settings:
- self.delete()
+ self.delete_settings()
+ if delete_cookie:
+ self.delete_cookie()
self._init_settings()
def _init_settings(self) -> None:
@@ -193,7 +198,7 @@ def _save_settings(self) -> None:
"You will be prompted to enter your email/password again when the cookie expires"
)
- def delete(self) -> None:
+ def delete_settings(self) -> None:
"""
Delete the settings file
@@ -209,6 +214,18 @@ def delete(self) -> None:
else:
logger.info("No settings to delete")
+ def delete_cookie(self) -> None:
+ """
+ Delete the cookie file
+
+ :return: None
+ """
+ if os.path.isfile(self._cookies_path):
+ os.remove(self._cookies_path)
+ logger.info(f"Cookie file deleted: {self._cookies_path}")
+ else:
+ logger.info("No cookie file to delete")
+
def prompt_email(self) -> None:
"""
Prompt for Udemy email only. Does not prompt for saving
diff --git a/udemy_enroller/udemy.py b/udemy_enroller/udemy.py
index 25df61d..8e7cd6d 100644
--- a/udemy_enroller/udemy.py
+++ b/udemy_enroller/udemy.py
@@ -497,7 +497,7 @@ def _cache_cookies(self, cookies: Dict) -> None:
:param cookies:
:return:
"""
- logger.info("Caching cookies for future use")
+ logger.info("Caching cookie for future use")
with open(self._cookie_file, "a+") as f:
f.write(json.dumps(cookies))
@@ -508,10 +508,13 @@ def _load_cookies(self) -> Dict:
:return:
"""
cookies = None
- logger.info("Loading cookies from file")
+
if os.path.isfile(self._cookie_file):
+ logger.info("Loading cookie from file")
with open(self._cookie_file) as f:
cookies = json.loads(f.read())
+ else:
+ logger.info("No cookie available")
return cookies
def _delete_cookies(self) -> None:
@@ -520,5 +523,5 @@ def _delete_cookies(self) -> None:
:return:
"""
- logger.info("Deleting cookies")
+ logger.info("Deleting cookie")
os.remove(self._cookie_file)
From 96ec36cdc7d7d8c5e337858cbc3c53caa6258a0a Mon Sep 17 00:00:00 2001
From: cullzie
Date: Mon, 18 Oct 2021 15:55:05 +0100
Subject: [PATCH 11/19] Adding selenium enrollment
---
pyproject.toml | 2 +
requirements.txt | 2 +
tests/core/test_driver_manager.py | 120 ++++++++
udemy_enroller/__init__.py | 4 +-
udemy_enroller/cli.py | 35 ++-
udemy_enroller/driver_manager.py | 102 +++++++
udemy_enroller/runner.py | 85 +++++-
udemy_enroller/{udemy.py => udemy_rest.py} | 0
udemy_enroller/udemy_ui.py | 321 +++++++++++++++++++++
9 files changed, 664 insertions(+), 7 deletions(-)
create mode 100644 tests/core/test_driver_manager.py
create mode 100644 udemy_enroller/driver_manager.py
rename udemy_enroller/{udemy.py => udemy_rest.py} (100%)
create mode 100644 udemy_enroller/udemy_ui.py
diff --git a/pyproject.toml b/pyproject.toml
index 317e5aa..178b8b5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,10 +6,12 @@ authors = [""]
[tool.poetry.dependencies]
python = "^3.8"
+selenium = "^3.141.0"
beautifulsoup4 = "^4.9.3"
"ruamel.yaml" = "^0.16.12"
cloudscraper = "^1.2.56"
requests = "^2.25.1"
+webdriver-manager = "^3.2.2"
aiohttp = {extras = ["speedups"], version = "^3.7.3"}
[tool.poetry.dev-dependencies]
diff --git a/requirements.txt b/requirements.txt
index 3f150d0..0711b79 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,5 @@ beautifulsoup4
ruamel.yaml
requests
cloudscraper
+webdriver-manager
+selenium
diff --git a/tests/core/test_driver_manager.py b/tests/core/test_driver_manager.py
new file mode 100644
index 0000000..35ec783
--- /dev/null
+++ b/tests/core/test_driver_manager.py
@@ -0,0 +1,120 @@
+from unittest import mock
+
+import pytest
+
+from udemy_enroller import DriverManager
+from udemy_enroller.driver_manager import (
+ ALL_VALID_BROWSER_STRINGS,
+ VALID_EDGE_STRINGS,
+ VALID_FIREFOX_STRINGS,
+ VALID_INTERNET_EXPLORER_STRINGS,
+ VALID_OPERA_STRINGS,
+)
+
+
+@pytest.mark.parametrize(
+ "browser_name",
+ [
+ ("chrome"),
+ ("chromium"),
+ ("edge"),
+ ("firefox"),
+ ("opera"),
+ ("internet_explorer"),
+ ("tor"),
+ ],
+ ids=(
+ "create driver chrome",
+ "create driver chromium",
+ "create driver edge",
+ "create driver firefox",
+ "create driver opera",
+ "create driver internet_explorer",
+ "unsupported browser",
+ ),
+)
+@mock.patch("udemy_enroller.driver_manager.webdriver")
+@mock.patch("udemy_enroller.driver_manager.ChromeDriverManager")
+@mock.patch("udemy_enroller.driver_manager.GeckoDriverManager")
+@mock.patch("udemy_enroller.driver_manager.EdgeChromiumDriverManager")
+@mock.patch("udemy_enroller.driver_manager.IEDriverManager")
+@mock.patch("udemy_enroller.driver_manager.OperaDriverManager")
+@mock.patch("udemy_enroller.driver_manager.ChromeType")
+def test_driver_manager_init(
+ _,
+ mock_opera_driver_manager,
+ mock_internet_explorer_driver_manager,
+ mock_edge_driver_manager,
+ mock_firefox_driver_manager,
+ mock_chrome_driver_manager,
+ mock_selenium_web_driver,
+ browser_name,
+):
+ try:
+ dm = DriverManager(browser_name)
+ except ValueError:
+ assert browser_name not in ALL_VALID_BROWSER_STRINGS
+ else:
+ if browser_name in ("chrome",):
+ mock_selenium_web_driver.Chrome.assert_called_once_with(
+ mock_chrome_driver_manager().install(), options=None
+ )
+ assert dm.driver == mock_selenium_web_driver.Chrome()
+ elif browser_name in ("chromium",):
+ mock_selenium_web_driver.Chrome.assert_called_once_with(
+ mock_chrome_driver_manager().install()
+ )
+ assert dm.driver == mock_selenium_web_driver.Chrome()
+ elif browser_name in VALID_FIREFOX_STRINGS:
+ mock_selenium_web_driver.Firefox.assert_called_once_with(
+ executable_path=mock_firefox_driver_manager().install()
+ )
+ assert dm.driver == mock_selenium_web_driver.Firefox()
+ elif browser_name in VALID_OPERA_STRINGS:
+ mock_selenium_web_driver.Opera.assert_called_once_with(
+ executable_path=mock_opera_driver_manager().install()
+ )
+ assert dm.driver == mock_selenium_web_driver.Opera()
+ elif browser_name in VALID_EDGE_STRINGS:
+ mock_selenium_web_driver.Edge.assert_called_once_with(
+ mock_edge_driver_manager().install()
+ )
+ assert dm.driver == mock_selenium_web_driver.Edge()
+ elif browser_name in VALID_INTERNET_EXPLORER_STRINGS:
+ mock_selenium_web_driver.Ie.assert_called_once_with(
+ mock_internet_explorer_driver_manager().install()
+ )
+ assert dm.driver == mock_selenium_web_driver.Ie()
+
+
+@pytest.mark.parametrize(
+ "browser_name,is_ci_build",
+ [
+ ("chrome", True),
+ ("chrome", False),
+ ],
+ ids=("chrome is ci build", "chrome is not ci build"),
+)
+@mock.patch("udemy_enroller.driver_manager.webdriver")
+@mock.patch("udemy_enroller.driver_manager.ChromeOptions")
+@mock.patch("udemy_enroller.driver_manager.ChromeDriverManager")
+@mock.patch("udemy_enroller.driver_manager.ChromeType")
+def test_driver_manager_ci_build(
+ _,
+ mock_chrome_driver_manager,
+ mock_chrome_options,
+ mock_selenium_web_driver,
+ browser_name,
+ is_ci_build,
+):
+
+ dm = DriverManager(browser_name, is_ci_build=is_ci_build)
+
+ if is_ci_build:
+ options = mock_chrome_options()
+ else:
+ options = None
+ mock_selenium_web_driver.Chrome.assert_called_once_with(
+ mock_chrome_driver_manager().install(), options=options
+ )
+ assert dm.driver == mock_selenium_web_driver.Chrome()
diff --git a/udemy_enroller/__init__.py b/udemy_enroller/__init__.py
index f86f828..7a53447 100644
--- a/udemy_enroller/__init__.py
+++ b/udemy_enroller/__init__.py
@@ -1,6 +1,8 @@
+from .driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager
from .logging import load_logging_config
from .scrapers.manager import ScraperManager
from .settings import Settings
-from .udemy import UdemyActions, UdemyStatus
+from .udemy_rest import UdemyActions, UdemyStatus
+from .udemy_ui import UdemyActionsUI
load_logging_config()
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index 8f5f308..b2a7a11 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -3,9 +3,9 @@
from argparse import Namespace
from typing import Tuple, Union
-from udemy_enroller import Settings
+from udemy_enroller import ALL_VALID_BROWSER_STRINGS, DriverManager, Settings
from udemy_enroller.logging import get_logger
-from udemy_enroller.runner import redeem_courses
+from udemy_enroller.runner import redeem_courses, redeem_courses_ui
logger = get_logger()
@@ -39,6 +39,7 @@ def determine_if_scraper_enabled(
def run(
+ browser: str,
tutorialbar_enabled: bool,
discudemy_enabled: bool,
coursevania_enabled: bool,
@@ -48,6 +49,7 @@ def run(
"""
Run the udemy enroller script
+ :param str browser: Name of the browser we want to create a driver for
:param bool tutorialbar_enabled:
:param bool discudemy_enabled:
:param bool coursevania_enabled:
@@ -56,9 +58,24 @@ def run(
:return:
"""
settings = Settings(delete_settings)
- redeem_courses(
- settings, tutorialbar_enabled, discudemy_enabled, coursevania_enabled, max_pages
- )
+ if browser:
+ dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build)
+ redeem_courses_ui(
+ dm.driver,
+ settings,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages,
+ )
+ else:
+ redeem_courses(
+ settings,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages,
+ )
def parse_args() -> Namespace:
@@ -69,6 +86,13 @@ def parse_args() -> Namespace:
"""
parser = argparse.ArgumentParser(description="Udemy Enroller")
+ parser.add_argument(
+ "--browser",
+ type=str,
+ default=None,
+ choices=ALL_VALID_BROWSER_STRINGS,
+ help="Browser to use for Udemy Enroller",
+ )
parser.add_argument(
"--tutorialbar",
action="store_true",
@@ -123,6 +147,7 @@ def main():
args.tutorialbar, args.discudemy, args.coursevania
)
run(
+ args.browser,
tutorialbar_enabled,
discudemy_enabled,
coursevania_enabled,
diff --git a/udemy_enroller/driver_manager.py b/udemy_enroller/driver_manager.py
new file mode 100644
index 0000000..f97a873
--- /dev/null
+++ b/udemy_enroller/driver_manager.py
@@ -0,0 +1,102 @@
+from selenium import webdriver
+from selenium.webdriver.chrome.options import Options as ChromeOptions
+from webdriver_manager.chrome import ChromeDriverManager
+from webdriver_manager.firefox import GeckoDriverManager
+from webdriver_manager.microsoft import EdgeChromiumDriverManager, IEDriverManager
+from webdriver_manager.opera import OperaDriverManager
+from webdriver_manager.utils import ChromeType
+
+from udemy_enroller.logging import get_logger
+
+logger = get_logger()
+
+VALID_FIREFOX_STRINGS = {"ff", "firefox"}
+VALID_CHROME_STRINGS = {"chrome", "google-chrome"}
+VALID_CHROMIUM_STRINGS = {"chromium"}
+VALID_INTERNET_EXPLORER_STRINGS = {"internet_explorer", "ie"}
+VALID_OPERA_STRINGS = {"opera"}
+VALID_EDGE_STRINGS = {"edge"}
+
+ALL_VALID_BROWSER_STRINGS = (
+ VALID_FIREFOX_STRINGS.union(VALID_CHROME_STRINGS)
+ .union(VALID_CHROMIUM_STRINGS)
+ .union(VALID_CHROMIUM_STRINGS)
+ .union(VALID_INTERNET_EXPLORER_STRINGS)
+ .union(VALID_OPERA_STRINGS)
+ .union(VALID_EDGE_STRINGS)
+)
+
+
+class DriverManager:
+ def __init__(self, browser: str, is_ci_build: bool = False):
+ self.driver = None
+ self.options = None
+ self.browser = browser
+ self.is_ci_build = is_ci_build
+ self._init_driver()
+
+ def _init_driver(self):
+ """
+ Initialize the correct web driver based on the users requested browser
+
+ :return: None
+ """
+
+ if self.browser.lower() in VALID_CHROME_STRINGS:
+ if self.is_ci_build:
+ self.options = self._build_ci_options_chrome()
+
+ self.driver = webdriver.Chrome(
+ ChromeDriverManager().install(), options=self.options
+ )
+ elif self.browser.lower() in VALID_CHROMIUM_STRINGS:
+ self.driver = webdriver.Chrome(
+ ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()
+ )
+ elif self.browser.lower() in VALID_EDGE_STRINGS:
+ self.driver = webdriver.Edge(EdgeChromiumDriverManager().install())
+ elif self.browser.lower() in VALID_FIREFOX_STRINGS:
+ self.driver = webdriver.Firefox(
+ executable_path=GeckoDriverManager().install()
+ )
+ elif self.browser.lower() in VALID_OPERA_STRINGS:
+ self.driver = webdriver.Opera(
+ executable_path=OperaDriverManager().install()
+ )
+ elif self.browser.lower() in VALID_INTERNET_EXPLORER_STRINGS:
+ self.driver = webdriver.Ie(IEDriverManager().install())
+ else:
+ raise ValueError("No matching browser found")
+
+ # Get around captcha
+ self.driver.execute_cdp_cmd(
+ "Page.addScriptToEvaluateOnNewDocument",
+ {
+ "source": "const newProto = navigator.__proto__;"
+ "delete newProto.webdriver;"
+ "navigator.__proto__ = newProto;"
+ },
+ )
+ # Maximize the browser
+ self.driver.maximize_window()
+
+ @staticmethod
+ def _build_ci_options_chrome():
+ """
+ Build chrome options required to run in CI
+
+ :return:
+ """
+ # Having the user-agent with Headless param was always leading to robot check
+ user_agent = (
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 "
+ "Safari/537.36"
+ )
+ options = ChromeOptions()
+ # We need to run headless when using github CI
+ options.add_argument("--headless")
+ options.add_argument("user-agent={0}".format(user_agent))
+ options.add_argument("accept-language=en-GB,en-US;q=0.9,en;q=0.8")
+ options.add_argument("--window-size=1325x744")
+ logger.info("This is a CI run")
+ return options
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index 10e9270..32c4926 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -3,7 +3,13 @@
import time
from typing import Union
-from udemy_enroller import ScraperManager, Settings, UdemyActions, UdemyStatus
+from udemy_enroller import (
+ ScraperManager,
+ Settings,
+ UdemyActions,
+ UdemyActionsUI,
+ UdemyStatus,
+)
from udemy_enroller.logging import get_logger
logger = get_logger()
@@ -78,3 +84,80 @@ def redeem_courses(
_redeem_courses(settings, scrapers)
except Exception as e:
logger.error(f"Exception in redeem courses: {e}")
+
+
+def _redeem_courses_ui(
+ driver,
+ settings: Settings,
+ scrapers: ScraperManager,
+) -> None:
+ """
+ Method to scrape courses from tutorialbar.com and enroll in them on udemy
+
+ :param Settings settings: Core settings used for Udemy
+ :param ScraperManager scrapers:
+ :return:
+ """
+ udemy_actions = UdemyActionsUI(driver, settings)
+ udemy_actions.login()
+ loop = asyncio.get_event_loop()
+
+ while True:
+ udemy_course_links = loop.run_until_complete(scrapers.run())
+
+ if udemy_course_links:
+ for course_link in udemy_course_links:
+ try:
+ status = udemy_actions.enroll(course_link)
+ if status == UdemyStatus.ENROLLED.value:
+ # Try to avoid udemy throttling by sleeping for 1-5 seconds
+ sleep_time = random.choice(range(1, 5))
+ logger.debug(
+ f"Sleeping for {sleep_time} seconds between enrolments"
+ )
+ time.sleep(sleep_time)
+ except KeyboardInterrupt:
+ logger.error("Exiting the script")
+ return
+ except Exception as e:
+ logger.error(f"Unexpected exception: {e}")
+ finally:
+ if settings.is_ci_build:
+ logger.info("We have attempted to subscribe to 1 udemy course")
+ logger.info("Ending test")
+ return
+ else:
+ udemy_actions.stats.table()
+ logger.info("All scrapers complete")
+ return
+
+
+def redeem_courses_ui(
+ driver,
+ settings: Settings,
+ tutorialbar_enabled: bool,
+ discudemy_enabled: bool,
+ coursevania_enabled: bool,
+ max_pages: Union[int, None],
+) -> None:
+ """
+ Wrapper of _redeem_courses so we always close browser on completion
+
+ :param Settings settings: Core settings used for Udemy
+ :param bool tutorialbar_enabled: Boolean signifying if tutorialbar scraper should run
+ :param bool discudemy_enabled: Boolean signifying if discudemy scraper should run
+ :param bool coursevania_enabled: Boolean signifying if coursevania scraper should run
+ :param int max_pages: Max pages to scrape from sites (if pagination exists)
+ :return:
+ """
+
+ try:
+ scrapers = ScraperManager(
+ tutorialbar_enabled, discudemy_enabled, coursevania_enabled, max_pages
+ )
+ _redeem_courses_ui(driver, settings, scrapers)
+ except Exception as e:
+ logger.error(f"Exception in redeem courses: {e}")
+ finally:
+ logger.info("Closing browser")
+ driver.quit()
diff --git a/udemy_enroller/udemy.py b/udemy_enroller/udemy_rest.py
similarity index 100%
rename from udemy_enroller/udemy.py
rename to udemy_enroller/udemy_rest.py
diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py
new file mode 100644
index 0000000..26f2f1b
--- /dev/null
+++ b/udemy_enroller/udemy_ui.py
@@ -0,0 +1,321 @@
+import time
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import List
+
+from selenium.common.exceptions import NoSuchElementException, TimeoutException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.remote.webdriver import WebDriver, WebElement
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+from udemy_enroller.exceptions import LoginException, RobotException
+from udemy_enroller.logging import get_logger
+from udemy_enroller.settings import Settings
+
+logger = get_logger()
+
+
+@dataclass(unsafe_hash=True)
+class RunStatistics:
+ prices: List[float] = field(default_factory=list)
+
+ expired: int = 0
+ enrolled: int = 0
+ already_enrolled: int = 0
+ unwanted_language: int = 0
+ unwanted_category: int = 0
+
+ course_ids_start: int = 0
+ course_ids_end: int = 0
+
+ start_time = None
+ end_time = None
+
+ currency_symbol = "$"
+
+ def savings(self):
+ return sum(self.prices) or 0
+
+ def table(self):
+ logger.info("==================Run Statistics==================")
+ logger.info(f"Enrolled: {self.enrolled}")
+ logger.info(f"Unwanted Category: {self.unwanted_category}")
+ logger.info(f"Unwanted Language: {self.unwanted_language}")
+ logger.info(f"Already Claimed: {self.already_enrolled}")
+ logger.info(f"Expired: {self.expired}")
+ logger.info(f"Total Enrolments: {self.course_ids_end}")
+ logger.info(
+ f"Savings: {self.currency_symbol}{self.savings():.2f}"
+ )
+ logger.info("==================Run Statistics==================")
+
+
+class UdemyStatus(Enum):
+ """
+ Possible statuses of udemy course
+ """
+
+ ALREADY_ENROLLED = "ALREADY_ENROLLED"
+ ENROLLED = "ENROLLED"
+ EXPIRED = "EXPIRED"
+ UNWANTED_LANGUAGE = "UNWANTED_LANGUAGE"
+ UNWANTED_CATEGORY = "UNWANTED_CATEGORY"
+
+
+class UdemyActionsUI:
+ """
+ Contains any logic related to interacting with udemy website
+ """
+
+ DOMAIN = "https://www.udemy.com"
+
+ def __init__(self, driver: WebDriver, settings: Settings):
+ self.driver = driver
+ self.settings = settings
+ self.logged_in = False
+ self.stats = RunStatistics()
+
+ def login(self, is_retry=False) -> None:
+ """
+ Login to your udemy account
+
+ :param bool is_retry: Is this is a login retry and we still have captcha raise RobotException
+
+ :return: None
+ """
+ if not self.logged_in:
+ self.driver.get(f"{self.DOMAIN}/join/login-popup/")
+ try:
+ email_element = self.driver.find_element_by_name("email")
+ email_element.send_keys(self.settings.email)
+
+ password_element = self.driver.find_element_by_name("password")
+ password_element.send_keys(self.settings.password)
+
+ self.driver.find_element_by_name("submit").click()
+ except NoSuchElementException as e:
+ is_robot = self._check_if_robot()
+ if is_robot and not is_retry:
+ input(
+ "Before login. Please solve the captcha before proceeding. Hit enter once solved "
+ )
+ self.login(is_retry=True)
+ return
+ if is_robot and is_retry:
+ raise RobotException("I am a bot!")
+ raise e
+ else:
+ user_dropdown_xpath = "//a[@data-purpose='user-dropdown']"
+ try:
+ WebDriverWait(self.driver, 10).until(
+ EC.presence_of_element_located((By.XPATH, user_dropdown_xpath))
+ )
+ except TimeoutException:
+ is_robot = self._check_if_robot()
+ if is_robot and not is_retry:
+ input(
+ "After login. Please solve the captcha before proceeding. Hit enter once solved "
+ )
+ if self._check_if_robot():
+ raise RobotException("I am a bot!")
+ self.logged_in = True
+ return
+ raise LoginException("Udemy user failed to login")
+ self.logged_in = True
+
+ def enroll(self, url: str) -> str:
+ """
+ Redeems the course url passed in
+
+ :param str url: URL of the course to redeem
+ :return: A string detailing course status
+ """
+ self.driver.get(url)
+
+ course_name = self.driver.title
+
+ if not self._check_languages():
+ return UdemyStatus.UNWANTED_LANGUAGE.value
+
+ if not self._check_categories():
+ return UdemyStatus.UNWANTED_CATEGORY.value
+
+ # TODO: Make this depend on an element.
+ time.sleep(2)
+
+ # Enroll Now 1
+ buy_course_button_xpath = "//button[@data-purpose='buy-this-course-button']"
+ # We need to wait for this element to be clickable before checking if already purchased
+ WebDriverWait(self.driver, 10).until(
+ EC.element_to_be_clickable((By.XPATH, buy_course_button_xpath))
+ )
+
+ # Check if already enrolled. If add to cart is available we have not yet enrolled
+ if not self._check_enrolled(course_name):
+ element_present = EC.presence_of_element_located(
+ (By.XPATH, buy_course_button_xpath)
+ )
+ WebDriverWait(self.driver, 10).until(element_present).click()
+
+ # Enroll Now 2
+ enroll_button_xpath = (
+ "//div[contains(@class, 'styles--checkout-pane-outer')]//button"
+ )
+ element_present = EC.presence_of_element_located(
+ (
+ By.XPATH,
+ enroll_button_xpath,
+ )
+ )
+ WebDriverWait(self.driver, 10).until(element_present)
+
+ # Check if zipcode exists before doing this
+ if self.settings.zip_code:
+ # zipcode is only required in certain regions (e.g USA)
+ try:
+ element_present = EC.presence_of_element_located(
+ (
+ By.ID,
+ "billingAddressSecondaryInput",
+ )
+ )
+ WebDriverWait(self.driver, 5).until(element_present).send_keys(
+ self.settings.zip_code
+ )
+
+ # After you put the zip code in, the page refreshes itself and disables the enroll button for a split
+ # second.
+ enroll_button_is_clickable = EC.element_to_be_clickable(
+ (By.XPATH, enroll_button_xpath)
+ )
+ WebDriverWait(self.driver, 5).until(enroll_button_is_clickable)
+ except (TimeoutException, NoSuchElementException):
+ pass
+
+ # Make sure the price has loaded
+ price_class_loading = "udi-circle-loader"
+ WebDriverWait(self.driver, 10).until_not(
+ EC.presence_of_element_located((By.CLASS_NAME, price_class_loading))
+ )
+
+ # Make sure the course is Free
+ if not self._check_price(course_name):
+ return UdemyStatus.EXPIRED.value
+
+ # Check if state/province element exists
+ billing_state_element_id = "billingAddressSecondarySelect"
+ billing_state_elements = self.driver.find_elements_by_id(
+ billing_state_element_id
+ )
+ if billing_state_elements:
+ # If we are here it means a state/province element exists and needs to be filled
+ # Open the dropdown menu
+ billing_state_elements[0].click()
+
+ # Pick the first element in the state/province dropdown
+ first_state_xpath = (
+ "//select[@id='billingAddressSecondarySelect']//option[2]"
+ )
+ element_present = EC.presence_of_element_located(
+ (By.XPATH, first_state_xpath)
+ )
+ WebDriverWait(self.driver, 10).until(element_present).click()
+
+ # Hit the final Enroll now button
+ enroll_button_is_clickable = EC.element_to_be_clickable(
+ (By.XPATH, enroll_button_xpath)
+ )
+ WebDriverWait(self.driver, 10).until(enroll_button_is_clickable).click()
+
+ # Wait for success page to load
+ success_element_class = "alert-success"
+ WebDriverWait(self.driver, 10).until(
+ EC.presence_of_element_located((By.CLASS_NAME, success_element_class))
+ )
+
+ logger.info(f"Successfully enrolled in: {course_name}")
+ return UdemyStatus.ENROLLED.value
+ else:
+ return UdemyStatus.ALREADY_ENROLLED.value
+
+ def _check_enrolled(self, course_name):
+ add_to_cart_xpath = "//div[@data-purpose='add-to-cart']"
+ add_to_cart_elements = self.driver.find_elements_by_xpath(add_to_cart_xpath)
+ if not add_to_cart_elements or (
+ add_to_cart_elements and not add_to_cart_elements[0].is_displayed()
+ ):
+ logger.debug(f"Already enrolled in {course_name}")
+ self.stats.already_enrolled += 1
+ return True
+ return False
+
+ def _check_languages(self):
+ if self.settings.languages:
+ locale_xpath = "//div[@data-purpose='lead-course-locale']"
+ element_text = (
+ WebDriverWait(self.driver, 10)
+ .until(EC.presence_of_element_located((By.XPATH, locale_xpath)))
+ .text
+ )
+
+ if element_text not in self.settings.languages:
+ logger.debug(f"Course language not wanted: {element_text}")
+ self.stats.unwanted_language += 1
+ return False
+ return True
+
+ def _check_categories(self):
+ if self.settings.categories:
+ # If the wanted categories are specified, get all the categories of the course by
+ # scraping the breadcrumbs on the top
+
+ breadcrumbs_path = "udlite-breadcrumb"
+ breadcrumbs_text_path = "udlite-heading-sm"
+ breadcrumbs: WebElement = self.driver.find_element_by_class_name(
+ breadcrumbs_path
+ )
+ breadcrumbs = breadcrumbs.find_elements_by_class_name(breadcrumbs_text_path)
+ breadcrumbs = [bc.text for bc in breadcrumbs] # Get only the text
+
+ for category in self.settings.categories:
+ if category in breadcrumbs:
+ return True
+ else:
+ logger.debug("Skipping course as it does not have a wanted category")
+ self.stats.unwanted_category += 1
+ return False
+ return True
+
+ def _check_price(self, course_name):
+ price_xpath = "//span[@data-purpose='total-price']//span"
+ price_elements = self.driver.find_elements_by_xpath(price_xpath)
+ # We get elements here as one of there are 2 matches for this xpath
+
+ for price_element in price_elements:
+ # We are only interested in the element which is displaying the price details
+ if price_element.is_displayed():
+ _price = price_element.text
+ # Extract the numbers from the price text
+ # This logic should work for different locales and currencies
+ _numbers = "".join(filter(lambda x: x if x.isdigit() else None, _price))
+ if _numbers.isdigit() and int(_numbers) > 0:
+ logger.debug(
+ f"Skipping course as it now costs {_price}: {course_name}"
+ )
+ self.stats.expired += 1
+ return False
+ return True
+
+ def _check_if_robot(self) -> bool:
+ """
+ Simply checks if the captcha element is present on login if email/password elements are not
+
+ :return: Bool
+ """
+ is_robot = True
+ try:
+ self.driver.find_element_by_id("px-captcha")
+ except NoSuchElementException:
+ is_robot = False
+ return is_robot
From 0b115a077a2be1ac8dbfe7326cfd884d46e9abab Mon Sep 17 00:00:00 2001
From: cullzie
Date: Wed, 20 Oct 2021 07:33:11 +0100
Subject: [PATCH 12/19] Fix success xpath
---
udemy_enroller/udemy_ui.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py
index 26f2f1b..5442ffa 100644
--- a/udemy_enroller/udemy_ui.py
+++ b/udemy_enroller/udemy_ui.py
@@ -229,9 +229,9 @@ def enroll(self, url: str) -> str:
WebDriverWait(self.driver, 10).until(enroll_button_is_clickable).click()
# Wait for success page to load
- success_element_class = "alert-success"
+ success_element_class = "//div[contains(@class, 'success-alert-banner-container')]"
WebDriverWait(self.driver, 10).until(
- EC.presence_of_element_located((By.CLASS_NAME, success_element_class))
+ EC.presence_of_element_located((By.XPATH, success_element_class))
)
logger.info(f"Successfully enrolled in: {course_name}")
From 2753d7641e3acaacafd733b53e715d6b5f6d609b Mon Sep 17 00:00:00 2001
From: cullzie
Date: Wed, 20 Oct 2021 07:44:42 +0100
Subject: [PATCH 13/19] Run through black
---
udemy_enroller/runner.py | 6 +++++-
udemy_enroller/udemy_ui.py | 4 +++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index 97fa6ab..0084fc9 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -159,7 +159,11 @@ def redeem_courses_ui(
try:
scrapers = ScraperManager(
- freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, coursevania_enabled, max_pages
+ freebiesglobal_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages,
)
_redeem_courses_ui(driver, settings, scrapers)
except Exception as e:
diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py
index 5442ffa..9122bbb 100644
--- a/udemy_enroller/udemy_ui.py
+++ b/udemy_enroller/udemy_ui.py
@@ -229,7 +229,9 @@ def enroll(self, url: str) -> str:
WebDriverWait(self.driver, 10).until(enroll_button_is_clickable).click()
# Wait for success page to load
- success_element_class = "//div[contains(@class, 'success-alert-banner-container')]"
+ success_element_class = (
+ "//div[contains(@class, 'success-alert-banner-container')]"
+ )
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, success_element_class))
)
From 0840aa9bd2291e29a2c2227cbb5d342cb2fe8d4a Mon Sep 17 00:00:00 2001
From: cullzie
Date: Fri, 22 Oct 2021 15:57:03 +0100
Subject: [PATCH 14/19] Fix stats and updates to xpaths
---
udemy_enroller/runner.py | 26 ++++++++--
udemy_enroller/udemy_ui.py | 103 +++++++++++++++++++++++--------------
2 files changed, 88 insertions(+), 41 deletions(-)
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index 0084fc9..d6a9e43 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -3,12 +3,19 @@
import time
from typing import Union
+from selenium.common.exceptions import (
+ NoSuchElementException,
+ TimeoutException,
+ WebDriverException,
+)
+
from udemy_enroller import (
ScraperManager,
Settings,
UdemyActions,
UdemyActionsUI,
UdemyStatus,
+ exceptions,
)
from udemy_enroller.logging import get_logger
@@ -96,8 +103,9 @@ def _redeem_courses_ui(
scrapers: ScraperManager,
) -> None:
"""
- Method to scrape courses from tutorialbar.com and enroll in them on udemy
+ Method to scrape courses from the supported sites and enroll in them on udemy.
+ :param WebDriver driver: WebDriver to use to complete enrolment
:param Settings settings: Core settings used for Udemy
:param ScraperManager scrapers:
:return:
@@ -110,7 +118,9 @@ def _redeem_courses_ui(
udemy_course_links = loop.run_until_complete(scrapers.run())
if udemy_course_links:
- for course_link in udemy_course_links:
+ for course_link in set(
+ udemy_course_links
+ ): # Cast to set to remove duplicate links
try:
status = udemy_actions.enroll(course_link)
if status == UdemyStatus.ENROLLED.value:
@@ -120,8 +130,17 @@ def _redeem_courses_ui(
f"Sleeping for {sleep_time} seconds between enrolments"
)
time.sleep(sleep_time)
+ except NoSuchElementException as e:
+ logger.error(f"No such element: {e}")
+ except TimeoutException:
+ logger.error(f"Timeout on link: {course_link}")
+ except WebDriverException:
+ logger.error(f"Webdriver exception on link: {course_link}")
except KeyboardInterrupt:
- logger.error("Exiting the script")
+ logger.warning("Exiting the script")
+ return
+ except exceptions.RobotException as e:
+ logger.error(e)
return
except Exception as e:
logger.error(f"Unexpected exception: {e}")
@@ -148,6 +167,7 @@ def redeem_courses_ui(
"""
Wrapper of _redeem_courses so we always close browser on completion
+ :param WebDriver driver: WebDriver to use to complete enrolment
:param Settings settings: Core settings used for Udemy
:param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run
:param bool tutorialbar_enabled: Boolean signifying if tutorialbar scraper should run
diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py
index 9122bbb..738248a 100644
--- a/udemy_enroller/udemy_ui.py
+++ b/udemy_enroller/udemy_ui.py
@@ -1,5 +1,6 @@
import time
from dataclasses import dataclass, field
+from datetime import datetime
from enum import Enum
from typing import List
@@ -26,28 +27,28 @@ class RunStatistics:
unwanted_language: int = 0
unwanted_category: int = 0
- course_ids_start: int = 0
- course_ids_end: int = 0
-
start_time = None
- end_time = None
- currency_symbol = "$"
+ currency_symbol = None
def savings(self):
return sum(self.prices) or 0
def table(self):
+ if self.currency_symbol is None:
+ self.currency_symbol = "ยค"
+ run_time_seconds = (datetime.utcnow() - self.start_time).total_seconds()
+
logger.info("==================Run Statistics==================")
logger.info(f"Enrolled: {self.enrolled}")
logger.info(f"Unwanted Category: {self.unwanted_category}")
logger.info(f"Unwanted Language: {self.unwanted_language}")
logger.info(f"Already Claimed: {self.already_enrolled}")
logger.info(f"Expired: {self.expired}")
- logger.info(f"Total Enrolments: {self.course_ids_end}")
logger.info(
f"Savings: {self.currency_symbol}{self.savings():.2f}"
)
+ logger.info(f"Total run time (seconds): {run_time_seconds}s")
logger.info("==================Run Statistics==================")
@@ -75,6 +76,7 @@ def __init__(self, driver: WebDriver, settings: Settings):
self.settings = settings
self.logged_in = False
self.stats = RunStatistics()
+ self.stats.start_time = datetime.utcnow()
def login(self, is_retry=False) -> None:
"""
@@ -135,10 +137,10 @@ def enroll(self, url: str) -> str:
course_name = self.driver.title
- if not self._check_languages():
+ if not self._check_languages(course_name):
return UdemyStatus.UNWANTED_LANGUAGE.value
- if not self._check_categories():
+ if not self._check_categories(course_name):
return UdemyStatus.UNWANTED_CATEGORY.value
# TODO: Make this depend on an element.
@@ -236,7 +238,8 @@ def enroll(self, url: str) -> str:
EC.presence_of_element_located((By.XPATH, success_element_class))
)
- logger.info(f"Successfully enrolled in: {course_name}")
+ logger.info(f"Successfully enrolled in: '{course_name}'")
+ self.stats.enrolled += 1
return UdemyStatus.ENROLLED.value
else:
return UdemyStatus.ALREADY_ENROLLED.value
@@ -247,12 +250,13 @@ def _check_enrolled(self, course_name):
if not add_to_cart_elements or (
add_to_cart_elements and not add_to_cart_elements[0].is_displayed()
):
- logger.debug(f"Already enrolled in {course_name}")
+ logger.debug(f"Already enrolled in '{course_name}'")
self.stats.already_enrolled += 1
return True
return False
- def _check_languages(self):
+ def _check_languages(self, course_identifier):
+ is_valid_language = True
if self.settings.languages:
locale_xpath = "//div[@data-purpose='lead-course-locale']"
element_text = (
@@ -263,11 +267,15 @@ def _check_languages(self):
if element_text not in self.settings.languages:
logger.debug(f"Course language not wanted: {element_text}")
+ logger.debug(
+ f"Course '{course_identifier}' language not wanted: {element_text}"
+ )
self.stats.unwanted_language += 1
- return False
- return True
+ is_valid_language = False
+ return is_valid_language
- def _check_categories(self):
+ def _check_categories(self, course_identifier):
+ is_valid_category = True
if self.settings.categories:
# If the wanted categories are specified, get all the categories of the course by
# scraping the breadcrumbs on the top
@@ -278,36 +286,55 @@ def _check_categories(self):
breadcrumbs_path
)
breadcrumbs = breadcrumbs.find_elements_by_class_name(breadcrumbs_text_path)
- breadcrumbs = [bc.text for bc in breadcrumbs] # Get only the text
+ breadcrumb_text = [bc.text for bc in breadcrumbs] # Get only the text
for category in self.settings.categories:
- if category in breadcrumbs:
- return True
+ if category in breadcrumb_text:
+ is_valid_category = True
+ break
else:
- logger.debug("Skipping course as it does not have a wanted category")
+ logger.debug(
+ f"Skipping course '{course_identifier}' as it does not have a wanted category"
+ )
self.stats.unwanted_category += 1
- return False
- return True
+ is_valid_category = False
+ return is_valid_category
def _check_price(self, course_name):
- price_xpath = "//span[@data-purpose='total-price']//span"
- price_elements = self.driver.find_elements_by_xpath(price_xpath)
- # We get elements here as one of there are 2 matches for this xpath
-
- for price_element in price_elements:
- # We are only interested in the element which is displaying the price details
- if price_element.is_displayed():
- _price = price_element.text
- # Extract the numbers from the price text
- # This logic should work for different locales and currencies
- _numbers = "".join(filter(lambda x: x if x.isdigit() else None, _price))
- if _numbers.isdigit() and int(_numbers) > 0:
- logger.debug(
- f"Skipping course as it now costs {_price}: {course_name}"
- )
- self.stats.expired += 1
- return False
- return True
+ course_is_free = True
+ price_xpath = "//div[contains(@class, 'styles--checkout-pane-outer')]//span[@data-purpose='total-price']//span"
+ price_element = self.driver.find_element_by_xpath(price_xpath)
+
+ # We are only interested in the element which is displaying the price details
+ if price_element.is_displayed():
+ _price = price_element.text
+ # This logic should work for different locales and currencies
+ checkout_price = self._format_price(_price)
+ if None or checkout_price > 0:
+ logger.debug(
+ f"Skipping course '{course_name}' as it now costs {_price}"
+ )
+ self.stats.expired += 1
+ course_is_free = False
+
+ # Get the listed price of the course for stats
+ if course_is_free:
+ list_price_xpath = "//div[contains(@class, 'styles--checkout-pane-outer')]//td[@data-purpose='list-price']//span"
+ list_price_element = self.driver.find_element_by_xpath(list_price_xpath)
+ list_price = self._format_price(list_price_element.text)
+ if list_price is not None:
+ self.stats.prices.append(list_price)
+ return course_is_free
+
+ def _format_price(self, raw_price: str):
+ formatted_price = None
+ try:
+ formatted_price = float(raw_price[1:])
+ if not self.stats.currency_symbol:
+ self.stats.currency_symbol = raw_price[0]
+ except ValueError:
+ pass
+ return formatted_price
def _check_if_robot(self) -> bool:
"""
From e88acfeeab18a0abcb88e333f7675022d347c9e1 Mon Sep 17 00:00:00 2001
From: cullzie
Date: Sat, 30 Oct 2021 10:24:10 +0100
Subject: [PATCH 15/19] Fix for region price check and stats. Remove REST path
from cli
---
pyproject.toml | 1 +
requirements.txt | 1 +
udemy_enroller/cli.py | 34 ++++++++---------------
udemy_enroller/runner.py | 1 +
udemy_enroller/udemy_ui.py | 56 ++++++++++++++++++++++++--------------
5 files changed, 50 insertions(+), 43 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 178b8b5..d684bf8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ cloudscraper = "^1.2.56"
requests = "^2.25.1"
webdriver-manager = "^3.2.2"
aiohttp = {extras = ["speedups"], version = "^3.7.3"}
+price-parser = "^0.3.4"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
diff --git a/requirements.txt b/requirements.txt
index 0711b79..38be014 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ requests
cloudscraper
webdriver-manager
selenium
+price-parser
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index 5bf0142..7ed962a 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -5,7 +5,7 @@
from udemy_enroller import ALL_VALID_BROWSER_STRINGS, DriverManager, Settings
from udemy_enroller.logging import get_logger
-from udemy_enroller.runner import redeem_courses, redeem_courses_ui
+from udemy_enroller.runner import redeem_courses_ui
logger = get_logger()
@@ -79,26 +79,17 @@ def run(
:return:
"""
settings = Settings(delete_settings, delete_cookie)
- if browser:
- dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build)
- redeem_courses_ui(
- dm.driver,
- settings,
- freebiesglobal_enabled,
- tutorialbar_enabled,
- discudemy_enabled,
- coursevania_enabled,
- max_pages,
- )
- else:
- redeem_courses(
- settings,
- freebiesglobal_enabled,
- tutorialbar_enabled,
- discudemy_enabled,
- coursevania_enabled,
- max_pages,
- )
+
+ dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build)
+ redeem_courses_ui(
+ dm.driver,
+ settings,
+ freebiesglobal_enabled,
+ tutorialbar_enabled,
+ discudemy_enabled,
+ coursevania_enabled,
+ max_pages,
+ )
def parse_args() -> Namespace:
@@ -112,7 +103,6 @@ def parse_args() -> Namespace:
parser.add_argument(
"--browser",
type=str,
- default=None,
choices=ALL_VALID_BROWSER_STRINGS,
help="Browser to use for Udemy Enroller",
)
diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py
index d6a9e43..0f76c89 100644
--- a/udemy_enroller/runner.py
+++ b/udemy_enroller/runner.py
@@ -137,6 +137,7 @@ def _redeem_courses_ui(
except WebDriverException:
logger.error(f"Webdriver exception on link: {course_link}")
except KeyboardInterrupt:
+ udemy_actions.stats.table()
logger.warning("Exiting the script")
return
except exceptions.RobotException as e:
diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py
index 738248a..04e2f95 100644
--- a/udemy_enroller/udemy_ui.py
+++ b/udemy_enroller/udemy_ui.py
@@ -1,9 +1,11 @@
import time
from dataclasses import dataclass, field
from datetime import datetime
+from decimal import Decimal
from enum import Enum
from typing import List
+from price_parser import Price
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver, WebElement
@@ -19,7 +21,7 @@
@dataclass(unsafe_hash=True)
class RunStatistics:
- prices: List[float] = field(default_factory=list)
+ prices: List[Decimal] = field(default_factory=list)
expired: int = 0
enrolled: int = 0
@@ -35,21 +37,25 @@ def savings(self):
return sum(self.prices) or 0
def table(self):
- if self.currency_symbol is None:
- self.currency_symbol = "ยค"
- run_time_seconds = (datetime.utcnow() - self.start_time).total_seconds()
-
- logger.info("==================Run Statistics==================")
- logger.info(f"Enrolled: {self.enrolled}")
- logger.info(f"Unwanted Category: {self.unwanted_category}")
- logger.info(f"Unwanted Language: {self.unwanted_language}")
- logger.info(f"Already Claimed: {self.already_enrolled}")
- logger.info(f"Expired: {self.expired}")
- logger.info(
- f"Savings: {self.currency_symbol}{self.savings():.2f}"
- )
- logger.info(f"Total run time (seconds): {run_time_seconds}s")
- logger.info("==================Run Statistics==================")
+ # Only show the table if we have something to show
+ if self.prices:
+ if self.currency_symbol is None:
+ self.currency_symbol = "ยค"
+ run_time_seconds = int(
+ (datetime.utcnow() - self.start_time).total_seconds()
+ )
+
+ logger.info("==================Run Statistics==================")
+ logger.info(f"Enrolled: {self.enrolled}")
+ logger.info(f"Unwanted Category: {self.unwanted_category}")
+ logger.info(f"Unwanted Language: {self.unwanted_language}")
+ logger.info(f"Already Claimed: {self.already_enrolled}")
+ logger.info(f"Expired: {self.expired}")
+ logger.info(
+ f"Savings: {self.currency_symbol}{self.savings():.2f}"
+ )
+ logger.info(f"Total run time (seconds): {run_time_seconds}s")
+ logger.info("==================Run Statistics==================")
class UdemyStatus(Enum):
@@ -309,8 +315,16 @@ def _check_price(self, course_name):
if price_element.is_displayed():
_price = price_element.text
# This logic should work for different locales and currencies
- checkout_price = self._format_price(_price)
- if None or checkout_price > 0:
+ checkout_price = Price.fromstring(_price)
+
+ # Set the currency for stats
+ if (
+ self.stats.currency_symbol is None
+ and checkout_price.currency is not None
+ ):
+ self.stats.currency_symbol = checkout_price.currency
+
+ if checkout_price.amount is None or checkout_price.amount > 0:
logger.debug(
f"Skipping course '{course_name}' as it now costs {_price}"
)
@@ -321,9 +335,9 @@ def _check_price(self, course_name):
if course_is_free:
list_price_xpath = "//div[contains(@class, 'styles--checkout-pane-outer')]//td[@data-purpose='list-price']//span"
list_price_element = self.driver.find_element_by_xpath(list_price_xpath)
- list_price = self._format_price(list_price_element.text)
- if list_price is not None:
- self.stats.prices.append(list_price)
+ list_price = Price.fromstring(list_price_element.text)
+ if list_price.amount is not None:
+ self.stats.prices.append(list_price.amount)
return course_is_free
def _format_price(self, raw_price: str):
From 4cde722dd6f092d7492f17c3e9c24ce67daa1c70 Mon Sep 17 00:00:00 2001
From: cullzie
Date: Sat, 30 Oct 2021 10:26:15 +0100
Subject: [PATCH 16/19] Remove unused method
---
udemy_enroller/cli.py | 1 +
udemy_enroller/udemy_ui.py | 10 ----------
2 files changed, 1 insertion(+), 10 deletions(-)
diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py
index 7ed962a..5d8a6cd 100644
--- a/udemy_enroller/cli.py
+++ b/udemy_enroller/cli.py
@@ -102,6 +102,7 @@ def parse_args() -> Namespace:
parser.add_argument(
"--browser",
+ required=True,
type=str,
choices=ALL_VALID_BROWSER_STRINGS,
help="Browser to use for Udemy Enroller",
diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py
index 04e2f95..7137bc5 100644
--- a/udemy_enroller/udemy_ui.py
+++ b/udemy_enroller/udemy_ui.py
@@ -340,16 +340,6 @@ def _check_price(self, course_name):
self.stats.prices.append(list_price.amount)
return course_is_free
- def _format_price(self, raw_price: str):
- formatted_price = None
- try:
- formatted_price = float(raw_price[1:])
- if not self.stats.currency_symbol:
- self.stats.currency_symbol = raw_price[0]
- except ValueError:
- pass
- return formatted_price
-
def _check_if_robot(self) -> bool:
"""
Simply checks if the captcha element is present on login if email/password elements are not
From 2acb192afa9a605eefbf732d1c9087de4d53798e Mon Sep 17 00:00:00 2001
From: cullzie
Date: Tue, 9 Nov 2021 15:57:48 +0000
Subject: [PATCH 17/19] Bump version 3.2.0 -> 4.0.0
---
pyproject.toml | 4 ++--
setup.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index d684bf8..b082eb8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "automatic-udemy-course-enroller-get-paid-udemy-courses-for-free"
-version = "3.2.0"
+version = "4.0.0"
description = ""
authors = [""]
@@ -28,7 +28,7 @@ requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.bumpver]
-current_version = "3.2.0"
+current_version = "4.0.0"
version_pattern = "MAJOR.MINOR.PATCH"
commit_message = "Bump version {old_version} -> {new_version}"
commit = true
diff --git a/setup.py b/setup.py
index 6a09d21..7a5c8cb 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@
setup(
name="udemy-enroller",
- version="3.2.0",
+ version="4.0.0",
long_description=long_description,
long_description_content_type="text/markdown",
author="aapatre",
From c8e60f92981b09f58b3562938e3f75cc8879b2e1 Mon Sep 17 00:00:00 2001
From: cullzie
Date: Tue, 9 Nov 2021 16:03:09 +0000
Subject: [PATCH 18/19] Updated README and CHANGELOG for v4.0.0
---
CHANGELOG.md | 13 ++++++++++++-
README.md | 7 +++++--
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa5f25d..f7a8c08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [4.0.0] - 2021-11-09
+
+### Added
+- Added support for browser enrolment
+- New coupon source from freebiesglobal
+
+### Removed
+- Remove REST based enrolment since it is no longer working
+
## [3.2.0] - 2021-09-13
### Added
@@ -89,7 +98,9 @@ can continue as normal
zip, extract, install the requirement and get a working version of this
project running locally. Suitable for users who are not looking forward to
contribute.
-
+
+[4.0.0]:
+ https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/releases/tag/v4.0.0
[3.2.0]:
https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/releases/tag/v3.2.0
[3.1.0]:
diff --git a/README.md b/README.md
index c4cfbdf..36d855f 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,7 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
2 . The script can be passed arguments:
- `--help`: View full list of arguments available
+- `--browser=`: Run with a specific browser
- `--discudemy`: Run the discudemy scraper only
- `--coursevania`: Run the coursevania scraper only
- `--tutorialbar`: Run the tutorialbar scraper only
@@ -92,9 +93,11 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer
- `--debug`: Enable debug logging
-3 . Run the script in terminal like so:
+3 . Run the script in terminal with your target browser:
-- `udemy_enroller`
+- `udemy_enroller --browser=firefox`
+- `udemy_enroller --browser=chrome`
+- `udemy_enroller --browser=chromium`
4 . The bot starts scraping the course links from the first **All Courses** page
on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all), [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts
From 0e4b3349275b5e6e8ae31df4f304e73ededc8d7e Mon Sep 17 00:00:00 2001
From: cullzie
Date: Fri, 12 Nov 2021 20:09:01 +0000
Subject: [PATCH 19/19] Fix build and release date
---
.github/workflows/python-package.yml | 2 +-
CHANGELOG.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 41e5ee2..9432e68 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -53,4 +53,4 @@ jobs:
UDEMY_PASSWORD: ${{ secrets.UDEMY_PASSWORD }}
CI_TEST: "True"
run: |
- poetry run python udemy_enroller.py --debug
+ poetry run python udemy_enroller.py --browser=chrome --debug
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7a8c08..1542615 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [4.0.0] - 2021-11-09
+## [4.0.0] - 2021-11-12
### Added
- Added support for browser enrolment