diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7548acf --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +course.txt +pack.sublime-project +pack.sublime-workspace +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/AUTHOR.md b/AUTHOR.md new file mode 100644 index 0000000..50f087c --- /dev/null +++ b/AUTHOR.md @@ -0,0 +1,4 @@ +# *Nasir Khan (r0ot h3x49)* + + - **https://r0oth3x49.herokuapp.com** + - **https://github.com/r0oth3x49** \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..483352f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Change Log + + +## 0.2 (2018-06-04) + +Features: + - Download specific quality for course videos (option: `-q / --quality`). + - Get user input if no credentials provided using command line argument. + - List down all available resolution for a video in a course (option: `--info`). + - Download multiple courses by providing text file containing list of courses. + +Bugfixes: + - ZeroDivision error fixed. + + +## 0.1 (2017-08-29) + +Features: + - Resume capability for a course video. + - Downloads all available subtitles if any attached with video. + - Saves course to user provided path (directory), default is current directory (option: `-d / --directory`). + - Skip captions/subtitle and download course only (option: `--skip-sub`). + - Download captions/subtitle only thanks to @leo459028 (option: `--sub-only`). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c42d8f --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + +Copyright (c) 2018 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. \ No newline at end of file diff --git a/README.md b/README.md index a4b150f..385f5fd 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,153 @@ +[![GitHub stars](https://img.shields.io/github/stars/r0oth3x49/lynda-dl.svg?style=flat-square)](https://github.com/r0oth3x49/lynda-dl/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/r0oth3x49/lynda-dl.svg?style=flat-square)](https://github.com/r0oth3x49/lynda-dl/network) +[![GitHub issues](https://img.shields.io/github/issues/r0oth3x49/lynda-dl.svg?style=flat-square)](https://github.com/r0oth3x49/lynda-dl/issues) + + # lynda-dl -**A cross-platform python based utility to download courses from lynda.com for personal offline use.** +**A cross-platform python based utility to download courses from lynda for personal offline use.** + +[![lynda.png](https://s26.postimg.cc/bsm316qax/lynda.png)](https://postimg.cc/image/8lrjhk5ut/) + +## ***Features*** + +- Resume capability for a course video. +- Supports organization and individual lynda users both. +- List down course contents and video resolution, suggest the best resolution (option: `--info`). +- Download/skip all available subtitles for a video (options: `--skip-sub, --skip-sub`). +- Download lecture(s) requested resolution (option: `-q / --quality`). +- Download course to user requested path (option: `-d / --directory`). + +## ***Issue Reporting Guideline*** + +To maintain an effective bugfix workflow and make sure issues will be solved, I ask reporters to follow some simple guidelines. + +Before creating an issue, please do the following: + +1. **Use the GitHub issue search** — check if the issue has already been reported. +2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` in the repository. +3. Make sure, that information you are about to report is related to this repository + and not the one available ***Python's repository***, Because this repository cannot be downloaded via pip. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What was the course url? What steps will reproduce the issue? What OS +experience the problem? All these details will help to fix any potential bugs as soon as possible. -[![lynda-dl.png](https://s26.postimg.cc/upmyj37tl/lynda-dl.png)](https://postimg.cc/image/937y22991/) +### ***Example:*** -### Requirements +> Short and descriptive example bug report title +> +> A summary of the issue and the OS environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a lynda course link to reproduce the error. +> +> Any other information you want to share that is relevant to the issue being reported. + +## ***Requirements*** - Python (2 or 3) - Python `pip` - Python module `requests` - Python module `colorama` +- Python module `unidecode` +- Python module `six` +- Python module `requests[security]` or `pyOpenSSL` -### Install modules +## ***Module Installation*** pip install -r requirements.txt -### Tested on +## ***Tested on*** -- Windows 7/8/8.1 +- Windows 7/8/8.1/10 - Kali linux (2017.2) - - -### Download lynda-dl +- Ubuntu-LTS (64-bit) (tested with super user) +- Mac OSX 10.9.5 (tested with super user) + +## ***Download lynda-dl*** You can download the latest version of lynda-dl by cloning the GitHub repository. git clone https://github.com/r0oth3x49/lynda-dl.git -### Usage +## ***Usage*** ***Download course using user credentials*** - python lynda-dl.py https://www.lynda.com/COURSE_NAME/ID.html - + python lynda-dl.py https://www.lynda.com/COURSE_NAME/ID.html + ***OR*** - python lynda-dl.py -u user@domain.com -p p4ssw0rd https://www.lynda.com/COURSE_NAME/ID.html - + python lynda-dl.py -u user@domain.com -p p4ssw0rd https://www.lynda.com/COURSE_NAME/ID.html + ***Download course using organization's library card*** - python lynda-dl.py -o organization https://www.lynda.com/COURSE_NAME/ID.html - + python lynda-dl.py -o organization https://www.lynda.com/COURSE_NAME/ID.html + ***OR*** - python lynda-dl.py -u library_card_num -p library_card_pin -o organization https://www.lynda.com/COURSE_NAME/ID.html - - + python lynda-dl.py -u library_card_num -p library_card_pin -o organization https://www.lynda.com/COURSE_NAME/ID.html + + ***Download course to a specific location using user credentials*** - python lynda-dl.py https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" - + python lynda-dl.py https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" + ***OR*** - python lynda-dl.py -u user@domain.com -p p4ssw0rd https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" + python lynda-dl.py -u user@domain.com -p p4ssw0rd https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" - + ***Download course to a specific location using organization's library card*** - python lynda-dl.py -o organization https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" - + python lynda-dl.py -o organization https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" + ***OR*** - python lynda-dl.py -u library_card_num -p library_card_pin -o organization https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" - + python lynda-dl.py -u library_card_num -p library_card_pin -o organization https://www.lynda.com/COURSE_NAME/ID.html -d "/path/to/directory/" + -### Advanced Usage +## **Advanced Usage**

 Author: Nasir khan (r0ot h3x49)
 
-Usage: lynda-dl.py [-u (USERNAME/LIBRARY CARD NUMBER)][-p (PASSWORD/LIBRARY CARD PIN)]
-                   [-o ORGANIZATION] COURSE_URL [-s/--sub-only] [-d DIRECTORY]
-
-Options:
-  General:
-    -h, --help          Shows the help for program.
-    -v, --version       Shows the version of program.
-
-  Advance:
-    -u, --username      Username or Library Card Number.
-    -p, --password      Password or Library Card Pin.
-    -o, --organization  Organization, registered at Lynda.
-    -s, --sub-only      Download the captions/subtitle only
-    -d, --directory     Output directory where the videos will be saved,
-                        default is current directory.
-  
-  Example:
-	python lynda-dl.py https://www.lynda.com/course_name/id.html
-
+usage: lynda-dl.py [-h] [-v] [-u] [-p] [-o] [-d] [-q] [--info] [--sub-only] + [--skip-sub] + course + +A cross-platform python based utility to download courses from lynda for +personal offline use. + +positional arguments: + course Lynda course or file containing list of courses. + +General: + -h, --help Shows the help. + -v, --version Shows the version. +Authentication: + -u , --username Username or Library Card Number. + -p , --password Password or Library Card Pin. + -o , --organization Organization, registered at Lynda. -### Note -
Do not change the position of any argument as given under the Usage, this may cause an error or failur in downloading of course.
+Advance: + -d , --directory Download to specific directory. + -q , --quality Download specific video quality. + +Others: + --info List all lectures with available resolution. + --sub-only Download captions/subtitle only. + --skip-sub Download course but skip captions/subtitle. + +Example: + python lynda-dl.py COURSE_URL + python lynda-dl.py -o organization COURSE_URL + + diff --git a/lynda-dl.py b/lynda-dl.py index 9f2418a..f07fece 100644 --- a/lynda-dl.py +++ b/lynda-dl.py @@ -1,452 +1,348 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from __future__ import unicode_literals import os -import re +import sys import time import lynda -import optparse +import argparse -from sys import * from pprint import pprint - -from lynda.colorized import * +from lynda import __version__ +from lynda._colorized import * from lynda._compat import pyver -from lynda.colorized.banner import banner - - -getpass = lynda.GetPass() -course_dl = lynda.Downloader() -extract_info = lynda.LyndaInfoExtractor() - - - -class LyndaDownload: - - def __init__(self, url, user, passw, org=None): - self.url = url - self.user = user - self.passw = passw - self.org = org - - def login(self): - if self.org is not None: - extract_info.login(self.user, self.passw, self.org) - else: - extract_info.login(self.user, self.passw) - - def logout(self): - extract_info.logout() - - def generate_filename(self, title): - ok = re.compile(r'[^/]') - - if os.name == "nt": - ok = re.compile(r'[^\\/:*?"<>|]') - - filename = "".join(x if ok.match(x) else "_" for x in title) - return filename - - # Source taken from http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console - def printProgress(self, iteration, total, fileSize='' , downloaded = '' , rate = '' ,suffix = '', barLength = 100): - filledLength = int(round(barLength * iteration / float(total))) - percents = format(100.00 * (iteration / float(total)), '.2f') - bar = fc + sd + ('█' if os.name is 'posix' else '#') * filledLength + fw + sd +'-' * (barLength - filledLength) - stdout.write('{}{}[{}{}*{}{}] : {}{}{}/{} {}% |{}{}{}| {} {}s ETA \r'.format(fc,sd,fm,sb,fc,sd,fg,sb,fileSize,downloaded,percents,bar,fg,sb,rate,suffix)) - stdout.flush() - - def Download(self, total, recvd, ratio, rate, eta): - if total <= 1048576: - TotalSize = round(float(total) / 1024, 2) - Receiving = round(float(recvd) / 1024, 2) - Size = format(TotalSize if TotalSize < 1024.00 else TotalSize/1024.00, '.2f') - Received = format(Receiving if Receiving < 1024.00 else Receiving/1024.00,'.2f') - SGb_SMb = 'KB' if TotalSize < 1024.00 else 'MB' - RGb_RMb = 'KB ' if Receiving < 1024.00 else 'MB ' - else: - TotalSize = round(float(total) / 1048576, 2) - Receiving = round(float(recvd) / 1048576, 2) - Size = format(TotalSize if TotalSize < 1024.00 else TotalSize/1024.00, '.2f') - Received = format(Receiving if Receiving < 1024.00 else Receiving/1024.00,'.2f') - SGb_SMb = 'MB' if TotalSize < 1024.00 else 'GB' - RGb_RMb = 'MB ' if Receiving < 1024.00 else 'GB ' - - Dl_Speed = round(float(rate) , 2) - dls = format(Dl_Speed if Dl_Speed < 1024.00 else Dl_Speed/1024.00, '.2f') - Mb_kB = 'kB/s ' if Dl_Speed < 1024.00 else 'MB/s ' - (mins, secs) = divmod(eta, 60) - (hours, mins) = divmod(mins, 60) - if hours > 99: - eta = "--:--:--" - if hours == 0: - eta = "%02d:%02d" % (mins, secs) - else: - eta = "%02d:%02d:%02d" % (hours, mins, secs) - self.printProgress(Receiving, TotalSize, fileSize = str(Size) + str(SGb_SMb) , downloaded = str(Received) + str(RGb_RMb), rate = str(dls) + str(Mb_kB), suffix = str(eta), barLength = 40) - - def Downloader(self, url, title, path): - out = course_dl.download(url, title, filepath=path, quiet=True, callback=self.Download) - if isinstance(out, dict) and len(out) > 0: - msg = out.get('msg') - status = out.get('status') - if status == 'True': - return msg - else: - if msg == 'Lynda Says (HTTP Error 401 : Unauthorized)': - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Lynda Says (HTTP Error 401 : Unauthorized)") - print (fc + sd + "[" + fw + sb + "*" + fc + sd + "] : " + fw + sd + "Try to run the lynda-dl again...") - exit(0) - else: - return msg - - def InfoExtractor(self, outto=None, sub_only=False): - current_dir = os.getcwd() - print(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading webpage..") - time.sleep(2) - course_path = extract_info.match_id(self.url) - course_id = (self.url.split('/')[-1]).split('-')[0] - print(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Extracting course information..") - time.sleep(0.6) - if not outto: - course_name = current_dir + '\\' + course_path if os.name == 'nt' else current_dir + '/' + course_path - else: - course_p = "%s\\%s" % (outto, course_path) if os.name == 'nt' else "%s/%s" % (outto, course_path) - course_name = course_p - - print(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Downloading " + fb + sb + "'%s'" % (course_path.replace('-',' '))) - self.login() - print(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading course information webpages ..") - videos_dict = extract_info.real_extract(self.url, course_path) - fileZip = extract_info.ExtractExerciseFile(course_id, course_path) - self.logout() - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Counting no of chapters..") - time.sleep(0.3) - if isinstance(videos_dict, dict): - print (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fw + sd + "Found ('%s') chapter(s).\n" % (len(videos_dict))) - j = 1 - for chap in sorted(videos_dict): - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fm + sb + "Downloading chapter : (%s of %s)" % (j, len(videos_dict))) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fw + sd + "Chapter (%s)" % (chap)) - chapter_path = course_name + '\\' + chap if os.name == 'nt' else course_name + '/' + chap - try: - os.makedirs(chapter_path) - except Exception as e: - pass - if os.path.exists(chapter_path): - os.chdir(chapter_path) - print (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fc + sd + "Found ('%s') lecture(s)." % (len(videos_dict[chap]))) - i = 1 - for lecture_name, _urls in sorted(videos_dict[chap].items()): - source_ak, source_ed = _urls.get('AKAMAI') , _urls.get('EDGECAST') - if sub_only: - if not source_ak or not source_ed: - _data = _urls['en'].get('data') - lecture_name = self.generate_filename(lecture_name) - filepath = os.path.join(chapter_path, lecture_name) - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading lecture : (%s of %s)" % (i, len(videos_dict[chap]))) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)" % (lecture_name)) - if os.path.isfile(filepath): - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_name) + fy + sb + "(already downloaded).") - else: - if pyver == 3: - with open(lecture_name, "w", encoding="utf-8") as f: - f.write(_data) - f.close() - else: - with open(lecture_name, "w") as f: - f.write(_data) - f.close() - print (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded (%s)" % (lecture_name)) - else: - pass - else: - if not source_ak or not source_ed: - _data = _urls['en'].get('data') - lecture_name = self.generate_filename(lecture_name) - filepath = os.path.join(chapter_path, lecture_name) - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading lecture : (%s of %s)" % (i, len(videos_dict[chap]))) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)" % (lecture_name)) - if os.path.isfile(filepath): - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_name) + fy + sb + "(already downloaded).") - else: - if pyver == 3: - with open(lecture_name, "w", encoding="utf-8") as f: - f.write(_data) - f.close() - else: - with open(lecture_name, "w") as f: - f.write(_data) - f.close() - print (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded (%s)" % (lecture_name)) - else: - _url = source_ak.get('720') or source_ed.get('720') - if not _url: - _url = source_ak.get('360') or source_ed.get('360') - if _url: - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading lecture : (%s of %s)" % (i, len(videos_dict[chap]))) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)" % (lecture_name)) - msg = self.Downloader(_url, lecture_name, chapter_path) - if msg == 'already downloaded': - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_name) + fy + sb + "(already downloaded).") - elif msg == 'download': - print (fc + sd + "\n[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded (%s)" % (lecture_name)) - else: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_name) + fc + sb + "(download skipped).") - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}".format(msg)) - i += 1 - j += 1 - print ('') - os.chdir(current_dir) - if isinstance(fileZip, dict): - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Counting no of files attached with this course..") - time.sleep(0.3) - print (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fc + sd + "Found ('%s') lecture(s)." % (len(fileZip))) - j = 1 - for file_name, url in sorted(fileZip.items()): - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading file : (%s of %s)" % (j, len(fileZip))) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)" % (file_name)) - msg = self.Downloader(url, file_name, course_name) - if msg == 'already downloaded': - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_name) + fy + sb + "(already downloaded).") - elif msg == 'download': - print (fc + sd + "\n[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded (%s)" % (lecture_name)) - else: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_name) + fc + sb + "(download skipped).") - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}".format(msg)) - j += 1 - - print ('') - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fb + sb + "(%s)" % (course_path.replace('-',' ')) + fg + sb + " Downloaded successfully.") - +from lynda._getpass import GetPass +from lynda._progress import ProgressBar +from lynda._colorized.banner import banner +getpass = GetPass() + + +class Lynda(ProgressBar): + + def __init__(self, url, username='', password='', organization=''): + self.url = url + self.username = username + self.password = password + self.organization = organization + super(Lynda, self).__init__() + + def course_list_down(self): + if not self.organization: + sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as " + fm + sb +"(%s)" % (self.username) + fg + sb +"...\n") + if self.organization: + sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as organization " + fm + sb +"(%s)" % (self.organization) + fg + sb +"...\n") + course = lynda.course(url=self.url, username=self.username, password=self.password, organization=self.organization) + course_id = course.id + course_name = course.title + chapters = course.get_chapters() + total_lectures = course.lectures + total_chapters = course.chapters + asset = course.asset + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Course " + fb + sb + "'%s'.\n" % (course_name)) + sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Chapter(s) (%s).\n" % (total_chapters)) + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (total_lectures)) + for chapter in chapters: + chapter_id = chapter.id + chapter_index = chapter.index + chapter_title = chapter.title + lectures = chapter.get_lectures() + lectures_count = chapter.lectures + sys.stdout.write ('\n' + fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s-%s)\n" % (chapter_title, chapter_id)) + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (lectures_count)) + for lecture in lectures: + lecture_id = lecture.id + lecture_index = lecture.index + lecture_title = lecture.title + lecture_subtitles = lecture.subtitles + lecture_best = lecture.getbest() + lecture_streams = lecture.streams + if lecture_streams: + sys.stdout.write(fc + sd + " - " + fy + sb + "duration : " + fm + sb + str(lecture.duration)+ fy + sb + ".\n") + sys.stdout.write(fc + sd + " - " + fy + sb + "Lecture id : " + fm + sb + str(lecture_id)+ fy + sb + ".\n") + for stream in lecture_streams: + content_length = stream.get_filesize() + if content_length != 0: + if content_length <= 1048576.00: + size = round(float(content_length) / 1024.00, 2) + sz = format(size if size < 1024.00 else size/1024.00, '.2f') + in_MB = 'KB' if size < 1024.00 else 'MB' + else: + size = round(float(content_length) / 1048576, 2) + sz = format(size if size < 1024.00 else size/1024.00, '.2f') + in_MB = "MB " if size < 1024.00 else 'GB ' + if lecture_best.dimention[1] == stream.dimention[1]: + in_MB = in_MB + fc + sb + "(Best)" + fg + sd + sys.stdout.write('\t- ' + fg + sd + "{:<23} {:<8}{}{}{}{}\n".format(str(stream), str(stream.dimention[1]) + 'p', sz, in_MB, fy, sb)) + content_length = asset.get_filesize() + if content_length != 0: + if content_length <= 1048576.00: + size = round(float(content_length) / 1024.00, 2) + sz = format(size if size < 1024.00 else size/1024.00, '.2f') + in_MB = 'KB' if size < 1024.00 else 'MB' + else: + size = round(float(content_length) / 1048576, 2) + sz = format(size if size < 1024.00 else size/1024.00, '.2f') + in_MB = "MB " if size < 1024.00 else 'GB ' + sys.stdout.write('\t- ' + fg + sd + "{:<23} {:<8}{}{}{}{}\n\n".format(str(asset), asset.extension, sz, in_MB, fy, sb)) + + def download_assets(self, assets='', filepath=''): + if assets: + title = assets.filename + sys.stdout.write(fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading asset(s)\n") + sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)\n" % (title)) + try: + retval = assets.download(filepath=filepath, quiet=True, callback=self.show_progress) + except KeyboardInterrupt: + sys.stdout.write (fc + sd + "\n[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n") + sys.exit(0) + msg = retval.get('msg') + if msg == 'already downloaded': + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Asset : '%s' " % (title) + fy + sb + "(already downloaded).\n") + elif msg == 'download': + sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded (%s)\n" % (title)) + else: + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Asset : '%s' " % (title) + fc + sb + "(download skipped).\n") + sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}\n".format(msg)) + + def download_subtitles(self, subtitle='', filepath=''): + if subtitle: + title = subtitle.title + '-' + subtitle.language + filename = "%s\\%s" % (filepath, subtitle.filename) if os.name == 'nt' else "%s/%s" % (filepath, subtitle.filename) + try: + retval = subtitle.download(filepath=filepath) + except KeyboardInterrupt: + sys.stdout.write (fc + sd + "\n[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n") + sys.exit(0) + def download_lectures(self, lecture_best='', lecture_title='', inner_index='', lectures_count='', filepath=''): + if lecture_best: + sys.stdout.write(fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) : ({index} of {total})\n".format(index=inner_index, total=lectures_count)) + sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloading (%s)\n" % (lecture_title)) + try: + retval = lecture_best.download(filepath=filepath, quiet=True, callback=self.show_progress) + except KeyboardInterrupt: + sys.stdout.write (fc + sd + "\n[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n") + sys.exit(0) + msg = retval.get('msg') + if msg == 'already downloaded': + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_title) + fy + sb + "(already downloaded).\n") + elif msg == 'download': + sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Downloaded (%s)\n" % (lecture_title)) + else: + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture : '%s' " % (lecture_title) + fc + sb + "(download skipped).\n") + sys.stdout.write (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "{}\n".format(msg)) + def download_captions_only(self, subtitle='', filepath=''): + if subtitle: + self.download_subtitles(subtitle=subtitle, filepath=filepath) + + def download_lectures_only(self, lecture_best='', lecture_title='', inner_index='', lectures_count='', filepath=''): + if lecture_best: + self.download_lectures(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, filepath=filepath) + + def download_lectures_and_captions(self, lecture_best='', lecture_title='', inner_index='', lectures_count='', subtitle='', filepath=''): + if lecture_best: + self.download_lectures(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=inner_index, lectures_count=lectures_count, filepath=filepath) + if subtitle: + self.download_subtitles(subtitle=subtitle, filepath=filepath) + + def course_download(self, path='', quality='', caption_only=False, skip_captions=False): + if not self.organization: + sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as " + fm + sb +"(%s)" % (self.username) + fg + sb +"...\n") + if self.organization: + sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as organization " + fm + sb +"(%s)" % (self.organization) + fg + sb +"...\n") + course = lynda.course(url=self.url, username=self.username, password=self.password, organization=self.organization) + course_id = course.id + course_name = course.title + chapters = course.get_chapters() + total_lectures = course.lectures + total_chapters = course.chapters + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Course " + fb + sb + "'%s'.\n" % (course_name)) + sys.stdout.write (fc + sd + "[" + fm + sb + "+" + fc + sd + "] : " + fg + sd + "Chapter(s) (%s).\n" % (total_chapters)) + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Lecture(s) (%s).\n" % (total_lectures)) + if path: + if '~' in path: + path = os.path.expanduser(path) + course_path = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name) + else: + path = os.getcwd() + course_path = "%s\\%s" % (path, course_name) if os.name == 'nt' else "%s/%s" % (path, course_name) + + for chapter in chapters: + chapter_id = chapter.id + chapter_index = chapter.index + chapter_title = chapter.title + lectures = chapter.get_lectures() + lectures_count = chapter.lectures + filepath = "%s\\%s" % (course_path, chapter_title) if os.name == 'nt' else "%s/%s" % (course_path, chapter_title) + status = course.create_chapter(filepath=filepath) + sys.stdout.write (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fm + sb + "Downloading chapter : ({index} of {total})\n".format(index=chapter_index, total=total_chapters)) + sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Chapter (%s)\n" % (chapter_title)) + sys.stdout.write (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Found (%s) lectures ...\n" % (lectures_count)) + for lecture in lectures: + lecture_id = lecture.id + lecture_index = lecture.index + lecture_title = lecture.title + lecture_subtitles = lecture.subtitles + lecture_best = lecture.getbest() + lecture_streams = lecture.streams + if caption_only and not skip_captions: + self.download_captions_only(subtitle=lecture_subtitles, filepath=filepath) + elif skip_captions and not caption_only: + lecture_best = lecture.get_quality(best_quality=lecture_best, streams=lecture_streams, requested=quality) + self.download_lectures_only(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_index, lectures_count=lectures_count, filepath=filepath) + else: + lecture_best = lecture.get_quality(best_quality=lecture_best, streams=lecture_streams, requested=quality) + self.download_lectures_and_captions(lecture_best=lecture_best, lecture_title=lecture_title, inner_index=lecture_index, lectures_count=lectures_count, subtitle=lecture_subtitles, filepath=filepath) + self.download_assets(assets=course.asset, filepath=course_path) def main(): - ban = banner() - print (ban) - us = '''%prog [-u (USERNAME/LIBRARY CARD NUMBER)][-p (PASSWORD/LIBRARY CARD PIN)] - [-o ORGANIZATION] COURSE_URL [-s/--sub-only] [-d DIRECTORY]''' - version = "%prog version 1.0" - parser = optparse.OptionParser(usage=us,version=version,conflict_handler="resolve") - - general = optparse.OptionGroup(parser, 'General') - general.add_option( - '-h', '--help', - action='help', - help='Shows the help for program.') - general.add_option( - '-v', '--version', - action='version', - help='Shows the version of program.') - - downloader = optparse.OptionGroup(parser, "Advance") - downloader.add_option( - "-u", "--username", - action='store_true', - dest='username',\ - help="Username or Library Card Number.") - downloader.add_option( - "-p", "--password", - action='store_true', - dest='password',\ - help="Password or Library Card Pin.") - downloader.add_option( - "-o", "--organization", - action='store_true', - dest='org',\ - help="Organization, registered at Lynda.") - downloader.add_option( - "-s","--sub-only", - action='store_true', - dest='sub_only',\ - default=False,\ - help="Download the captions/subtitle only") - downloader.add_option( - "-d","--directory", - action='store_true', - dest='output',\ - help="Output directory where the videos will be saved, default is current directory.") - - - parser.add_option_group(general) - parser.add_option_group(downloader) - - (options, args) = parser.parse_args() - - if not options.username and not options.password: - username = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Username : " + fg + sb - password = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Password : " + fc + sb - email = getpass.getuser(prompt=username) - passwd = getpass.getpass(prompt=password) - print ("") - if options.org: - if options.output and not options.sub_only: - org = args[0] - try: - url = args[1] - except IndexError: - parser.print_usage() - else: - save_to = args[2] - if email and passwd: - lynda = LyndaDownload(url, email, passwd, org=org) - lynda.InfoExtractor(outto=save_to) - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - elif options.output and options.sub_only: - org = args[0] - try: - url = args[1] - except IndexError: - parser.print_usage() - else: - save_to = args[2] - if email and passwd: - lynda = LyndaDownload(url, email, passwd, org=org) - lynda.InfoExtractor(outto=save_to, sub_only=True) - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - - elif not options.output and not options.sub_only: - org = args[0] - try: - url = args[1] - except IndexError: - parser.print_usage() - else: - if email and passwd: - lynda = LyndaDownload(url, email, passwd, org=org) - lynda.InfoExtractor() - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - elif not options.output and options.sub_only: - org = args[0] - try: - url = args[1] - except IndexError: - parser.print_usage() - else: - if email and passwd: - lynda = LyndaDownload(url, email, passwd, org=org) - lynda.InfoExtractor(sub_only=True) - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - - else: - if options.output and not options.sub_only: - try: - url = args[0] - except IndexError: - parser.print_usage() - else: - save_to = args[1] - if email and passwd: - lynda = LyndaDownload(url, email, passwd) - lynda.InfoExtractor(outto=save_to) - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - - elif options.output and options.sub_only: - try: - url = args[0] - except IndexError: - parser.print_usage() - else: - save_to = args[1] - if email and passwd: - lynda = LyndaDownload(url, email, passwd) - lynda.InfoExtractor(outto=save_to, sub_only=True) - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - - elif not options.output and not options.sub_only: - try: - url = args[0] - except IndexError: - parser.print_usage() - else: - if email and passwd: - lynda = LyndaDownload(url, email, passwd) - lynda.InfoExtractor() - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - - elif not options.output and options.sub_only: - try: - url = args[0] - except IndexError: - parser.print_usage() - else: - if email and passwd: - lynda = LyndaDownload(url, email, passwd) - lynda.InfoExtractor(sub_only=True) - else: - print (fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required..") - exit(0) - - elif options.username and options.password: - if options.org: - if options.output: - username = args[0] - password = args[1] - org = args[2] - try: - url = args[3] - except IndexError: - parser.print_usage() - else: - save_to = args[4] - lynda = LyndaDownload(url, username, password, org=org) - lynda.InfoExtractor(outto=save_to) - else: - username = args[0] - password = args[1] - org = args[2] - try: - url = args[3] - except IndexError: - parser.print_usage() - else: - lynda = LyndaDownload(url, username, password, org=org) - lynda.InfoExtractor() - else: - if options.output: - username = args[0] - password = args[1] - try: - url = args[2] - except IndexError: - parser.print_usage() - else: - save_to = args[3] - lynda = LyndaDownload(url, username, password) - lynda.InfoExtractor(outto=save_to) - else: - username = args[0] - password = args[1] - try: - url = args[2] - except IndexError: - parser.print_usage() - else: - lynda = LyndaDownload(url, username, password) - lynda.InfoExtractor() - else: - parser.print_usage() + sys.stdout.write(banner()) + version = "%(prog)s {version}".format(version=__version__) + description = 'A cross-platform python based utility to download courses from lynda for personal offline use.' + parser = argparse.ArgumentParser(description=description, conflict_handler="resolve") + parser.add_argument('course', help="Lynda course or file containing list of courses.", type=str) + general = parser.add_argument_group("General") + general.add_argument( + '-h', '--help',\ + action='help',\ + help="Shows the help.") + general.add_argument( + '-v', '--version',\ + action='version',\ + version=version,\ + help="Shows the version.") + + authentication = parser.add_argument_group("Authentication") + authentication.add_argument( + '-u', '--username',\ + dest='username',\ + type=str,\ + help="Username or Library Card Number.",metavar='') + authentication.add_argument( + '-p', '--password',\ + dest='password',\ + type=str,\ + help="Password or Library Card Pin.",metavar='') + authentication.add_argument( + '-o', '--organization',\ + dest='org',\ + type=str,\ + help="Organization, registered at Lynda.",metavar='') + + advance = parser.add_argument_group("Advance") + advance.add_argument( + '-d', '--directory',\ + dest='output',\ + type=str,\ + help="Download to specific directory.",metavar='') + advance.add_argument( + '-q', '--quality',\ + dest='quality',\ + type=int,\ + help="Download specific video quality.",metavar='') + + other = parser.add_argument_group("Others") + other.add_argument( + '--info',\ + dest='info',\ + action='store_true',\ + help="List all lectures with available resolution.") + other.add_argument( + '--sub-only',\ + dest='caption_only',\ + action='store_true',\ + help="Download captions/subtitle only.") + other.add_argument( + '--skip-sub',\ + dest='skip_captions',\ + action='store_true',\ + help="Download course but skip captions/subtitle.") + + options = parser.parse_args() + + if os.path.isfile(options.course): + f_in = open(options.course) + courses = [line for line in (l.strip() for l in f_in) if line] + f_in.close() + sys.stdout.write (fc + sd + "[" + fw + sb + "+" + fc + sd + "] : " + fw + sd + "Found (%s) courses ..\n" % (len(courses))) + for course in courses: + + if not options.username and not options.password: + username = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Username : " + fg + sb + password = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Password : " + fc + sb + email = getpass.getuser(prompt=username) + passwd = getpass.getpass(prompt=password) + if email and passwd: + lynda = Lynda(url=course, username=email, password=passwd, organization=options.org) + else: + sys.stdout.write('\n' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required.\n") + sys.exit(0) + + if options.info: + lynda.course_list_down() + + if not options.info: + if options.caption_only and not options.skip_captions: + lynda.course_download(caption_only=options.caption_only, path=options.output) + elif not options.caption_only and options.skip_captions: + lynda.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality) + else: + lynda.course_download(path=options.output, quality=options.quality) + + elif options.username and options.password: + lynda = Lynda(url=course, username=options.username, password=options.password, organization=options.org) + if options.info: + lynda.course_list_down() + + if not options.info: + if options.caption_only and not options.skip_captions: + lynda.course_download(caption_only=options.caption_only, path=options.output) + elif not options.caption_only and options.skip_captions: + lynda.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality) + else: + lynda.course_download(path=options.output, quality=options.quality) + + if not os.path.isfile(options.course): + + if not options.username and not options.password: + username = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Username : " + fg + sb + password = fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Password : " + fc + sb + email = getpass.getuser(prompt=username) + passwd = getpass.getpass(prompt=password) + if email and passwd: + lynda = Lynda(url=options.course, username=email, password=passwd, organization=options.org) + else: + sys.stdout.write('\n' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Username and password is required.\n") + sys.exit(0) + + if options.info: + lynda.course_list_down() + + if not options.info: + if options.caption_only and not options.skip_captions: + lynda.course_download(caption_only=options.caption_only, path=options.output) + elif not options.caption_only and options.skip_captions: + lynda.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality) + else: + lynda.course_download(path=options.output, quality=options.quality) + + elif options.username and options.password: + lynda = Lynda(url=options.course, username=options.username, password=options.password, organization=options.org) + if options.info: + lynda.course_list_down() + + if not options.info: + if options.caption_only and not options.skip_captions: + lynda.course_download(caption_only=options.caption_only, path=options.output) + elif not options.caption_only and options.skip_captions: + lynda.course_download(skip_captions=options.skip_captions, path=options.output, quality=options.quality) + else: + lynda.course_download(path=options.output, quality=options.quality) if __name__ == '__main__': - try: - main() - except KeyboardInterrupt: - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fr + sd + "User Interrupted..") - time.sleep(0.8) - - + try: + main() + except KeyboardInterrupt: + sys.stdout.write ('\n' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n") + sys.exit(0) \ No newline at end of file diff --git a/lynda/__init__.py b/lynda/__init__.py index 0e81eab..a8795d5 100644 --- a/lynda/__init__.py +++ b/lynda/__init__.py @@ -1,6 +1,32 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- +__version__ = "0.2" +__author__ = "Nasir Khan (r0ot h3x49)" +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2018 Nasir Khan (r0ot h3x49)' -from ._downloader import Downloader -from ._extractor import LyndaInfoExtractor -from ._getpass import GetPass +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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. + +''' + + +from ._lynda import course diff --git a/lynda/_auth.py b/lynda/_auth.py new file mode 100644 index 0000000..765c096 --- /dev/null +++ b/lynda/_auth.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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. + +''' + +from pprint import pprint +from ._compat import ( + re, + sys, + json, + requests, + conn_error, + HEADERS, + LOGOUT_URL, + USER_LOGIN_URL, + AJAX_USERNAME, + AJAX_PASSWORD, + ORG_LOGIN_URL, + AJAX_ORGNIZATION, + ) +from ._colorized import * + +class LyndaAuth(object): + + def __init__(self, username='', password='', organization=''): + self.username = username + self.password = password + self.organization = organization + self._session = requests.Session() + + def _user_login_steps(self, form_html, fallback_action_url, extra_form_data, referrer_url): + csrftoken = re.search(r'name="-_-"\s+value="(.*)"', form_html) + if csrftoken: + data = {'-_-' : csrftoken.group(1)} + if data and isinstance(data, dict): + data.update(extra_form_data) + try: + webpage = self._session.post(fallback_action_url, data=data, headers={'Referer': referrer_url, 'X-Requested-With': 'XMLHttpRequest'}).json() + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + else: + return webpage, fallback_action_url + else: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Failed to extract csrftoken.\n") + sys.exit(0) + + def _user_session(self): + try: + webpage = self._session.get(USER_LOGIN_URL, headers=HEADERS).text + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + else: + sigin_form = re.search(r'(?s)(]+data-form-name=["\']signin["\'][^>]*>.+?)', webpage) + if sigin_form: + form_html = sigin_form.group() + sigin_webpage, signin_url = self._user_login_steps(form_html, AJAX_PASSWORD, {'email': self.username}, USER_LOGIN_URL) + password_form = sigin_webpage.get('body') + if not password_form: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "[-] : Lynda Says : Sorry, we do not recognize that email or username.\n") + sys.exit(0) + response, url = self._user_login_steps(password_form, AJAX_USERNAME, {'email': self.username, 'password': self.password}, signin_url) + response_text = response.get('UserID') if response.get('UserID') else response.get('password') + if response_text != "The username or password is invalid.": + self._session.headers.update({'User-Agent' : HEADERS.get('User-Agent')}) + return self._session + else: + return None + else: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Failed to extract login-form..\n") + sys.exit(0) + + def _org_login_steps(self, fallback_action_url, extra_form_data, extra_headers, referrer_url): + + if fallback_action_url == AJAX_ORGNIZATION: + HEADERS.update({'-_-' : extra_headers.get('-_-'), 'Referer' : referrer_url}) + else: + HEADERS.update(extra_headers) + HEADERS.pop('-_-') + + try: + response = self._session.post(fallback_action_url, data=extra_form_data, headers=HEADERS) + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + else: + if fallback_action_url == AJAX_ORGNIZATION: + response = response.json() + _org_url, referrer = response.get('RedirectUrl').replace('http', 'https') if not 'https' in response.get('RedirectUrl') else response.get('RedirectUrl'), response.get('RedirectUrl') + return _org_url, referrer + else: + response = response.text + logged_in_username = re.search(r'data-qa="eyebrow_account_menu">(.*)', response) + if logged_in_username: + return logged_in_username.group(1), None + else: + return None, None + + def _organization_session(self): + try: + webpage = self._session.get(ORG_LOGIN_URL, headers={'User-Agent' : HEADERS.get('User-Agent')}).text + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + else: + data = re.search(r'var\s+lynda\s+=\s+(?P{.+?});', webpage) + if data: + json_data = json.loads(data.group(1)) + organization_login_url, referrer_url = self._org_login_steps(AJAX_ORGNIZATION, {'org' : self.organization}, json_data, ORG_LOGIN_URL) + try: + webpage = self._session.get(referrer_url).text + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + else: + csrftoken = re.search(r'name="seasurf"\s+value="(.*)"', webpage) + if csrftoken: + csrftoken = csrftoken.group(1) + login_data = dict( + libraryCardNumber=self.username, + libraryCardPin=self.password, + libraryCardPasswordVerify="", + org=self.organization, + currentView="login", + seasurf=csrftoken + ) + response, _ = self._org_login_steps(organization_login_url, login_data, {'Referer' : referrer_url}, referrer_url) + if response: + self._session.headers.update({'User-Agent' : HEADERS.get('User-Agent')}) + return self._session + else: + return None + else: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Failed to extract csrftoken.\n") + sys.exit(0) + else: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Failed to extract login-form..\n") + sys.exit(0) + + def authenticate(self): + if self.organization: + return self._organization_session() + else: + return self._user_session() \ No newline at end of file diff --git a/lynda/_colorized/__init__.py b/lynda/_colorized/__init__.py new file mode 100644 index 0000000..5a7529d --- /dev/null +++ b/lynda/_colorized/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/python + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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. + +''' + +from .colors import * +from . import banner diff --git a/lynda/_colorized/banner.py b/lynda/_colorized/banner.py new file mode 100644 index 0000000..5852c07 --- /dev/null +++ b/lynda/_colorized/banner.py @@ -0,0 +1,42 @@ +#!/usr/bin/python + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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. + +''' + +from .colors import * + +def banner(): + banner = """%s%s + + oooo .o8 .o8 oooo + `888 "888 "888 `888 + 888 oooo ooo ooo. .oo. .oooo888 .oooo. .oooo888 888 +%s%s 888 `88. .8' `888P"Y88b d88' `888 `P )88b d88' `888 888%s%s +%s%s 888 `88..8' 888 888 888 888 .oP"888 8888 888 888 888%s%s + 888 `888' 888 888 888 888 d8( 888 888 888 888 + o888o .8' o888o o888o `Y8bod88P" `Y888""8o `Y8bod88P" o888o + .o..P' + `Y8P'\t\t\t\t%s%sVersion : %s%s0.2\n\t\t\t\t\t%s%sAuthor : %s%sNasir Khan (r0ot h3x49)\n\t\t\t\t\t%s%sGithub : %s%shttps://github.com/r0oth3x49 + + +""" % (fc, sb, fm, sb, fc, sb, fm, sb, fc, sb, fy,sb, fg, sd, fy,sb, fg, sd, fy,sb, fg, sd) + return banner diff --git a/lynda/_colorized/colors.py b/lynda/_colorized/colors.py new file mode 100644 index 0000000..d4a7787 --- /dev/null +++ b/lynda/_colorized/colors.py @@ -0,0 +1,79 @@ +#!/usr/bin/python + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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. + +''' + +from colorama import init,Fore,Back,Style +import os + +if os.name == "posix": + ## ---------------------------------------------------------------------------------------------------------------------- ## + init(autoreset=True) + # colors foreground text: + fc = "\033[0;96m" + fg = "\033[0;92m" + fw = "\033[0;97m" + fr = "\033[0;91m" + fb = "\033[0;94m" + fy = "\033[0;33m" + fm = "\033[0;35m" + + # colors background text: + bc = "\033[46m" + bg = "\033[42m" + bw = "\033[47m" + br = "\033[41m" + bb = "\033[44m" + by = "\033[43m" + bm = "\033[45m" + + # colors style text: + sd = Style.DIM + sn = Style.NORMAL + sb = Style.BRIGHT +else: + ## ---------------------------------------------------------------------------------------------------------------------- ## + init(autoreset=True) + # colors foreground text: + fc = Fore.CYAN + fg = Fore.GREEN + fw = Fore.WHITE + fr = Fore.RED + fb = Fore.BLUE + fy = Fore.YELLOW + fm = Fore.MAGENTA + + + # colors background text: + bc = Back.CYAN + bg = Back.GREEN + bw = Back.WHITE + br = Back.RED + bb = Back.BLUE + by = Fore.YELLOW + bm = Fore.MAGENTA + + # colors style text: + sd = Style.DIM + sn = Style.NORMAL + sb = Style.BRIGHT + ## ---------------------------------------------------------------------------------------------------------------------- ## diff --git a/lynda/_compat.py b/lynda/_compat.py index 7aad6ef..21c69c1 100644 --- a/lynda/_compat.py +++ b/lynda/_compat.py @@ -1,68 +1,127 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import sys +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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 os +import sys +import time +import json +import requests if sys.version_info[:2] >= (3, 0): - import requests,re + + import ssl import urllib.request as compat_urllib - from urllib.request import Request as compat_request - from urllib.request import urlopen as compat_urlopen + from urllib.error import HTTPError as compat_httperr from urllib.error import URLError as compat_urlerr from urllib.parse import urlparse as compat_urlparse + from urllib.request import Request as compat_request + from urllib.request import urlopen as compat_urlopen from urllib.request import build_opener as compat_opener - uni, pyver = str, 3 + from html.parser import HTMLParser as compat_HTMLParser + from requests.exceptions import ConnectionError as conn_error + + encoding, pyver = str, 3 + ssl._create_default_https_context = ssl._create_unverified_context else: - import requests,re + import urllib2 as compat_urllib + from urllib2 import Request as compat_request from urllib2 import urlopen as compat_urlopen from urllib2 import URLError as compat_urlerr from urllib2 import HTTPError as compat_httperr - from urllib2 import urlparse as compat_urlparse from urllib2 import build_opener as compat_opener - uni, pyver = unicode, 2 - - -org_url = "https://www.lynda.com/signin/organization" -xorg_url = "https://www.lynda.com/ajax/signin/organization" -sigin_url = 'https://www.lynda.com/signin' -passw_url = 'https://www.lynda.com/signin/password' -user_url = 'https://www.lynda.com/signin/user' -logout = "https://www.lynda.com/signout" -course_url = "https://www.lynda.com/ajax/player?courseId=%s&type=course" -get_url = "https://www.lynda.com/ajax/course/%s/%s/play" -ex_url = "https://www.lynda.com/ajax/course/%s/0/getupselltabs" -cc_url = "https://www.lynda.com/ajax/player?videoId={video_id}&type=transcript" -user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)" -std_headers = { - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)', - 'X-Requested-With': 'XMLHttpRequest', - 'Host': 'www.lynda.com'} - -__ALL__ =[ - "compat_request", - "requests", - "logout", - "re", - "compat_urllib", - "compat_urlparse", - "compat_urlerr", - "compat_httperr", - "sigin_url", - "passw_url", - "user_url", - "org_url", - "course_url", - "get_url", - "ex_url", - "std_headers", - "compat_urlopen", - "compat_opener", - "user_agent", - "cc_url", - "xorg_url", - "pyver" + from urlparse import urlparse as compat_urlparse + from HTMLParser import HTMLParser as compat_HTMLParser + from requests.exceptions import ConnectionError as conn_error + + encoding, pyver = unicode, 2 + + +NO_DEFAULT = object() + +# endpoints for login via email / password +USER_LOGIN_URL = "https://www.lynda.com/signin" +AJAX_USERNAME = "https://www.lynda.com/signin/user" +AJAX_PASSWORD = "https://www.lynda.com/signin/password" + +# endpoints for login via organiztion library card & pin +ORG_LOGIN_URL = "https://www.lynda.com/signin/organization" +AJAX_ORGNIZATION = "https://www.lynda.com/ajax/signin/organization" + +# endpoint for logout .. +LOGOUT_URL = "https://www.lynda.com/signout" + + +COURSE_URL = "https://www.lynda.com/ajax/player?courseId={course_id}&type=course" +VIDEO_URL = "https://www.lynda.com/ajax/course/{course_id}/{video_id}/play" +CAPTIONS_URL = "https://www.lynda.com/ajax/player?videoId={video_id}&type=transcript" +EXERCISE_FILES_URL = "https://www.lynda.com/ajax/course/{course_id}/0/getupselltabs" + + +HEADERS = { + 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15', + 'X-Requested-With' : 'XMLHttpRequest', + 'Host': 'www.lynda.com' + } + + +__ALL__ = [ + 're', + 'os', + 'sys', + 'time', + 'json', + 'pyver', + 'encoding', + 'requests', + 'conn_error', + 'compat_urlerr', + 'compat_opener', + 'compat_urllib', + 'compat_urlopen', + 'compat_request', + 'compat_httperr', + 'compat_urlparse', + 'compat_HTMLParser', + 'HEADERS', + 'NO_DEFAULT', + + 'USER_LOGIN_URL', + 'AJAX_USERNAME', + 'AJAX_PASSWORD', + + 'ORG_LOGIN_URL', + 'AJAX_ORGNIZATION', + + 'LOGOUT_URL', + + 'COURSE_URL', + 'VIDEO_URL', + 'CAPTIONS_URL', + 'EXERCISE_FILES_URL', ] diff --git a/lynda/_downloader.py b/lynda/_downloader.py deleted file mode 100644 index 6e9b210..0000000 --- a/lynda/_downloader.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/python - -import time -import os,sys -from ._compat import ( - compat_request, - compat_urlopen, - compat_urlerr, - compat_httperr, - compat_opener, - user_agent, - std_headers, - re, - ) -early_py_version = sys.version_info[:2] < (2, 7) - - -class Downloader: - - def _generate_filename(self, title): - ok = re.compile(r'[^/]') - - if os.name == "nt": - ok = re.compile(r'[^\\/:*?"<>|]') - - filename = "".join(x if ok.match(x) else "_" for x in title) - if ".zip" in title: - filename = title - else: - filename += ".mp4" - - return filename - - def cancel(self): - if self._active: - self._active = True - return True - - - def download(self, url, title, filepath="", quiet=False, callback=lambda *x: None): - savedir = filename = "" - retVal = {} - - if filepath and os.path.isdir(filepath): - savedir, filename = filepath, self._generate_filename(title) - - elif filepath: - savedir, filename = os.path.split(filepath) - - else: - filename = self._generate_filename(title) - - filepath = os.path.join(savedir, filename) - - if os.path.isfile(filepath): - retVal = {"status" : "True", "msg" : "already downloaded"} - return retVal - - temp_filepath = filepath + ".part" - - status_string = (' {:,} Bytes [{:.2%}] received. Rate: [{:4.0f} ' - 'KB/s]. ETA: [{:.0f} secs]') - - - if early_py_version: - status_string = (' {0:} Bytes [{1:.2%}] received. Rate:' - ' [{2:4.0f} KB/s]. ETA: [{3:.0f} secs]') - - try: - req = compat_request(url, headers={'user-agent':user_agent}) - response = compat_urlopen(req) - except compat_httperr as e: - if e.code == 401: - retVal = {"status" : "False", "msg" : "Lynda Says (HTTP Error 401 : Unauthorized)"} - else: - retVal = {"status" : "False", "msg" : "HTTPError-{} : download link is expired ..".format(e.code)} - return retVal - except compat_urlerr as e: - retVal = {"status" : "False", "msg" : "URLError : either your internet connection is not working or server aborted the request"} - return retVal - else: - total = int(response.info()['Content-Length'].strip()) - chunksize, bytesdone, t0 = 16384, 0, time.time() - - fmode, offset = "wb", 0 - - if os.path.exists(temp_filepath): - if os.stat(temp_filepath).st_size < total: - offset = os.stat(temp_filepath).st_size - fmode = "ab" - - outfh = open(temp_filepath, fmode) - - if offset: - resume_opener = compat_opener() - resume_opener.addheaders = [('User-Agent', user_agent), - ("Range", "bytes=%s-" % offset)] - try: - response = resume_opener.open(url) - except compat_urlerr as e: - retVal = {"status" : "False", "msg" : "URLError : either your internet connection is not working or server aborted the request"} - return retVal - except compat_httperr as e: - if e.code == 401: - retVal = {"status" : "False", "msg" : "Lynda Says (HTTP Error 401 : Unauthorized)"} - else: - retVal = {"status" : "False", "msg" : "HTTPError-{} : download link is expired ..".format(e.code)} - return retVal - else: - bytesdone = offset - - self._active = True - while self._active: - chunk = response.read(chunksize) - outfh.write(chunk) - elapsed = time.time() - t0 - bytesdone += len(chunk) - if elapsed: - try: - rate = ((float(bytesdone) - float(offset)) / 1024.0) / elapsed - eta = (total - bytesdone) / (rate * 1024.0) - except ZeroDivisionError as e: - outfh.close() - try: - os.unlink(temp_filepath) - except Exception as e: - pass - retVal = {"status" : "False", "msg" : "ZeroDivisionError : it seems, lecture has malfunction or is zero byte(s) .."} - return retVal - else: - rate = 0 - eta = 0 - progress_stats = (bytesdone, bytesdone * 1.0 / total, rate, eta) - - if not chunk: - outfh.close() - break - if not quiet: - status = status_string.format(*progress_stats) - sys.stdout.write("\r" + status + ' ' * 4 + "\r") - sys.stdout.flush() - - if callback: - callback(total, *progress_stats) - - if self._active: - os.rename(temp_filepath, filepath) - retVal = {"status" : "True", "msg" : "download"} - else: - outfh.close() - retVal = {"status" : "True", "msg" : "download"} - return retVal diff --git a/lynda/_extract.py b/lynda/_extract.py new file mode 100644 index 0000000..d8ec1bb --- /dev/null +++ b/lynda/_extract.py @@ -0,0 +1,279 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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 os +import sys +import json + +from pprint import pprint +from ._auth import LyndaAuth +from ._compat import ( + re, + time, + encoding, + conn_error, + LOGOUT_URL, + COURSE_URL, + VIDEO_URL, + CAPTIONS_URL, + EXERCISE_FILES_URL, + ) +from ._sanitize import ( + slugify, + sanitize, + SLUG_OK + ) +from ._colorized import * +from ._progress import ProgressBar + + +class Lynda(ProgressBar): + + _TIMECODE_REGEX = r'\[(?P\d+:\d+:\d+[\.,]\d+)\]' + _VALID_URL = r'''https?://(?:www|m)\.(?:lynda\.com|educourse\.ga)/(?P(?:[^/]+/){2,3}(?P\d+))-2\.html''' + _VALID_COURSE_URL = r'''(?x)(?:(.+)/(?P[a-zA-Z0-9_-]+)|(?P[a-zA-Z0-9_-]+))/(?P[a-zA-Z0-9_-]+)/(?P\d+)'''#r'''https?://(?:www|m)\.(?:lynda\.com|educourse\.ga)/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)/(?P\d+)-2\.html''' + _VIDEO_URL = r'''(?x)https?://(?:www\.)?(?:lynda\.com|educourse\.ga)/(?:(?:[^/]+/){2,3}(?P\d+)|player/embed)/(?P\d+)''' + + def __init__(self): + self._session = '' + + def _extract_course_info(self, url): + mobj = re.match(self._VALID_URL, url) + if mobj: + mobject = re.match(self._VALID_COURSE_URL, mobj.group('course_path')) + if mobject: + course_name, course_id = mobject.group('course_name'), mobject.group('course_id') + if not mobject: + course_name, course_id = mobj.group('course_path').split('/')[-2], mobj.group('course_id') + if not mobj: + mobj = re.match(self._VIDEO_URL, url) + course_name, course_id = None, mobj.group('course_id') + if not mobj: + sys.stdout.write('\033[2K\033[1G' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Course URL seems incorrect.\n") + exit(0) + return course_name, course_id + + def _clean(self, text): + ok = re.compile(r'[^\\/:*?"<>|]') + text = "".join(x if ok.match(x) else "_" for x in text) + text = re.sub(r'^\d*\s*\.*\s*', '', text) + return re.sub('\.+$', '', text) if text.endswith(".") else text + + def _sanitize(self, unsafetext): + text = sanitize(slugify(unsafetext, lower=False, spaces=True, ok=SLUG_OK + '()._-')) + return text + + def _login(self, username='', password='', organization=''): + auth = LyndaAuth(username=username, password=password, organization=organization) + self._session = auth.authenticate() + if self._session is not None: + return {'login' : 'successful'} + else: + return {'login' : 'failed'} + + def _logout(self): + if self._session: + self._session.get(LOGOUT_URL) + return + + def _fix_subtitles(self, subs): + srt = '' + seq_counter = 0 + for pos in range(0, len(subs) - 1): + seq_current = subs[pos] + m_current = re.match(self._TIMECODE_REGEX, seq_current['Timecode']) + if m_current is None: + continue + seq_next = subs[pos + 1] + m_next = re.match(self._TIMECODE_REGEX, seq_next['Timecode']) + if m_next is None: + continue + appear_time = m_current.group('timecode') + disappear_time = m_next.group('timecode') + text = seq_current['Caption'].strip() + if text: + seq_counter += 1 + srt += '%s\r\n%s --> %s\r\n%s\r\n\r\n' % (seq_counter, appear_time, disappear_time, text) + if srt: + return srt + + def _extract_assets(self, course_id): + url = EXERCISE_FILES_URL.format(course_id=course_id) + _temp = {} + _temp['type'] = 'file' + try: + response = self._session.get(url).json() + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + if response and isinstance(response, dict): + exercise_tab = (response.get('exercisetab')).replace('\r', '').replace('\n', '').replace('\t', '') + mobjf = re.search(r'(?is)]+?class=(["\'])exercise-name\1*[^>]*>(?P.+?)', exercise_tab) + mobjurl = re.search(r'(?is)]+?href=(["\'])(?P.+)"\s*role', exercise_tab) + + if mobjf: + filename = mobjf.group('filename') + extension = filename.split('.', 1)[-1] + _temp['filename'] = filename + _temp['extension'] = extension + + if mobjurl: + _url = 'https://www.lynda.com{href}'.format(href=mobjurl.group('href')) + try: + response = self._session.get(_url, stream=True) + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + if response.url != _url: + download_url = response.url + file_size = response.headers.get('Content-Length') + _temp['download_url'] = download_url + _temp['file_size'] = file_size + return _temp + + def _extract_subtitles(self, video_id): + url = CAPTIONS_URL.format(video_id=video_id) + try: + subs = self._session.get(url).json() + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + if subs: + return { + 'type' : 'subtitle', + 'language' : 'en', + 'extension' : 'srt', + 'subtitle_data' : self._fix_subtitles(subs), + } + else: + return { + 'type' : 'subtitle', + 'language' : 'en', + 'extension' : 'srt', + 'subtitle_data' : None, + } + + def _extract_sources(self, course_id, lecture_id): + _temp = [] + url = VIDEO_URL.format(course_id=course_id, video_id=lecture_id) + try: + play = self._session.get(url).json() + except conn_error as e: + sys.stdout.write(fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + if play and isinstance(play, list): + for entry in play: + urls = entry.get('urls') + if not isinstance(urls, dict): + continue + cdn = entry.get('name').lower() + for height, dl_url in urls.items(): + if height == '64' or height == 64: + continue + if height == '540' or height == 540: + width = '960' + if height == '720' or height == 720: + width = '1280' + if height == '360' or height == 360: + width = '640' + _temp.append({ + 'type' : cdn, + 'height' : int(height), + 'width' : int(width), + 'extension' : 'mp4', + 'download_url' : dl_url, + }) + return _temp + + def _real_extract(self, url=''): + + try: + response = self._session.get(url) + except conn_error as e: + sys.stdout.write('\033[2K\033[1G' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + if url != response.url: + url = response.url + + _lynda = {} + course_name, course_id = self._extract_course_info(url=url) + course_url = COURSE_URL.format(course_id=course_id) + try: + course_json = self._session.get(course_url).json() + except conn_error as e: + sys.stdout.write('\033[2K\033[1G' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sb + "Connection error : make sure your internet connection is working.\n") + sys.exit(0) + + course = course_json.get('Chapters') + + _lynda['course_id'] = course_json.get('ID') or course_id + _lynda['course_title'] = self._sanitize(self._clean(course_json.get('Title'))) or course_name + assets = self._extract_assets(course_id) + _lynda['asset'] = assets + _lynda['chapters'] = [] + + + if course: + _lynda['total_chapters'] = len(course) + _lynda['total_lectures'] = sum([len(chapter.get('Videos', [])) for chapter in course]) + + for entry in course: + chapter_id = entry.get('ID') + chapter_index = entry.get('ChapterIndex') + chapter_title = self._sanitize(self._clean(entry.get('Title'))) + chapter = "{0:02d} {1!s}".format(chapter_index, chapter_title) + lectures = entry.get('Videos', []) + if lectures and len(lectures) > 0: + _temp_lectures = [] + for entry in lectures: + text = '\033[2K\033[1G\r' + fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Downloading course information .. " + self._spinner(text) + lecture_id = entry.get('ID') + lecture_index = entry.get('VideoIndex') + lecture_title = self._sanitize(self._clean(entry.get('Title'))) + lecture = "{0:03d} {1!s}".format(lecture_index, lecture_title) + duration = entry.get('DurationInSeconds') + sources = self._extract_sources(course_id, lecture_id) + subtitles = self._extract_subtitles(lecture_id) + _temp_lectures.append({ + 'lecture_id' : lecture_id, + 'lecture_index' : lecture_index, + 'lecture_title' : lecture, + 'sources' : sources, + 'sources_count' : len(sources), + 'subtitles' : subtitles, + 'duration' : duration, + }) + if chapter not in _lynda['chapters']: + _lynda['chapters'].append({ + 'chapter_id' : chapter_id, + 'chapter_index' : chapter_index, + 'chapter_title' : chapter, + 'lectures' : _temp_lectures, + 'lectures_count' : len(_temp_lectures) + }) + + return _lynda \ No newline at end of file diff --git a/lynda/_extractor.py b/lynda/_extractor.py deleted file mode 100644 index 7ec42f2..0000000 --- a/lynda/_extractor.py +++ /dev/null @@ -1,288 +0,0 @@ -#!/usr/bin/python - -import os -import sys -import json -import time -from pprint import pprint -from .colorized import * -from ._compat import ( - re, - logout, - ex_url, - cc_url, - get_url, - requests, - org_url, - user_url, - sigin_url, - passw_url, - course_url, - std_headers, - compat_urlparse, - xorg_url, - ) -from ._sanitize import ( - slugify, - sanitize, - SLUG_OK -) - -early_py_version = sys.version_info[:2] < (2, 7) -session = requests.Session() - -class LyndaInfoExtractor: - - _TIMECODE_REGEX = r'\[(?P\d+:\d+:\d+[\.,]\d+)\]' - - def match_id(self, url): - course_name = url.split("/")[-2] - if course_name: - return course_name - - def _sanitize(self, unsafetext): - text = sanitize(slugify(unsafetext, lower=False, spaces=True, ok=SLUG_OK + '()-_-')) - return text - - def _get_org_csrf_token(self, login_url): - try: - response = session.get(login_url) - match = re.search(r'name="seasurf"\s+value="(.*)"', response.text) - return match.group(1) - except AttributeError: - session.get(logout) - response = re.search(r'name="seasurf"\s+value="(.*)"', response.text) - return match.group(1) - - def _hidden_inputs(self, form_html): - try: - match = re.search(r'name="-_-"\s+value="(.*)"', str(form_html)) - return {"-_-":match.group(1)} - except AttributeError: - session.get(logout) - response = re.search(r'name="-_-"\s+value="(.*)"', str(form_html)) - return {"-_-":match.group(1)} - - def _login_step(self, form_html, fallback_action_url, extra_form_data, referrer_url): - action_url = fallback_action_url - form_data = self._hidden_inputs(form_html) - form_data.update(extra_form_data) - - try: - response = session.post(action_url, data=form_data, - headers={ - 'Referer': referrer_url, - 'X-Requested-With': 'XMLHttpRequest', - }).json() - except Exception as e: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "Error : {}.".format(e)) - exit(0) - - return response, action_url - - def _extract_organization_url(self, org): - try: - resp = session.get(org_url).text - json_data = json.loads(re.search(r'lynda\s*=\s*(?P{.+?});', resp).group(1)) - except: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "failed to extract csrf token for organization ..") - exit(0) - else: - headers = {'User-Agent' : 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)', - '-_-' : json_data.get('-_-'), 'Referer' : org_url, 'X-Requested-With' : 'XMLHttpRequest'} - data = {'org' : org} - try: - resp = session.post(xorg_url, data=data, headers=headers).json() - except: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "failed to extract json url for organization ..") - exit(0) - else: - url, referer = resp.get('RedirectUrl').replace('http', 'https') if not 'https' in resp.get('RedirectUrl') else resp.get('RedirectUrl'), resp.get('RedirectUrl') - return url, referer - - - def login(self, user, passw, org=None): - if org: - sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as organization : " + fm + sb +"(%s)" % (org) + fg + sb +"...\n") - organization = org - lib_card_num = user - lib_card_pin = passw - login_url, referer = self._extract_organization_url(organization) - csrftoken = self._get_org_csrf_token(referer) - login_data = dict( - libraryCardNumber=str(lib_card_num), - libraryCardPin=str(lib_card_pin), - libraryCardPasswordVerify="", - org=str(organization), - currentView="login", - seasurf=csrftoken - ) - std_headers['Referer'] = referer - response = session.post(login_url, data=login_data, headers=std_headers) - response_text = response.text - try: - name = re.search(r'data-qa="eyebrow_account_menu">(.*)', response_text).group(1)#response_text.split('')[0] - except: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "Logged in failed.") - sys.exit(0) - else: - # print(name) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Logged in successfully.") - else: - username = user - sys.stdout.write(fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to login as user : " + fm + sb +"(%s)" % (username) + fg + sb +"...\n") - password = passw - sigin_page = session.get(sigin_url) - sigin_form = re.search(r'(?s)(]+data-form-name=["\']signin["\'][^>]*>.+?)',sigin_page.text) - form_html = sigin_form.group() - sign_page, signin_url = self._login_step(form_html, passw_url, {'email': username}, sigin_url) - try: - password_form = sign_page["body"] - except KeyError: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "Lynda Says : Sorry, we do not recognize that email or username.") - sys.exit(0) - response, url = self._login_step(password_form, user_url, {'email': username, 'password': password}, signin_url) - response_text = response.get('UserID') if response.get('UserID') else response.get('password') - if response_text != "The username or password is invalid.": - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Logged in successfully.") - else: - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "Lynda Says : The password is invalid") - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fr + sb + "Logged in failed.") - sys.exit(0) - - - def logout(self): - print (fc + sd + "\n[" + fm + sb + "*" + fc + sd + "] : " + fg + sd + "Downloaded course information webpages successfully..") - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Trying to logout now...") - session.get(logout) - print (fc + sd + "[" + fm + sb + "*" + fc + sd + "] : " + fg + sb + "Logged out successfully.") - - - def _generate_dirname(self, title): - ok = re.compile(r'[^/]') - - if os.name == "nt": - ok = re.compile(r'[^\\/:*?"<>|]') - - dirname = "".join(x if ok.match(x) else "_" for x in title) - return dirname - - - def course_videos_count(self, json): - count = 0 - unaccessible_videos = 0 - for chapter in json["Chapters"]: - for video in chapter.get('Videos', []): - if video.get('HasAccess') == False: - unaccessible_videos += 1 - continue - count += 1 - - return count - - def ExtractExerciseFile(self, courseId, courseName): - fileZip = {} - lUrl = [] - cUrl = ex_url % (courseId) - try: - r = session.get(cUrl).json() - ex = r.get("exercisetab") - matchUrl = re.findall('href="(.+)"\srole', ex) - matchName = re.findall('span\sclass="exercise-name">(.+) 0 else [] + self._streams = streams + + def _process_subtitles(self): + subtitles = InternLyndaLectureSubtitles(self._info['subtitles'], self) if self._info['subtitles'].get('subtitle_data') > 0 else {} + self._subtitles = subtitles + + +class InternLyndaLectureStream(LyndaLectureStream): + + def __init__(self, sources, parent): + super(InternLyndaLectureStream, self).__init__(parent) + + self._mediatype = sources.get('type') + self._extension = sources.get('extension') + height = sources.get('height') or 0 + width = sources.get('width') or 0 + self._resolution = '%sx%s' % (width, height) + self._dimention = width, height + self._quality = self._resolution + self._url = sources.get('download_url') + + +class InternLyndaLectureAssets(LyndaLectureAssets): + + def __init__(self, assets, parent): + super(InternLyndaLectureAssets, self).__init__(parent) + + self._mediatype = assets.get('type') + self._extension = assets.get('extension') + self._filename = assets.get('filename') + self._url = assets.get('download_url') + self._fsize = assets.get('file_size') + + +class InternLyndaLectureSubtitles(LyndaLectureSubtitles): + + def __init__(self, subtitles, parent): + super(InternLyndaLectureSubtitles, self).__init__(parent) + + self._mediatype = subtitles.get('type') + self._extension = subtitles.get('extension') + self._language = subtitles.get('language') + self._data = subtitles.get('subtitle_data') \ No newline at end of file diff --git a/lynda/_lynda.py b/lynda/_lynda.py new file mode 100644 index 0000000..ff3ffa1 --- /dev/null +++ b/lynda/_lynda.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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. + +''' + +from ._internal import InternLyndaCourse as Lynda + + +def course(url, username='', password='', organization='', basic=True, callback=None): + """Returns lynda course instance. + + @params: + url : Lynda course url required : type (string). + username : Lynda email account required : type (string). + password : Lynda account password required : type (string) + organization : Lynda organization name optional : type (string) + """ + return Lynda(url, username, password, organization, basic, callback) \ No newline at end of file diff --git a/lynda/_progress.py b/lynda/_progress.py new file mode 100644 index 0000000..51de643 --- /dev/null +++ b/lynda/_progress.py @@ -0,0 +1,87 @@ +#!/usr/bin/python + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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 itertools +from ._compat import ( + os, + sys, + time, + pyver, + ) +from ._colorized import * + +_spin = itertools.cycle(['-', '|', '/', '\\']) + +class ProgressBar(object): + + def _spinner(self, text): + spin = _spin.next() if pyver == 2 else _spin.__next__() + sys.stdout.write(text + spin) + sys.stdout.flush() + time.sleep(0.02) + + # thanks to https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console + def _progress(self, iteration, total, prefix = '' , file_size='' , downloaded = '' , rate = '' ,suffix = '', bar_length = 30): + filledLength = int(round(bar_length * iteration / float(total))) + percents = format(100.00 * (iteration / float(total)), '.2f') + bar = fc + sd + '#' * filledLength + fw + sd +'-' * (bar_length - filledLength) + if '0.00' not in rate: + sys.stdout.write('\033[2K\033[1G\r\r{}{}[{}{}*{}{}] : {}{}{}/{} {}% |{}{}{}| {} {}'.format(fc,sd,fm,sb,fc,sd,fg,sb,file_size,downloaded,percents,bar,fg,sb,rate,suffix)) + sys.stdout.flush() + + def show_progress(self, total, recvd, ratio, rate, eta): + if total <= 1048576: + _total_size = round(float(total) / 1024.00, 2) + _receiving = round(float(recvd) / 1024.00, 2) + _size = format(_total_size if _total_size < 1024.00 else _total_size/1024.00, '.2f') + _received = format(_receiving if _receiving < 1024.00 else _receiving/1024.00,'.2f') + suffix_size = 'KB' if _total_size < 1024.00 else 'MB' + suffix_recvd = 'KB' if _receiving < 1024.00 else 'MB' + else: + _total_size = round(float(total) / 1048576, 2) + _receiving = round(float(recvd) / 1048576, 2) + _size = format(_total_size if _total_size < 1024.00 else _total_size/1024.00, '.2f') + _received = format(_receiving if _receiving < 1024.00 else _receiving/1024.00,'.2f') + suffix_size = 'MB' if _total_size < 1024.00 else 'GB' + suffix_recvd = 'MB' if _receiving < 1024.00 else 'GB' + + _rate = round(float(rate) , 2) + rate = format(_rate if _rate < 1024.00 else _rate/1024.00, '.2f') + suffix_rate = 'kB/s' if _rate < 1024.00 else 'MB/s' + (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" + + self._progress(_receiving, _total_size, file_size = str(_size) + str(suffix_size) ,\ + downloaded = str(_received) + str(suffix_recvd),\ + rate = str(rate) + str(suffix_rate),\ + suffix = str(eta),\ + bar_length = 30) diff --git a/lynda/_shared.py b/lynda/_shared.py new file mode 100644 index 0000000..0e31047 --- /dev/null +++ b/lynda/_shared.py @@ -0,0 +1,688 @@ +#!/usr/bin/python + +''' + +Author : Nasir Khan (r0ot h3x49) +Github : https://github.com/r0oth3x49 +License : MIT + + +Copyright (c) 2018 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 signal +from ._compat import ( + re, + os, + sys, + time, + pyver, + requests, + conn_error, + compat_urlerr, + compat_opener, + compat_request, + compat_urlopen, + compat_httperr, + HEADERS, + ) +from ._colorized import * +early_py_version = sys.version_info[:2] < (2, 7) + +def Interrupt(signal, frame): + sys.stdout.write ('\n' + fc + sd + "[" + fr + sb + "-" + fc + sd + "] : " + fr + sd + "User Interrupted..\n") + sys.exit(0) +signal.signal(signal.SIGINT, Interrupt) + +class LyndaCourse(object): + + def __init__(self, url, username='', password='', organization='', basic=True, callback=None): + + self._url = url + self._username = username + self._password = password + self._organization = organization + self._callback = callback or (lambda x: None) + self._have_basic = False + + self._id = None + self._title = None + self._chapters_count = None + self._total_lectures = None + + self._chapters = [] + self._assets = {} + + if basic: + self._fetch_course() + + def _fetch_course(self): + raise NotImplementedError + + @property + def id(self): + if not self._id: + self._fetch_course() + return self._id + + @property + def title(self): + if not self._title: + self._fetch_course() + return self._title + + @property + def asset(self): + if not self._assets: + self._process_assets() + return self._assets + + @property + def chapters(self): + if not self._chapters_count: + self._fetch_course() + return self._chapters_count + + @property + def lectures(self): + if not self._total_lectures: + self._fetch_course() + return self._total_lectures + + def get_chapters(self): + if not self._chapters: + self._fetch_course() + return self._chapters + + def create_chapter(self, filepath=''): + try: + os.makedirs(filepath) + except Exception as e: + return {'msg' : 'exists'} + else: + return {'msg' : 'created'} + +class LyndaChapters(object): + + def __init__(self): + + self._chapter_id = None + self._chapter_index = None + self._chapter_title = None + self._lectures_count = None + + self._lectures = [] + + def __repr__(self): + chapter = "{title}".format(title=self.title) + return chapter + + @property + def id(self): + return self._chapter_id + + @property + def index(self): + return self._chapter_index + + @property + def title(self): + return self._chapter_title + + @property + def lectures(self): + return self._lectures_count + + def get_lectures(self): + return self._lectures + +class LyndaLectures(object): + + def __init__(self): + + self._best = None + self._duration = None + self._extension = None + self._lecture_id = None + self._lecture_title = None + self._lecture_index = None + self._sources_count = None + + self._streams = [] + self._subtitles = [] + + def __repr__(self): + lecture = "{title}".format(title=self.title) + return lecture + + @property + def id(self): + return self._lecture_id + + @property + def index(self): + return self._lecture_index + + @property + def title(self): + return self._lecture_title + + @property + def duration(self): + return self._duration + + @property + def extension(self): + return self._extension + + @property + def streams(self): + if not self._streams: + self._process_streams() + return self._streams + + @property + def subtitles(self): + if not self._subtitles: + self._process_subtitles() + return self._subtitles + + def _getbest(self): + streams = self.streams + if not streams: + return None + def _sortkey(x, keyres=0, keyftype=0): + keyres = int(x.resolution.split('x')[0]) + keyftype = x.extension + st = (keyftype, keyres) + return st + + self._best = max(streams, key=_sortkey) + if self._best.get_filesize() == 0: + self._best = max(streams[::-1], key=_sortkey) + return self._best + + def getbest(self): + return self._getbest() + + def get_quality(self, best_quality='', streams='', requested=''): + if requested: + index = 0 + while index < len(streams): + dimension = int(streams[index].dimention[1]) + if dimension == requested: + best = streams[index] + break + index += 1 + if not best: + best = best_quality + if not requested: + best = best_quality + return best + +class LyndaLectureStream(object): + + def __init__(self, parent): + + self._mediatype = None + self._quality = None + self._resolution = None + self._dimention = None + self._extension = None + self._url = None + + self._parent = parent + self._filename = None + self._fsize = None + self._active = False + + def __repr__(self): + out = "%s:%s@%s" % (self.mediatype, self.extension, self.quality) + return out + + def _generate_filename(self): + ok = re.compile(r'[^\\/:*?"<>|]') + filename = "".join(x if ok.match(x) else "_" for x in self.title) + filename += "." + self.extension + return filename + + @property + def resolution(self): + return self._resolution + + @property + def quality(self): + return self._quality + + @property + def url(self): + return self._url + + @property + def id(self): + return self._parent.id + + @property + def dimention(self): + return self._dimention + + @property + def extension(self): + return self._extension + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() + return self._filename + + @property + def title(self): + return self._parent.title + + @property + def mediatype(self): + return self._mediatype + + def get_filesize(self): + if not self._fsize: + try: + cl = 'content-length' + self._fsize = requests.get(self.url, stream=True, headers={'User-Agent': HEADERS.get('User-Agent')}).headers[cl] + except conn_error as e: + self._fsize = 0 + return self._fsize + + + def download(self, filepath="", quiet=False, callback=lambda *x: None): + savedir = filename = "" + retVal = {} + + if filepath and os.path.isdir(filepath): + savedir, filename = filepath, self.filename + + elif filepath: + savedir, filename = os.path.split(filepath) + + else: + filename = self.filename + + filepath = os.path.join(savedir, filename) + + if os.path.isfile(filepath): + retVal = {"status" : "True", "msg" : "already downloaded"} + return retVal + + temp_filepath = filepath + ".part" + + status_string = (' {:,} Bytes [{:.2%}] received. Rate: [{:4.0f} ' + 'KB/s]. ETA: [{:.0f} secs]') + + + if early_py_version: + status_string = (' {0:} Bytes [{1:.2%}] received. Rate:' + ' [{2:4.0f} KB/s]. ETA: [{3:.0f} secs]') + + try: + req = compat_request(self.url, headers={'User-Agent' : HEADERS.get('User-Agent')}) + response = compat_urlopen(req) + except compat_urlerr as e: + retVal = {"status" : "False", "msg" : "URLError : either your internet connection is not working or server aborted the request"} + return retVal + except compat_httperr as e: + if e.code == 401: + retVal = {"status" : "False", "msg" : "Udemy Says (HTTP Error 401 : Unauthorized)"} + else: + retVal = {"status" : "False", "msg" : "HTTPError-{} : direct download link is expired run the udemy-dl with '--skip-sub' option ...".format(e.code)} + return retVal + else: + total = int(response.info()['Content-Length'].strip()) + chunksize, bytesdone, t0 = 16384, 0, time.time() + + fmode, offset = "wb", 0 + + if os.path.exists(temp_filepath): + if os.stat(temp_filepath).st_size < total: + offset = os.stat(temp_filepath).st_size + fmode = "ab" + + try: + outfh = open(temp_filepath, fmode) + except Exception as e: + if os.name == 'nt': + file_length = len(temp_filepath) + if file_length > 255: + retVal = {"status" : "False", "msg" : "file length is too long to create. try downloading to other drive (e.g :- -o 'E:\\')"} + return retVal + retVal = {"status" : "False", "msg" : "Reason : {}".format(e)} + return retVal + + if offset: + resume_opener = compat_opener() + resume_opener.addheaders = [('User-Agent', HEADERS.get('User-Agent')), + ("Range", "bytes=%s-" % offset)] + try: + response = resume_opener.open(self.url) + except compat_urlerr as e: + retVal = {"status" : "False", "msg" : "URLError : either your internet connection is not working or server aborted the request"} + return retVal + except compat_httperr as e: + if e.code == 401: + retVal = {"status" : "False", "msg" : "Udemy Says (HTTP Error 401 : Unauthorized)"} + else: + retVal = {"status" : "False", "msg" : "HTTPError-{} : direct download link is expired run the udemy-dl with '--skip-sub' option ...".format(e.code)} + return retVal + else: + bytesdone = offset + + self._active = True + while self._active: + chunk = response.read(chunksize) + outfh.write(chunk) + elapsed = time.time() - t0 + bytesdone += len(chunk) + if elapsed: + try: + rate = ((float(bytesdone) - float(offset)) / 1024.0) / elapsed + eta = (total - bytesdone) / (rate * 1024.0) + except ZeroDivisionError as e: + outfh.close() + try: + os.unlink(temp_filepath) + except Exception as e: + pass + retVal = {"status" : "False", "msg" : "ZeroDivisionError : it seems, lecture has malfunction or is zero byte(s) .."} + return retVal + else: + rate = 0 + eta = 0 + progress_stats = (bytesdone, bytesdone * 1.0 / total, rate, eta) + + if not chunk: + outfh.close() + break + if not quiet: + status = status_string.format(*progress_stats) + sys.stdout.write("\r" + status + ' ' * 4 + "\r") + sys.stdout.flush() + + if callback: + callback(total, *progress_stats) + + if self._active: + os.rename(temp_filepath, filepath) + retVal = {"status" : "True", "msg" : "download"} + else: + outfh.close() + retVal = {"status" : "True", "msg" : "download"} + + return retVal + +class LyndaLectureSubtitles(object): + + def __init__(self, parent): + + self._mediatype = None + self._extension = None + self._language = None + self._data = None + + self._parent = parent + self._filename = None + + def __repr__(self): + out = "%s:%s@%s" % (self.mediatype, self.language, self.extension) + return out + + def _generate_filename(self): + ok = re.compile(r'[^\\/:*?"<>|]') + filename = "".join(x if ok.match(x) else "_" for x in self.title) + filename += "-{}.{}".format(self.language, self.extension) + return filename + + @property + def id(self): + return self._parent.id + + @property + def data(self): + return self._data + + @property + def extension(self): + return self._extension + + @property + def language(self): + return self._language + + @property + def title(self): + return self._parent.title + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() + return self._filename + + @property + def mediatype(self): + return self._mediatype + + def download(self, filepath=""): + savedir = filename = "" + retVal = {} + + if filepath and os.path.isdir(filepath): + savedir, filename = filepath, self.filename + + elif filepath: + savedir, filename = os.path.split(filepath) + + else: + filename = self.filename + + filepath = os.path.join(savedir, filename) + + if os.path.isfile(filepath): + retVal = {"status" : "True", "msg" : "already downloaded"} + return retVal + + with open(filepath, 'w') as f: + try: + f.write(self.data) + except Exception as e: + retVal = {'status' : 'False', 'msg' : '{}'.format(e)} + else: + retVal = {'status' : 'True', 'msg' : 'download'} + f.close() + return retVal + +class LyndaLectureAssets(object): + + def __init__(self, parent): + + self._extension = None + self._mediatype = None + self._url = None + + self._parent = parent + self._filename = None + self._fsize = None + self._active = False + + def __repr__(self): + out = "%s:%s@%s" % (self.mediatype, self.extension, self.extension) + return out + + def _generate_filename(self): + ok = re.compile(r'[^\\/:*?"<>|]') + filename = "".join(x if ok.match(x) else "_" for x in self.title) + filename += ".{}".format(self.extension) + return filename + + @property + def id(self): + return self._parent.id + + @property + def url(self): + return self._url + + @property + def extension(self): + return self._extension + + @property + def title(self): + return self._parent.title + + @property + def filename(self): + if not self._filename: + self._filename = self._generate_filename() + return self._filename + + @property + def mediatype(self): + return self._mediatype + + def get_filesize(self): + return self._fsize + + + def download(self, filepath="", quiet=False, callback=lambda *x: None): + savedir = filename = "" + retVal = {} + + if filepath and os.path.isdir(filepath): + savedir, filename = filepath, self.filename + + elif filepath: + savedir, filename = os.path.split(filepath) + + else: + filename = self.filename + + filepath = os.path.join(savedir, filename) + + if os.path.isfile(filepath): + retVal = {"status" : "True", "msg" : "already downloaded"} + return retVal + + temp_filepath = filepath + ".part" + + status_string = (' {:,} Bytes [{:.2%}] received. Rate: [{:4.0f} ' + 'KB/s]. ETA: [{:.0f} secs]') + + + if early_py_version: + status_string = (' {0:} Bytes [{1:.2%}] received. Rate:' + ' [{2:4.0f} KB/s]. ETA: [{3:.0f} secs]') + + try: + req = compat_request(self.url, headers={'User-Agent' : HEADERS.get('User-Agent')}) + response = compat_urlopen(req) + except compat_urlerr as e: + retVal = {"status" : "False", "msg" : "URLError : either your internet connection is not working or server aborted the request"} + return retVal + except compat_httperr as e: + if e.code == 401: + retVal = {"status" : "False", "msg" : "Udemy Says (HTTP Error 401 : Unauthorized)"} + else: + retVal = {"status" : "False", "msg" : "HTTPError-{} : direct download link is expired run the udemy-dl with '--skip-sub' option ...".format(e.code)} + return retVal + else: + total = int(response.info()['Content-Length'].strip()) + chunksize, bytesdone, t0 = 16384, 0, time.time() + + fmode, offset = "wb", 0 + + if os.path.exists(temp_filepath): + if os.stat(temp_filepath).st_size < total: + offset = os.stat(temp_filepath).st_size + fmode = "ab" + + try: + outfh = open(temp_filepath, fmode) + except Exception as e: + if os.name == 'nt': + file_length = len(temp_filepath) + if file_length > 255: + retVal = {"status" : "False", "msg" : "file length is too long to create. try downloading to other drive (e.g :- -o 'E:\\')"} + return retVal + retVal = {"status" : "False", "msg" : "Reason : {}".format(e)} + return retVal + + if offset: + resume_opener = compat_opener() + resume_opener.addheaders = [('User-Agent', HEADERS.get('User-Agent')), + ("Range", "bytes=%s-" % offset)] + try: + response = resume_opener.open(self.url) + except compat_urlerr as e: + retVal = {"status" : "False", "msg" : "URLError : either your internet connection is not working or server aborted the request"} + return retVal + except compat_httperr as e: + if e.code == 401: + retVal = {"status" : "False", "msg" : "Udemy Says (HTTP Error 401 : Unauthorized)"} + else: + retVal = {"status" : "False", "msg" : "HTTPError-{} : direct download link is expired run the udemy-dl with '--skip-sub' option ...".format(e.code)} + return retVal + else: + bytesdone = offset + + self._active = True + while self._active: + chunk = response.read(chunksize) + outfh.write(chunk) + elapsed = time.time() - t0 + bytesdone += len(chunk) + if elapsed: + try: + rate = ((float(bytesdone) - float(offset)) / 1024.0) / elapsed + eta = (total - bytesdone) / (rate * 1024.0) + except ZeroDivisionError as e: + outfh.close() + try: + os.unlink(temp_filepath) + except Exception as e: + pass + retVal = {"status" : "False", "msg" : "ZeroDivisionError : it seems, lecture has malfunction or is zero byte(s) .."} + return retVal + else: + rate = 0 + eta = 0 + progress_stats = (bytesdone, bytesdone * 1.0 / total, rate, eta) + + if not chunk: + outfh.close() + break + if not quiet: + status = status_string.format(*progress_stats) + sys.stdout.write("\r" + status + ' ' * 4 + "\r") + sys.stdout.flush() + + if callback: + callback(total, *progress_stats) + + if self._active: + os.rename(temp_filepath, filepath) + retVal = {"status" : "True", "msg" : "download"} + else: + outfh.close() + retVal = {"status" : "True", "msg" : "download"} + + return retVal \ No newline at end of file diff --git a/lynda/colorized/Output.py b/lynda/colorized/Output.py deleted file mode 100644 index 3f8a237..0000000 --- a/lynda/colorized/Output.py +++ /dev/null @@ -1,53 +0,0 @@ -from colorama import init,Fore,Back,Style -import os - -if os.name == "posix": - # colors foreground text: - fc = "\033[0;96m" - fg = "\033[0;92m" - fw = "\033[0;97m" - fr = "\033[0;91m" - fb = "\033[0;94m" - fy = "\033[0;33m" - fm = "\033[0;35m" - - # colors background text: - bc = "\033[46m" - bg = "\033[42m" - bw = "\033[47m" - br = "\033[41m" - bb = "\033[44m" - by = "\033[43m" - bm = "\033[45m" - - # colors style text: - sd = Style.DIM - sn = Style.NORMAL - sb = Style.BRIGHT -else: - ## ---------------------------------------------------------------------------------------------------------------------- ## - init(autoreset=True) - # colors foreground text: - fc = Fore.CYAN - fg = Fore.GREEN - fw = Fore.WHITE - fr = Fore.RED - fb = Fore.BLUE - fy = Fore.YELLOW - fm = Fore.MAGENTA - - - # colors background text: - bc = Back.CYAN - bg = Back.GREEN - bw = Back.WHITE - br = Back.RED - bb = Back.BLUE - by = Fore.YELLOW - bm = Fore.MAGENTA - - # colors style text: - sd = Style.DIM - sn = Style.NORMAL - sb = Style.BRIGHT - ## ---------------------------------------------------------------------------------------------------------------------- ## diff --git a/lynda/colorized/__init__.py b/lynda/colorized/__init__.py deleted file mode 100644 index 0b742ed..0000000 --- a/lynda/colorized/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .Output import * -from . import banner diff --git a/lynda/colorized/banner.py b/lynda/colorized/banner.py deleted file mode 100644 index 6ca9d2c..0000000 --- a/lynda/colorized/banner.py +++ /dev/null @@ -1,16 +0,0 @@ -from .Output import * - -def banner(): - banner = """%s%s -+-------------------------------------------------------------------------------+ -| oooo .o8 .o8 oooo | -| `888 "888 "888 `888 | -| 888 oooo ooo ooo. .oo. .oooo888 .oooo. .oooo888 888 | -|%s%s 888 `88. .8' `888P"Y88b d88' `888 `P )88b d88' `888 888%s%s | -|%s%s 888 `88..8' 888 888 888 888 .oP"888 8888888 888 888 888%s%s | -| 888 `888' 888 888 888 888 d8( 888 888 888 888 | -| o888o .8' o888o o888o `Y8bod88P" `Y888""8o `Y8bod88P" o888o | -| .o..P' | -| `Y8P' %s%sCoded By : %s%sNasir Khan (r0ot h3x49)%s%s | -+-------------------------------------------------------------------------------+\n""" % (fc, sb, fm, sb, fc, sb, fm, sb, fc, sb, fy,sb, fg, sd, fc, sb) - return banner diff --git a/requirements.txt b/requirements.txt index 472ae3c..3c0b657 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ +requests[security] +six colorama requests -six -unidecode \ No newline at end of file +unidecode +pyOpenSSL