mirror of
https://cdm-project.com/Download-Tools/udemy-downloader.git
synced 2025-04-30 00:54: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
115
main.py
115
main.py
@ -9,8 +9,9 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from http.cookiejar import MozillaCookieJar
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from typing import IO, Union
|
||||||
|
|
||||||
import browser_cookie3
|
import browser_cookie3
|
||||||
import demoji
|
import demoji
|
||||||
@ -191,7 +192,7 @@ def pre_run():
|
|||||||
"--browser",
|
"--browser",
|
||||||
dest="browser",
|
dest="browser",
|
||||||
help="The browser to extract cookies from",
|
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(
|
parser.add_argument(
|
||||||
"--use-h265",
|
"--use-h265",
|
||||||
@ -374,6 +375,10 @@ class Udemy:
|
|||||||
cj = browser_cookie3.chromium()
|
cj = browser_cookie3.chromium()
|
||||||
elif browser == "vivaldi":
|
elif browser == "vivaldi":
|
||||||
cj = browser_cookie3.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):
|
def _get_quiz(self, quiz_id):
|
||||||
self.session._headers.update(
|
self.session._headers.update(
|
||||||
@ -977,7 +982,9 @@ class Udemy:
|
|||||||
soup = BeautifulSoup(course_html, "lxml")
|
soup = BeautifulSoup(course_html, "lxml")
|
||||||
data = soup.find("div", {"class": "ud-component--course-taking--app"})
|
data = soup.find("div", {"class": "ud-component--course-taking--app"})
|
||||||
if not data:
|
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()
|
self.session.terminate()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
data_args = data.attrs["data-module-args"]
|
data_args = data.attrs["data-module-args"]
|
||||||
@ -1218,28 +1225,31 @@ def durationtoseconds(period):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def mux_process(video_title, video_filepath, audio_filepath, output_path):
|
def mux_process(
|
||||||
"""
|
video_filepath: str,
|
||||||
@author Jayapraveen
|
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"
|
codec = "hevc_nvenc" if use_nvenc else "libx265"
|
||||||
transcode = "-hwaccel cuda -hwaccel_output_format cuda" if use_nvenc else ""
|
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 os.name == "nt":
|
||||||
if use_h265:
|
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(
|
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}"'
|
||||||
transcode, video_filepath, audio_filepath, codec, h265_crf, h265_preset, video_title, output_path
|
|
||||||
)
|
|
||||||
else:
|
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
|
video_filepath, audio_filepath, video_title, output_path
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if use_h265:
|
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(
|
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}"'
|
||||||
transcode, video_filepath, audio_filepath, codec, h265_crf, h265_preset, video_title, output_path
|
|
||||||
)
|
|
||||||
else:
|
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
|
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
|
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):
|
def handle_segments(url, format_id, lecture_id, video_title, output_path, chapter_dir):
|
||||||
os.chdir(os.path.join(chapter_dir))
|
os.chdir(os.path.join(chapter_dir))
|
||||||
|
|
||||||
video_filepath_enc = lecture_id + ".encrypted.mp4"
|
video_filepath_enc = lecture_id + ".encrypted.mp4"
|
||||||
audio_filepath_enc = lecture_id + ".encrypted.m4a"
|
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")
|
temp_output_path = os.path.join(chapter_dir, lecture_id + ".mp4")
|
||||||
|
|
||||||
logger.info("> Downloading Lecture Tracks...")
|
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!")
|
logger.warning("Return code from the downloader was non-0 (error), skipping!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
audio_kid = None
|
||||||
|
video_kid = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
video_kid = extract_kid(video_filepath_enc)
|
video_kid = extract_kid(video_filepath_enc)
|
||||||
logger.info("KID for video file is: " + video_kid)
|
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")
|
logger.exception(f"Error extracting audio kid")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
audio_key = None
|
||||||
|
video_key = None
|
||||||
|
|
||||||
|
if audio_kid is not None:
|
||||||
try:
|
try:
|
||||||
logger.info("> Decrypting video, this might take a minute...")
|
audio_key = keys[audio_kid]
|
||||||
ret_code = decrypt(video_kid, video_filepath_enc, video_filepath_dec)
|
except KeyError:
|
||||||
if ret_code != 0:
|
logger.error(
|
||||||
logger.error("> Return code from the decrypter was non-0 (error), skipping!")
|
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
|
return
|
||||||
logger.info("> Decryption complete")
|
|
||||||
logger.info("> Decrypting audio, this might take a minute...")
|
if video_kid is not None:
|
||||||
decrypt(audio_kid, audio_filepath_enc, audio_filepath_dec)
|
try:
|
||||||
if ret_code != 0:
|
video_key = keys[video_kid]
|
||||||
logger.error("> Return code from the decrypter was non-0 (error), skipping!")
|
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
|
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...")
|
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:
|
if ret_code != 0:
|
||||||
logger.error("> Return code from ffmpeg was non-0 (error), skipping!")
|
logger.error("> Return code from ffmpeg was non-0 (error), skipping!")
|
||||||
return
|
return
|
||||||
@ -1351,8 +1362,6 @@ def handle_segments(url, format_id, lecture_id, video_title, output_path, chapte
|
|||||||
logger.info("> Cleaning up temporary files...")
|
logger.info("> Cleaning up temporary files...")
|
||||||
os.remove(video_filepath_enc)
|
os.remove(video_filepath_enc)
|
||||||
os.remove(audio_filepath_enc)
|
os.remove(audio_filepath_enc)
|
||||||
os.remove(video_filepath_dec)
|
|
||||||
os.remove(audio_filepath_dec)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(f"Error: ")
|
logger.exception(f"Error: ")
|
||||||
finally:
|
finally:
|
||||||
@ -1602,8 +1611,8 @@ def process_normal_quiz(quiz, lecture, chapter_dir):
|
|||||||
with open("quiz_template.html", "r") as f:
|
with open("quiz_template.html", "r") as f:
|
||||||
html = f.read()
|
html = f.read()
|
||||||
quiz_data = {
|
quiz_data = {
|
||||||
"quiz_id":lecture["data"].get("id"),
|
"quiz_id": lecture["data"].get("id"),
|
||||||
"quiz_description":lecture["data"].get("description"),
|
"quiz_description": lecture["data"].get("description"),
|
||||||
"quiz_title": lecture["data"].get("title"),
|
"quiz_title": lecture["data"].get("title"),
|
||||||
"pass_percent": lecture.get("data").get("pass_percent"),
|
"pass_percent": lecture.get("data").get("pass_percent"),
|
||||||
"questions": quiz["contents"],
|
"questions": quiz["contents"],
|
||||||
|
4
utils.py
4
utils.py
@ -24,14 +24,14 @@ def extract_kid(mp4_file):
|
|||||||
if not os.path.exists(mp4_file):
|
if not os.path.exists(mp4_file):
|
||||||
raise Exception("File does not exist")
|
raise Exception("File does not exist")
|
||||||
for box in boxes:
|
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")
|
pssh_box = next(x for x in box.pssh if x.system_id == "edef8ba979d64acea3c827dcd51d21ed")
|
||||||
hex = codecs.decode(pssh_box.payload, "hex")
|
hex = codecs.decode(pssh_box.payload, "hex")
|
||||||
|
|
||||||
pssh = widevine_pssh_data_pb2.WidevinePsshData()
|
pssh = widevine_pssh_data_pb2.WidevinePsshData()
|
||||||
pssh.ParseFromString(hex)
|
pssh.ParseFromString(hex)
|
||||||
content_id = base64.b16encode(pssh.content_id)
|
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
|
# No Moof or PSSH header found
|
||||||
return None
|
return None
|
Loading…
x
Reference in New Issue
Block a user