]> jfr.im git - yt-dlp.git/blob - yt_dlp/extractor/playsuisse.py
[extractor/playsuisse] Support new url format (#6528)
[yt-dlp.git] / yt_dlp / extractor / playsuisse.py
1 import json
2
3 from .common import InfoExtractor
4 from ..utils import int_or_none, traverse_obj
5
6
7 class PlaySuisseIE(InfoExtractor):
8 _VALID_URL = r'https?://(?:www\.)?playsuisse\.ch/(?:watch|detail)/(?:[^#]*[?&]episodeId=)?(?P<id>[0-9]+)'
9 _TESTS = [
10 {
11 # Old URL
12 'url': 'https://www.playsuisse.ch/watch/763211/0',
13 'only_matching': True,
14 },
15 {
16 # episode in a series
17 'url': 'https://www.playsuisse.ch/watch/763182?episodeId=763211',
18 'md5': '82df2a470b2dfa60c2d33772a8a60cf8',
19 'info_dict': {
20 'id': '763211',
21 'ext': 'mp4',
22 'title': 'Knochen',
23 'description': 'md5:8ea7a8076ba000cd9e8bc132fd0afdd8',
24 'duration': 3344,
25 'series': 'Wilder',
26 'season': 'Season 1',
27 'season_number': 1,
28 'episode': 'Knochen',
29 'episode_number': 1,
30 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
31 }
32 }, {
33 # film
34 'url': 'https://www.playsuisse.ch/watch/808675',
35 'md5': '818b94c1d2d7c4beef953f12cb8f3e75',
36 'info_dict': {
37 'id': '808675',
38 'ext': 'mp4',
39 'title': 'Der Läufer',
40 'description': 'md5:9f61265c7e6dcc3e046137a792b275fd',
41 'duration': 5280,
42 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
43 }
44 }, {
45 # series (treated as a playlist)
46 'url': 'https://www.playsuisse.ch/detail/1115687',
47 'info_dict': {
48 'description': 'md5:e4a2ae29a8895823045b5c3145a02aa3',
49 'id': '1115687',
50 'series': 'They all came out to Montreux',
51 'title': 'They all came out to Montreux',
52 },
53 'playlist': [{
54 'info_dict': {
55 'description': 'md5:f2462744834b959a31adc6292380cda2',
56 'duration': 3180,
57 'episode': 'Folge 1',
58 'episode_number': 1,
59 'id': '1112663',
60 'season': 'Season 1',
61 'season_number': 1,
62 'series': 'They all came out to Montreux',
63 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
64 'title': 'Folge 1',
65 'ext': 'mp4'
66 },
67 }, {
68 'info_dict': {
69 'description': 'md5:9dfd308699fe850d3bce12dc1bad9b27',
70 'duration': 2935,
71 'episode': 'Folge 2',
72 'episode_number': 2,
73 'id': '1112661',
74 'season': 'Season 1',
75 'season_number': 1,
76 'series': 'They all came out to Montreux',
77 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
78 'title': 'Folge 2',
79 'ext': 'mp4'
80 },
81 }, {
82 'info_dict': {
83 'description': 'md5:14a93a3356b2492a8f786ab2227ef602',
84 'duration': 2994,
85 'episode': 'Folge 3',
86 'episode_number': 3,
87 'id': '1112664',
88 'season': 'Season 1',
89 'season_number': 1,
90 'series': 'They all came out to Montreux',
91 'thumbnail': 're:https://playsuisse-img.akamaized.net/',
92 'title': 'Folge 3',
93 'ext': 'mp4'
94 }
95 }],
96 }
97 ]
98
99 _GRAPHQL_QUERY = '''
100 query AssetWatch($assetId: ID!) {
101 assetV2(id: $assetId) {
102 ...Asset
103 episodes {
104 ...Asset
105 }
106 }
107 }
108 fragment Asset on AssetV2 {
109 id
110 name
111 description
112 duration
113 episodeNumber
114 seasonNumber
115 seriesName
116 medias {
117 type
118 url
119 }
120 thumbnail16x9 {
121 ...ImageDetails
122 }
123 thumbnail2x3 {
124 ...ImageDetails
125 }
126 thumbnail16x9WithTitle {
127 ...ImageDetails
128 }
129 thumbnail2x3WithTitle {
130 ...ImageDetails
131 }
132 }
133 fragment ImageDetails on AssetImage {
134 id
135 url
136 }'''
137
138 def _get_media_data(self, media_id):
139 # NOTE In the web app, the "locale" header is used to switch between languages,
140 # However this doesn't seem to take effect when passing the header here.
141 response = self._download_json(
142 'https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql',
143 media_id, data=json.dumps({
144 'operationName': 'AssetWatch',
145 'query': self._GRAPHQL_QUERY,
146 'variables': {'assetId': media_id}
147 }).encode('utf-8'),
148 headers={'Content-Type': 'application/json', 'locale': 'de'})
149
150 return response['data']['assetV2']
151
152 def _real_extract(self, url):
153 media_id = self._match_id(url)
154 media_data = self._get_media_data(media_id)
155 info = self._extract_single(media_data)
156 if media_data.get('episodes'):
157 info.update({
158 '_type': 'playlist',
159 'entries': map(self._extract_single, media_data['episodes']),
160 })
161 return info
162
163 def _extract_single(self, media_data):
164 thumbnails = traverse_obj(media_data, lambda k, _: k.startswith('thumbnail'))
165
166 formats, subtitles = [], {}
167 for media in traverse_obj(media_data, 'medias', default=[]):
168 if not media.get('url') or media.get('type') != 'HLS':
169 continue
170 f, subs = self._extract_m3u8_formats_and_subtitles(
171 media['url'], media_data['id'], 'mp4', m3u8_id='HLS', fatal=False)
172 formats.extend(f)
173 self._merge_subtitles(subs, target=subtitles)
174
175 return {
176 'id': media_data['id'],
177 'title': media_data.get('name'),
178 'description': media_data.get('description'),
179 'thumbnails': thumbnails,
180 'duration': int_or_none(media_data.get('duration')),
181 'formats': formats,
182 'subtitles': subtitles,
183 'series': media_data.get('seriesName'),
184 'season_number': int_or_none(media_data.get('seasonNumber')),
185 'episode': media_data.get('name') if media_data.get('episodeNumber') else None,
186 'episode_number': int_or_none(media_data.get('episodeNumber')),
187 }