diff --git a/README.md b/README.md index 4fc320e..30bc721 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,10 @@ - **This tool will not work without decryption keys. Do not bother installing unless you already have keys or can obtain them!** - **Downloading courses is against Udemy's Terms of Service, I am NOT held responsible for your account getting suspended as a result from the use of this program!** - This program is WIP, the code is provided as-is and I am not held resposible for any legal issues resulting from the use of this program. -- You can find information on downgrading the CDM version on the wiki, __please note that CDM version 2209 is already revoked as of 12/5/2021 and no longer works on Udemy as of 2/1/2022. This information is just to have it available.__ - - Chrome: https://github.com/Puyodead1/udemy-downloader/wiki/Downgrade-CDM-to-2209-on-Chrome-(Windows) - - FireFox: https://github.com/Puyodead1/udemy-downloader/wiki/Downgrade-CDM-to-2209-on-FireFox-(Windows) # Description -Utility script to download Udemy courses, has support for DRM videos but requires the user to aquire the decryption key (for legal reasons).
+Utility script to download Udemy courses, has support for DRM videos but requires the user to acquire the decryption key (for legal reasons).
Windows is the primary development OS, but I've made an effort to support Linux also (Mac untested). # Requirements @@ -50,7 +47,7 @@ You will need to get a few things before you can use this program: - rename `.env.sample` to `.env` _(you only need to do this if you plan to use the .env file to store your bearer token)_ - rename `keyfile.example.json` to `keyfile.json` -## Aquire Bearer Token +## Acquire Bearer Token - Firefox: [Udemy-DL Guide](https://github.com/r0oth3x49/udemy-dl/issues/389#issuecomment-491903900) - Chrome: [Udemy-DL Guide](https://github.com/r0oth3x49/udemy-dl/issues/389#issuecomment-492569372) @@ -58,7 +55,7 @@ You will need to get a few things before you can use this program: ## Key ID and Key -It is up to you to aquire the key and key ID. Please **DO NOT** ask me for help acquiring these, decrypting DRM protected content can be considered piracy. The tool required for this has already been discused in a GitHub issue. +It is up to you to acquire the key and key ID. Please **DO NOT** ask me for help acquiring these, decrypting DRM protected content can be considered piracy. The tool required for this has already been discused in a GitHub issue. - Enter the key and key id in the `keyfile.json` - ![keyfile example](https://i.imgur.com/e5aU0ng.png) @@ -95,20 +92,19 @@ Note the link is `/course` not `/program-taking`. It is also important that the # Advanced Usage ``` -usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [-cd CONCURRENT_DOWNLOADS] [--disable-ipv6] [--skip-lectures] [--download-assets] [--download-captions] - [--keep-vtt] [--skip-hls] [--info] [--id-as-course-name] [--save-to-file] [--load-from-file] [--log-level LOG_LEVEL] [-v] +usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [-cd CONCURRENT_DOWNLOADS] [--disable-ipv6] [--skip-lectures] [--download-assets] [--download-captions] [--keep-vtt] [--skip-hls] + [--info] [--id-as-course-name] [-sc] [--save-to-file] [--load-from-file] [--log-level LOG_LEVEL] [--use-h265] [--h265-crf H265_CRF] [--h265-preset H265_PRESET] [-v] Udemy Downloader -optional arguments: +options: -h, --help show this help message and exit -c COURSE_URL, --course-url COURSE_URL The URL of the course to download -b BEARER_TOKEN, --bearer BEARER_TOKEN The Bearer token to use -q QUALITY, --quality QUALITY - Download specific video quality. If the requested quality isn't available, the closest quality will be used. If not specified, the best quality will be downloaded - for each lecture + Download specific video quality. If the requested quality isn't available, the closest quality will be used. If not specified, the best quality will be downloaded for each lecture -l LANG, --lang LANG The language to download for captions, specify 'all' to download all captions (Default is 'en') -cd CONCURRENT_DOWNLOADS, --concurrent-downloads CONCURRENT_DOWNLOADS The number of maximum concurrent downloads for segments (HLS and DASH, must be a number 1-30) @@ -120,12 +116,18 @@ optional arguments: --skip-hls If specified, hls streams will be skipped (faster fetching) (hls streams usually contain 1080p quality for non-drm lectures) --info If specified, only course information will be printed, nothing will be downloaded --id-as-course-name If specified, the course id will be used in place of the course name for the output directory. This is a 'hack' to reduce the path length - --save-to-file If specified, course content will be saved to a file that can be loaded later with --load-from-file, this can reduce processing time (Note that asset links expire - after a certain amount of time) - --load-from-file If specified, course content will be loaded from a previously saved file with --save-to-file, this can reduce processing time (Note that asset links expire after a - certain amount of time) + -sc, --subscription-course + Mark the course as a subscription based course, use this if you are having problems with the program auto detecting it + --save-to-file If specified, course content will be saved to a file that can be loaded later with --load-from-file, this can reduce processing time (Note that asset links expire after a certain + amount of time) + --load-from-file If specified, course content will be loaded from a previously saved file with --save-to-file, this can reduce processing time (Note that asset links expire after a certain amount of + time) --log-level LOG_LEVEL Logging level: one of DEBUG, INFO, ERROR, WARNING, CRITICAL (Default is INFO) + --use-h265 If specified, videos will be encoded with the H.265 codec + --h265-crf H265_CRF Set a custom CRF value for H.265 encoding. FFMPEG default is 28 + --h265-preset H265_PRESET + Set a custom preset value for H.265 encoding. FFMPEG default is medium -v, --version show program's version number and exit ``` @@ -170,6 +172,12 @@ optional arguments: - `python main.py -c --log-level CRITICAL` - Use course ID as the course name: - `python main.py -c --id-as-course-name` +- Encode in H.265: + - `python main.py -c --use-h265` +- Encode in H.265 with custom CRF: + - `python main.py -c --use-h265 -h265-crf 20` +- Encode in H.265 with custom preset: + - `python main.py -c --use-h265 --h265-preset faster` If you encounter errors while downloading such as diff --git a/main.py b/main.py index 015f08d..3c820c6 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,6 @@ from html.parser import HTMLParser as compat_HTMLParser from pathlib import Path from typing import IO -import cloudscraper import m3u8 import requests import yt_dlp @@ -52,6 +51,9 @@ info = None keys = {} id_as_course_name = False is_subscription_course = False +use_h265 = False +h265_crf = 28 +h265_preset = "medium" # from https://stackoverflow.com/a/21978778/9785713 @@ -64,7 +66,7 @@ def log_subprocess_output(prefix: str, pipe: IO[bytes]): # this is the first function that is called, we parse the arguments, setup the logger, and ensure that required directories exist def pre_run(): - global cookies, dl_assets, skip_lectures, dl_captions, caption_locale, quality, bearer_token, portal_name, course_name, keep_vtt, skip_hls, concurrent_downloads, disable_ipv6, load_from_file, save_to_file, bearer_token, course_url, info, logger, keys, id_as_course_name, is_subscription_course, LOG_LEVEL + global cookies, dl_assets, skip_lectures, dl_captions, caption_locale, quality, bearer_token, portal_name, course_name, keep_vtt, skip_hls, concurrent_downloads, disable_ipv6, load_from_file, save_to_file, bearer_token, course_url, info, logger, keys, id_as_course_name, is_subscription_course, LOG_LEVEL, use_h265, h265_crf, h265_preset # make sure the directory exists if not os.path.exists(DOWNLOAD_DIR): @@ -153,12 +155,12 @@ def pre_run(): help="If specified, the course id will be used in place of the course name for the output directory. This is a 'hack' to reduce the path length", ) parser.add_argument( + "-sc", "--subscription-course", dest="is_subscription_course", action="store_true", help="Mark the course as a subscription based course, use this if you are having problems with the program auto detecting it", ) - parser.add_argument( "--save-to-file", dest="save_to_file", @@ -177,6 +179,26 @@ def pre_run(): type=str, help="Logging level: one of DEBUG, INFO, ERROR, WARNING, CRITICAL (Default is INFO)", ) + parser.add_argument( + "--use-h265", + dest="use_h265", + action="store_true", + help="If specified, videos will be encoded with the H.265 codec", + ) + parser.add_argument( + "--h265-crf", + dest="h265_crf", + type=int, + default=28, + help="Set a custom CRF value for H.265 encoding. FFMPEG default is 28", + ) + parser.add_argument( + "--h265-preset", + dest="h265_preset", + type=str, + default="medium", + help="Set a custom preset value for H.265 encoding. FFMPEG default is medium", + ) parser.add_argument("-v", "--version", action="version", version="You are running version {version}".format(version=__version__)) args = parser.parse_args() @@ -215,6 +237,12 @@ def pre_run(): course_url = args.course_url if args.info: info = args.info + if args.use_h265: + use_h265 = True + if args.h265_crf: + h265_crf = args.h265_crf + if args.h265_preset: + h265_preset = args.h265_preset if args.log_level: if args.log_level.upper() == "DEBUG": LOG_LEVEL = logging.DEBUG @@ -919,26 +947,6 @@ class UdemyAuth(object): self.password = password self._cache = cache_session self._session = Session() - self._cloudsc = cloudscraper.create_scraper() - - def _form_hidden_input(self, form_id): - try: - resp = self._cloudsc.get(LOGIN_URL) - 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 authenticate(self, bearer_token=""): if bearer_token: @@ -990,13 +998,23 @@ def mux_process(video_title, video_filepath, audio_filepath, output_path): @author Jayapraveen """ if os.name == "nt": - command = 'ffmpeg -nostdin -loglevel error -y -i "{}" -i "{}" -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format( - video_filepath, audio_filepath, video_title, output_path - ) + if use_h265: + command = 'ffmpeg -y -i "{}" -i "{}" -c:v libx265 -crf {} -preset {} -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format( + video_filepath, audio_filepath, h265_crf, h265_preset, video_title, output_path + ) + else: + command = 'ffmpeg -y -i "{}" -i "{}" -c:v copy -vtag hvc1 -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format( + video_filepath, audio_filepath, video_title, output_path + ) else: - command = 'nice -n 7 ffmpeg -nostdin -loglevel error -y -i "{}" -i "{}" -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format( - video_filepath, audio_filepath, video_title, output_path - ) + if use_h265: + command = 'nide -n 7 ffmpeg -y -i "{}" -i "{}" -c:v libx265 -crf {} -preset {} -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format( + video_filepath, audio_filepath, h265_crf, h265_preset, video_title, output_path + ) + else: + command = 'nide -n 7 ffmpeg -y -i "{}" -i "{}" -c:v copy -vtag hvc1 -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format( + video_filepath, audio_filepath, video_title, output_path + ) process = subprocess.Popen(command, shell=True) log_subprocess_output("FFMPEG-STDOUT", process.stdout) @@ -1320,17 +1338,28 @@ def process_lecture(lecture, lecture_path, lecture_file_name, chapter_dir): source_type = source.get("type") if source_type == "hls": temp_filepath = lecture_path.replace(".mp4", ".%(ext)s") - args = ["yt-dlp", "--force-generic-extractor", "--concurrent-fragments", f"{concurrent_downloads}", "--downloader", "aria2c", "-o", f"{temp_filepath}", f"{url}"] + cmd = ["yt-dlp", "--force-generic-extractor", "--concurrent-fragments", f"{concurrent_downloads}", "--downloader", "aria2c", "-o", f"{temp_filepath}", f"{url}"] if disable_ipv6: - args.append("--downloader-args") - args.append('aria2c:"--disable-ipv6"') - process = subprocess.Popen(args) + cmd.append("--downloader-args") + cmd.append('aria2c:"--disable-ipv6"') + process = subprocess.Popen(cmd) log_subprocess_output("YTDLP-STDOUT", process.stdout) log_subprocess_output("YTDLP-STDERR", process.stderr) ret_code = process.wait() if ret_code == 0: # os.rename(temp_filepath, lecture_path) logger.info(" > HLS Download success") + cmd = ["ffmpeg", "-y", "-i", lecture_path, "-c:v", "libx265", "-c:a", "copy", lecture_path + ".mp4"] + process = subprocess.Popen(cmd) + log_subprocess_output("FFMPEG-STDOUT", process.stdout) + log_subprocess_output("FFMPEG-STDERR", process.stderr) + ret_code = process.wait() + if ret_code == 0: + os.remove(lecture_path) + os.remove(lecture_path + ".mp4", lecture_path) + logger.info(" > Encoding complete") + else: + logger.error(" > Encoding returned non-zero return code") else: ret_code = download_aria(url, chapter_dir, lecture_title + ".mp4") logger.debug(f" > Download return code: {ret_code}") diff --git a/requirements.txt b/requirements.txt index 87685fc..4ed8200 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ m3u8 colorama yt-dlp bitstring -cloudscraper unidecode beautifulsoup4 lxml