2 from __future__ 
import unicode_literals
 
  12 from .common 
import InfoExtractor
 
  13 from ..compat 
import ( 
  15     compat_urllib_parse_urlencode
, 
  25 class VRVBaseIE(InfoExtractor
): 
  32     def _call_api(self
, path
, video_id
, note
, data
=None): 
  33         # https://tools.ietf.org/html/rfc5849#section-3 
  34         base_url 
= self
._API
_DOMAIN 
+ '/core/' + path
 
  36             ('oauth_consumer_key', self
._API
_PARAMS
['oAuthKey']), 
  37             ('oauth_nonce', ''.join([random
.choice(string
.ascii_letters
) for _ 
in range(32)])), 
  38             ('oauth_signature_method', 'HMAC-SHA1'), 
  39             ('oauth_timestamp', int(time
.time())), 
  42             query
.append(('oauth_token', self
._TOKEN
)) 
  43         encoded_query 
= compat_urllib_parse_urlencode(query
) 
  44         headers 
= self
.geo_verification_headers() 
  46             data 
= json
.dumps(data
).encode() 
  47             headers
['Content-Type'] = 'application/json' 
  48         base_string 
= '&'.join([ 
  49             'POST' if data 
else 'GET', 
  50             compat_urllib_parse
.quote(base_url
, ''), 
  51             compat_urllib_parse
.quote(encoded_query
, '')]) 
  52         oauth_signature 
= base64
.b64encode(hmac
.new( 
  53             (self
._API
_PARAMS
['oAuthSecret'] + '&' + self
._TOKEN
_SECRET
).encode('ascii'), 
  54             base_string
.encode(), hashlib
.sha1
).digest()).decode() 
  55         encoded_query 
+= '&oauth_signature=' + compat_urllib_parse
.quote(oauth_signature
, '') 
  57             return self
._download
_json
( 
  58                 '?'.join([base_url
, encoded_query
]), video_id
, 
  59                 note
='Downloading %s JSON metadata' % note
, headers
=headers
, data
=data
) 
  60         except ExtractorError 
as e
: 
  61             if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code 
== 401: 
  62                 raise ExtractorError(json
.loads(e
.cause
.read().decode())['message'], expected
=True) 
  65     def _call_cms(self
, path
, video_id
, note
): 
  66         if not self
._CMS
_SIGNING
: 
  67             self
._CMS
_SIGNING 
= self
._call
_api
('index', video_id
, 'CMS Signing')['cms_signing'] 
  68         return self
._download
_json
( 
  69             self
._API
_DOMAIN 
+ path
, video_id
, query
=self
._CMS
_SIGNING
, 
  70             note
='Downloading %s JSON metadata' % note
, headers
=self
.geo_verification_headers()) 
  72     def _get_cms_resource(self
, resource_key
, video_id
): 
  73         return self
._call
_api
( 
  74             'cms_resource', video_id
, 'resource path', data
={ 
  75                 'resource_key': resource_key
, 
  76             })['__links__']['cms_resource']['href'] 
  78     def _real_initialize(self
): 
  79         webpage 
= self
._download
_webpage
( 
  80             'https://vrv.co/', None, headers
=self
.geo_verification_headers()) 
  81         self
._API
_PARAMS 
= self
._parse
_json
(self
._search
_regex
( 
  83                 r
'window\.__APP_CONFIG__\s*=\s*({.+?})(?:</script>|;)', 
  84                 r
'window\.__APP_CONFIG__\s*=\s*({.+})' 
  85             ], webpage
, 'app config'), None)['cxApiParams'] 
  86         self
._API
_DOMAIN 
= self
._API
_PARAMS
.get('apiDomain', 'https://api.vrv.co') 
  89 class VRVIE(VRVBaseIE
): 
  91     _VALID_URL 
= r
'https?://(?:www\.)?vrv\.co/watch/(?P<id>[A-Z0-9]+)' 
  93         'url': 'https://vrv.co/watch/GR9PNZ396/Hidden-America-with-Jonah-Ray:BOSTON-WHERE-THE-PAST-IS-THE-PRESENT', 
  97             'title': 'BOSTON: WHERE THE PAST IS THE PRESENT', 
  98             'description': 'md5:4ec8844ac262ca2df9e67c0983c6b83f', 
  99             'uploader_id': 'seeso', 
 103             'skip_download': True, 
 107         'url': 'https://vrv.co/watch/G6NQXZ1J6/Lily-CAT', 
 110             'title': 'Lily C.A.T', 
 111             'description': 'md5:988b031e7809a6aeb60968be4af7db07', 
 115     _NETRC_MACHINE 
= 'vrv' 
 117     def _real_initialize(self
): 
 118         super(VRVIE
, self
)._real
_initialize
() 
 120         email
, password 
= self
._get
_login
_info
() 
 124         token_credentials 
= self
._call
_api
( 
 125             'authenticate/by:credentials', None, 'Token Credentials', data
={ 
 127                 'password': password
, 
 129         self
._TOKEN 
= token_credentials
['oauth_token'] 
 130         self
._TOKEN
_SECRET 
= token_credentials
['oauth_token_secret'] 
 132     def _extract_vrv_formats(self
, url
, video_id
, stream_format
, audio_lang
, hardsub_lang
): 
 133         if not url 
or stream_format 
not in ('hls', 'dash', 'adaptive_hls'): 
 137             stream_id_list
.append('audio-%s' % audio_lang
) 
 139             stream_id_list
.append('hardsub-%s' % hardsub_lang
) 
 140         format_id 
= stream_format
 
 142             format_id 
+= '-' + '-'.join(stream_id_list
) 
 143         if 'hls' in stream_format
: 
 144             adaptive_formats 
= self
._extract
_m
3u8_formats
( 
 145                 url
, video_id
, 'mp4', m3u8_id
=format_id
, 
 146                 note
='Downloading %s information' % format_id
, 
 148         elif stream_format 
== 'dash': 
 149             adaptive_formats 
= self
._extract
_mpd
_formats
( 
 150                 url
, video_id
, mpd_id
=format_id
, 
 151                 note
='Downloading %s information' % format_id
, 
 154             for f 
in adaptive_formats
: 
 155                 if f
.get('acodec') != 'none': 
 156                     f
['language'] = audio_lang
 
 157         return adaptive_formats
 
 159     def _real_extract(self
, url
): 
 160         video_id 
= self
._match
_id
(url
) 
 162         object_data 
= self
._call
_cms
(self
._get
_cms
_resource
( 
 163             'cms:/objects/' + video_id
, video_id
), video_id
, 'object')['items'][0] 
 164         resource_path 
= object_data
['__links__']['resource']['href'] 
 165         video_data 
= self
._call
_cms
(resource_path
, video_id
, 'video') 
 166         title 
= video_data
['title'] 
 167         description 
= video_data
.get('description') 
 169         if video_data
.get('__class__') == 'movie_listing': 
 170             items 
= self
._call
_cms
( 
 171                 video_data
['__links__']['movie_listing/movies']['href'], 
 172                 video_id
, 'movie listing').get('items') or [] 
 176                     item_id 
= item
.get('id') 
 179                     entries
.append(self
.url_result( 
 180                         'https://vrv.co/watch/' + item_id
, 
 181                         self
.ie_key(), item_id
, item
.get('title'))) 
 182                 return self
.playlist_result(entries
, video_id
, title
, description
) 
 183             video_data 
= items
[0] 
 185         streams_path 
= video_data
['__links__'].get('streams', {}).get('href') 
 187             self
.raise_login_required() 
 188         streams_json 
= self
._call
_cms
(streams_path
, video_id
, 'streams') 
 190         audio_locale 
= streams_json
.get('audio_locale') 
 192         for stream_type
, streams 
in streams_json
.get('streams', {}).items(): 
 193             if stream_type 
in ('adaptive_hls', 'adaptive_dash'): 
 194                 for stream 
in streams
.values(): 
 195                     formats
.extend(self
._extract
_vrv
_formats
( 
 196                         stream
.get('url'), video_id
, stream_type
.split('_')[1], 
 197                         audio_locale
, stream
.get('hardsub_locale'))) 
 198         self
._sort
_formats
(formats
) 
 201         for k 
in ('captions', 'subtitles'): 
 202             for subtitle 
in streams_json
.get(k
, {}).values(): 
 203                 subtitle_url 
= subtitle
.get('url') 
 206                 subtitles
.setdefault(subtitle
.get('locale', 'en-US'), []).append({ 
 208                     'ext': subtitle
.get('format', 'ass'), 
 212         for thumbnail 
in video_data
.get('images', {}).get('thumbnails', []): 
 213             thumbnail_url 
= thumbnail
.get('source') 
 214             if not thumbnail_url
: 
 217                 'url': thumbnail_url
, 
 218                 'width': int_or_none(thumbnail
.get('width')), 
 219                 'height': int_or_none(thumbnail
.get('height')), 
 226             'subtitles': subtitles
, 
 227             'thumbnails': thumbnails
, 
 228             'description': description
, 
 229             'duration': float_or_none(video_data
.get('duration_ms'), 1000), 
 230             'uploader_id': video_data
.get('channel_id'), 
 231             'series': video_data
.get('series_title'), 
 232             'season': video_data
.get('season_title'), 
 233             'season_number': int_or_none(video_data
.get('season_number')), 
 234             'season_id': video_data
.get('season_id'), 
 236             'episode_number': int_or_none(video_data
.get('episode_number')), 
 237             'episode_id': video_data
.get('production_episode_id'), 
 241 class VRVSeriesIE(VRVBaseIE
): 
 242     IE_NAME 
= 'vrv:series' 
 243     _VALID_URL 
= r
'https?://(?:www\.)?vrv\.co/series/(?P<id>[A-Z0-9]+)' 
 245         'url': 'https://vrv.co/series/G68VXG3G6/The-Perfect-Insider', 
 249         'playlist_mincount': 11, 
 252     def _real_extract(self
, url
): 
 253         series_id 
= self
._match
_id
(url
) 
 255         seasons_path 
= self
._get
_cms
_resource
( 
 256             'cms:/seasons?series_id=' + series_id
, series_id
) 
 257         seasons_data 
= self
._call
_cms
(seasons_path
, series_id
, 'seasons') 
 260         for season 
in seasons_data
.get('items', []): 
 261             episodes_path 
= season
['__links__']['season/episodes']['href'] 
 262             episodes 
= self
._call
_cms
(episodes_path
, series_id
, 'episodes') 
 263             for episode 
in episodes
.get('items', []): 
 264                 episode_id 
= episode
['id'] 
 265                 entries
.append(self
.url_result( 
 266                     'https://vrv.co/watch/' + episode_id
, 
 267                     'VRV', episode_id
, episode
.get('title'))) 
 269         return self
.playlist_result(entries
, series_id
)