├── LICENSE ├── README.md └── tutsplus-dl.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Wan Liuyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tutsplus-dl 2 | 3 | [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=1473706)](https://www.bountysource.com/trackers/1473706-sfdye-tutsplus-dl?utm_source=1473706&utm_medium=shield&utm_campaign=TRACKER_BADGE) 4 | 5 | Inspired by [coursera-dl][coursera-dl], `tutsplus-dl` is a tool to batch download course videos from tut+ premium (e.g. the amazing [Perfect Workflow in Sublime Text 2][sublime_course] by Jeffrey Way). 6 | 7 | [coursera-dl]: https://github.com/coursera-dl/coursera 8 | [sublime_course]: https://tutsplus.com/course/improve-workflow-in-sublime-text-2/ 9 | 10 | ## Features 11 | * Batch download all videos from one course 12 | * Automatic renaming videos and group by directory 13 | 14 | ## Install dependencies 15 | 16 | * Requests: `pip install requests` 17 | * BeautifulSoup: `pip install beautifulsoup4` 18 | 19 | ## Usage 20 | `python tutsplus.py url_to_course` 21 | 22 | ## List of free tut+ premium course 23 | 24 | * [Perfect Workflow in Sublime Text 2](https://tutsplus.com/course/improve-workflow-in-sublime-text-2/) 25 | * [jQuery UI 101: The Essentials](https://tutsplus.com/course/jquery-ui-101-the-essentials/) 26 | * [Let’s Learn Ember](https://tutsplus.com/course/lets-learn-ember/) 27 | * [Hands-on Angular](https://tutsplus.com/course/hands-on-angular/) 28 | * [Android for the Busy Developer](https://tutsplus.com/course/android-for-the-busy-developer/) 29 | * [WebFormDesignAndDev](https://tutsplus.com/course/web-form-design-and-development/) 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tutsplus-dl.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import math 5 | import requests 6 | from bs4 import BeautifulSoup 7 | 8 | def format_bytes(bytes): 9 | """ 10 | Get human readable version of given bytes. 11 | Ripped from https://github.com/rg3/youtube-dl 12 | """ 13 | if bytes is None: 14 | return 'N/A' 15 | if type(bytes) is str: 16 | bytes = float(bytes) 17 | if bytes == 0.0: 18 | exponent = 0 19 | else: 20 | exponent = int(math.log(bytes, 1024.0)) 21 | suffix = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][exponent] 22 | converted = float(bytes) / float(1024 ** exponent) 23 | return '{0:.2f}{1}'.format(converted, suffix) 24 | 25 | class DownloadProgress(object): 26 | """ 27 | Report download progress. 28 | Inspired by https://github.com/rg3/youtube-dl 29 | """ 30 | 31 | def __init__(self, total): 32 | if total in [0, '0', None]: 33 | self._total = None 34 | else: 35 | self._total = int(total) 36 | 37 | self._current = 0 38 | self._start = 0 39 | self._now = 0 40 | 41 | self._finished = False 42 | 43 | def start(self): 44 | self._now = time.time() 45 | self._start = self._now 46 | 47 | def stop(self): 48 | self._now = time.time() 49 | self._finished = True 50 | self._total = self._current 51 | self.report_progress() 52 | 53 | def read(self, bytes): 54 | self._now = time.time() 55 | self._current += bytes 56 | self.report_progress() 57 | 58 | def calc_percent(self): 59 | if self._total is None: 60 | return '--%' 61 | percentage = int(float(self._current) / float(self._total) * 100.0) 62 | done = int(percentage/2) 63 | return '[{0: <50}] {1}%'.format(done * '#', percentage) 64 | 65 | def calc_speed(self): 66 | dif = self._now - self._start 67 | if self._current == 0 or dif < 0.001: # One millisecond 68 | return '---b/s' 69 | return '{0}/s'.format(format_bytes(float(self._current) / dif)) 70 | 71 | def report_progress(self): 72 | """Report download progress.""" 73 | percent = self.calc_percent() 74 | total = format_bytes(self._total) 75 | 76 | speed = self.calc_speed() 77 | total_speed_report = '{0} at {1}'.format(total, speed) 78 | 79 | report = '\r{0: <56} {1: >30}'.format(percent, total_speed_report) 80 | 81 | if self._finished: 82 | print report 83 | else: 84 | print report 85 | print 86 | sys.stdout.flush() 87 | 88 | 89 | def download(url): 90 | """ 91 | download single file from url 92 | """ 93 | 94 | base_dir = os.path.dirname(__file__) 95 | 96 | r = requests.get(url, stream=True) 97 | course_name = r.url.split('/')[5] 98 | filename = r.url.split('/')[6][:r.url.split('/')[6].find('?')] 99 | filesize = (int)(r.headers.get('content-length')) 100 | 101 | # create course directory if does not exist 102 | if not os.path.exists(course_name): 103 | os.makedirs(course_name) 104 | 105 | progress = DownloadProgress(filesize) 106 | chunk_size = 1048576 107 | 108 | if r.ok: 109 | print '%s found (%.1f M)' % (filename, filesize/1024/1024) 110 | else: 111 | print 'Could not find video' 112 | return 113 | 114 | uri = os.path.join(base_dir, course_name, filename) 115 | 116 | if os.path.isfile(uri): 117 | if os.path.getsize(uri) != filesize: 118 | print 'file seems corrupted, download again' 119 | else: 120 | print 'already downloaded, skipping...' 121 | print 122 | return 123 | 124 | with open(uri, 'wb') as handle: 125 | print 'Start downloading...' 126 | progress.start() 127 | for chunk in r.iter_content(chunk_size): 128 | if not chunk: 129 | progress.stop() 130 | break 131 | progress.read(chunk_size) 132 | 133 | handle.write(chunk) 134 | 135 | print '%s downloaded' % filename 136 | print 137 | 138 | def main(): 139 | if len(sys.argv) < 2: 140 | print 'Usage: python tuts_downloader url_to_course' 141 | return 142 | 143 | url = sys.argv[1] 144 | r = requests.get(url) 145 | soup = BeautifulSoup(r.text) 146 | 147 | for section in soup.find_all('tr', 'section-row'): 148 | lesson_url = section.td.a['href'] 149 | lesson_soup = BeautifulSoup(requests.get(lesson_url).text) 150 | video_url = lesson_soup.find('div', 'post-buttons').a['href'] 151 | download(video_url) 152 | 153 | if __name__ == '__main__': 154 | main() 155 | --------------------------------------------------------------------------------