mirror of
https://cdm-project.com/Download-Tools/udemy-downloader.git
synced 2025-04-30 02:14:25 +02:00
commit
1d43d19a47
@ -66,8 +66,8 @@ You can now run the program, see the examples below. The course will download to
|
||||
# Advanced Usage
|
||||
|
||||
```
|
||||
usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [--skip-lectures] [--download-assets] [--download-captions]
|
||||
[--keep-vtt] [--skip-hls] [--info]
|
||||
usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [-cd CONCURRENT_DOWNLOADS] [--skip-lectures] [--download-assets]
|
||||
[--download-captions] [--keep-vtt] [--skip-hls] [--info]
|
||||
|
||||
Udemy Downloader
|
||||
|
||||
@ -81,6 +81,8 @@ optional arguments:
|
||||
Download specific video quality. If the requested quality isn't available, the closest quality will be used. If not
|
||||
specified, the best quality will be downloaded for each lecture
|
||||
-l LANG, --lang LANG The language to download for captions, specify 'all' to download all captions (Default is 'en')
|
||||
-cd CONCURRENT_DOWNLOADS, --concurrent-downloads CONCURRENT_DOWNLOADS
|
||||
The number of maximum concurrent downloads for segments (HLS and DASH, must be a number 1-50)
|
||||
--skip-lectures If specified, lectures won't be downloaded
|
||||
--download-assets If specified, lecture assets will be downloaded
|
||||
--download-captions If specified, captions will be downloaded
|
||||
@ -117,6 +119,9 @@ optional arguments:
|
||||
- `python main.py -c <Course URL> --skip-hls`
|
||||
- Print course information only:
|
||||
- `python main.py -c <Course URL> --info`
|
||||
- Specify max number of concurrent downloads:
|
||||
- `python main.py -c <Course URL> --concurrent-downloads 20`
|
||||
- `python main.py -c <Course URL> -cd 20`
|
||||
|
||||
# Credits
|
||||
|
||||
|
317
main.py
317
main.py
@ -18,8 +18,8 @@ from vtt_to_srt import convert
|
||||
from requests.exceptions import ConnectionError as conn_error
|
||||
from html.parser import HTMLParser as compat_HTMLParser
|
||||
from sanitize import sanitize, slugify, SLUG_OK
|
||||
from pyffmpeg import FFMPeg as FFMPEG
|
||||
import subprocess
|
||||
import yt_dlp
|
||||
|
||||
home_dir = os.getcwd()
|
||||
download_dir = os.path.join(os.getcwd(), "out_dir")
|
||||
@ -237,19 +237,17 @@ class Udemy:
|
||||
return _temp
|
||||
|
||||
def _extract_media_sources(self, sources):
|
||||
_audio = []
|
||||
_video = []
|
||||
_temp = []
|
||||
if sources and isinstance(sources, list):
|
||||
for source in sources:
|
||||
_type = source.get("type")
|
||||
src = source.get("src")
|
||||
|
||||
if _type == "application/dash+xml":
|
||||
video, audio = self._extract_mpd(src)
|
||||
if video and audio:
|
||||
_video.extend(video)
|
||||
_audio.extend(audio)
|
||||
return (_video, _audio)
|
||||
out = self._extract_mpd(src)
|
||||
if out:
|
||||
_temp.extend(out)
|
||||
return _temp
|
||||
|
||||
def _extract_subtitles(self, tracks):
|
||||
_temp = []
|
||||
@ -308,83 +306,49 @@ class Udemy:
|
||||
return _temp
|
||||
|
||||
def _extract_mpd(self, url):
|
||||
"""extract mpd streams"""
|
||||
_video = []
|
||||
_audio = []
|
||||
"""extracts mpd streams"""
|
||||
_temp = []
|
||||
try:
|
||||
resp = self.session._get(url)
|
||||
resp.raise_for_status()
|
||||
raw_data = resp.text
|
||||
mpd_object = MPEGDASHParser.parse(raw_data)
|
||||
ytdl = yt_dlp.YoutubeDL({
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
"allow_unplayable_formats": True
|
||||
})
|
||||
results = ytdl.extract_info(url,
|
||||
download=False,
|
||||
force_generic_extractor=True)
|
||||
seen = set()
|
||||
for period in mpd_object.periods:
|
||||
for adapt_set in period.adaptation_sets:
|
||||
content_type = adapt_set.mime_type
|
||||
if content_type == "video/mp4":
|
||||
for rep in adapt_set.representations:
|
||||
for segment in rep.segment_templates:
|
||||
segment_count = 1
|
||||
timeline = segment.segment_timelines[0]
|
||||
segment_count += len(timeline.Ss)
|
||||
for s in timeline.Ss:
|
||||
if s.r:
|
||||
segment_count += s.r
|
||||
formats = results.get("formats")
|
||||
|
||||
segment_extension = segment.media.split(
|
||||
".")[-1]
|
||||
height = rep.height
|
||||
width = rep.width
|
||||
format_id = results.get("format_id")
|
||||
best_audio_format_id = format_id.split("+")[1]
|
||||
best_audio = next((x for x in formats
|
||||
if x.get("format_id") == best_audio_format_id),
|
||||
None)
|
||||
for f in formats:
|
||||
if "video" in f.get("format_note"):
|
||||
# is a video stream
|
||||
format_id = f.get("format_id")
|
||||
extension = f.get("ext")
|
||||
height = f.get("height")
|
||||
width = f.get("width")
|
||||
|
||||
if height not in seen:
|
||||
seen.add(height)
|
||||
_video.append({
|
||||
"type":
|
||||
"dash",
|
||||
"content_type":
|
||||
"video",
|
||||
"height":
|
||||
height,
|
||||
"width":
|
||||
width,
|
||||
"extension":
|
||||
segment_extension,
|
||||
"segment_count":
|
||||
segment_count,
|
||||
"media":
|
||||
segment.media,
|
||||
"initialization":
|
||||
segment.initialization
|
||||
})
|
||||
elif content_type == "audio/mp4":
|
||||
for rep in adapt_set.representations:
|
||||
for segment in rep.segment_templates:
|
||||
segment_count = 1
|
||||
timeline = segment.segment_timelines[0]
|
||||
segment_count += len(timeline.Ss)
|
||||
for s in timeline.Ss:
|
||||
if s.r:
|
||||
segment_count += s.r
|
||||
|
||||
segment_extension = segment.media.split(
|
||||
".")[-1]
|
||||
|
||||
_audio.append({
|
||||
"type":
|
||||
"dash",
|
||||
"content_type":
|
||||
"audio",
|
||||
"extension":
|
||||
segment_extension,
|
||||
"segment_count":
|
||||
segment_count,
|
||||
"media":
|
||||
segment.media,
|
||||
"initialization":
|
||||
segment.initialization
|
||||
})
|
||||
if height and height not in seen:
|
||||
seen.add(height)
|
||||
_temp.append({
|
||||
"type": "dash",
|
||||
"height": str(height),
|
||||
"width": str(width),
|
||||
"format_id": f"{format_id},{best_audio_format_id}",
|
||||
"extension": extension,
|
||||
"download_url": f.get("manifest_url")
|
||||
})
|
||||
else:
|
||||
# unknown format type
|
||||
continue
|
||||
except Exception as error:
|
||||
print(f"Udemy Says : '{error}' while fetching mpd manifest")
|
||||
return (_video, _audio)
|
||||
print(f"Error fetching MPD streams: '{error}'")
|
||||
return _temp
|
||||
|
||||
def extract_course_name(self, url):
|
||||
"""
|
||||
@ -865,24 +829,20 @@ def cleanup(path):
|
||||
os.removedirs(path)
|
||||
|
||||
|
||||
def mux_process(video_title, lecture_working_dir, output_path):
|
||||
def mux_process(video_title, video_filepath, audio_filepath, output_path):
|
||||
"""
|
||||
@author Jayapraveen
|
||||
"""
|
||||
if os.name == "nt":
|
||||
command = "ffmpeg -y -i \"{}\" -i \"{}\" -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title=\"{}\" \"{}\"".format(
|
||||
os.path.join(lecture_working_dir, "decrypted_audio.mp4"),
|
||||
os.path.join(lecture_working_dir, "decrypted_video.mp4"),
|
||||
video_title, output_path)
|
||||
video_filepath, audio_filepath, video_title, output_path)
|
||||
else:
|
||||
command = "nice -n 7 ffmpeg -y -i \"{}\" -i \"{}\" -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title=\"{}\" \"{}\"".format(
|
||||
os.path.join(lecture_working_dir, "decrypted_audio.mp4"),
|
||||
os.path.join(lecture_working_dir, "decrypted_video.mp4"),
|
||||
video_title, output_path)
|
||||
video_filepath, audio_filepath, video_title, output_path)
|
||||
os.system(command)
|
||||
|
||||
|
||||
def decrypt(kid, filename, lecture_working_dir):
|
||||
def decrypt(kid, in_filepath, out_filepath):
|
||||
"""
|
||||
@author Jayapraveen
|
||||
"""
|
||||
@ -891,101 +851,49 @@ def decrypt(kid, filename, lecture_working_dir):
|
||||
key = keyfile[kid.lower()]
|
||||
if (os.name == "nt"):
|
||||
os.system(f"mp4decrypt --key 1:%s \"%s\" \"%s\"" %
|
||||
(key,
|
||||
os.path.join(lecture_working_dir,
|
||||
"encrypted_{}.mp4".format(filename)),
|
||||
os.path.join(lecture_working_dir,
|
||||
"decrypted_{}.mp4".format(filename))))
|
||||
(key, in_filepath, out_filepath))
|
||||
else:
|
||||
os.system(f"nice -n 7 mp4decrypt --key 1:%s \"%s\" \"%s\"" %
|
||||
(key,
|
||||
os.path.join(lecture_working_dir,
|
||||
"encrypted_{}.mp4".format(filename)),
|
||||
os.path.join(lecture_working_dir,
|
||||
"decrypted_{}.mp4".format(filename))))
|
||||
(key, in_filepath, out_filepath))
|
||||
print("> Decryption complete")
|
||||
except KeyError:
|
||||
raise KeyError("Key not found")
|
||||
|
||||
|
||||
def handle_segments(video_source, audio_source, video_title,
|
||||
lecture_working_dir, output_path):
|
||||
"""
|
||||
@author Jayapraveen
|
||||
"""
|
||||
no_vid_segments = video_source.get("segment_count")
|
||||
no_aud_segments = audio_source.get("segment_count")
|
||||
|
||||
audio_media = audio_source.get("media")
|
||||
audio_init = audio_source.get("initialization")
|
||||
audio_extension = audio_source.get("extension")
|
||||
|
||||
video_media = video_source.get("media")
|
||||
video_init = video_source.get("initialization")
|
||||
video_extension = video_source.get("extension")
|
||||
|
||||
audio_urls = audio_init + "\n dir={}\n out=audio_0.mp4\n".format(
|
||||
lecture_working_dir)
|
||||
video_urls = video_init + "\n dir={}\n out=video_0.mp4\n".format(
|
||||
lecture_working_dir)
|
||||
|
||||
list_path = os.path.join(lecture_working_dir, "list.txt")
|
||||
|
||||
for i in range(1, no_aud_segments):
|
||||
audio_urls += audio_media.replace(
|
||||
"$Number$", str(i)) + "\n dir={}\n out=audio_{}.mp4\n".format(
|
||||
lecture_working_dir, i)
|
||||
for i in range(1, no_vid_segments):
|
||||
video_urls += video_media.replace(
|
||||
"$Number$", str(i)) + "\n dir={}\n out=video_{}.mp4\n".format(
|
||||
lecture_working_dir, i)
|
||||
|
||||
with open(list_path, 'w') as f:
|
||||
f.write("{}\n{}".format(audio_urls, video_urls))
|
||||
f.close()
|
||||
|
||||
print("> Downloading Lecture Segments...")
|
||||
def handle_segments(url, format_id, video_title, lecture_working_dir,
|
||||
output_path, concurrent_connections):
|
||||
temp_filepath = output_path.replace(".mp4", "")
|
||||
temp_filepath = temp_filepath + ".mpd-part"
|
||||
video_filepath_enc = temp_filepath + ".mp4"
|
||||
audio_filepath_enc = temp_filepath + ".m4a"
|
||||
video_filepath_dec = temp_filepath + ".decrypted.mp4"
|
||||
audio_filepath_dec = temp_filepath + ".decrypted.m4a"
|
||||
print("> Downloading Lecture Tracks...")
|
||||
ret_code = subprocess.Popen([
|
||||
"aria2c", "-i", list_path, "-j16", "-s20", "-x16", "-c",
|
||||
"--auto-file-renaming=false", "--summary-interval=0"
|
||||
"yt-dlp", "--force-generic-extractor", "--allow-unplayable-formats",
|
||||
"--concurrent-fragments", f"{concurrent_connections}", "--downloader",
|
||||
"aria2c", "--fixup", "never", "-k", "-o", f"{temp_filepath}.%(ext)s",
|
||||
"-f", format_id, f"{url}"
|
||||
]).wait()
|
||||
print("> Lecture Segments Downloaded")
|
||||
print("> Lecture Tracks Downloaded")
|
||||
|
||||
print("Return code: " + str(ret_code))
|
||||
|
||||
os.remove(list_path)
|
||||
|
||||
video_kid = extract_kid(os.path.join(lecture_working_dir, "video_0.mp4"))
|
||||
video_kid = extract_kid(video_filepath_enc)
|
||||
print("KID for video file is: " + video_kid)
|
||||
|
||||
audio_kid = extract_kid(os.path.join(lecture_working_dir, "audio_0.mp4"))
|
||||
audio_kid = extract_kid(audio_filepath_enc)
|
||||
print("KID for audio file is: " + audio_kid)
|
||||
|
||||
os.chdir(lecture_working_dir)
|
||||
|
||||
if os.name == "nt":
|
||||
video_concat_command = "copy /b " + "+".join([
|
||||
f"video_{i}.{video_extension}" for i in range(0, no_vid_segments)
|
||||
]) + " encrypted_video.mp4"
|
||||
audio_concat_command = "copy /b " + "+".join([
|
||||
f"audio_{i}.{audio_extension}" for i in range(0, no_aud_segments)
|
||||
]) + " encrypted_audio.mp4"
|
||||
else:
|
||||
video_concat_command = "cat " + " ".join([
|
||||
f"video_{i}.{video_extension}" for i in range(0, no_aud_segments)
|
||||
]) + " > encrypted_video.mp4"
|
||||
audio_concat_command = "cat " + " ".join([
|
||||
f"audio_{i}.{audio_extension}" for i in range(0, no_vid_segments)
|
||||
]) + " > encrypted_audio.mp4"
|
||||
os.system(video_concat_command)
|
||||
os.system(audio_concat_command)
|
||||
os.chdir(home_dir)
|
||||
try:
|
||||
decrypt(video_kid, "video", lecture_working_dir)
|
||||
decrypt(audio_kid, "audio", lecture_working_dir)
|
||||
os.chdir(home_dir)
|
||||
mux_process(video_title, lecture_working_dir, output_path)
|
||||
cleanup(lecture_working_dir)
|
||||
decrypt(video_kid, video_filepath_enc, video_filepath_dec)
|
||||
decrypt(audio_kid, audio_filepath_enc, audio_filepath_dec)
|
||||
mux_process(video_title, video_filepath_dec, audio_filepath_dec,
|
||||
output_path)
|
||||
os.remove(video_filepath_enc)
|
||||
os.remove(audio_filepath_enc)
|
||||
os.remove(video_filepath_dec)
|
||||
os.remove(audio_filepath_dec)
|
||||
except Exception as e:
|
||||
print(f"Error: ", e)
|
||||
|
||||
@ -1113,31 +1021,31 @@ def process_caption(caption, lecture_title, lecture_dir, keep_vtt, tries=0):
|
||||
print(f" > Error converting caption: {e}")
|
||||
|
||||
|
||||
def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
||||
def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token,
|
||||
concurrent_connections):
|
||||
lecture_title = lecture.get("lecture_title")
|
||||
is_encrypted = lecture.get("is_encrypted")
|
||||
lecture_video_sources = lecture.get("video_sources")
|
||||
lecture_audio_sources = lecture.get("audio_sources")
|
||||
lecture_sources = lecture.get("video_sources")
|
||||
|
||||
if is_encrypted:
|
||||
if len(lecture_audio_sources) > 0 and len(lecture_video_sources) > 0:
|
||||
if len(lecture_sources) > 0:
|
||||
lecture_working_dir = os.path.join(working_dir,
|
||||
str(lecture.get("asset_id")))
|
||||
|
||||
if not os.path.isfile(lecture_path):
|
||||
video_source = lecture_video_sources[
|
||||
-1] # last index is the best quality
|
||||
audio_source = lecture_audio_sources[-1]
|
||||
source = lecture_sources[-1] # last index is the best quality
|
||||
if isinstance(quality, int):
|
||||
video_source = min(
|
||||
lecture_video_sources,
|
||||
source = min(
|
||||
lecture_sources,
|
||||
key=lambda x: abs(int(x.get("height")) - quality))
|
||||
if not os.path.exists(lecture_working_dir):
|
||||
os.mkdir(lecture_working_dir)
|
||||
print(f" > Lecture '%s' has DRM, attempting to download" %
|
||||
lecture_title)
|
||||
handle_segments(video_source, audio_source, lecture_title,
|
||||
lecture_working_dir, lecture_path)
|
||||
handle_segments(source.get("download_url"),
|
||||
source.get("format_id"), lecture_title,
|
||||
lecture_working_dir, lecture_path,
|
||||
concurrent_connections)
|
||||
else:
|
||||
print(
|
||||
" > Lecture '%s' is already downloaded, skipping..." %
|
||||
@ -1145,7 +1053,7 @@ def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
||||
else:
|
||||
print(f" > Lecture '%s' is missing media links" %
|
||||
lecture_title)
|
||||
print(len(lecture_audio_sources), len(lecture_video_sources))
|
||||
print(len(lecture_sources))
|
||||
else:
|
||||
sources = lecture.get("sources")
|
||||
sources = sorted(sources,
|
||||
@ -1173,8 +1081,14 @@ def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
||||
if source_type == "hls":
|
||||
temp_filepath = lecture_path.replace(".mp4", "")
|
||||
temp_filepath = temp_filepath + ".hls-part.mp4"
|
||||
retVal = FFMPEG(None, url, access_token,
|
||||
temp_filepath).download()
|
||||
# retVal = FFMPEG(None, url, access_token,
|
||||
# temp_filepath).download()
|
||||
ret_code = subprocess.Popen([
|
||||
"yt-dlp", "--force-generic-extractor",
|
||||
"--concurrent-fragments",
|
||||
f"{concurrent_connections}", "--downloader",
|
||||
"aria2c", "-o", f"{temp_filepath}", f"{url}"
|
||||
]).wait()
|
||||
if retVal:
|
||||
os.rename(temp_filepath, lecture_path)
|
||||
print(" > HLS Download success")
|
||||
@ -1191,7 +1105,7 @@ def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
||||
|
||||
|
||||
def parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
||||
caption_locale, keep_vtt, access_token):
|
||||
caption_locale, keep_vtt, access_token, concurrent_connections):
|
||||
total_chapters = _udemy.get("total_chapters")
|
||||
total_lectures = _udemy.get("total_lectures")
|
||||
print(f"Chapter(s) ({total_chapters})")
|
||||
@ -1234,9 +1148,11 @@ def parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
||||
continue
|
||||
else:
|
||||
lecture_path = os.path.join(
|
||||
chapter_dir, "{}.mp4".format(sanitize(lecture_title)))
|
||||
chapter_dir,
|
||||
sanitize(lecture_title) + ".mp4")
|
||||
process_lecture(lecture, lecture_path, chapter_dir,
|
||||
quality, access_token)
|
||||
quality, access_token,
|
||||
concurrent_connections)
|
||||
|
||||
if dl_assets:
|
||||
assets = lecture.get("assets")
|
||||
@ -1407,6 +1323,14 @@ if __name__ == "__main__":
|
||||
type=str,
|
||||
help="The language to download for captions, specify 'all' to download all captions (Default is 'en')",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-cd",
|
||||
"--concurrent-downloads",
|
||||
dest="concurrent_downloads",
|
||||
type=int,
|
||||
help=
|
||||
"The number of maximum concurrent downloads for segments (HLS and DASH, must be a number 1-30)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-lectures",
|
||||
dest="skip_lectures",
|
||||
@ -1467,6 +1391,7 @@ if __name__ == "__main__":
|
||||
course_name = None
|
||||
keep_vtt = False
|
||||
skip_hls = False
|
||||
concurrent_downloads = 10
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.download_assets:
|
||||
@ -1483,6 +1408,15 @@ if __name__ == "__main__":
|
||||
keep_vtt = args.keep_vtt
|
||||
if args.skip_hls:
|
||||
skip_hls = args.skip_hls
|
||||
if args.concurrent_downloads:
|
||||
concurrent_downloads = args.concurrent_downloads
|
||||
|
||||
if concurrent_downloads <= 0:
|
||||
# if the user gave a number that is less than or equal to 0, set cc to default of 10
|
||||
concurrent_downloads = 10
|
||||
elif concurrent_downloads > 30:
|
||||
# if the user gave a number thats greater than 30, set cc to the max of 30
|
||||
concurrent_downloads = 30
|
||||
|
||||
aria_ret_val = check_for_aria()
|
||||
if not aria_ret_val:
|
||||
@ -1559,7 +1493,8 @@ if __name__ == "__main__":
|
||||
course_info(_udemy)
|
||||
else:
|
||||
parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
||||
caption_locale, keep_vtt, access_token)
|
||||
caption_locale, keep_vtt, access_token,
|
||||
concurrent_downloads)
|
||||
else:
|
||||
_udemy = {}
|
||||
_udemy["access_token"] = access_token
|
||||
@ -1615,6 +1550,9 @@ if __name__ == "__main__":
|
||||
counter += 1
|
||||
|
||||
if lecture_id:
|
||||
print(
|
||||
f"Processing {course.index(entry)} of {len(course)}"
|
||||
)
|
||||
retVal = []
|
||||
|
||||
if isinstance(asset, dict):
|
||||
@ -1701,12 +1639,11 @@ if __name__ == "__main__":
|
||||
# encrypted
|
||||
data = asset.get("media_sources")
|
||||
if data and isinstance(data, list):
|
||||
video_media_sources, audio_media_sources = udemy._extract_media_sources(
|
||||
data)
|
||||
sources = udemy._extract_media_sources(data)
|
||||
tracks = asset.get("captions")
|
||||
# duration = asset.get("time_estimation")
|
||||
subtitles = udemy._extract_subtitles(tracks)
|
||||
sources_count = len(video_media_sources)
|
||||
sources_count = len(sources)
|
||||
subtitle_count = len(subtitles)
|
||||
lectures.append({
|
||||
"index": lecture_counter,
|
||||
@ -1716,8 +1653,7 @@ if __name__ == "__main__":
|
||||
# "duration": duration,
|
||||
"assets": retVal,
|
||||
"assets_count": len(retVal),
|
||||
"video_sources": video_media_sources,
|
||||
"audio_sources": audio_media_sources,
|
||||
"video_sources": sources,
|
||||
"subtitles": subtitles,
|
||||
"subtitle_count": subtitle_count,
|
||||
"sources_count": sources_count,
|
||||
@ -1792,4 +1728,5 @@ if __name__ == "__main__":
|
||||
course_info(_udemy)
|
||||
else:
|
||||
parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
||||
caption_locale, keep_vtt, access_token)
|
||||
caption_locale, keep_vtt, access_token,
|
||||
concurrent_downloads)
|
||||
|
277
pyffmpeg.py
277
pyffmpeg.py
@ -1,277 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# pylint: disable=R,C,W,E
|
||||
"""
|
||||
Author : Nasir Khan (r0ot h3x49)
|
||||
Github : https://github.com/r0oth3x49
|
||||
License : MIT
|
||||
Copyright (c) 2018-2025 Nasir Khan (r0ot h3x49)
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
|
||||
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
from colorama import Fore, Style
|
||||
|
||||
|
||||
class FFMPeg:
|
||||
|
||||
_PROGRESS_PATTERN = re.compile(
|
||||
r"(frame|fps|total_size|out_time|bitrate|speed|progress)\s*\=\s*(\S+)")
|
||||
|
||||
def __init__(self,
|
||||
duration,
|
||||
url,
|
||||
token,
|
||||
filepath,
|
||||
quiet=False,
|
||||
callback=lambda *x: None):
|
||||
self.url = url
|
||||
self.filepath = filepath
|
||||
self.quiet = quiet
|
||||
self.duration = duration
|
||||
self.callback = callback
|
||||
self.token = token
|
||||
|
||||
def _command(self):
|
||||
"""
|
||||
ffmpeg.exe -headers "Authorization: Bearer {token}" -i "" -c copy -bsf:a aac_adtstoasc out.mp4
|
||||
"""
|
||||
command = [
|
||||
"ffmpeg",
|
||||
"-headers",
|
||||
f"Authorization: Bearer {self.token}",
|
||||
"-i",
|
||||
f"{self.url}",
|
||||
"-c",
|
||||
"copy",
|
||||
"-bsf:a",
|
||||
"aac_adtstoasc",
|
||||
f"{self.filepath}",
|
||||
"-y",
|
||||
"-progress",
|
||||
"pipe:2",
|
||||
]
|
||||
return command
|
||||
|
||||
def _fetch_total_duration(self, line):
|
||||
duration_in_secs = 0
|
||||
duration_regex = re.compile(
|
||||
r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}")
|
||||
mobj = duration_regex.search(line)
|
||||
if mobj:
|
||||
duration_tuple = mobj.groups()
|
||||
duration_in_secs = (int(duration_tuple[0]) * 60 +
|
||||
int(duration_tuple[1]) * 60 +
|
||||
int(duration_tuple[2]))
|
||||
else:
|
||||
duration_in_secs = self.duration
|
||||
return duration_in_secs
|
||||
|
||||
def _fetch_current_duration_done(self, time_str):
|
||||
time_str = time_str.split(":")
|
||||
return (int(time_str[0]) * 60 + int(time_str[1]) * 60 +
|
||||
int(time_str[2].split(".")[0]))
|
||||
|
||||
def _prepare_time_str(self, secs):
|
||||
(mins, secs) = divmod(secs, 60)
|
||||
(hours, mins) = divmod(mins, 60)
|
||||
if hours > 99:
|
||||
time_str = "--:--:--"
|
||||
if hours == 0:
|
||||
time_str = "%02d:%02ds" % (mins, secs)
|
||||
else:
|
||||
time_str = "%02d:%02d:%02ds" % (hours, mins, secs)
|
||||
return time_str
|
||||
|
||||
def _progress(self,
|
||||
iterations,
|
||||
total,
|
||||
bytesdone,
|
||||
speed,
|
||||
elapsed,
|
||||
bar_length=30,
|
||||
fps=None):
|
||||
offset = 0
|
||||
filled_length = int(round(bar_length * iterations / float(total)))
|
||||
percents = format(100.00 * (iterations * 1.0 / float(total)), ".2f")
|
||||
|
||||
if bytesdone <= 1048576:
|
||||
_receiving = round(float(bytesdone) / 1024.00, 2)
|
||||
_received = format(
|
||||
_receiving if _receiving < 1024.00 else _receiving / 1024.00,
|
||||
".2f")
|
||||
suffix_recvd = "KB" if _receiving < 1024.00 else "MB"
|
||||
else:
|
||||
_receiving = round(float(bytesdone) / 1048576, 2)
|
||||
_received = format(
|
||||
_receiving if _receiving < 1024.00 else _receiving / 1024.00,
|
||||
".2f")
|
||||
suffix_recvd = "MB" if _receiving < 1024.00 else "GB"
|
||||
|
||||
suffix_rate = "Kb/s" if speed < 1024.00 else "Mb/s"
|
||||
if fps:
|
||||
suffix_rate += f" {fps}/fps"
|
||||
if elapsed:
|
||||
rate = ((float(iterations) - float(offset)) / 1024.0) / elapsed
|
||||
eta = (total - iterations) / (rate * 1024.0)
|
||||
else:
|
||||
rate = 0
|
||||
eta = 0
|
||||
rate = format(speed if speed < 1024.00 else speed / 1024.00, ".2f")
|
||||
(mins, secs) = divmod(eta, 60)
|
||||
(hours, mins) = divmod(mins, 60)
|
||||
if hours > 99:
|
||||
eta = "--:--:--"
|
||||
if hours == 0:
|
||||
eta = "eta %02d:%02ds" % (mins, secs)
|
||||
else:
|
||||
eta = "eta %02d:%02d:%02ds" % (hours, mins, secs)
|
||||
if secs == 0:
|
||||
eta = "\n"
|
||||
|
||||
total_time = self._prepare_time_str(total)
|
||||
done_time = self._prepare_time_str(iterations)
|
||||
downloaded = f"{total_time}/{done_time}"
|
||||
|
||||
received_bytes = str(_received) + str(suffix_recvd)
|
||||
percents = f"{received_bytes} {percents}"
|
||||
|
||||
self.hls_progress(
|
||||
downloaded=downloaded,
|
||||
percents=percents,
|
||||
filled_length=filled_length,
|
||||
rate=str(rate) + str(suffix_rate),
|
||||
suffix=eta,
|
||||
bar_length=bar_length,
|
||||
)
|
||||
|
||||
def hls_progress(self,
|
||||
downloaded,
|
||||
percents,
|
||||
filled_length,
|
||||
rate,
|
||||
suffix,
|
||||
bar_length=30):
|
||||
bar = (Fore.CYAN + Style.DIM + "#" * filled_length + Fore.WHITE +
|
||||
Style.DIM + "-" * (bar_length - filled_length))
|
||||
sys.stdout.write(
|
||||
"\033[2K\033[1G\r\r{}{}[{}{}*{}{}] : {}{}{} {}% |{}{}{}| {} {}".
|
||||
format(
|
||||
Fore.CYAN,
|
||||
Style.DIM,
|
||||
Fore.MAGENTA,
|
||||
Style.BRIGHT,
|
||||
Fore.CYAN,
|
||||
Style.DIM,
|
||||
Fore.GREEN,
|
||||
Style.BRIGHT,
|
||||
downloaded,
|
||||
percents,
|
||||
bar,
|
||||
Fore.GREEN,
|
||||
Style.BRIGHT,
|
||||
rate,
|
||||
suffix,
|
||||
))
|
||||
sys.stdout.flush()
|
||||
|
||||
def _parse_progress(self, line):
|
||||
items = {
|
||||
key: value
|
||||
for key, value in self._PROGRESS_PATTERN.findall(line)
|
||||
}
|
||||
return items
|
||||
|
||||
def download(self):
|
||||
total_time = None
|
||||
t0 = time.time()
|
||||
progress_lines = []
|
||||
active = True
|
||||
retVal = {}
|
||||
command = self._command()
|
||||
bytes_done = 0
|
||||
download_speed = 0
|
||||
try:
|
||||
with subprocess.Popen(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE) as proc:
|
||||
while active:
|
||||
elapsed = time.time() - t0
|
||||
try:
|
||||
line = proc.stderr.readline().decode("utf-8").strip()
|
||||
if not total_time:
|
||||
total_time = self._fetch_total_duration(line)
|
||||
if "progress=end" in line:
|
||||
try:
|
||||
self._progress(
|
||||
total_time,
|
||||
total_time,
|
||||
bytes_done,
|
||||
download_speed,
|
||||
elapsed,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
retVal = {
|
||||
"status": "False",
|
||||
"msg": "Error: KeyboardInterrupt",
|
||||
}
|
||||
raise KeyboardInterrupt
|
||||
except Exception as err:
|
||||
{"status": "False", "msg": f"Error: {err}"}
|
||||
active = False
|
||||
retVal = {"status": "True", "msg": "download"}
|
||||
break
|
||||
if "progress" not in line:
|
||||
progress_lines.append(line)
|
||||
else:
|
||||
lines = "\n".join(progress_lines)
|
||||
items = self._parse_progress(lines)
|
||||
if items:
|
||||
secs = self._fetch_current_duration_done(
|
||||
items.get("out_time"))
|
||||
_tsize = (
|
||||
items.get("total_size").lower().replace(
|
||||
"kb", ""))
|
||||
_brate = (items.get("bitrate").lower().replace(
|
||||
"kbits/s", ""))
|
||||
fps = items.get("fps")
|
||||
bytes_done = float(
|
||||
_tsize) if _tsize != "n/a" else 0
|
||||
download_speed = float(
|
||||
_brate) if _brate != "n/a" else 0
|
||||
try:
|
||||
self._progress(
|
||||
secs,
|
||||
total_time,
|
||||
bytes_done,
|
||||
download_speed,
|
||||
elapsed,
|
||||
fps=fps,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
retVal = {
|
||||
"status": "False",
|
||||
"msg": "Error: KeyboardInterrupt",
|
||||
}
|
||||
raise KeyboardInterrupt
|
||||
except Exception as err:
|
||||
{"status": "False", "msg": f"Error: {err}"}
|
||||
progress_lines = []
|
||||
except KeyboardInterrupt:
|
||||
active = False
|
||||
retVal = {
|
||||
"status": "False",
|
||||
"msg": "Error: KeyboardInterrupt"
|
||||
}
|
||||
raise KeyboardInterrupt
|
||||
except KeyboardInterrupt:
|
||||
raise KeyboardInterrupt
|
||||
return retVal
|
@ -7,4 +7,5 @@ protobuf
|
||||
webvtt-py
|
||||
pysrt
|
||||
m3u8
|
||||
colorama
|
||||
colorama
|
||||
yt-dlp
|
Loading…
x
Reference in New Issue
Block a user