- 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:
Puyodead1 2024-07-18 18:46:43 -04:00
parent 4b80e32433
commit d123403434
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
2 changed files with 67 additions and 58 deletions

115
main.py
View File

@ -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"],

View File

@ -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