imseldrith commited on
Commit
9382654
·
verified ·
1 Parent(s): 7594a64

Update base.py

Browse files
Files changed (1) hide show
  1. base.py +1067 -1065
base.py CHANGED
@@ -1,1065 +1,1067 @@
1
- import json
2
- import os
3
- import re
4
- import sys
5
- import threading
6
- import time
7
- import traceback
8
- from datetime import datetime, timezone
9
- from decimal import Decimal
10
- from urllib.parse import parse_qs, unquote, urlparse, urlsplit, urlunparse
11
-
12
- import cloudscraper
13
- import requests
14
- import rookiepy
15
- from bs4 import BeautifulSoup as bs
16
-
17
- from colors import fb, fc, fg, flb, flg, fm, fr, fy
18
-
19
- VERSION = "v2.3.1"
20
-
21
- scraper_dict: dict = {
22
- "Udemy Freebies": "uf",
23
- "Tutorial Bar": "tb",
24
- "Real Discount": "rd",
25
- "Course Vania": "cv",
26
- "IDownloadCoupons": "idc",
27
- "E-next": "en",
28
- "Discudemy": "du",
29
- }
30
-
31
- LINKS = {
32
- "github": "https://github.com/techtanic/Discounted-Udemy-Course-Enroller",
33
- "support": "https://techtanic.github.io/duce/support",
34
- "discord": "https://discord.gg/wFsfhJh4Rh",
35
- }
36
-
37
-
38
- class LoginException(Exception):
39
- """Login Error
40
-
41
- Args:
42
- Exception (str): Exception Reason
43
- """
44
-
45
- pass
46
-
47
-
48
- class RaisingThread(threading.Thread):
49
- def run(self):
50
- self._exc = None
51
- try:
52
- super().run()
53
- except Exception as e:
54
- self._exc = e
55
-
56
- def join(self, timeout=None):
57
- super().join(timeout=timeout)
58
- if self._exc:
59
- raise self._exc
60
-
61
-
62
- def resource_path(relative_path):
63
- if hasattr(sys, "_MEIPASS"):
64
- return os.path.join(sys._MEIPASS, relative_path)
65
- return os.path.join(os.path.abspath("."), relative_path)
66
-
67
-
68
- class Scraper:
69
- """
70
- Scrapers: RD,TB, CV, IDC, EN, DU, UF
71
- """
72
-
73
- def __init__(
74
- self,
75
- site_to_scrape: list = list(scraper_dict.keys()),
76
- debug: bool = False,
77
- ):
78
- self.sites = site_to_scrape
79
- self.debug = debug
80
- for site in self.sites:
81
- code_name = scraper_dict[site]
82
- setattr(self, f"{code_name}_length", 0)
83
- setattr(self, f"{code_name}_data", [])
84
- setattr(self, f"{code_name}_done", False)
85
- setattr(self, f"{code_name}_progress", 0)
86
- setattr(self, f"{code_name}_error", "")
87
-
88
- def get_scraped_courses(self, target: object) -> list:
89
- threads = []
90
- scraped_data = {}
91
- for site in self.sites:
92
- t = threading.Thread(
93
- target=target,
94
- args=(site,),
95
- daemon=True,
96
- )
97
- t.start()
98
- threads.append(t)
99
- time.sleep(0.2)
100
- for t in threads:
101
- t.join()
102
- for site in self.sites:
103
- scraped_data[site] = getattr(self, f"{scraper_dict[site]}_data")
104
- return scraped_data
105
-
106
- def append_to_list(self, target: list, title: str, link: str):
107
- target.append((title, link))
108
-
109
- def fetch_page_content(self, url: str, headers: dict = None) -> bytes:
110
- return requests.get(url, headers=headers).content
111
-
112
- def parse_html(self, content: str):
113
- return bs(content, "html5lib")
114
-
115
- def handle_exception(self, site_code: str):
116
- setattr(self, f"{site_code}_error", traceback.format_exc())
117
- setattr(self, f"{site_code}_length", -1)
118
- setattr(self, f"{site_code}_done", True)
119
- if self.debug:
120
- print(getattr(self, f"{site_code}_error"))
121
-
122
- def cleanup_link(self, link: str) -> str:
123
- parsed_url = urlparse(link)
124
-
125
- if parsed_url.netloc == "www.udemy.com":
126
- return link
127
-
128
- if parsed_url.netloc == "click.linksynergy.com":
129
- query_params = parse_qs(parsed_url.query)
130
-
131
- if "RD_PARM1" in query_params:
132
- return unquote(query_params["RD_PARM1"][0])
133
- elif "murl" in query_params:
134
- return unquote(query_params["murl"][0])
135
- else:
136
- return ""
137
- raise ValueError(f"Unknown link format: {link}")
138
-
139
- def du(self):
140
- try:
141
- all_items = []
142
- head = {
143
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
144
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
145
- }
146
-
147
- for page in range(1, 4):
148
- content = self.fetch_page_content(
149
- f"https://www.discudemy.com/all/{page}", headers=head
150
- )
151
- soup = self.parse_html(content)
152
- page_items = soup.find_all("a", {"class": "card-header"})
153
- all_items.extend(page_items)
154
- self.du_length = len(all_items)
155
- if self.debug:
156
- print("Length:", self.du_length)
157
- for index, item in enumerate(all_items):
158
- self.du_progress = index
159
- title = item.string
160
- url = item["href"].split("/")[-1]
161
- content = self.fetch_page_content(
162
- f"https://www.discudemy.com/go/{url}", headers=head
163
- )
164
- soup = self.parse_html(content)
165
- link = soup.find("div", {"class": "ui segment"}).a["href"]
166
- if self.debug:
167
- print(title, link)
168
- self.append_to_list(self.du_data, title, link)
169
-
170
- except:
171
- self.handle_exception("du")
172
- self.du_done = True
173
- if self.debug:
174
- print("Return Length:", len(self.du_data))
175
-
176
- def uf(self):
177
- try:
178
- all_items = []
179
- for page in range(1, 4):
180
- content = self.fetch_page_content(
181
- f"https://www.udemyfreebies.com/free-udemy-courses/{page}"
182
- )
183
- soup = self.parse_html(content)
184
- page_items = soup.find_all("a", {"class": "theme-img"})
185
- all_items.extend(page_items)
186
- self.uf_length = len(all_items)
187
- if self.debug:
188
- print("Length:", self.uf_length)
189
- for index, item in enumerate(all_items):
190
- title = item.img["alt"]
191
- link = requests.get(
192
- f"https://www.udemyfreebies.com/out/{item['href'].split('/')[4]}"
193
- ).url
194
- self.append_to_list(self.uf_data, title, link)
195
- self.uf_progress = index
196
-
197
- except:
198
- self.handle_exception("uf")
199
- self.uf_done = True
200
- if self.debug:
201
- print("Return Length:", len(self.uf_data))
202
-
203
- def tb(self):
204
- try:
205
- all_items = []
206
-
207
- for page in range(1, 5):
208
- content = self.fetch_page_content(
209
- f"https://www.tutorialbar.com/all-courses/page/{page}"
210
- )
211
- soup = self.parse_html(content)
212
- page_items = soup.find_all(
213
- "h2", class_="mb15 mt0 font110 mobfont100 fontnormal lineheight20"
214
- )
215
- all_items.extend(page_items)
216
- self.tb_length = len(all_items)
217
- if self.debug:
218
- print("Length:", self.tb_length)
219
-
220
- for index, item in enumerate(all_items):
221
- self.tb_progress = index
222
- title = item.a.string
223
- url = item.a["href"]
224
- content = self.fetch_page_content(url)
225
- soup = self.parse_html(content)
226
- link = soup.find("a", class_="btn_offer_block re_track_btn")["href"]
227
- if "www.udemy.com" in link:
228
- self.append_to_list(self.tb_data, title, link)
229
-
230
- except:
231
- self.handle_exception("tb")
232
- self.tb_done = True
233
- if self.debug:
234
- print("Return Length:", len(self.tb_data))
235
-
236
- def rd(self):
237
- all_items = []
238
-
239
- try:
240
- headers = {
241
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
242
- "Host": "www.real.discount",
243
- "Connection": "Keep-Alive",
244
- "dnt": "1",
245
- }
246
- try:
247
- r = requests.get(
248
- "https://www.real.discount/api-web/all-courses/?store=Udemy&page=1&per_page=500&orderby=date&free=1&editorschoices=0",
249
- headers=headers,
250
- timeout=(10, 30),
251
- ).json()
252
- except requests.exceptions.Timeout:
253
- self.rd_error = "Timeout"
254
- self.rd_length = -1
255
- self.rd_done = True
256
- return
257
- all_items.extend(r["results"])
258
-
259
- self.rd_length = len(all_items)
260
- if self.debug:
261
- print("Length:", self.rd_length)
262
- for index, item in enumerate(all_items):
263
- self.rd_progress = index
264
- title: str = item["name"]
265
- link: str = item["url"]
266
- link = self.cleanup_link(link)
267
- if link:
268
- self.append_to_list(self.rd_data, title, link)
269
-
270
- except:
271
- self.handle_exception("rd")
272
- if self.debug:
273
- print("Return Length:", len(self.rd_data))
274
- self.rd_done = True
275
-
276
- def cv(self):
277
- try:
278
- content = self.fetch_page_content("https://coursevania.com/courses/")
279
- soup = self.parse_html(content)
280
- try:
281
- nonce = json.loads(
282
- re.search(
283
- r"var stm_lms_nonces = ({.*?});", soup.text, re.DOTALL
284
- ).group(1)
285
- )["load_content"]
286
- if self.debug:
287
- print("Nonce:", nonce)
288
- except IndexError:
289
- self.cv_error = "Nonce not found"
290
- self.cv_length = -1
291
- self.cv_done = True
292
- return
293
- r = requests.get(
294
- "https://coursevania.com/wp-admin/admin-ajax.php?&template=courses/grid&args={%22posts_per_page%22:%2260%22}&action=stm_lms_load_content&nonce="
295
- + nonce
296
- + "&sort=date_high"
297
- ).json()
298
-
299
- soup = self.parse_html(r["content"])
300
- page_items = soup.find_all(
301
- "div", {"class": "stm_lms_courses__single--title"}
302
- )
303
- self.cv_length = len(page_items)
304
- if self.debug:
305
- print("Small Length:", self.cv_length)
306
- for index, item in enumerate(page_items):
307
- self.cv_progress = index
308
- title = item.h5.string
309
- content = self.fetch_page_content(item.a["href"])
310
- soup = self.parse_html(content)
311
- link = soup.find(
312
- "a",
313
- {"class": "masterstudy-button-affiliate__link"},
314
- )["href"]
315
- self.append_to_list(self.cv_data, title, link)
316
-
317
- except:
318
- self.handle_exception("cv")
319
- self.cv_done = True
320
- if self.debug:
321
- print("Return Length:", len(self.cv_data))
322
-
323
- def idc(self):
324
- try:
325
- all_items = []
326
- for page in range(1, 5):
327
- content = self.fetch_page_content(
328
- f"https://idownloadcoupon.com/product-category/udemy/page/{page}"
329
- )
330
- soup = self.parse_html(content)
331
- page_items = soup.find_all(
332
- "a",
333
- attrs={
334
- "class": "woocommerce-LoopProduct-link woocommerce-loop-product__link"
335
- },
336
- )
337
- all_items.extend(page_items)
338
- self.idc_length = len(all_items)
339
- if self.debug:
340
- print("Length:", self.idc_length)
341
- for index, item in enumerate(all_items):
342
- self.idc_progress = index
343
- title = item.h2.string
344
- link_num = item["href"].split("/")[4]
345
- if link_num == "85":
346
- continue
347
- link = f"https://idownloadcoupon.com/udemy/{link_num}/"
348
-
349
- r = requests.get(
350
- link,
351
- allow_redirects=False,
352
- )
353
- link = unquote(r.headers["Location"])
354
- link = self.cleanup_link(link)
355
- self.append_to_list(self.idc_data, title, link)
356
-
357
- except:
358
- self.handle_exception("idc")
359
- self.idc_done = True
360
- if self.debug:
361
- print("Return Length:", len(self.idc_data))
362
-
363
- def en(self):
364
- try:
365
- all_items = []
366
- for page in range(1, 6):
367
- content = self.fetch_page_content(
368
- f"https://jobs.e-next.in/course/udemy/{page}"
369
- )
370
- soup = self.parse_html(content)
371
- page_items = soup.find_all(
372
- "a", {"class": "btn btn-secondary btn-sm btn-block"}
373
- )
374
- all_items.extend(page_items)
375
-
376
- self.en_length = len(all_items)
377
-
378
- if self.debug:
379
- print("Length:", self.en_length)
380
- for index, item in enumerate(all_items):
381
- self.en_progress = index
382
- content = self.fetch_page_content(item["href"])
383
- soup = self.parse_html(content)
384
- title = soup.find("h3").string.strip()
385
- link = soup.find("a", {"class": "btn btn-primary"})["href"]
386
- self.append_to_list(self.en_data, title, link)
387
-
388
- except:
389
- self.handle_exception("en")
390
- self.en_done = True
391
- if self.debug:
392
- print("Return Length:", len(self.en_data))
393
- print(self.en_data)
394
-
395
-
396
- class Udemy:
397
- def __init__(self, interface: str, debug: bool = False):
398
- self.interface = interface
399
- self.client = cloudscraper.CloudScraper()
400
- headers = {
401
- "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
402
- "Accept": "application/json, text/plain, */*",
403
- "Accept-Language": "en-GB,en;q=0.5",
404
- "Referer": "https://www.udemy.com/",
405
- "X-Requested-With": "XMLHttpRequest",
406
- "DNT": "1",
407
- "Connection": "keep-alive",
408
- "Sec-Fetch-Dest": "empty",
409
- "Sec-Fetch-Mode": "cors",
410
- "Sec-Fetch-Site": "same-origin",
411
- "Pragma": "no-cache",
412
- "Cache-Control": "no-cache",
413
- }
414
-
415
- self.client.headers.update(headers)
416
- self.debug = debug
417
-
418
- def print(self, content: str, color: str = "red", **kargs):
419
- content = str(content)
420
- colours_dict = {
421
- "yellow": fy,
422
- "red": fr,
423
- "blue": fb,
424
- "light blue": flb,
425
- "green": fg,
426
- "light green": flg,
427
- "cyan": fc,
428
- "magenta": fm,
429
- }
430
- if self.interface == "gui":
431
- self.window["out"].print(content, text_color=color, **kargs)
432
- else:
433
- print(colours_dict[color] + content, **kargs)
434
-
435
- def get_date_from_utc(self, d: str):
436
- utc_dt = datetime.strptime(d, "%Y-%m-%dT%H:%M:%SZ")
437
- dt = utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
438
- return dt.strftime("%B %d, %Y")
439
-
440
- def get_now_to_utc(self):
441
- return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
442
-
443
- def load_settings(self):
444
- try:
445
- with open(f"duce-{self.interface}-settings.json") as f:
446
- self.settings = json.load(f)
447
- except FileNotFoundError:
448
- with open(
449
- resource_path(f"default-duce-{self.interface}-settings.json")
450
- ) as f:
451
- self.settings = json.load(f)
452
- if (
453
- self.interface == "cli" and "use_browser_cookies" not in self.settings
454
- ): # v2.1
455
- self.settings.get("use_browser_cookies", False)
456
- # v2.2
457
- if "course_update_threshold_months" not in self.settings:
458
- self.settings["course_update_threshold_months"] = 24 # 2 years
459
-
460
- self.settings["languages"] = dict(
461
- sorted(self.settings["languages"].items(), key=lambda item: item[0])
462
- )
463
- self.save_settings()
464
- self.title_exclude = "\n".join(self.settings["title_exclude"])
465
- self.instructor_exclude = "\n".join(self.settings["instructor_exclude"])
466
-
467
- def save_settings(self):
468
- with open(f"duce-{self.interface}-settings.json", "w") as f:
469
- json.dump(self.settings, f, indent=4)
470
-
471
- def make_cookies(self, client_id: str, access_token: str, csrf_token: str):
472
- self.cookie_dict = dict(
473
- client_id=client_id,
474
- access_token=access_token,
475
- csrf_token=csrf_token,
476
- )
477
-
478
- def fetch_cookies(self):
479
- """Gets cookies from browser
480
- Sets cookies_dict, cookie_jar
481
- """
482
- cookies = rookiepy.to_cookiejar(rookiepy.load(["www.udemy.com"]))
483
- self.cookie_dict: dict = requests.utils.dict_from_cookiejar(cookies)
484
- self.cookie_jar = cookies
485
-
486
- def get_enrolled_courses(self):
487
- """Get enrolled courses
488
- Sets enrolled_courses
489
-
490
- {slug:enrollment_time}
491
- """
492
- next_page = "https://www.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-enroll_time&fields[course]=enrollment_time,url&page_size=100"
493
- courses = {}
494
- while next_page:
495
- r = self.client.get(
496
- next_page,
497
- ).json()
498
- for course in r["results"]:
499
- slug = course["url"].split("/")[2]
500
- courses[slug] = course["enrollment_time"]
501
- next_page = r["next"]
502
- self.enrolled_courses = courses
503
-
504
- def compare_versions(self, version1, version2):
505
- v1_parts = list(map(int, version1.split(".")))
506
- v2_parts = list(map(int, version2.split(".")))
507
- max_length = max(len(v1_parts), len(v2_parts))
508
- v1_parts.extend([0] * (max_length - len(v1_parts)))
509
- v2_parts.extend([0] * (max_length - len(v2_parts)))
510
-
511
- for v1, v2 in zip(v1_parts, v2_parts):
512
- if v1 < v2:
513
- return -1
514
- elif v1 > v2:
515
- return 1
516
- return 0
517
-
518
- def check_for_update(self) -> tuple[str, str]:
519
- r_version = (
520
- requests.get(
521
- "https://api.github.com/repos/techtanic/Discounted-Udemy-Course-Enroller/releases/latest"
522
- )
523
- .json()["tag_name"]
524
- .removeprefix("v")
525
- )
526
- c_version = VERSION.removeprefix("v")
527
-
528
- comparison = self.compare_versions(c_version, r_version)
529
-
530
- if comparison == -1:
531
- return (
532
- f"Update {r_version} Available",
533
- f"Update {r_version} Available",
534
- )
535
- elif comparison == 0:
536
- return (
537
- f"Login {c_version}",
538
- f"Discounted-Udemy-Course-Enroller {c_version}",
539
- )
540
- else:
541
- return (
542
- f"Dev Login {c_version}",
543
- f"Dev Discounted-Udemy-Course-Enroller {c_version}",
544
- )
545
-
546
- def manual_login(self, email: str, password: str):
547
- """Manual Login to Udemy using email and password and sets cookies
548
- Args:
549
- email (str): Email
550
- password (str): Password
551
- Raises:
552
- LoginException: Login Error
553
- """
554
- # s = cloudscraper.CloudScraper()
555
-
556
- s = requests.session()
557
- r = s.get(
558
- "https://www.udemy.com/join/signup-popup/?locale=en_US&response_type=html&next=https%3A%2F%2Fwww.udemy.com%2Flogout%2F",
559
- headers={"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)"},
560
- # headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0",
561
- # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
562
- # 'Accept-Language': 'en-US,en;q=0.5',
563
- # #'Accept-Encoding': 'gzip, deflate, br',
564
- # 'DNT': '1',
565
- # 'Connection': 'keep-alive',
566
- # 'Upgrade-Insecure-Requests': '1',
567
- # 'Sec-Fetch-Dest': 'document',
568
- # 'Sec-Fetch-Mode': 'navigate',
569
- # 'Sec-Fetch-Site': 'none',
570
- # 'Sec-Fetch-User': '?1',
571
- # 'Pragma': 'no-cache',
572
- # 'Cache-Control': 'no-cache'},
573
- )
574
- try:
575
- csrf_token = r.cookies["csrftoken"]
576
- except:
577
- if self.debug:
578
- print(r.text)
579
- data = {
580
- "csrfmiddlewaretoken": csrf_token,
581
- "locale": "en_US",
582
- "email": email,
583
- "password": password,
584
- }
585
-
586
- # ss = requests.session()
587
- s.cookies.update(r.cookies)
588
- s.headers.update(
589
- {
590
- "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
591
- "Accept": "application/json, text/plain, */*",
592
- "Accept-Language": "en-GB,en;q=0.5",
593
- "Referer": "https://www.udemy.com/join/login-popup/?passwordredirect=True&response_type=json",
594
- "Origin": "https://www.udemy.com",
595
- "DNT": "1",
596
- "Host": "www.udemy.com",
597
- "Connection": "keep-alive",
598
- "Sec-Fetch-Dest": "empty",
599
- "Sec-Fetch-Mode": "cors",
600
- "Sec-Fetch-Site": "same-origin",
601
- "Pragma": "no-cache",
602
- "Cache-Control": "no-cache",
603
- }
604
- )
605
- s = cloudscraper.create_scraper(sess=s)
606
- r = s.post(
607
- "https://www.udemy.com/join/login-popup/?passwordredirect=True&response_type=json",
608
- data=data,
609
- allow_redirects=False,
610
- )
611
- if r.text.__contains__("returnUrl"):
612
- self.make_cookies(
613
- r.cookies["client_id"], r.cookies["access_token"], csrf_token
614
- )
615
- else:
616
- login_error = r.json()["error"]["data"]["formErrors"][0]
617
- if login_error[0] == "Y":
618
- raise LoginException("Too many logins per hour try later")
619
- elif login_error[0] == "T":
620
- raise LoginException("Email or password incorrect")
621
- else:
622
- raise LoginException(login_error)
623
-
624
- def get_session_info(self):
625
- """Get Session info
626
- Sets Client Session, currency and name
627
- """
628
- s = cloudscraper.CloudScraper()
629
- # headers = {
630
- # "authorization": "Bearer " + self.cookie_dict["access_token"],
631
- # "accept": "application/json, text/plain, */*",
632
- # "x-requested-with": "XMLHttpRequest",
633
- # "x-forwarded-for": str(
634
- # ".".join(map(str, (random.randint(0, 255) for _ in range(4))))
635
- # ),
636
- # "x-udemy-authorization": "Bearer " + self.cookie_dict["access_token"],
637
- # "content-type": "application/json;charset=UTF-8",
638
- # "origin": "https://www.udemy.com",
639
- # "referer": "https://www.udemy.com/",
640
- # "dnt": "1",
641
- # "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
642
- # }
643
-
644
- headers = {
645
- "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
646
- "Accept": "application/json, text/plain, */*",
647
- "Accept-Language": "en-GB,en;q=0.5",
648
- "Referer": "https://www.udemy.com/",
649
- "X-Requested-With": "XMLHttpRequest",
650
- "DNT": "1",
651
- "Connection": "keep-alive",
652
- "Sec-Fetch-Dest": "empty",
653
- "Sec-Fetch-Mode": "cors",
654
- "Sec-Fetch-Site": "same-origin",
655
- "Pragma": "no-cache",
656
- "Cache-Control": "no-cache",
657
- }
658
-
659
- r = s.get(
660
- "https://www.udemy.com/api-2.0/contexts/me/?header=True",
661
- cookies=self.cookie_dict,
662
- headers=headers,
663
- )
664
- r = r.json()
665
- if self.debug:
666
- print(r)
667
- if not r["header"]["isLoggedIn"]:
668
- raise LoginException("Login Failed")
669
-
670
- self.display_name: str = r["header"]["user"]["display_name"]
671
- r = s.get(
672
- "https://www.udemy.com/api-2.0/shopping-carts/me/",
673
- headers=headers,
674
- cookies=self.cookie_dict,
675
- )
676
- r = r.json()
677
- self.currency: str = r["user"]["credit"]["currency_code"]
678
-
679
- s = cloudscraper.CloudScraper()
680
- s.cookies.update(self.cookie_dict)
681
- s.headers.update(headers)
682
- s.keep_alive = False
683
- self.client = s
684
- self.get_enrolled_courses()
685
-
686
- def is_keyword_excluded(self, title: str) -> bool:
687
- title_words = title.casefold().split()
688
- for word in title_words:
689
- word = word.casefold()
690
- if word in self.title_exclude:
691
- return True
692
- return False
693
-
694
- def is_instructor_excluded(self, instructors: list) -> bool:
695
- for instructor in instructors:
696
- if instructor in self.settings["instructor_exclude"]:
697
- return True
698
- return False
699
-
700
- def is_course_updated(self, last_update: str | None) -> bool:
701
- if not last_update:
702
- return True
703
- current_date = datetime.now()
704
- last_update_date = datetime.strptime(last_update, "%Y-%m-%d")
705
- # Calculate the difference in years and months
706
- years = current_date.year - last_update_date.year
707
- months = current_date.month - last_update_date.month
708
- days = current_date.day - last_update_date.day
709
-
710
- # Adjust the months and years if necessary
711
- if days < 0:
712
- months -= 1
713
-
714
- if months < 0:
715
- years -= 1
716
- months += 12
717
-
718
- # Calculate the total month difference
719
- month_diff = years * 12 + months
720
- return month_diff < self.settings["course_update_threshold_months"]
721
-
722
- def is_user_dumb(self) -> bool:
723
- self.sites = [key for key, value in self.settings["sites"].items() if value]
724
- self.categories = [
725
- key for key, value in self.settings["categories"].items() if value
726
- ]
727
- self.languages = [
728
- key for key, value in self.settings["languages"].items() if value
729
- ]
730
- self.instructor_exclude = self.settings["instructor_exclude"]
731
- self.title_exclude = self.settings["title_exclude"]
732
- self.min_rating = self.settings["min_rating"]
733
- return not all([bool(self.sites), bool(self.categories), bool(self.languages)])
734
-
735
- def save_course(self):
736
- if self.settings["save_txt"]:
737
- self.txt_file.write(f"{self.title} - {self.link}\n")
738
- self.txt_file.flush()
739
- os.fsync(self.txt_file.fileno())
740
-
741
- def remove_duplicate_courses(self):
742
- existing_links = set()
743
- new_data = {}
744
- for key, courses in self.scraped_data.items():
745
- new_data[key] = []
746
- for title, link in courses:
747
- link = self.normalize_link(link)
748
- if link not in existing_links:
749
- new_data[key].append((title, link))
750
- existing_links.add(link)
751
- self.scraped_data = {k: v for k, v in new_data.items() if v}
752
-
753
- def normalize_link(self, link):
754
- parsed_url = urlparse(link)
755
- path = (
756
- parsed_url.path if parsed_url.path.endswith("/") else parsed_url.path + "/"
757
- )
758
- return urlunparse(
759
- (
760
- parsed_url.scheme,
761
- parsed_url.netloc,
762
- path,
763
- parsed_url.params,
764
- parsed_url.query,
765
- parsed_url.fragment,
766
- )
767
- )
768
-
769
- def get_course_id(self, url):
770
- course = {
771
- "course_id": None,
772
- "url": url,
773
- "is_invalid": False,
774
- "is_free": None,
775
- "is_excluded": None,
776
- "retry": None,
777
- "msg": "Report to developer",
778
- }
779
- url = re.sub(r"\W+$", "", unquote(url))
780
- try:
781
- r = self.client.get(url)
782
- except requests.exceptions.ConnectionError:
783
- if self.debug:
784
- print(r.text)
785
- course["retry"] = True
786
- return course
787
- course["url"] = r.url
788
- soup = bs(r.content, "html5lib")
789
-
790
- course_id = soup.find("body").get("data-clp-course-id", "invalid")
791
-
792
- if course_id == "invalid":
793
- course["is_invalid"] = True
794
- course["msg"] = "Course ID not found: Report to developer"
795
- return course
796
- course["course_id"] = course_id
797
- dma = json.loads(soup.find("body")["data-module-args"])
798
- if self.debug:
799
- with open("debug/dma.json", "w") as f:
800
- json.dump(dma, f, indent=4)
801
-
802
- if dma.get("view_restriction"):
803
- course["is_invalid"] = True
804
- course["msg"] = dma["serverSideProps"]["limitedAccess"]["errorMessage"][
805
- "title"
806
- ]
807
- return course
808
-
809
- course["is_free"] = not dma["serverSideProps"]["course"].get("isPaid", True)
810
- if not self.debug and self.is_course_excluded(dma):
811
- course["is_excluded"] = True
812
- return course
813
-
814
- return course
815
-
816
- def is_course_excluded(self, dma):
817
- instructors = [
818
- i["absolute_url"].split("/")[-2]
819
- for i in dma["serverSideProps"]["course"]["instructors"]["instructors_info"]
820
- if i["absolute_url"]
821
- ]
822
- lang = dma["serverSideProps"]["course"]["localeSimpleEnglishTitle"]
823
- cat = dma["serverSideProps"]["topicMenu"]["breadcrumbs"][0]["title"]
824
- rating = dma["serverSideProps"]["course"]["rating"]
825
- last_update = dma["serverSideProps"]["course"]["lastUpdateDate"]
826
-
827
- if not self.is_course_updated(last_update):
828
- self.print(
829
- f"Course excluded: Last updated {last_update}", color="light blue"
830
- )
831
- elif self.is_instructor_excluded(instructors):
832
- self.print(f"Instructor excluded: {instructors[0]}", color="light blue")
833
- elif self.is_keyword_excluded(self.title):
834
- self.print("Keyword Excluded", color="light blue")
835
- elif cat not in self.categories:
836
- self.print(f"Category excluded: {cat}", color="light blue")
837
- elif lang not in self.languages:
838
- self.print(f"Language excluded: {lang}", color="light blue")
839
- elif rating < self.min_rating:
840
- self.print(f"Low rating: {rating}", color="light blue")
841
- else:
842
- return False
843
- return True
844
-
845
- def extract_course_coupon(self, url):
846
- params = parse_qs(urlsplit(url).query)
847
- return params.get("couponCode", [False])[0]
848
-
849
- def check_course(self, course_id, coupon_code=None):
850
- url = f"https://www.udemy.com/api-2.0/course-landing-components/{course_id}/me/?components=purchase"
851
- if coupon_code:
852
- url += f",redeem_coupon&couponCode={coupon_code}"
853
-
854
- r = self.client.get(url).json()
855
- if self.debug:
856
- with open("test/check_course.json", "w") as f:
857
- json.dump(r, f, indent=4)
858
- amount = (
859
- r.get("purchase", {})
860
- .get("data", {})
861
- .get("list_price", {})
862
- .get("amount", "retry")
863
- )
864
- coupon_valid = False
865
-
866
- if coupon_code and "redeem_coupon" in r:
867
- discount = r["purchase"]["data"]["pricing_result"]["discount_percent"]
868
- status = r["redeem_coupon"]["discount_attempts"][0]["status"]
869
- coupon_valid = discount == 100 and status == "applied"
870
-
871
- return Decimal(amount), coupon_valid
872
-
873
- def start_enrolling(self):
874
- self.remove_duplicate_courses()
875
- self.initialize_counters()
876
- self.setup_txt_file()
877
-
878
- total_courses = sum(len(courses) for courses in self.scraped_data.values())
879
- previous_courses_count = 0
880
- for site_index, (site, courses) in enumerate(self.scraped_data.items()):
881
- self.print(f"\nSite: {site} [{len(courses)}]", color="cyan")
882
-
883
- for index, (title, link) in enumerate(courses):
884
- self.title = title
885
- self.link = link
886
- self.print_course_info(previous_courses_count + index, total_courses)
887
- self.handle_course_enrollment()
888
- previous_courses_count += len(courses)
889
-
890
- def initialize_counters(self):
891
- self.successfully_enrolled_c = 0
892
- self.already_enrolled_c = 0
893
- self.expired_c = 0
894
- self.excluded_c = 0
895
- self.amount_saved_c = 0
896
-
897
- def setup_txt_file(self):
898
- if self.settings["save_txt"]:
899
- os.makedirs("Courses/", exist_ok=True)
900
- self.txt_file = open(
901
- f"Courses/{time.strftime('%Y-%m-%d--%H-%M')}.txt", "w", encoding="utf-8"
902
- )
903
-
904
- def print_course_info(self, index, total_courses):
905
- self.print(f"[{index + 1} / {total_courses}] ", color="magenta", end=" ")
906
- self.print(self.title, color="yellow", end=" ")
907
- self.print(self.link, color="blue")
908
-
909
- def handle_course_enrollment(self):
910
- slug = self.link.split("/")[4]
911
-
912
- if slug in self.enrolled_courses:
913
- self.print(
914
- f"You purchased this course on {self.get_date_from_utc(self.enrolled_courses[slug])}",
915
- color="light blue",
916
- )
917
- self.already_enrolled_c += 1
918
- return
919
-
920
- course = self.get_course_id(self.link)
921
- if course["is_invalid"]:
922
- self.print(course["msg"], color="red")
923
- self.excluded_c += 1
924
- elif course["retry"]:
925
- self.print("Retrying...", color="red")
926
- time.sleep(1)
927
- self.handle_course_enrollment()
928
- elif course["is_excluded"]:
929
- self.excluded_c += 1
930
- elif course["is_free"]:
931
- self.handle_free_course(course["course_id"])
932
- elif not course["is_free"]:
933
- self.handle_discounted_course(course["course_id"])
934
- else:
935
- self.print("Unknown Error: Report this link to the developer", color="red")
936
- self.excluded_c += 1
937
-
938
- def handle_free_course(self, course_id):
939
- if self.settings["discounted_only"]:
940
- self.print("Free course excluded", color="light blue")
941
- self.excluded_c += 1
942
- else:
943
- success = self.free_checkout(course_id)
944
- if success:
945
- self.print("Successfully Subscribed", color="green")
946
- self.successfully_enrolled_c += 1
947
- self.save_course()
948
- else:
949
- self.print(
950
- "Unknown Error: Report this link to the developer", color="red"
951
- )
952
- self.expired_c += 1
953
-
954
- def discounted_checkout(self, coupon, course_id) -> dict:
955
- payload = {
956
- "checkout_environment": "Marketplace",
957
- "checkout_event": "Submit",
958
- "payment_info": {
959
- "method_id": "0",
960
- "payment_method": "free-method",
961
- "payment_vendor": "Free",
962
- },
963
- "shopping_info": {
964
- "items": [
965
- {
966
- "buyable": {"id": course_id, "type": "course"},
967
- "discountInfo": {"code": coupon},
968
- "price": {"amount": 0, "currency": self.currency.upper()},
969
- }
970
- ],
971
- "is_cart": False,
972
- },
973
- }
974
- headers = {
975
- "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
976
- "Accept": "application/json, text/plain, */*",
977
- "Accept-Language": "en-US",
978
- "Referer": f"https://www.udemy.com/payment/checkout/express/course/{course_id}/?discountCode={coupon}",
979
- "Content-Type": "application/json",
980
- "X-Requested-With": "XMLHttpRequest",
981
- "x-checkout-is-mobile-app": "false",
982
- "Origin": "https://www.udemy.com",
983
- "DNT": "1",
984
- "Sec-GPC": "1",
985
- "Connection": "keep-alive",
986
- "Sec-Fetch-Dest": "empty",
987
- "Sec-Fetch-Mode": "cors",
988
- "Sec-Fetch-Site": "same-origin",
989
- "Priority": "u=0",
990
- }
991
- csrftoken = None
992
- for cookie in self.client.cookies:
993
- if cookie.name == "csrftoken":
994
- csrftoken = cookie.value
995
- break
996
-
997
- if csrftoken:
998
- headers["X-CSRFToken"] = csrftoken
999
- else:
1000
- raise ValueError("CSRF token not found")
1001
-
1002
- r = self.client.post(
1003
- "https://www.udemy.com/payment/checkout-submit/",
1004
- json=payload,
1005
- headers=headers,
1006
- )
1007
- try:
1008
- r = r.json()
1009
- except:
1010
- self.print(r.text, color="red")
1011
- self.print("Unknown Error: Report this to the developer", color="red")
1012
- return r
1013
-
1014
- def free_checkout(self, course_id):
1015
- self.client.get(f"https://www.udemy.com/course/subscribe/?courseId={course_id}")
1016
- r = self.client.get(
1017
- f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/?fields%5Bcourse%5D=%40default%2Cbuyable_object_type%2Cprimary_subcategory%2Cis_private"
1018
- ).json()
1019
- return r.get("_class") == "course"
1020
-
1021
- def handle_discounted_course(self, course_id):
1022
- coupon_code = self.extract_course_coupon(self.link)
1023
- amount, coupon_valid = self.check_course(course_id, coupon_code)
1024
- if amount == "retry":
1025
- self.print("Retrying...", color="red")
1026
- time.sleep(1)
1027
- self.handle_discounted_course(course_id)
1028
- elif coupon_valid: # elif coupon_code and coupon_valid:
1029
- self.process_coupon(course_id, coupon_code, amount)
1030
- else:
1031
- self.print("Coupon Expired", color="red")
1032
- self.expired_c += 1
1033
-
1034
- def process_coupon(self, course_id, coupon_code, amount):
1035
- checkout_response = self.discounted_checkout(coupon_code, course_id)
1036
- if msg := checkout_response.get("detail"):
1037
- self.print(msg, color="red")
1038
- try:
1039
- wait_time = int(re.search(r"\d+", checkout_response["detail"]).group(0))
1040
- except:
1041
- self.print(
1042
- "Unknown Error: Report this link to the developer", color="red"
1043
- )
1044
- self.print(checkout_response, color="red")
1045
- wait_time = 60
1046
- time.sleep(wait_time + 1)
1047
- self.process_coupon(course_id, coupon_code, amount)
1048
- elif checkout_response["status"] == "succeeded":
1049
- self.print("Successfully Enrolled To Course :)", color="green")
1050
- self.successfully_enrolled_c += 1
1051
- self.enrolled_courses[course_id] = self.get_now_to_utc()
1052
- self.amount_saved_c += amount
1053
- self.save_course()
1054
- time.sleep(3.7)
1055
- elif checkout_response["status"] == "failed":
1056
- message = checkout_response["message"]
1057
- if "item_already_subscribed" in message:
1058
- self.print("Already Enrolled", color="light blue")
1059
- self.already_enrolled_c += 1
1060
- else:
1061
- self.print("Unknown Error: Report this to the developer", color="red")
1062
- self.print(checkout_response, color="red")
1063
- else:
1064
- self.print("Unknown Error: Report this to the developer", color="red")
1065
- self.print(checkout_response, color="red")
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ import sys
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from datetime import datetime, timezone
9
+ from decimal import Decimal
10
+ from urllib.parse import parse_qs, unquote, urlparse, urlsplit, urlunparse
11
+
12
+ import cloudscraper
13
+ import requests
14
+ import rookiepy
15
+ from bs4 import BeautifulSoup as bs
16
+
17
+ from colors import fb, fc, fg, flb, flg, fm, fr, fy
18
+
19
+ VERSION = "v2.3.2"
20
+
21
+ scraper_dict: dict = {
22
+ "Udemy Freebies": "uf",
23
+ "Tutorial Bar": "tb",
24
+ "Real Discount": "rd",
25
+ "Course Vania": "cv",
26
+ "IDownloadCoupons": "idc",
27
+ "E-next": "en",
28
+ "Discudemy": "du",
29
+ }
30
+
31
+ LINKS = {
32
+ "github": "https://github.com/techtanic/Discounted-Udemy-Course-Enroller",
33
+ "support": "https://techtanic.github.io/duce/support",
34
+ "discord": "https://discord.gg/wFsfhJh4Rh",
35
+ }
36
+
37
+
38
+ class LoginException(Exception):
39
+ """Login Error
40
+
41
+ Args:
42
+ Exception (str): Exception Reason
43
+ """
44
+
45
+ pass
46
+
47
+
48
+ class RaisingThread(threading.Thread):
49
+ def run(self):
50
+ self._exc = None
51
+ try:
52
+ super().run()
53
+ except Exception as e:
54
+ self._exc = e
55
+
56
+ def join(self, timeout=None):
57
+ super().join(timeout=timeout)
58
+ if self._exc:
59
+ raise self._exc
60
+
61
+
62
+ def resource_path(relative_path):
63
+ if hasattr(sys, "_MEIPASS"):
64
+ return os.path.join(sys._MEIPASS, relative_path)
65
+ return os.path.join(os.path.abspath("."), relative_path)
66
+
67
+
68
+ class Scraper:
69
+ """
70
+ Scrapers: RD,TB, CV, IDC, EN, DU, UF
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ site_to_scrape: list = list(scraper_dict.keys()),
76
+ debug: bool = False,
77
+ ):
78
+ self.sites = site_to_scrape
79
+ self.debug = debug
80
+ for site in self.sites:
81
+ code_name = scraper_dict[site]
82
+ setattr(self, f"{code_name}_length", 0)
83
+ setattr(self, f"{code_name}_data", [])
84
+ setattr(self, f"{code_name}_done", False)
85
+ setattr(self, f"{code_name}_progress", 0)
86
+ setattr(self, f"{code_name}_error", "")
87
+
88
+ def get_scraped_courses(self, target: object) -> list:
89
+ threads = []
90
+ scraped_data = {}
91
+ for site in self.sites:
92
+ t = threading.Thread(
93
+ target=target,
94
+ args=(site,),
95
+ daemon=True,
96
+ )
97
+ t.start()
98
+ threads.append(t)
99
+ time.sleep(0.2)
100
+ for t in threads:
101
+ t.join()
102
+ for site in self.sites:
103
+ scraped_data[site] = getattr(self, f"{scraper_dict[site]}_data")
104
+ return scraped_data
105
+
106
+ def append_to_list(self, target: list, title: str, link: str):
107
+ target.append((title, link))
108
+
109
+ def fetch_page_content(self, url: str, headers: dict = None) -> bytes:
110
+ return requests.get(url, headers=headers).content
111
+
112
+ def parse_html(self, content: str):
113
+ return bs(content, "html5lib")
114
+
115
+ def handle_exception(self, site_code: str):
116
+ setattr(self, f"{site_code}_error", traceback.format_exc())
117
+ setattr(self, f"{site_code}_length", -1)
118
+ setattr(self, f"{site_code}_done", True)
119
+ if self.debug:
120
+ print(getattr(self, f"{site_code}_error"))
121
+
122
+ def cleanup_link(self, link: str) -> str:
123
+ parsed_url = urlparse(link)
124
+
125
+ if parsed_url.netloc == "www.udemy.com":
126
+ return link
127
+
128
+ if parsed_url.netloc == "click.linksynergy.com":
129
+ query_params = parse_qs(parsed_url.query)
130
+
131
+ if "RD_PARM1" in query_params:
132
+ return unquote(query_params["RD_PARM1"][0])
133
+ elif "murl" in query_params:
134
+ return unquote(query_params["murl"][0])
135
+ else:
136
+ return ""
137
+ raise ValueError(f"Unknown link format: {link}")
138
+
139
+ def du(self):
140
+ try:
141
+ all_items = []
142
+ head = {
143
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
144
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
145
+ }
146
+
147
+ for page in range(1, 4):
148
+ content = self.fetch_page_content(
149
+ f"https://www.discudemy.com/all/{page}", headers=head
150
+ )
151
+ soup = self.parse_html(content)
152
+ page_items = soup.find_all("a", {"class": "card-header"})
153
+ all_items.extend(page_items)
154
+ self.du_length = len(all_items)
155
+ if self.debug:
156
+ print("Length:", self.du_length)
157
+ for index, item in enumerate(all_items):
158
+ self.du_progress = index
159
+ title = item.string
160
+ url = item["href"].split("/")[-1]
161
+ content = self.fetch_page_content(
162
+ f"https://www.discudemy.com/go/{url}", headers=head
163
+ )
164
+ soup = self.parse_html(content)
165
+ link = soup.find("div", {"class": "ui segment"}).a["href"]
166
+ if self.debug:
167
+ print(title, link)
168
+ self.append_to_list(self.du_data, title, link)
169
+
170
+ except:
171
+ self.handle_exception("du")
172
+ self.du_done = True
173
+ if self.debug:
174
+ print("Return Length:", len(self.du_data))
175
+
176
+ def uf(self):
177
+ try:
178
+ all_items = []
179
+ for page in range(1, 4):
180
+ content = self.fetch_page_content(
181
+ f"https://www.udemyfreebies.com/free-udemy-courses/{page}"
182
+ )
183
+ soup = self.parse_html(content)
184
+ page_items = soup.find_all("a", {"class": "theme-img"})
185
+ all_items.extend(page_items)
186
+ self.uf_length = len(all_items)
187
+ if self.debug:
188
+ print("Length:", self.uf_length)
189
+ for index, item in enumerate(all_items):
190
+ title = item.img["alt"]
191
+ link = requests.get(
192
+ f"https://www.udemyfreebies.com/out/{item['href'].split('/')[4]}"
193
+ ).url
194
+ self.append_to_list(self.uf_data, title, link)
195
+ self.uf_progress = index
196
+
197
+ except:
198
+ self.handle_exception("uf")
199
+ self.uf_done = True
200
+ if self.debug:
201
+ print("Return Length:", len(self.uf_data))
202
+
203
+ def tb(self):
204
+ try:
205
+ all_items = []
206
+
207
+ for page in range(1, 5):
208
+ content = self.fetch_page_content(
209
+ f"https://www.tutorialbar.com/all-courses/page/{page}"
210
+ )
211
+ soup = self.parse_html(content)
212
+ page_items = soup.find_all(
213
+ "h2", class_="mb15 mt0 font110 mobfont100 fontnormal lineheight20"
214
+ )
215
+ all_items.extend(page_items)
216
+ self.tb_length = len(all_items)
217
+ if self.debug:
218
+ print("Length:", self.tb_length)
219
+
220
+ for index, item in enumerate(all_items):
221
+ self.tb_progress = index
222
+ title = item.a.string
223
+ url = item.a["href"]
224
+ content = self.fetch_page_content(url)
225
+ soup = self.parse_html(content)
226
+ link = soup.find("a", class_="btn_offer_block re_track_btn")["href"]
227
+ if "www.udemy.com" in link:
228
+ self.append_to_list(self.tb_data, title, link)
229
+
230
+ except:
231
+ self.handle_exception("tb")
232
+ self.tb_done = True
233
+ if self.debug:
234
+ print("Return Length:", len(self.tb_data))
235
+
236
+ def rd(self):
237
+ all_items = []
238
+
239
+ try:
240
+ headers = {
241
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
242
+ "Host": "cdn.real.discount",
243
+ "Connection": "Keep-Alive",
244
+ "dnt": "1",
245
+ "referer": "https://www.real.discount/",
246
+ }
247
+ try:
248
+ r = requests.get(
249
+ "https://cdn.real.discount/api/courses?page=1&limit=500&sortBy=sale_start&store=Udemy&freeOnly=true",
250
+ headers=headers,
251
+ timeout=(10, 30),
252
+ ).json()
253
+ except requests.exceptions.Timeout:
254
+ self.rd_error = "Timeout"
255
+ self.rd_length = -1
256
+ self.rd_done = True
257
+ return
258
+ all_items.extend(r["items"])
259
+
260
+ self.rd_length = len(all_items)
261
+ if self.debug:
262
+ print("Length:", self.rd_length)
263
+ for index, item in enumerate(all_items):
264
+ self.rd_progress = index
265
+ title: str = item["name"]
266
+ link: str = item["url"]
267
+ link = self.cleanup_link(link)
268
+ if link:
269
+ self.append_to_list(self.rd_data, title, link)
270
+
271
+ except:
272
+ self.handle_exception("rd")
273
+ if self.debug:
274
+ print("Return Length:", len(self.rd_data))
275
+ self.rd_done = True
276
+
277
+ def cv(self):
278
+ try:
279
+ content = self.fetch_page_content("https://coursevania.com/courses/")
280
+ soup = self.parse_html(content)
281
+ try:
282
+ nonce = json.loads(
283
+ re.search(
284
+ r"var stm_lms_nonces = ({.*?});", soup.text, re.DOTALL
285
+ ).group(1)
286
+ )["load_content"]
287
+ if self.debug:
288
+ print("Nonce:", nonce)
289
+ except IndexError:
290
+ self.cv_error = "Nonce not found"
291
+ self.cv_length = -1
292
+ self.cv_done = True
293
+ return
294
+ r = requests.get(
295
+ "https://coursevania.com/wp-admin/admin-ajax.php?&template=courses/grid&args={%22posts_per_page%22:%2260%22}&action=stm_lms_load_content&nonce="
296
+ + nonce
297
+ + "&sort=date_high"
298
+ ).json()
299
+
300
+ soup = self.parse_html(r["content"])
301
+ page_items = soup.find_all(
302
+ "div", {"class": "stm_lms_courses__single--title"}
303
+ )
304
+ self.cv_length = len(page_items)
305
+ if self.debug:
306
+ print("Small Length:", self.cv_length)
307
+ for index, item in enumerate(page_items):
308
+ self.cv_progress = index
309
+ title = item.h5.string
310
+ content = self.fetch_page_content(item.a["href"])
311
+ soup = self.parse_html(content)
312
+ link = soup.find(
313
+ "a",
314
+ {"class": "masterstudy-button-affiliate__link"},
315
+ )["href"]
316
+ self.append_to_list(self.cv_data, title, link)
317
+
318
+ except:
319
+ self.handle_exception("cv")
320
+ self.cv_done = True
321
+ if self.debug:
322
+ print("Return Length:", len(self.cv_data))
323
+
324
+ def idc(self):
325
+ try:
326
+ all_items = []
327
+ for page in range(1, 5):
328
+ content = self.fetch_page_content(
329
+ f"https://idownloadcoupon.com/product-category/udemy/page/{page}"
330
+ )
331
+ soup = self.parse_html(content)
332
+ page_items = soup.find_all(
333
+ "a",
334
+ attrs={
335
+ "class": "woocommerce-LoopProduct-link woocommerce-loop-product__link"
336
+ },
337
+ )
338
+ all_items.extend(page_items)
339
+ self.idc_length = len(all_items)
340
+ if self.debug:
341
+ print("Length:", self.idc_length)
342
+ for index, item in enumerate(all_items):
343
+ self.idc_progress = index
344
+ title = item.h2.string
345
+ link_num = item["href"].split("/")[4]
346
+ if link_num == "85":
347
+ continue
348
+ link = f"https://idownloadcoupon.com/udemy/{link_num}/"
349
+
350
+ r = requests.get(
351
+ link,
352
+ allow_redirects=False,
353
+ )
354
+ link = unquote(r.headers["Location"])
355
+ link = self.cleanup_link(link)
356
+ self.append_to_list(self.idc_data, title, link)
357
+
358
+ except:
359
+ self.handle_exception("idc")
360
+ self.idc_done = True
361
+ if self.debug:
362
+ print("Return Length:", len(self.idc_data))
363
+
364
+ def en(self):
365
+ try:
366
+ all_items = []
367
+ for page in range(1, 6):
368
+ content = self.fetch_page_content(
369
+ f"https://jobs.e-next.in/course/udemy/{page}"
370
+ )
371
+ soup = self.parse_html(content)
372
+ page_items = soup.find_all(
373
+ "a", {"class": "btn btn-secondary btn-sm btn-block"}
374
+ )
375
+ all_items.extend(page_items)
376
+
377
+ self.en_length = len(all_items)
378
+
379
+ if self.debug:
380
+ print("Length:", self.en_length)
381
+ for index, item in enumerate(all_items):
382
+ self.en_progress = index
383
+ content = self.fetch_page_content(item["href"])
384
+ soup = self.parse_html(content)
385
+ title = soup.find("h3").string.strip()
386
+ link = soup.find("a", {"class": "btn btn-primary"})["href"]
387
+ self.append_to_list(self.en_data, title, link)
388
+
389
+ except:
390
+ self.handle_exception("en")
391
+ self.en_done = True
392
+ if self.debug:
393
+ print("Return Length:", len(self.en_data))
394
+ print(self.en_data)
395
+
396
+
397
+ class Udemy:
398
+ def __init__(self, interface: str, debug: bool = False):
399
+ self.interface = interface
400
+ # self.client = cloudscraper.CloudScraper()
401
+ self.client = requests.session()
402
+ headers = {
403
+ "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
404
+ "Accept": "application/json, text/plain, */*",
405
+ "Accept-Language": "en-GB,en;q=0.5",
406
+ "Referer": "https://www.udemy.com/",
407
+ "X-Requested-With": "XMLHttpRequest",
408
+ "DNT": "1",
409
+ "Connection": "keep-alive",
410
+ "Sec-Fetch-Dest": "empty",
411
+ "Sec-Fetch-Mode": "cors",
412
+ "Sec-Fetch-Site": "same-origin",
413
+ "Pragma": "no-cache",
414
+ "Cache-Control": "no-cache",
415
+ }
416
+
417
+ self.client.headers.update(headers)
418
+ self.debug = debug
419
+
420
+ def print(self, content: str, color: str = "red", **kargs):
421
+ content = str(content)
422
+ colours_dict = {
423
+ "yellow": fy,
424
+ "red": fr,
425
+ "blue": fb,
426
+ "light blue": flb,
427
+ "green": fg,
428
+ "light green": flg,
429
+ "cyan": fc,
430
+ "magenta": fm,
431
+ }
432
+ if self.interface == "gui":
433
+ self.window["out"].print(content, text_color=color, **kargs)
434
+ else:
435
+ print(colours_dict[color] + content, **kargs)
436
+
437
+ def get_date_from_utc(self, d: str):
438
+ utc_dt = datetime.strptime(d, "%Y-%m-%dT%H:%M:%SZ")
439
+ dt = utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
440
+ return dt.strftime("%B %d, %Y")
441
+
442
+ def get_now_to_utc(self):
443
+ return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
444
+
445
+ def load_settings(self):
446
+ try:
447
+ with open(f"duce-{self.interface}-settings.json") as f:
448
+ self.settings = json.load(f)
449
+ except FileNotFoundError:
450
+ with open(
451
+ resource_path(f"default-duce-{self.interface}-settings.json")
452
+ ) as f:
453
+ self.settings = json.load(f)
454
+ if (
455
+ self.interface == "cli" and "use_browser_cookies" not in self.settings
456
+ ): # v2.1
457
+ self.settings.get("use_browser_cookies", False)
458
+ # v2.2
459
+ if "course_update_threshold_months" not in self.settings:
460
+ self.settings["course_update_threshold_months"] = 24 # 2 years
461
+
462
+ self.settings["languages"] = dict(
463
+ sorted(self.settings["languages"].items(), key=lambda item: item[0])
464
+ )
465
+ self.save_settings()
466
+ self.title_exclude = "\n".join(self.settings["title_exclude"])
467
+ self.instructor_exclude = "\n".join(self.settings["instructor_exclude"])
468
+
469
+ def save_settings(self):
470
+ with open(f"duce-{self.interface}-settings.json", "w") as f:
471
+ json.dump(self.settings, f, indent=4)
472
+
473
+ def make_cookies(self, client_id: str, access_token: str, csrf_token: str):
474
+ self.cookie_dict = dict(
475
+ client_id=client_id,
476
+ access_token=access_token,
477
+ csrf_token=csrf_token,
478
+ )
479
+
480
+ def fetch_cookies(self):
481
+ """Gets cookies from browser
482
+ Sets cookies_dict, cookie_jar
483
+ """
484
+ cookies = rookiepy.to_cookiejar(rookiepy.load(["www.udemy.com"]))
485
+ self.cookie_dict: dict = requests.utils.dict_from_cookiejar(cookies)
486
+ self.cookie_jar = cookies
487
+
488
+ def get_enrolled_courses(self):
489
+ """Get enrolled courses
490
+ Sets enrolled_courses
491
+
492
+ {slug:enrollment_time}
493
+ """
494
+ next_page = "https://www.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-enroll_time&fields[course]=enrollment_time,url&page_size=100"
495
+ courses = {}
496
+ while next_page:
497
+ r = self.client.get(
498
+ next_page,
499
+ ).json()
500
+ for course in r["results"]:
501
+ slug = course["url"].split("/")[2]
502
+ courses[slug] = course["enrollment_time"]
503
+ next_page = r["next"]
504
+ self.enrolled_courses = courses
505
+
506
+ def compare_versions(self, version1, version2):
507
+ v1_parts = list(map(int, version1.split(".")))
508
+ v2_parts = list(map(int, version2.split(".")))
509
+ max_length = max(len(v1_parts), len(v2_parts))
510
+ v1_parts.extend([0] * (max_length - len(v1_parts)))
511
+ v2_parts.extend([0] * (max_length - len(v2_parts)))
512
+
513
+ for v1, v2 in zip(v1_parts, v2_parts):
514
+ if v1 < v2:
515
+ return -1
516
+ elif v1 > v2:
517
+ return 1
518
+ return 0
519
+
520
+ def check_for_update(self) -> tuple[str, str]:
521
+ r_version = (
522
+ requests.get(
523
+ "https://api.github.com/repos/techtanic/Discounted-Udemy-Course-Enroller/releases/latest"
524
+ )
525
+ .json()["tag_name"]
526
+ .removeprefix("v")
527
+ )
528
+ c_version = VERSION.removeprefix("v")
529
+
530
+ comparison = self.compare_versions(c_version, r_version)
531
+
532
+ if comparison == -1:
533
+ return (
534
+ f"Update {r_version} Available",
535
+ f"Update {r_version} Available",
536
+ )
537
+ elif comparison == 0:
538
+ return (
539
+ f"Login {c_version}",
540
+ f"Discounted-Udemy-Course-Enroller {c_version}",
541
+ )
542
+ else:
543
+ return (
544
+ f"Dev Login {c_version}",
545
+ f"Dev Discounted-Udemy-Course-Enroller {c_version}",
546
+ )
547
+
548
+ def manual_login(self, email: str, password: str):
549
+ """Manual Login to Udemy using email and password and sets cookies
550
+ Args:
551
+ email (str): Email
552
+ password (str): Password
553
+ Raises:
554
+ LoginException: Login Error
555
+ """
556
+ # s = cloudscraper.CloudScraper()
557
+
558
+ s = requests.session()
559
+ r = s.get(
560
+ "https://www.udemy.com/join/signup-popup/?locale=en_US&response_type=html&next=https%3A%2F%2Fwww.udemy.com%2Flogout%2F",
561
+ headers={"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)"},
562
+ # headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0",
563
+ # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
564
+ # 'Accept-Language': 'en-US,en;q=0.5',
565
+ # #'Accept-Encoding': 'gzip, deflate, br',
566
+ # 'DNT': '1',
567
+ # 'Connection': 'keep-alive',
568
+ # 'Upgrade-Insecure-Requests': '1',
569
+ # 'Sec-Fetch-Dest': 'document',
570
+ # 'Sec-Fetch-Mode': 'navigate',
571
+ # 'Sec-Fetch-Site': 'none',
572
+ # 'Sec-Fetch-User': '?1',
573
+ # 'Pragma': 'no-cache',
574
+ # 'Cache-Control': 'no-cache'},
575
+ )
576
+ try:
577
+ csrf_token = r.cookies["csrftoken"]
578
+ except:
579
+ if self.debug:
580
+ print(r.text)
581
+ data = {
582
+ "csrfmiddlewaretoken": csrf_token,
583
+ "locale": "en_US",
584
+ "email": email,
585
+ "password": password,
586
+ }
587
+
588
+ # ss = requests.session()
589
+ s.cookies.update(r.cookies)
590
+ s.headers.update(
591
+ {
592
+ "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
593
+ "Accept": "application/json, text/plain, */*",
594
+ "Accept-Language": "en-GB,en;q=0.5",
595
+ "Referer": "https://www.udemy.com/join/login-popup/?passwordredirect=True&response_type=json",
596
+ "Origin": "https://www.udemy.com",
597
+ "DNT": "1",
598
+ "Host": "www.udemy.com",
599
+ "Connection": "keep-alive",
600
+ "Sec-Fetch-Dest": "empty",
601
+ "Sec-Fetch-Mode": "cors",
602
+ "Sec-Fetch-Site": "same-origin",
603
+ "Pragma": "no-cache",
604
+ "Cache-Control": "no-cache",
605
+ }
606
+ )
607
+ s = cloudscraper.create_scraper(sess=s)
608
+ r = s.post(
609
+ "https://www.udemy.com/join/login-popup/?passwordredirect=True&response_type=json",
610
+ data=data,
611
+ allow_redirects=False,
612
+ )
613
+ if r.text.__contains__("returnUrl"):
614
+ self.make_cookies(
615
+ r.cookies["client_id"], r.cookies["access_token"], csrf_token
616
+ )
617
+ else:
618
+ login_error = r.json()["error"]["data"]["formErrors"][0]
619
+ if login_error[0] == "Y":
620
+ raise LoginException("Too many logins per hour try later")
621
+ elif login_error[0] == "T":
622
+ raise LoginException("Email or password incorrect")
623
+ else:
624
+ raise LoginException(login_error)
625
+
626
+ def get_session_info(self):
627
+ """Get Session info
628
+ Sets Client Session, currency and name
629
+ """
630
+ s = cloudscraper.CloudScraper()
631
+ # headers = {
632
+ # "authorization": "Bearer " + self.cookie_dict["access_token"],
633
+ # "accept": "application/json, text/plain, */*",
634
+ # "x-requested-with": "XMLHttpRequest",
635
+ # "x-forwarded-for": str(
636
+ # ".".join(map(str, (random.randint(0, 255) for _ in range(4))))
637
+ # ),
638
+ # "x-udemy-authorization": "Bearer " + self.cookie_dict["access_token"],
639
+ # "content-type": "application/json;charset=UTF-8",
640
+ # "origin": "https://www.udemy.com",
641
+ # "referer": "https://www.udemy.com/",
642
+ # "dnt": "1",
643
+ # "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
644
+ # }
645
+
646
+ headers = {
647
+ "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
648
+ "Accept": "application/json, text/plain, */*",
649
+ "Accept-Language": "en-GB,en;q=0.5",
650
+ "Referer": "https://www.udemy.com/",
651
+ "X-Requested-With": "XMLHttpRequest",
652
+ "DNT": "1",
653
+ "Connection": "keep-alive",
654
+ "Sec-Fetch-Dest": "empty",
655
+ "Sec-Fetch-Mode": "cors",
656
+ "Sec-Fetch-Site": "same-origin",
657
+ "Pragma": "no-cache",
658
+ "Cache-Control": "no-cache",
659
+ }
660
+ r = s.get(
661
+ "https://www.udemy.com/api-2.0/contexts/me/?header=True",
662
+ cookies=self.cookie_dict,
663
+ headers=headers,
664
+ )
665
+ r = r.json()
666
+ if self.debug:
667
+ print(r)
668
+ if not r["header"]["isLoggedIn"]:
669
+ raise LoginException("Login Failed")
670
+
671
+ self.display_name: str = r["header"]["user"]["display_name"]
672
+ r = s.get(
673
+ "https://www.udemy.com/api-2.0/shopping-carts/me/",
674
+ headers=headers,
675
+ cookies=self.cookie_dict,
676
+ )
677
+ r = r.json()
678
+ self.currency: str = r["user"]["credit"]["currency_code"]
679
+
680
+ s = cloudscraper.CloudScraper()
681
+ s.cookies.update(self.cookie_dict)
682
+ s.headers.update(headers)
683
+ s.keep_alive = False
684
+ self.client = s
685
+ self.get_enrolled_courses()
686
+
687
+ def is_keyword_excluded(self, title: str) -> bool:
688
+ title_words = title.casefold().split()
689
+ for word in title_words:
690
+ word = word.casefold()
691
+ if word in self.title_exclude:
692
+ return True
693
+ return False
694
+
695
+ def is_instructor_excluded(self, instructors: list) -> bool:
696
+ for instructor in instructors:
697
+ if instructor in self.settings["instructor_exclude"]:
698
+ return True
699
+ return False
700
+
701
+ def is_course_updated(self, last_update: str | None) -> bool:
702
+ if not last_update:
703
+ return True
704
+ current_date = datetime.now()
705
+ last_update_date = datetime.strptime(last_update, "%Y-%m-%d")
706
+ # Calculate the difference in years and months
707
+ years = current_date.year - last_update_date.year
708
+ months = current_date.month - last_update_date.month
709
+ days = current_date.day - last_update_date.day
710
+
711
+ # Adjust the months and years if necessary
712
+ if days < 0:
713
+ months -= 1
714
+
715
+ if months < 0:
716
+ years -= 1
717
+ months += 12
718
+
719
+ # Calculate the total month difference
720
+ month_diff = years * 12 + months
721
+ return month_diff < self.settings["course_update_threshold_months"]
722
+
723
+ def is_user_dumb(self) -> bool:
724
+ self.sites = [key for key, value in self.settings["sites"].items() if value]
725
+ self.categories = [
726
+ key for key, value in self.settings["categories"].items() if value
727
+ ]
728
+ self.languages = [
729
+ key for key, value in self.settings["languages"].items() if value
730
+ ]
731
+ self.instructor_exclude = self.settings["instructor_exclude"]
732
+ self.title_exclude = self.settings["title_exclude"]
733
+ self.min_rating = self.settings["min_rating"]
734
+ return not all([bool(self.sites), bool(self.categories), bool(self.languages)])
735
+
736
+ def save_course(self):
737
+ if self.settings["save_txt"]:
738
+ self.txt_file.write(f"{self.title} - {self.link}\n")
739
+ self.txt_file.flush()
740
+ os.fsync(self.txt_file.fileno())
741
+
742
+ def remove_duplicate_courses(self):
743
+ existing_links = set()
744
+ new_data = {}
745
+ for key, courses in self.scraped_data.items():
746
+ new_data[key] = []
747
+ for title, link in courses:
748
+ link = self.normalize_link(link)
749
+ if link not in existing_links:
750
+ new_data[key].append((title, link))
751
+ existing_links.add(link)
752
+ self.scraped_data = {k: v for k, v in new_data.items() if v}
753
+
754
+ def normalize_link(self, link):
755
+ parsed_url = urlparse(link)
756
+ path = (
757
+ parsed_url.path if parsed_url.path.endswith("/") else parsed_url.path + "/"
758
+ )
759
+ return urlunparse(
760
+ (
761
+ parsed_url.scheme,
762
+ parsed_url.netloc,
763
+ path,
764
+ parsed_url.params,
765
+ parsed_url.query,
766
+ parsed_url.fragment,
767
+ )
768
+ )
769
+
770
+ def get_course_id(self, url):
771
+ course = {
772
+ "course_id": None,
773
+ "url": url,
774
+ "is_invalid": False,
775
+ "is_free": None,
776
+ "is_excluded": None,
777
+ "retry": None,
778
+ "msg": "Report to developer",
779
+ }
780
+ url = re.sub(r"\W+$", "", unquote(url))
781
+ try:
782
+ r = self.client.get(url)
783
+ except requests.exceptions.ConnectionError:
784
+ if self.debug:
785
+ print(r.text)
786
+ course["retry"] = True
787
+ return course
788
+ course["url"] = r.url
789
+ soup = bs(r.content, "html5lib")
790
+
791
+ course_id = soup.find("body").get("data-clp-course-id", "invalid")
792
+
793
+ if course_id == "invalid":
794
+ course["is_invalid"] = True
795
+ course["msg"] = "Course ID not found: Report to developer"
796
+ return course
797
+ course["course_id"] = course_id
798
+ dma = json.loads(soup.find("body")["data-module-args"])
799
+ if self.debug:
800
+ with open("debug/dma.json", "w") as f:
801
+ json.dump(dma, f, indent=4)
802
+
803
+ if dma.get("view_restriction"):
804
+ course["is_invalid"] = True
805
+ course["msg"] = dma["serverSideProps"]["limitedAccess"]["errorMessage"][
806
+ "title"
807
+ ]
808
+ return course
809
+
810
+ course["is_free"] = not dma["serverSideProps"]["course"].get("isPaid", True)
811
+ if not self.debug and self.is_course_excluded(dma):
812
+ course["is_excluded"] = True
813
+ return course
814
+
815
+ return course
816
+
817
+ def is_course_excluded(self, dma):
818
+ instructors = [
819
+ i["absolute_url"].split("/")[-2]
820
+ for i in dma["serverSideProps"]["course"]["instructors"]["instructors_info"]
821
+ if i["absolute_url"]
822
+ ]
823
+ lang = dma["serverSideProps"]["course"]["localeSimpleEnglishTitle"]
824
+ cat = dma["serverSideProps"]["topicMenu"]["breadcrumbs"][0]["title"]
825
+ rating = dma["serverSideProps"]["course"]["rating"]
826
+ last_update = dma["serverSideProps"]["course"]["lastUpdateDate"]
827
+
828
+ if not self.is_course_updated(last_update):
829
+ self.print(
830
+ f"Course excluded: Last updated {last_update}", color="light blue"
831
+ )
832
+ elif self.is_instructor_excluded(instructors):
833
+ self.print(f"Instructor excluded: {instructors[0]}", color="light blue")
834
+ elif self.is_keyword_excluded(self.title):
835
+ self.print("Keyword Excluded", color="light blue")
836
+ elif cat not in self.categories:
837
+ self.print(f"Category excluded: {cat}", color="light blue")
838
+ elif lang not in self.languages:
839
+ self.print(f"Language excluded: {lang}", color="light blue")
840
+ elif rating < self.min_rating:
841
+ self.print(f"Low rating: {rating}", color="light blue")
842
+ else:
843
+ return False
844
+ return True
845
+
846
+ def extract_course_coupon(self, url):
847
+ params = parse_qs(urlsplit(url).query)
848
+ return params.get("couponCode", [False])[0]
849
+
850
+ def check_course(self, course_id, coupon_code=None):
851
+ url = f"https://www.udemy.com/api-2.0/course-landing-components/{course_id}/me/?components=purchase"
852
+ if coupon_code:
853
+ url += f",redeem_coupon&couponCode={coupon_code}"
854
+
855
+ r = self.client.get(url).json()
856
+ if self.debug:
857
+ with open("test/check_course.json", "w") as f:
858
+ json.dump(r, f, indent=4)
859
+ amount = (
860
+ r.get("purchase", {})
861
+ .get("data", {})
862
+ .get("list_price", {})
863
+ .get("amount", "retry")
864
+ )
865
+ coupon_valid = False
866
+
867
+ if coupon_code and "redeem_coupon" in r:
868
+ discount = r["purchase"]["data"]["pricing_result"]["discount_percent"]
869
+ status = r["redeem_coupon"]["discount_attempts"][0]["status"]
870
+ coupon_valid = discount == 100 and status == "applied"
871
+
872
+ return Decimal(amount), coupon_valid
873
+
874
+ def start_enrolling(self):
875
+ self.remove_duplicate_courses()
876
+ self.initialize_counters()
877
+ self.setup_txt_file()
878
+
879
+ total_courses = sum(len(courses) for courses in self.scraped_data.values())
880
+ previous_courses_count = 0
881
+ for site_index, (site, courses) in enumerate(self.scraped_data.items()):
882
+ self.print(f"\nSite: {site} [{len(courses)}]", color="cyan")
883
+
884
+ for index, (title, link) in enumerate(courses):
885
+ self.title = title
886
+ self.link = link
887
+ self.print_course_info(previous_courses_count + index, total_courses)
888
+ self.handle_course_enrollment()
889
+ previous_courses_count += len(courses)
890
+
891
+ def initialize_counters(self):
892
+ self.successfully_enrolled_c = 0
893
+ self.already_enrolled_c = 0
894
+ self.expired_c = 0
895
+ self.excluded_c = 0
896
+ self.amount_saved_c = 0
897
+
898
+ def setup_txt_file(self):
899
+ if self.settings["save_txt"]:
900
+ os.makedirs("Courses/", exist_ok=True)
901
+ self.txt_file = open(
902
+ f"Courses/{time.strftime('%Y-%m-%d--%H-%M')}.txt", "w", encoding="utf-8"
903
+ )
904
+
905
+ def print_course_info(self, index, total_courses):
906
+ self.print(f"[{index + 1} / {total_courses}] ", color="magenta", end=" ")
907
+ self.print(self.title, color="yellow", end=" ")
908
+ self.print(self.link, color="blue")
909
+
910
+ def handle_course_enrollment(self):
911
+ slug = self.link.split("/")[4]
912
+
913
+ if slug in self.enrolled_courses:
914
+ self.print(
915
+ f"You purchased this course on {self.get_date_from_utc(self.enrolled_courses[slug])}",
916
+ color="light blue",
917
+ )
918
+ self.already_enrolled_c += 1
919
+ return
920
+
921
+ course = self.get_course_id(self.link)
922
+ if course["is_invalid"]:
923
+ self.print(course["msg"], color="red")
924
+ self.excluded_c += 1
925
+ elif course["retry"]:
926
+ self.print("Retrying...", color="red")
927
+ time.sleep(1)
928
+ self.handle_course_enrollment()
929
+ elif course["is_excluded"]:
930
+ self.excluded_c += 1
931
+ elif course["is_free"]:
932
+ self.handle_free_course(course["course_id"])
933
+ elif not course["is_free"]:
934
+ self.handle_discounted_course(course["course_id"])
935
+ else:
936
+ self.print("Unknown Error: Report this link to the developer", color="red")
937
+ self.excluded_c += 1
938
+
939
+ def handle_free_course(self, course_id):
940
+ if self.settings["discounted_only"]:
941
+ self.print("Free course excluded", color="light blue")
942
+ self.excluded_c += 1
943
+ else:
944
+ success = self.free_checkout(course_id)
945
+ if success:
946
+ self.print("Successfully Subscribed", color="green")
947
+ self.successfully_enrolled_c += 1
948
+ self.save_course()
949
+ else:
950
+ self.print(
951
+ "Unknown Error: Report this link to the developer", color="red"
952
+ )
953
+ self.expired_c += 1
954
+
955
+ def discounted_checkout(self, coupon, course_id) -> dict:
956
+ payload = {
957
+ "checkout_environment": "Marketplace",
958
+ "checkout_event": "Submit",
959
+ "payment_info": {
960
+ "method_id": "0",
961
+ "payment_method": "free-method",
962
+ "payment_vendor": "Free",
963
+ },
964
+ "shopping_info": {
965
+ "items": [
966
+ {
967
+ "buyable": {"id": course_id, "type": "course"},
968
+ "discountInfo": {"code": coupon},
969
+ "price": {"amount": 0, "currency": self.currency.upper()},
970
+ }
971
+ ],
972
+ "is_cart": False,
973
+ },
974
+ }
975
+ headers = {
976
+ "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
977
+ "Accept": "application/json, text/plain, */*",
978
+ "Accept-Language": "en-US",
979
+ "Referer": f"https://www.udemy.com/payment/checkout/express/course/{course_id}/?discountCode={coupon}",
980
+ "Content-Type": "application/json",
981
+ "X-Requested-With": "XMLHttpRequest",
982
+ "x-checkout-is-mobile-app": "false",
983
+ "Origin": "https://www.udemy.com",
984
+ "DNT": "1",
985
+ "Sec-GPC": "1",
986
+ "Connection": "keep-alive",
987
+ "Sec-Fetch-Dest": "empty",
988
+ "Sec-Fetch-Mode": "cors",
989
+ "Sec-Fetch-Site": "same-origin",
990
+ "Priority": "u=0",
991
+ }
992
+ # csrftoken = None
993
+ # for cookie in self.client.cookies:
994
+ # if cookie.name == "csrftoken":
995
+ # csrftoken = cookie.value
996
+ # break
997
+
998
+ # if csrftoken:
999
+ # headers["X-CSRFToken"] = csrftoken
1000
+ # else:
1001
+ # raise ValueError("CSRF token not found")
1002
+
1003
+ r = self.client.post(
1004
+ "https://www.udemy.com/payment/checkout-submit/",
1005
+ json=payload,
1006
+ headers=headers,
1007
+ )
1008
+ try:
1009
+ r = r.json()
1010
+ except:
1011
+ self.print(r.text, color="red")
1012
+ self.print("Unknown Error: Report this to the developer", color="red")
1013
+ return {"status": "failed", "message": "Unknown Error"}
1014
+ return r
1015
+
1016
+ def free_checkout(self, course_id):
1017
+ self.client.get(f"https://www.udemy.com/course/subscribe/?courseId={course_id}")
1018
+ r = self.client.get(
1019
+ f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/?fields%5Bcourse%5D=%40default%2Cbuyable_object_type%2Cprimary_subcategory%2Cis_private"
1020
+ ).json()
1021
+ return r.get("_class") == "course"
1022
+
1023
+ def handle_discounted_course(self, course_id):
1024
+ coupon_code = self.extract_course_coupon(self.link)
1025
+ amount, coupon_valid = self.check_course(course_id, coupon_code)
1026
+ if amount == "retry":
1027
+ self.print("Retrying...", color="red")
1028
+ time.sleep(1)
1029
+ self.handle_discounted_course(course_id)
1030
+ elif coupon_valid: # elif coupon_code and coupon_valid:
1031
+ self.process_coupon(course_id, coupon_code, amount)
1032
+ else:
1033
+ self.print("Coupon Expired", color="red")
1034
+ self.expired_c += 1
1035
+
1036
+ def process_coupon(self, course_id, coupon_code, amount):
1037
+ checkout_response = self.discounted_checkout(coupon_code, course_id)
1038
+ if msg := checkout_response.get("detail"):
1039
+ self.print(msg, color="red")
1040
+ try:
1041
+ wait_time = int(re.search(r"\d+", checkout_response["detail"]).group(0))
1042
+ except:
1043
+ self.print(
1044
+ "Unknown Error: Report this link to the developer", color="red"
1045
+ )
1046
+ self.print(checkout_response, color="red")
1047
+ wait_time = 60
1048
+ time.sleep(wait_time + 1.5)
1049
+ self.process_coupon(course_id, coupon_code, amount)
1050
+ elif checkout_response["status"] == "succeeded":
1051
+ self.print("Successfully Enrolled To Course :)", color="green")
1052
+ self.successfully_enrolled_c += 1
1053
+ self.enrolled_courses[course_id] = self.get_now_to_utc()
1054
+ self.amount_saved_c += amount
1055
+ self.save_course()
1056
+ time.sleep(3.8)
1057
+ elif checkout_response["status"] == "failed":
1058
+ message = checkout_response["message"]
1059
+ if "item_already_subscribed" in message:
1060
+ self.print("Already Enrolled", color="light blue")
1061
+ self.already_enrolled_c += 1
1062
+ else:
1063
+ self.print("Unknown Error: Report this to the developer", color="red")
1064
+ self.print(checkout_response, color="red")
1065
+ else:
1066
+ self.print("Unknown Error: Report this to the developer", color="red")
1067
+ self.print(checkout_response, color="red")