1 from .adobepass
import AdobePassIE
2 from ..networking
import HEADRequest
6 get_element_html_by_class
,
20 class BravoTVIE(AdobePassIE
):
21 _VALID_URL
= r
'https?://(?:www\.)?(?P<site>bravotv|oxygen)\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
23 'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
27 'title': 'The Top Chef Season 16 Winner Is...',
28 'description': 'Find out who takes the title of Top Chef!',
29 'upload_date': '20190314',
30 'timestamp': 1552591860,
34 'episode': 'The Top Chef Season 16 Winner Is...',
36 'season': 'Season 16',
37 'thumbnail': r
're:^https://.+\.jpg',
39 'params': {'skip_download': 'm3u8'},
41 'url': 'https://www.bravotv.com/top-chef/season-20/episode-1/london-calling',
45 'title': 'London Calling',
46 'description': 'md5:5af95a8cbac1856bd10e7562f86bb759',
47 'upload_date': '20230310',
48 'timestamp': 1678410000,
52 'episode': 'London Calling',
54 'season': 'Season 20',
55 'chapters': 'count:7',
56 'thumbnail': r
're:^https://.+\.jpg',
59 'params': {'skip_download': 'm3u8'},
60 'skip': 'This video requires AdobePass MSO credentials',
62 'url': 'https://www.oxygen.com/in-ice-cold-blood/season-1/closing-night',
66 'title': 'Closing Night',
67 'description': 'md5:3170065c5c2f19548d72a4cbc254af63',
68 'upload_date': '20180401',
69 'timestamp': 1522623600,
72 'series': 'In Ice Cold Blood',
73 'episode': 'Closing Night',
76 'chapters': 'count:6',
77 'thumbnail': r
're:^https://.+\.jpg',
80 'params': {'skip_download': 'm3u8'},
81 'skip': 'This video requires AdobePass MSO credentials',
83 'url': 'https://www.oxygen.com/in-ice-cold-blood/season-2/episode-16/videos/handling-the-horwitz-house-after-the-murder-season-2',
87 'title': '\'Handling The Horwitz House After The Murder (Season 2, Episode 16)',
88 'description': 'md5:f9d638dd6946a1c1c0533a9c6100eae5',
89 'upload_date': '20190617',
90 'timestamp': 1560790800,
93 'series': 'In Ice Cold Blood',
94 'episode': '\'Handling The Horwitz House After The Murder (Season 2, Episode 16)',
97 'thumbnail': r
're:^https://.+\.jpg',
100 'params': {'skip_download': 'm3u8'},
102 'url': 'https://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
103 'only_matching': True,
106 def _real_extract(self
, url
):
107 site
, display_id
= self
._match
_valid
_url
(url
).group('site', 'id')
108 webpage
= self
._download
_webpage
(url
, display_id
)
109 settings
= self
._search
_json
(
110 r
'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>', webpage
, 'settings', display_id
)
111 tve
= extract_attributes(get_element_html_by_class('tve-video-deck-app', webpage
) or '')
114 'formats': 'm3u,mpeg4',
118 account_pid
= tve
.get('data-mpx-media-account-pid') or 'HNK2IC'
119 account_id
= tve
['data-mpx-media-account-id']
120 metadata
= self
._parse
_json
(
121 tve
.get('data-normalized-video', ''), display_id
, fatal
=False, transform_source
=unescapeHTML
)
122 video_id
= tve
.get('data-guid') or metadata
['guid']
123 if tve
.get('data-entitlement') == 'auth':
124 auth
= traverse_obj(settings
, ('tve_adobe_auth', {dict}
)) or {}
125 site
= remove_end(site
, 'tv')
126 release_pid
= tve
['data-release-pid']
127 resource
= self
._get
_mvpd
_resource
(
128 tve
.get('data-adobe-pass-resource-id') or auth
.get('adobePassResourceId') or site
,
129 tve
['data-title'], release_pid
, tve
.get('data-rating'))
131 'switch': 'HLSServiceSecure',
132 'auth': self
._extract
_mvpd
_auth
(
133 url
, release_pid
, auth
.get('adobePassRequestorId') or site
, resource
),
137 ls_playlist
= traverse_obj(settings
, ('ls_playlist', ..., {dict}
), get_all
=False) or {}
138 account_pid
= ls_playlist
.get('mpxMediaAccountPid') or 'PHSl-B'
139 account_id
= ls_playlist
['mpxMediaAccountId']
140 video_id
= ls_playlist
['defaultGuid']
141 metadata
= traverse_obj(
142 ls_playlist
, ('videos', lambda _
, v
: v
['guid'] == video_id
, {dict}
), get_all
=False)
144 tp_url
= f
'https://link.theplatform.com/s/{account_pid}/media/guid/{account_id}/{video_id}'
145 tp_metadata
= self
._download
_json
(
146 update_url_query(tp_url
, {'format': 'preview'}), video_id
, fatal
=False)
148 chapters
= traverse_obj(tp_metadata
, ('chapters', ..., {
149 'start_time': ('startTime', {float_or_none(scale
=1000)}),
150 'end_time': ('endTime', {float_or_none(scale
=1000)}),
152 # prune pointless single chapters that span the entire duration from short videos
153 if len(chapters
) == 1 and not traverse_obj(chapters
, (0, 'end_time')):
156 m3u8_url
= self
._request
_webpage
(HEADRequest(
157 update_url_query(f
'{tp_url}/stream.m3u8', query
)), video_id
, 'Checking m3u8 URL').url
158 if 'mpeg_cenc' in m3u8_url
:
159 self
.report_drm(video_id
)
160 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(m3u8_url
, video_id
, 'mp4', m3u8_id
='hls')
165 'subtitles': subtitles
,
166 'chapters': chapters
,
167 **merge_dicts(traverse_obj(tp_metadata
, {
169 'description': 'description',
170 'duration': ('duration', {float_or_none(scale
=1000)}),
171 'timestamp': ('pubDate', {float_or_none(scale
=1000)}),
172 'season_number': (('pl1$seasonNumber', 'nbcu$seasonNumber'), {int_or_none}
),
173 'episode_number': (('pl1$episodeNumber', 'nbcu$episodeNumber'), {int_or_none}
),
174 'series': (('pl1$show', 'nbcu$show'), (None, ...), {str}
),
175 'episode': (('title', 'pl1$episodeNumber', 'nbcu$episodeNumber'), {str_or_none}
),
176 'age_limit': ('ratings', ..., 'rating', {parse_age_limit}
),
177 }, get_all
=False), traverse_obj(metadata
, {
179 'description': 'description',
180 'duration': ('durationInSeconds', {int_or_none}
),
181 'timestamp': ('airDate', {unified_timestamp}
),
182 'thumbnail': ('thumbnailUrl', {url_or_none}
),
183 'season_number': ('seasonNumber', {int_or_none}
),
184 'episode_number': ('episodeNumber', {int_or_none}
),
185 'episode': 'episodeTitle',