]>
Commit | Line | Data |
---|---|---|
fb009b7f | 1 | from .adobepass import AdobePassIE |
3d2623a8 | 2 | from ..networking import HEADRequest |
fb009b7f | 3 | from ..utils import ( |
06966cb8 | 4 | extract_attributes, |
c5370857 | 5 | float_or_none, |
06966cb8 | 6 | get_element_html_by_class, |
7 | int_or_none, | |
8 | merge_dicts, | |
9 | parse_age_limit, | |
10 | remove_end, | |
11 | str_or_none, | |
12 | traverse_obj, | |
13 | unescapeHTML, | |
14 | unified_timestamp, | |
15 | update_url_query, | |
16 | url_or_none, | |
fb009b7f | 17 | ) |
63f41d38 | 18 | |
19 | ||
fb009b7f | 20 | class BravoTVIE(AdobePassIE): |
06966cb8 | 21 | _VALID_URL = r'https?://(?:www\.)?(?P<site>bravotv|oxygen)\.com/(?:[^/]+/)+(?P<id>[^/?#]+)' |
fb009b7f | 22 | _TESTS = [{ |
81d989c2 | 23 | 'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is', |
63f41d38 | 24 | 'info_dict': { |
06966cb8 | 25 | 'id': '3923059', |
63f41d38 | 26 | 'ext': 'mp4', |
81d989c2 RA |
27 | 'title': 'The Top Chef Season 16 Winner Is...', |
28 | 'description': 'Find out who takes the title of Top Chef!', | |
81d989c2 RA |
29 | 'upload_date': '20190314', |
30 | 'timestamp': 1552591860, | |
c5370857 KC |
31 | 'season_number': 16, |
32 | 'episode_number': 15, | |
33 | 'series': 'Top Chef', | |
34 | 'episode': 'The Top Chef Season 16 Winner Is...', | |
06966cb8 | 35 | 'duration': 190.357, |
36 | 'season': 'Season 16', | |
37 | 'thumbnail': r're:^https://.+\.jpg', | |
38 | }, | |
39 | 'params': {'skip_download': 'm3u8'}, | |
fb009b7f | 40 | }, { |
06966cb8 | 41 | 'url': 'https://www.bravotv.com/top-chef/season-20/episode-1/london-calling', |
42 | 'info_dict': { | |
43 | 'id': '9000234570', | |
44 | 'ext': 'mp4', | |
45 | 'title': 'London Calling', | |
46 | 'description': 'md5:5af95a8cbac1856bd10e7562f86bb759', | |
47 | 'upload_date': '20230310', | |
48 | 'timestamp': 1678410000, | |
49 | 'season_number': 20, | |
50 | 'episode_number': 1, | |
51 | 'series': 'Top Chef', | |
52 | 'episode': 'London Calling', | |
53 | 'duration': 3266.03, | |
54 | 'season': 'Season 20', | |
55 | 'chapters': 'count:7', | |
56 | 'thumbnail': r're:^https://.+\.jpg', | |
57 | 'age_limit': 14, | |
58 | }, | |
59 | 'params': {'skip_download': 'm3u8'}, | |
60 | 'skip': 'This video requires AdobePass MSO credentials', | |
61 | }, { | |
62 | 'url': 'https://www.oxygen.com/in-ice-cold-blood/season-1/closing-night', | |
63 | 'info_dict': { | |
64 | 'id': '3692045', | |
65 | 'ext': 'mp4', | |
66 | 'title': 'Closing Night', | |
67 | 'description': 'md5:3170065c5c2f19548d72a4cbc254af63', | |
68 | 'upload_date': '20180401', | |
69 | 'timestamp': 1522623600, | |
70 | 'season_number': 1, | |
71 | 'episode_number': 1, | |
72 | 'series': 'In Ice Cold Blood', | |
73 | 'episode': 'Closing Night', | |
74 | 'duration': 2629.051, | |
75 | 'season': 'Season 1', | |
76 | 'chapters': 'count:6', | |
77 | 'thumbnail': r're:^https://.+\.jpg', | |
78 | 'age_limit': 14, | |
79 | }, | |
80 | 'params': {'skip_download': 'm3u8'}, | |
81 | 'skip': 'This video requires AdobePass MSO credentials', | |
2181983a | 82 | }, { |
83 | 'url': 'https://www.oxygen.com/in-ice-cold-blood/season-2/episode-16/videos/handling-the-horwitz-house-after-the-murder-season-2', | |
06966cb8 | 84 | 'info_dict': { |
85 | 'id': '3974019', | |
86 | 'ext': 'mp4', | |
87 | 'title': '\'Handling The Horwitz House After The Murder (Season 2, Episode 16)', | |
88 | 'description': 'md5:f9d638dd6946a1c1c0533a9c6100eae5', | |
89 | 'upload_date': '20190617', | |
90 | 'timestamp': 1560790800, | |
91 | 'season_number': 2, | |
92 | 'episode_number': 16, | |
93 | 'series': 'In Ice Cold Blood', | |
94 | 'episode': '\'Handling The Horwitz House After The Murder (Season 2, Episode 16)', | |
95 | 'duration': 68.235, | |
96 | 'season': 'Season 2', | |
97 | 'thumbnail': r're:^https://.+\.jpg', | |
98 | 'age_limit': 14, | |
99 | }, | |
100 | 'params': {'skip_download': 'm3u8'}, | |
101 | }, { | |
102 | 'url': 'https://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1', | |
2181983a | 103 | 'only_matching': True, |
fb009b7f | 104 | }] |
63f41d38 | 105 | |
106 | def _real_extract(self, url): | |
06966cb8 | 107 | site, display_id = self._match_valid_url(url).group('site', 'id') |
fb009b7f | 108 | webpage = self._download_webpage(url, display_id) |
06966cb8 | 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 '') | |
fb009b7f | 112 | query = { |
06966cb8 | 113 | 'manifest': 'm3u', |
114 | 'formats': 'm3u,mpeg4', | |
fb009b7f | 115 | } |
06966cb8 | 116 | |
fb009b7f | 117 | if tve: |
06966cb8 | 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'] | |
fb009b7f | 127 | resource = self._get_mvpd_resource( |
06966cb8 | 128 | tve.get('data-adobe-pass-resource-id') or auth.get('adobePassResourceId') or site, |
129 | tve['data-title'], release_pid, tve.get('data-rating')) | |
130 | query.update({ | |
131 | 'switch': 'HLSServiceSecure', | |
132 | 'auth': self._extract_mvpd_auth( | |
133 | url, release_pid, auth.get('adobePassRequestorId') or site, resource), | |
134 | }) | |
c5370857 | 135 | |
06966cb8 | 136 | else: |
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) | |
c5370857 | 143 | |
06966cb8 | 144 | tp_url = f'https://link.theplatform.com/s/{account_pid}/media/guid/{account_id}/{video_id}' |
c5370857 | 145 | tp_metadata = self._download_json( |
06966cb8 | 146 | update_url_query(tp_url, {'format': 'preview'}), video_id, fatal=False) |
147 | ||
148 | seconds_or_none = lambda x: float_or_none(x, 1000) | |
149 | chapters = traverse_obj(tp_metadata, ('chapters', ..., { | |
150 | 'start_time': ('startTime', {seconds_or_none}), | |
151 | 'end_time': ('endTime', {seconds_or_none}), | |
152 | })) | |
153 | # prune pointless single chapters that span the entire duration from short videos | |
154 | if len(chapters) == 1 and not traverse_obj(chapters, (0, 'end_time')): | |
155 | chapters = None | |
c5370857 | 156 | |
1fe5bf24 | 157 | m3u8_url = self._request_webpage(HEADRequest( |
3d2623a8 | 158 | update_url_query(f'{tp_url}/stream.m3u8', query)), video_id, 'Checking m3u8 URL').url |
1fe5bf24 | 159 | if 'mpeg_cenc' in m3u8_url: |
160 | self.report_drm(video_id) | |
161 | formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4', m3u8_id='hls') | |
06966cb8 | 162 | |
163 | return { | |
164 | 'id': video_id, | |
165 | 'formats': formats, | |
166 | 'subtitles': subtitles, | |
167 | 'chapters': chapters, | |
168 | **merge_dicts(traverse_obj(tp_metadata, { | |
169 | 'title': 'title', | |
170 | 'description': 'description', | |
171 | 'duration': ('duration', {seconds_or_none}), | |
172 | 'timestamp': ('pubDate', {seconds_or_none}), | |
173 | 'season_number': (('pl1$seasonNumber', 'nbcu$seasonNumber'), {int_or_none}), | |
174 | 'episode_number': (('pl1$episodeNumber', 'nbcu$episodeNumber'), {int_or_none}), | |
175 | 'series': (('pl1$show', 'nbcu$show'), (None, ...), {str}), | |
176 | 'episode': (('title', 'pl1$episodeNumber', 'nbcu$episodeNumber'), {str_or_none}), | |
177 | 'age_limit': ('ratings', ..., 'rating', {parse_age_limit}), | |
178 | }, get_all=False), traverse_obj(metadata, { | |
179 | 'title': 'title', | |
180 | 'description': 'description', | |
181 | 'duration': ('durationInSeconds', {int_or_none}), | |
182 | 'timestamp': ('airDate', {unified_timestamp}), | |
183 | 'thumbnail': ('thumbnailUrl', {url_or_none}), | |
184 | 'season_number': ('seasonNumber', {int_or_none}), | |
185 | 'episode_number': ('episodeNumber', {int_or_none}), | |
186 | 'episode': 'episodeTitle', | |
187 | 'series': 'show', | |
188 | })) | |
189 | } |