vincegeratorix

edx-dl

Dec 21st, 2013
323
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.20 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. # python 2/3 compatibility imports
  5. from __future__ import print_function
  6. from __future__ import unicode_literals
  7.  
  8. try:
  9.     from http.cookiejar import CookieJar
  10. except ImportError:
  11.     from cookielib import CookieJar
  12.  
  13. try:
  14.     from urllib.parse import urlencode
  15. except ImportError:
  16.     from urllib import urlencode
  17.  
  18. try:
  19.     from urllib.request import urlopen
  20.     from urllib.request import build_opener
  21.     from urllib.request import install_opener
  22.     from urllib.request import HTTPCookieProcessor
  23.     from urllib.request import Request
  24.     from urllib.request import URLError
  25. except ImportError:
  26.     from urllib2 import urlopen
  27.     from urllib2 import build_opener
  28.     from urllib2 import install_opener
  29.     from urllib2 import HTTPCookieProcessor
  30.     from urllib2 import Request
  31.     from urllib2 import URLError
  32. # we alias the raw_input function for python 3 compatibility
  33. try:
  34.     input = raw_input
  35. except:
  36.     pass
  37.  
  38. import getopt
  39. import getpass
  40.  
  41. import json
  42. import os
  43. import re
  44. import sys
  45. from subprocess import Popen, PIPE
  46. from datetime import timedelta, datetime
  47. from bs4 import BeautifulSoup
  48.  
  49. BASE_URL = 'https://courses.edx.org'
  50. EDX_HOMEPAGE = BASE_URL + '/login_ajax'
  51. LOGIN_API = BASE_URL + '/login_ajax'
  52. DASHBOARD = BASE_URL + '/dashboard'
  53.  
  54. YOUTUBE_VIDEO_ID_LENGTH = 11
  55.  
  56.  
  57. ## If no download directory is specified, we use the default one
  58. DEFAULT_DOWNLOAD_DIRECTORY = "./Downloaded/"
  59. DOWNLOAD_DIRECTORY = DEFAULT_DOWNLOAD_DIRECTORY
  60.  
  61. ## If nothing else is chosen, we chose the default user agent:
  62.  
  63. DEFAULT_USER_AGENTS = {"google-chrome": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.63 Safari/537.31",
  64.                        "firefox": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0",
  65.                        "default": 'edX-downloader/0.01'}
  66.  
  67. USER_AGENT = DEFAULT_USER_AGENTS["default"]
  68.  
  69. USER_EMAIL = ""
  70. USER_PSWD = ""
  71.  
  72.  
  73. def get_initial_token():
  74.     """
  75.    Create initial connection to get authentication token for future requests.
  76.  
  77.    Returns a string to be used in subsequent connections with the
  78.    X-CSRFToken header or the empty string if we didn't find any token in
  79.    the cookies.
  80.    """
  81.     cj = CookieJar()
  82.     opener = build_opener(HTTPCookieProcessor(cj))
  83.     install_opener(opener)
  84.     opener.open(EDX_HOMEPAGE)
  85.  
  86.     for cookie in cj:
  87.         if cookie.name == 'csrftoken':
  88.             return cookie.value
  89.  
  90.     return ''
  91.  
  92.  
  93. def get_page_contents(url, headers):
  94.     """
  95.    Get the contents of the page at the URL given by url. While making the
  96.    request, we use the headers given in the dictionary in headers.
  97.    """
  98.     result = urlopen(Request(url, None, headers))
  99.     try:
  100.         charset = result.headers.get_content_charset(failobj="utf-8")  #for python3
  101.     except:
  102.         charset = result.info().getparam('charset') or 'utf-8'
  103.     return result.read().decode(charset)
  104.  
  105. def directory_name(initial_name):
  106.     import string
  107.     allowed_chars = string.digits+string.ascii_letters+" _."
  108.     result_name = ""
  109.     for ch in initial_name:
  110.         if allowed_chars.find(ch) != -1:
  111.             result_name+=ch
  112.     return result_name if result_name != "" else "course_folder"
  113.  
  114. def parse_commandline_options(argv):
  115.     global USER_EMAIL, USER_PSWD, DOWNLOAD_DIRECTORY, USER_AGENT
  116.     opts, args = getopt.getopt(argv,
  117.                                "u:p:",
  118.                                ["download-dir=", "user-agent=", "custom-user-agent="])
  119.     for opt, arg in opts :
  120.         if opt == "-u" :
  121.             USER_EMAIL = arg
  122.  
  123.         elif opt == "-p" :
  124.             USER_PSWD = arg
  125.  
  126.         elif opt == "--download-dir" :
  127.             if arg.strip()[0] == "~" :
  128.                 arg = os.path.expanduser(arg)
  129.             DOWNLOAD_DIRECTORY = arg
  130.  
  131.         elif opt == "--user-agent" :
  132.             if arg in DEFAULT_USER_AGENTS.keys():
  133.                 USER_AGENT = DEFAULT_USER_AGENTS[arg]
  134.  
  135.  
  136.         elif opt == "--custom-user-agent":
  137.             USER_AGENT = arg
  138.  
  139.         elif opt == "-h":
  140.             usage()
  141.  
  142.  
  143. def usage() :
  144.     print("command-line options:")
  145.     print("""-u <username>: (Optional) indicate the username.
  146. -p <password>: (Optional) indicate the password.
  147. --download-dir=<path>: (Optional) save downloaded files in <path>
  148. --user-agent=<chrome|firefox>: (Optional) use Google Chrome's of Firefox 24's
  149.             default user agent as user agent
  150. --custom-user-agent="MYUSERAGENT": (Optional) use the string "MYUSERAGENT" as
  151.             user agent
  152. """)
  153.  
  154.  
  155. def json2srt(o):
  156.     i = 1
  157.     output = ''
  158.     for (s, e, t) in zip(o['start'], o['end'], o['text']):
  159.         if t == "":
  160.             continue
  161.         output += str(i) + '\n'
  162.         s = datetime(1, 1, 1) + timedelta(seconds=s/1000.)
  163.         e = datetime(1, 1, 1) + timedelta(seconds=e/1000.)
  164.         output += "%02d:%02d:%02d,%03d --> %02d:%02d:%02d,%03d" % \
  165.               (s.hour, s.minute, s.second, s.microsecond/1000,
  166.                e.hour, e.minute, e.second, e.microsecond/1000) + '\n'
  167.         output += t + "\n\n"
  168.         i += 1
  169.     return output
  170.  
  171. def main():
  172.     global USER_EMAIL, USER_PSWD
  173.     try:
  174.         parse_commandline_options(sys.argv[1:])
  175.     except getopt.GetoptError:
  176.         usage()
  177.         sys.exit(2)
  178.  
  179.     if USER_EMAIL == "":
  180.         USER_EMAIL = input('Username: ')
  181.     if USER_PSWD == "":
  182.         USER_PSWD = getpass.getpass()
  183.  
  184.     if USER_EMAIL == "" or USER_PSWD == "":
  185.         print("You must supply username AND password to log-in")
  186.         sys.exit(2)
  187.  
  188.     # Prepare Headers
  189.     headers = {
  190.         'User-Agent': USER_AGENT,
  191.         'Accept': 'application/json, text/javascript, */*; q=0.01',
  192.         'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
  193.         'Referer': EDX_HOMEPAGE,
  194.         'X-Requested-With': 'XMLHttpRequest',
  195.         'X-CSRFToken': get_initial_token(),
  196.     }
  197.  
  198.     # Login
  199.     post_data = urlencode({'email': USER_EMAIL, 'password': USER_PSWD,
  200.                            'remember': False}).encode('utf-8')
  201.     request = Request(LOGIN_API, post_data, headers)
  202.     response = urlopen(request)
  203.     resp = json.loads(response.read().decode('utf-8'))
  204.     if not resp.get('success', False):
  205.         print(resp.get('value', "Wrong Email or Password."))
  206.         exit(2)
  207.  
  208.     # Get user info/courses
  209.     dash = get_page_contents(DASHBOARD, headers)
  210.     soup = BeautifulSoup(dash)
  211.     data = soup.find_all('ul')[1]
  212.     USERNAME = data.find_all('span')[1].string
  213.     USEREMAIL = data.find_all('span')[3].string
  214.     COURSES = soup.find_all('article', 'course')
  215.     courses = []
  216.     for COURSE in COURSES:
  217.         c_name = COURSE.h3.text.strip()
  218.         c_link = 'https://courses.edx.org' + COURSE.a['href']
  219.         if c_link.endswith('info') or c_link.endswith('info/'):
  220.             state = 'Started'
  221.         else:
  222.             state = 'Not yet'
  223.         courses.append((c_name, c_link, state))
  224.     numOfCourses = len(courses)
  225.  
  226.     # Welcome and Choose Course
  227.  
  228.     print('Welcome %s' % USERNAME)
  229.     print('You can access %d courses on edX' % numOfCourses)
  230.  
  231.     c = 0
  232.     for course in courses:
  233.         c += 1
  234.         print('%d - %s -> %s' % (c, course[0], course[2]))
  235.  
  236.     c_number = int(input('Enter Course Number: '))
  237.     while c_number > numOfCourses or courses[c_number - 1][2] != 'Started':
  238.         print('Enter a valid Number for a Started Course ! between 1 and ',
  239.               numOfCourses)
  240.         c_number = int(input('Enter Course Number: '))
  241.     selected_course = courses[c_number - 1]
  242.     COURSEWARE = selected_course[1].replace('info', 'courseware')
  243.  
  244.     ## Getting Available Weeks
  245.     courseware = get_page_contents(COURSEWARE, headers)
  246.     soup = BeautifulSoup(courseware)
  247.  
  248.     data = soup.find("section",
  249.                      {"class": "content-wrapper"}).section.div.div.nav
  250.     WEEKS = data.find_all('div')
  251.     weeks = [(w.h3.a.string, ['https://courses.edx.org' + a['href'] for a in
  252.              w.ul.find_all('a')]) for w in WEEKS]
  253.     numOfWeeks = len(weeks)
  254.  
  255.     # Choose Week or choose all
  256.     print('%s has %d weeks so far' % (selected_course[0], numOfWeeks))
  257.     w = 0
  258.     for week in weeks:
  259.         w += 1
  260.         print('%d - Download %s videos' % (w, week[0].strip()))
  261.     print('%d - Download them all' % (numOfWeeks + 1))
  262.  
  263.     w_number = int(input('Enter Your Choice: '))
  264.     while w_number > numOfWeeks + 1:
  265.         print('Enter a valid Number between 1 and %d' % (numOfWeeks + 1))
  266.         w_number = int(input('Enter Your Choice: '))
  267.  
  268.     if w_number == numOfWeeks + 1:
  269.         links = [link for week in weeks for link in week[1]]
  270.     else:
  271.         links = weeks[w_number - 1][1]
  272.  
  273.     video_id = []
  274.     subsUrls = []
  275.     regexpSubs = re.compile(r'data-caption-asset-path=(?:&#34;|")([^"&]*)(?:&#34;|")')
  276.     splitter = re.compile(r'data-streams=(?:&#34;|").*1.0[0]*:')
  277.     extra_youtube = re.compile(r'//w{0,3}\.youtube.com/embed/([^ \?&]*)[\?& ]')
  278.     for link in links:
  279.         print("Processing '%s'..." % link)
  280.         page = get_page_contents(link, headers)
  281.        
  282.         id_container = splitter.split(page)[1:]
  283.         video_id += [link[:YOUTUBE_VIDEO_ID_LENGTH] for link in
  284.                      id_container]
  285.         subsUrls += [BASE_URL + regexpSubs.search(container).group(1) + id + ".srt.sjson"
  286.                         for id, container in zip(video_id[-len(id_container):], id_container)]
  287.         # Try to download some extra videos which is referred by iframe
  288.         extra_ids = extra_youtube.findall(page)
  289.         video_id += [link[:YOUTUBE_VIDEO_ID_LENGTH] for link in
  290.                      extra_ids]
  291.         subsUrls += ['' for link in extra_ids]
  292.  
  293.     video_link = ['http://youtube.com/watch?v=' + v_id
  294.                   for v_id in video_id]
  295.  
  296.     if (len(video_link) < 1):
  297.       print('WARNING: No downloadable video found. ')
  298.       sys.exit(0)
  299.     # Get Available Video_Fmts
  300.     os.system('youtube-dl -F %s' % video_link[-1])
  301.     video_fmt = int(input('Choose Format code: '))
  302.  
  303.     # Get subtitles
  304.     youtube_subs = False
  305.     edx_subs = False
  306.  
  307.     down_subs = input('Download subtitles (y/n)? ')
  308.     if str.lower(down_subs) == 'y':
  309.         print('1 - YouTube with fallback from edX (default)')
  310.         print('2 - YouTube only')
  311.         print('3 - edX only')
  312.         try:
  313.             down_subs = int(input("Get from: "))
  314.             if down_subs not in (1, 2, 3):
  315.                 raise ValueError
  316.         except ValueError:
  317.             down_subs = 1
  318.  
  319.         if down_subs == 1:
  320.             youtube_subs = True
  321.             edx_subs = True
  322.             print("Selected: YouTube with fallback from edX")
  323.         elif down_subs == 2:
  324.             youtube_subs = True
  325.             print("Selected: YouTube only")
  326.         elif down_subs == 3:
  327.             edx_subs = True
  328.             print("Selected: edX's subs only")
  329.  
  330.     # Say where it's gonna download files, just for clarity's sake.
  331.     print("[download] Saving videos into: " + DOWNLOAD_DIRECTORY)
  332.  
  333.     # Download Videos
  334.     c = 0
  335.     for v, s in zip(video_link, subsUrls):
  336.         c += 1
  337.         cmd = ["youtube-dl", "-o", DOWNLOAD_DIRECTORY + '/' + directory_name(selected_course[0]) + '/' +
  338.                                    str(c).zfill(2) + "-%(title)s.%(ext)s", "-f", str(video_fmt)]
  339.         if youtube_subs:
  340.             cmd.append('--write-sub')
  341.         cmd.append(str(v))
  342.  
  343.         popen_youtube = Popen(cmd, stdout=PIPE, stderr=PIPE)
  344.  
  345.         youtube_stdout = b''
  346.         enc = sys.getdefaultencoding()
  347.         while True:  # Save the output to youtube_stdout while this being echoed
  348.             tmp = popen_youtube.stdout.read(1)
  349.             youtube_stdout += tmp
  350.             print(tmp.decode(enc), end="")
  351.             sys.stdout.flush()
  352.             # do it until the process finish and there isn't output
  353.             if tmp == b"" and popen_youtube.poll() is not None:
  354.                 break
  355.  
  356.         if youtube_subs:
  357.             youtube_stderr = popen_youtube.communicate()[1]
  358.             if re.search(b'Some error while getting the subtitles', youtube_stderr):
  359.                 if edx_subs:
  360.                     print("YouTube hasn't subtitles. Fallbacking from edX")
  361.                 else:
  362.                     print("WARNING: Subtitles missing")
  363.  
  364.         if edx_subs and s != '':  # write edX subs
  365.             try:
  366.                 jsonString = get_page_contents(s, headers)
  367.                 jsonObject = json.loads(jsonString)
  368.                 subs_string = json2srt(jsonObject)
  369.  
  370.                 regexp_filename = re.compile(
  371.                     b'(?:\[download\] ([^\n^\r]*?)(?: has already been downloaded))|(?:Destination: *([^\n^\r]*))')
  372.                 match = re.search(regexp_filename, youtube_stdout)
  373.                 subs_filename = (match.group(1) or match.group(2)).decode('utf-8')[:-4]
  374.                 print('[download] ed-x subtitles: %s' % subs_filename+'.srt')
  375.                 open(os.path.join(os.getcwd(), subs_filename)+'.srt', 'wb+').write(subs_string.encode('utf-8'))
  376.             except URLError as e:
  377.                 print('Warning: edX subtitles (error:%s)' % e.reason)
  378.  
  379.  
  380. if __name__ == '__main__':
  381.     try:
  382.         main()
  383.     except KeyboardInterrupt :
  384.         print("\n\nCTRL-C detected, shutting down....")
  385.         sys.exit(0)
Advertisement
Add Comment
Please, Sign In to add comment