mirror of
https://cdm-project.com/Download-Tools/udemy-downloader.git
synced 2025-04-29 19:34:25 +02:00
updates
- added back the ability to use cookies.txt file (netscape format) - updated error message for course subscription when failed to find classes - remove requirement for shaka-packager in favor of ffmpeg (v5+)
This commit is contained in:
parent
4b80e32433
commit
d123403434
111
main.py
111
main.py
@ -9,8 +9,9 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from http.cookiejar import MozillaCookieJar
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
from typing import IO, Union
|
||||
|
||||
import browser_cookie3
|
||||
import demoji
|
||||
@ -191,7 +192,7 @@ def pre_run():
|
||||
"--browser",
|
||||
dest="browser",
|
||||
help="The browser to extract cookies from",
|
||||
choices=["chrome", "firefox", "opera", "edge", "brave", "chromium", "vivaldi", "safari"],
|
||||
choices=["chrome", "firefox", "opera", "edge", "brave", "chromium", "vivaldi", "safari", "file"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-h265",
|
||||
@ -374,6 +375,10 @@ class Udemy:
|
||||
cj = browser_cookie3.chromium()
|
||||
elif browser == "vivaldi":
|
||||
cj = browser_cookie3.vivaldi()
|
||||
elif browser == "file":
|
||||
# load netscape cookies from file
|
||||
cj = MozillaCookieJar("cookies.txt")
|
||||
cj.load()
|
||||
|
||||
def _get_quiz(self, quiz_id):
|
||||
self.session._headers.update(
|
||||
@ -977,7 +982,9 @@ class Udemy:
|
||||
soup = BeautifulSoup(course_html, "lxml")
|
||||
data = soup.find("div", {"class": "ud-component--course-taking--app"})
|
||||
if not data:
|
||||
logger.fatal("Unable to extract arguments from course page! Make sure you have a cookies.txt file!")
|
||||
logger.fatal(
|
||||
"Could not find course data. Possible causes are: Missing cookies.txt file, incorrect url (should end with /learn), not logged in to udemy in specified browser."
|
||||
)
|
||||
self.session.terminate()
|
||||
sys.exit(1)
|
||||
data_args = data.attrs["data-module-args"]
|
||||
@ -1218,28 +1225,31 @@ def durationtoseconds(period):
|
||||
return None
|
||||
|
||||
|
||||
def mux_process(video_title, video_filepath, audio_filepath, output_path):
|
||||
"""
|
||||
@author Jayapraveen
|
||||
"""
|
||||
def mux_process(
|
||||
video_filepath: str,
|
||||
audio_filepath: str,
|
||||
video_title: str,
|
||||
output_path: str,
|
||||
audio_key: Union[str | None] = None,
|
||||
video_key: Union[str | None] = None,
|
||||
):
|
||||
codec = "hevc_nvenc" if use_nvenc else "libx265"
|
||||
transcode = "-hwaccel cuda -hwaccel_output_format cuda" if use_nvenc else ""
|
||||
audio_decryption_arg = f"-decryption_key {audio_key}" if audio_key is not None else ""
|
||||
video_decryption_arg = f"-decryption_key {video_key}" if video_key is not None else ""
|
||||
|
||||
if os.name == "nt":
|
||||
if use_h265:
|
||||
command = 'ffmpeg {} -y -i "{}" -i "{}" -c:v {} -vtag hvc1 -crf {} -preset {} -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format(
|
||||
transcode, video_filepath, audio_filepath, codec, h265_crf, h265_preset, video_title, output_path
|
||||
)
|
||||
command = f'ffmpeg {transcode} -y {video_decryption_arg} -i "{video_filepath}" {audio_decryption_arg} -i "{audio_filepath}" -c:v {codec} -vtag hvc1 -crf {h265_crf} -preset {h265_preset} -c:a copy -fflags +bitexact -shortest -map_metadata -1 -metadata title="{video_title}" "{output_path}"'
|
||||
else:
|
||||
command = 'ffmpeg -y -i "{}" -i "{}" -c:v copy -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format(
|
||||
command = f'ffmpeg -y {video_decryption_arg} -i "{video_filepath}" {audio_decryption_arg} -i "{audio_filepath}" -c copy -fflags +bitexact -shortest -map_metadata -1 -metadata title="{video_title}" "{output_path}"'.format(
|
||||
video_filepath, audio_filepath, video_title, output_path
|
||||
)
|
||||
else:
|
||||
if use_h265:
|
||||
command = 'nice -n 7 ffmpeg {} -y -i "{}" -i "{}" -c:v libx265 -vtag hvc1 -crf {} -preset {} -c:a copy -fflags +bitexact -map_metadata -1 -metadata title="{}" "{}"'.format(
|
||||
transcode, video_filepath, audio_filepath, codec, h265_crf, h265_preset, video_title, output_path
|
||||
)
|
||||
command = f'nice -n 7 ffmpeg {transcode} -y {video_decryption_arg} -i "{video_filepath}" {audio_decryption_arg} -i "{audio_filepath}" -c:v {codec} -vtag hvc1 -crf {h265_crf} -preset {h265_preset} -c:a copy -fflags +bitexact -shortest -map_metadata -1 -metadata title="{video_title}" "{output_path}"'
|
||||
else:
|
||||
command = 'nice -n 7 ffmpeg -y -i "{}" -i "{}" -c:v copy -c:a copy -fflags +bitexact -shortest -map_metadata -1 -metadata title="{}" "{}"'.format(
|
||||
command = f'nice -n 7 ffmpeg -y {video_decryption_arg} -i "{video_filepath}" {audio_decryption_arg} -i "{audio_filepath}" -c copy -fflags +bitexact -shortest -map_metadata -1 -metadata title="{video_title}" "{output_path}"'.format(
|
||||
video_filepath, audio_filepath, video_title, output_path
|
||||
)
|
||||
|
||||
@ -1253,34 +1263,11 @@ def mux_process(video_title, video_filepath, audio_filepath, output_path):
|
||||
return ret_code
|
||||
|
||||
|
||||
def decrypt(kid, in_filepath, out_filepath):
|
||||
try:
|
||||
key = keys[kid.lower()]
|
||||
except KeyError:
|
||||
raise KeyError("Key not found")
|
||||
|
||||
if os.name == "nt":
|
||||
command = f'shaka-packager --enable_raw_key_decryption --keys key_id={kid}:key={key} input="{in_filepath}",stream_selector="0",output="{out_filepath}"'
|
||||
else:
|
||||
command = f'nice -n 7 shaka-packager --enable_raw_key_decryption --keys key_id={kid}:key={key} input="{in_filepath}",stream_selector="0",output="{out_filepath}"'
|
||||
|
||||
process = subprocess.Popen(command, shell=True)
|
||||
log_subprocess_output("SHAKA-STDOUT", process.stdout)
|
||||
log_subprocess_output("SHAKA-STDERR", process.stderr)
|
||||
ret_code = process.wait()
|
||||
if ret_code != 0:
|
||||
raise Exception("Decryption returned a non-zero exit code")
|
||||
|
||||
return ret_code
|
||||
|
||||
|
||||
def handle_segments(url, format_id, lecture_id, video_title, output_path, chapter_dir):
|
||||
os.chdir(os.path.join(chapter_dir))
|
||||
|
||||
video_filepath_enc = lecture_id + ".encrypted.mp4"
|
||||
audio_filepath_enc = lecture_id + ".encrypted.m4a"
|
||||
video_filepath_dec = lecture_id + ".decrypted.mp4"
|
||||
audio_filepath_dec = lecture_id + ".decrypted.m4a"
|
||||
temp_output_path = os.path.join(chapter_dir, lecture_id + ".mp4")
|
||||
|
||||
logger.info("> Downloading Lecture Tracks...")
|
||||
@ -1314,6 +1301,9 @@ def handle_segments(url, format_id, lecture_id, video_title, output_path, chapte
|
||||
logger.warning("Return code from the downloader was non-0 (error), skipping!")
|
||||
return
|
||||
|
||||
audio_kid = None
|
||||
video_kid = None
|
||||
|
||||
try:
|
||||
video_kid = extract_kid(video_filepath_enc)
|
||||
logger.info("KID for video file is: " + video_kid)
|
||||
@ -1328,21 +1318,42 @@ def handle_segments(url, format_id, lecture_id, video_title, output_path, chapte
|
||||
logger.exception(f"Error extracting audio kid")
|
||||
return
|
||||
|
||||
audio_key = None
|
||||
video_key = None
|
||||
|
||||
if audio_kid is not None:
|
||||
try:
|
||||
logger.info("> Decrypting video, this might take a minute...")
|
||||
ret_code = decrypt(video_kid, video_filepath_enc, video_filepath_dec)
|
||||
if ret_code != 0:
|
||||
logger.error("> Return code from the decrypter was non-0 (error), skipping!")
|
||||
audio_key = keys[audio_kid]
|
||||
except KeyError:
|
||||
logger.error(
|
||||
f"Audio key not found for {audio_kid}, if you have the key then you probably didn't add them to the key file correctly."
|
||||
)
|
||||
return
|
||||
logger.info("> Decryption complete")
|
||||
logger.info("> Decrypting audio, this might take a minute...")
|
||||
decrypt(audio_kid, audio_filepath_enc, audio_filepath_dec)
|
||||
if ret_code != 0:
|
||||
logger.error("> Return code from the decrypter was non-0 (error), skipping!")
|
||||
|
||||
if video_kid is not None:
|
||||
try:
|
||||
video_key = keys[video_kid]
|
||||
except KeyError:
|
||||
logger.error(
|
||||
f"Video key not found for {audio_kid}, if you have the key then you probably didn't add them to the key file correctly."
|
||||
)
|
||||
return
|
||||
logger.info("> Decryption complete")
|
||||
|
||||
try:
|
||||
# logger.info("> Decrypting video, this might take a minute...")
|
||||
# ret_code = decrypt(video_kid, video_filepath_enc, video_filepath_dec)
|
||||
# if ret_code != 0:
|
||||
# logger.error("> Return code from the decrypter was non-0 (error), skipping!")
|
||||
# return
|
||||
# logger.info("> Decryption complete")
|
||||
# logger.info("> Decrypting audio, this might take a minute...")
|
||||
# decrypt(audio_kid, audio_filepath_enc, audio_filepath_dec)
|
||||
# if ret_code != 0:
|
||||
# logger.error("> Return code from the decrypter was non-0 (error), skipping!")
|
||||
# return
|
||||
# logger.info("> Decryption complete")
|
||||
logger.info("> Merging video and audio, this might take a minute...")
|
||||
mux_process(video_title, video_filepath_dec, audio_filepath_dec, temp_output_path)
|
||||
mux_process(video_filepath_enc, audio_filepath_enc, video_title, temp_output_path, audio_key, video_key)
|
||||
if ret_code != 0:
|
||||
logger.error("> Return code from ffmpeg was non-0 (error), skipping!")
|
||||
return
|
||||
@ -1351,8 +1362,6 @@ def handle_segments(url, format_id, lecture_id, video_title, output_path, chapte
|
||||
logger.info("> Cleaning up temporary files...")
|
||||
os.remove(video_filepath_enc)
|
||||
os.remove(audio_filepath_enc)
|
||||
os.remove(video_filepath_dec)
|
||||
os.remove(audio_filepath_dec)
|
||||
except Exception:
|
||||
logger.exception(f"Error: ")
|
||||
finally:
|
||||
|
4
utils.py
4
utils.py
@ -24,14 +24,14 @@ def extract_kid(mp4_file):
|
||||
if not os.path.exists(mp4_file):
|
||||
raise Exception("File does not exist")
|
||||
for box in boxes:
|
||||
if box.header.box_type == 'moov':
|
||||
if box.header.box_type == "moov":
|
||||
pssh_box = next(x for x in box.pssh if x.system_id == "edef8ba979d64acea3c827dcd51d21ed")
|
||||
hex = codecs.decode(pssh_box.payload, "hex")
|
||||
|
||||
pssh = widevine_pssh_data_pb2.WidevinePsshData()
|
||||
pssh.ParseFromString(hex)
|
||||
content_id = base64.b16encode(pssh.content_id)
|
||||
return content_id.decode("utf-8")
|
||||
return content_id.decode("utf-8").lower()
|
||||
|
||||
# No Moof or PSSH header found
|
||||
return None
|
Loading…
x
Reference in New Issue
Block a user