|
|
@ -27,6 +27,60 @@ from ..utils import ( |
|
|
|
class PluralsightBaseIE(InfoExtractor): |
|
|
|
_API_BASE = 'https://app.pluralsight.com' |
|
|
|
|
|
|
|
_GRAPHQL_EP = '%s/player/api/graphql' % _API_BASE |
|
|
|
_GRAPHQL_HEADERS = { |
|
|
|
'Content-Type': 'application/json;charset=UTF-8', |
|
|
|
} |
|
|
|
_GRAPHQL_COURSE_TMPL = ''' |
|
|
|
query BootstrapPlayer { |
|
|
|
rpc { |
|
|
|
bootstrapPlayer { |
|
|
|
profile { |
|
|
|
firstName |
|
|
|
lastName |
|
|
|
email |
|
|
|
username |
|
|
|
userHandle |
|
|
|
authed |
|
|
|
isAuthed |
|
|
|
plan |
|
|
|
} |
|
|
|
course(courseId: "%s") { |
|
|
|
name |
|
|
|
title |
|
|
|
courseHasCaptions |
|
|
|
translationLanguages { |
|
|
|
code |
|
|
|
name |
|
|
|
} |
|
|
|
supportsWideScreenVideoFormats |
|
|
|
timestamp |
|
|
|
modules { |
|
|
|
name |
|
|
|
title |
|
|
|
duration |
|
|
|
formattedDuration |
|
|
|
author |
|
|
|
authorized |
|
|
|
clips { |
|
|
|
authorized |
|
|
|
clipId |
|
|
|
duration |
|
|
|
formattedDuration |
|
|
|
id |
|
|
|
index |
|
|
|
moduleIndex |
|
|
|
moduleTitle |
|
|
|
name |
|
|
|
title |
|
|
|
watched |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}''' |
|
|
|
|
|
|
|
def _download_course(self, course_id, url, display_id): |
|
|
|
try: |
|
|
|
return self._download_course_rpc(course_id, url, display_id) |
|
|
@ -39,20 +93,14 @@ class PluralsightBaseIE(InfoExtractor): |
|
|
|
|
|
|
|
def _download_course_rpc(self, course_id, url, display_id): |
|
|
|
response = self._download_json( |
|
|
|
'%s/player/functions/rpc' % self._API_BASE, display_id, |
|
|
|
'Downloading course JSON', |
|
|
|
data=json.dumps({ |
|
|
|
'fn': 'bootstrapPlayer', |
|
|
|
'payload': { |
|
|
|
'courseId': course_id, |
|
|
|
}, |
|
|
|
}).encode('utf-8'), |
|
|
|
headers={ |
|
|
|
'Content-Type': 'application/json;charset=utf-8', |
|
|
|
'Referer': url, |
|
|
|
}) |
|
|
|
|
|
|
|
course = try_get(response, lambda x: x['payload']['course'], dict) |
|
|
|
self._GRAPHQL_EP, display_id, data=json.dumps({ |
|
|
|
'query': self._GRAPHQL_COURSE_TMPL % course_id, |
|
|
|
'variables': {} |
|
|
|
}).encode('utf-8'), headers=self._GRAPHQL_HEADERS) |
|
|
|
|
|
|
|
course = try_get( |
|
|
|
response, lambda x: x['data']['rpc']['bootstrapPlayer']['course'], |
|
|
|
dict) |
|
|
|
if course: |
|
|
|
return course |
|
|
|
|
|
|
@ -90,6 +138,28 @@ class PluralsightIE(PluralsightBaseIE): |
|
|
|
'only_matching': True, |
|
|
|
}] |
|
|
|
|
|
|
|
GRAPHQL_VIEWCLIP_TMPL = ''' |
|
|
|
query viewClip { |
|
|
|
viewClip(input: { |
|
|
|
author: "%(author)s", |
|
|
|
clipIndex: %(clipIndex)d, |
|
|
|
courseName: "%(courseName)s", |
|
|
|
includeCaptions: %(includeCaptions)s, |
|
|
|
locale: "%(locale)s", |
|
|
|
mediaType: "%(mediaType)s", |
|
|
|
moduleName: "%(moduleName)s", |
|
|
|
quality: "%(quality)s" |
|
|
|
}) { |
|
|
|
urls { |
|
|
|
url |
|
|
|
cdn |
|
|
|
rank |
|
|
|
source |
|
|
|
}, |
|
|
|
status |
|
|
|
} |
|
|
|
}''' |
|
|
|
|
|
|
|
def _real_initialize(self): |
|
|
|
self._login() |
|
|
|
|
|
|
@ -277,7 +347,7 @@ class PluralsightIE(PluralsightBaseIE): |
|
|
|
f = QUALITIES[quality].copy() |
|
|
|
clip_post = { |
|
|
|
'author': author, |
|
|
|
'includeCaptions': False, |
|
|
|
'includeCaptions': 'false', |
|
|
|
'clipIndex': int(clip_idx), |
|
|
|
'courseName': course_name, |
|
|
|
'locale': 'en', |
|
|
@ -286,11 +356,23 @@ class PluralsightIE(PluralsightBaseIE): |
|
|
|
'quality': '%dx%d' % (f['width'], f['height']), |
|
|
|
} |
|
|
|
format_id = '%s-%s' % (ext, quality) |
|
|
|
viewclip = self._download_json( |
|
|
|
'%s/video/clips/viewclip' % self._API_BASE, display_id, |
|
|
|
'Downloading %s viewclip JSON' % format_id, fatal=False, |
|
|
|
data=json.dumps(clip_post).encode('utf-8'), |
|
|
|
headers={'Content-Type': 'application/json;charset=utf-8'}) |
|
|
|
|
|
|
|
try: |
|
|
|
viewclip = self._download_json( |
|
|
|
self._GRAPHQL_EP, display_id, |
|
|
|
'Downloading %s viewclip graphql' % format_id, |
|
|
|
data=json.dumps({ |
|
|
|
'query': self.GRAPHQL_VIEWCLIP_TMPL % clip_post, |
|
|
|
'variables': {} |
|
|
|
}).encode('utf-8'), |
|
|
|
headers=self._GRAPHQL_HEADERS)['data']['viewClip'] |
|
|
|
except ExtractorError: |
|
|
|
# Still works but most likely will go soon |
|
|
|
viewclip = self._download_json( |
|
|
|
'%s/video/clips/viewclip' % self._API_BASE, display_id, |
|
|
|
'Downloading %s viewclip JSON' % format_id, fatal=False, |
|
|
|
data=json.dumps(clip_post).encode('utf-8'), |
|
|
|
headers={'Content-Type': 'application/json;charset=utf-8'}) |
|
|
|
|
|
|
|
# Pluralsight tracks multiple sequential calls to ViewClip API and start |
|
|
|
# to return 429 HTTP errors after some time (see |
|
|
|