diff --git a/README.md b/README.md index a07a71e..d6c5724 100644 --- a/README.md +++ b/README.md @@ -104,41 +104,41 @@ optional arguments: ``` - Passing a Bearer Token and Course ID as an argument - - `python udemy_downloader\UdemyDownloader.py -c -b ` - - `python udemy_downloader\UdemyDownloader.py -c https://www.udemy.com/courses/myawesomecourse -b ` + - `python udemy_downloader -c -b ` + - `python udemy_downloader -c https://www.udemy.com/courses/myawesomecourse -b ` - Download a specific quality - - `python udemy_downloader\UdemyDownloader.py -c -q 720` + - `python udemy_downloader -c -q 720` - Download assets along with lectures - - `python udemy_downloader\UdemyDownloader.py -c --download-assets` + - `python udemy_downloader -c --download-assets` - Download assets and specify a quality - - `python udemy_downloader\UdemyDownloader.py -c -q 360 --download-assets` + - `python udemy_downloader -c -q 360 --download-assets` - Download captions (Defaults to English) - - `python udemy_downloader\UdemyDownloader.py -c --download-captions` + - `python udemy_downloader -c --download-captions` - Download captions with specific language - - `python udemy_downloader\UdemyDownloader.py -c --download-captions -l en` - English subtitles - - `python udemy_downloader\UdemyDownloader.py -c --download-captions -l es` - Spanish subtitles - - `python udemy_downloader\UdemyDownloader.py -c --download-captions -l it` - Italian subtitles - - `python udemy_downloader\UdemyDownloader.py -c --download-captions -l pl` - Polish Subtitles - - `python udemy_downloader\UdemyDownloader.py -c --download-captions -l all` - Downloads all subtitles + - `python udemy_downloader -c --download-captions -l en` - English subtitles + - `python udemy_downloader -c --download-captions -l es` - Spanish subtitles + - `python udemy_downloader -c --download-captions -l it` - Italian subtitles + - `python udemy_downloader -c --download-captions -l pl` - Polish Subtitles + - `python udemy_downloader -c --download-captions -l all` - Downloads all subtitles - etc - Skip downloading lecture videos - - `python udemy_downloader\UdemyDownloader.py -c --skip-lectures --download-captions` - Downloads only captions - - `python udemy_downloader\UdemyDownloader.py -c --skip-lectures --download-assets` - Downloads only assets + - `python udemy_downloader -c --skip-lectures --download-captions` - Downloads only captions + - `python udemy_downloader -c --skip-lectures --download-assets` - Downloads only assets - Keep .VTT caption files: - - `python udemy_downloader\UdemyDownloader.py -c --download-captions --keep-vtt` + - `python udemy_downloader -c --download-captions --keep-vtt` - Skip parsing HLS Streams (HLS streams usually contain 1080p quality for Non-DRM lectures): - - `python udemy_downloader\UdemyDownloader.py -c --skip-hls` + - `python udemy_downloader -c --skip-hls` - Print course information only: - - `python udemy_downloader\UdemyDownloader.py -c --info` + - `python udemy_downloader -c --info` - Specify max number of concurrent downloads: - - `python udemy_downloader\UdemyDownloader.py -c --concurrent-downloads 20` - - `python udemy_downloader\UdemyDownloader.py -c -cd 20` + - `python udemy_downloader -c --concurrent-downloads 20` + - `python udemy_downloader -c -cd 20` - Encode in H.265: - - `python udemy_downloader\UdemyDownloader.py -c --use-h265` + - `python udemy_downloader -c --use-h265` - Encode in H.265 with custom CRF: - - `python udemy_downloader\UdemyDownloader.py -c --use-h265 -h265-crf 20` + - `python udemy_downloader -c --use-h265 -h265-crf 20` - Encode in H.265 with custom preset: - - `python udemy_downloader\UdemyDownloader.py -c --use-h265 --h265-preset faster` + - `python udemy_downloader -c --use-h265 --h265-preset faster` # Credits diff --git a/udemy_downloader/UdemyDownloader.py b/udemy_downloader/UdemyDownloader.py index b12cf97..b129fdc 100644 --- a/udemy_downloader/UdemyDownloader.py +++ b/udemy_downloader/UdemyDownloader.py @@ -51,6 +51,7 @@ _udemy_path = os.path.join(saved_dir, "_udemy.json") udemy = None parser = None +iknowwhatimdoing = False retry = 3 _udemy = {} course_url = None @@ -82,6 +83,7 @@ use_h265 = False h265_crf = 28 h265_preset = "medium" + def download_segments(url, format_id, video_title, output_path, lecture_file_name, chapter_dir): os.chdir(os.path.join(chapter_dir)) file_name = lecture_file_name.replace("%", "").replace(".mp4", "") @@ -103,7 +105,6 @@ def download_segments(url, format_id, video_title, output_path, lecture_file_nam print("Return code from the downloader was non-0 (error), skipping!") return - # tries to decrypt audio and video, and then merge them try: # tries to decrypt audio @@ -113,9 +114,11 @@ def download_segments(url, format_id, video_title, output_path, lecture_file_nam audio_key = keys[audio_kid.lower()] print("> Decrypting audio...") - ret_code = decrypt(audio_key, audio_filepath_enc, audio_filepath_dec) + ret_code = decrypt( + audio_key, audio_filepath_enc, audio_filepath_dec) if(ret_code != 0): - print("WARN: Decrypting returned a non-0 result code which usually indicated an error!") + print( + "WARN: Decrypting returned a non-0 result code which usually indicated an error!") else: print("Decryption complete") except KeyError: @@ -129,32 +132,35 @@ def download_segments(url, format_id, video_title, output_path, lecture_file_nam video_key = keys[video_kid.lower()] print("> Decrypting video...") - ret_code2 = decrypt(video_key, video_filepath_enc, video_filepath_dec) + ret_code2 = decrypt( + video_key, video_filepath_enc, video_filepath_dec) if(ret_code2 != 0): - print("WARN: Decrypting returned a non-0 result code which usually indicated an error!") + print( + "WARN: Decrypting returned a non-0 result code which usually indicated an error!") else: print("Decryption complete") except KeyError: print("Video key not found!") raise RuntimeError("No video key") - # tries to merge audio and video # this should run only if both audio and video decryption returned 0 codes print("> Merging audio and video files...") - ret_code3 = merge(video_title=video_title, video_filepath=video_filepath_dec, audio_filepath=audio_filepath_dec, output_path=output_path, use_h265=use_h265, h265_crf=h265_crf, h265_preset=h265_preset) + ret_code3 = merge(video_title=video_title, video_filepath=video_filepath_dec, audio_filepath=audio_filepath_dec, + output_path=output_path, use_h265=use_h265, h265_crf=h265_crf, h265_preset=h265_preset) if(ret_code3 != 0): - print("WARN: Merging returned a non-0 result code which usually indicated an error!") - + print( + "WARN: Merging returned a non-0 result code which usually indicated an error!") if(ret_code == 0 and ret_code2 == 0 and ret_code3 == 0): print("> Cleaning up...") # remove all the temporary files left over after decryption and merging if there were no errors - remove_files((video_filepath_enc, video_filepath_dec, audio_filepath_enc, audio_filepath_dec)) + remove_files((video_filepath_enc, video_filepath_dec, + audio_filepath_enc, audio_filepath_dec)) print("> Cleanup complete") except Exception as e: print(e) - + os.chdir(home_dir) @@ -200,7 +206,7 @@ def download_aria(url, file_dir, filename): print("Return code: " + str(ret_code)) -def process_caption(caption, lecture_title, lecture_dir, keep_vtt, tries=0): +def process_caption(caption, lecture_title, lecture_dir, tries=0): filename = f"%s_%s.%s" % (sanitize(lecture_title), caption.get("language"), caption.get("extension")) filename_no_ext = f"%s_%s" % (sanitize(lecture_title), @@ -241,6 +247,78 @@ def process_lecture(lecture, lecture_path, lecture_file_name, chapter_dir): is_encrypted = lecture.get("is_encrypted") lecture_sources = lecture.get("video_sources") + if dl_assets: + assets = lecture.get("assets") + print(" > Processing {} asset(s) for lecture...".format( + len(assets))) + + for asset in assets: + asset_type = asset.get("type") + filename = asset.get("filename") + download_url = asset.get("download_url") + asset_id = asset.get("id") + + if asset_type == "article": + print( + "If you're seeing this message, that means that you reached a secret area that I haven't finished! jk I haven't implemented handling for this asset type, please report this at https://github.com/Puyodead1/udemy-downloader/issues so I can add it. When reporting, please provide the following information: " + ) + print("AssetType: Article; AssetData: ", asset) + # html_content = lecture.get("html_content") + # lecture_path = os.path.join( + # chapter_dir, "{}.html".format(sanitize(lecture_title))) + # try: + # with open(lecture_path, 'w') as f: + # f.write(html_content) + # f.close() + # except Exception as e: + # print("Failed to write html file: ", e) + # continue + elif asset_type == "video": + print( + "If you're seeing this message, that means that you reached a secret area that I haven't finished! jk I haven't implemented handling for this asset type, please report this at https://github.com/Puyodead1/udemy-downloader/issues so I can add it. When reporting, please provide the following information: " + ) + print("AssetType: Video; AssetData: ", asset) + elif asset_type == "audio" or asset_type == "e-book" or asset_type == "file" or asset_type == "presentation": + try: + download_aria(download_url, chapter_dir, + f"{asset_id}-{filename}") + except Exception as e: + print("> Error downloading asset: ", e) + continue + elif asset_type == "external_link": + filepath = os.path.join(chapter_dir, filename) + savedirs, name = os.path.split(filepath) + filename = u"external-assets-links.txt" + filename = os.path.join(savedirs, filename) + file_data = [] + if os.path.isfile(filename): + file_data = [ + i.strip().lower() + for i in open(filename, + encoding="utf-8", + errors="ignore") if i + ] + + content = u"\n{}\n{}\n".format(name, download_url) + if name.lower() not in file_data: + with open(filename, + 'a', + encoding="utf-8", + errors="ignore") as f: + f.write(content) + f.close() + + subtitles = lecture.get("subtitles") + if dl_captions and subtitles: + selected_subtitles = [] + print("Processing {} caption(s)...".format(len(subtitles))) + for subtitle in subtitles: + lang = subtitle.get("language") + if lang == caption_locale or caption_locale == "all": + selected_subtitles.append(subtitle) + process_caption(subtitle, lecture_title, chapter_dir) + print("Selected {} captions".format(len(selected_subtitles))) + if is_encrypted: if len(lecture_sources) > 0: source = lecture_sources[-1] # last index is the best quality @@ -251,8 +329,8 @@ def process_lecture(lecture, lecture_path, lecture_file_name, chapter_dir): print(f" > Lecture '%s' has DRM, attempting to download" % lecture_title) download_segments(source.get("download_url"), - source.get( - "format_id"), lecture_title, lecture_path, lecture_file_name, chapter_dir) + source.get( + "format_id"), lecture_title, lecture_path, lecture_file_name, chapter_dir) else: print(f" > Lecture '%s' is missing media links" % lecture_title) @@ -287,7 +365,6 @@ def process_lecture(lecture, lecture_path, lecture_file_name, chapter_dir): "aria2c", "-o", f"{temp_filepath}", f"{url}" ]).wait() if ret_code == 0: - # os.rename(temp_filepath, lecture_path) print(" > HLS Download success") else: download_aria(url, chapter_dir, lecture_title + ".mp4") @@ -338,7 +415,6 @@ def parse(): print( f" > Processing lecture {lecture_index} of {total_lectures}") if not skip_lectures: - print(lecture_file_name) # Check if the lecture is already downloaded if os.path.isfile(lecture_path): print( @@ -360,76 +436,8 @@ def parse(): print(" > Failed to write html file: ", e) continue else: - process_lecture(lecture, lecture_path, lecture_file_name, chapter_dir) - - if dl_assets: - assets = lecture.get("assets") - print(" > Processing {} asset(s) for lecture...".format( - len(assets))) - - for asset in assets: - asset_type = asset.get("type") - filename = asset.get("filename") - download_url = asset.get("download_url") - asset_id = asset.get("id") - - if asset_type == "article": - print( - "If you're seeing this message, that means that you reached a secret area that I haven't finished! jk I haven't implemented handling for this asset type, please report this at https://github.com/Puyodead1/udemy-downloader/issues so I can add it. When reporting, please provide the following information: " - ) - print("AssetType: Article; AssetData: ", asset) - # html_content = lecture.get("html_content") - # lecture_path = os.path.join( - # chapter_dir, "{}.html".format(sanitize(lecture_title))) - # try: - # with open(lecture_path, 'w') as f: - # f.write(html_content) - # f.close() - # except Exception as e: - # print("Failed to write html file: ", e) - # continue - elif asset_type == "video": - print( - "If you're seeing this message, that means that you reached a secret area that I haven't finished! jk I haven't implemented handling for this asset type, please report this at https://github.com/Puyodead1/udemy-downloader/issues so I can add it. When reporting, please provide the following information: " - ) - print("AssetType: Video; AssetData: ", asset) - elif asset_type == "audio" or asset_type == "e-book" or asset_type == "file" or asset_type == "presentation": - try: - download_aria(download_url, chapter_dir, - f"{asset_id}-{filename}") - except Exception as e: - print("> Error downloading asset: ", e) - continue - elif asset_type == "external_link": - filepath = os.path.join(chapter_dir, filename) - savedirs, name = os.path.split(filepath) - filename = u"external-assets-links.txt" - filename = os.path.join(savedirs, filename) - file_data = [] - if os.path.isfile(filename): - file_data = [ - i.strip().lower() - for i in open(filename, - encoding="utf-8", - errors="ignore") if i - ] - - content = u"\n{}\n{}\n".format(name, download_url) - if name.lower() not in file_data: - with open(filename, - 'a', - encoding="utf-8", - errors="ignore") as f: - f.write(content) - f.close() - - subtitles = lecture.get("subtitles") - if dl_captions and subtitles: - print("Processing {} caption(s)...".format(len(subtitles))) - for subtitle in subtitles: - lang = subtitle.get("language") - if lang == caption_locale or caption_locale == "all": - process_caption(subtitle, lecture_title, chapter_dir) + process_lecture(lecture, lecture_path, + lecture_file_name, chapter_dir) def process_course(): @@ -481,15 +489,15 @@ def process_course(): if isinstance(asset, dict): asset_type = (asset.get("asset_type").lower() - or asset.get("assetType").lower) + or asset.get("assetType").lower) if asset_type == "article": if isinstance(supp_assets, - list) and len(supp_assets) > 0: + list) and len(supp_assets) > 0: retVal = udemy._extract_supplementary_assets( supp_assets) elif asset_type == "video": if isinstance(supp_assets, - list) and len(supp_assets) > 0: + list) and len(supp_assets) > 0: retVal = udemy._extract_supplementary_assets( supp_assets) elif asset_type == "e-book": @@ -641,6 +649,7 @@ def process_course(): if entry ]) + def get_course_information(): global course_info, course_id, title, course_title, portal_name if(load_from_file): @@ -658,6 +667,7 @@ def get_course_information(): course_title = course_info.get("published_title") portal_name = course_info.get("portal_name") + def get_course_content(): global course_content if load_from_file: @@ -666,18 +676,22 @@ def get_course_content(): course_content = json.loads(f.read()) else: print("course_content.json not found, falling back to fetching") - course_content = udemy._extract_course_json(course_url, course_id, portal_name) + course_content = udemy._extract_course_json( + course_url, course_id, portal_name) else: - course_content = udemy._extract_course_json(course_url, course_id, portal_name) + course_content = udemy._extract_course_json( + course_url, course_id, portal_name) + def parse_data(): global _udemy - if load_from_file: + if load_from_file and os.path.exists(_udemy_path): f = open(_udemy_path, 'r') _udemy = json.loads(f.read()) else: process_course() + def _print_course_info(course_data): print("\n\n\n\n") course_title = course_data.get("title") @@ -746,6 +760,7 @@ def _print_course_info(course_data): if chapter_index != chapter_count: print("\n\n") + def setup_parser(): global parser parser = argparse.ArgumentParser(description='Udemy Downloader') @@ -839,6 +854,12 @@ def setup_parser(): default="medium", help="Set a custom preset value for H.265 encoding. FFMPEG default is medium", ) + parser.add_argument( + "--iknowwhatimdoing", + dest="iknowwhatimdoing", + action="store_true", + help=argparse.SUPPRESS, + ) parser.add_argument( "--save-to-file", dest="save_to_file", @@ -854,10 +875,10 @@ def setup_parser(): parser.add_argument("-v", "--version", action="version", version='You are running version {version}'.format(version=__version__)) - + def process_args(args): - global course_url, bearer_token, dl_assets, caption_locale, skip_lectures, quality, keep_vtt, skip_hls, print_info, load_from_file, save_to_file, concurrent_connections, use_h265, h265_crf, h265_preset - + global course_url, bearer_token, dl_assets, dl_captions, caption_locale, skip_lectures, quality, keep_vtt, skip_hls, print_info, load_from_file, save_to_file, concurrent_connections, use_h265, h265_crf, h265_preset, iknowwhatimdoing + course_url = args.course_url if args.download_assets: dl_assets = True @@ -893,6 +914,8 @@ def process_args(args): h265_crf = args.h265_crf if args.h265_preset: h265_preset = args.h265_preset + if args.iknowwhatimdoing: + iknowwhatimdoing = args.iknowwhatimdoing if args.load_from_file: print( @@ -907,6 +930,7 @@ def process_args(args): else: bearer_token = os.getenv("UDEMY_BEARER") + def ensure_dependencies_installed(): aria_ret_val = check_for_aria() if not aria_ret_val: @@ -925,6 +949,7 @@ def ensure_dependencies_installed(): ) sys.exit(1) + def check_dirs(): if not os.path.exists(saved_dir): os.makedirs(saved_dir) @@ -932,29 +957,17 @@ def check_dirs(): if not os.path.exists(download_dir): os.makedirs(download_dir) -def load_keys(): + +def try_load_keys(): global keys f = open(keyfile_path, 'r') keys = json.loads(f.read()) + def UdemyDownloader(): global udemy, course, resource check_dirs() - # warn that the keyfile is not found - if not os.path.isfile(keyfile_path): - print("!!! Keyfile not found! This means you probably didn't rename the keyfile correctly, DRM lecture decryption will fail! If you aren't downloading DRM encrypted courses, you can ignore this message. !!!") - print("Waiting for 10 seconds...") - time.sleep(10) - - load_keys() - - # ensure 3rd party binaries are installed - ensure_dependencies_installed(); - - # loads the .env file - load_dotenv() - # Creates a new parser and sets up the arguments setup_parser() @@ -962,6 +975,22 @@ def UdemyDownloader(): args = parser.parse_args() process_args(args=args) + # warn that the keyfile is not found + if not os.path.exists(keyfile_path): + print("!!! Keyfile not found! This means you probably didn't rename the keyfile correctly, DRM lecture decryption will fail! If you aren't downloading DRM encrypted courses, you can ignore this message. !!!") + if not iknowwhatimdoing: + print("Waiting for 10 seconds...") + time.sleep(10) + + else: + try_load_keys() + + # ensure 3rd party binaries are installed + ensure_dependencies_installed() + + # loads the .env file + load_dotenv() + udemy = Udemy(access_token=bearer_token) print("> Fetching course information, this may take a minute...") @@ -989,7 +1018,7 @@ def UdemyDownloader(): 'w') as f: f.write(json.dumps(course_content)) print("Saved course content to file") - + course = course_content.get("results") resource = course_content.get("detail") @@ -1010,7 +1039,7 @@ def UdemyDownloader(): if save_to_file: with open(_udemy_path, - 'w') as f: + 'w') as f: f.write(json.dumps(_udemy)) print("Saved parsed data to file") @@ -1021,4 +1050,4 @@ def UdemyDownloader(): if __name__ == "__main__": - UdemyDownloader() \ No newline at end of file + UdemyDownloader() diff --git a/udemy_downloader/__main__.py b/udemy_downloader/__main__.py new file mode 100644 index 0000000..2115621 --- /dev/null +++ b/udemy_downloader/__main__.py @@ -0,0 +1,4 @@ +from UdemyDownloader import UdemyDownloader + +if __name__ == "__main__": + UdemyDownloader() diff --git a/udemy_downloader/utils.py b/udemy_downloader/utils.py index 3b139e7..b953ee8 100644 --- a/udemy_downloader/utils.py +++ b/udemy_downloader/utils.py @@ -9,6 +9,7 @@ from mp4parse import F4VParser from widevine_pssh_pb2 import WidevinePsshData from sanitize import sanitize, slugify, SLUG_OK + def extract_kid(mp4_file): """ Parameters @@ -26,7 +27,8 @@ def extract_kid(mp4_file): boxes = F4VParser.parse(filename=mp4_file) for box in boxes: 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") pssh = WidevinePsshData() @@ -37,6 +39,7 @@ def extract_kid(mp4_file): # No Moof or PSSH header found return None + def _clean(text): ok = re.compile(r'[^\\/:*?!"<>|]') text = "".join(x if ok.match(x) else "_" for x in text) @@ -49,6 +52,7 @@ def _sanitize(self, unsafetext): slugify(unsafetext, lower=False, spaces=True, ok=SLUG_OK + "().[]"))) return text + def durationtoseconds(period): """ @author Jayapraveen @@ -74,6 +78,7 @@ def durationtoseconds(period): print("Duration Format Error") return None + def cleanup(path): """ @author Jayapraveen @@ -86,39 +91,47 @@ def cleanup(path): print(f"Error deleting file: {file_list}") os.removedirs(path) + def remove_files(files): for file in files: os.remove(file) + def merge(video_title, video_filepath, audio_filepath, output_path, use_h265, h265_crf, h265_preset): """ @author Jayapraveen """ if os.name == "nt": 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) + 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) + command = "ffmpeg -y -i \"{}\" -i \"{}\" -c:v copy -c:a copy -fflags +bitexact -map_metadata -1 -metadata title=\"{}\" \"{}\"".format( + video_filepath, audio_filepath, video_title, output_path) else: 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) + 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) + command = "nide -n 7 ffmpeg -y -i \"{}\" -i \"{}\" -c:v copy -c:a copy -fflags +bitexact -map_metadata -1 -metadata title=\"{}\" \"{}\"".format( + video_filepath, audio_filepath, video_title, output_path) return os.system(command) + def decrypt(key, in_filepath, out_filepath): """ @author Jayapraveen """ if (os.name == "nt"): ret_code = os.system(f"mp4decrypt --key 1:%s \"%s\" \"%s\"" % - (key, in_filepath, out_filepath)) + (key, in_filepath, out_filepath)) else: ret_code = os.system(f"nice -n 7 mp4decrypt --key 1:%s \"%s\" \"%s\"" % - (key, in_filepath, out_filepath)) + (key, in_filepath, out_filepath)) return ret_code + def check_for_aria(): try: subprocess.Popen(["aria2c", "-v"], @@ -161,4 +174,4 @@ def check_for_mp4decrypt(): print( "> Unexpected exception while checking for MP4Decrypt, please tell the program author about this! ", e) - return True \ No newline at end of file + return True