mirror of
https://cdm-project.com/Download-Tools/udemy-downloader.git
synced 2025-05-03 09:54:26 +02:00
Merge branch 'Puyodead1:master' into master
This commit is contained in:
commit
2b3fb655ca
@ -66,8 +66,8 @@ You can now run the program, see the examples below. The course will download to
|
|||||||
# Advanced Usage
|
# Advanced Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [--skip-lectures] [--download-assets] [--download-captions]
|
usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [-cd CONCURRENT_DOWNLOADS] [--skip-lectures] [--download-assets]
|
||||||
[--keep-vtt] [--skip-hls] [--info]
|
[--download-captions] [--keep-vtt] [--skip-hls] [--info]
|
||||||
|
|
||||||
Udemy Downloader
|
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
|
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
|
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')
|
-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
|
--skip-lectures If specified, lectures won't be downloaded
|
||||||
--download-assets If specified, lecture assets will be downloaded
|
--download-assets If specified, lecture assets will be downloaded
|
||||||
--download-captions If specified, captions 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`
|
- `python main.py -c <Course URL> --skip-hls`
|
||||||
- Print course information only:
|
- Print course information only:
|
||||||
- `python main.py -c <Course URL> --info`
|
- `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
|
# Credits
|
||||||
|
|
||||||
|
366
main.py
366
main.py
@ -1,4 +1,15 @@
|
|||||||
import os, requests, json, glob, argparse, sys, re, time, asyncio, json, cloudscraper, m3u8
|
import os
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import cloudscraper
|
||||||
|
import m3u8
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from mpegdash.parser import MPEGDASHParser
|
from mpegdash.parser import MPEGDASHParser
|
||||||
@ -7,10 +18,9 @@ from vtt_to_srt import convert
|
|||||||
from requests.exceptions import ConnectionError as conn_error
|
from requests.exceptions import ConnectionError as conn_error
|
||||||
from html.parser import HTMLParser as compat_HTMLParser
|
from html.parser import HTMLParser as compat_HTMLParser
|
||||||
from sanitize import sanitize, slugify, SLUG_OK
|
from sanitize import sanitize, slugify, SLUG_OK
|
||||||
from pyffmpeg import FFMPeg as FFMPEG
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
home_dir = os.getcwd()
|
|
||||||
download_dir = os.path.join(os.getcwd(), "out_dir")
|
download_dir = os.path.join(os.getcwd(), "out_dir")
|
||||||
working_dir = os.path.join(os.getcwd(), "working_dir")
|
working_dir = os.path.join(os.getcwd(), "working_dir")
|
||||||
keyfile_path = os.path.join(os.getcwd(), "keyfile.json")
|
keyfile_path = os.path.join(os.getcwd(), "keyfile.json")
|
||||||
@ -27,7 +37,7 @@ LOGIN_URL = "https://www.udemy.com/join/login-popup/?ref=&display_type=popup&loc
|
|||||||
LOGOUT_URL = "https://www.udemy.com/user/logout"
|
LOGOUT_URL = "https://www.udemy.com/user/logout"
|
||||||
COURSE_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/cached-subscriber-curriculum-items?fields[asset]=results,title,external_url,time_estimation,download_urls,slide_urls,filename,asset_type,captions,media_license_token,course_is_drmed,media_sources,stream_urls,body&fields[chapter]=object_index,title,sort_order&fields[lecture]=id,title,object_index,asset,supplementary_assets,view_html&page_size=10000"
|
COURSE_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/cached-subscriber-curriculum-items?fields[asset]=results,title,external_url,time_estimation,download_urls,slide_urls,filename,asset_type,captions,media_license_token,course_is_drmed,media_sources,stream_urls,body&fields[chapter]=object_index,title,sort_order&fields[lecture]=id,title,object_index,asset,supplementary_assets,view_html&page_size=10000"
|
||||||
COURSE_SEARCH = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&page=1&page_size=500&search={course_name}"
|
COURSE_SEARCH = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&page=1&page_size=500&search={course_name}"
|
||||||
SUBSCRIBED_COURSES = "https://www.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-last_accessed&fields[course]=id,title,url&page=1&page_size=12"
|
SUBSCRIBED_COURSES = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-last_accessed&fields[course]=id,title,url&page=1&page_size=12"
|
||||||
MY_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&ordering=-last_accessed,-access_time&page=1&page_size=10000"
|
MY_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&ordering=-last_accessed,-access_time&page=1&page_size=10000"
|
||||||
COLLECTION_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses-collections/?collection_has_courses=True&course_limit=20&fields[course]=last_accessed_time,title,published_title&fields[user_has_subscribed_courses_collection]=@all&page=1&page_size=1000"
|
COLLECTION_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses-collections/?collection_has_courses=True&course_limit=20&fields[course]=last_accessed_time,title,published_title&fields[user_has_subscribed_courses_collection]=@all&page=1&page_size=1000"
|
||||||
|
|
||||||
@ -74,6 +84,7 @@ class Udemy:
|
|||||||
download_urls = entry.get("download_urls")
|
download_urls = entry.get("download_urls")
|
||||||
external_url = entry.get("external_url")
|
external_url = entry.get("external_url")
|
||||||
asset_type = entry.get("asset_type").lower()
|
asset_type = entry.get("asset_type").lower()
|
||||||
|
id = entry.get("id")
|
||||||
if asset_type == "file":
|
if asset_type == "file":
|
||||||
if download_urls and isinstance(download_urls, dict):
|
if download_urls and isinstance(download_urls, dict):
|
||||||
extension = filename.rsplit(
|
extension = filename.rsplit(
|
||||||
@ -85,6 +96,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"download_url": download_url,
|
"download_url": download_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
elif asset_type == "sourcecode":
|
elif asset_type == "sourcecode":
|
||||||
if download_urls and isinstance(download_urls, dict):
|
if download_urls and isinstance(download_urls, dict):
|
||||||
@ -98,6 +110,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"download_url": download_url,
|
"download_url": download_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
elif asset_type == "externallink":
|
elif asset_type == "externallink":
|
||||||
_temp.append({
|
_temp.append({
|
||||||
@ -106,6 +119,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": "txt",
|
"extension": "txt",
|
||||||
"download_url": external_url,
|
"download_url": external_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
@ -113,6 +127,7 @@ class Udemy:
|
|||||||
_temp = []
|
_temp = []
|
||||||
download_urls = assets.get("download_urls")
|
download_urls = assets.get("download_urls")
|
||||||
filename = assets.get("filename")
|
filename = assets.get("filename")
|
||||||
|
id = asset.get("id")
|
||||||
if download_urls and isinstance(download_urls, dict):
|
if download_urls and isinstance(download_urls, dict):
|
||||||
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
||||||
download_url = download_urls.get("Presentation", [])[0].get("file")
|
download_url = download_urls.get("Presentation", [])[0].get("file")
|
||||||
@ -121,6 +136,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"download_url": download_url,
|
"download_url": download_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
@ -128,6 +144,7 @@ class Udemy:
|
|||||||
_temp = []
|
_temp = []
|
||||||
download_urls = assets.get("download_urls")
|
download_urls = assets.get("download_urls")
|
||||||
filename = assets.get("filename")
|
filename = assets.get("filename")
|
||||||
|
id = asset.get("id")
|
||||||
if download_urls and isinstance(download_urls, dict):
|
if download_urls and isinstance(download_urls, dict):
|
||||||
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
||||||
download_url = download_urls.get("File", [])[0].get("file")
|
download_url = download_urls.get("File", [])[0].get("file")
|
||||||
@ -136,6 +153,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"download_url": download_url,
|
"download_url": download_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
@ -143,6 +161,7 @@ class Udemy:
|
|||||||
_temp = []
|
_temp = []
|
||||||
download_urls = assets.get("download_urls")
|
download_urls = assets.get("download_urls")
|
||||||
filename = assets.get("filename")
|
filename = assets.get("filename")
|
||||||
|
id = asset.get("id")
|
||||||
if download_urls and isinstance(download_urls, dict):
|
if download_urls and isinstance(download_urls, dict):
|
||||||
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
||||||
download_url = download_urls.get("E-Book", [])[0].get("file")
|
download_url = download_urls.get("E-Book", [])[0].get("file")
|
||||||
@ -151,6 +170,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"download_url": download_url,
|
"download_url": download_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
@ -158,6 +178,7 @@ class Udemy:
|
|||||||
_temp = []
|
_temp = []
|
||||||
download_urls = assets.get("download_urls")
|
download_urls = assets.get("download_urls")
|
||||||
filename = assets.get("filename")
|
filename = assets.get("filename")
|
||||||
|
id = asset.get("id")
|
||||||
if download_urls and isinstance(download_urls, dict):
|
if download_urls and isinstance(download_urls, dict):
|
||||||
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
extension = filename.rsplit(".", 1)[-1] if "." in filename else ""
|
||||||
download_url = download_urls.get("Audio", [])[0].get("file")
|
download_url = download_urls.get("Audio", [])[0].get("file")
|
||||||
@ -166,6 +187,7 @@ class Udemy:
|
|||||||
"filename": filename,
|
"filename": filename,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"download_url": download_url,
|
"download_url": download_url,
|
||||||
|
"id": id
|
||||||
})
|
})
|
||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
@ -214,19 +236,17 @@ class Udemy:
|
|||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
def _extract_media_sources(self, sources):
|
def _extract_media_sources(self, sources):
|
||||||
_audio = []
|
_temp = []
|
||||||
_video = []
|
|
||||||
if sources and isinstance(sources, list):
|
if sources and isinstance(sources, list):
|
||||||
for source in sources:
|
for source in sources:
|
||||||
_type = source.get("type")
|
_type = source.get("type")
|
||||||
src = source.get("src")
|
src = source.get("src")
|
||||||
|
|
||||||
if _type == "application/dash+xml":
|
if _type == "application/dash+xml":
|
||||||
video, audio = self._extract_mpd(src)
|
out = self._extract_mpd(src)
|
||||||
if video and audio:
|
if out:
|
||||||
_video.extend(video)
|
_temp.extend(out)
|
||||||
_audio.extend(audio)
|
return _temp
|
||||||
return (_video, _audio)
|
|
||||||
|
|
||||||
def _extract_subtitles(self, tracks):
|
def _extract_subtitles(self, tracks):
|
||||||
_temp = []
|
_temp = []
|
||||||
@ -285,83 +305,49 @@ class Udemy:
|
|||||||
return _temp
|
return _temp
|
||||||
|
|
||||||
def _extract_mpd(self, url):
|
def _extract_mpd(self, url):
|
||||||
"""extract mpd streams"""
|
"""extracts mpd streams"""
|
||||||
_video = []
|
_temp = []
|
||||||
_audio = []
|
|
||||||
try:
|
try:
|
||||||
resp = self.session._get(url)
|
ytdl = yt_dlp.YoutubeDL({
|
||||||
resp.raise_for_status()
|
'quiet': True,
|
||||||
raw_data = resp.text
|
'no_warnings': True,
|
||||||
mpd_object = MPEGDASHParser.parse(raw_data)
|
"allow_unplayable_formats": True
|
||||||
|
})
|
||||||
|
results = ytdl.extract_info(url,
|
||||||
|
download=False,
|
||||||
|
force_generic_extractor=True)
|
||||||
seen = set()
|
seen = set()
|
||||||
for period in mpd_object.periods:
|
formats = results.get("formats")
|
||||||
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
|
|
||||||
|
|
||||||
segment_extension = segment.media.split(
|
format_id = results.get("format_id")
|
||||||
".")[-1]
|
best_audio_format_id = format_id.split("+")[1]
|
||||||
height = rep.height
|
best_audio = next((x for x in formats
|
||||||
width = rep.width
|
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:
|
if height and height not in seen:
|
||||||
seen.add(height)
|
seen.add(height)
|
||||||
_video.append({
|
_temp.append({
|
||||||
"type":
|
"type": "dash",
|
||||||
"dash",
|
"height": str(height),
|
||||||
"content_type":
|
"width": str(width),
|
||||||
"video",
|
"format_id": f"{format_id},{best_audio_format_id}",
|
||||||
"height":
|
"extension": extension,
|
||||||
height,
|
"download_url": f.get("manifest_url")
|
||||||
"width":
|
})
|
||||||
width,
|
else:
|
||||||
"extension":
|
# unknown format type
|
||||||
segment_extension,
|
continue
|
||||||
"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
|
|
||||||
})
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"Udemy Says : '{error}' while fetching mpd manifest")
|
print(f"Error fetching MPD streams: '{error}'")
|
||||||
return (_video, _audio)
|
return _temp
|
||||||
|
|
||||||
def extract_course_name(self, url):
|
def extract_course_name(self, url):
|
||||||
"""
|
"""
|
||||||
@ -642,6 +628,7 @@ class Session(object):
|
|||||||
# Thanks to a great open source utility youtube-dl ..
|
# Thanks to a great open source utility youtube-dl ..
|
||||||
class HTMLAttributeParser(compat_HTMLParser): # pylint: disable=W
|
class HTMLAttributeParser(compat_HTMLParser): # pylint: disable=W
|
||||||
"""Trivial HTML parser to gather the attributes for a single element"""
|
"""Trivial HTML parser to gather the attributes for a single element"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.attrs = {}
|
self.attrs = {}
|
||||||
compat_HTMLParser.__init__(self)
|
compat_HTMLParser.__init__(self)
|
||||||
@ -796,7 +783,7 @@ if not os.path.exists(working_dir):
|
|||||||
if not os.path.exists(download_dir):
|
if not os.path.exists(download_dir):
|
||||||
os.makedirs(download_dir)
|
os.makedirs(download_dir)
|
||||||
|
|
||||||
#Get the keys
|
# Get the keys
|
||||||
with open(keyfile_path, 'r') as keyfile:
|
with open(keyfile_path, 'r') as keyfile:
|
||||||
keyfile = keyfile.read()
|
keyfile = keyfile.read()
|
||||||
keyfile = json.loads(keyfile)
|
keyfile = json.loads(keyfile)
|
||||||
@ -807,7 +794,7 @@ def durationtoseconds(period):
|
|||||||
@author Jayapraveen
|
@author Jayapraveen
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#Duration format in PTxDxHxMxS
|
# Duration format in PTxDxHxMxS
|
||||||
if (period[:2] == "PT"):
|
if (period[:2] == "PT"):
|
||||||
period = period[2:]
|
period = period[2:]
|
||||||
day = int(period.split("D")[0] if 'D' in period else 0)
|
day = int(period.split("D")[0] if 'D' in period else 0)
|
||||||
@ -841,24 +828,20 @@ def cleanup(path):
|
|||||||
os.removedirs(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
|
@author Jayapraveen
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
command = "ffmpeg -y -i \"{}\" -i \"{}\" -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title=\"{}\" \"{}\"".format(
|
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"),
|
video_filepath, audio_filepath, video_title, output_path)
|
||||||
os.path.join(lecture_working_dir, "decrypted_video.mp4"),
|
|
||||||
video_title, output_path)
|
|
||||||
else:
|
else:
|
||||||
command = "nice -n 7 ffmpeg -y -i \"{}\" -i \"{}\" -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title=\"{}\" \"{}\"".format(
|
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"),
|
video_filepath, audio_filepath, video_title, output_path)
|
||||||
os.path.join(lecture_working_dir, "decrypted_video.mp4"),
|
|
||||||
video_title, output_path)
|
|
||||||
os.system(command)
|
os.system(command)
|
||||||
|
|
||||||
|
|
||||||
def decrypt(kid, filename, lecture_working_dir):
|
def decrypt(kid, in_filepath, out_filepath):
|
||||||
"""
|
"""
|
||||||
@author Jayapraveen
|
@author Jayapraveen
|
||||||
"""
|
"""
|
||||||
@ -867,101 +850,49 @@ def decrypt(kid, filename, lecture_working_dir):
|
|||||||
key = keyfile[kid.lower()]
|
key = keyfile[kid.lower()]
|
||||||
if (os.name == "nt"):
|
if (os.name == "nt"):
|
||||||
os.system(f"mp4decrypt --key 1:%s \"%s\" \"%s\"" %
|
os.system(f"mp4decrypt --key 1:%s \"%s\" \"%s\"" %
|
||||||
(key,
|
(key, in_filepath, out_filepath))
|
||||||
os.path.join(lecture_working_dir,
|
|
||||||
"encrypted_{}.mp4".format(filename)),
|
|
||||||
os.path.join(lecture_working_dir,
|
|
||||||
"decrypted_{}.mp4".format(filename))))
|
|
||||||
else:
|
else:
|
||||||
os.system(f"nice -n 7 mp4decrypt --key 1:%s \"%s\" \"%s\"" %
|
os.system(f"nice -n 7 mp4decrypt --key 1:%s \"%s\" \"%s\"" %
|
||||||
(key,
|
(key, in_filepath, out_filepath))
|
||||||
os.path.join(lecture_working_dir,
|
|
||||||
"encrypted_{}.mp4".format(filename)),
|
|
||||||
os.path.join(lecture_working_dir,
|
|
||||||
"decrypted_{}.mp4".format(filename))))
|
|
||||||
print("> Decryption complete")
|
print("> Decryption complete")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError("Key not found")
|
raise KeyError("Key not found")
|
||||||
|
|
||||||
|
|
||||||
def handle_segments(video_source, audio_source, video_title,
|
def handle_segments(url, format_id, video_title, lecture_working_dir,
|
||||||
lecture_working_dir, output_path):
|
output_path, concurrent_connections):
|
||||||
"""
|
temp_filepath = output_path.replace("%", "").replace(".mp4", "")
|
||||||
@author Jayapraveen
|
temp_filepath = temp_filepath + ".mpd-part"
|
||||||
"""
|
video_filepath_enc = temp_filepath + ".mp4"
|
||||||
no_vid_segments = video_source.get("segment_count")
|
audio_filepath_enc = temp_filepath + ".m4a"
|
||||||
no_aud_segments = audio_source.get("segment_count")
|
video_filepath_dec = temp_filepath + ".decrypted.mp4"
|
||||||
|
audio_filepath_dec = temp_filepath + ".decrypted.m4a"
|
||||||
audio_media = audio_source.get("media")
|
print("> Downloading Lecture Tracks...")
|
||||||
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...")
|
|
||||||
ret_code = subprocess.Popen([
|
ret_code = subprocess.Popen([
|
||||||
"aria2c", "-i", list_path, "-j16", "-s20", "-x16", "-c",
|
"yt-dlp", "--force-generic-extractor", "--allow-unplayable-formats",
|
||||||
"--auto-file-renaming=false", "--summary-interval=0"
|
"--concurrent-fragments", f"{concurrent_connections}", "--downloader",
|
||||||
|
"aria2c", "--fixup", "never", "-k", "-o", f"{temp_filepath}.%(ext)s",
|
||||||
|
"-f", format_id, f"{url}"
|
||||||
]).wait()
|
]).wait()
|
||||||
print("> Lecture Segments Downloaded")
|
print("> Lecture Tracks Downloaded")
|
||||||
|
|
||||||
print("Return code: " + str(ret_code))
|
print("Return code: " + str(ret_code))
|
||||||
|
|
||||||
os.remove(list_path)
|
video_kid = extract_kid(video_filepath_enc)
|
||||||
|
|
||||||
video_kid = extract_kid(os.path.join(lecture_working_dir, "video_0.mp4"))
|
|
||||||
print("KID for video file is: " + video_kid)
|
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)
|
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:
|
try:
|
||||||
decrypt(video_kid, "video", lecture_working_dir)
|
decrypt(video_kid, video_filepath_enc, video_filepath_dec)
|
||||||
decrypt(audio_kid, "audio", lecture_working_dir)
|
decrypt(audio_kid, audio_filepath_enc, audio_filepath_dec)
|
||||||
os.chdir(home_dir)
|
mux_process(video_title, video_filepath_dec, audio_filepath_dec,
|
||||||
mux_process(video_title, lecture_working_dir, output_path)
|
output_path)
|
||||||
cleanup(lecture_working_dir)
|
os.remove(video_filepath_enc)
|
||||||
|
os.remove(audio_filepath_enc)
|
||||||
|
os.remove(video_filepath_dec)
|
||||||
|
os.remove(audio_filepath_dec)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: ", e)
|
print(f"Error: ", e)
|
||||||
|
|
||||||
@ -1089,31 +1020,31 @@ def process_caption(caption, lecture_title, lecture_dir, keep_vtt, tries=0):
|
|||||||
print(f" > Error converting caption: {e}")
|
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")
|
lecture_title = lecture.get("lecture_title")
|
||||||
is_encrypted = lecture.get("is_encrypted")
|
is_encrypted = lecture.get("is_encrypted")
|
||||||
lecture_video_sources = lecture.get("video_sources")
|
lecture_sources = lecture.get("video_sources")
|
||||||
lecture_audio_sources = lecture.get("audio_sources")
|
|
||||||
|
|
||||||
if is_encrypted:
|
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,
|
lecture_working_dir = os.path.join(working_dir,
|
||||||
str(lecture.get("asset_id")))
|
str(lecture.get("asset_id")))
|
||||||
|
|
||||||
if not os.path.isfile(lecture_path):
|
if not os.path.isfile(lecture_path):
|
||||||
video_source = lecture_video_sources[
|
source = lecture_sources[-1] # last index is the best quality
|
||||||
-1] # last index is the best quality
|
|
||||||
audio_source = lecture_audio_sources[-1]
|
|
||||||
if isinstance(quality, int):
|
if isinstance(quality, int):
|
||||||
video_source = min(
|
source = min(
|
||||||
lecture_video_sources,
|
lecture_sources,
|
||||||
key=lambda x: abs(int(x.get("height")) - quality))
|
key=lambda x: abs(int(x.get("height")) - quality))
|
||||||
if not os.path.exists(lecture_working_dir):
|
if not os.path.exists(lecture_working_dir):
|
||||||
os.mkdir(lecture_working_dir)
|
os.mkdir(lecture_working_dir)
|
||||||
print(f" > Lecture '%s' has DRM, attempting to download" %
|
print(f" > Lecture '%s' has DRM, attempting to download" %
|
||||||
lecture_title)
|
lecture_title)
|
||||||
handle_segments(video_source, audio_source, lecture_title,
|
handle_segments(source.get("download_url"),
|
||||||
lecture_working_dir, lecture_path)
|
source.get("format_id"), lecture_title,
|
||||||
|
lecture_working_dir, lecture_path,
|
||||||
|
concurrent_connections)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
" > Lecture '%s' is already downloaded, skipping..." %
|
" > Lecture '%s' is already downloaded, skipping..." %
|
||||||
@ -1121,7 +1052,7 @@ def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
|||||||
else:
|
else:
|
||||||
print(f" > Lecture '%s' is missing media links" %
|
print(f" > Lecture '%s' is missing media links" %
|
||||||
lecture_title)
|
lecture_title)
|
||||||
print(len(lecture_audio_sources), len(lecture_video_sources))
|
print(len(lecture_sources))
|
||||||
else:
|
else:
|
||||||
sources = lecture.get("sources")
|
sources = lecture.get("sources")
|
||||||
sources = sorted(sources,
|
sources = sorted(sources,
|
||||||
@ -1149,8 +1080,14 @@ def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
|||||||
if source_type == "hls":
|
if source_type == "hls":
|
||||||
temp_filepath = lecture_path.replace(".mp4", "")
|
temp_filepath = lecture_path.replace(".mp4", "")
|
||||||
temp_filepath = temp_filepath + ".hls-part.mp4"
|
temp_filepath = temp_filepath + ".hls-part.mp4"
|
||||||
retVal = FFMPEG(None, url, access_token,
|
# retVal = FFMPEG(None, url, access_token,
|
||||||
temp_filepath).download()
|
# 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:
|
if retVal:
|
||||||
os.rename(temp_filepath, lecture_path)
|
os.rename(temp_filepath, lecture_path)
|
||||||
print(" > HLS Download success")
|
print(" > HLS Download success")
|
||||||
@ -1167,7 +1104,7 @@ def process_lecture(lecture, lecture_path, lecture_dir, quality, access_token):
|
|||||||
|
|
||||||
|
|
||||||
def parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
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_chapters = _udemy.get("total_chapters")
|
||||||
total_lectures = _udemy.get("total_lectures")
|
total_lectures = _udemy.get("total_lectures")
|
||||||
print(f"Chapter(s) ({total_chapters})")
|
print(f"Chapter(s) ({total_chapters})")
|
||||||
@ -1210,9 +1147,11 @@ def parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
lecture_path = os.path.join(
|
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,
|
process_lecture(lecture, lecture_path, chapter_dir,
|
||||||
quality, access_token)
|
quality, access_token,
|
||||||
|
concurrent_connections)
|
||||||
|
|
||||||
if dl_assets:
|
if dl_assets:
|
||||||
assets = lecture.get("assets")
|
assets = lecture.get("assets")
|
||||||
@ -1223,6 +1162,7 @@ def parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
|||||||
asset_type = asset.get("type")
|
asset_type = asset.get("type")
|
||||||
filename = asset.get("filename")
|
filename = asset.get("filename")
|
||||||
download_url = asset.get("download_url")
|
download_url = asset.get("download_url")
|
||||||
|
asset_id = asset.get("id")
|
||||||
|
|
||||||
if asset_type == "article":
|
if asset_type == "article":
|
||||||
print(
|
print(
|
||||||
@ -1246,7 +1186,8 @@ def parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
|||||||
print("AssetType: Video; AssetData: ", asset)
|
print("AssetType: Video; AssetData: ", asset)
|
||||||
elif asset_type == "audio" or asset_type == "e-book" or asset_type == "file" or asset_type == "presentation":
|
elif asset_type == "audio" or asset_type == "e-book" or asset_type == "file" or asset_type == "presentation":
|
||||||
try:
|
try:
|
||||||
download_aria(download_url, chapter_dir, filename)
|
download_aria(download_url, chapter_dir,
|
||||||
|
f"{asset_id}-{filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("> Error downloading asset: ", e)
|
print("> Error downloading asset: ", e)
|
||||||
continue
|
continue
|
||||||
@ -1372,16 +1313,22 @@ if __name__ == "__main__":
|
|||||||
"--quality",
|
"--quality",
|
||||||
dest="quality",
|
dest="quality",
|
||||||
type=int,
|
type=int,
|
||||||
help=
|
help="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",
|
||||||
"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",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-l",
|
"-l",
|
||||||
"--lang",
|
"--lang",
|
||||||
dest="lang",
|
dest="lang",
|
||||||
type=str,
|
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=
|
help=
|
||||||
"The language to download for captions, specify 'all' to download all captions (Default is 'en')",
|
"The number of maximum concurrent downloads for segments (HLS and DASH, must be a number 1-30)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--skip-lectures",
|
"--skip-lectures",
|
||||||
@ -1411,15 +1358,13 @@ if __name__ == "__main__":
|
|||||||
"--skip-hls",
|
"--skip-hls",
|
||||||
dest="skip_hls",
|
dest="skip_hls",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help=
|
help="If specified, hls streams will be skipped (faster fetching) (hls streams usually contain 1080p quality for non-drm lectures)",
|
||||||
"If specified, hls streams will be skipped (faster fetching) (hls streams usually contain 1080p quality for non-drm lectures)",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--info",
|
"--info",
|
||||||
dest="info",
|
dest="info",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help=
|
help="If specified, only course information will be printed, nothing will be downloaded",
|
||||||
"If specified, only course information will be printed, nothing will be downloaded",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -1445,6 +1390,7 @@ if __name__ == "__main__":
|
|||||||
course_name = None
|
course_name = None
|
||||||
keep_vtt = False
|
keep_vtt = False
|
||||||
skip_hls = False
|
skip_hls = False
|
||||||
|
concurrent_downloads = 10
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.download_assets:
|
if args.download_assets:
|
||||||
@ -1461,13 +1407,22 @@ if __name__ == "__main__":
|
|||||||
keep_vtt = args.keep_vtt
|
keep_vtt = args.keep_vtt
|
||||||
if args.skip_hls:
|
if args.skip_hls:
|
||||||
skip_hls = 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()
|
aria_ret_val = check_for_aria()
|
||||||
if not aria_ret_val:
|
if not aria_ret_val:
|
||||||
print("> Aria2c is missing from your system or path!")
|
print("> Aria2c is missing from your system or path!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
ffmpeg_ret_val = check_for_aria()
|
ffmpeg_ret_val = check_for_ffmpeg()
|
||||||
if not ffmpeg_ret_val:
|
if not ffmpeg_ret_val:
|
||||||
print("> FFMPEG is missing from your system or path!")
|
print("> FFMPEG is missing from your system or path!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -1537,7 +1492,8 @@ if __name__ == "__main__":
|
|||||||
course_info(_udemy)
|
course_info(_udemy)
|
||||||
else:
|
else:
|
||||||
parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
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:
|
else:
|
||||||
_udemy = {}
|
_udemy = {}
|
||||||
_udemy["access_token"] = access_token
|
_udemy["access_token"] = access_token
|
||||||
@ -1593,6 +1549,9 @@ if __name__ == "__main__":
|
|||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
if lecture_id:
|
if lecture_id:
|
||||||
|
print(
|
||||||
|
f"Processing {course.index(entry)} of {len(course)}"
|
||||||
|
)
|
||||||
retVal = []
|
retVal = []
|
||||||
|
|
||||||
if isinstance(asset, dict):
|
if isinstance(asset, dict):
|
||||||
@ -1679,12 +1638,11 @@ if __name__ == "__main__":
|
|||||||
# encrypted
|
# encrypted
|
||||||
data = asset.get("media_sources")
|
data = asset.get("media_sources")
|
||||||
if data and isinstance(data, list):
|
if data and isinstance(data, list):
|
||||||
video_media_sources, audio_media_sources = udemy._extract_media_sources(
|
sources = udemy._extract_media_sources(data)
|
||||||
data)
|
|
||||||
tracks = asset.get("captions")
|
tracks = asset.get("captions")
|
||||||
# duration = asset.get("time_estimation")
|
# duration = asset.get("time_estimation")
|
||||||
subtitles = udemy._extract_subtitles(tracks)
|
subtitles = udemy._extract_subtitles(tracks)
|
||||||
sources_count = len(video_media_sources)
|
sources_count = len(sources)
|
||||||
subtitle_count = len(subtitles)
|
subtitle_count = len(subtitles)
|
||||||
lectures.append({
|
lectures.append({
|
||||||
"index": lecture_counter,
|
"index": lecture_counter,
|
||||||
@ -1694,8 +1652,7 @@ if __name__ == "__main__":
|
|||||||
# "duration": duration,
|
# "duration": duration,
|
||||||
"assets": retVal,
|
"assets": retVal,
|
||||||
"assets_count": len(retVal),
|
"assets_count": len(retVal),
|
||||||
"video_sources": video_media_sources,
|
"video_sources": sources,
|
||||||
"audio_sources": audio_media_sources,
|
|
||||||
"subtitles": subtitles,
|
"subtitles": subtitles,
|
||||||
"subtitle_count": subtitle_count,
|
"subtitle_count": subtitle_count,
|
||||||
"sources_count": sources_count,
|
"sources_count": sources_count,
|
||||||
@ -1770,4 +1727,5 @@ if __name__ == "__main__":
|
|||||||
course_info(_udemy)
|
course_info(_udemy)
|
||||||
else:
|
else:
|
||||||
parse_new(_udemy, quality, skip_lectures, dl_assets, dl_captions,
|
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
|
webvtt-py
|
||||||
pysrt
|
pysrt
|
||||||
m3u8
|
m3u8
|
||||||
colorama
|
colorama
|
||||||
|
yt-dlp
|
Loading…
x
Reference in New Issue
Block a user