Initial commit
This commit is contained in:
commit
d088d935f0
68
README.md
Normal file
68
README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Downloader for Linux Academy
|
||||||
|
|
||||||
|
This script will download courses from [Linux Academy](https://linuxacademy.com) for offline consumption.
|
||||||
|
|
||||||
|
## Important Notice
|
||||||
|
|
||||||
|
**Use of this script is for personal consumption of content only.** The content this script downloads is protected by copyright and must not be shared.
|
||||||
|
|
||||||
|
#### Good uses
|
||||||
|
|
||||||
|
* Downloading a lesson before embarking to a destination with little or no internet access.
|
||||||
|
* Keep lessons you've completed for a personal backup.
|
||||||
|
|
||||||
|
#### Bad uses
|
||||||
|
|
||||||
|
* Uploading the downloaded videos to a content sharing site.
|
||||||
|
* Sending copies of the videos to your friends and family.
|
||||||
|
* Hoarding lessons for future consumption beyond your subscription period.
|
||||||
|
|
||||||
|
Please exercise good judgement when using this script. The folks at Linux Academy work hard to make quality courses and you should support them by paying for a subscription if you can. You may also wish to speak with your employer to find out if they would be willing to pay for your subscription.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Tested on a fresh install of Ubuntu 18.04 desktop. Your mileage may vary depending on your OS.
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-pip git unzip ffmpeg
|
||||||
|
sudo pip3 install selenium youtube-dl
|
||||||
|
|
||||||
|
### Browser
|
||||||
|
|
||||||
|
You will need Chrome or Firefox and its matching driver.
|
||||||
|
|
||||||
|
#### Chrome
|
||||||
|
|
||||||
|
Install [Google Chrome](https://www.google.com/chrome/) and download the appropriate [ChromeDriver](https://chromedriver.chromium.org/downloads) version. Make sure the `chromedriver` executable is in your PATH (e.g., `/usr/local/bin`).
|
||||||
|
|
||||||
|
sudo dpkg -i google-chrome-stable_current_amd64.deb
|
||||||
|
unzip chromedriver_linux64.zip
|
||||||
|
sudo mv chromedriver /usr/local/bin
|
||||||
|
|
||||||
|
#### Firefox
|
||||||
|
|
||||||
|
Install Mozilla Firefox and download [geckodriver](https://github.com/mozilla/geckodriver/releases).
|
||||||
|
|
||||||
|
sudo apt install firefox
|
||||||
|
tar xzf geckodriver-*-linux64.tar.gz
|
||||||
|
sudo mv geckodriver /usr/local/bin
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This will only work with an active Linux Academy subscription. If you do not have one, please get one [here](https://linuxacademy.com/pricing/).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./linuxacademy-dl.py [-u|--username] [-p|--password] [-d|--download-dir] [-c|--cookies-file] COURSE_URL
|
||||||
|
|
||||||
|
Options may be replaced with environment variables. Command line options take precedence.
|
||||||
|
LADL_USERNAME, LADL_PASSWORD, LADL_DIR, LADL_COOKIES
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$ ./linuxacademy-dl.py -u person@exmple.com -p p@ssw0rd https://linuxacademy.com/cp/modules/view/id/346
|
||||||
|
|
||||||
|
$ export LADL_USERNAME=person@example.com
|
||||||
|
$ export LADL_PASSWORD=p@ssw0rd
|
||||||
|
$ export LADL_DIR=/home/jdoe/linux-academy
|
||||||
|
$ ./linuxacademy-dl.py https://linuxacademy.com/cp/modules/view/id/346
|
||||||
|
|
||||||
|
The username/email and password fields are required. The cookie file will opt to `$PWD/cookies.txt` and the download directory will default to `$SCRIPT_DIR/download`, where `$SCRIPT_DIR` is the path of the `linuxacademy-dl.py` script.
|
215
linuxacademy-dl.py
Normal file
215
linuxacademy-dl.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import getopt
|
||||||
|
import shutil
|
||||||
|
import youtube_dl
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
|
||||||
|
def usage(error=0, msg=None):
|
||||||
|
if msg:
|
||||||
|
print(msg)
|
||||||
|
print("Usage:")
|
||||||
|
print("\t{} [-u|--username] [-p|--password] [-d|--download-dir] [-c|--cookies-file] COURSE_URL\n"
|
||||||
|
.format(sys.argv[0]))
|
||||||
|
print("\tOptions may be replaced with environment variables. Command line options take precedence.")
|
||||||
|
print("\t\tLADL_USERNAME, LADL_PASSWORD, LADL_DIR, LADL_COOKIES\n")
|
||||||
|
print("Examples:")
|
||||||
|
print("\t$ ./linuxacademy-dl.py -u person@exmple.com -p p@ssw0rd https://linuxacademy.com/cp/modules/view/id/346\n")
|
||||||
|
print("\t$ export LADL_USERNAME=person@example.com")
|
||||||
|
print("\t$ export LADL_PASSWORD=p@ssw0rd")
|
||||||
|
print("\t$ export LADL_DIR=/home/jdoe/linux-academy")
|
||||||
|
print("\t$ ./linuxacademy-dl.py https://linuxacademy.com/cp/modules/view/id/346")
|
||||||
|
exit(error)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
opts = args = []
|
||||||
|
username = password = download_dir = cookies_file = None
|
||||||
|
ignore_missing_title = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], 'hu:p:d:c:',
|
||||||
|
['help', 'username=', 'password=', 'download-dir=', 'cookies-file=',
|
||||||
|
'ignore-missing-title'])
|
||||||
|
except getopt.GetoptError as err:
|
||||||
|
print(str(err))
|
||||||
|
usage(1)
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
usage(1, "Missing course URL.")
|
||||||
|
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt in ('-h', '--help'):
|
||||||
|
usage()
|
||||||
|
elif opt in ('-u', '--username'):
|
||||||
|
username = arg
|
||||||
|
elif opt in ('-p', '--password'):
|
||||||
|
password = arg
|
||||||
|
elif opt in ('-d', '--download-dir'):
|
||||||
|
download_dir = arg
|
||||||
|
elif opt in ('-c', '--cookies-file'):
|
||||||
|
cookies_file = arg
|
||||||
|
elif opt == '--ignore-missing-title':
|
||||||
|
ignore_missing_title = True
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
username = os.environ.get('LADL_USERNAME') or usage(1, "Missing username.")
|
||||||
|
if password is None:
|
||||||
|
password = os.environ.get('LADL_PASSWORD') or usage(1, "Missing password.")
|
||||||
|
if download_dir is None:
|
||||||
|
download_dir = os.environ.get('LADL_DIR', "{}/download".format(os.path.dirname(os.path.realpath(__file__))))
|
||||||
|
if cookies_file is None:
|
||||||
|
cookies_file = os.environ.get('LADL_COOKIES', "{}/cookies.txt".format(os.path.dirname(os.path.realpath(__file__))))
|
||||||
|
|
||||||
|
course_url = args[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'download_dir': download_dir,
|
||||||
|
'cookies_file': cookies_file,
|
||||||
|
'course_url': course_url,
|
||||||
|
'ignore_missing_title': ignore_missing_title
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def launch_browser(name):
|
||||||
|
if name == 'chrome':
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
options.add_argument("--headless")
|
||||||
|
options.add_argument("--window-size=1920,1080")
|
||||||
|
driver = webdriver.Chrome(options=options)
|
||||||
|
else:
|
||||||
|
options = webdriver.FirefoxOptions()
|
||||||
|
options.add_argument("--headless")
|
||||||
|
options.add_argument("--window-size=1920,1080")
|
||||||
|
driver = webdriver.Firefox(options=options)
|
||||||
|
|
||||||
|
driver.get("https://linuxacademy.com/")
|
||||||
|
return driver
|
||||||
|
|
||||||
|
|
||||||
|
def la_login(driver, args):
|
||||||
|
link = driver.find_element_by_partial_link_text('Log In')
|
||||||
|
link.click()
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
user = driver.find_element_by_name('username')
|
||||||
|
user.send_keys(args['username'])
|
||||||
|
password = driver.find_element_by_name('password')
|
||||||
|
password.send_keys(args['password'])
|
||||||
|
password.send_keys(Keys.RETURN)
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
driver.find_element_by_id('navigationUsername')
|
||||||
|
print("Login success.")
|
||||||
|
except NoSuchElementException:
|
||||||
|
print("Login failed. Exiting.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def load_course(driver, args):
|
||||||
|
course_title = None
|
||||||
|
driver.get(args['course_url'])
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
course_title = driver.find_elements_by_class_name('course-title')[0].text
|
||||||
|
except IndexError:
|
||||||
|
if args['ignore_missing_title']:
|
||||||
|
course_title = "COURSE-ID-{}".format(args['course_url'].split('/')[-1])
|
||||||
|
else:
|
||||||
|
print("Error: Could not find course title. Try running with --ignore-missing-title.")
|
||||||
|
exit(3)
|
||||||
|
|
||||||
|
return course_title
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_video_list(driver):
|
||||||
|
lessons = driver.find_elements_by_tag_name('a')
|
||||||
|
video_list = []
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
for lesson in lessons:
|
||||||
|
try:
|
||||||
|
url = lesson.get_attribute('href')
|
||||||
|
if '/course/' in url:
|
||||||
|
counter += 1
|
||||||
|
video_list.append(
|
||||||
|
{'url': url, 'counter': "{:03d}".format(counter), 'title': lesson.text.split('\n')[0]})
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return video_list
|
||||||
|
|
||||||
|
|
||||||
|
def write_cookies(driver, file):
|
||||||
|
cookies = open(file, 'a+')
|
||||||
|
cookies.write("# Netscape HTTP Cookie File\n\n")
|
||||||
|
|
||||||
|
for c in driver.get_cookies():
|
||||||
|
expiry = c.get('expiry') if c.get('expiry') else 0
|
||||||
|
any_domain_flag = str(c['domain'].startswith('.')).upper()
|
||||||
|
cookie = "{}\t{}\t{}\t{}\t{}\t{}\t{}\n".format(
|
||||||
|
c['domain'], any_domain_flag, c['path'], str(c['secure']).upper(), expiry, c['name'], c['value'])
|
||||||
|
cookies.write(cookie)
|
||||||
|
|
||||||
|
cookies.close()
|
||||||
|
|
||||||
|
|
||||||
|
def download_video(driver, args, course_title, video):
|
||||||
|
driver.get(video['url'])
|
||||||
|
time.sleep(5)
|
||||||
|
write_cookies(driver, args['cookies_file'])
|
||||||
|
|
||||||
|
file_name = "{} - {}".format(video['counter'], video['title'].replace('/', '_'))
|
||||||
|
print("Downloading: {}...".format(file_name))
|
||||||
|
|
||||||
|
ydl_opts = {'cookiefile': args['cookies_file'], 'force_generic_extractor': True, 'quiet': True, 'no_warnings': True,
|
||||||
|
'outtmpl': '{}/{}/{}.%(ext)s'.format(args['download_dir'], course_title, file_name),
|
||||||
|
'restrictfilenames': True}
|
||||||
|
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
ydl.download([video['url']])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
if shutil.which('chromedriver'):
|
||||||
|
print("Launching Chrome...")
|
||||||
|
driver = launch_browser('chrome')
|
||||||
|
elif shutil.which('geckodriver'):
|
||||||
|
print("Launching Firefox...")
|
||||||
|
driver = launch_browser('firefox')
|
||||||
|
else:
|
||||||
|
print("Error: No browser driver found.")
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
print("Logging in...")
|
||||||
|
la_login(driver, args)
|
||||||
|
|
||||||
|
print("Loading course...")
|
||||||
|
course_title = load_course(driver, args)
|
||||||
|
print("Course title: {}".format(course_title))
|
||||||
|
|
||||||
|
print("Fetching video list...")
|
||||||
|
video_list = fetch_video_list(driver)
|
||||||
|
print("Found {} video{}.".format(len(video_list), "s" if len(video_list) > 1 else ""))
|
||||||
|
|
||||||
|
for video in video_list:
|
||||||
|
download_video(driver, args, course_title, video)
|
||||||
|
|
||||||
|
os.remove(args['cookies_file'])
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in New Issue
Block a user