|
| 1 | +import requests, json, sys, re, os |
| 2 | +from slugify import slugify |
| 3 | + |
| 4 | +class Skillshare(object): |
| 5 | + |
| 6 | + def __init__( |
| 7 | + self, |
| 8 | + cookie, |
| 9 | + download_path=os.environ.get('FILE_PATH', './Skillshare'), |
| 10 | + pk='BCpkADawqM2OOcM6njnM7hf9EaK6lIFlqiXB0iWjqGWUQjU7R8965xUvIQNqdQbnDTLz0IAO7E6Ir2rIbXJtFdzrGtitoee0n1XXRliD-RH9A-svuvNW9qgo3Bh34HEZjXjG4Nml4iyz3KqF', |
| 11 | + brightcove_account_id=3695997568001, |
| 12 | + ): |
| 13 | + self.cookie = cookie.strip().strip('"') |
| 14 | + self.download_path = download_path |
| 15 | + self.pk = pk.strip() |
| 16 | + self.brightcove_account_id = brightcove_account_id |
| 17 | + self.pythonversion = 3 if sys.version_info >= (3, 0) else 2 |
| 18 | + |
| 19 | + def is_unicode_string(self, string): |
| 20 | + if (self.pythonversion == 3 and isinstance(string, str)) or (self.pythonversion == 2 and isinstance(string, unicode)): |
| 21 | + return True |
| 22 | + |
| 23 | + else: |
| 24 | + return False |
| 25 | + |
| 26 | + def download_course_by_url(self, url): |
| 27 | + m = re.match('https://www.skillshare.com/classes/.*?/(\\d+)', url) |
| 28 | + assert m, 'Failed to parse class ID from URL' |
| 29 | + self.download_course_by_class_id(m.group(1)) |
| 30 | + |
| 31 | + def download_course_by_class_id(self, class_id): |
| 32 | + data = self.fetch_course_data_by_class_id(class_id=class_id) |
| 33 | + teacher_name = None |
| 34 | + if 'vanity_username' in data['_embedded']['teacher']: |
| 35 | + teacher_name = data['_embedded']['teacher']['vanity_username'] |
| 36 | + if not teacher_name: |
| 37 | + teacher_name = data['_embedded']['teacher']['full_name'] |
| 38 | + assert teacher_name, 'Failed to read teacher name from data' |
| 39 | + if self.is_unicode_string(teacher_name): |
| 40 | + teacher_name = teacher_name.encode('ascii', 'replace') |
| 41 | + title = data['title'] |
| 42 | + if self.is_unicode_string(title): |
| 43 | + title = title.encode('ascii', 'replace') |
| 44 | + base_path = os.path.abspath(os.path.join(self.download_path, slugify(teacher_name), slugify(title))).rstrip('/') |
| 45 | + if not os.path.exists(base_path): |
| 46 | + os.makedirs(base_path) |
| 47 | + for u in data['_embedded']['units']['_embedded']['units']: |
| 48 | + for s in u['_embedded']['sessions']['_embedded']['sessions']: |
| 49 | + video_id = None |
| 50 | + if 'video_hashed_id' in s: |
| 51 | + if s['video_hashed_id']: |
| 52 | + video_id = s['video_hashed_id'].split(':')[1] |
| 53 | + assert video_id, 'Failed to read video ID from data' |
| 54 | + s_title = s['title'] |
| 55 | + if self.is_unicode_string(s_title): |
| 56 | + s_title = s_title.encode('ascii', 'replace') |
| 57 | + file_name = '{} - {}'.format(str(s['index'] + 1).zfill(2), slugify(s_title)) |
| 58 | + self.download_video(fpath='{base_path}/{session}.mp4'.format(base_path=base_path, |
| 59 | + session=file_name), |
| 60 | + video_id=video_id) |
| 61 | + print('') |
| 62 | + |
| 63 | + def fetch_course_data_by_class_id(self, class_id): |
| 64 | + res = requests.get(url=('https://api.skillshare.com/classes/{}'.format(class_id)), |
| 65 | + headers={'Accept':'application/vnd.skillshare.class+json;,version=0.8', |
| 66 | + 'User-Agent':'Skillshare/4.1.1; Android 5.1.1', |
| 67 | + 'Host':'api.skillshare.com', |
| 68 | + 'cookie':self.cookie}) |
| 69 | + assert res.status_code == 200, 'Fetch error, code == {}'.format(res.status_code) |
| 70 | + return res.json() |
| 71 | + |
| 72 | + def download_video(self, fpath, video_id): |
| 73 | + meta_url = 'https://edge.api.brightcove.com/playback/v1/accounts/{account_id}/videos/{video_id}'.format(account_id=(self.brightcove_account_id), |
| 74 | + video_id=video_id) |
| 75 | + meta_res = requests.get(meta_url, |
| 76 | + headers={'Accept':'application/json;pk={}'.format(self.pk), |
| 77 | + 'User-Agent':'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0', |
| 78 | + 'Origin':'https://www.skillshare.com'}) |
| 79 | + assert not meta_res.status_code != 200, 'Failed to fetch video meta' |
| 80 | + for x in meta_res.json()['sources']: |
| 81 | + if 'container' in x: |
| 82 | + if x['container'] == 'MP4' and 'src' in x: |
| 83 | + dl_url = x['src'] |
| 84 | + break |
| 85 | + |
| 86 | + print('Downloading {}...'.format(fpath)) |
| 87 | + if os.path.exists(fpath): |
| 88 | + print('Video already downloaded, skipping...') |
| 89 | + return |
| 90 | + with open(fpath, 'wb') as (f): |
| 91 | + response = requests.get(dl_url, allow_redirects=True, stream=True) |
| 92 | + total_length = response.headers.get('content-length') |
| 93 | + if not total_length: |
| 94 | + f.write(response.content) |
| 95 | + else: |
| 96 | + dl = 0 |
| 97 | + total_length = int(total_length) |
| 98 | + for data in response.iter_content(chunk_size=4096): |
| 99 | + dl += len(data) |
| 100 | + f.write(data) |
| 101 | + done = int(50 * dl / total_length) |
| 102 | + sys.stdout.write('\r[%s%s]' % ('=' * done, ' ' * (50 - done))) |
| 103 | + sys.stdout.flush() |
| 104 | + |
| 105 | + print('') |
0 commit comments