]> jfr.im git - yt-dlp.git/blob - yt_dlp/extractor/ertgr.py
[misc] Add `hatch`, `ruff`, `pre-commit` and improve dev docs (#7409)
[yt-dlp.git] / yt_dlp / extractor / ertgr.py
1 import json
2 import re
3
4 from .common import InfoExtractor
5 from ..compat import compat_str
6 from ..utils import (
7 ExtractorError,
8 clean_html,
9 determine_ext,
10 dict_get,
11 int_or_none,
12 merge_dicts,
13 parse_age_limit,
14 parse_iso8601,
15 parse_qs,
16 str_or_none,
17 try_get,
18 url_or_none,
19 variadic,
20 )
21
22
23 class ERTFlixBaseIE(InfoExtractor):
24 def _call_api(
25 self, video_id, method='Player/AcquireContent', api_version=1,
26 param_headers=None, data=None, headers=None, **params):
27 platform_codename = {'platformCodename': 'www'}
28 headers_as_param = {'X-Api-Date-Format': 'iso', 'X-Api-Camel-Case': False}
29 headers_as_param.update(param_headers or {})
30 headers = headers or {}
31 if data:
32 headers['Content-Type'] = headers_as_param['Content-Type'] = 'application/json;charset=utf-8'
33 data = json.dumps(merge_dicts(platform_codename, data)).encode('utf-8')
34 query = merge_dicts(
35 {} if data else platform_codename,
36 {'$headers': json.dumps(headers_as_param)},
37 params)
38 response = self._download_json(
39 'https://api.app.ertflix.gr/v%s/%s' % (str(api_version), method),
40 video_id, fatal=False, query=query, data=data, headers=headers)
41 if try_get(response, lambda x: x['Result']['Success']) is True:
42 return response
43
44 def _call_api_get_tiles(self, video_id, *tile_ids):
45 requested_tile_ids = [video_id] + list(tile_ids)
46 requested_tiles = [{'Id': tile_id} for tile_id in requested_tile_ids]
47 tiles_response = self._call_api(
48 video_id, method='Tile/GetTiles', api_version=2,
49 data={'RequestedTiles': requested_tiles})
50 tiles = try_get(tiles_response, lambda x: x['Tiles'], list) or []
51 if tile_ids:
52 if sorted([tile['Id'] for tile in tiles]) != sorted(requested_tile_ids):
53 raise ExtractorError('Requested tiles not found', video_id=video_id)
54 return tiles
55 try:
56 return next(tile for tile in tiles if tile['Id'] == video_id)
57 except StopIteration:
58 raise ExtractorError('No matching tile found', video_id=video_id)
59
60
61 class ERTFlixCodenameIE(ERTFlixBaseIE):
62 IE_NAME = 'ertflix:codename'
63 IE_DESC = 'ERTFLIX videos by codename'
64 _VALID_URL = r'ertflix:(?P<id>[\w-]+)'
65 _TESTS = [{
66 'url': 'ertflix:monogramma-praxitelis-tzanoylinos',
67 'md5': '5b9c2cd171f09126167e4082fc1dd0ef',
68 'info_dict': {
69 'id': 'monogramma-praxitelis-tzanoylinos',
70 'ext': 'mp4',
71 'title': 'md5:ef0b439902963d56c43ac83c3f41dd0e',
72 },
73 },
74 ]
75
76 def _extract_formats_and_subs(self, video_id):
77 media_info = self._call_api(video_id, codename=video_id)
78 formats, subs = [], {}
79 for media_file in try_get(media_info, lambda x: x['MediaFiles'], list) or []:
80 for media in try_get(media_file, lambda x: x['Formats'], list) or []:
81 fmt_url = url_or_none(try_get(media, lambda x: x['Url']))
82 if not fmt_url:
83 continue
84 ext = determine_ext(fmt_url)
85 if ext == 'm3u8':
86 formats_, subs_ = self._extract_m3u8_formats_and_subtitles(
87 fmt_url, video_id, m3u8_id='hls', ext='mp4', fatal=False)
88 elif ext == 'mpd':
89 formats_, subs_ = self._extract_mpd_formats_and_subtitles(
90 fmt_url, video_id, mpd_id='dash', fatal=False)
91 else:
92 formats.append({
93 'url': fmt_url,
94 'format_id': str_or_none(media.get('Id')),
95 })
96 continue
97 formats.extend(formats_)
98 self._merge_subtitles(subs_, target=subs)
99
100 return formats, subs
101
102 def _real_extract(self, url):
103 video_id = self._match_id(url)
104
105 formats, subs = self._extract_formats_and_subs(video_id)
106
107 if formats:
108 return {
109 'id': video_id,
110 'formats': formats,
111 'subtitles': subs,
112 'title': self._generic_title(url),
113 }
114
115
116 class ERTFlixIE(ERTFlixBaseIE):
117 IE_NAME = 'ertflix'
118 IE_DESC = 'ERTFLIX videos'
119 _VALID_URL = r'https?://www\.ertflix\.gr/(?:[^/]+/)?(?:series|vod)/(?P<id>[a-z]{3}\.\d+)'
120 _TESTS = [{
121 'url': 'https://www.ertflix.gr/vod/vod.173258-aoratoi-ergates',
122 'md5': '6479d5e60fd7e520b07ba5411dcdd6e7',
123 'info_dict': {
124 'id': 'aoratoi-ergates',
125 'ext': 'mp4',
126 'title': 'md5:c1433d598fbba0211b0069021517f8b4',
127 'description': 'md5:01a64d113c31957eb7eb07719ab18ff4',
128 'thumbnail': r're:https?://.+\.jpg',
129 'episode_id': 'vod.173258',
130 'timestamp': 1639648800,
131 'upload_date': '20211216',
132 'duration': 3166,
133 'age_limit': 8,
134 },
135 }, {
136 'url': 'https://www.ertflix.gr/series/ser.3448-monogramma',
137 'info_dict': {
138 'id': 'ser.3448',
139 'age_limit': 8,
140 'description': 'Η εκπομπή σαράντα ετών που σημάδεψε τον πολιτισμό μας.',
141 'title': 'Μονόγραμμα',
142 },
143 'playlist_mincount': 64,
144 }, {
145 'url': 'https://www.ertflix.gr/series/ser.3448-monogramma?season=1',
146 'info_dict': {
147 'id': 'ser.3448',
148 'age_limit': 8,
149 'description': 'Η εκπομπή σαράντα ετών που σημάδεψε τον πολιτισμό μας.',
150 'title': 'Μονόγραμμα',
151 },
152 'playlist_count': 22,
153 }, {
154 'url': 'https://www.ertflix.gr/series/ser.3448-monogramma?season=1&season=2021%20-%202022',
155 'info_dict': {
156 'id': 'ser.3448',
157 'age_limit': 8,
158 'description': 'Η εκπομπή σαράντα ετών που σημάδεψε τον πολιτισμό μας.',
159 'title': 'Μονόγραμμα',
160 },
161 'playlist_mincount': 36,
162 }, {
163 'url': 'https://www.ertflix.gr/series/ser.164991-to-diktuo-1?season=1-9',
164 'info_dict': {
165 'id': 'ser.164991',
166 'age_limit': 8,
167 'description': 'Η πρώτη ελληνική εκπομπή με θεματολογία αποκλειστικά γύρω από το ίντερνετ.',
168 'title': 'Το δίκτυο',
169 },
170 'playlist_mincount': 9,
171 }, {
172 'url': 'https://www.ertflix.gr/en/vod/vod.127652-ta-kalytera-mas-chronia-ep1-mia-volta-sto-feggari',
173 'only_matching': True,
174 }]
175
176 def _extract_episode(self, episode):
177 codename = try_get(episode, lambda x: x['Codename'], compat_str)
178 title = episode.get('Title')
179 description = clean_html(dict_get(episode, ('ShortDescription', 'TinyDescription', )))
180 if not codename or not title or not episode.get('HasPlayableStream', True):
181 return
182 thumbnail = next((
183 url_or_none(thumb.get('Url'))
184 for thumb in variadic(dict_get(episode, ('Images', 'Image')) or {})
185 if thumb.get('IsMain')),
186 None)
187 return {
188 '_type': 'url_transparent',
189 'thumbnail': thumbnail,
190 'id': codename,
191 'episode_id': episode.get('Id'),
192 'title': title,
193 'alt_title': episode.get('Subtitle'),
194 'description': description,
195 'timestamp': parse_iso8601(episode.get('PublishDate')),
196 'duration': episode.get('DurationSeconds'),
197 'age_limit': self._parse_age_rating(episode),
198 'url': 'ertflix:%s' % (codename, ),
199 }
200
201 @staticmethod
202 def _parse_age_rating(info_dict):
203 return parse_age_limit(
204 info_dict.get('AgeRating')
205 or (info_dict.get('IsAdultContent') and 18)
206 or (info_dict.get('IsKidsContent') and 0))
207
208 def _extract_series(self, video_id, season_titles=None, season_numbers=None):
209 media_info = self._call_api(video_id, method='Tile/GetSeriesDetails', id=video_id)
210
211 series = try_get(media_info, lambda x: x['Series'], dict) or {}
212 series_info = {
213 'age_limit': self._parse_age_rating(series),
214 'title': series.get('Title'),
215 'description': dict_get(series, ('ShortDescription', 'TinyDescription', )),
216 }
217 if season_numbers:
218 season_titles = season_titles or []
219 for season in try_get(series, lambda x: x['Seasons'], list) or []:
220 if season.get('SeasonNumber') in season_numbers and season.get('Title'):
221 season_titles.append(season['Title'])
222
223 def gen_episode(m_info, season_titles):
224 for episode_group in try_get(m_info, lambda x: x['EpisodeGroups'], list) or []:
225 if season_titles and episode_group.get('Title') not in season_titles:
226 continue
227 episodes = try_get(episode_group, lambda x: x['Episodes'], list)
228 if not episodes:
229 continue
230 season_info = {
231 'season': episode_group.get('Title'),
232 'season_number': int_or_none(episode_group.get('SeasonNumber')),
233 }
234 try:
235 episodes = [(int(ep['EpisodeNumber']), ep) for ep in episodes]
236 episodes.sort()
237 except (KeyError, ValueError):
238 episodes = enumerate(episodes, 1)
239 for n, episode in episodes:
240 info = self._extract_episode(episode)
241 if info is None:
242 continue
243 info['episode_number'] = n
244 info.update(season_info)
245 yield info
246
247 return self.playlist_result(
248 gen_episode(media_info, season_titles), playlist_id=video_id, **series_info)
249
250 def _real_extract(self, url):
251 video_id = self._match_id(url)
252 if video_id.startswith('ser.'):
253 param_season = parse_qs(url).get('season', [None])
254 param_season = [
255 (have_number, int_or_none(v) if have_number else str_or_none(v))
256 for have_number, v in
257 [(int_or_none(ps) is not None, ps) for ps in param_season]
258 if v is not None
259 ]
260 season_kwargs = {
261 k: [v for is_num, v in param_season if is_num is c] or None
262 for k, c in
263 [('season_titles', False), ('season_numbers', True)]
264 }
265 return self._extract_series(video_id, **season_kwargs)
266
267 return self._extract_episode(self._call_api_get_tiles(video_id))
268
269
270 class ERTWebtvEmbedIE(InfoExtractor):
271 IE_NAME = 'ertwebtv:embed'
272 IE_DESC = 'ert.gr webtv embedded videos'
273 _BASE_PLAYER_URL_RE = re.escape('//www.ert.gr/webtv/live-uni/vod/dt-uni-vod.php')
274 _VALID_URL = rf'https?:{_BASE_PLAYER_URL_RE}\?([^#]+&)?f=(?P<id>[^#&]+)'
275 _EMBED_REGEX = [rf'<iframe[^>]+?src=(?P<_q1>["\'])(?P<url>(?:https?:)?{_BASE_PLAYER_URL_RE}\?(?:(?!(?P=_q1)).)+)(?P=_q1)']
276
277 _TESTS = [{
278 'url': 'https://www.ert.gr/webtv/live-uni/vod/dt-uni-vod.php?f=trailers/E2251_TO_DIKTYO_E09_16-01_1900.mp4&bgimg=/photos/2022/1/to_diktio_ep09_i_istoria_tou_diadiktiou_stin_Ellada_1021x576.jpg',
279 'md5': 'f9e9900c25c26f4ecfbddbb4b6305854',
280 'info_dict': {
281 'id': 'trailers/E2251_TO_DIKTYO_E09_16-01_1900.mp4',
282 'title': 'md5:914f06a73cd8b62fbcd6fb90c636e497',
283 'ext': 'mp4',
284 'thumbnail': 'https://program.ert.gr/photos/2022/1/to_diktio_ep09_i_istoria_tou_diadiktiou_stin_Ellada_1021x576.jpg'
285 },
286 }]
287
288 def _real_extract(self, url):
289 video_id = self._match_id(url)
290 formats, subs = self._extract_m3u8_formats_and_subtitles(
291 f'https://mediastream.ert.gr/vodedge/_definst_/mp4:dvrorigin/{video_id}/playlist.m3u8',
292 video_id, 'mp4')
293 thumbnail_id = parse_qs(url).get('bgimg', [None])[0]
294 if thumbnail_id and not thumbnail_id.startswith('http'):
295 thumbnail_id = f'https://program.ert.gr{thumbnail_id}'
296 return {
297 'id': video_id,
298 'title': f'VOD - {video_id}',
299 'thumbnail': thumbnail_id,
300 'formats': formats,
301 'subtitles': subs,
302 }