From d058446dae3dfb4124b3a2693eff0ddbcad106dc Mon Sep 17 00:00:00 2001 From: r0oth3x49 Date: Sat, 12 Sep 2020 16:32:54 +0500 Subject: [PATCH] udemy-dl v1.0, added support to download hls based streams, added support to keep webvtt, fixed #477 (added logging to errors and warnings) --- .gitignore | 1 + CHANGELOG.md | 15 + LICENSE | 2 +- README.md | 57 +- udemy-dl.py | 1917 ++++++------------- udemy/__init__.py | 24 +- udemy/_auth.py | 86 - udemy/_compat.py | 116 -- udemy/_extract.py | 659 ------- udemy/_internal.py | 158 -- udemy/_progress.py | 87 - udemy/_shared.py | 719 ------- udemy/_utils.py | 202 -- udemy/auth.py | 120 ++ udemy/{_colorized => colorized}/__init__.py | 6 +- udemy/{_colorized => colorized}/banner.py | 8 +- udemy/{_colorized => colorized}/colors.py | 8 +- udemy/compat.py | 89 + udemy/extract.py | 792 ++++++++ udemy/ffmpeg.py | 245 +++ udemy/{_getpass.py => getpass.py} | 58 +- udemy/internal.py | 208 ++ udemy/logger.py | 297 +++ udemy/progress.py | 170 ++ udemy/{_sanitize.py => sanitize.py} | 116 +- udemy/{_session.py => session.py} | 45 +- udemy/shared.py | 746 ++++++++ udemy/{_udemy.py => udemy.py} | 31 +- udemy/utils.py | 334 ++++ udemy/{_vtt2srt.py => vtt2srt.py} | 84 +- 30 files changed, 3779 insertions(+), 3621 deletions(-) delete mode 100644 udemy/_auth.py delete mode 100644 udemy/_compat.py delete mode 100644 udemy/_extract.py delete mode 100644 udemy/_internal.py delete mode 100644 udemy/_progress.py delete mode 100644 udemy/_shared.py delete mode 100644 udemy/_utils.py create mode 100644 udemy/auth.py rename udemy/{_colorized => colorized}/__init__.py (90%) rename udemy/{_colorized => colorized}/banner.py (90%) rename udemy/{_colorized => colorized}/colors.py (92%) create mode 100644 udemy/compat.py create mode 100644 udemy/extract.py create mode 100644 udemy/ffmpeg.py rename udemy/{_getpass.py => getpass.py} (74%) create mode 100644 udemy/internal.py create mode 100644 udemy/logger.py create mode 100644 udemy/progress.py rename udemy/{_sanitize.py => sanitize.py} (54%) rename udemy/{_session.py => session.py} (53%) create mode 100644 udemy/shared.py rename udemy/{_udemy.py => udemy.py} (62%) create mode 100644 udemy/utils.py rename udemy/{_vtt2srt.py => vtt2srt.py} (56%) diff --git a/.gitignore b/.gitignore index 3cfcbfc..0bee2ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ cookies.txt new_cook.txt new_cookies.txt +.udemy-dl.conf pack.sublime-project pack.sublime-workspace # Byte-compiled / optimized / DLL files diff --git a/CHANGELOG.md b/CHANGELOG.md index 9604754..4c92c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Change Log +## 1.0 (2020-09-12) + +Features: + - Added proper session management. + - Restructure code to make it bit nicer. + - Added proper logging for errors and warning which fixes (#477). + - Added support to download multiple courses from file. + - Added support to download by default just EN subtitle. (could use `--sub-lang` to download others) + - Added switch to keep WebVTT subtitles (option: `--keep-vtt`). + - Added support to fetch/skip HLS streams such as 1080p etc. (option to skip `--skip-hls`). + - Removed `--names`, `--save` switches. + - Removed `--cache` switch as proper session management is added. + - Removed `--unsafe` switch now unicode characters are handled properly in code. + - Added support to download/skip all available assets for a video (options: `--assets-only, --skip-assets`). + ## 0.5 (2018-05-21) Features: diff --git a/LICENSE b/LICENSE index 6c42d8f..9d31ece 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2018 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, diff --git a/README.md b/README.md index 49a7f0d..0d53213 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,17 @@ ## ***Features*** - +- Added proper session management. - Resume capability for a course video. +- Added proper logging errors and warnings. +- Support multiple courses download from file. - Supports organization and individual udemy users both. -- Save course direct download links to a text file (option: `--save`). -- Cache credentials to a file and use it later for login purpose (option: `--cache`). +- Added support to download hls based streams if available. +- Convert WebVTT to SRT but donot delete WebVTT. (option: `--keep-vtt`) +- Skip fetching HLS streams, This will make the fetching fast. (option: `--skip-hls`) - List down course contents and video resolution, suggest the best resolution (option: `--info`). - Download/skip all available subtitles for a video (options: `--sub-only, --skip-sub`). +- Download/skip all available assets for a video (options: `--assets-only, --skip-assets`). - Download specific chapter in a course (option: `-c / --chapter`). - Download specific lecture in a chapter (option: `-l / --lecture`). - Download specific subtitle for a lecture (option: `-s / --sub-lang`). @@ -38,8 +42,6 @@ - Download lecture(s) in requested resolution (option: `-q / --quality`). - Download course to user requested path (option: `-o / --output`). - Authentication using cookies (option: `-k / --cookies`). -- Download/save lecture names (option: `--names`). -- Download lectures containing unsafe *unicode* characters in title/name (option: `--unsafe`). ## ***How to login with cookie*** @@ -56,18 +58,26 @@ access_token=JKU9QNs2IQDBKoYKvOBclSPXN97baf32o1Jo2L9vX ## ***Requirements*** -- Python (2 or 3) +- Python 3 - Python `pip` - Python module `requests` - Python module `colorama` - Python module `unidecode` - Python module `six` +- Python module `cloudscraper` - Python module `requests[security]` or `pyOpenSSL` +- FFmpeg (to downlaod hls based streams properly) ## ***Module Installation*** - pip install -r requirements.txt - + pip install -r requirements.txt + +## ***HLS streams download requirements*** +- You would need FFmpeg to be installed and added to environment variable so that udemy-dl can access. +- Download [FFmpeg from here](https://ffmpeg.org/download.html) +- On ubuntu you can install it via `apt install ffmpeg`. +- Add to environment variables then udemy-dl will be able to use it when download HLS streams. + ## ***Tested on*** - Windows 7/8/8.1/10 @@ -79,7 +89,7 @@ access_token=JKU9QNs2IQDBKoYKvOBclSPXN97baf32o1Jo2L9vX You can download the latest version of udemy-dl by cloning the GitHub repository. - git clone https://github.com/r0oth3x49/udemy-dl.git + git clone https://github.com/r0oth3x49/udemy-dl.git ## ***Usage*** @@ -87,6 +97,10 @@ You can download the latest version of udemy-dl by cloning the GitHub repository ***Download a course*** python udemy-dl.py COURSE_URL + +***Download a courses from file*** + + python udemy-dl.py FILE-CONTAINING-COURSE-URLs ***Download course with specific resolution*** @@ -142,14 +156,11 @@ You can download the latest version of udemy-dl by cloning the GitHub repository

 Author: Nasir khan (r0ot h3x49)
 
-usage: udemy-dl.py [-h] [-v] [-u] [-p] [-k] [-o] [-q] [-c] [-l] [-s]
-                   [--chapter-start] [--chapter-end] [--lecture-start]
-                   [--lecture-end] [--save] [--info] [--cache] [--names]
-                   [--unsafe] [--sub-only] [--skip-sub]
+usage: udemy-dl.py [-h] [-v] [-u] [-p] [-k] [-o] [-q] [-c] [-l] [-s] [--chapter-start] [--chapter-end] [--lecture-start] [--lecture-end] [--info]
+                   [--keep-vtt] [--sub-only] [--skip-sub] [--skip-hls] [--assets-only] [--skip-assets]
                    course
 
-A cross-platform python based utility to download courses from udemy for
-personal offline use.
+A cross-platform python based utility to download courses from udemy for personal offline use.
 
 positional arguments:
   course            Udemy course.
@@ -175,13 +186,13 @@ Advance:
   --lecture-end     Download till specific position within chapter(s).
 
 Others:
-  --save            Do not download but save links to a file.
   --info            List all lectures with available resolution.
-  --cache           Cache your credentials to use it later.
-  --names           Do not download but save lecture names to file.
-  --unsafe          Download all course with unsafe names.
+  --keep-vtt        Keep WebVTT caption(s).
   --sub-only        Download captions/subtitle only.
   --skip-sub        Download course but skip captions/subtitle.
+  --skip-hls        Download course but skip hls streams. (fast fetching).
+  --assets-only     Download asset(s) only.
+  --skip-assets     Download course but skip asset(s).
 
 Example:
   python udemy-dl.py  COURSE_URL
@@ -191,11 +202,5 @@ Example:
 
 
 
-## ***Todo (for next release)***
- - Restructure code.
- - add proper logging for information and errors.
- - add support to download multiple courses from file
- - add support to download just EN subtitles by default
- - add switch to keep vtt subtitles as well.
- - Add support to download 1080p if available. (most waited feature)
+## ***TODO***
  - Add support to download course on a flaky connection.
diff --git a/udemy-dl.py b/udemy-dl.py
index ae29a9e..2cc8fae 100644
--- a/udemy-dl.py
+++ b/udemy-dl.py
@@ -1,1456 +1,609 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 # -*- coding: utf-8 -*-
+# pylint: disable=R,C0330,C0301,C0303
+
+"""
+
+Author  : Nasir Khan (r0ot h3x49)
+Github  : https://github.com/r0oth3x49
+License : MIT
+
+
+Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the
+Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 
+THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""
 
 import os
 import sys
-import time
-import udemy
-import codecs
 import argparse
 
-from pprint import pprint
-from udemy import __version__
-from udemy._colorized import *
-from udemy._compat import pyver
-from udemy._getpass import GetPass
-from udemy._vtt2srt import WebVtt2Srt
-from udemy._progress import ProgressBar
-from udemy._colorized.banner import banner
-from udemy._utils import cache_credentials
-from udemy._utils import use_cached_credentials
-getpass = GetPass()
+import udemy
+from udemy.logger import logger
+from udemy.getpass import getpass
+from udemy.vtt2srt import WebVtt2Srt
+from udemy.progress import ProgressBar
+from udemy.colorized.banner import banner
+from udemy.utils import (
+    to_configs,
+    to_filepath,
+    load_configs,
+    to_human_readable,
+    extract_url_or_courses,
+)
 
 
 class Udemy(WebVtt2Srt, ProgressBar):
+    """Udemy is class which implements downloading/lising and all"""
 
-    def __init__(self, url, username='', password='', cookies=''):
-        self.url        =   url
-        self.username   =   username
-        self.password   =   password
-        self.cookies    =   cookies
+    def __init__(self, url_or_courses, username="", password="", cookies=""):
+        self.username = username
+        self.password = password
+        self.cookies = cookies
+        self.url_or_courses = url_or_courses
         super(Udemy, self).__init__()
 
-    def _write_to_file(self, filepath='', lecture='', names_only=False, unsafe=False):
-        retVal = {}
-        filename = filepath
-        filename += '-names-only.txt' if names_only else ".txt"
-        fmode = "a"
-        if not unsafe:
-            title = lecture.title
-            url = lecture.url
-            url_or_name = url if not names_only else title
-            url_or_name += "\n"
-        if unsafe:
-            title = u'%s' % (lecture.unsafe_title)
-            url = lecture.url
-            url_or_name = url if not names_only else title
-            url_or_name += "\n"
-
-        try:
-            f = codecs.open(filename, fmode, encoding='utf-8', errors='ignore')
-            f.write(url_or_name)
-        except (OSError, Exception, UnicodeDecodeError) as e:
-            retVal = {'status' : 'False', 'msg' : '{}'.format(e)}
-        else:
-            retVal = {'status' : 'True', 'msg' : 'download'}
-            f.close()
-
-        return retVal
+    def download_assets(self, assets, filepath):
+        """This function will simply download the asstes.."""
+        if assets:
+            for asset in assets:
+                title = asset.filename
+                logger.info(msg="Downloading asset(s)", new_line=True, before=True)
+                logger.info(msg=f"Downloading ({title})", new_line=True)
+                try:
+                    retval = asset.download(
+                        filepath=filepath, quiet=True, callback=self.show_progress,
+                    )
+                    msg = retval.get("msg")
+                    if msg == "already downloaded":
+                        logger.already_downloaded(msg=f"Asset : '{title}'")
+                    elif msg == "download":
+                        logger.info(msg=f"Downloaded  ({title})", new_line=True)
+                    else:
+                        logger.download_skipped(msg=f"Asset : '{title}' ", reason=msg)
+                except KeyboardInterrupt:
+                    logger.error(msg="User Interrupted..", new_line=True)
+                    sys.exit(0)
 
-    def course_save(self, path='', quality='', caption_only=False, skip_captions=False, names_only=False, unsafe=False):
-        if not self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as " + fm + sb +"(%s)" % (self.username) +  fg + sb +"...\n")
-        if self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login using cookies ...\n")
-        course = udemy.course(url=self.url, username=self.username, password=self.password, cookies=self.cookies)
-        course_id = course.id
-        course_name = course.title
-        total_lectures = course.lectures
-        total_chapters = course.chapters
-        course_name = (course_name.lower()).replace(' ', '-')
-        chapters = course.get_chapters()
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Course " + fb + sb + "'%s'.\n" % (course_name))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Chapter(s) (%s).\n" % (total_chapters))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (total_lectures))
-        if path:
-            if '~' in path:
-                path    = os.path.expanduser(path)
-            course_path    = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name)
-        else:
-            path        = os.getcwd()
-            course_path = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name)
-        filepath = '%s.txt' % (course_path)
-        if os.path.isfile(filepath):
-            with open(filepath, 'w') as f:
-                f.close()
-        if not names_only:
-            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Writing course content(s) to '%s.txt'\n" % (course_name))
-        if names_only:
-            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Writing course lecture names to '%s-names-only.txt'\n" % (course_name))
-        for chapter in chapters:
-            chapter_id = chapter.id
-            chapter_title = chapter.title
-            lectures = chapter.get_lectures()
-            lectures_count = chapter.lectures
-            for lecture in lectures:
-                lecture_id = lecture.id
-                lecture_streams = lecture.streams
-                lecture_best = lecture.getbest()
-                lecture_assets = lecture.assets
-                lecture_subtitles = lecture.subtitles
-                if quality:
-                    index = 0
-                    while index < len(lecture_streams):
-                        dimension = int(lecture_streams[index].dimention[1])
-                        if dimension == quality:
-                            lecture_best = lecture_streams[index]
-                            break
-                        index += 1
-                    if not lecture_best:
-                        lecture_best = lecture_best
-                if caption_only and not skip_captions:
-                    if lecture_subtitles:
-                        for subtitle in lecture_subtitles:
-                            self._write_to_file(filepath=course_path, lecture=subtitle, names_only=names_only, unsafe=unsafe)
-                    if lecture_assets:
-                        for asset in lecture_assets:
-                            self._write_to_file(filepath=course_path, lecture=asset, names_only=names_only, unsafe=unsafe)
-                elif skip_captions and not caption_only:
-                    if lecture_best:
-                        self._write_to_file(filepath=course_path, lecture=lecture_best, names_only=names_only, unsafe=unsafe)
-                    if lecture_assets:
-                        for asset in lecture_assets:
-                            self._write_to_file(filepath=course_path, lecture=asset, names_only=names_only, unsafe=unsafe)
+    def download_lecture(self, lecture, filepath, current, total, quality):
+        """This function will simply download the lectures.."""
+        if quality:
+            lecture = lecture.get_quality(quality)
+        if lecture:
+            title = lecture.title
+            logger.info(
+                msg=f"Lecture(s) : ({current} of {total})", new_line=True, before=True
+            )
+            logger.info(msg=f"Downloading ({title})", new_line=True)
+            try:
+                retval = lecture.download(
+                    filepath=filepath, quiet=True, callback=self.show_progress,
+                )
+                msg = retval.get("msg")
+                if msg == "already downloaded":
+                    logger.already_downloaded(msg=f"Lecture : '{title}'")
+                elif msg == "download":
+                    logger.info(msg=f"Downloaded  ({title})", new_line=True)
                 else:
-                    if lecture_best:
-                        self._write_to_file(filepath=course_path, lecture=lecture_best, names_only=names_only, unsafe=unsafe)
-                    if lecture_assets:
-                        for asset in lecture_assets:
-                            self._write_to_file(filepath=course_path, lecture=asset, names_only=names_only, unsafe=unsafe)
-                    if lecture_subtitles:
-                        for subtitle in lecture_subtitles:
-                            self._write_to_file(filepath=course_path, lecture=subtitle, names_only=names_only, unsafe=unsafe)
-        if not names_only:
-            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Written successfully under '{name}.txt'.\n".format(name=course_path))
-        if names_only:
-            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Written successfully under '{name}-names-only.txt'.\n".format(name=course_path))
+                    logger.download_skipped(msg=f"Lecture : '{title}' ", reason=msg)
+            except KeyboardInterrupt:
+                logger.error(msg="User Interrupted..", new_line=True)
+                sys.exit(0)
+
+    def downalod_subtitles(self, subtitles, filepath, language="en", keep_vtt=False):
+        """This function will simply download the subtitles.."""
+        if language and subtitles:
+            subtitle = subtitles.pop()
+            subtitles = subtitle.get_subtitle(language)
+        if subtitles:
+            for sub in subtitles:
+                title = f"{sub.title}.{sub.language}"
+                filename = os.path.join(filepath, sub.filename)
+                logger.info(msg="Downloading subtitle(s)", new_line=True, before=True)
+                logger.info(msg=f"Downloading ({title})", new_line=True)
+                try:
+                    retval = sub.download(
+                        filepath=filepath, quiet=True, callback=self.show_progress,
+                    )
+                    msg = retval.get("msg")
+                    if msg == "already downloaded":
+                        logger.already_downloaded(msg=f"Subtitle : '{title}'")
+                    elif msg == "download":
+                        logger.info(msg=f"Downloaded  ({title})", new_line=True)
+                        self.convert(filename=filename, keep_vtt=keep_vtt)
+                    else:
+                        logger.download_skipped(
+                            msg=f"Subtitle : '{title}' ", reason=msg
+                        )
+                except KeyboardInterrupt:
+                    logger.error(msg="User Interrupted..", new_line=True)
+                    sys.exit(0)
 
-    def course_list_down(self, chapter_number='', lecture_number='', unsafe=False):
+    def course_listdown(
+        self,
+        chapter_number=None,
+        chapter_start=None,
+        chapter_end=None,
+        lecture_number=None,
+        lecture_start=None,
+        lecture_end=None,
+        skip_hls_stream=False,
+    ):
+        """This function will listdown the course contents .."""
         if not self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as " + fm + sb +"(%s)" % (self.username) +  fg + sb +"...\n")
+            logger.info(msg="Trying to login as", status=self.username)
         if self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login using cookies ...\n")
-        course = udemy.course(url=self.url, username=self.username, password=self.password, cookies=self.cookies)
-        course_id = course.id
-        course_name = course.title
-        total_lectures = course.lectures
-        total_chapters = course.chapters
-        chapters = course.get_chapters()
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Course " + fb + sb + "'%s'.\n" % (course_name))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Chapter(s) (%s).\n" % (total_chapters))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (total_lectures))
-        if chapter_number and chapter_number > 0 and chapter_number <= total_chapters:
-            chapter = chapters[chapter_number-1]
-            chapter_id = chapter.id
-            chapter_title = chapter.title if not unsafe else "%02d" % (int(chapter.index))
-            lectures = chapter.get_lectures()
-            lectures_count = chapter.lectures
-            sys.stdout.write ('\n' + fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s-%s)\n" % (chapter_title, chapter_id))
-            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (lectures_count))
-            if lecture_number and lecture_number > 0 and lecture_number <= lectures_count:
-                lecture = lectures[lecture_number-1]
-                lecture_id = lecture.id
-                lecture_streams = lecture.streams
-                lecture_best = lecture.getbest()
-                lecture_assets = lecture.assets
-                lecture_subtitles = lecture.subtitles
-                if lecture_streams:
-                    sys.stdout.write(fc + sd + "     - " + fy + sb + "duration   : " + fm + sb + str(lecture.duration)+ fy + sb + ".\n")
-                    sys.stdout.write(fc + sd + "     - " + fy + sb + "Lecture id : " + fm + sb + str(lecture_id)+ fy + sb + ".\n")
-                    for stream in lecture_streams:
-                        content_length = stream.get_filesize()
-                        if content_length != 0:
-                            if content_length <= 1048576.00:
-                                size = round(float(content_length) / 1024.00, 2)
-                                sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                in_MB = 'KB' if size < 1024.00 else 'MB'
-                            else:
-                                size = round(float(content_length) / 1048576, 2)
-                                sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                in_MB = "MB " if size < 1024.00 else 'GB '
-                            if lecture_best.dimention[1] == stream.dimention[1]:
-                                in_MB = in_MB + fc + sb + "(Best)" + fg + sd
-                            sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(stream), stream.dimention[1] + 'p', sz, in_MB, fy, sb))
-                if lecture_assets:
-                    for asset in lecture_assets:
-                        if asset.mediatype != 'external_link':
-                            content_length = asset.get_filesize()
-                            if content_length != 0:
-                                if content_length <= 1048576.00:
-                                    size = round(float(content_length) / 1024.00, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = 'KB' if size < 1024.00 else 'MB'
-                                else:
-                                    size = round(float(content_length) / 1048576, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = "MB " if size < 1024.00 else 'GB '
-                                sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(asset), asset.extension, sz, in_MB, fy, sb))
-                if lecture_subtitles:
-                    for subtitle in lecture_subtitles:
-                        content_length = subtitle.get_filesize()
-                        if content_length != 0:
-                            if content_length <= 1048576.00:
-                                size = round(float(content_length) / 1024.00, 2)
-                                sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                in_MB = 'KB' if size < 1024.00 else 'MB'
-                            else:
-                                size = round(float(content_length) / 1048576, 2)
-                                sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                in_MB = "MB " if size < 1024.00 else 'GB '
-                            sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(subtitle), subtitle.extension, sz, in_MB, fy, sb))
-            else:
-                for lecture in lectures:
-                    lecture_id = lecture.id
-                    lecture_streams = lecture.streams
-                    lecture_best = lecture.getbest()
-                    lecture_assets = lecture.assets
-                    lecture_subtitles = lecture.subtitles
-                    if lecture_streams:
-                        sys.stdout.write(fc + sd + "     - " + fy + sb + "duration   : " + fm + sb + str(lecture.duration)+ fy + sb + ".\n")
-                        sys.stdout.write(fc + sd + "     - " + fy + sb + "Lecture id : " + fm + sb + str(lecture_id)+ fy + sb + ".\n")
-                        for stream in lecture_streams:
-                            content_length = stream.get_filesize()
-                            if content_length != 0:
-                                if content_length <= 1048576.00:
-                                    size = round(float(content_length) / 1024.00, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = 'KB' if size < 1024.00 else 'MB'
-                                else:
-                                    size = round(float(content_length) / 1048576, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = "MB " if size < 1024.00 else 'GB '
-                                if lecture_best.dimention[1] == stream.dimention[1]:
-                                    in_MB = in_MB + fc + sb + "(Best)" + fg + sd
-                                sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(stream), stream.dimention[1] + 'p', sz, in_MB, fy, sb))
-                    if lecture_assets:
-                        for asset in lecture_assets:
-                            if asset.mediatype != 'external_link':
-                                content_length = asset.get_filesize()
-                                if content_length != 0:
-                                    if content_length <= 1048576.00:
-                                        size = round(float(content_length) / 1024.00, 2)
-                                        sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                        in_MB = 'KB' if size < 1024.00 else 'MB'
-                                    else:
-                                        size = round(float(content_length) / 1048576, 2)
-                                        sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                        in_MB = "MB " if size < 1024.00 else 'GB '
-                                    sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(asset), asset.extension, sz, in_MB, fy, sb))
-                    if lecture_subtitles:
-                        for subtitle in lecture_subtitles:
-                            content_length = subtitle.get_filesize()
-                            if content_length != 0:
-                                if content_length <= 1048576.00:
-                                    size = round(float(content_length) / 1024.00, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = 'KB' if size < 1024.00 else 'MB'
-                                else:
-                                    size = round(float(content_length) / 1048576, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = "MB " if size < 1024.00 else 'GB '
-                                sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(subtitle), subtitle.extension, sz, in_MB, fy, sb))
-        else:
+            logger.info(msg="Trying to login using session cookie", new_line=True)
+        for url in self.url_or_courses:
+            course = udemy.course(
+                url=url,
+                username=self.username,
+                password=self.password,
+                cookies=self.cookies,
+                skip_hls_stream=skip_hls_stream,
+            )
+            course_name = course.title
+            chapters = course.get_chapters(
+                chapter_number=chapter_number,
+                chapter_start=chapter_start,
+                chapter_end=chapter_end,
+            )
+            total_lectures = course.lectures
+            total_chapters = course.chapters
+            logger.success(msg=course_name, course=True)
+            logger.info(msg=f"Chapter(s) ({total_chapters})", new_line=True)
+            logger.info(msg=f"Lecture(s) ({total_lectures})", new_line=True)
             for chapter in chapters:
                 chapter_id = chapter.id
-                chapter_title = chapter.title if not unsafe else "%02d" % (int(chapter.index))
-                lectures = chapter.get_lectures()
+                chapter_title = chapter.title
+                lectures = chapter.get_lectures(
+                    lecture_number=lecture_number,
+                    lecture_start=lecture_start,
+                    lecture_end=lecture_end,
+                )
                 lectures_count = chapter.lectures
-                sys.stdout.write ('\n' + fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s-%s)\n" % (chapter_title, chapter_id))
-                sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (lectures_count))
+                logger.info(
+                    msg=f"Chapter ({chapter_title}-{chapter_id})",
+                    new_line=True,
+                    before=True,
+                    cc=15,
+                    cc_msg=15,
+                )
+                logger.info(msg=f"Lecture(s) ({lectures_count})", new_line=True)
                 for lecture in lectures:
                     lecture_id = lecture.id
                     lecture_streams = lecture.streams
                     lecture_best = lecture.getbest()
                     lecture_assets = lecture.assets
                     lecture_subtitles = lecture.subtitles
-                    if lecture_streams:
-                        sys.stdout.write(fc + sd + "     - " + fy + sb + "duration   : " + fm + sb + str(lecture.duration)+ fy + sb + ".\n")
-                        sys.stdout.write(fc + sd + "     - " + fy + sb + "Lecture id : " + fm + sb + str(lecture_id)+ fy + sb + ".\n")
-                        for stream in lecture_streams:
+                    if not lecture_streams:
+                        continue
+                    logger.info(
+                        indent="     - ",
+                        msg="duration   : ",
+                        new_line=True,
+                        cc=80,
+                        cc_msg=10,
+                        post_msg=f"{lecture.duration}.",
+                        cc_pmsg=80,
+                    )
+                    logger.info(
+                        indent="     - ",
+                        msg="Lecture id : ",
+                        new_line=True,
+                        cc=80,
+                        cc_msg=10,
+                        post_msg=f"{lecture_id}.",
+                        cc_pmsg=80,
+                    )
+                    indent = "\t- "
+                    for stream in lecture_streams:
+                        post_msg = None
+                        if stream.is_hls:
+                            human_readable = ""
+                        if not stream.is_hls:
                             content_length = stream.get_filesize()
-                            if content_length != 0:
-                                if content_length <= 1048576.00:
-                                    size = round(float(content_length) / 1024.00, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = 'KB' if size < 1024.00 else 'MB'
-                                else:
-                                    size = round(float(content_length) / 1048576, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = "MB " if size < 1024.00 else 'GB '
-                                if lecture_best.dimention[1] == stream.dimention[1]:
-                                    in_MB = in_MB + fc + sb + "(Best)" + fg + sd
-                                sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(stream), stream.dimention[1] + 'p', sz, in_MB, fy, sb))
+                            if content_length == 0:
+                                continue
+                            human_readable = to_human_readable(content_length)
+                            if lecture_best.quality == stream.quality:
+                                post_msg = "(Best)"
+                        msg = "{:<22} {:<8}{}".format(
+                            f"{stream}", f"{stream.quality}p", human_readable
+                        )
+                        logger.info(
+                            indent=indent,
+                            msg=msg,
+                            new_line=True,
+                            cc=15,
+                            post_msg=post_msg,
+                            cc_pmsg=30,
+                        )
                     if lecture_assets:
                         for asset in lecture_assets:
-                            if asset.mediatype != 'external_link':
-                                content_length = asset.get_filesize()
-                                if content_length != 0:
-                                    if content_length <= 1048576.00:
-                                        size = round(float(content_length) / 1024.00, 2)
-                                        sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                        in_MB = 'KB' if size < 1024.00 else 'MB'
-                                    else:
-                                        size = round(float(content_length) / 1048576, 2)
-                                        sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                        in_MB = "MB " if size < 1024.00 else 'GB '
-                                    sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(asset), asset.extension, sz, in_MB, fy, sb))
+                            if asset.mediatype == "external_link":
+                                continue
+                            content_length = asset.get_filesize()
+                            if content_length == 0:
+                                continue
+                            human_readable = to_human_readable(content_length)
+                            msg = "{:<22} {:<8}{}".format(
+                                f"{asset}", asset.extension, human_readable
+                            )
+                            logger.info(
+                                indent=indent, msg=msg, new_line=True, cc=15,
+                            )
                     if lecture_subtitles:
-                        for subtitle in lecture_subtitles:
-                            content_length = subtitle.get_filesize()
-                            if content_length != 0:
-                                if content_length <= 1048576.00:
-                                    size = round(float(content_length) / 1024.00, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = 'KB' if size < 1024.00 else 'MB'
-                                else:
-                                    size = round(float(content_length) / 1048576, 2)
-                                    sz = format(size if size < 1024.00 else size/1024.00, '.2f')
-                                    in_MB = "MB " if size < 1024.00 else 'GB '
-                                sys.stdout.write('\t- ' + fg + sd + "{:<22} {:<8}{}{}{}{}\n".format(str(subtitle), subtitle.extension, sz, in_MB, fy, sb))
-
-    def download_assets(self, lecture_assets='', filepath='', unsafe=False):
-        if lecture_assets:
-            for assets in lecture_assets:
-                title = assets.filename if not unsafe else "%s" % (assets)
-                mediatype = assets.mediatype
-                if mediatype == "external_link":
-                    assets.download(filepath=filepath, unsafe=unsafe, quiet=True, callback=self.show_progress)
-                else:
-                    sys.stdout.write(fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading asset(s)\n")
-                    sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)\n" % (title))
-                    try:
-                        retval = assets.download(filepath=filepath, unsafe=unsafe, quiet=True, callback=self.show_progress)
-                    except KeyboardInterrupt:
-                        sys.stdout.write (fc + sd + "\n[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                        sys.exit(0)
-                    else:
-                        msg     = retval.get('msg')
-                        if msg == 'already downloaded':
-                            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Asset : '%s' " % (title) + fy + sb + "(already downloaded).\n")
-                        elif msg == 'download':
-                            sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded  (%s)\n" % (title))
-                        else:
-                            sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Asset : '%s' " % (title) + fc + sb + "(download skipped).\n")
-                            sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}\n".format(msg))
-
-    def download_subtitles(self, lecture_subtitles='', language='', filepath='', unsafe=False):
-        if language:
-            _lecture_subtitles = [i for i in lecture_subtitles if i.language == language]
-            if _lecture_subtitles:
-                lecture_subtitles = _lecture_subtitles
-        if lecture_subtitles:
-            for subtitles in lecture_subtitles:
-                title = subtitles.title + '-' + subtitles.language if not unsafe else "%s" % (subtitles)
-                if not unsafe:
-                    filename = "%s\\%s" % (filepath, subtitles.filename) if os.name == 'nt' else "%s/%s" % (filepath, subtitles.filename)
-                if unsafe:
-                    filename = u"%s\\%s" % (filepath, subtitles.unsafe_filename) if os.name == 'nt' else u"%s/%s" % (filepath, subtitles.unsafe_filename)
-
-                sys.stdout.write(fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading subtitle(s)\n")
-                sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)\n" % (title))
-                
-                try:
-                    retval = subtitles.download(filepath=filepath, unsafe=unsafe, quiet=True, callback=self.show_progress)
-                except KeyboardInterrupt:
-                    sys.stdout.write (fc + sd + "\n[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                    sys.exit(0)
-                else:
-                    msg     = retval.get('msg')
-                    if msg == 'already downloaded':
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Subtitle : '%s' " % (title) + fy + sb + "(already downloaded).\n")
-                        self.convert(filename=filename)
-                    elif msg == 'download':
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded  (%s)\n" % (title))
-                        self.convert(filename=filename)
-                    else:
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Subtitle : '%s' " % (title) + fc + sb + "(download skipped).\n")
-                        sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}\n".format(msg))
-
-    def download_lectures(self, lecture_best='', lecture_title='', inner_index='', lectures_count='', filepath='', unsafe=False):
-        if lecture_best:
-            sys.stdout.write(fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) : ({index} of {total})\n".format(index=inner_index, total=lectures_count))
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)\n" % (lecture_title))
-            try:
-                retval = lecture_best.download(filepath=filepath, unsafe=unsafe, quiet=True, callback=self.show_progress)
-            except KeyboardInterrupt:
-                sys.stdout.write (fc + sd + "\n[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                sys.exit(0)
-            else:
-                msg     = retval.get('msg')
-                if msg == 'already downloaded':
-                    if not unsafe:
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_title) + fy + sb + "(already downloaded).\n")
-                    if unsafe:
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "'%s' " % (lecture_title) + fy + sb + "(already downloaded).\n")
-                elif msg == 'download':
-                    sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded  (%s)\n" % (lecture_title))
-                else:
-                    if not unsafe:
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_title) + fc + sb + "(download skipped).\n")
-                    if unsafe:
-                        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "'%s' " % (lecture_title) + fc + sb + "(download skipped).\n")
-                    sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}\n".format(msg))
-
-    def download_captions_only(self, lecture_subtitles='', language='', lecture_assets='', filepath='', unsafe=False):
-        if lecture_subtitles:
-            self.download_subtitles(lecture_subtitles=lecture_subtitles, language=language, filepath=filepath, unsafe=unsafe)
-        if lecture_assets:
-            self.download_assets(lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-
-    def download_lectures_only(self, lecture_best='', lecture_title='', inner_index='', lectures_count='', lecture_assets='', filepath='', unsafe=False):
-        if lecture_best:
-            self.download_lectures(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, filepath=filepath, unsafe=unsafe)
-        if lecture_assets:
-            self.download_assets(lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-
-    def download_lectures_and_captions(self, lecture_best='', lecture_title='', inner_index='', lectures_count='', lecture_subtitles='', language='', lecture_assets='', filepath='', unsafe=False):
-        if lecture_best:
-            self.download_lectures(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, filepath=filepath, unsafe=unsafe)
-        if lecture_subtitles:
-            self.download_subtitles(lecture_subtitles=lecture_subtitles, language=language, filepath=filepath, unsafe=unsafe)
-        if lecture_assets:
-            self.download_assets(lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-
-    def course_download(self, path='', quality='', language='', caption_only=False, skip_captions=False, unsafe=False):
-        if not self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as " + fm + sb +"(%s)" % (self.username) +  fg + sb +"...\n")
-        if self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login using cookies ...\n")
-        course = udemy.course(url=self.url, username=self.username, password=self.password, cookies=self.cookies)
-        course_id = course.id
-        course_name = course.title
-        chapters = course.get_chapters()
-        total_lectures = course.lectures
-        total_chapters = course.chapters
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Course " + fb + sb + "'%s'.\n" % (course_name))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Chapter(s) (%s).\n" % (total_chapters))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (total_lectures))
-        if path:
-            if '~' in path:
-                path    = os.path.expanduser(path)
-            course_path    = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name)
-        else:
-            path        = os.getcwd()
-            course_path = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name)
-        for chapter in chapters:
-            chapter_id = chapter.id
-            chapter_index = chapter.index
-            chapter_title = chapter.title
-            lectures = chapter.get_lectures()
-            lectures_count = chapter.lectures
-            if unsafe:
-                filepath = u"%s\\%s" % (course_path, chapter.unsafe_title) if os.name == 'nt' else u"%s/%s" % (course_path, chapter.unsafe_title)
-            if not unsafe:
-                filepath = "%s\\%s" % (course_path, chapter_title) if os.name == 'nt' else "%s/%s" % (course_path, chapter_title)
-            try:
-                os.makedirs(filepath)
-            except Exception as e:
-                pass
-            sys.stdout.write (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fm + sb + "Downloading chapter : ({index} of {total})\n".format(index=chapter_index, total=total_chapters))
-            if not unsafe:
-                sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s)\n" % (chapter_title))
-                sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Found (%s) lectures ...\n" % (lectures_count))
-            if unsafe:
-                sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%02d-%s)\n" % (int(chapter_index), chapter_id))
-                sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (lectures_count))
-            inner_index = 1
-            for lecture in lectures:
-                lecture_id = lecture.id
-                lecture_index = lecture.index
-                lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
-                lecture_assets = lecture.assets
-                lecture_subtitles = lecture.subtitles
-                lecture_best = lecture.getbest()
-                lecture_streams = lecture.streams
-                if caption_only and not skip_captions:
-                    self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                elif skip_captions and not caption_only:
-                    if quality:
-                        index = 0
-                        while index < len(lecture_streams):
-                            dimension = int(lecture_streams[index].dimention[1])
-                            if dimension == quality:
-                                lecture_best = lecture_streams[index]
-                                break
-                            index += 1
-                        if not lecture_best:
-                            lecture_best = lecture_best
-                    if lecture.html:
-                        lecture.dump(filepath=filepath, unsafe=unsafe)
-                    self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                else:
-                    if quality:
-                        index = 0
-                        while index < len(lecture_streams):
-                            dimension = int(lecture_streams[index].dimention[1])
-                            if dimension == quality:
-                                lecture_best = lecture_streams[index]
-                                break
-                            index += 1
-                        if not lecture_best:
-                            lecture_best = lecture_best
-                    if lecture.html:
-                        lecture.dump(filepath=filepath, unsafe=unsafe)
-                    self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                inner_index += 1
-
-    def chapter_download(self, chapter_number='', chapter_start='', chapter_end='', lecture_number='', lecture_start='', lecture_end='', path='', quality='', language='', caption_only=False, skip_captions=False, unsafe=False):
+                        for sub in lecture_subtitles:
+                            content_length = sub.get_filesize()
+                            if content_length == 0:
+                                continue
+                            human_readable = to_human_readable(content_length)
+                            msg = "{:<22} {:<8}{}".format(
+                                f"{sub}", sub.extension, human_readable
+                            )
+                            logger.info(
+                                indent=indent, msg=msg, new_line=True, cc=15,
+                            )
+
+    def course_download(
+        self,
+        path="",
+        quality="",
+        language="en",
+        dl_assets=True,
+        dl_lecture=True,
+        dl_subtitles=True,
+        chapter_number=None,
+        chapter_start=None,
+        chapter_end=None,
+        lecture_number=None,
+        lecture_start=None,
+        lecture_end=None,
+        logs_filepath=None,
+        keep_vtt=False,
+        skip_hls_stream=False,
+    ):
+        """This function will download the course contents .."""
         if not self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as " + fm + sb +"(%s)" % (self.username) +  fg + sb +"...\n")
+            logger.info(msg="Trying to login as", status=self.username)
         if self.cookies:
-            sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login using cookies ...\n")
-        course = udemy.course(url=self.url, username=self.username, password=self.password, cookies=self.cookies)
-        course_id = course.id
-        course_name = course.title
-        chapters = course.get_chapters()
-        total_lectures = course.lectures
-        total_chapters = course.chapters
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Course " + fb + sb + "'%s'.\n" % (course_name))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Chapter(s) (%s).\n" % (total_chapters))
-        sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (total_lectures))
-        if path:
-            if '~' in path:
-                path    = os.path.expanduser(path)
-            course_path    = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name)
-        else:
-            path        = os.getcwd()
-            course_path = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name)
-        _lectures_start, _lectures_end = lecture_start, lecture_end
-        if chapter_start and not chapter_end:
-            chapter_end = total_chapters
-        if chapter_number and chapter_number > 0 and chapter_number <= total_chapters:
-            chapter = chapters[chapter_number-1]
-            if chapter:
-                chapter_id = chapter.id
+            logger.info(msg="Trying to login using session cookie", new_line=True)
+        for url in self.url_or_courses:
+            course = udemy.course(
+                url=url,
+                username=self.username,
+                password=self.password,
+                cookies=self.cookies,
+                skip_hls_stream=skip_hls_stream,
+            )
+            course_name = course.title
+            if path:
+                if "~" in path:
+                    path = os.path.expanduser(path)
+            course_path = os.path.join(path, course_name)
+            if not logs_filepath:
+                logs_filepath = os.path.join(course_path, "udemy-dl.log")
+            chapters = course.get_chapters(
+                chapter_number=chapter_number,
+                chapter_start=chapter_start,
+                chapter_end=chapter_end,
+            )
+            total_lectures = course.lectures
+            total_chapters = course.chapters
+            logger.success(msg=course_name, course=True)
+            logger.info(msg=f"Chapter(s) ({total_chapters})", new_line=True)
+            logger.info(msg=f"Lecture(s) ({total_lectures})", new_line=True)
+            for chapter in chapters:
                 chapter_index = chapter.index
                 chapter_title = chapter.title
-                lectures = chapter.get_lectures()
+                lectures = chapter.get_lectures(
+                    lecture_number=lecture_number,
+                    lecture_start=lecture_start,
+                    lecture_end=lecture_end,
+                )
                 lectures_count = chapter.lectures
-                if lecture_end and lecture_end > lectures_count:
-                    lecture_end = lectures_count
-
-                if unsafe:
-                    filepath = u"%s\\%s" % (course_path, chapter.unsafe_title) if os.name == 'nt' else u"%s/%s" % (course_path, chapter.unsafe_title)
-                if not unsafe:
-                    filepath = "%s\\%s" % (course_path, chapter_title) if os.name == 'nt' else "%s/%s" % (course_path, chapter_title)
-                try:
-                    os.makedirs(filepath)
-                except Exception as e:
-                    pass
-                sys.stdout.write (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fm + sb + "Downloading chapter : ({index})\n".format(index=chapter_index))
-                if not unsafe:
-                    sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s)\n" % (chapter_title))
-                    sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Found (%s) lectures ...\n" % (lectures_count))
-                if unsafe:
-                    sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%02d-%s)\n" % (int(chapter_index), chapter_id))
-                    sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (lectures_count))
-                lecture_start = _lectures_start
-                lecture_end = lectures_count if lecture_start and not lecture_end else _lectures_end
-                if lecture_number and lecture_number > 0 and lecture_number <= lectures_count:
-                    lecture = lectures[lecture_number-1]
-                    lecture_id = lecture.id
-                    lecture_index = lecture.index
-                    lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
+                filepath = to_filepath(course_path, chapter_title)
+                logger.set_log_filepath(logs_filepath)
+                chapter_progress = (
+                    chapter_index
+                    if chapter_number
+                    else f"{chapter_index} of {total_chapters}"
+                )
+                logger.info(
+                    msg=f"Downloading chapter : ({chapter_progress})",
+                    new_line=True,
+                    before=True,
+                    cc=80,
+                    cc_msg=80,
+                )
+                logger.info(
+                    msg=f"Chapter ({chapter_title})", new_line=True, cc=15, cc_msg=60
+                )
+                logger.info(
+                    msg=f"Found ({lectures_count}) lecture(s).", new_line=True,
+                )
+                lecture_index = 0
+                if lecture_number:
+                    lecture_index = lecture_number - 1
+                if lecture_start:
+                    lecture_index = lecture_start - 1
+                if lecture_index < 0:
+                    lecture_index = 0
+                for lecture in lectures:
                     lecture_assets = lecture.assets
                     lecture_subtitles = lecture.subtitles
                     lecture_best = lecture.getbest()
-                    lecture_streams = lecture.streams
-                    if caption_only and not skip_captions:
-                        self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                    elif skip_captions and not caption_only:
-                        if quality:
-                            index = 0
-                            while index < len(lecture_streams):
-                                dimension = int(lecture_streams[index].dimention[1])
-                                if dimension == quality:
-                                    lecture_best = lecture_streams[index]
-                                    break
-                                index += 1
-                            if not lecture_best:
-                                lecture_best = lecture_best
+                    if dl_lecture:
+                        lecture_index = lecture_index + 1
                         if lecture.html:
                             lecture.dump(filepath=filepath)
-                        self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_number, lectures_count=lectures_count, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                    else:
-                        if quality:
-                            index = 0
-                            while index < len(lecture_streams):
-                                dimension = int(lecture_streams[index].dimention[1])
-                                if dimension == quality:
-                                    lecture_best = lecture_streams[index]
-                                    break
-                                index += 1
-                            if not lecture_best:
-                                lecture_best = lecture_best
-                        if lecture.html:
-                            lecture.dump(filepath=filepath)
-                        self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_number, lectures_count=lectures_count, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                elif lecture_start and lecture_start > 0 and lecture_start <= lecture_end and lecture_end <= lectures_count:
-                    while lecture_start <= lecture_end:
-                        lecture = lectures[lecture_start-1]
-                        lecture_id = lecture.id
-                        lecture_index = lecture.index
-                        lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
-                        lecture_assets = lecture.assets
-                        lecture_subtitles = lecture.subtitles
-                        lecture_best = lecture.getbest()
-                        lecture_streams = lecture.streams
-                        if caption_only and not skip_captions:
-                            self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        elif skip_captions and not caption_only:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            if lecture.html:
-                                lecture.dump(filepath=filepath, unsafe=unsafe)
-                            self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_start, lectures_count=lecture_end, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        else:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            if lecture.html:
-                                lecture.dump(filepath=filepath, unsafe=unsafe)
-                            self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_start, lectures_count=lecture_end, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        lecture_start += 1
-                else:
-                    inner_index = 1
-                    for lecture in lectures:
-                        lecture_id = lecture.id
-                        lecture_index = lecture.index
-                        lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
-                        lecture_assets = lecture.assets
-                        lecture_subtitles = lecture.subtitles
-                        lecture_best = lecture.getbest()
-                        lecture_streams = lecture.streams
-                        if caption_only and not skip_captions:
-                            self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        elif skip_captions and not caption_only:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            if lecture.html:
-                                lecture.dump(filepath=filepath, unsafe=unsafe)
-                            self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        else:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            if lecture.html:
-                                lecture.dump(filepath=filepath, unsafe=unsafe)
-                            self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        inner_index += 1
-        elif chapter_start and chapter_start > 0 and chapter_start <= chapter_end and chapter_end <= total_chapters:
-            while chapter_start <= chapter_end:
-                chapter = chapters[chapter_start-1]
-                chapter_id = chapter.id
-                chapter_index = chapter.index
-                chapter_title = chapter.title
-                lectures = chapter.get_lectures()
-                lectures_count = chapter.lectures
-                if unsafe:
-                    filepath = u"%s\\%s" % (course_path, chapter.unsafe_title) if os.name == 'nt' else u"%s/%s" % (course_path, chapter.unsafe_title)
-                if not unsafe:
-                    filepath = "%s\\%s" % (course_path, chapter_title) if os.name == 'nt' else "%s/%s" % (course_path, chapter_title)
-                try:
-                    os.makedirs(filepath)
-                except Exception as e:
-                    pass
-                sys.stdout.write (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fm + sb + "Downloading chapter : ({index} of {total})\n".format(index=chapter_start, total=chapter_end))
-                if not unsafe:
-                    sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s)\n" % (chapter_title))
-                    sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Found (%s) lectures ...\n" % (lectures_count))
-                if unsafe:
-                    sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%02d-%s)\n" % (int(chapter_index), chapter_id))
-                    sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (lectures_count))
-                lecture_start = _lectures_start
-                lecture_end = lectures_count if lecture_start and not lecture_end else _lectures_end
-                if lecture_number and lecture_number > 0 and lecture_number <= lectures_count:
-                    lecture = lectures[lecture_number-1]
-                    lecture_id = lecture.id
-                    lecture_index = lecture.index
-                    lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
-                    lecture_assets = lecture.assets
-                    lecture_subtitles = lecture.subtitles
-                    lecture_best = lecture.getbest()
-                    lecture_streams = lecture.streams
-                    if caption_only and not skip_captions:
-                        self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                    elif skip_captions and not caption_only:
-                        if quality:
-                            index = 0
-                            while index < len(lecture_streams):
-                                dimension = int(lecture_streams[index].dimention[1])
-                                if dimension == quality:
-                                    lecture_best = lecture_streams[index]
-                                    break
-                                index += 1
-                            if not lecture_best:
-                                lecture_best = lecture_best
-                        self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_number, lectures_count=lectures_count, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                    else:
-                        if quality:
-                            index = 0
-                            while index < len(lecture_streams):
-                                dimension = int(lecture_streams[index].dimention[1])
-                                if dimension == quality:
-                                    lecture_best = lecture_streams[index]
-                                    break
-                                index += 1
-                            if not lecture_best:
-                                lecture_best = lecture_best
-                        self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_number, lectures_count=lectures_count, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                elif lecture_start and lecture_start > 0 and lecture_start <= lecture_end and lecture_end <= lectures_count:
-                    while lecture_start <= lecture_end:
-                        lecture = lectures[lecture_start-1]
-                        lecture_id = lecture.id
-                        lecture_index = lecture.index
-                        lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
-                        lecture_assets = lecture.assets
-                        lecture_subtitles = lecture.subtitles
-                        lecture_best = lecture.getbest()
-                        lecture_streams = lecture.streams
-                        if caption_only and not skip_captions:
-                            self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        elif skip_captions and not caption_only:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_start, lectures_count=lecture_end, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        else:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_start, lectures_count=lecture_end, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        lecture_start += 1
-                else:
-                    inner_index = 1
-                    for lecture in lectures:
-                        lecture_id = lecture.id
-                        lecture_index = lecture.index
-                        lecture_title = lecture.title if not unsafe else "Lecture id : %s" % (lecture_id)
-                        lecture_assets = lecture.assets
-                        lecture_subtitles = lecture.subtitles
-                        lecture_best = lecture.getbest()
-                        lecture_streams = lecture.streams
-                        if caption_only and not skip_captions:
-                            self.download_captions_only(lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        elif skip_captions and not caption_only:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        else:
-                            if quality:
-                                index = 0
-                                while index < len(lecture_streams):
-                                    dimension = int(lecture_streams[index].dimention[1])
-                                    if dimension == quality:
-                                        lecture_best = lecture_streams[index]
-                                        break
-                                    index += 1
-                                if not lecture_best:
-                                    lecture_best = lecture_best
-                            self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, lecture_subtitles=lecture_subtitles, language=language, lecture_assets=lecture_assets, filepath=filepath, unsafe=unsafe)
-                        inner_index += 1
-                chapter_start += 1
-        else:
-            if not chapter_end and not chapter_number and not chapter_start:
-                sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Argument(s) are missing : Chapter(s) range or chapter(s) number is required.\n")
-            elif chapter_end and chapter_end > total_chapters or chapter_number and chapter_number > total_chapters:
-                sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Chapter(s) Range exceeded : Chapter(s) ending or chapter(s) number is out of range\n")
-            elif chapter_start and chapter_start > chapter_end:
-                sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Chapter(s) Range exception : Chapter(s) starting point cannot be greater than chapter(s) ending point\n")
-            elif chapter_end and not chapter_start:
-                sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Argument(s) are missing : Chapter(s) range starting point is missing ..\n")
-            sys.stdout.write (fc + sd + "[" + fy + sb + "i" + fc + sd + "] : " + fw + sb + "Chapter(s) number or range should be in between ({start} to {end}).\n".format(start=1, end=total_chapters))
-            sys.exit(0)
+                        self.download_lecture(
+                            lecture_best,
+                            filepath,
+                            lecture_index,
+                            lectures_count,
+                            quality,
+                        )
+                    if dl_assets:
+                        self.download_assets(lecture_assets, filepath)
+                    if dl_subtitles:
+                        self.downalod_subtitles(
+                            lecture_subtitles,
+                            filepath,
+                            language=language,
+                            keep_vtt=keep_vtt,
+                        )
+
 
 def main():
+    """main function"""
     sys.stdout.write(banner())
-    version     = "%(prog)s {version}".format(version=__version__)
-    description = 'A cross-platform python based utility to download courses from udemy for personal offline use.'
-    parser = argparse.ArgumentParser(description=description, conflict_handler="resolve")
-    parser.add_argument('course', help="Udemy course.", type=str)
+    version = "%(prog)s {version}".format(version="0.6")
+    description = "A cross-platform python based utility to download courses from udemy for personal offline use."
+    parser = argparse.ArgumentParser(
+        description=description, conflict_handler="resolve"
+    )
+    parser.add_argument("course", help="Udemy course.", type=str)
     general = parser.add_argument_group("General")
+    general.add_argument("-h", "--help", action="help", help="Shows the help.")
     general.add_argument(
-        '-h', '--help',\
-        action='help',\
-        help="Shows the help.")
-    general.add_argument(
-        '-v', '--version',\
-        action='version',\
-        version=version,\
-        help="Shows the version.")
+        "-v", "--version", action="version", version=version, help="Shows the version."
+    )
 
     authentication = parser.add_argument_group("Authentication")
     authentication.add_argument(
-        '-u', '--username',\
-        dest='username',\
-        type=str,\
-        help="Username in udemy.",metavar='')
+        "-u",
+        "--username",
+        dest="username",
+        type=str,
+        help="Username in udemy.",
+        metavar="",
+    )
     authentication.add_argument(
-        '-p', '--password',\
-        dest='password',\
-        type=str,\
-        help="Password of your account.",metavar='')
+        "-p",
+        "--password",
+        dest="password",
+        type=str,
+        help="Password of your account.",
+        metavar="",
+    )
     authentication.add_argument(
-        '-k', '--cookies',\
-        dest='cookies',\
-        type=str,\
-        help="Cookies to authenticate with.",metavar='')
+        "-k",
+        "--cookies",
+        dest="cookies",
+        type=str,
+        help="Cookies to authenticate with.",
+        metavar="",
+    )
 
     advance = parser.add_argument_group("Advance")
     advance.add_argument(
-        '-o', '--output',\
-        dest='output',\
-        type=str,\
-        help="Download to specific directory.",metavar='')
+        "-o",
+        "--output",
+        dest="output",
+        type=str,
+        default=os.getcwd(),
+        help="Download to specific directory.",
+        metavar="",
+    )
     advance.add_argument(
-        '-q', '--quality',\
-        dest='quality',\
-        type=int,\
-        help="Download specific video quality.",metavar='')
+        "-q",
+        "--quality",
+        dest="quality",
+        type=int,
+        help="Download specific video quality.",
+        metavar="",
+    )
     advance.add_argument(
-        '-c', '--chapter',\
-        dest='chapter',\
-        type=int,\
-        help="Download specific chapter from course.",metavar='')
+        "-c",
+        "--chapter",
+        dest="chapter",
+        type=int,
+        help="Download specific chapter from course.",
+        metavar="",
+    )
     advance.add_argument(
-        '-l', '--lecture',\
-        dest='lecture',\
-        type=int,\
-        help="Download specific lecture from chapter(s).",metavar='')
+        "-l",
+        "--lecture",
+        dest="lecture",
+        type=int,
+        help="Download specific lecture from chapter(s).",
+        metavar="",
+    )
     advance.add_argument(
-        '-s', '--sub-lang',\
-        dest='language',\
-        type=str,\
-        help="Download specific subtitle/caption (e.g:- en).",metavar='')
+        "-s",
+        "--sub-lang",
+        dest="language",
+        type=str,
+        help="Download specific subtitle/caption (e.g:- en).",
+        metavar="",
+        default="en",
+    )
     advance.add_argument(
-        '--chapter-start',\
-        dest='chapter_start',\
-        type=int,\
-        help="Download from specific position within course.",metavar='')
+        "--chapter-start",
+        dest="chapter_start",
+        type=int,
+        help="Download from specific position within course.",
+        metavar="",
+    )
     advance.add_argument(
-        '--chapter-end',\
-        dest='chapter_end',\
-        type=int,\
-        help="Download till specific position within course.",metavar='')
+        "--chapter-end",
+        dest="chapter_end",
+        type=int,
+        help="Download till specific position within course.",
+        metavar="",
+    )
     advance.add_argument(
-        '--lecture-start',\
-        dest='lecture_start',\
-        type=int,\
-        help="Download from specific position within chapter(s).",metavar='')
+        "--lecture-start",
+        dest="lecture_start",
+        type=int,
+        help="Download from specific position within chapter(s).",
+        metavar="",
+    )
     advance.add_argument(
-        '--lecture-end',\
-        dest='lecture_end',\
-        type=int,\
-        help="Download till specific position within chapter(s).",metavar='')
+        "--lecture-end",
+        dest="lecture_end",
+        type=int,
+        help="Download till specific position within chapter(s).",
+        metavar="",
+    )
 
     other = parser.add_argument_group("Others")
     other.add_argument(
-        '--save',\
-        dest='save',\
-        action='store_true',\
-        help="Do not download but save links to a file.")
+        "--info",
+        dest="info",
+        action="store_true",
+        help="List all lectures with available resolution.",
+    )
     other.add_argument(
-        '--info',\
-        dest='list',\
-        action='store_true',\
-        help="List all lectures with available resolution.")
+        "--keep-vtt",
+        dest="keep_vtt",
+        action="store_true",
+        help="Keep WebVTT caption(s).",
+    )
     other.add_argument(
-        '--cache',\
-        dest='cache',\
-        action='store_true',\
-        help="Cache your credentials to use it later.")
+        "--sub-only",
+        dest="caption_only",
+        action="store_true",
+        help="Download captions/subtitle only.",
+    )
     other.add_argument(
-        '--names',\
-        dest='names_only',\
-        action='store_true',\
-        help="Do not download but save lecture names to file.")
+        "--skip-sub",
+        dest="skip_captions",
+        action="store_true",
+        help="Download course but skip captions/subtitle.",
+    )
     other.add_argument(
-        '--unsafe',\
-        dest='unsafe',\
-        action='store_true',\
-        help="Download all course with unsafe names.")
+        "--skip-hls",
+        dest="skip_hls_stream",
+        action="store_true",
+        help="Download course but skip hls streams. (fast fetching).",
+    )
     other.add_argument(
-        '--sub-only',\
-        dest='caption_only',\
-        action='store_true',\
-        help="Download captions/subtitle only.")
+        "--assets-only",
+        dest="assets_only",
+        action="store_true",
+        help="Download asset(s) only.",
+    )
     other.add_argument(
-        '--skip-sub',\
-        dest='skip_captions',\
-        action='store_true',\
-        help="Download course but skip captions/subtitle.")
-
-    options = parser.parse_args()
-    if options.names_only:
-        options.save = True
-
-    if options.cookies:
-        f_in = open(options.cookies)
-        cookies = '\n'.join([line for line in (l.strip() for l in f_in) if line])
-        f_in.close()
-        if options.cache:
-            cache_credentials(username="", password="", quality=options.quality, output=options.output, language=options.language)
-        udemy = Udemy(url=options.course, cookies=cookies)
-        if not options.cache:
-            config = use_cached_credentials()
-            if config and isinstance(config, dict):
-                sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Loading configs..")
-                options.quality = config.get('quality')
-                options.output = config.get('output')
-                options.language = config.get('language')
-                time.sleep(1)
-                sys.stdout.write ("\r" + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Loading configs.. (" + fc + sb + "done" + fg + sd + ")\n")
-                sys.stdout.flush()
-        if options.list and not options.save:
-                try:
-                    udemy.course_list_down(chapter_number=options.chapter, lecture_number=options.lecture, unsafe=options.unsafe)
-                except KeyboardInterrupt as e:
-                    sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                    sys.exit(0)
-        elif not options.list and options.save:
-            try:
-                udemy.course_save(path=options.output, quality=options.quality, caption_only=options.caption_only, skip_captions=options.skip_captions, names_only=options.names_only, unsafe=options.unsafe)
-            except KeyboardInterrupt as e:
-                sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                sys.exit(0)
-        elif not options.list and not options.save:
-
-            if options.chapter and not options.chapter_end and not options.chapter_start:
-
-                if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                else:
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_number=options.chapter, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_number=options.chapter, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-            elif options.chapter_start and options.chapter_end and not options.chapter:
-                
-                if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-                        
-                else:
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-            elif options.chapter_start and not options.chapter_end and not options.chapter:
-                
-                if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                else:
-                    if options.caption_only and not options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                    elif not options.caption_only and options.skip_captions:
-                        udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                    else:
-                        udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-            else:
-
-                if options.caption_only and not options.skip_captions:
-
-                    udemy.course_download(caption_only=options.caption_only, path=options.output, language=options.language, unsafe=options.unsafe)
-
-                elif not options.caption_only and options.skip_captions:
-
-                    udemy.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality, unsafe=options.unsafe)
-
-                else:
-
-                    udemy.course_download(path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-    if not options.cookies:
-        if not options.username and not options.password:
-            username = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Username : " + fg + sb
-            password = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Password : " + fc + sb
-            config = use_cached_credentials()
-            if config and isinstance(config, dict):
-                sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Loading configs..")
-                email = config.get('username')
-                passwd = config.get('password')
-                options.quality = config.get('quality')
-                options.output = config.get('output')
-                options.language = config.get('language')
-                time.sleep(1)
-                if email and passwd:
-                    sys.stdout.write ("\r" + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Loading configs.. (" + fc + sb + "done" + fg + sd + ")\n")
-                else:
-                    sys.stdout.write ("\r" + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Loading configs.. (" + fr + sb + "failed" + fg + sd + ")\n")
-                    email = getpass.getuser(prompt=username)
-                    passwd = getpass.getpass(prompt=password)
-                    print("")
-            else:
-                email = getpass.getuser(prompt=username)
-                passwd = getpass.getpass(prompt=password)
-                print("")
-            if email and passwd:
-                if options.cache:
-                    cache_credentials(username=email, password=passwd, quality=options.quality, output=options.output, language=options.language)
-                udemy = Udemy(url=options.course, username=email, password=passwd)
-            else:
-                sys.stdout.write('\n' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required.\n")
-                sys.exit(0)
-
-            if options.list and not options.save:
-                try:
-                    udemy.course_list_down(chapter_number=options.chapter, lecture_number=options.lecture, unsafe=options.unsafe)
-                except KeyboardInterrupt as e:
-                    sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                    sys.exit(0)
-            elif not options.list and options.save:
-                try:
-                    udemy.course_save(path=options.output, quality=options.quality, caption_only=options.caption_only, skip_captions=options.skip_captions, names_only=options.names_only, unsafe=options.unsafe)
-                except KeyboardInterrupt as e:
-                    sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                    sys.exit(0)
-            elif not options.list and not options.save:
-
-                if options.chapter and not options.chapter_end and not options.chapter_start:
-
-                    if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.chapter_start and options.chapter_end and not options.chapter:
-                    
-                    if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-                            
-                    else:
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.chapter_start and not options.chapter_end and not options.chapter:
-                    
-                    if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                else:
-
-                    if options.caption_only and not options.skip_captions:
-
-                        udemy.course_download(caption_only=options.caption_only, path=options.output, language=options.language, unsafe=options.unsafe)
-
-                    elif not options.caption_only and options.skip_captions:
-
-                        udemy.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-
-                        udemy.course_download(path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-        elif options.username and options.password:
-            
-            udemy = Udemy(url=options.course, username=options.username, password=options.password)
-            
-            if options.cache:
-                cache_credentials(username=options.username, password=options.password, quality=options.quality, output=options.output, language=options.language)
-
-            if options.list and not options.save:
-                try:
-                    udemy.course_list_down(chapter_number=options.chapter, lecture_number=options.lecture, unsafe=options.unsafe)
-                except KeyboardInterrupt as e:
-                    sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                    sys.exit(0)
-            elif not options.list and options.save:
-                # pprint(vars(options))
-                # exit(0)
-                try:
-                    udemy.course_save(path=options.output, quality=options.quality, caption_only=options.caption_only, skip_captions=options.skip_captions, names_only=options.names_only, unsafe=options.unsafe)
-                except KeyboardInterrupt as e:
-                    sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
-                    sys.exit(0)
-            elif not options.list and not options.save:
-
-                if options.chapter and not options.chapter_end and not options.chapter_start:
-
-                    if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter,lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_number=options.chapter, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_number=options.chapter, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.chapter_start and options.chapter_end and not options.chapter:
-
-                    if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, chapter_end=options.chapter_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                elif options.chapter_start and not options.chapter_end and not options.chapter:
-
-                    if options.lecture and not options.lecture_end and not options.lecture_start:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_number=options.lecture, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, lecture_end=options.lecture_end, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    elif options.lecture_start and not options.lecture_end and not options.lecture:
-
-
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, lecture_start=options.lecture_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-                        if options.caption_only and not options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, language=options.language, caption_only=options.caption_only, unsafe=options.unsafe)
-                        elif not options.caption_only and options.skip_captions:
-                            udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, quality=options.quality, skip_captions=options.skip_captions, unsafe=options.unsafe)
-                        else:
-                            udemy.chapter_download(chapter_start=options.chapter_start, path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-                else:
-
-                    if options.caption_only and not options.skip_captions:
-
-                        udemy.course_download(caption_only=options.caption_only, path=options.output, language=options.language, unsafe=options.unsafe)
-
-                    elif not options.caption_only and options.skip_captions:
-
-                        udemy.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality, unsafe=options.unsafe)
-
-                    else:
-
-                        udemy.course_download(path=options.output, language=options.language, quality=options.quality, unsafe=options.unsafe)
-
-if __name__ == '__main__':
+        "--skip-assets",
+        dest="skip_assets",
+        action="store_true",
+        help="Download course but skip asset(s).",
+    )
+
+    args = parser.parse_args()
+    if args.cookies:
+        f_in = open(args.cookies)
+        with open(args.cookies) as f_in:
+            cookies = "\n".join([line for line in (l.strip() for l in f_in) if line])
+        args.cookies = cookies
+    if not args.username and not args.password and not args.cookies:
+        configs = load_configs()
+        if not configs:
+            args.username = getpass.getuser(prompt="Username : ")
+            args.password = getpass.getpass(prompt="Password : ")
+            print("\n")
+        if configs:
+            cookies = configs.get("cookies")
+            if not cookies:
+                args.username = configs.get("username")
+                args.password = configs.get("password")
+            if cookies:
+                args.cookies = cookies
+            args.quality = args.quality if args.quality else configs.get("quality")
+            args.output = args.output if args.output else configs.get("output")
+            args.language = args.language if args.language else configs.get("language")
+    url_or_courses = extract_url_or_courses(args.course)
+    udemy_obj = Udemy(
+        url_or_courses=url_or_courses,
+        username=args.username,
+        password=args.password,
+        cookies=args.cookies,
+    )
+    # setting the caching default so that we can avoid future login attemps.
+    _ = to_configs(
+        username=args.username,
+        password=args.password,
+        cookies=args.cookies,
+        quality=args.quality,
+        output=args.output,
+        language=args.language,
+    )
+    dl_assets = dl_lecture = dl_subtitles = True
+    if args.assets_only:
+        dl_lecture = False
+        dl_subtitles = False
+    if args.skip_assets:
+        dl_assets = False
+    if args.caption_only:
+        dl_lecture = False
+        dl_assets = False
+    if args.skip_captions:
+        dl_subtitles = False
+    if not args.info:
+        udemy_obj.course_download(
+            path=args.output,
+            quality=args.quality,
+            language=args.language,
+            dl_assets=dl_assets,
+            dl_lecture=dl_lecture,
+            dl_subtitles=dl_subtitles,
+            chapter_number=args.chapter,
+            chapter_start=args.chapter_start,
+            chapter_end=args.chapter_end,
+            lecture_number=args.lecture,
+            lecture_start=args.lecture_start,
+            lecture_end=args.lecture_end,
+            keep_vtt=args.keep_vtt,
+            skip_hls_stream=args.skip_hls_stream,
+        )
+    if args.info:
+        udemy_obj.course_listdown(
+            chapter_number=args.chapter,
+            chapter_start=args.chapter_start,
+            chapter_end=args.chapter_end,
+            lecture_number=args.lecture,
+            lecture_start=args.lecture_start,
+            lecture_end=args.lecture_end,
+            skip_hls_stream=args.skip_hls_stream,
+        )
+
+
+if __name__ == "__main__":
     try:
         main()
     except KeyboardInterrupt:
-        sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n")
+        logger.error(msg="User Interrupted..", new_line=True)
         sys.exit(0)
diff --git a/udemy/__init__.py b/udemy/__init__.py
index e0d9be2..7508061 100644
--- a/udemy/__init__.py
+++ b/udemy/__init__.py
@@ -1,19 +1,15 @@
-#!/usr/bin/python
+# pylint: disable=R,C
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
-__version__ = "0.5"
-__author__  = "Nasir Khan (r0ot h3x49)"
-__license__ = 'MIT'
-__copyright__ = 'Copyright (c) 2018 Nasir Khan (r0ot h3x49)'
-
-"""
+'''
 
-Author  : Nasir Khan (r0ot h3x49)
-Github  : https://github.com/r0oth3x49
+Author 	: Nasir Khan (r0ot h3x49)
+Github 	: https://github.com/r0oth3x49
 License : MIT
 
 
-Copyright (c) 2020 Nasir Khan (r0ot h3x49)
+Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49)
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the
 Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
@@ -26,7 +22,11 @@
 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 
 THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
-"""
+'''
 
+__version__ = "1.0"
+__author__  = "Nasir Khan (r0ot h3x49)"
+__license__ = 'MIT'
+__copyright__ = 'Copyright (c) 2018 Nasir Khan (r0ot h3x49)'
 
-from ._udemy import course
+from udemy.udemy import course, fetch_enrolled_courses
diff --git a/udemy/_auth.py b/udemy/_auth.py
deleted file mode 100644
index 4ebd1d9..0000000
--- a/udemy/_auth.py
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-
-Author  : Nasir Khan (r0ot h3x49)
-Github  : https://github.com/r0oth3x49
-License : MIT
-
-
-Copyright (c) 2020 Nasir Khan (r0ot h3x49)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the
-Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
-and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
-ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 
-THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""
-
-from pprint import pprint
-from ._session import Session
-from ._compat import (
-        sys,
-        time,
-        conn_error,
-        LOGIN_URL,
-        )
-from ._utils import (
-        parse_json,
-        js_to_json,
-        search_regex,
-        hidden_inputs,
-        unescapeHTML,
-        )
-from ._colorized import *
-
-class UdemyAuth(object):
-
-    def __init__(self, username='', password=''):
-        self.username = username
-        self.password = password
-        self._session = Session()
-
-    def _form_hidden_input(self, form_id):
-        try:
-            webpage = self._session._get(LOGIN_URL).text
-        except conn_error as e:
-            sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n")
-            time.sleep(0.8)
-            sys.exit(0)
-        else:
-            login_form = hidden_inputs(
-                            search_regex(
-                                r'(?is)]+?id=(["\'])%s\1[^>]*>(?P
.+?)
' % form_id, - webpage, - '%s form' % form_id, - group='form' - ) - ) - login_form.update({ - 'email' : self.username, - 'password' : self.password, - }) - return login_form - - def authenticate(self, access_token='', client_id=''): - if not access_token and not client_id: - data = self._form_hidden_input(form_id='login-form') - auth_response = self._session._post(LOGIN_URL, data=data, redirect=False) - auth_cookies = auth_response.cookies - - access_token = auth_cookies.get('access_token', '') - client_id = auth_cookies.get('client_id', '') - - if access_token: - self._session._set_auth_headers(access_token=access_token, client_id=client_id) - return self._session - else: - self._session._set_auth_headers() - return None diff --git a/udemy/_compat.py b/udemy/_compat.py deleted file mode 100644 index fd9e0d7..0000000 --- a/udemy/_compat.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 -License : MIT - - -Copyright (c) 2020 Nasir Khan (r0ot h3x49) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -import re -import os -import sys -import time -import json -import codecs -import requests -if sys.version_info[:2] >= (3, 0): - - import ssl - import urllib.request as compat_urllib - - from urllib.error import HTTPError as compat_httperr - from urllib.error import URLError as compat_urlerr - from urllib.parse import urlparse as compat_urlparse - from urllib.request import Request as compat_request - from urllib.request import urlopen as compat_urlopen - from urllib.request import build_opener as compat_opener - from html.parser import HTMLParser as compat_HTMLParser - from http.cookies import SimpleCookie as ParseCookie - from requests.exceptions import ConnectionError as conn_error - - encoding, pyver = str, 3 - ssl._create_default_https_context = ssl._create_unverified_context - -else: - - import urllib2 as compat_urllib - - from urllib2 import Request as compat_request - from urllib2 import urlopen as compat_urlopen - from urllib2 import URLError as compat_urlerr - from urllib2 import HTTPError as compat_httperr - from urllib2 import build_opener as compat_opener - from urlparse import urlparse as compat_urlparse - from Cookie import SimpleCookie as ParseCookie - from HTMLParser import HTMLParser as compat_HTMLParser - from requests.exceptions import ConnectionError as conn_error - - encoding, pyver = unicode, 2 - - -NO_DEFAULT = object() -LOGIN_URL = 'https://www.udemy.com/join/login-popup/?displayType=ajax&display_type=popup&showSkipButton=1&returnUrlAfterLogin=https'#'https://www.udemy.com/api-2.0/auth/udemy-auth/login/?fields[user]=access_token' -LOGOUT_URL = 'https://www.udemy.com/user/logout' - -WISHLIST_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/wishlisted-courses?fields[course]=id,url,published_title&ordering=-access_time&page=1&page_size=1000" -COLLECTION_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses-collections/?collection_has_courses=True&course_limit=20&fields[course]=last_accessed_time,published_title&fields[user_has_subscribed_courses_collection]=@all&page=1&page_size=1000" -MY_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,published_title&ordering=-last_accessed,-access_time&page=1&page_size=10000" -COURSE_SEARCH = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,published_title&page=1&page_size=1000&ordering=-last_accessed,-access_time&search={course_name}" -COURSE_URL = 'https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/cached-subscriber-curriculum-items?fields[asset]=results,external_url,time_estimation,download_urls,slide_urls,filename,asset_type,captions,stream_urls,body&fields[chapter]=object_index,title,sort_order&fields[lecture]=id,title,object_index,asset,supplementary_assets,view_html&page_size=10000' -HEADERS = { - 'Origin': 'www.udemy.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0', - 'Referer': 'https://www.udemy.com/join/login-popup/', - 'Accept': '*/*', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate, br', - 'Connection': 'keep-alive' - } - - -__ALL__ = [ - 're', - 'os', - 'sys', - 'time', - 'json', - 'pyver', - 'codecs', - 'encoding', - 'requests', - 'conn_error', - 'compat_urlerr', - 'compat_opener', - 'compat_urllib', - 'compat_urlopen', - 'compat_request', - 'compat_httperr', - 'compat_urlparse', - 'compat_HTMLParser', - 'ParseCookie', - 'HEADERS', - 'LOGIN_URL', - 'NO_DEFAULT', - 'COURSE_URL', - 'LOGOUT_URL', - 'WISHLIST_URL', - 'COLLECTION_URL', - 'MY_COURSES_URL', - 'COURSE_SEARCH' - ] diff --git a/udemy/_extract.py b/udemy/_extract.py deleted file mode 100644 index 845a960..0000000 --- a/udemy/_extract.py +++ /dev/null @@ -1,659 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" - -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 -License : MIT - - -Copyright (c) 2020 Nasir Khan (r0ot h3x49) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -import os -import re -import sys -import json - -from pprint import pprint -from ._auth import UdemyAuth -from ._utils import ( - parse_json, - js_to_json, - search_regex, - unescapeHTML - ) -from ._compat import ( - re, - time, - encoding, - conn_error, - COURSE_URL, - ParseCookie, - MY_COURSES_URL, - COURSE_SEARCH, - COLLECTION_URL - ) -from ._sanitize import ( - slugify, - sanitize, - SLUG_OK - ) -from ._colorized import * -from ._progress import ProgressBar - - -class Udemy(ProgressBar): - - def __init__(self): - self._session = '' - self._cookies = '' - - - def _clean(self, text): - ok = re.compile(r'[^\\/:*?"<>|]') - text = "".join(x if ok.match(x) else "_" for x in text) - text = re.sub(r'\.+$', '', text.strip()) - return text - - def _course_name(self, url): - mobj = re.search(r'(?i)(?://(?P.+?).udemy.com/(?:course(/draft)*/)?(?P[a-zA-Z0-9_-]+))', url) - if mobj: - return mobj.group('portal_name'), mobj.group('name_or_id') - - def _extract_cookie_string(self, raw_cookies): - cookies = {} - try: - # client_id = re.search(r'(?i)(?:client_id=(?P\w+))', raw_cookies) - access_token = re.search(r'(?i)(?:access_token=(?P\w+))', raw_cookies) - except: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Cookies error, Request Headers is required.\n") - sys.stdout.write(fc + sd + "[" + fm + sb + "i" + fc + sd + "] : " + fg + sb + "Copy Request Headers for single request to a file, while you are logged in.\n") - sys.exit(0) - if not access_token: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Cookies error, unable to find access_token, proper cookies required.\n") - sys.stdout.flush() - sys.exit(0) - access_token = access_token.group('access_token') - cookies.update({'access_token': access_token}) - #'client_id': client_id.group('client_id'), - return cookies - - def _sanitize(self, unsafetext): - text = sanitize(slugify(unsafetext, lower=False, spaces=True, ok=SLUG_OK + '().[]')) - return text - - def _login(self, username='', password='', cookies=''): - if not cookies: - auth = UdemyAuth(username=username, password=password) - self._session = auth.authenticate() - if cookies: - self._cookies = self._extract_cookie_string(raw_cookies=cookies) - access_token = self._cookies.get('access_token') - client_id = self._cookies.get('client_id') - time.sleep(0.3) - auth = UdemyAuth() - self._session = auth.authenticate(access_token=access_token, client_id=client_id) - self._session._session.cookies.update(self._cookies) - if self._session is not None: - return {'login' : 'successful'} - else: - return {'login' : 'failed'} - - def _logout(self): - return self._session.terminate() - - def _subscribed_courses(self, portal_name, course_name): - results = [] - self._session._headers.update({ - 'Host' : '{portal_name}.udemy.com'.format(portal_name=portal_name), - 'Referer' : 'https://{portal_name}.udemy.com/home/my-courses/search/?q={course_name}'.format(portal_name=portal_name, course_name=course_name) - }) - url = COURSE_SEARCH.format(portal_name=portal_name, course_name=course_name) - try: - webpage = self._session._get(url).json() - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - except (ValueError, Exception) as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "%s.\n" % (e)) - time.sleep(0.8) - sys.exit(0) - else: - results = webpage.get('results', []) - return results - - def _my_courses(self, portal_name): - results = [] - try: - url = MY_COURSES_URL.format(portal_name=portal_name) - webpage = self._session._get(url).json() - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - except (ValueError, Exception) as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "%s.\n" % (e)) - time.sleep(0.8) - sys.exit(0) - else: - results = webpage.get('results', []) - return results - - def _archived_courses(self, portal_name): - results = [] - try: - url = MY_COURSES_URL.format(portal_name=portal_name) + "&is_archived=true" - webpage = self._session._get(url).json() - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - except (ValueError, Exception) as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "%s.\n" % (e)) - time.sleep(0.8) - sys.exit(0) - else: - results = webpage.get('results', []) - return results - - def _subscribed_collection_courses(self, portal_name): - url = COLLECTION_URL.format(portal_name=portal_name) - courses_lists = [] - try: - webpage = self._session._get(url).json() - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - except (ValueError, Exception) as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "%s.\n" % (e)) - time.sleep(0.8) - sys.exit(0) - else: - results = webpage.get('results', []) - if results: - [courses_lists.extend(courses.get('courses', [])) for courses in results if courses.get('courses', [])] - return courses_lists - - def __extract_course(self, response, course_name): - _temp = {} - if response: - for entry in response: - course_id = str(entry.get('id')) - published_title = entry.get('published_title') - if course_name in (published_title, course_id): - _temp = entry - break - return _temp - - def _extract_course_info(self, url): - portal_name, course_name = self._course_name(url) - course = {} - results = self._subscribed_courses(portal_name=portal_name, course_name=course_name) - course = self.__extract_course(response=results, course_name=course_name) - if not course: - results = self._my_courses(portal_name=portal_name) - course = self.__extract_course(response=results, course_name=course_name) - if not course: - results = self._subscribed_collection_courses(portal_name=portal_name) - course = self.__extract_course(response=results, course_name=course_name) - if not course: - results = self._archived_courses(portal_name=portal_name) - course = self.__extract_course(response=results, course_name=course_name) - - if course: - course.update({'portal_name' : portal_name}) - return course.get('id'), course - if not course: - sys.stdout.write('\033[2K\033[1G\r\r' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fg + sb + "Downloading course information, course id not found .. (%s%sfailed%s%s)\n" % (fr, sb, fg, sb)) - sys.stdout.write(fc + sd + "[" + fw + sb + "i" + fc + sd + "] : " + fw + sb + "It seems either you are not enrolled or you have to visit the course atleast once while you are logged in.\n") - sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to logout now...\n") - if not self._cookies: - self._logout() - sys.stdout.write(fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sb + "Logged out successfully.\n") - sys.exit(0) - - def _extract_large_course_content(self, url, current_size=10000, page_size=300): - data = {} - url = url.replace(str(current_size), str(page_size)) if url.endswith(str(current_size)) else url - try: - data = self._session._get(url).json() - except (Exception, ValueError) as error: - data = self._extract_large_course_content(url, current_size=300, page_size=50) - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - else: - _next = data.get('next') - while _next: - try: - resp = self._session._get(_next).json() - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - else: - _next = resp.get('next') - results = resp.get('results') - if results and isinstance(results, list): - for d in resp['results']: - data['results'].append(d) - return data - - def _extract_course_json(self, url, course_id, portal_name): - self._session._headers.update({'Referer' : url}) - url = COURSE_URL.format(portal_name=portal_name, course_id=course_id) - try: - resp = self._session._get(url) - if resp.status_code in [502, 503]: - resp = self._extract_large_course_content(url=url) - else: - resp = resp.json() - except conn_error as e: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") - time.sleep(0.8) - sys.exit(0) - except (ValueError, Exception) as e: - resp = self._extract_large_course_content(url=url) - return resp - else: - return resp - - def _html_to_json(self, view_html, lecture_id): - data = parse_json( - search_regex( - r'videojs-setup-data=(["\'])(?P{.+?})\1', - view_html, - 'setup data', - default='{}', - group='data'), - lecture_id, - transform_source=unescapeHTML, - fatal=False - ) - text_tracks = parse_json( - search_regex( - r'text-tracks=(["\'])(?P\[.+?\])\1', - view_html, - 'text tracks', - default='{}', - group='data'), - lecture_id, - transform_source=lambda s: js_to_json(unescapeHTML(s)), - fatal=False - ) - return data, text_tracks - - def _extract_ppt(self, assets): - _temp = [] - download_urls = assets.get('download_urls') - slides_urls = assets.get('slide_urls') - filename = self._sanitize(assets.get('filename')) - if download_urls and isinstance(download_urls, dict): - extension = filename.rsplit('.', 1)[-1] if '.' in filename else '' - download_url = download_urls.get('Presentation', [])[0].get('file') - _temp.append({ - 'type' : 'presentation', - 'filename' : filename, - 'extension' : extension, - 'download_url' : download_url - }) - return _temp - - def _extract_file(self, assets): - _temp = [] - download_urls = assets.get('download_urls') - filename = self._sanitize(assets.get('filename')) - if download_urls and isinstance(download_urls, dict): - extension = filename.rsplit('.', 1)[-1] if '.' in filename else '' - download_url = download_urls.get('File', [])[0].get('file') - _temp.append({ - 'type' : 'file', - 'filename' : filename, - 'extension' : extension, - 'download_url' : download_url - }) - return _temp - - def _extract_ebook(self, assets): - _temp = [] - download_urls = assets.get('download_urls') - filename = self._sanitize(assets.get('filename')) - if download_urls and isinstance(download_urls, dict): - extension = filename.rsplit('.', 1)[-1] if '.' in filename else '' - download_url = download_urls.get('E-Book', [])[0].get('file') - _temp.append({ - 'type' : 'ebook', - 'filename' : filename, - 'extension' : extension, - 'download_url' : download_url - }) - return _temp - - def _extract_audio(self, assets): - _temp = [] - download_urls = assets.get('download_urls') - filename = self._sanitize(assets.get('filename')) - if download_urls and isinstance(download_urls, dict): - extension = filename.rsplit('.', 1)[-1] if '.' in filename else '' - download_url = download_urls.get('Audio', [])[0].get('file') - _temp.append({ - 'type' : 'audio', - 'filename' : filename, - 'extension' : extension, - 'download_url' : download_url - }) - return _temp - - def _extract_sources(self, sources): - _temp = [] - if sources and isinstance(sources, list): - for source in sources: - label = source.get('label') - download_url = source.get('file') - if not download_url: - continue - if label.lower() == 'audio': - continue - height = label if label else None - if height == "2160": - width = "3840" - elif height == "1440": - width = "2560" - elif height == "1080": - width = "1920" - elif height == "720": - width = "1280" - elif height == "480": - width = "854" - elif height == "360": - width = "640" - elif height == "240": - width = "426" - else: - width = "256" - if source.get('type') == 'application/x-mpegURL' or 'm3u8' in download_url: - continue - else: - _type = source.get('type') - _temp.append({ - 'type' : 'video', - 'height' : height, - 'width' : width, - 'extension' : _type.replace('video/', ''), - 'download_url' : download_url, - }) - return _temp - - def _extract_subtitles(self, tracks): - _temp = [] - if tracks and isinstance(tracks, list): - for track in tracks: - if not isinstance(track, dict): - continue - if track.get('_class') != 'caption': - continue - download_url = track.get('url') - if not download_url or not isinstance(download_url, encoding): - continue - lang = track.get('language') or track.get('srclang') or track.get('label') or track['locale_id'].split('_')[0] - ext = 'vtt' if 'vtt' in download_url.rsplit('.', 1)[-1] else 'srt' - _temp.append({ - 'type' : 'subtitle', - 'language' : lang, - 'extension' : ext, - 'download_url' : download_url, - }) - return _temp - - def _extract_supplementary_assets(self, supp_assets): - _temp = [] - for entry in supp_assets: - file_id = entry.get('id') - filename = self._sanitize(entry.get('filename')) - download_urls = entry.get('download_urls') - external_url = entry.get('external_url') - slide_url = entry.get('slide_urls') - asset_type = entry.get('asset_type').lower() - if asset_type == 'file': - if download_urls and isinstance(download_urls, dict): - extension = filename.rsplit('.', 1)[-1] if '.' in filename else '' - download_url = download_urls.get('File', [])[0].get('file') - _temp.append({ - 'type' : 'file', - 'filename' : filename, - 'extension' : extension, - 'download_url' : download_url, - }) - elif asset_type == 'sourcecode': - if download_urls and isinstance(download_urls, dict): - extension = filename.rsplit('.', 1)[-1] if '.' in filename else '' - download_url = download_urls.get('SourceCode', [])[0].get('file') - _temp.append({ - 'type' : 'source_code', - 'filename' : filename, - 'extension' : extension, - 'download_url' : download_url, - }) - elif asset_type == 'externallink': - _temp.append({ - 'type' : 'external_link', - 'filename' : filename, - 'extension' : 'txt', - 'download_url' : external_url, - }) - return _temp - - def _real_extract(self, url=''): - - _udemy = {} - course_id, course_info = self._extract_course_info(url) - - if course_info and isinstance(course_info, dict): - course_title = course_info.get('published_title') - portal_name = course_info.get('portal_name') - - course_json = self._extract_course_json(url, course_id, portal_name) - course = course_json.get('results') - resource = course_json.get('detail') - - if resource: - if not self._cookies: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Udemy Says : {}{}{} Run udemy-dl against course within few seconds.\n".format(resource, fw, sb)) - if self._cookies: - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Udemy Says : {}{}{} cookies seems to be expired.\n".format(resource, fw, sb)) - sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to logout now...\n") - if not self._cookies: - self._logout() - sys.stdout.write(fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sb + "Logged out successfully.\n") - sys.exit(0) - - _udemy['course_id'] = course_id - _udemy['course_title'] = course_title - _udemy['chapters'] = [] - - counter = -1 - - if course: - for entry in course: - - clazz = entry.get('_class') - asset = entry.get('asset') - supp_assets = entry.get('supplementary_assets') - - if clazz == 'chapter': - lectures = [] - chapter_index = entry.get('object_index') - chapter_title = self._clean(self._sanitize(entry.get('title'))) - chapter = "{0:02d} {1!s}".format(chapter_index, chapter_title) - unsafe_chapter = u'{0:02d} '.format(chapter_index) + self._clean(entry.get('title')) - if chapter not in _udemy['chapters']: - _udemy['chapters'].append({ - 'chapter_title' : chapter, - 'chapter_id' : entry.get("id"), - 'chapter_index' : chapter_index, - 'unsafe_chapter' : unsafe_chapter, - 'lectures' : [], - }) - counter += 1 - elif clazz == 'lecture': - - lecture_id = entry.get("id") - if len(_udemy['chapters']) == 0: - lectures = [] - chapter_index = entry.get('object_index') - chapter_title = self._clean(self._sanitize(entry.get('title'))) - chapter = "{0:03d} {1!s}".format(chapter_index, chapter_title) - unsafe_chapter = u'{0:02d} '.format(chapter_index) + self._clean(entry.get('title')) - if chapter not in _udemy['chapters']: - _udemy['chapters'].append({ - 'chapter_title' : chapter, - 'chapter_id' : lecture_id, - 'chapter_index' : chapter_index, - 'unsafe_chapter' : unsafe_chapter, - 'lectures' : [], - }) - counter += 1 - - if lecture_id: - - view_html = entry.get('view_html') - retVal = [] - - - if isinstance(asset, dict): - asset_type = asset.get('asset_type').lower() or asset.get('assetType').lower() - if asset_type == 'article': - if isinstance(supp_assets, list) and len(supp_assets) > 0: - retVal = self._extract_supplementary_assets(supp_assets) - elif asset_type == 'video': - if isinstance(supp_assets, list) and len(supp_assets) > 0: - retVal = self._extract_supplementary_assets(supp_assets) - elif asset_type == 'e-book': - retVal = self._extract_ebook(asset) - elif asset_type == 'file': - retVal = self._extract_file(asset) - elif asset_type == 'presentation': - retVal = self._extract_ppt(asset) - elif asset_type == 'audio': - retVal = self._extract_audio(asset) - - - if view_html: - text = '\r' + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Downloading course information .. " - self._spinner(text) - lecture_index = entry.get('object_index') - lecture_title = self._clean(self._sanitize(entry.get('title'))) - lecture = "{0:03d} {1!s}".format(lecture_index, lecture_title) - unsafe_lecture = u'{0:03d} '.format(lecture_index) + entry.get('title') - data, subs = self._html_to_json(view_html, lecture_id) - if data and isinstance(data, dict): - sources = data.get('sources') - tracks = data.get('tracks') if isinstance(data.get('tracks'), list) else subs - duration = data.get('duration') - lectures.append({ - 'lecture_index' : lecture_index, - 'lectures_id' : lecture_id, - 'lecture_title' : lecture, - 'unsafe_lecture' : unsafe_lecture, - 'duration' : duration, - 'assets' : retVal, - 'assets_count' : len(retVal), - 'sources' : self._extract_sources(sources), - 'subtitles' : self._extract_subtitles(tracks), - 'subtitle_count' : len(self._extract_subtitles(tracks)), - 'sources_count' : len(self._extract_sources(sources)), - }) - else: - lectures.append({ - 'lecture_index' : lecture_index, - 'lectures_id' : lecture_id, - 'lecture_title' : lecture, - 'unsafe_lecture' : unsafe_lecture, - 'html_content' : view_html, - 'extension' : 'html', - 'assets' : retVal, - 'assets_count' : len(retVal), - 'subtitle_count' : 0, - 'sources_count' : 0, - }) - if not view_html: - text = '\r' + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Downloading course information .. " - self._spinner(text) - lecture_index = entry.get('object_index') - lecture_title = self._clean(self._sanitize(entry.get('title'))) - lecture = "{0:03d} {1!s}".format(lecture_index, lecture_title) - unsafe_lecture = u'{0:03d} '.format(lecture_index) + self._clean(entry.get('title')) - data = asset.get('stream_urls') - if data and isinstance(data, dict): - sources = data.get('Video') - tracks = asset.get('captions') - duration = asset.get('time_estimation') - lectures.append({ - 'lecture_index' : lecture_index, - 'lectures_id' : lecture_id, - 'lecture_title' : lecture, - 'unsafe_lecture' : unsafe_lecture, - 'duration' : duration, - 'assets' : retVal, - 'assets_count' : len(retVal), - 'sources' : self._extract_sources(sources), - 'subtitles' : self._extract_subtitles(tracks), - 'subtitle_count' : len(self._extract_subtitles(tracks)), - 'sources_count' : len(self._extract_sources(sources)), - }) - else: - lectures.append({ - 'lecture_index' : lecture_index, - 'lectures_id' : lecture_id, - 'lecture_title' : lecture, - 'unsafe_lecture' : unsafe_lecture, - 'html_content' : asset.get('body'), - 'extension' : 'html', - 'assets' : retVal, - 'assets_count' : len(retVal), - 'subtitle_count' : 0, - 'sources_count' : 0, - }) - - _udemy['chapters'][counter]['lectures'] = lectures - _udemy['chapters'][counter]['lectures_count'] = len(lectures) - elif clazz == 'quiz': - lecture_id = entry.get("id") - if len(_udemy['chapters']) == 0: - lectures = [] - chapter_index = entry.get('object_index') - chapter_title = self._clean(self._sanitize(entry.get('title'))) - chapter = "{0:03d} {1!s}".format(chapter_index, chapter_title) - unsafe_chapter = u'{0:02d} '.format(chapter_index) + self._clean(entry.get('title')) - if chapter not in _udemy['chapters']: - _udemy['chapters'].append({ - 'chapter_title' : chapter, - 'unsafe_chapter' : unsafe_chapter, - 'chapter_id' : lecture_id, - 'chapter_index' : chapter_index, - 'lectures' : [], - }) - counter += 1 - _udemy['chapters'][counter]['lectures'] = lectures - _udemy['chapters'][counter]['lectures_count'] = len(lectures) - _udemy['total_chapters'] = len(_udemy['chapters']) - _udemy['total_lectures'] = sum([entry.get('lectures_count', 0) for entry in _udemy['chapters'] if entry]) - - return _udemy diff --git a/udemy/_internal.py b/udemy/_internal.py deleted file mode 100644 index 0cc1346..0000000 --- a/udemy/_internal.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" - -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 -License : MIT - - -Copyright (c) 2020 Nasir Khan (r0ot h3x49) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -import sys -import time - -from ._colorized import * -from ._extract import Udemy -from ._shared import ( - UdemyCourse, - UdemyChapters, - UdemyLectures, - UdemyLectureStream, - UdemyLectureAssets, - UdemyLectureSubtitles - ) - - -class InternUdemyCourse(UdemyCourse, Udemy): - def __init__(self, *args, **kwargs): - self._info = '' - super(InternUdemyCourse, self).__init__(*args, **kwargs) - - def _fetch_course(self): - if self._have_basic: - return - if not self._cookies: - auth = self._login(username=self._username, password=self._password) - if self._cookies: - auth = self._login(cookies=self._cookies) - if auth.get('login') == 'successful': - sys.stdout.write(fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sb + "Logged in successfully.\n") - sys.stdout.write('\r' + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Downloading course information .. \r") - self._info = self._real_extract(self._url) - time.sleep(1) - sys.stdout.write('\r' + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Downloaded course information .. (done)\r\n") - self._id = self._info['course_id'] - self._title = self._info['course_title'] - self._chapters_count = self._info['total_chapters'] - self._total_lectures = self._info['total_lectures'] - self._chapters = [InternUdemyChapter(z) for z in self._info['chapters']] - sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to logout now...\n") - if not self._cookies: - self._logout() - sys.stdout.write(fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sb + "Logged out successfully.\n") - self._have_basic = True - if auth.get('login') == 'failed': - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Failed to login ..\n") - sys.exit(0) - - -class InternUdemyChapter(UdemyChapters): - - def __init__(self, chapter): - super(InternUdemyChapter, self).__init__() - - self._chapter_id = chapter['chapter_id'] - self._chapter_title = chapter['chapter_title'] - self._unsafe_title = chapter['unsafe_chapter'] - self._chapter_index = chapter['chapter_index'] - self._lectures_count = chapter.get('lectures_count', 0) - self._lectures = [InternUdemyLecture(z) for z in chapter['lectures']] if self._lectures_count > 0 else [] - - -class InternUdemyLecture(UdemyLectures): - - def __init__(self, lectures): - super(InternUdemyLecture, self).__init__() - self._info = lectures - - self._lecture_id = self._info['lectures_id'] - self._lecture_title = self._info['lecture_title'] - self._unsafe_title = self._info['unsafe_lecture'] - self._lecture_index = self._info['lecture_index'] - - self._subtitles_count = self._info.get('subtitle_count', 0) - self._sources_count = self._info.get('sources_count', 0) - self._assets_count = self._info.get('assets_count', 0) - self._extension = self._info.get('extension') - self._html_content = self._info.get('html_content') - self._duration = self._info.get('duration') - if self._duration: - duration = int(self._duration) - (mins, secs) = divmod(duration, 60) - (hours, mins) = divmod(mins, 60) - if hours == 0: - self._duration = "%02d:%02d" % (mins, secs) - else: - self._duration = "%02d:%02d:%02d" % (hours, mins, secs) - - - def _process_streams(self): - streams = [InternUdemyLectureStream(z, self) for z in self._info['sources']] if self._sources_count > 0 else [] - self._streams = streams - - def _process_assets(self): - assets = [InternUdemyLectureAssets(z, self) for z in self._info['assets']] if self._assets_count > 0 else [] - self._assets = assets - - def _process_subtitles(self): - subtitles = [InternUdemyLectureSubtitles(z, self) for z in self._info['subtitles']] if self._subtitles_count > 0 else [] - self._subtitles = subtitles - -class InternUdemyLectureStream(UdemyLectureStream): - - def __init__(self, sources, parent): - super(InternUdemyLectureStream, self).__init__(parent) - - self._mediatype = sources.get('type') - self._extension = sources.get('extension') - height = sources.get('height', 0) - width = sources.get('width', 0) - self._resolution = '%sx%s' % (width, height) - self._dimention = width, height - self._quality = self._resolution - self._url = sources.get('download_url') - -class InternUdemyLectureAssets(UdemyLectureAssets): - - def __init__(self, assets, parent): - super(InternUdemyLectureAssets, self).__init__(parent) - - self._mediatype = assets.get('type') - self._extension = assets.get('extension') - self._filename = '{0:03d} {1!s}'.format(parent._lecture_index, assets.get('filename')) - self._url = assets.get('download_url') - -class InternUdemyLectureSubtitles(UdemyLectureSubtitles): - - def __init__(self, subtitles, parent): - super(InternUdemyLectureSubtitles, self).__init__(parent) - - self._mediatype = subtitles.get('type') - self._extension = subtitles.get('extension') - self._language = subtitles.get('language') - self._url = subtitles.get('download_url') diff --git a/udemy/_progress.py b/udemy/_progress.py deleted file mode 100644 index 6de03c7..0000000 --- a/udemy/_progress.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/python - -""" - -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 -License : MIT - - -Copyright (c) 2020 Nasir Khan (r0ot h3x49) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -import itertools -from ._compat import ( - os, - sys, - time, - pyver, - ) -from ._colorized import * - -_spin = itertools.cycle(['-', '|', '/', '\\']) - -class ProgressBar(object): - - def _spinner(self, text): - spin = _spin.next() if pyver == 2 else _spin.__next__() - sys.stdout.write(text + spin) - sys.stdout.flush() - time.sleep(0.02) - - # thanks to https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console - def _progress(self, iteration, total, prefix = '' , file_size='' , downloaded = '' , rate = '' ,suffix = '', bar_length = 30): - filledLength = int(round(bar_length * iteration / float(total))) - percents = format(100.00 * (iteration / float(total)), '.2f') - bar = fc + sd + '#' * filledLength + fw + sd +'-' * (bar_length - filledLength) - if '0.00' not in rate: - sys.stdout.write('\033[2K\033[1G\r\r{}{}[{}{}*{}{}] : {}{}{}/{} {}% |{}{}{}| {} {}'.format(fc,sd,fm,sb,fc,sd,fg,sb,file_size,downloaded,percents,bar,fg,sb,rate,suffix)) - sys.stdout.flush() - - def show_progress(self, total, recvd, ratio, rate, eta): - if total <= 1048576: - _total_size = round(float(total) / 1024.00, 2) - _receiving = round(float(recvd) / 1024.00, 2) - _size = format(_total_size if _total_size < 1024.00 else _total_size/1024.00, '.2f') - _received = format(_receiving if _receiving < 1024.00 else _receiving/1024.00,'.2f') - suffix_size = 'KB' if _total_size < 1024.00 else 'MB' - suffix_recvd = 'KB' if _receiving < 1024.00 else 'MB' - else: - _total_size = round(float(total) / 1048576, 2) - _receiving = round(float(recvd) / 1048576, 2) - _size = format(_total_size if _total_size < 1024.00 else _total_size/1024.00, '.2f') - _received = format(_receiving if _receiving < 1024.00 else _receiving/1024.00,'.2f') - suffix_size = 'MB' if _total_size < 1024.00 else 'GB' - suffix_recvd = 'MB' if _receiving < 1024.00 else 'GB' - - _rate = round(float(rate) , 2) - rate = format(_rate if _rate < 1024.00 else _rate/1024.00, '.2f') - suffix_rate = 'kB/s' if _rate < 1024.00 else 'MB/s' - (mins, secs) = divmod(eta, 60) - (hours, mins) = divmod(mins, 60) - if hours > 99: - eta = "--:--:--" - if hours == 0: - eta = "eta %02d:%02ds" % (mins, secs) - else: - eta = "eta %02d:%02d:%02ds" % (hours, mins, secs) - if secs == 0: - eta = "\n" - - self._progress(_receiving, _total_size, file_size = str(_size) + str(suffix_size) ,\ - downloaded = str(_received) + str(suffix_recvd),\ - rate = str(rate) + str(suffix_rate),\ - suffix = str(eta),\ - bar_length = 30) diff --git a/udemy/_shared.py b/udemy/_shared.py deleted file mode 100644 index 8ded01e..0000000 --- a/udemy/_shared.py +++ /dev/null @@ -1,719 +0,0 @@ -#!/usr/bin/python - -""" - -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 -License : MIT - - -Copyright (c) 2020 Nasir Khan (r0ot h3x49) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" -from ._compat import ( - re, - os, - sys, - time, - pyver, - codecs, - requests, - conn_error, - compat_urlerr, - compat_opener, - compat_request, - compat_urlopen, - compat_httperr, - HEADERS - ) - -early_py_version = sys.version_info[:2] < (2, 7) - - -class Downloader(object): - - def __init__(self): - self._url = None - self._filename = None - self._mediatype = None - self._extension = None - self._sess = requests.session() - - @property - def url(self): - """abac""" - return self._url - - @property - def mediatype(self): - return self._mediatype - - @property - def extension(self): - return self._extension - - @property - def filename(self): - if not self._filename: - self._filename = self._generate_filename() - return self._filename - - @property - def unsafe_filename(self): - if not self._filename: - self._filename = self._generate_unsafe_filename() - return self._filename - - def _generate_filename(): - pass - - def _generate_unsafe_filename(): - pass - - def _write_external_links(self, filepath, unsafe=False): - retVal = {} - savedirs, name = os.path.split(filepath) - filename = 'external-assets-links.txt' if not unsafe else u'external-assets-links.txt' - filename = os.path.join(savedirs, filename) - - file_data = [] - if os.path.isfile(filename): - file_data = [i.strip().lower() for i in codecs.open(filename, encoding='utf-8') if i] - - try: - f = codecs.open(filename, 'a', encoding='utf-8', errors='ignore') - data = '\n{}\n{}\n'.format(name, self.url) if not unsafe else u'\n{}\n{}\n'.format( - name, self.url) - if name.lower() not in file_data: - f.write(data) - except (OSError, Exception, UnicodeDecodeError) as e: - retVal = {'status' : 'False', 'msg' : '{}'.format(e)} - else: - retVal = {'status' : 'True', 'msg' : 'download'} - f.close() - - return retVal - - def download(self, filepath="", unsafe=False, quiet=False, callback=lambda *x: None): - savedir = filename = "" - retVal = {} - - if filepath and os.path.isdir(filepath): - savedir, filename = filepath, self.filename if not unsafe else self.unsafe_filename - - elif filepath: - savedir, filename = os.path.split(filepath) - - else: - filename = self.filename if not unsafe else self.unsafe_filename - - filepath = os.path.join(savedir, filename) - if os.name == "nt" and len(filepath) > 250: - filepath = "\\\\?\\{}".format(filepath) - - if self.mediatype == 'external_link': - return self._write_external_links(filepath, unsafe) - - if filepath and filepath.endswith('.vtt'): - filepath_vtt2srt = filepath.replace('.vtt', '.srt') - if os.path.isfile(filepath_vtt2srt): - retVal = {"status" : "True", "msg" : "already downloaded"} - return retVal - - if os.path.isfile(filepath): - retVal = {"status": "True", "msg": "already downloaded"} - return retVal - - temp_filepath = filepath + ".part" - - self._active = True - bytes_to_be_downloaded = 0 - fmode, offset = "wb", 0 - chunksize, bytesdone, t0 = 16384, 0, time.time() - headers = {'User-Agent': HEADERS.get('User-Agent'), "Accept-Encoding": None} - if os.path.exists(temp_filepath): - offset = os.stat(temp_filepath).st_size - - if offset: - offset_range = 'bytes={}-'.format(offset) - headers['Range'] = offset_range - bytesdone = offset - fmode = "ab" - - status_string = (' {:,} Bytes [{:.2%}] received. Rate: [{:4.0f} ' - 'KB/s]. ETA: [{:.0f} secs]') - - if early_py_version: - status_string = (' {0:} Bytes [{1:.2%}] received. Rate:' - ' [{2:4.0f} KB/s]. ETA: [{3:.0f} secs]') - - try: - try: - response = self._sess.get(self.url, headers=headers, stream=True, timeout=10) - except conn_error as error: - return {'status': 'False', 'msg': 'ConnectionError: %s' % (str(error))} - if response.ok: - bytes_to_be_downloaded = total = int(response.headers.get('Content-Length')) - if bytesdone > 0: - bytes_to_be_downloaded = bytes_to_be_downloaded + bytesdone - total = bytes_to_be_downloaded - with open(temp_filepath, fmode) as media_file: - is_malformed = False - for chunk in response.iter_content(chunksize): - if not chunk: - break - media_file.write(chunk) - elapsed = time.time() - t0 - bytesdone += len(chunk) - if elapsed: - try: - rate = ((float(bytesdone) - float(offset)) / 1024.0) / elapsed - eta = (total - bytesdone) / (rate * 1024.0) - except ZeroDivisionError: - is_malformed = True - try: - os.unlink(temp_filepath) - except Exception: - pass - retVal = {"status" : "False", "msg" : "ZeroDivisionError : it seems, lecture has malfunction or is zero byte(s) .."} - break - else: - rate = 0 - eta = 0 - - if not is_malformed: - progress_stats = ( - bytesdone, bytesdone * 1.0 / total, rate, eta) - - if not quiet: - status = status_string.format(*progress_stats) - sys.stdout.write( - "\r" + status + ' ' * 4 + "\r") - sys.stdout.flush() - - if callback: - callback(total, *progress_stats) - if not response.ok: - code = response.status_code - reason = response.reason - retVal = { - "status": "False", "msg": "Udemy returned HTTP Code %s: %s" % (code, reason)} - response.close() - except KeyboardInterrupt as error: - raise error - except Exception as error: - retVal = {"status": "False", - "msg": "Reason : {}".format(str(error))} - return retVal - # check if file is downloaded completely - if os.path.isfile(temp_filepath): - total_bytes_done = os.stat(temp_filepath).st_size - if total_bytes_done == bytes_to_be_downloaded: - self._active = False - if total_bytes_done < bytes_to_be_downloaded: - # set active to be True as remaining bytes to be downloaded - self._active = True - # try downloading back again remaining bytes until we download completely - self.download(filepath=filepath, - unsafe=unsafe, - quiet=quiet) - - - if not self._active: - os.rename(temp_filepath, filepath) - retVal = {"status": "True", "msg": "download"} - - return retVal - -class UdemyCourse(object): - - def __init__(self, url, username='', password='', cookies='', basic=True, callback=None): - - self._url = url - self._username = username - self._password = password - self._cookies = cookies - self._callback = callback or (lambda x: None) - self._have_basic = False - - self._id = None - self._title = None - self._chapters_count = None - self._total_lectures = None - - self._chapters = [] - - if basic: - self._fetch_course() - - def _fetch_course(self): - raise NotImplementedError - - @property - def id(self): - if not self._id: - self._fetch_course() - return self._id - - @property - def title(self): - if not self._title: - self._fetch_course() - return self._title - - @property - def chapters(self): - if not self._chapters_count: - self._fetch_course() - return self._chapters_count - - @property - def lectures(self): - if not self._total_lectures: - self._fetch_course() - return self._total_lectures - - def get_chapters(self): - if not self._chapters: - self._fetch_course() - return self._chapters - -class UdemyChapters(object): - - def __init__(self): - - self._chapter_id = None - self._chapter_index = None - self._chapter_title = None - self._unsafe_title = None - self._lectures_count = None - - self._lectures = [] - - def __repr__(self): - chapter = "{title}".format(title=self.title) - return chapter - - @property - def id(self): - return self._chapter_id - - @property - def index(self): - return self._chapter_index - - @property - def title(self): - return self._chapter_title - - @property - def unsafe_title(self): - return self._unsafe_title - - @property - def lectures(self): - return self._lectures_count - - def get_lectures(self): - return self._lectures - -class UdemyLectures(object): - - def __init__(self): - - self._best = None - self._duration = None - self._extension = None - self._lecture_id = None - self._lecture_title = None - self._unsafe_title = None - self._lecture_index = None - self._sources_count = None - self._assets_count = None - self._subtitles_count = None - self._html_content = None - - self._assets = [] - self._streams = [] - self._subtitles = [] - - def __repr__(self): - lecture = "{title}".format(title=self.title) - return lecture - - @property - def id(self): - return self._lecture_id - - @property - def index(self): - return self._lecture_index - - @property - def title(self): - return self._lecture_title - - @property - def unsafe_title(self): - return self._unsafe_title - - @property - def html(self): - return self._html_content - - @property - def duration(self): - return self._duration - - @property - def extension(self): - return self._extension - - @property - def assets(self): - if not self._assets: - self._process_assets() - return self._assets - - @property - def streams(self): - if not self._streams: - self._process_streams() - return self._streams - - @property - def subtitles(self): - if not self._subtitles: - self._process_subtitles() - return self._subtitles - - def _getbest(self): - streams = self.streams - if not streams: - return None - def _sortkey(x, keyres=0, keyftype=0): - keyres = int(x.resolution.split('x')[0]) - keyftype = x.extension - st = (keyftype, keyres) - return st - - self._best = max(streams, key=_sortkey) - return self._best - - def getbest(self): - return self._getbest() - - def dump(self, filepath, unsafe=False): - retVal = {} - data = ''' - - - - %s - - -
-
-
-

%s

-
-
-
- - - - ''' % (self.title, self.html) - html = data.encode('utf-8').strip() - if not unsafe: - filename = "%s\\%s" % (filepath, self.title) if os.name == 'nt' else "%s/%s" % (filepath, self.title) - filename += ".html" - if unsafe: - filename = u"%s\\%s" % (filepath, self.unsafe_title) if os.name == 'nt' else u"%s/%s" % (filepath, self.unsafe_title) - filename += ".html" - - if os.path.isfile(filename): - retVal = {"status" : "True", "msg" : "already downloaded"} - return retVal - - try: - f = codecs.open(filename, 'wb', errors='ignore') - f.write(html) - except (OSError, Exception, UnicodeDecodeError) as e: - retVal = {'status' : 'False', 'msg' : '{}'.format(e)} - else: - retVal = {'status' : 'True', 'msg' : 'download'} - f.close() - - return retVal - -class UdemyLectureStream(Downloader): - - - def __init__(self, parent): - - self._mediatype = None - self._quality = None - self._resolution = None - self._dimention = None - self._extension = None - self._url = None - - self._parent = parent - self._filename = None - self._fsize = None - self._active = False - - Downloader.__init__(self) - - def __repr__(self): - out = "%s:%s@%s" % (self.mediatype, self.extension, self.quality) - return out - - def _generate_filename(self): - ok = re.compile(r'[^\\/:*?"<>|]') - filename = "".join(x if ok.match(x) else "_" for x in self.title) - filename += "." + self.extension - return filename - - def _generate_unsafe_filename(self): - ok = re.compile(r'[^\\/:*?"<>|]') - filename = "".join(x if ok.match(x) else "_" for x in self.unsafe_title) - filename += "." + self.extension - return filename - - @property - def resolution(self): - return self._resolution - - @property - def quality(self): - return self._quality - - @property - def url(self): - return self._url - - @property - def id(self): - return self._parent.id - - @property - def dimention(self): - return self._dimention - - @property - def extension(self): - return self._extension - - @property - def filename(self): - if not self._filename: - self._filename = self._generate_filename() - return self._filename - - @property - def title(self): - return self._parent.title - - @property - def unsafe_title(self): - return self._parent.unsafe_title - - @property - def unsafe_filename(self): - if not self._filename: - self._filename = self._generate_unsafe_filename() - return self._filename - - @property - def mediatype(self): - return self._mediatype - - def get_filesize(self): - if not self._fsize: - headers = {'User-Agent': HEADERS.get('User-Agent')} - try: - with requests.get(self.url, stream=True, headers=headers) as resp: - if resp.ok: - self._fsize = float(resp.headers.get('Content-Length', 0)) - if not resp.ok: - self._fsize = 0 - except conn_error: - self._fsize = 0 - return self._fsize - -class UdemyLectureAssets(Downloader): - - def __init__(self, parent): - - self._extension = None - self._mediatype = None - self._url = None - - self._parent = parent - self._filename = None - self._fsize = None - self._active = False - - Downloader.__init__(self) - - def __repr__(self): - out = "%s:%s@%s" % (self.mediatype, self.extension, self.extension) - return out - - def _generate_filename(self): - ok = re.compile(r'[^\\/:*?"<>|]') - filename = "".join(x if ok.match(x) else "_" for x in self.title) - filename += ".{}".format(self.extension) - return filename - - def _generate_unsafe_filename(self): - ok = re.compile(r'[^\\/:*?"<>|]') - filename = "".join(x if ok.match(x) else "_" for x in self.unsafe_title) - filename += ".{}".format(self.extension) - return filename - - @property - def id(self): - return self._parent.id - - @property - def url(self): - return self._url - - @property - def extension(self): - return self._extension - - @property - def title(self): - return self._parent.title - - @property - def unsafe_title(self): - return self._parent.unsafe_title - - @property - def filename(self): - if not self._filename: - self._filename = self._generate_filename() - return self._filename - - @property - def unsafe_filename(self): - if not self._filename: - self._filename = self._generate_unsafe_filename() - return self._filename - - @property - def mediatype(self): - return self._mediatype - - def get_filesize(self): - if not self._fsize: - headers = {'User-Agent': HEADERS.get('User-Agent')} - try: - with requests.get(self.url, stream=True, headers=headers) as resp: - if resp.ok: - self._fsize = float(resp.headers.get('Content-Length', 0)) - if not resp.ok: - self._fsize = 0 - except conn_error: - self._fsize = 0 - return self._fsize - -class UdemyLectureSubtitles(Downloader): - - def __init__(self, parent): - - self._mediatype = None - self._extension = None - self._language = None - self._url = None - - self._parent = parent - self._filename = None - self._fsize = None - self._active = False - - Downloader.__init__(self) - - def __repr__(self): - out = "%s:%s@%s" % (self.mediatype, self.language, self.extension) - return out - - def _generate_filename(self): - ok = re.compile(r'[^\\/:*?"<>|]') - filename = "".join(x if ok.match(x) else "_" for x in self.title) - filename += ".{}.{}".format(self.language, self.extension) - return filename - - def _generate_unsafe_filename(self): - ok = re.compile(r'[^\\/:*?"<>|]') - filename = "".join(x if ok.match(x) else "_" for x in self.unsafe_title) - filename += ".{}.{}".format(self.language, self.extension) - return filename - - @property - def id(self): - return self._parent.id - - @property - def url(self): - return self._url - - @property - def extension(self): - return self._extension - - @property - def language(self): - return self._language - - @property - def title(self): - return self._parent.title - - @property - def unsafe_title(self): - return self._parent.unsafe_title - - @property - def filename(self): - if not self._filename: - self._filename = self._generate_filename() - return self._filename - - @property - def unsafe_filename(self): - if not self._filename: - self._filename = self._generate_unsafe_filename() - return self._filename - - @property - def mediatype(self): - return self._mediatype - - def get_filesize(self): - if not self._fsize: - headers = {'User-Agent': HEADERS.get('User-Agent')} - try: - with requests.get(self.url, stream=True, headers=headers) as resp: - if resp.ok: - self._fsize = float(resp.headers.get('Content-Length', 0)) - if not resp.ok: - self._fsize = 0 - except conn_error: - self._fsize = 0 - return self._fsize diff --git a/udemy/_utils.py b/udemy/_utils.py deleted file mode 100644 index a80dd97..0000000 --- a/udemy/_utils.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 -License : MIT - - -Copyright (c) 2020 Nasir Khan (r0ot h3x49) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -from ._compat import ( - os, - re, - sys, - json, - NO_DEFAULT, - compat_HTMLParser, - ) - -def cache_credentials(username, password, quality="", output="", language=""): - fname = "configuration" - fmode = "w" - creds = { - "username" : username, - "password" : password, - "quality" : quality, - "output" : output, - "language" : language - } - fout = open(fname, fmode) - json.dump(creds, fout, indent=4) - fout.close() - return "cached" - -def use_cached_credentials(): - fname = "configuration" - try: - fout = open(fname) - except IOError as e: - creds = '' - return creds - except Exception as e: - creds = '' - return creds - else: - creds = json.load(fout) - fout.close() - return creds - - -# Thanks to a great open source utility youtube-dl .. -class HTMLAttributeParser(compat_HTMLParser): - """Trivial HTML parser to gather the attributes for a single element""" - def __init__(self): - self.attrs = {} - compat_HTMLParser.__init__(self) - - def handle_starttag(self, tag, attrs): - self.attrs = dict(attrs) - -def unescapeHTML(s): - clean = compat_HTMLParser() - data = clean.unescape(s) - return data - -def extract_attributes(html_element): - """Given a string for an HTML element such as - - Decode and return a dictionary of attributes. - { - 'a': 'foo', 'b': 'bar', c: 'baz', d: 'boz', - 'empty': '', 'noval': None, 'entity': '&', - 'sq': '"', 'dq': '\'' - }. - NB HTMLParser is stricter in Python 2.6 & 3.2 than in later versions, - but the cases in the unit test will work for all of 2.6, 2.7, 3.2-3.5. - """ - parser = HTMLAttributeParser() - try: - parser.feed(html_element) - parser.close() - except compat_HTMLParseError: - pass - return parser.attrs - -def hidden_inputs(html): - html = re.sub(r'', '', html) - hidden_inputs = {} - for input in re.findall(r'(?i)(]+>)', html): - attrs = extract_attributes(input) - if not input: - continue - if attrs.get('type') not in ('hidden', 'submit'): - continue - name = attrs.get('name') or attrs.get('id') - value = attrs.get('value') - if name and value is not None: - hidden_inputs[name] = value - return hidden_inputs - -def search_regex(pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None): - """ - Perform a regex search on the given string, using a single or a list of - patterns returning the first matching group. - In case of failure return a default value or raise a WARNING or a - RegexNotFoundError, depending on fatal, specifying the field name. - """ - if isinstance(pattern, str): - mobj = re.search(pattern, string, flags) - else: - for p in pattern: - mobj = re.search(p, string, flags) - if mobj: - break - - _name = name - - if mobj: - if group is None: - # return the first matching group - return next(g for g in mobj.groups() if g is not None) - else: - return mobj.group(group) - elif default is not NO_DEFAULT: - return default - elif fatal: - print('[-] Unable to extract %s' % _name) - exit(0) - else: - print('[-] unable to extract %s' % _name) - exit(0) - -def parse_json(json_string, video_id, transform_source=None, fatal=True): - if transform_source: - json_string = transform_source(json_string) - try: - return json.loads(json_string) - except ValueError as ve: - errmsg = '[-] %s: Failed to parse JSON ' % video_id - if fatal: - print(errmsg, ve) - else: - print(errmsg + str(ve)) - -def js_to_json(code): - COMMENT_RE = r'/\*(?:(?!\*/).)*?\*/|//[^\n]*' - SKIP_RE = r'\s*(?:{comment})?\s*'.format(comment=COMMENT_RE) - INTEGER_TABLE = ( - (r'(?s)^(0[xX][0-9a-fA-F]+){skip}:?$'.format(skip=SKIP_RE), 16), - (r'(?s)^(0+[0-7]+){skip}:?$'.format(skip=SKIP_RE), 8), - ) - - def fix_kv(m): - v = m.group(0) - if v in ('true', 'false', 'null'): - return v - elif v.startswith('/*') or v.startswith('//') or v == ',': - return "" - - if v[0] in ("'", '"'): - v = re.sub(r'(?s)\\.|"', lambda m: { - '"': '\\"', - "\\'": "'", - '\\\n': '', - '\\x': '\\u00', - }.get(m.group(0), m.group(0)), v[1:-1]) - - for regex, base in INTEGER_TABLE: - im = re.match(regex, v) - if im: - i = int(im.group(1), base) - return '"%d":' % i if v.endswith(':') else '%d' % i - - return '"%s"' % v - - return re.sub(r'''(?sx) - "(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"| - '(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'| - {comment}|,(?={skip}[\]}}])| - [a-zA-Z_][.a-zA-Z_0-9]*| - \b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:{skip}:)?| - [0-9]+(?={skip}:) - '''.format(comment=COMMENT_RE, skip=SKIP_RE), fix_kv, code) diff --git a/udemy/auth.py b/udemy/auth.py new file mode 100644 index 0000000..d27cd97 --- /dev/null +++ b/udemy/auth.py @@ -0,0 +1,120 @@ +# pylint: disable=R,C +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +from udemy.compat import conn_error, LOGIN_URL, cloudscraper +from udemy.logger import logger +from udemy.session import Session +from udemy.utils import ( + search_regex, + hidden_inputs, + to_configs, + load_configs, + extract_cookie_string, +) + + +class UdemyAuth(object): + def __init__(self, username="", password=""): + self.username = username + self.password = password + self._session = Session() + self._cloudsc = cloudscraper.create_scraper() + + def _form_hidden_input(self, form_id): + try: + resp = self._cloudsc.get(LOGIN_URL) # pylint: disable=W + resp.raise_for_status() + webpage = resp.text + except conn_error as error: + raise error + else: + login_form = hidden_inputs( + search_regex( + r'(?is)]+?id=(["\'])%s\1[^>]*>(?P
.+?)
' + % form_id, + webpage, + "%s form" % form_id, + group="form", + ) + ) + login_form.update({"email": self.username, "password": self.password}) + return login_form + + def is_session_exists(self): + is_exists = False + conf = load_configs() + if conf: + cookies = conf.get("cookies") + if cookies: + cookies = extract_cookie_string(cookies) + access_token = cookies.get("access_token") + client_id = cookies.get("client_id") + self._session._set_auth_headers( # pylint: disable=W + access_token=access_token, client_id=client_id + ) + self._session._session.cookies.update( # pylint: disable=W + {"access_token": access_token} + ) + try: + url = "https://www.udemy.com/api-2.0/courses/" + resp = self._session._get(url) # pylint: disable=W + resp.raise_for_status() + is_exists = True + except Exception as error: # pylint: disable=W + logger.error( + msg=f"Udemy Says: {error} session cookie seems to be expired..." + ) + is_exists = False + return is_exists, conf + + def authenticate(self, access_token="", client_id=""): + if not access_token and not client_id: + data = self._form_hidden_input(form_id="login-form") + self._cloudsc.headers.update({"Referer": LOGIN_URL}) + auth_response = self._cloudsc.post( # pylint: disable=W + LOGIN_URL, data=data, allow_redirects=False + ) # pylint: disable=W + auth_cookies = auth_response.cookies + + access_token = auth_cookies.get("access_token", "") + client_id = auth_cookies.get("client_id", "") + + if access_token: + # dump cookies to configs + _ = to_configs( + username=self.username, + password=self.password, + cookies=f"access_token={access_token}", + ) + self._session._set_auth_headers( # pylint: disable=W + access_token=access_token, client_id=client_id + ) + self._session._session.cookies.update( # pylint: disable=W + {"access_token": access_token} + ) + return self._session, access_token + else: + self._session._set_auth_headers() # pylint: disable=W + return None, None diff --git a/udemy/_colorized/__init__.py b/udemy/colorized/__init__.py similarity index 90% rename from udemy/_colorized/__init__.py rename to udemy/colorized/__init__.py index 5a7529d..1eb1e5f 100644 --- a/udemy/_colorized/__init__.py +++ b/udemy/colorized/__init__.py @@ -7,7 +7,7 @@ License : MIT -Copyright (c) 2018 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -22,5 +22,5 @@ ''' -from .colors import * -from . import banner +from udemy.colorized.colors import * +from udemy.colorized.banner import banner diff --git a/udemy/_colorized/banner.py b/udemy/colorized/banner.py similarity index 90% rename from udemy/_colorized/banner.py rename to udemy/colorized/banner.py index c476008..e20856b 100644 --- a/udemy/_colorized/banner.py +++ b/udemy/colorized/banner.py @@ -2,12 +2,12 @@ ''' -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 License : MIT -Copyright (c) 2018 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -32,7 +32,7 @@ def banner(): %s%s / /_/ / /_/ / __/ / / / / / /_/ /_____/ /_/ / / %s%s \__,_/\__,_/\___/_/ /_/ /_/\__, / \__,_/_/ %s%s /____/ - %s%sVersion : %s%s0.5 + %s%sVersion : %s%s1.0 %s%sAuthor : %s%sNasir Khan (r0ot h3x49) %s%sGithub : %s%shttps://github.com/r0oth3x49 diff --git a/udemy/_colorized/colors.py b/udemy/colorized/colors.py similarity index 92% rename from udemy/_colorized/colors.py rename to udemy/colorized/colors.py index ba79b11..7b83695 100644 --- a/udemy/_colorized/colors.py +++ b/udemy/colorized/colors.py @@ -1,3 +1,4 @@ +# pylint: disable=R,C,W,E #!/usr/bin/python """ @@ -7,7 +8,7 @@ License : MIT -Copyright (c) 2018 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -21,11 +22,9 @@ THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ - from colorama import init, Fore, Back, Style init(autoreset=True) -# colors foreground text: fc = Fore.CYAN fg = Fore.GREEN fw = Fore.WHITE @@ -34,8 +33,6 @@ fy = Fore.YELLOW fm = Fore.MAGENTA - -# colors background text: bc = Back.CYAN bg = Back.GREEN bw = Back.WHITE @@ -44,7 +41,6 @@ by = Fore.YELLOW bm = Fore.MAGENTA -# colors style text: sd = Style.DIM sn = Style.NORMAL sb = Style.BRIGHT diff --git a/udemy/compat.py b/udemy/compat.py new file mode 100644 index 0000000..e64d9f9 --- /dev/null +++ b/udemy/compat.py @@ -0,0 +1,89 @@ +# pylint: disable=R,C,W,E +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + +import re +import io +import os +import sys +import time +import json +import m3u8 +import codecs +import requests +import cloudscraper +from html.parser import HTMLParser as compat_HTMLParser +from http.cookies import SimpleCookie as ParseCookie +from requests.exceptions import ConnectionError as conn_error + +encoding = str + +NO_DEFAULT = object() +LOGIN_URL = "https://www.udemy.com/join/login-popup/?ref=&display_type=popup&loc" +LOGOUT_URL = "https://www.udemy.com/user/logout" + +WISHLIST_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/wishlisted-courses?fields[course]=id,url,published_title&ordering=-access_time&page=1&page_size=1000" +COLLECTION_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses-collections/?collection_has_courses=True&course_limit=20&fields[course]=last_accessed_time,published_title&fields[user_has_subscribed_courses_collection]=@all&page=1&page_size=1000" +MY_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,published_title&ordering=-last_accessed,-access_time&page=1&page_size=10000" +COURSE_SEARCH = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,published_title&page=1&page_size=1000&ordering=-last_accessed,-access_time&search={course_name}" +COURSE_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/cached-subscriber-curriculum-items?fields[asset]=results,external_url,time_estimation,download_urls,slide_urls,filename,asset_type,captions,stream_urls,body&fields[chapter]=object_index,title,sort_order&fields[lecture]=id,title,object_index,asset,supplementary_assets,view_html&page_size=10000" +SUBSCRIBED_COURSES = "https://www.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-last_accessed&fields[course]=id,url&page=1&page_size=12" +HEADERS = { + "Origin": "www.udemy.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0", + "Referer": "https://www.udemy.com/join/login-popup/", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", +} + + +__ALL__ = [ + "re", + "io", + "os", + "sys", + "time", + "json", + "pyver", + "codecs", + "encoding", + "requests", + "conn_error", + "cloudscraper", + "compat_HTMLParser", + "ParseCookie", + "HEADERS", + "LOGIN_URL", + "NO_DEFAULT", + "COURSE_URL", + "LOGOUT_URL", + "WISHLIST_URL", + "COLLECTION_URL", + "MY_COURSES_URL", + "COURSE_SEARCH", + "SUBSCRIBED_COURSES", +] diff --git a/udemy/extract.py b/udemy/extract.py new file mode 100644 index 0000000..829b1d0 --- /dev/null +++ b/udemy/extract.py @@ -0,0 +1,792 @@ +# pylint: disable=R,C,W +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + + +from udemy.auth import UdemyAuth +from udemy.utils import ( + parse_json, + js_to_json, + search_regex, + unescapeHTML, + extract_cookie_string, +) +from udemy.compat import ( + re, + sys, + time, + m3u8, + encoding, + conn_error, + COURSE_URL, + MY_COURSES_URL, + COURSE_SEARCH, + COLLECTION_URL, + SUBSCRIBED_COURSES, +) +from udemy.sanitize import slugify, sanitize, SLUG_OK +from udemy.logger import logger +from udemy.getpass import getpass + + +class Udemy: + def __init__(self): + self._session = "" + self._cookies = "" + self._access_token = "" + + def _clean(self, text): + ok = re.compile(r'[^\\/:*?"<>|]') + text = "".join(x if ok.match(x) else "_" for x in text) + text = re.sub(r"\.+$", "", text.strip()) + return text + + def _sanitize(self, unsafetext): + text = sanitize( + slugify(unsafetext, lower=False, spaces=True, ok=SLUG_OK + "().[]") + ) + return text + + def _course_name(self, url): + mobj = re.search( + r"(?i)(?://(?P.+?).udemy.com/(?:course(/draft)*/)?(?P[a-zA-Z0-9_-]+))", + url, + ) + if mobj: + return mobj.group("portal_name"), mobj.group("name_or_id") + + def _login(self, username="", password="", cookies=""): + # check if we already have session on udemy. + auth = UdemyAuth() + is_exists, conf = auth.is_session_exists() + if is_exists: + logger.info( + msg="Using existing session..", new_line=True, + ) + cookies = conf.get("cookies") + if not is_exists: + cookies = None + if not username and not password: + logger.info( + msg="Updating session cookie..", new_line=True, + ) + username = conf.get("username") + password = conf.get("password") + if not username and not password: + print("") + username = getpass.getuser(prompt="Username : ") + password = getpass.getpass(prompt="Password : ") + print("\n") + if not cookies: + auth.username = username + auth.password = password + self._session, self._access_token = auth.authenticate() + if cookies: + self._cookies = extract_cookie_string(raw_cookies=cookies) + self._access_token = self._cookies.get("access_token") + client_id = self._cookies.get("client_id") + time.sleep(0.3) + self._session, _ = auth.authenticate( + access_token=self._access_token, client_id=client_id + ) + self._session._session.cookies.update(self._cookies) + if self._session is not None: + return {"login": "successful"} + else: + return {"login": "failed"} + + def _logout(self): + """terminates current session if it's based on username/password and keeps the session cookie""" + return self._session.terminate() + + def _subscribed_courses(self, portal_name, course_name): + results = [] + self._session._headers.update( + { + "Host": "{portal_name}.udemy.com".format(portal_name=portal_name), + "Referer": "https://{portal_name}.udemy.com/home/my-courses/search/?q={course_name}".format( + portal_name=portal_name, course_name=course_name + ), + } + ) + url = COURSE_SEARCH.format(portal_name=portal_name, course_name=course_name) + try: + webpage = self._session._get(url).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except (ValueError, Exception) as error: + logger.error(msg=f"Udemy Says: {error} on {url}") + time.sleep(0.8) + sys.exit(0) + else: + results = webpage.get("results", []) + return results + + def _my_courses(self, portal_name): + results = [] + try: + url = MY_COURSES_URL.format(portal_name=portal_name) + webpage = self._session._get(url).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except (ValueError, Exception) as error: + logger.error(msg=f"Udemy Says: {error}") + time.sleep(0.8) + sys.exit(0) + else: + results = webpage.get("results", []) + return results + + def _archived_courses(self, portal_name): + results = [] + try: + url = MY_COURSES_URL.format(portal_name=portal_name) + url = f"{url}&is_archived=true" + webpage = self._session._get(url).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except (ValueError, Exception) as error: + logger.error(msg=f"Udemy Says: {error}") + time.sleep(0.8) + sys.exit(0) + else: + results = webpage.get("results", []) + return results + + def _subscribed_collection_courses(self, portal_name): + url = COLLECTION_URL.format(portal_name=portal_name) + courses_lists = [] + try: + webpage = self._session._get(url).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except (ValueError, Exception) as error: + logger.error(msg=f"Udemy Says: {error}") + time.sleep(0.8) + sys.exit(0) + else: + results = webpage.get("results", []) + if results: + [ + courses_lists.extend(courses.get("courses", [])) + for courses in results + if courses.get("courses", []) + ] + return courses_lists + + def _extract_subscribed_courses(self): + def clean_urls(courses): + _urls = [] + courses = [ + dict(tupleized) + for tupleized in set(tuple(item.items()) for item in courses) + ] + for entry in courses: + logger.progress(msg="Fetching all enrolled course(s) url(s).. ") + url = entry.get("url") + if not url: + continue + url = f"https://www.udemy.com{url}" + _urls.append(url) + _urls = list(set(_urls)) + return _urls + + _temp = [] + try: + response = self._session._get(SUBSCRIBED_COURSES).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except (ValueError, Exception) as error: + logger.error(msg=f"Udemy Says: {error}") + time.sleep(0.8) + sys.exit(0) + else: + results = response.get("results", []) + _temp.extend(results) + _next = response.get("next") + logger.progress(msg="Fetching all enrolled course(s) url(s).. ") + while _next: + logger.progress(msg="Fetching all enrolled course(s) url(s).. ") + try: + resp = self._session._get(_next) + resp.raise_for_status() + resp = resp.json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except Exception as error: + logger.error(msg=f"Udemy Says: error, {error}") + time.sleep(0.8) + sys.exit(0) + else: + _next = resp.get("next") + results = resp.get("results", []) + _temp.extend(results) + if _temp: + _temp = clean_urls(_temp) + return _temp + + def __extract_course(self, response, course_name): + _temp = {} + if response: + for entry in response: + course_id = str(entry.get("id")) + published_title = entry.get("published_title") + if course_name in (published_title, course_id): + _temp = entry + break + return _temp + + def _extract_course_info(self, url): + portal_name, course_name = self._course_name(url) + course = {} + results = self._subscribed_courses( + portal_name=portal_name, course_name=course_name + ) + course = self.__extract_course(response=results, course_name=course_name) + if not course: + results = self._my_courses(portal_name=portal_name) + course = self.__extract_course(response=results, course_name=course_name) + if not course: + results = self._subscribed_collection_courses(portal_name=portal_name) + course = self.__extract_course(response=results, course_name=course_name) + if not course: + results = self._archived_courses(portal_name=portal_name) + course = self.__extract_course(response=results, course_name=course_name) + + if course: + course.update({"portal_name": portal_name}) + return course.get("id"), course + if not course: + logger.failed(msg="Downloading course information, course id not found .. ") + logger.info( + msg="It seems either you are not enrolled or you have to visit the course atleast once while you are logged in.", + new_line=True, + ) + logger.info( + msg="Trying to logout now...", new_line=True, + ) + if not self._cookies: + self._logout() + logger.info( + msg="Logged out successfully.", new_line=True, + ) + sys.exit(0) + + def _extract_large_course_content(self, url): + url = url.replace("10000", "300") if url.endswith("10000") else url + try: + data = self._session._get(url).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + else: + _next = data.get("next") + while _next: + try: + resp = self._session._get(_next).json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + else: + _next = resp.get("next") + results = resp.get("results") + if results and isinstance(results, list): + for d in resp["results"]: + data["results"].append(d) + return data + + def _extract_course_json(self, url, course_id, portal_name): + self._session._headers.update({"Referer": url}) + url = COURSE_URL.format(portal_name=portal_name, course_id=course_id) + try: + resp = self._session._get(url) + if resp.status_code in [502, 503]: + resp = self._extract_large_course_content(url=url) + else: + resp = resp.json() + except conn_error as error: + logger.error(msg=f"Udemy Says: Connection error, {error}") + time.sleep(0.8) + sys.exit(0) + except (ValueError, Exception): + resp = self._extract_large_course_content(url=url) + return resp + else: + return resp + + def _html_to_json(self, view_html, lecture_id): + data = parse_json( + search_regex( + r'videojs-setup-data=(["\'])(?P{.+?})\1', + view_html, + "setup data", + default="{}", + group="data", + ), + lecture_id, + transform_source=unescapeHTML, + fatal=False, + ) + text_tracks = parse_json( + search_regex( + r'text-tracks=(["\'])(?P\[.+?\])\1', + view_html, + "text tracks", + default="{}", + group="data", + ), + lecture_id, + transform_source=lambda s: js_to_json(unescapeHTML(s)), + fatal=False, + ) + return data, text_tracks + + def _extract_m3u8(self, url): + """extracts m3u8 streams""" + _temp = [] + try: + resp = self._session._get(url) + resp.raise_for_status() + raw_data = resp.text + m3u8_object = m3u8.loads(raw_data) + playlists = m3u8_object.playlists + seen = set() + for pl in playlists: + resolution = pl.stream_info.resolution + codecs = pl.stream_info.codecs + if not resolution: + continue + if not codecs: + continue + width, height = resolution + download_url = pl.uri + if height not in seen: + seen.add(height) + _temp.append( + { + "type": "hls", + "height": str(height), + "width": str(width), + "extension": "mp4", + "download_url": download_url, + } + ) + except Exception as error: + logger.error(msg=f"Udemy Says : '{error}' while fetching hls streams..") + return _temp + + def _extract_ppt(self, assets): + _temp = [] + download_urls = assets.get("download_urls") + filename = self._sanitize(assets.get("filename")) + if download_urls and isinstance(download_urls, dict): + extension = filename.rsplit(".", 1)[-1] if "." in filename else "" + download_url = download_urls.get("Presentation", [])[0].get("file") + _temp.append( + { + "type": "presentation", + "filename": filename, + "extension": extension, + "download_url": download_url, + } + ) + return _temp + + def _extract_file(self, assets): + _temp = [] + download_urls = assets.get("download_urls") + filename = self._sanitize(assets.get("filename")) + if download_urls and isinstance(download_urls, dict): + extension = filename.rsplit(".", 1)[-1] if "." in filename else "" + download_url = download_urls.get("File", [])[0].get("file") + _temp.append( + { + "type": "file", + "filename": filename, + "extension": extension, + "download_url": download_url, + } + ) + return _temp + + def _extract_ebook(self, assets): + _temp = [] + download_urls = assets.get("download_urls") + filename = self._sanitize(assets.get("filename")) + if download_urls and isinstance(download_urls, dict): + extension = filename.rsplit(".", 1)[-1] if "." in filename else "" + download_url = download_urls.get("E-Book", [])[0].get("file") + _temp.append( + { + "type": "ebook", + "filename": filename, + "extension": extension, + "download_url": download_url, + } + ) + return _temp + + def _extract_audio(self, assets): + _temp = [] + download_urls = assets.get("download_urls") + filename = self._sanitize(assets.get("filename")) + if download_urls and isinstance(download_urls, dict): + extension = filename.rsplit(".", 1)[-1] if "." in filename else "" + download_url = download_urls.get("Audio", [])[0].get("file") + _temp.append( + { + "type": "audio", + "filename": filename, + "extension": extension, + "download_url": download_url, + } + ) + return _temp + + def _extract_sources(self, sources, skip_hls_stream=False): + _temp = [] + if sources and isinstance(sources, list): + for source in sources: + label = source.get("label") + download_url = source.get("file") + if not download_url: + continue + if label.lower() == "audio": + continue + height = label if label else None + if height == "2160": + width = "3840" + elif height == "1440": + width = "2560" + elif height == "1080": + width = "1920" + elif height == "720": + width = "1280" + elif height == "480": + width = "854" + elif height == "360": + width = "640" + elif height == "240": + width = "426" + else: + width = "256" + if ( + source.get("type") == "application/x-mpegURL" + or "m3u8" in download_url + ): + if not skip_hls_stream: + out = self._extract_m3u8(download_url) + if out: + _temp.extend(out) + else: + _type = source.get("type") + _temp.append( + { + "type": "video", + "height": height, + "width": width, + "extension": _type.replace("video/", ""), + "download_url": download_url, + } + ) + return _temp + + def _extract_subtitles(self, tracks): + _temp = [] + if tracks and isinstance(tracks, list): + for track in tracks: + if not isinstance(track, dict): + continue + if track.get("_class") != "caption": + continue + download_url = track.get("url") + if not download_url or not isinstance(download_url, encoding): + continue + lang = ( + track.get("language") + or track.get("srclang") + or track.get("label") + or track["locale_id"].split("_")[0] + ) + ext = "vtt" if "vtt" in download_url.rsplit(".", 1)[-1] else "srt" + _temp.append( + { + "type": "subtitle", + "language": lang, + "extension": ext, + "download_url": download_url, + } + ) + return _temp + + def _extract_supplementary_assets(self, supp_assets): + _temp = [] + for entry in supp_assets: + filename = self._sanitize(entry.get("filename")) + download_urls = entry.get("download_urls") + external_url = entry.get("external_url") + asset_type = entry.get("asset_type").lower() + if asset_type == "file": + if download_urls and isinstance(download_urls, dict): + extension = filename.rsplit(".", 1)[-1] if "." in filename else "" + download_url = download_urls.get("File", [])[0].get("file") + _temp.append( + { + "type": "file", + "filename": filename, + "extension": extension, + "download_url": download_url, + } + ) + elif asset_type == "sourcecode": + if download_urls and isinstance(download_urls, dict): + extension = filename.rsplit(".", 1)[-1] if "." in filename else "" + download_url = download_urls.get("SourceCode", [])[0].get("file") + _temp.append( + { + "type": "source_code", + "filename": filename, + "extension": extension, + "download_url": download_url, + } + ) + elif asset_type == "externallink": + _temp.append( + { + "type": "external_link", + "filename": filename, + "extension": "txt", + "download_url": external_url, + } + ) + return _temp + + def _real_extract(self, url="", skip_hls_stream=False): + + _udemy = {} + course_id, course_info = self._extract_course_info(url) + + if course_info and isinstance(course_info, dict): + course_title = course_info.get("published_title") + portal_name = course_info.get("portal_name") + + course_json = self._extract_course_json(url, course_id, portal_name) + course = course_json.get("results") + resource = course_json.get("detail") + + if resource: + if not self._cookies: + logger.error( + msg=f"Udemy Says : '{resource}' Run udemy-dl against course within few seconds" + ) + if self._cookies: + logger.error( + msg=f"Udemy Says : '{resource}' cookies seems to be expired" + ) + logger.info( + msg="Trying to logout now...", new_line=True, + ) + if not self._cookies: + self._logout() + logger.info( + msg="Logged out successfully.", new_line=True, + ) + sys.exit(0) + + _udemy["access_token"] = self._access_token + _udemy["course_id"] = course_id + _udemy["course_title"] = course_title + _udemy["chapters"] = [] + + counter = -1 + + if course: + for entry in course: + + clazz = entry.get("_class") + asset = entry.get("asset") + supp_assets = entry.get("supplementary_assets") + + if clazz == "chapter": + lectures = [] + chapter_index = entry.get("object_index") + chapter_title = "{0:02d} ".format(chapter_index) + self._clean( + entry.get("title") + ) + if chapter_title not in _udemy["chapters"]: + _udemy["chapters"].append( + { + "chapter_title": chapter_title, + "chapter_id": entry.get("id"), + "chapter_index": chapter_index, + "lectures": [], + } + ) + counter += 1 + elif clazz == "lecture": + + lecture_id = entry.get("id") + if len(_udemy["chapters"]) == 0: + lectures = [] + chapter_index = entry.get("object_index") + chapter_title = "{0:02d} ".format(chapter_index) + self._clean( + entry.get("title") + ) + if chapter_title not in _udemy["chapters"]: + _udemy["chapters"].append( + { + "chapter_title": chapter_title, + "chapter_id": lecture_id, + "chapter_index": chapter_index, + "lectures": [], + } + ) + counter += 1 + + if lecture_id: + + retVal = [] + + if isinstance(asset, dict): + asset_type = ( + asset.get("asset_type").lower() + or asset.get("assetType").lower() + ) + if asset_type == "article": + if ( + isinstance(supp_assets, list) + and len(supp_assets) > 0 + ): + retVal = self._extract_supplementary_assets( + supp_assets + ) + elif asset_type == "video": + if ( + isinstance(supp_assets, list) + and len(supp_assets) > 0 + ): + retVal = self._extract_supplementary_assets( + supp_assets + ) + elif asset_type == "e-book": + retVal = self._extract_ebook(asset) + elif asset_type == "file": + retVal = self._extract_file(asset) + elif asset_type == "presentation": + retVal = self._extract_ppt(asset) + elif asset_type == "audio": + retVal = self._extract_audio(asset) + + logger.progress(msg="Downloading course information .. ") + lecture_index = entry.get("object_index") + lecture_title = "{0:03d} ".format(lecture_index) + self._clean( + entry.get("title") + ) + data = asset.get("stream_urls") + if data and isinstance(data, dict): + sources = data.get("Video") + tracks = asset.get("captions") + duration = asset.get("time_estimation") + sources = self._extract_sources( + sources, skip_hls_stream=skip_hls_stream + ) + subtitles = self._extract_subtitles(tracks) + sources_count = len(sources) + subtitle_count = len(subtitles) + lectures.append( + { + "lecture_index": lecture_index, + "lectures_id": lecture_id, + "lecture_title": lecture_title, + "duration": duration, + "assets": retVal, + "assets_count": len(retVal), + "sources": sources, + "subtitles": subtitles, + "subtitle_count": subtitle_count, + "sources_count": sources_count, + } + ) + else: + lectures.append( + { + "lecture_index": lecture_index, + "lectures_id": lecture_id, + "lecture_title": lecture_title, + "html_content": asset.get("body"), + "extension": "html", + "assets": retVal, + "assets_count": len(retVal), + "subtitle_count": 0, + "sources_count": 0, + } + ) + + _udemy["chapters"][counter]["lectures"] = lectures + _udemy["chapters"][counter]["lectures_count"] = len(lectures) + elif clazz == "quiz": + lecture_id = entry.get("id") + if len(_udemy["chapters"]) == 0: + lectures = [] + chapter_index = entry.get("object_index") + chapter_title = "{0:02d} ".format(chapter_index) + self._clean( + entry.get("title") + ) + if chapter_title not in _udemy["chapters"]: + _udemy["chapters"].append( + { + "chapter_title": chapter_title, + "chapter_id": lecture_id, + "chapter_index": chapter_index, + "lectures": [], + } + ) + counter += 1 + _udemy["chapters"][counter]["lectures"] = lectures + _udemy["chapters"][counter]["lectures_count"] = len(lectures) + _udemy["total_chapters"] = len(_udemy["chapters"]) + _udemy["total_lectures"] = sum( + [ + entry.get("lectures_count", 0) + for entry in _udemy["chapters"] + if entry + ] + ) + + return _udemy diff --git a/udemy/ffmpeg.py b/udemy/ffmpeg.py new file mode 100644 index 0000000..a2439ea --- /dev/null +++ b/udemy/ffmpeg.py @@ -0,0 +1,245 @@ +#!/usr/bin/python3 +# pylint: disable=R,C,W,E + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +import subprocess +from udemy.compat import re, time + +# from udemy.logger import logger +from udemy.progress import progress + + +class FFMPeg: + + _PROGRESS_PATTERN = re.compile( + r"(frame|fps|total_size|out_time|bitrate|speed|progress)\s*\=\s*(\S+)" + ) + + def __init__( + self, duration, url, token, filepath, quiet=False, callback=lambda *x: None + ): + self.url = url + self.filepath = filepath + self.quiet = quiet + self.duration = duration + self.callback = callback + self.token = token + + def _command(self): + """ + ffmpeg.exe -headers "Authorization: Bearer {token}" -i "" -c copy -bsf:a aac_adtstoasc out.mp4 + """ + command = [ + "ffmpeg", + "-headers", + f"Authorization: Bearer {self.token}", + "-i", + f"{self.url}", + "-c", + "copy", + "-bsf:a", + "aac_adtstoasc", + f"{self.filepath}", + "-y", + "-progress", + "pipe:2", + ] + return command + + def _fetch_total_duration(self, line): + duration_in_secs = 0 + duration_regex = re.compile(r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}") + mobj = duration_regex.search(line) + if mobj: + duration_tuple = mobj.groups() + duration_in_secs = ( + int(duration_tuple[0]) * 60 + + int(duration_tuple[1]) * 60 + + int(duration_tuple[2]) + ) + else: + duration_in_secs = self.duration + return duration_in_secs + + def _fetch_current_duration_done(self, time_str): + time_str = time_str.split(":") + return ( + int(time_str[0]) * 60 + + int(time_str[1]) * 60 + + int(time_str[2].split(".")[0]) + ) + + def _prepare_time_str(self, secs): + (mins, secs) = divmod(secs, 60) + (hours, mins) = divmod(mins, 60) + if hours > 99: + time_str = "--:--:--" + if hours == 0: + time_str = "%02d:%02ds" % (mins, secs) + else: + time_str = "%02d:%02d:%02ds" % (hours, mins, secs) + return time_str + + def _progress( + self, iterations, total, bytesdone, speed, elapsed, bar_length=30, fps=None + ): + offset = 0 + filled_length = int(round(bar_length * iterations / float(total))) + percents = format(100.00 * (iterations * 1.0 / float(total)), ".2f") + + if bytesdone <= 1048576: + _receiving = round(float(bytesdone) / 1024.00, 2) + _received = format( + _receiving if _receiving < 1024.00 else _receiving / 1024.00, ".2f" + ) + suffix_recvd = "KB" if _receiving < 1024.00 else "MB" + else: + _receiving = round(float(bytesdone) / 1048576, 2) + _received = format( + _receiving if _receiving < 1024.00 else _receiving / 1024.00, ".2f" + ) + suffix_recvd = "MB" if _receiving < 1024.00 else "GB" + + suffix_rate = "Kb/s" if speed < 1024.00 else "Mb/s" + if fps: + suffix_rate += f" {fps}/fps" + if elapsed: + rate = ((float(iterations) - float(offset)) / 1024.0) / elapsed + eta = (total - iterations) / (rate * 1024.0) + else: + rate = 0 + eta = 0 + rate = format(speed if speed < 1024.00 else speed / 1024.00, ".2f") + (mins, secs) = divmod(eta, 60) + (hours, mins) = divmod(mins, 60) + if hours > 99: + eta = "--:--:--" + if hours == 0: + eta = "eta %02d:%02ds" % (mins, secs) + else: + eta = "eta %02d:%02d:%02ds" % (hours, mins, secs) + if secs == 0: + eta = "\n" + + total_time = self._prepare_time_str(total) + done_time = self._prepare_time_str(iterations) + downloaded = f"{total_time}/{done_time}" + + received_bytes = str(_received) + str(suffix_recvd) + percents = f"{received_bytes} {percents}" + + progress.hls_progress( + downloaded=downloaded, + percents=percents, + filled_length=filled_length, + rate=str(rate) + str(suffix_rate), + suffix=eta, + bar_length=bar_length, + ) + + def _parse_progress(self, line): + items = {key: value for key, value in self._PROGRESS_PATTERN.findall(line)} + return items + + def download(self): + total_time = None + t0 = time.time() + progress_lines = [] + active = True + retVal = {} + command = self._command() + bytes_done = 0 + download_speed = 0 + try: + with subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) as proc: + while active: + elapsed = time.time() - t0 + try: + line = proc.stderr.readline().decode("utf-8").strip() + if not total_time: + total_time = self._fetch_total_duration(line) + if "progress=end" in line: + try: + self._progress( + total_time, + total_time, + bytes_done, + download_speed, + elapsed, + ) + except KeyboardInterrupt: + retVal = { + "status": "False", + "msg": "Error: KeyboardInterrupt", + } + raise KeyboardInterrupt + except Exception as err: + {"status": "False", "msg": f"Error: {err}"} + active = False + retVal = {"status": "True", "msg": "download"} + break + if "progress" not in line: + progress_lines.append(line) + else: + lines = "\n".join(progress_lines) + items = self._parse_progress(lines) + if items: + secs = self._fetch_current_duration_done( + items.get("out_time") + ) + _tsize = ( + items.get("total_size").lower().replace("kb", "") + ) + _brate = ( + items.get("bitrate").lower().replace("kbits/s", "") + ) + fps = items.get("fps") + bytes_done = float(_tsize) if _tsize != "n/a" else 0 + download_speed = float(_brate) if _brate != "n/a" else 0 + try: + self._progress( + secs, + total_time, + bytes_done, + download_speed, + elapsed, + fps=fps, + ) + except KeyboardInterrupt: + retVal = { + "status": "False", + "msg": "Error: KeyboardInterrupt", + } + raise KeyboardInterrupt + except Exception as err: + {"status": "False", "msg": f"Error: {err}"} + progress_lines = [] + except KeyboardInterrupt: + active = False + retVal = {"status": "False", "msg": "Error: KeyboardInterrupt"} + raise KeyboardInterrupt + except KeyboardInterrupt: + raise KeyboardInterrupt + return retVal diff --git a/udemy/_getpass.py b/udemy/getpass.py similarity index 74% rename from udemy/_getpass.py rename to udemy/getpass.py index 8375034..ccec36c 100644 --- a/udemy/_getpass.py +++ b/udemy/getpass.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/python3 +# pylint: disable=R,C,W,E """ @@ -7,7 +8,7 @@ License : MIT -Copyright (c) 2020 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -24,16 +25,15 @@ import os import sys -import time -if os.name == 'nt': - import msvcrt + +if os.name == "nt": from msvcrt import getch as _win_getch else: import tty import termios -class GetPass(object): +class GetPass(object): def _unix_getch(self): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) @@ -43,38 +43,33 @@ def _unix_getch(self): finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch - - def getuser(self, prompt='Username : '): + + def getuser(self, prompt="Username : "): """Prompt for Username """ - if sys.version_info[:2] >= (3, 0): - sys.stdout.write('{}'.format(prompt)) - sys.stdout.flush() - username = input() - else: - sys.stdout.write('{}'.format(prompt)) - sys.stdout.flush() - username = raw_input() + sys.stdout.write("{}".format(prompt)) + sys.stdout.flush() + username = input() return username - def getpass(self, prompt='Password : '): + def getpass(self, prompt="Password : "): """Prompt for password and replace each character by asterik (*)""" - sys.stdout.write('{}'.format(prompt)) + sys.stdout.write("{}".format(prompt)) sys.stdout.flush() pw = "" while True: - c = _win_getch() if os.name == 'nt' else self._unix_getch() - if os.name == 'nt': + c = _win_getch() if os.name == "nt" else self._unix_getch() + if os.name == "nt": if ord(c) == 13: break if ord(c) == 3: raise KeyboardInterrupt if ord(c) == 8: if len(pw) > 0: - pw = pw[:-1] - s = "*" * len(pw) - sys.stdout.write('\033[2K\033[1G') + pw = pw[:-1] + s = "*" * len(pw) + sys.stdout.write("\033[2K\033[1G") sys.stdout.flush() - sys.stdout.write('\r\r\r{}{}'.format(prompt, s)) + sys.stdout.write("\r\r\r{}{}".format(prompt, s)) sys.stdout.flush() else: pass @@ -86,10 +81,10 @@ def getpass(self, prompt='Password : '): pass else: if sys.version_info[:2] >= (3, 0): - pw = pw + c.decode('utf-8') + pw = pw + c.decode("utf-8") else: pw = pw + c - sys.stdout.write('*') + sys.stdout.write("*") sys.stdout.flush() else: if ord(c) == 13: @@ -98,11 +93,11 @@ def getpass(self, prompt='Password : '): raise KeyboardInterrupt if ord(c) == 127: if len(pw) > 0: - pw = pw[:-1] - s = "*" * len(pw) - sys.stdout.write('\033[2K\033[1G') + pw = pw[:-1] + s = "*" * len(pw) + sys.stdout.write("\033[2K\033[1G") sys.stdout.flush() - sys.stdout.write('\r\r\r{}{}'.format(prompt, s)) + sys.stdout.write("\r\r\r{}{}".format(prompt, s)) sys.stdout.flush() else: pass @@ -118,3 +113,6 @@ def getpass(self, prompt='Password : '): sys.stdout.flush() return pw + + +getpass = GetPass() diff --git a/udemy/internal.py b/udemy/internal.py new file mode 100644 index 0000000..f6f8c08 --- /dev/null +++ b/udemy/internal.py @@ -0,0 +1,208 @@ +# pylint: disable=R,C,W +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +from udemy.compat import time, sys +from udemy.logger import logger +from udemy.extract import Udemy +from udemy.shared import ( + UdemyCourse, + UdemyCourses, + UdemyChapters, + UdemyLectures, + UdemyLectureStream, + UdemyLectureAssets, + UdemyLectureSubtitles, +) + + +class InternUdemyCourses(UdemyCourses, Udemy): + def __init__(self, *args, **kwargs): + super(InternUdemyCourses, self).__init__(*args, **kwargs) + + def _fetch_course(self): + auth = {} + if not self._cookies: + auth = self._login(username=self._username, password=self._password) + if not auth and self._cookies: + auth = self._login(cookies=self._cookies) + if auth.get("login") == "successful": + logger.info(msg="Logged in successfully.", new_line=True) + logger.info(msg="Fetching all enrolled course(s) url(s)..") + self._courses = self._extract_subscribed_courses() + time.sleep(1) + logger.success(msg="Fetching all enrolled course(s) url(s).. ") + self._logout() + if auth.get("login") == "failed": + logger.error(msg="Failed to login ..\n") + sys.exit(0) + + +class InternUdemyCourse(UdemyCourse, Udemy): + def __init__(self, *args, **kwargs): + self._info = "" + super(InternUdemyCourse, self).__init__(*args, **kwargs) + + def _fetch_course(self): + if self._have_basic: + return + auth = {} + if not self._cookies: + auth = self._login(username=self._username, password=self._password) + if not auth and self._cookies: + auth = self._login(cookies=self._cookies) + if auth.get("login") == "successful": + logger.info(msg="Logged in successfully.", new_line=True) + logger.info(msg="Downloading course information ..") + self._info = self._real_extract(self._url, skip_hls_stream=self._skip_hls_stream) + time.sleep(1) + logger.success(msg="Downloaded course information .. ") + access_token = self._info["access_token"] + self._id = self._info["course_id"] + self._title = self._info["course_title"] + self._chapters_count = self._info["total_chapters"] + self._total_lectures = self._info["total_lectures"] + self._chapters = [ + InternUdemyChapter(z, access_token=access_token) + for z in self._info["chapters"] + ] + logger.info( + msg="Trying to logout now...", new_line=True, + ) + if not self._cookies: + self._logout() + logger.info( + msg="Logged out successfully.", new_line=True, + ) + self._have_basic = True + if auth.get("login") == "failed": + logger.error(msg="Failed to login ..\n") + sys.exit(0) + + +class InternUdemyChapter(UdemyChapters): + def __init__(self, chapter, access_token=None): + super(InternUdemyChapter, self).__init__() + + self._chapter_id = chapter["chapter_id"] + self._chapter_title = chapter["chapter_title"] + self._chapter_index = chapter["chapter_index"] + self._lectures_count = chapter.get("lectures_count", 0) + self._lectures = ( + [ + InternUdemyLecture(z, access_token=access_token) + for z in chapter["lectures"] + ] + if self._lectures_count > 0 + else [] + ) + + +class InternUdemyLecture(UdemyLectures): + def __init__(self, lectures, access_token=None): + super(InternUdemyLecture, self).__init__() + self._access_token = access_token + self._info = lectures + + self._lecture_id = self._info["lectures_id"] + self._lecture_title = self._info["lecture_title"] + self._lecture_index = self._info["lecture_index"] + + self._subtitles_count = self._info.get("subtitle_count", 0) + self._sources_count = self._info.get("sources_count", 0) + self._assets_count = self._info.get("assets_count", 0) + self._extension = self._info.get("extension") + self._html_content = self._info.get("html_content") + self._duration = self._info.get("duration") + if self._duration: + duration = int(self._duration) + (mins, secs) = divmod(duration, 60) + (hours, mins) = divmod(mins, 60) + if hours == 0: + self._duration = "%02d:%02d" % (mins, secs) + else: + self._duration = "%02d:%02d:%02d" % (hours, mins, secs) + + def _process_streams(self): + streams = ( + [InternUdemyLectureStream(z, self) for z in self._info["sources"]] + if self._sources_count > 0 + else [] + ) + self._streams = sorted(streams, key=lambda k: k.quality) + self._streams = sorted(self._streams, key=lambda k: k.mediatype) + + def _process_assets(self): + assets = ( + [InternUdemyLectureAssets(z, self) for z in self._info["assets"]] + if self._assets_count > 0 + else [] + ) + self._assets = assets + + def _process_subtitles(self): + subtitles = ( + [InternUdemyLectureSubtitles(z, self) for z in self._info["subtitles"]] + if self._subtitles_count > 0 + else [] + ) + self._subtitles = subtitles + + +class InternUdemyLectureStream(UdemyLectureStream): + def __init__(self, sources, parent): + super(InternUdemyLectureStream, self).__init__(parent) + + self._mediatype = sources.get("type") + self._extension = sources.get("extension") + self._token = parent._access_token + height = sources.get("height", "0") + width = sources.get("width", "0") + self._resolution = "%sx%s" % (width, height) + self._dimension = width, height + self._quality = int(height) + self._is_hls = "hls" in self._mediatype + self._url = sources.get("download_url") + + +class InternUdemyLectureAssets(UdemyLectureAssets): + def __init__(self, assets, parent): + super(InternUdemyLectureAssets, self).__init__(parent) + + self._mediatype = assets.get("type") + self._extension = assets.get("extension") + self._filename = "{0:03d} {1!s}".format( + parent._lecture_index, assets.get("filename") + ) + self._url = assets.get("download_url") + + +class InternUdemyLectureSubtitles(UdemyLectureSubtitles): + def __init__(self, subtitles, parent): + super(InternUdemyLectureSubtitles, self).__init__(parent) + + self._mediatype = subtitles.get("type") + self._extension = subtitles.get("extension") + self._language = subtitles.get("language") + self._url = subtitles.get("download_url") diff --git a/udemy/logger.py b/udemy/logger.py new file mode 100644 index 0000000..e4d6215 --- /dev/null +++ b/udemy/logger.py @@ -0,0 +1,297 @@ +# pylint: disable=R,C,W +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, Fore.REDee of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modiFore.YELLOW, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFore.REDINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING Fore.REDOM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + +import sys +import logging +from colorama import init, Fore, Style +from udemy.progress import ProgressBar + +init(autoreset=True) +log = logging.getLogger("udemy-dl") # pylint: disable=C + + +def set_color(string, level=None): + """ + set the string color + """ + color_levels = { + 10: "%s%s{}" % (Style.BRIGHT, Fore.YELLOW), + 15: "%s%s{}" % (Style.BRIGHT, Fore.WHITE), + 20: "%s%s{}" % (Style.DIM, Fore.GREEN), + 30: "%s%s{}" % (Style.BRIGHT, Fore.CYAN), + 40: "%s%s{}" % (Style.BRIGHT, Fore.RED), + 50: "%s%s{}" % (Style.DIM, Fore.BLUE), + 55: "%s%s{}" % (Style.BRIGHT, Fore.BLUE), + 60: "%s%s{}" % (Style.DIM, Fore.WHITE), + 70: "%s%s{}" % (Style.BRIGHT, Fore.GREEN), + 80: "%s%s{}" % (Style.BRIGHT, Fore.MAGENTA), + 90: "%s%s{}" % (Style.DIM, Fore.MAGENTA), + } + if level is None: + return color_levels[70].format(string) + else: + return color_levels[int(level)].format(string) + + +class Logging(ProgressBar): + """ + Custom logging class for udemy + """ + + def set_log_filepath(self, log_filepath): + file_handler = logging.FileHandler(log_filepath) + logging.basicConfig( + format="[%(asctime)s][%(name)s] %(levelname)-5.5s %(message)s", + level=logging.INFO, + handlers=[file_handler], + ) + + def info( + self, + msg, + status="", + new_line=False, + before=False, + indent=None, + cc=None, + cc_msg=None, + post_msg=None, + cc_pmsg=None, + ): + """This function prints already downloaded msg""" + _type = set_color(string="i", level=cc if cc else 80) + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + _type + + Fore.CYAN + + Style.DIM + + "] : " + ) + if indent: + prefix = set_color( + string=f"\033[2K\033[1G\r\r{indent}", level=cc if cc else 30 + ) + if status: + # log.info(f"{msg} ({status})") + msg = ( + set_color(f"{msg} (", level=cc_msg if cc_msg else 70) + + set_color(status, level=80) + + set_color(")\r\n", level=70) + ) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + else: + if not new_line: + # log.info(f"{msg}") + msg = set_color(f"{msg}\r", level=cc_msg if cc_msg else 70) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + if new_line: + if post_msg and cc_pmsg: + # log.info(f"{msg}{post_msg}") + msg = set_color(f"{msg}", level=cc_msg if cc_msg else 70) + post_msg = set_color(string=f"{post_msg}\r\n", level=cc_pmsg) + msg += post_msg + else: + # log.info(f"{msg}") + msg = set_color(f"{msg}\r\n", level=cc_msg if cc_msg else 70) + string = prefix + msg + if before: + string = "\r\n" + string + sys.stdout.write(string) + sys.stdout.flush() + + def progress(self, msg): + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.MAGENTA + + Style.BRIGHT + + "i" + + Fore.CYAN + + Style.DIM + + "] : " + ) + msg = set_color(f"{msg}", level=70) + string = prefix + msg + self._spinner(string) + + def success(self, msg, course=False): + """This function prints already downloaded msg""" + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.MAGENTA + + Style.BRIGHT + + "+" + + Fore.CYAN + + Style.DIM + + "] : " + ) + if course: + msg = set_color("Course ", level=70) + set_color(f"'{msg}'\r\n", level=55) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + if not course: + msg = ( + set_color(f"{msg} (", level=70) + + set_color("done", level=30) + + set_color(")\r\n", level=70) + ) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + + def failed(self, msg): + """This function prints already downloaded msg""" + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.RED + + Style.BRIGHT + + "-" + + Fore.CYAN + + Style.DIM + + "] : " + ) + log.error(msg) + msg = ( + set_color(f"{msg} (", level=70) + + set_color("failed", level=40) + + set_color(")\r\n", level=70) + ) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + + def warning(self, msg): + """This function prints already downloaded msg""" + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.MAGENTA + + Style.BRIGHT + + "*" + + Fore.CYAN + + Style.DIM + + "] : " + ) + log.warning(msg) + msg = set_color(f"{msg}\n", level=70) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + + def error(self, msg, new_line=False): + """This function prints already downloaded msg""" + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.RED + + Style.BRIGHT + + "-" + + Fore.CYAN + + Style.DIM + + "] : " + ) + log.error(msg) + if not new_line: + msg = set_color(f"{msg}\n", level=40) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + if new_line: + msg = set_color(f"{msg}\n", level=40) + string = "\n" + prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + + def already_downloaded(self, msg): + """This function prints already downloaded msg""" + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.MAGENTA + + Style.BRIGHT + + "+" + + Fore.CYAN + + Style.DIM + + "] : " + ) + msg = ( + set_color(f"{msg} (", level=70) + + set_color("already downloaded", level=10) + + set_color(")\r\n", level=70) + ) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + + def download_skipped(self, msg, reason=""): + """This function prints already downloaded msg""" + prefix = ( + "\033[2K\033[1G\r\r" + + Fore.CYAN + + Style.DIM + + "[" + + Fore.MAGENTA + + Style.BRIGHT + + "+" + + Fore.CYAN + + Style.DIM + + "] : " + ) + log.warning(msg) + msg = ( + set_color(f"{msg} (", level=70) + + set_color("download skipped", level=30) + + set_color(")\r\n", level=70) + ) + string = prefix + msg + sys.stdout.write(string) + sys.stdout.flush() + if reason: + self.error(msg=reason, new_line=True) + + +logger = Logging() diff --git a/udemy/progress.py b/udemy/progress.py new file mode 100644 index 0000000..c624563 --- /dev/null +++ b/udemy/progress.py @@ -0,0 +1,170 @@ +# pylint: disable=R,C,W +#!/usr/bin/env python3 + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + +import itertools +from udemy.compat import sys, time +from udemy.logger import Fore, Style + + +class ProgressBar(object): + """ + Custom progress bar for udemy + """ + + SPINNER = itertools.cycle(["-", "|", "/", "\\"]) + + def _spinner(self, text): + spin = self.SPINNER.__next__() + sys.stdout.write(text + spin) + sys.stdout.flush() + time.sleep(0.02) + + # thanks to https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console + def _progress( + self, + iteration, + total, + prefix="", + file_size="", + downloaded="", + rate="", + suffix="", + bar_length=30, + ): + filledLength = int(round(bar_length * iteration / float(total))) + percents = format(100.00 * (iteration / float(total)), ".2f") + bar = ( + Fore.CYAN + + Style.DIM + + "#" * filledLength + + Fore.WHITE + + Style.DIM + + "-" * (bar_length - filledLength) + ) + if "0.00" not in rate: + sys.stdout.write( + "\033[2K\033[1G\r\r{}{}[{}{}*{}{}] : {}{}{}/{} {}% |{}{}{}| {} {}".format( + Fore.CYAN, + Style.DIM, + Fore.MAGENTA, + Style.BRIGHT, + Fore.CYAN, + Style.DIM, + Fore.GREEN, + Style.BRIGHT, + file_size, + downloaded, + percents, + bar, + Fore.GREEN, + Style.BRIGHT, + rate, + suffix, + ) + ) + sys.stdout.flush() + + def hls_progress( + self, downloaded, percents, filled_length, rate, suffix, bar_length=30 + ): + bar = ( + Fore.CYAN + + Style.DIM + + "#" * filled_length + + Fore.WHITE + + Style.DIM + + "-" * (bar_length - filled_length) + ) + sys.stdout.write( + "\033[2K\033[1G\r\r{}{}[{}{}*{}{}] : {}{}{} {}% |{}{}{}| {} {}".format( + Fore.CYAN, + Style.DIM, + Fore.MAGENTA, + Style.BRIGHT, + Fore.CYAN, + Style.DIM, + Fore.GREEN, + Style.BRIGHT, + downloaded, + percents, + bar, + Fore.GREEN, + Style.BRIGHT, + rate, + suffix, + ) + ) + sys.stdout.flush() + + def show_progress(self, total, recvd, ratio, rate, eta): + if total <= 1048576: + _total_size = round(float(total) / 1024.00, 2) + _receiving = round(float(recvd) / 1024.00, 2) + _size = format( + _total_size if _total_size < 1024.00 else _total_size / 1024.00, ".2f" + ) + _received = format( + _receiving if _receiving < 1024.00 else _receiving / 1024.00, ".2f" + ) + suffix_size = "KB" if _total_size < 1024.00 else "MB" + suffix_recvd = "KB" if _receiving < 1024.00 else "MB" + else: + _total_size = round(float(total) / 1048576, 2) + _receiving = round(float(recvd) / 1048576, 2) + _size = format( + _total_size if _total_size < 1024.00 else _total_size / 1024.00, ".2f" + ) + _received = format( + _receiving if _receiving < 1024.00 else _receiving / 1024.00, ".2f" + ) + suffix_size = "MB" if _total_size < 1024.00 else "GB" + suffix_recvd = "MB" if _receiving < 1024.00 else "GB" + + _rate = round(float(rate), 2) + rate = format(_rate if _rate < 1024.00 else _rate / 1024.00, ".2f") + suffix_rate = "kB/s" if _rate < 1024.00 else "MB/s" + (mins, secs) = divmod(eta, 60) + (hours, mins) = divmod(mins, 60) + if hours > 99: + eta = "--:--:--" + if hours == 0: + eta = "eta %02d:%02ds" % (mins, secs) + else: + eta = "eta %02d:%02d:%02ds" % (hours, mins, secs) + if secs == 0: + eta = "\n" + + self._progress( + _receiving, + _total_size, + file_size=str(_size) + str(suffix_size), + downloaded=str(_received) + str(suffix_recvd), + rate=str(rate) + str(suffix_rate), + suffix=str(eta), + bar_length=30, + ) + + +progress = ProgressBar() diff --git a/udemy/_sanitize.py b/udemy/sanitize.py similarity index 54% rename from udemy/_sanitize.py rename to udemy/sanitize.py index fddb38e..487edfd 100644 --- a/udemy/_sanitize.py +++ b/udemy/sanitize.py @@ -1,16 +1,16 @@ -#!/usr/bin/python +# pylint: disable=R,C,W,E +#!/usr/bin/env python3 # -*- coding: utf-8 from __future__ import unicode_literals import re -import os import six import unicodedata from unidecode import unidecode -def smart_text(s, encoding='utf-8', errors='strict'): +def smart_text(s, encoding="utf-8", errors="strict"): if isinstance(s, six.text_type): return s @@ -20,7 +20,7 @@ def smart_text(s, encoding='utf-8', errors='strict'): s = six.text_type(s, encoding, errors) else: s = six.text_type(s) - elif hasattr(s, '__unicode__'): + elif hasattr(s, "__unicode__"): s = six.text_type(s) else: s = six.text_type(bytes(s), encoding, errors) @@ -30,10 +30,12 @@ def smart_text(s, encoding='utf-8', errors='strict'): # Extra characters outside of alphanumerics that we'll allow. -SLUG_OK = '-_~' +SLUG_OK = "-_~" -def slugify(s, ok=SLUG_OK, lower=True, spaces=False, only_ascii=False, space_replacement='-'): +def slugify( + s, ok=SLUG_OK, lower=True, spaces=False, only_ascii=False, space_replacement="-" +): """ Creates a unicode slug for given string with several options. @@ -60,75 +62,81 @@ def slugify(s, ok=SLUG_OK, lower=True, spaces=False, only_ascii=False, space_rep """ - if only_ascii and ok != SLUG_OK and hasattr(ok, 'decode'): + if only_ascii and ok != SLUG_OK and hasattr(ok, "decode"): try: - ok.decode('ascii') + ok.decode("ascii") except UnicodeEncodeError: - raise ValueError(('You can not use "only_ascii=True" with ' - 'a non ascii available chars in "ok" ("%s" given)') % ok) + raise ValueError( + ( + 'You can not use "only_ascii=True" with ' + 'a non ascii available chars in "ok" ("%s" given)' + ) + % ok + ) rv = [] - for c in unicodedata.normalize('NFKC', smart_text(s)): + for c in unicodedata.normalize("NFKC", smart_text(s)): cat = unicodedata.category(c)[0] - if cat in 'LN' or c in ok: + if cat in "LN" or c in ok: rv.append(c) - elif cat == 'Z': # space - rv.append(' ') - new = ''.join(rv).strip() + elif cat == "Z": # space + rv.append(" ") + new = "".join(rv).strip() if only_ascii: new = unidecode(new) if not spaces: if space_replacement and space_replacement not in ok: - space_replacement = ok[0] if ok else '' - new = re.sub('[%s\s]+' % space_replacement, space_replacement, new) + space_replacement = ok[0] if ok else "" + new = re.sub("[%s\s]+" % space_replacement, space_replacement, new) if lower: new = new.lower() return new + def sanitize(title): _locale = { - '194' : 'A', - '199' : 'C', - '286' : 'G', - '304' : 'I', - '206' : 'I', - '214' : 'O', - '350' : 'S', - '219' : 'U', - '226' : 'a', - '231' : 'c', - '287' : 'g', - '305' : 'i', - '238' : 'i', - '246' : 'o', - '351' : 's', - '251' : 'u', - '191' : '', - '225' : 'a', - '233' : 'e', - '237' : 'i', - '243' : 'o', - '250' : 'u', - '252' : 'u', - '168u' : 'u', - '241' : 'n', - '193' : 'A', - '201' : 'E', - '205' : 'I', - '211' : 'O', - '218' : 'U', - '220' : 'U', - '168U' : 'U', - '209' : 'N', - '223' : 'ss', + "194": "A", + "199": "C", + "286": "G", + "304": "I", + "206": "I", + "214": "O", + "350": "S", + "219": "U", + "226": "a", + "231": "c", + "287": "g", + "305": "i", + "238": "i", + "246": "o", + "351": "s", + "251": "u", + "191": "", + "225": "a", + "233": "e", + "237": "i", + "243": "o", + "250": "u", + "252": "u", + "168u": "u", + "241": "n", + "193": "A", + "201": "E", + "205": "I", + "211": "O", + "218": "U", + "220": "U", + "168U": "U", + "209": "N", + "223": "ss", } - _temp = ''.join([str(ord(i)) if ord(i) > 128 else i for i in title]) - for _ascii,_char in _locale.items(): + _temp = "".join([str(ord(i)) if ord(i) > 128 else i for i in title]) + for _ascii, _char in _locale.items(): if _ascii in _temp: _temp = _temp.replace(_ascii, _char) ok = re.compile(r'[^\\/:*?"<>]') - _title = ''.join(x if ok.match(x) else "_" for x in _temp) + _title = "".join(x if ok.match(x) else "_" for x in _temp) return _title diff --git a/udemy/_session.py b/udemy/session.py similarity index 53% rename from udemy/_session.py rename to udemy/session.py index b0f660c..68a542e 100644 --- a/udemy/_session.py +++ b/udemy/session.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +# pylint: disable=R,C,W +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -8,7 +9,7 @@ License : MIT -Copyright (c) 2020 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -23,52 +24,36 @@ """ -from ._compat import ( - sys, - time, - requests, - HEADERS, - LOGOUT_URL, - ) -from ._colorized import * +from udemy.compat import ( + requests, + HEADERS, +) class Session(object): - def __init__(self): self._headers = HEADERS self._session = requests.sessions.Session() - def _set_auth_headers(self, access_token='', client_id=''): - self._headers['Authorization'] = "Bearer {}".format(access_token) - self._headers['X-Udemy-Authorization'] = "Bearer {}".format(access_token) + def _set_auth_headers(self, access_token="", client_id=""): + self._headers["Authorization"] = "Bearer {}".format(access_token) + self._headers["X-Udemy-Authorization"] = "Bearer {}".format(access_token) def _get(self, url): session = self._session.get(url, headers=self._headers) if session.ok or session.status_code in [502, 503]: return session if not session.ok: - if session.status_code == 403: - msg = {'detail': 'You should use cookie base method to authenticate or try again in few minutes'} - else: - msg = {'detail': ''} - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Udemy Says : %s %s %s ...\n" % (session.status_code, session.reason, msg.get('detail', ''))) - time.sleep(0.8) - sys.exit(0) + raise Exception(f"{session.status_code} {session.reason}") def _post(self, url, data, redirect=True): - session = self._session.post(url, data, headers=self._headers, allow_redirects=redirect) + session = self._session.post( + url, data, headers=self._headers, allow_redirects=redirect + ) if session.ok: return session if not session.ok: - if session.status_code == 403: - msg = {'detail': 'You should use cookie base method to authenticate or try again in few minutes'} - else: - msg = {'detail': ''} - sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Udemy Says : %s %s %s ...\n" % (session.status_code, session.reason, msg.get('detail', ''))) - sys.stdout.flush() - time.sleep(0.8) - sys.exit(0) + raise Exception(f"{session.status_code} {session.reason}") def terminate(self): self._set_auth_headers() diff --git a/udemy/shared.py b/udemy/shared.py new file mode 100644 index 0000000..3035c30 --- /dev/null +++ b/udemy/shared.py @@ -0,0 +1,746 @@ +# pylint: disable=R,C +#!/usr/bin/env python3 + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +from udemy.compat import ( + re, + os, + sys, + time, + requests, + conn_error, + HEADERS, +) +from udemy.ffmpeg import FFMPeg +from udemy.utils import to_file, prepare_html + + +class Downloader(object): + def __init__(self): + self._url = None + self._filename = None + self._mediatype = None + self._extension = None + self._active = True + self._is_hls = False + self._token = None + self._sess = requests.session() + + @property + def url(self): + """abac""" + return self._url + + @property + def token(self): + return self._token + + @property + def is_hls(self): + return self._is_hls + + @property + def mediatype(self): + return self._mediatype + + @property + def extension(self): + return self._extension + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() # pylint: disable=E + return self._filename + + def _generate_filename(): # pylint: disable=E + pass + + def _write_external_links(self, filepath): + retVal = {} + savedirs, name = os.path.split(filepath) + filename = u"external-assets-links.txt" + filename = os.path.join(savedirs, filename) + file_data = [] + if os.path.isfile(filename): + file_data = [ + i.strip().lower() + for i in open(filename, encoding="utf-8", errors="ignore") + if i + ] + + content = u"\n{}\n{}\n".format(name, self.url) + if name.lower() not in file_data: + retVal = to_file(filename, "a", content) + return retVal + + def download( + self, filepath="", quiet=False, callback=lambda *x: None, + ): + savedir = filename = "" + retVal = {} + + if filepath and os.path.isdir(filepath): + savedir, filename = ( + filepath, + self.filename, + ) + + elif filepath: + savedir, filename = os.path.split(filepath) + + else: + filename = self.filename + + filepath = os.path.join(savedir, filename) + if os.name == "nt" and len(filepath) > 250: + filepath = "\\\\?\\{}".format(filepath) + + if self.mediatype == "external_link": + return self._write_external_links(filepath) + + if filepath and filepath.endswith(".vtt"): + filepath_vtt2srt = filepath.replace(".vtt", ".srt") + if os.path.isfile(filepath_vtt2srt): + retVal = {"status": "True", "msg": "already downloaded"} + return retVal + + if os.path.isfile(filepath): + retVal = {"status": "True", "msg": "already downloaded"} + return retVal + + temp_filepath = filepath + ".part" + + if self.is_hls: + temp_filepath = filepath.replace(".mp4", "") + temp_filepath = temp_filepath + ".hls-part.mp4" + retVal = FFMPeg(None, self.url, self.token, temp_filepath).download() + if retVal: + self._active = False + else: + bytes_to_be_downloaded = 0 + fmode, offset = "wb", 0 + chunksize, bytesdone, t0 = 16384, 0, time.time() + headers = {"User-Agent": HEADERS.get("User-Agent")} + if os.path.exists(temp_filepath): + offset = os.stat(temp_filepath).st_size + + if offset: + offset_range = "bytes={}-".format(offset) + headers["Range"] = offset_range + bytesdone = offset + fmode = "ab" + + status_string = ( + " {:,} Bytes [{:.2%}] received. Rate: [{:4.0f} " + "KB/s]. ETA: [{:.0f} secs]" + ) + + try: + try: + response = self._sess.get( + self.url, headers=headers, stream=True, timeout=10 + ) + except conn_error as error: + return { + "status": "False", + "msg": "ConnectionError: %s" % (str(error)), + } + if response.ok: + bytes_to_be_downloaded = total = int( + response.headers.get("Content-Length") + ) + if bytesdone > 0: + bytes_to_be_downloaded = bytes_to_be_downloaded + bytesdone + total = bytes_to_be_downloaded + with open(temp_filepath, fmode) as media_file: + is_malformed = False + for chunk in response.iter_content(chunksize): + if not chunk: + break + media_file.write(chunk) + elapsed = time.time() - t0 + bytesdone += len(chunk) + if elapsed: + try: + rate = ( + (float(bytesdone) - float(offset)) / 1024.0 + ) / elapsed + eta = (total - bytesdone) / (rate * 1024.0) + except ZeroDivisionError: + is_malformed = True + try: + os.unlink(temp_filepath) + except Exception: # pylint: disable=W + pass + retVal = { + "status": "False", + "msg": "ZeroDivisionError : it seems, lecture has malfunction or is zero byte(s) ..", + } + break + else: + rate = 0 + eta = 0 + + if not is_malformed: + progress_stats = ( + bytesdone, + bytesdone * 1.0 / total, + rate, + eta, + ) + + if not quiet: + status = status_string.format(*progress_stats) + sys.stdout.write("\r" + status + " " * 4 + "\r") + sys.stdout.flush() + + if callback: + callback(total, *progress_stats) + if not response.ok: + code = response.status_code + reason = response.reason + retVal = { + "status": "False", + "msg": "Udemy returned HTTP Code %s: %s" % (code, reason), + } + response.close() + except KeyboardInterrupt as error: + raise error + except Exception as error: # pylint: disable=W + retVal = {"status": "False", "msg": "Reason : {}".format(str(error))} + return retVal + # # check if file is downloaded completely + if os.path.isfile(temp_filepath): + total_bytes_done = os.stat(temp_filepath).st_size + if total_bytes_done == bytes_to_be_downloaded: + self._active = False + # if total_bytes_done < bytes_to_be_downloaded: + # # set active to be True as remaining bytes to be downloaded + # self._active = True + # # try downloading back again remaining bytes until we download completely + # self.download(filepath=filepath, quiet=quiet) + + if not self._active: + os.rename(temp_filepath, filepath) + retVal = {"status": "True", "msg": "download"} + + return retVal + + +class UdemyCourses(object): + def __init__(self, username="", password="", cookies="", basic=True): + + self._courses = [] + self._username = username + self._password = password + self._cookies = cookies + + if basic: + self._fetch_course() + + def _fetch_course(self): + raise NotImplementedError + + def dump_courses(self, filepath): + if not filepath: + filepath = os.path.join(os.getcwd(), "enrolled-courses.txt") + with open(filepath, "w") as fd: + courses_urls = "\n".join(self._courses) + fd.write(courses_urls) + return filepath + + @property + def courses(self): + return self._courses + + +class UdemyCourse(object): + def __init__( + self, url, username="", password="", cookies="", basic=True, skip_hls_stream=False, callback=None + ): + + self._url = url + self._username = username + self._password = password + self._cookies = cookies + self._skip_hls_stream = skip_hls_stream + self._callback = callback or (lambda x: None) + self._have_basic = False + + + self._id = None + self._title = None + self._chapters_count = None + self._total_lectures = None + + self._chapters = [] + + if basic: + self._fetch_course() + + def _fetch_course(self): + raise NotImplementedError + + @property + def id(self): + if not self._id: + self._fetch_course() + return self._id + + @property + def title(self): + if not self._title: + self._fetch_course() + return self._title + + @property + def chapters(self): + if not self._chapters_count: + self._fetch_course() + return self._chapters_count + + @property + def lectures(self): + if not self._total_lectures: + self._fetch_course() + return self._total_lectures + + def get_chapters(self, chapter_number=None, chapter_start=None, chapter_end=None): + if not self._chapters: + self._fetch_course() + if ( + chapter_number + and not chapter_start + and not chapter_end + and isinstance(chapter_number, int) + ): + is_okay = bool(0 < chapter_number <= self.chapters) + if is_okay: + self._chapters = [self._chapters[chapter_number - 1]] + if chapter_start and not chapter_number and isinstance(chapter_start, int): + is_okay = bool(0 < chapter_start <= self.chapters) + if is_okay: + self._chapters = self._chapters[chapter_start - 1 :] + if chapter_end and not chapter_number and isinstance(chapter_end, int): + is_okay = bool(0 < chapter_end <= self.chapters) + if is_okay: + self._chapters = self._chapters[: chapter_end - 1] + return self._chapters + + +class UdemyChapters(object): + def __init__(self): + + self._chapter_id = None + self._chapter_index = None + self._chapter_title = None + self._lectures_count = None + + self._lectures = [] + + def __repr__(self): + chapter = "{title}".format(title=self.title) + return chapter + + @property + def id(self): + return self._chapter_id + + @property + def index(self): + return self._chapter_index + + @property + def title(self): + return self._chapter_title + + @property + def lectures(self): + return self._lectures_count + + def get_lectures(self, lecture_number=None, lecture_start=None, lecture_end=None): + if ( + lecture_number + and not lecture_start + and not lecture_end + and isinstance(lecture_number, int) + ): + is_okay = bool(0 < lecture_number <= self.lectures) + if is_okay: + self._lectures = [self._lectures[lecture_number - 1]] + if lecture_start and not lecture_number and isinstance(lecture_start, int): + is_okay = bool(0 < lecture_start <= self.lectures) + if is_okay: + self._lectures = self._lectures[lecture_start - 1 :] + if lecture_end and not lecture_number and isinstance(lecture_end, int): + is_okay = bool(0 < lecture_end <= self.lectures) + if is_okay: + self._lectures = self._lectures[: lecture_end - 1] + return self._lectures + + +class UdemyLectures(object): + def __init__(self): + + self._best = None + self._duration = None + self._extension = None + self._lecture_id = None + self._lecture_title = None + self._lecture_index = None + self._sources_count = None + self._assets_count = None + self._subtitles_count = None + self._html_content = None + + self._assets = [] + self._streams = [] + self._subtitles = [] + + def __repr__(self): + lecture = "{title}".format(title=self.title) + return lecture + + @property + def id(self): + return self._lecture_id + + @property + def index(self): + return self._lecture_index + + @property + def title(self): + return self._lecture_title + + @property + def html(self): + return self._html_content + + @property + def duration(self): + return self._duration + + @property + def extension(self): + return self._extension + + @property + def assets(self): + if not self._assets: + self._process_assets() # pylint: disable=E + return self._assets + + @property + def streams(self): + if not self._streams: + self._process_streams() # pylint: disable=E + return self._streams + + @property + def subtitles(self): + if not self._subtitles: + self._process_subtitles() # pylint: disable=E + return self._subtitles + + def _getbest(self): + streams = self.streams + if not streams: + return None + + def _sortkey(x, keyres=0, keyftype=0): + keyres = int(x.resolution.split("x")[0]) + keyftype = x.extension + st = (keyftype, keyres) + return st + + self._best = max([i for i in streams if not i.is_hls], key=_sortkey) + return self._best + + def getbest(self): + return self._getbest() + + def dump(self, filepath): + retVal = {} + filename = os.path.join(filepath, self.title) + filename += ".html" + + if os.path.isfile(filename): + retVal = {"status": "True", "msg": "already downloaded"} + return retVal + contents = prepare_html(self.title, self.html) + retVal = to_file(filename, "wb", contents) + return retVal + + +class UdemyLectureStream(Downloader): + def __init__(self, parent): + + self._mediatype = None + self._quality = None + self._resolution = None + self._dimension = None + self._extension = None + self._url = None + + self._parent = parent + self._filename = None + self._fsize = None + self._active = False + self._is_hls = False + self._token = None + + Downloader.__init__(self) + + def __repr__(self): + out = "%s:%s@%s" % (self.mediatype, self.extension, self.quality) + return out + + def _generate_filename(self): + ok = re.compile(r'[^\\/:*?"<>|]') + filename = "".join(x if ok.match(x) else "_" for x in self.title) + filename += "." + self.extension + return filename + + @property + def resolution(self): + return self._resolution + + @property + def quality(self): + return self._quality + + @property + def url(self): + return self._url + + @property + def is_hls(self): + return self._is_hls + + @property + def token(self): + return self._token + + @property + def id(self): + return self._parent.id + + @property + def dimension(self): + return self._dimension + + @property + def extension(self): + return self._extension + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() + return self._filename + + @property + def title(self): + return self._parent.title + + @property + def mediatype(self): + return self._mediatype + + def get_quality(self, quality, preferred_mediatype="video"): + lecture = self._parent.getbest() + _temp = {} + for s in self._parent.streams: + if isinstance(quality, int) and s.quality == quality: + mediatype = s.mediatype + _temp[mediatype] = s + if _temp: + if preferred_mediatype in _temp: + lecture = _temp[preferred_mediatype] + else: + lecture = list(_temp.values()).pop() + return lecture + + def get_filesize(self): + if not self._fsize: + headers = {"User-Agent": HEADERS.get("User-Agent")} + try: + with requests.get(self.url, stream=True, headers=headers) as resp: + if resp.ok: + self._fsize = float(resp.headers.get("Content-Length", 0)) + if not resp.ok: + self._fsize = 0 + except conn_error: + self._fsize = 0 + return self._fsize + + +class UdemyLectureAssets(Downloader): + def __init__(self, parent): + + self._extension = None + self._mediatype = None + self._url = None + + self._parent = parent + self._filename = None + self._fsize = None + self._active = False + + Downloader.__init__(self) + + def __repr__(self): + out = "%s:%s@%s" % (self.mediatype, self.extension, self.extension) + return out + + def _generate_filename(self): + ok = re.compile(r'[^\\/:*?"<>|]') + filename = "".join(x if ok.match(x) else "_" for x in self.title) + filename += ".{}".format(self.extension) + return filename + + @property + def id(self): + return self._parent.id + + @property + def url(self): + return self._url + + @property + def extension(self): + return self._extension + + @property + def title(self): + return self._parent.title + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() + return self._filename + + @property + def mediatype(self): + return self._mediatype + + def get_filesize(self): + if not self._fsize: + headers = {"User-Agent": HEADERS.get("User-Agent")} + try: + with requests.get(self.url, stream=True, headers=headers) as resp: + if resp.ok: + self._fsize = float(resp.headers.get("Content-Length", 0)) + if not resp.ok: + self._fsize = 0 + except conn_error: + self._fsize = 0 + return self._fsize + + +class UdemyLectureSubtitles(Downloader): + def __init__(self, parent): + + self._mediatype = None + self._extension = None + self._language = None + self._url = None + + self._parent = parent + self._filename = None + self._fsize = None + self._active = False + + Downloader.__init__(self) + + def __repr__(self): + out = "%s:%s@%s" % (self.mediatype, self.language, self.extension) + return out + + def _generate_filename(self): + ok = re.compile(r'[^\\/:*?"<>|]') + filename = "".join(x if ok.match(x) else "_" for x in self.title) + filename += ".{}.{}".format(self.language, self.extension) + return filename + + @property + def id(self): + return self._parent.id + + @property + def url(self): + return self._url + + @property + def extension(self): + return self._extension + + @property + def language(self): + return self._language + + @property + def title(self): + return self._parent.title + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() + return self._filename + + @property + def mediatype(self): + return self._mediatype + + def get_subtitle(self, language, preferred_language="en"): + _temp = {} + subtitles = self._parent.subtitles + for sub in subtitles: + if sub.language == language: + _temp[sub.language] = [sub] + if _temp: + # few checks to keep things simple :D + if language in _temp: + _temp = _temp[language] + elif preferred_language in _temp and not language in _temp: + _temp = _temp[preferred_language] + if not _temp: + _temp = subtitles + return _temp + + def get_filesize(self): + if not self._fsize: + headers = {"User-Agent": HEADERS.get("User-Agent")} + try: + with requests.get(self.url, stream=True, headers=headers) as resp: + if resp.ok: + self._fsize = float(resp.headers.get("Content-Length", 0)) + if not resp.ok: + self._fsize = 0 + except conn_error: + self._fsize = 0 + return self._fsize diff --git a/udemy/_udemy.py b/udemy/udemy.py similarity index 62% rename from udemy/_udemy.py rename to udemy/udemy.py index 49dc5dc..fbbb478 100644 --- a/udemy/_udemy.py +++ b/udemy/udemy.py @@ -1,14 +1,15 @@ -#!/usr/bin/python +# pylint: disable=R,C +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" +''' -Author : Nasir Khan (r0ot h3x49) -Github : https://github.com/r0oth3x49 +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 License : MIT -Copyright (c) 2020 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -21,12 +22,13 @@ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" +''' -from ._internal import InternUdemyCourse as Udemy +from udemy.internal import InternUdemyCourse as Udemy +from udemy.internal import InternUdemyCourses as UdemyCourses -def course(url, username='', password='', cookies='', basic=True, callback=None): +def course(url, username='', password='', cookies='', basic=True, skip_hls_stream=False, callback=None): """Returns udemy course instance. @params: @@ -35,4 +37,15 @@ def course(url, username='', password='', cookies='', basic=True, callback=None) password : Udemy account password required : type (string) cookies : Udemy account logged in browser cookies optional : type (string) """ - return Udemy(url, username, password, cookies, basic, callback) \ No newline at end of file + return Udemy(url, username, password, cookies, basic, skip_hls_stream, callback) + +def fetch_enrolled_courses(username='', password='', cookies='', basic=True): + """Returns udemy course instance. + + @params: + url : Udemy course url required : type (string). + username : Udemy email account required : type (string). + password : Udemy account password required : type (string) + cookies : Udemy account logged in browser cookies optional : type (string) + """ + return UdemyCourses(username, password, cookies, basic) \ No newline at end of file diff --git a/udemy/utils.py b/udemy/utils.py new file mode 100644 index 0000000..0d5e57c --- /dev/null +++ b/udemy/utils.py @@ -0,0 +1,334 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# pylint: disable=R,C,E,W + +""" + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + +from udemy.compat import ( + re, + os, + sys, + json, + NO_DEFAULT, + compat_HTMLParser, +) +from udemy.logger import logger + + +def extract_cookie_string(raw_cookies): + cookies = {} + try: + access_token = re.search( + r"(?i)(?:access_token=(?P\w+))", raw_cookies + ) + except Exception as error: + logger.error( + msg=f"Cookies error, {error}, unable to extract access_token from cookies." + ) + sys.exit(0) + if not access_token: + logger.error(msg="Unable to find access_token, proper cookies required") + logger.info( + msg="follow: https://github.com/r0oth3x49/udemy-dl#how-to-login-with-cookie", + new_line=True, + ) + sys.stdout.flush() + sys.exit(0) + access_token = access_token.group("access_token") + cookies.update({"access_token": access_token}) + return cookies + +def extract_url_or_courses(url_or_filepath): + courses = [] + if os.path.isfile(url_or_filepath): + courses = [i.strip() for i in open(url_or_filepath)] + if not os.path.isfile(url_or_filepath): + courses = [url_or_filepath] + return courses + +def to_human_readable(content_length): + hr = "" + if content_length <= 1048576.00: + size = round(float(content_length) / 1024.00, 2) + sz = format(size if size < 1024.00 else size / 1024.00, ".2f",) + in_MB = "KB" if size < 1024.00 else "MB" + else: + size = round(float(content_length) / 1048576, 2) + sz = format(size if size < 1024.00 else size / 1024.00, ".2f",) + in_MB = "MB " if size < 1024.00 else "GB " + hr = f"{sz}{in_MB}" + return hr + + +def to_filepath(base, name): + filepath = os.path.join(base, name) + try: + os.makedirs(filepath) + except Exception as e: + pass + return filepath + + +def prepare_html(title, html): + data = """ + + + + %s + + +
+
+
+

%s

+
+
+
+ + + + """ % ( + title, + html, + ) + return data.encode("utf-8") + + +def to_file( + filename, fmode, content +): # filepath="", lecture="", fmode="a", names_only=False): + retVal = {} + + try: + with open(filename, fmode, encoding="utf-8", errors="ignore") as fd: + fd.write(content) + retVal = {"status": "True", "msg": "download"} + except (OSError, Exception, UnicodeDecodeError) as e: + retVal = {"status": "False", "msg": "{}".format(e)} + + return retVal + + +def to_configs( + username="", password="", cookies="", quality="", output="", language="" +): + configs = load_configs() + fname = ".udemy-dl.conf" + fmode = "w" + if configs: + cfu = configs.get("username") + cfp = configs.get("password") + cfc = configs.get("cookies") + cfq = configs.get("quality") + cfl = configs.get("language") + cfo = configs.get("output") + if username and cfu != username: + configs.update({"username": username}) + if password and cfp != password: + configs.update({"password": password}) + if cookies and cfc != cookies: + configs.update({"cookies": cookies}) + if quality and cfq != quality: + configs.update({"quality": quality}) + if language and cfl != language: + configs.update({"language": language}) + if output and cfo != output: + configs.update({"output": output}) + with open(fname, fmode) as fd: + json.dump(configs, fd, indent=4) + if not configs: + creds = { + "username": username, + "password": password, + "quality": quality, + "output": output, + "language": language, + "cookies": cookies, + } + with open(fname, fmode) as fd: + json.dump(creds, fd, indent=4) + return "cached" + + +def load_configs(): + fname = ".udemy-dl.conf" + configs = {} + if os.path.isfile(fname): + with open(fname) as fd: + configs = json.load(fd) + return configs + + +# Thanks to a great open source utility youtube-dl .. +class HTMLAttributeParser(compat_HTMLParser): # pylint: disable=W + """Trivial HTML parser to gather the attributes for a single element""" + + def __init__(self): + self.attrs = {} + compat_HTMLParser.__init__(self) + + def handle_starttag(self, tag, attrs): + self.attrs = dict(attrs) + + +def unescapeHTML(s): + clean = compat_HTMLParser() + data = clean.unescape(s) + return data + + +def extract_attributes(html_element): + """Given a string for an HTML element such as + + Decode and return a dictionary of attributes. + { + 'a': 'foo', 'b': 'bar', c: 'baz', d: 'boz', + 'empty': '', 'noval': None, 'entity': '&', + 'sq': '"', 'dq': '\'' + }. + NB HTMLParser is stricter in Python 2.6 & 3.2 than in later versions, + but the cases in the unit test will work for all of 2.6, 2.7, 3.2-3.5. + """ + parser = HTMLAttributeParser() + try: + parser.feed(html_element) + parser.close() + except Exception: # pylint: disable=W + pass + return parser.attrs + + +def hidden_inputs(html): + html = re.sub(r"", "", html) + hidden_inputs = {} # pylint: disable=W + for entry in re.findall(r"(?i)(]+>)", html): + attrs = extract_attributes(entry) + if not entry: + continue + if attrs.get("type") not in ("hidden", "submit"): + continue + name = attrs.get("name") or attrs.get("id") + value = attrs.get("value") + if name and value is not None: + hidden_inputs[name] = value + return hidden_inputs + + +def search_regex( + pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None +): + """ + Perform a regex search on the given string, using a single or a list of + patterns returning the first matching group. + In case of failure return a default value or raise a WARNING or a + RegexNotFoundError, depending on fatal, specifying the field name. + """ + if isinstance(pattern, str): + mobj = re.search(pattern, string, flags) + else: + for p in pattern: + mobj = re.search(p, string, flags) + if mobj: + break + + _name = name + + if mobj: + if group is None: + # return the first matching group + return next(g for g in mobj.groups() if g is not None) + else: + return mobj.group(group) + elif default is not NO_DEFAULT: + return default + elif fatal: + print("[-] Unable to extract %s" % _name) + exit(0) + else: + print("[-] unable to extract %s" % _name) + exit(0) + + +def parse_json(json_string, video_id, transform_source=None, fatal=True): + if transform_source: + json_string = transform_source(json_string) + try: + return json.loads(json_string) + except ValueError as ve: + errmsg = "[-] %s: Failed to parse JSON " % video_id + if fatal: + print(errmsg, ve) + else: + print(errmsg + str(ve)) + + +def js_to_json(code): + COMMENT_RE = r"/\*(?:(?!\*/).)*?\*/|//[^\n]*" + SKIP_RE = r"\s*(?:{comment})?\s*".format(comment=COMMENT_RE) + INTEGER_TABLE = ( + (r"(?s)^(0[xX][0-9a-fA-F]+){skip}:?$".format(skip=SKIP_RE), 16), + (r"(?s)^(0+[0-7]+){skip}:?$".format(skip=SKIP_RE), 8), + ) + + def fix_kv(m): + v = m.group(0) + if v in ("true", "false", "null"): + return v + elif v.startswith("/*") or v.startswith("//") or v == ",": + return "" + + if v[0] in ("'", '"'): + v = re.sub( + r'(?s)\\.|"', + lambda m: {'"': '\\"', "\\'": "'", "\\\n": "", "\\x": "\\u00",}.get( + m.group(0), m.group(0) + ), + v[1:-1], + ) + + for regex, base in INTEGER_TABLE: + im = re.match(regex, v) + if im: + i = int(im.group(1), base) + return '"%d":' % i if v.endswith(":") else "%d" % i + + return '"%s"' % v + + return re.sub( + r"""(?sx) + "(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"| + '(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'| + {comment}|,(?={skip}[\]}}])| + [a-zA-Z_][.a-zA-Z_0-9]*| + \b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:{skip}:)?| + [0-9]+(?={skip}:) + """.format( + comment=COMMENT_RE, skip=SKIP_RE + ), + fix_kv, + code, + ) diff --git a/udemy/_vtt2srt.py b/udemy/vtt2srt.py similarity index 56% rename from udemy/_vtt2srt.py rename to udemy/vtt2srt.py index b3cfe35..b327761 100644 --- a/udemy/_vtt2srt.py +++ b/udemy/vtt2srt.py @@ -1,4 +1,5 @@ -#!/usr/bin/env python +# pylint: disable=R,C +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -8,7 +9,7 @@ License : MIT -Copyright (c) 2020 Nasir Khan (r0ot h3x49) +Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, @@ -22,42 +23,39 @@ THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from ._utils import unescapeHTML -from ._compat import ( - os, - re, - sys, - pyver, - codecs, - ) +from udemy.utils import unescapeHTML +from udemy.compat import os, re, codecs class WebVtt2Srt(object): - _TIMECODE_REGEX = r'(?i)(?P(?:(?:\d{1,2}:)){1,2}\d{2}[\.,]\d+)' - _TIMECODE = r'(?i)(?P(?:(?:\d{1,2}:)){1,2}\d{2}[\.,]\d+)\s*-->\s*(?i)(?P(?:(?:\d{1,2}:)){1,2}\d{2}[\.,]\d+)' + _TIMECODE_REGEX = r"(?i)(?P(?:(?:\d{1,2}:)){1,2}\d{2}[\.,]\d+)" + _TIMECODE = r"(?i)(?P(?:(?:\d{1,2}:)){1,2}\d{2}[\.,]\d+)\s*-->\s*(?i)(?P(?:(?:\d{1,2}:)){1,2}\d{2}[\.,]\d+)" def _vttcontents(self, fname): try: - f = codecs.open(filename=fname, encoding='utf-8', errors='ignore') - except Exception as e: - return {'status' : 'False', 'msg' : 'failed to open file : file not found ..'} + f = codecs.open(filename=fname, encoding="utf-8", errors="ignore") + except Exception as error: # pylint: disable=W + return { + "status": "False", + "msg": f"failed to open file : error: {error} ..", + } content = [line for line in (l.strip() for l in f)] f.close() return content - def _write_srtcontent(self, fname, content): - with codecs.open(filename=fname, mode='a', encoding='utf-8') as fd: + with codecs.open(filename=fname, mode="a", encoding="utf-8") as fd: fd.write(content) fd.close() def _locate_timecode(self, content): + loc = "" for (loc, line) in enumerate(content): match = re.match(self._TIMECODE_REGEX, line, flags=re.U) if match: - return {'status' : True, 'location' : loc} - return {'status' : False, 'location' : loc} + return {"status": True, "location": loc} + return {"status": False, "location": loc} def _is_timecode(self, timecode): match = re.match(self._TIMECODE_REGEX, timecode, flags=re.U) @@ -66,46 +64,58 @@ def _is_timecode(self, timecode): return False def _fix_timecode(self, timecode): - _sdata = len(timecode.split(',')[0]) + _sdata = len(timecode.split(",")[0]) if _sdata == 5: - timecode = u'00:{code}'.format(code=timecode) + timecode = "00:{code}".format(code=timecode) if _sdata == 7: - timecode = u'0{code}'.format(code=timecode) + timecode = "0{code}".format(code=timecode) return timecode def _generate_timecode(self, sequence, timecode): match = re.match(self._TIMECODE, timecode, flags=re.U) if match: - start, end = self._fix_timecode(timecode=re.sub(r'[\.,]', ',', match.group('appeartime'))), self._fix_timecode(timecode=re.sub(r'[\.,]', ',', match.group('disappertime'))) - return u'{seq}\r\n{appeartime} --> {disappertime}\r\n'.format(seq=sequence, appeartime=start, disappertime=end) - return u'' - - def convert(self, filename=None, remove_vtt=True): + start, end = ( + self._fix_timecode( + timecode=re.sub(r"[\.,]", ",", match.group("appeartime")) + ), + self._fix_timecode( + timecode=re.sub(r"[\.,]", ",", match.group("disappertime")) + ), + ) + return "{seq}\r\n{appeartime} --> {disappertime}\r\n".format( + seq=sequence, appeartime=start, disappertime=end + ) + return "" + + def convert(self, filename=None, keep_vtt=False): if filename: seq = 1 - fname = filename.replace('.vtt', '.srt') + fname = filename.replace(".vtt", ".srt") content = self._vttcontents(fname=filename) if content and isinstance(content, list): timecode_loc = self._locate_timecode(content) - if not timecode_loc.get('status'): - return {'status' : 'False', 'msg' : 'subtitle file seems to have malfunction skipping conversion ..'} - for line in content[timecode_loc.get('location'):]: + if not timecode_loc.get("status"): + return { + "status": "False", + "msg": "subtitle file seems to have malfunction skipping conversion ..", + } + for line in content[timecode_loc.get("location") :]: flag = self._is_timecode(timecode=line) if flag: timecode = self._generate_timecode(seq, unescapeHTML(line)) self._write_srtcontent(fname, timecode) seq += 1 if not flag: - match = re.match('^([0-9]{1,3})$', line, flags=re.U) + match = re.match("^([0-9]{1,3})$", line, flags=re.U) if not match: - data = u'{content}\r\n'.format(content=line) + data = "{content}\r\n".format(content=line) self._write_srtcontent(fname, data) else: return content - - if remove_vtt: + + if not keep_vtt: try: os.unlink(filename) - except Exception as e: + except Exception: # pylint: disable=W pass - return {'status' : 'True', 'msg' : 'successfully generated subtitle in srt ...'} \ No newline at end of file + return {"status": "True", "msg": "successfully generated subtitle in srt ..."}