diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3d42315 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File with Arguments", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": [ + "-ds", + "-c", + "${input:cookie}" // Ask for cookie here + ] + } + ], + "inputs": [ + { + "id": "cookie", + "type": "promptString", + "description": "Enter the cookie", + "default": "cookie" + } + ] +} \ No newline at end of file diff --git a/main.py b/main.py index f9dec6d..4104d67 100644 --- a/main.py +++ b/main.py @@ -13,33 +13,27 @@ if sys.platform == 'win32': os.system('chcp 65001') -parser = argparse.ArgumentParser(add_help=False) +parser = argparse.ArgumentParser() -parser.add_argument("-h", "--help", action="store_true", help="Show this help message and exit") parser.add_argument("-c", "--session-cookie", help="Session Cookie", required=False) parser.add_argument("-y", "--ykt-host", help="RainClassroom Host", required=False, default="pro.yuketang.cn") -parser.add_argument("-i", "--idm", action="store_true", help="Use IDMan.exe") -parser.add_argument("-ni", "--no-idm", action="store_true", help="Don't use IDMan.exe, implied when the system is not Windows") -parser.add_argument("-a", "--all", action="store_true", help="Download all content without asking") -parser.add_argument("-na", "--no-all", action="store_true", help="Ask before downloading each course") +idm_sel_group = parser.add_mutually_exclusive_group() +idm_sel_group.add_argument("-i", "--idm", action="store_true", help="Use IDMan.exe") +idm_sel_group.add_argument("-ni", "--no-idm", action="store_true", help="Don't use IDMan.exe, implied when the system is not Windows") +content_sel_group = parser.add_mutually_exclusive_group(required=True) +content_sel_group.add_argument("-da", "--download-all", action="store_true", help="Download all content without asking") +content_sel_group.add_argument("-dq", "--download-ask", action="store_true", help="Ask before downloading each course") +content_sel_group.add_argument("-ds", "--download-select", action="store_true", help="Select courses to download before downloading") parser.add_argument("-nv", "--no-video", action="store_true", help="Don't Download Video") parser.add_argument("-np", "--no-ppt", action="store_true", help="Don't Download PPT") parser.add_argument("-npc", "--no-convert-ppt-to-pdf", action="store_true", help="Don't Convert PPT to PDF") parser.add_argument("-npa", "--no-ppt-answer", action="store_true", help="Don't Store PPT Problem Answer") -parser.add_argument("--course-name-filter", action="store", help="Filter Course Name", default=None) -parser.add_argument("--lesson-name-filter", action="store", help="Filter Lesson Name", default=None) +parser.add_argument("-cnf", "--course-name-filter", action="append", help="Filter Course Name", default=None) +parser.add_argument("-lnf", "--lesson-name-filter", action="append", help="Filter Lesson Name", default=None) -args = parser.parse_args() - -args.__setattr__('video', not args.no_video) -args.__setattr__('ppt', not args.no_ppt) -args.__setattr__('ppt_to_pdf', not args.no_convert_ppt_to_pdf) -args.__setattr__('ppt_problem_answer', not args.no_ppt_answer) - -# Check if no arguments are provided or only --help is provided -if args.help or len(sys.argv) == 1: - print("""RainClassroom Video Downloader - +original_format_help = parser.format_help +def format_help(): + return original_format_help() + """ requirements: - Python >= 3.12 - requests @@ -49,14 +43,25 @@ - aria2c (Download files multi-threaded & resume support) - ffmpeg with nvenc support (Concatenate video segments and convert to HEVC) -""") - print(parser.format_help()) +""" +parser.format_help = format_help + +original_print_help = parser.print_help +def print_help(file=None): + original_print_help(file) if sys.platform == 'win32': print('\nYOU SHALL RUN THIS EXECUTABLE FROM POWERSHELL WITH ARGUMENT!!') os.system('pause') - exit() +parser.print_help = print_help + +args = parser.parse_args() + +args.__setattr__('video', not args.no_video) +args.__setattr__('ppt', not args.no_ppt) +args.__setattr__('ppt_to_pdf', not args.no_convert_ppt_to_pdf) +args.__setattr__('ppt_problem_answer', not args.no_ppt_answer) # Check for dependencies try: @@ -85,17 +90,12 @@ print("PIL is not installed. Please install it using 'pip install pillow'", file=sys.stderr) exit(1) -if args.all and args.no_all: - print("'-a' and '-na' cannot be used together") -if args.idm and args.no_idm: - print("'-idm' and '-no_idm' cannot be used together") - -if args.all: - allin_flag = 1 -elif args.no_all: - allin_flag = 0 -else: - allin_flag = option.ask_for_allin() +if args.download_all: + download_type_flag = 1 +elif args.download_ask: + download_type_flag = 0 +elif args.download_select: + download_type_flag = 2 if sys.platform != 'win32': print("Inferring --no-idm flag as the system is not Windows") @@ -139,6 +139,8 @@ DOWNLOAD_FOLDER = "data" CACHE_FOLDER = "cache" +rainclassroom_sess.headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0" + os.makedirs(DOWNLOAD_FOLDER, exist_ok=True) os.makedirs(CACHE_FOLDER, exist_ok=True) @@ -188,9 +190,9 @@ rainclassroom_sess.post(f"https://{YKT_HOST}/pc/web_login", data=json.dumps({'UserID': userinfo['UserID'], 'Auth': userinfo['Auth']})) - # Store session - with open(f"{DOWNLOAD_FOLDER}/session.txt", "a", encoding='utf-8') as f: - f.write(rainclassroom_sess.cookies['sessionid'] + "\n") +# Store session +with open(f"{DOWNLOAD_FOLDER}/session.txt", "a", encoding='utf-8') as f: + f.write("\n" + rainclassroom_sess.cookies['sessionid'] + "\n") # --- --- --- Section Get Course List --- --- --- # @@ -205,7 +207,35 @@ courses = shown_courses['data']['list'] + hidden_courses['data']['classrooms'] if args.course_name_filter is not None: - courses = [c for c in courses if args.course_name_filter in c['name']] + courses = [c for c in courses if any(f in c['name'] for f in args.course_name_filter)] + +# Show a list of courses and ask for selection +if args.download_select: + done = False + + while not done: + print("Courses:") + for i, course in enumerate(courses): + print(f"{i + 1}. {course['course']['name']}({course['name']}) - {course['teacher']['name']}") + + selection = input("Select courses to download (e.g. `1, 2, 3-5, 10`): ") + + indexes = [] + for part in selection.split(","): + if "-" in part: + start, end = map(int, part.split("-")) + indexes.extend(range(start, end + 1)) + else: + indexes.append(int(part)) + + try: + selected_courses = [courses[i - 1] for i in indexes] + courses = selected_courses + download_type_flag = 1 + done = True + except IndexError: + print(traceback.format_exc()) + print("Invalid selection, please try again") rainclassroom_sess.cookies['xtbz'] = 'ykt' @@ -695,7 +725,7 @@ skip_flag = 0 try: print(course) - if not allin_flag: + if not download_type_flag: skip_flag = option.ask_for_input() if skip_flag: continue diff --git a/ppt_processing.py b/ppt_processing.py index 479bf5e..5cd49e1 100644 --- a/ppt_processing.py +++ b/ppt_processing.py @@ -46,7 +46,7 @@ f.write(f"{slide['cover']}\n out={DOWNLOAD_FOLDER}/{name_prefix}/{slide['index']}.jpg\n") images.append(f"{DOWNLOAD_FOLDER}/{name_prefix}/{slide['index']}.jpg") - ppt_download_command = (f"{ARIA2C_PATH} -i {CACHE_FOLDER}/ppt_download.txt -x 16 -s 16 -c " + ppt_download_command = (f"{ARIA2C_PATH} -i {CACHE_FOLDER}/ppt_download.txt -x 16 -j 16 -c " f"-l aria2c_ppt.log --log-level warn") if WINDOWS: diff --git a/video_processing.py b/video_processing.py index 1db3361..78826c6 100644 --- a/video_processing.py +++ b/video_processing.py @@ -12,18 +12,18 @@ WINDOWS = sys.platform == 'win32' -def download_segment(CACHE_FOLDER, url: str, order: int, name_prefix: str = ""): +def download_segment(CACHE_FOLDER, url: str, order: int, name_prefix: str = "") -> subprocess.CompletedProcess: print(f"Downloading {name_prefix} - {order}") video_download_command = (f"{ARIA2C_PATH} -o '{CACHE_FOLDER}/{name_prefix}-{order}.mp4'" - f" -x 16 -s 16 '{url}' -c -l aria2c_video.log --log-level warn") + f" -x 16 -s 16 -k 1M '{url}' --stream-piece-selector random -k 1M -c -l aria2c_video.log --log-level warn") if WINDOWS: result = subprocess.run(['powershell', '-Command', video_download_command], text=True) else: result = subprocess.run(video_download_command, shell=True) - return result + return result.returncode def download_segment_idm(CACHE_FOLDER, url: str, order: int, name_prefix: str = ""): @@ -151,8 +151,12 @@ for future in as_completed(future_to_order): order = future_to_order[future] try: - future.result() # Get the result (will raise exception if there was one) - print(f"Successfully downloaded {name_prefix} - {order}") + result = future.result() # Get the result (will raise exception if there was one) + if not idm_flag and result != 0: + print(f"Failed to download {name_prefix} - {order}, downloader returned {result}", file=sys.stderr) + has_error = True + else: + print(f"Successfully downloaded {name_prefix} - {order}") except Exception: print(traceback.format_exc()) print(f"Failed to download {name_prefix} - {order}", file=sys.stderr) @@ -189,8 +193,12 @@ for future in as_completed(future_to_order): order = future_to_order[future] try: - future.result() # Get the result (will raise exception if there was one) - print(f"Successfully downloaded {name_prefix} - {order}") + result = future.result() # Get the result (will raise exception if there was one) + if not idm_flag and result != 0: + print(f"Failed to download {name_prefix} - {order}, downloader returned {result}", file=sys.stderr) + has_error = True + else: + print(f"Successfully downloaded {name_prefix} - {order}") except Exception: print(traceback.format_exc()) print(f"Failed to download {name_prefix} - {order}", file=sys.stderr) @@ -226,14 +234,19 @@ for future in as_completed(future_to_order): order = future_to_order[future] try: - future.result() # Get the result (will raise exception if there was one) - print(f"Successfully downloaded {name_prefix} - {order}") + result = future.result() # Get the result (will raise exception if there was one) + if not idm_flag and result != 0: + print(f"Failed to download {name_prefix} - {order}, downloader returned {result}", file=sys.stderr) + has_error = True + else: + print(f"Successfully downloaded {name_prefix} - {order}") except Exception: print(traceback.format_exc()) print(f"Failed to download {name_prefix} - {order}", file=sys.stderr) has_error = True - return has_error + if has_error: + raise Exception("Failed to download some video segments.") def concatenate_segments(CACHE_FOLDER, DOWNLOAD_FOLDER, name_prefix, num_segments): @@ -255,7 +268,7 @@ # First video concatenation command using CUDA acceleration video_concatenating_command = ( - f"{FFMPEG_PATH} -f concat -safe 0 -hwaccel cuda -hwaccel_output_format cuda " + f"{FFMPEG_PATH} -f concat -safe 0 " f"-i '{CACHE_FOLDER}/concat.txt' " f"-c:v av1_nvenc -cq 36 -g 200 -bf 7 -b_strategy 1 -sc_threshold 80 -me_range 16 " f"-surfaces 64 -bufsize 12800k -refs 16 -r 7.5 -temporal-aq 1 -rc-lookahead 127 "