3 from .common
import InfoExtractor
14 class PlaySuisseIE(InfoExtractor
):
15 _NETRC_MACHINE
= 'playsuisse'
16 _VALID_URL
= r
'https?://(?:www\.)?playsuisse\.ch/(?:watch|detail)/(?:[^#]*[?&]episodeId=)?(?P<id>[0-9]+)'
20 'url': 'https://www.playsuisse.ch/watch/763211/0',
21 'only_matching': True,
25 'url': 'https://www.playsuisse.ch/watch/763182?episodeId=763211',
26 'md5': '82df2a470b2dfa60c2d33772a8a60cf8',
31 'description': 'md5:8ea7a8076ba000cd9e8bc132fd0afdd8',
38 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
42 'url': 'https://www.playsuisse.ch/watch/808675',
43 'md5': '818b94c1d2d7c4beef953f12cb8f3e75',
47 'title': 'Der Läufer',
48 'description': 'md5:9f61265c7e6dcc3e046137a792b275fd',
50 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
53 # series (treated as a playlist)
54 'url': 'https://www.playsuisse.ch/detail/1115687',
56 'description': 'md5:e4a2ae29a8895823045b5c3145a02aa3',
58 'series': 'They all came out to Montreux',
59 'title': 'They all came out to Montreux',
63 'description': 'md5:f2462744834b959a31adc6292380cda2',
70 'series': 'They all came out to Montreux',
71 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
77 'description': 'md5:9dfd308699fe850d3bce12dc1bad9b27',
84 'series': 'They all came out to Montreux',
85 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
91 'description': 'md5:14a93a3356b2492a8f786ab2227ef602',
98 'series': 'They all came out to Montreux',
99 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
108 query AssetWatch($assetId: ID!) {
109 assetV2(id: $assetId) {
116 fragment Asset on AssetV2 {
134 thumbnail16x9WithTitle {
137 thumbnail2x3WithTitle {
141 fragment ImageDetails on AssetImage {
145 _LOGIN_BASE_URL
= 'https://login.srgssr.ch/srgssrlogin.onmicrosoft.com'
146 _LOGIN_PATH
= 'B2C_1A__SignInV2'
149 def _perform_login(self
, username
, password
):
150 login_page
= self
._download
_webpage
(
151 'https://www.playsuisse.ch/api/sso/login', None, note
='Downloading login page',
152 query
={'x': 'x', 'locale': 'de', 'redirectUrl': 'https://www.playsuisse.ch/'})
153 settings
= self
._search
_json
(r
'var\s+SETTINGS\s*=', login_page
, 'settings', None)
155 csrf_token
= settings
['csrf']
156 query
= {'tx': settings
['transId'], 'p': self
._LOGIN
_PATH
}
158 status
= traverse_obj(self
._download
_json
(
159 f
'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/SelfAsserted', None, 'Logging in',
160 query
=query
, headers
={'X-CSRF-TOKEN': csrf_token
}, data
=urlencode_postdata({
161 'request_type': 'RESPONSE',
162 'signInName': username
,
163 'password': password
,
164 }), expected_status
=400), ('status', {int_or_none}
))
166 raise ExtractorError('Invalid username or password', expected
=True)
168 urlh
= self
._request
_webpage
(
169 f
'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/api/CombinedSigninAndSignup/confirmed',
170 None, 'Downloading ID token', query
={
171 'rememberMe': 'false',
172 'csrf_token': csrf_token
,
177 self
._ID
_TOKEN
= traverse_obj(parse_qs(urlh
.url
), ('id_token', 0))
178 if not self
._ID
_TOKEN
:
179 raise ExtractorError('Login failed')
181 def _get_media_data(self
, media_id
):
182 # NOTE In the web app, the "locale" header is used to switch between languages,
183 # However this doesn't seem to take effect when passing the header here.
184 response
= self
._download
_json
(
185 'https://www.playsuisse.ch/api/graphql',
186 media_id
, data
=json
.dumps({
187 'operationName': 'AssetWatch',
188 'query': self
._GRAPHQL
_QUERY
,
189 'variables': {'assetId': media_id
},
191 headers
={'Content-Type': 'application/json', 'locale': 'de'})
193 return response
['data']['assetV2']
195 def _real_extract(self
, url
):
196 if not self
._ID
_TOKEN
:
197 self
.raise_login_required(method
='password')
199 media_id
= self
._match
_id
(url
)
200 media_data
= self
._get
_media
_data
(media_id
)
201 info
= self
._extract
_single
(media_data
)
202 if media_data
.get('episodes'):
205 'entries': map(self
._extract
_single
, media_data
['episodes']),
209 def _extract_single(self
, media_data
):
210 thumbnails
= traverse_obj(media_data
, lambda k
, _
: k
.startswith('thumbnail'))
212 formats
, subtitles
= [], {}
213 for media
in traverse_obj(media_data
, 'medias', default
=[]):
214 if not media
.get('url') or media
.get('type') != 'HLS':
216 f
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
217 update_url_query(media
['url'], {'id_token': self
._ID
_TOKEN
}),
218 media_data
['id'], 'mp4', m3u8_id
='HLS', fatal
=False)
220 self
._merge
_subtitles
(subs
, target
=subtitles
)
223 'id': media_data
['id'],
224 'title': media_data
.get('name'),
225 'description': media_data
.get('description'),
226 'thumbnails': thumbnails
,
227 'duration': int_or_none(media_data
.get('duration')),
229 'subtitles': subtitles
,
230 'series': media_data
.get('seriesName'),
231 'season_number': int_or_none(media_data
.get('seasonNumber')),
232 'episode': media_data
.get('name') if media_data
.get('episodeNumber') else None,
233 'episode_number': int_or_none(media_data
.get('episodeNumber')),