1#!/usr/bin/env python3
2# coding: utf-8
3
4from __future__ import unicode_literals
5
6# Allow direct execution
7import os
8import sys
9import unittest
10sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
12import copy
13import json
14
15from test.helper import FakeYDL, assertRegexpMatches
16from yt_dlp import YoutubeDL
17from yt_dlp.compat import compat_os_name, compat_setenv, compat_str, compat_urllib_error
18from yt_dlp.extractor import YoutubeIE
19from yt_dlp.extractor.common import InfoExtractor
20from yt_dlp.postprocessor.common import PostProcessor
21from yt_dlp.utils import ExtractorError, int_or_none, match_filter_func, LazyList
22
23TEST_URL = 'http://localhost/sample.mp4'
24
25
26class YDL(FakeYDL):
27    def __init__(self, *args, **kwargs):
28        super(YDL, self).__init__(*args, **kwargs)
29        self.downloaded_info_dicts = []
30        self.msgs = []
31
32    def process_info(self, info_dict):
33        info_dict.pop('__original_infodict', None)
34        self.downloaded_info_dicts.append(info_dict)
35
36    def to_screen(self, msg):
37        self.msgs.append(msg)
38
39    def dl(self, *args, **kwargs):
40        assert False, 'Downloader must not be invoked for test_YoutubeDL'
41
42
43def _make_result(formats, **kwargs):
44    res = {
45        'formats': formats,
46        'id': 'testid',
47        'title': 'testttitle',
48        'extractor': 'testex',
49        'extractor_key': 'TestEx',
50        'webpage_url': 'http://example.com/watch?v=shenanigans',
51    }
52    res.update(**kwargs)
53    return res
54
55
56class TestFormatSelection(unittest.TestCase):
57    def test_prefer_free_formats(self):
58        # Same resolution => download webm
59        ydl = YDL()
60        ydl.params['prefer_free_formats'] = True
61        formats = [
62            {'ext': 'webm', 'height': 460, 'url': TEST_URL},
63            {'ext': 'mp4', 'height': 460, 'url': TEST_URL},
64        ]
65        info_dict = _make_result(formats)
66        yie = YoutubeIE(ydl)
67        yie._sort_formats(info_dict['formats'])
68        ydl.process_ie_result(info_dict)
69        downloaded = ydl.downloaded_info_dicts[0]
70        self.assertEqual(downloaded['ext'], 'webm')
71
72        # Different resolution => download best quality (mp4)
73        ydl = YDL()
74        ydl.params['prefer_free_formats'] = True
75        formats = [
76            {'ext': 'webm', 'height': 720, 'url': TEST_URL},
77            {'ext': 'mp4', 'height': 1080, 'url': TEST_URL},
78        ]
79        info_dict['formats'] = formats
80        yie = YoutubeIE(ydl)
81        yie._sort_formats(info_dict['formats'])
82        ydl.process_ie_result(info_dict)
83        downloaded = ydl.downloaded_info_dicts[0]
84        self.assertEqual(downloaded['ext'], 'mp4')
85
86        # No prefer_free_formats => prefer mp4 and webm
87        ydl = YDL()
88        ydl.params['prefer_free_formats'] = False
89        formats = [
90            {'ext': 'webm', 'height': 720, 'url': TEST_URL},
91            {'ext': 'mp4', 'height': 720, 'url': TEST_URL},
92            {'ext': 'flv', 'height': 720, 'url': TEST_URL},
93        ]
94        info_dict['formats'] = formats
95        yie = YoutubeIE(ydl)
96        yie._sort_formats(info_dict['formats'])
97        ydl.process_ie_result(info_dict)
98        downloaded = ydl.downloaded_info_dicts[0]
99        self.assertEqual(downloaded['ext'], 'mp4')
100
101        ydl = YDL()
102        ydl.params['prefer_free_formats'] = False
103        formats = [
104            {'ext': 'flv', 'height': 720, 'url': TEST_URL},
105            {'ext': 'webm', 'height': 720, 'url': TEST_URL},
106        ]
107        info_dict['formats'] = formats
108        yie = YoutubeIE(ydl)
109        yie._sort_formats(info_dict['formats'])
110        ydl.process_ie_result(info_dict)
111        downloaded = ydl.downloaded_info_dicts[0]
112        self.assertEqual(downloaded['ext'], 'webm')
113
114    def test_format_selection(self):
115        formats = [
116            {'format_id': '35', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL},
117            {'format_id': 'example-with-dashes', 'ext': 'webm', 'preference': 1, 'url': TEST_URL},
118            {'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': TEST_URL},
119            {'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': TEST_URL},
120            {'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': TEST_URL},
121        ]
122        info_dict = _make_result(formats)
123
124        def test(inp, *expected, multi=False):
125            ydl = YDL({
126                'format': inp,
127                'allow_multiple_video_streams': multi,
128                'allow_multiple_audio_streams': multi,
129            })
130            ydl.process_ie_result(info_dict.copy())
131            downloaded = map(lambda x: x['format_id'], ydl.downloaded_info_dicts)
132            self.assertEqual(list(downloaded), list(expected))
133
134        test('20/47', '47')
135        test('20/71/worst', '35')
136        test(None, '2')
137        test('webm/mp4', '47')
138        test('3gp/40/mp4', '35')
139        test('example-with-dashes', 'example-with-dashes')
140        test('all', '2', '47', '45', 'example-with-dashes', '35')
141        test('mergeall', '2+47+45+example-with-dashes+35', multi=True)
142
143    def test_format_selection_audio(self):
144        formats = [
145            {'format_id': 'audio-low', 'ext': 'webm', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL},
146            {'format_id': 'audio-mid', 'ext': 'webm', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL},
147            {'format_id': 'audio-high', 'ext': 'flv', 'preference': 3, 'vcodec': 'none', 'url': TEST_URL},
148            {'format_id': 'vid', 'ext': 'mp4', 'preference': 4, 'url': TEST_URL},
149        ]
150        info_dict = _make_result(formats)
151
152        ydl = YDL({'format': 'bestaudio'})
153        ydl.process_ie_result(info_dict.copy())
154        downloaded = ydl.downloaded_info_dicts[0]
155        self.assertEqual(downloaded['format_id'], 'audio-high')
156
157        ydl = YDL({'format': 'worstaudio'})
158        ydl.process_ie_result(info_dict.copy())
159        downloaded = ydl.downloaded_info_dicts[0]
160        self.assertEqual(downloaded['format_id'], 'audio-low')
161
162        formats = [
163            {'format_id': 'vid-low', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL},
164            {'format_id': 'vid-high', 'ext': 'mp4', 'preference': 2, 'url': TEST_URL},
165        ]
166        info_dict = _make_result(formats)
167
168        ydl = YDL({'format': 'bestaudio/worstaudio/best'})
169        ydl.process_ie_result(info_dict.copy())
170        downloaded = ydl.downloaded_info_dicts[0]
171        self.assertEqual(downloaded['format_id'], 'vid-high')
172
173    def test_format_selection_audio_exts(self):
174        formats = [
175            {'format_id': 'mp3-64', 'ext': 'mp3', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'},
176            {'format_id': 'ogg-64', 'ext': 'ogg', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'},
177            {'format_id': 'aac-64', 'ext': 'aac', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'},
178            {'format_id': 'mp3-32', 'ext': 'mp3', 'abr': 32, 'url': 'http://_', 'vcodec': 'none'},
179            {'format_id': 'aac-32', 'ext': 'aac', 'abr': 32, 'url': 'http://_', 'vcodec': 'none'},
180        ]
181
182        info_dict = _make_result(formats)
183        ydl = YDL({'format': 'best'})
184        ie = YoutubeIE(ydl)
185        ie._sort_formats(info_dict['formats'])
186        ydl.process_ie_result(copy.deepcopy(info_dict))
187        downloaded = ydl.downloaded_info_dicts[0]
188        self.assertEqual(downloaded['format_id'], 'aac-64')
189
190        ydl = YDL({'format': 'mp3'})
191        ie = YoutubeIE(ydl)
192        ie._sort_formats(info_dict['formats'])
193        ydl.process_ie_result(copy.deepcopy(info_dict))
194        downloaded = ydl.downloaded_info_dicts[0]
195        self.assertEqual(downloaded['format_id'], 'mp3-64')
196
197        ydl = YDL({'prefer_free_formats': True})
198        ie = YoutubeIE(ydl)
199        ie._sort_formats(info_dict['formats'])
200        ydl.process_ie_result(copy.deepcopy(info_dict))
201        downloaded = ydl.downloaded_info_dicts[0]
202        self.assertEqual(downloaded['format_id'], 'ogg-64')
203
204    def test_format_selection_video(self):
205        formats = [
206            {'format_id': 'dash-video-low', 'ext': 'mp4', 'preference': 1, 'acodec': 'none', 'url': TEST_URL},
207            {'format_id': 'dash-video-high', 'ext': 'mp4', 'preference': 2, 'acodec': 'none', 'url': TEST_URL},
208            {'format_id': 'vid', 'ext': 'mp4', 'preference': 3, 'url': TEST_URL},
209        ]
210        info_dict = _make_result(formats)
211
212        ydl = YDL({'format': 'bestvideo'})
213        ydl.process_ie_result(info_dict.copy())
214        downloaded = ydl.downloaded_info_dicts[0]
215        self.assertEqual(downloaded['format_id'], 'dash-video-high')
216
217        ydl = YDL({'format': 'worstvideo'})
218        ydl.process_ie_result(info_dict.copy())
219        downloaded = ydl.downloaded_info_dicts[0]
220        self.assertEqual(downloaded['format_id'], 'dash-video-low')
221
222        ydl = YDL({'format': 'bestvideo[format_id^=dash][format_id$=low]'})
223        ydl.process_ie_result(info_dict.copy())
224        downloaded = ydl.downloaded_info_dicts[0]
225        self.assertEqual(downloaded['format_id'], 'dash-video-low')
226
227        formats = [
228            {'format_id': 'vid-vcodec-dot', 'ext': 'mp4', 'preference': 1, 'vcodec': 'avc1.123456', 'acodec': 'none', 'url': TEST_URL},
229        ]
230        info_dict = _make_result(formats)
231
232        ydl = YDL({'format': 'bestvideo[vcodec=avc1.123456]'})
233        ydl.process_ie_result(info_dict.copy())
234        downloaded = ydl.downloaded_info_dicts[0]
235        self.assertEqual(downloaded['format_id'], 'vid-vcodec-dot')
236
237    def test_format_selection_string_ops(self):
238        formats = [
239            {'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL},
240            {'format_id': 'zxc-cxz', 'ext': 'webm', 'url': TEST_URL},
241        ]
242        info_dict = _make_result(formats)
243
244        # equals (=)
245        ydl = YDL({'format': '[format_id=abc-cba]'})
246        ydl.process_ie_result(info_dict.copy())
247        downloaded = ydl.downloaded_info_dicts[0]
248        self.assertEqual(downloaded['format_id'], 'abc-cba')
249
250        # does not equal (!=)
251        ydl = YDL({'format': '[format_id!=abc-cba]'})
252        ydl.process_ie_result(info_dict.copy())
253        downloaded = ydl.downloaded_info_dicts[0]
254        self.assertEqual(downloaded['format_id'], 'zxc-cxz')
255
256        ydl = YDL({'format': '[format_id!=abc-cba][format_id!=zxc-cxz]'})
257        self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
258
259        # starts with (^=)
260        ydl = YDL({'format': '[format_id^=abc]'})
261        ydl.process_ie_result(info_dict.copy())
262        downloaded = ydl.downloaded_info_dicts[0]
263        self.assertEqual(downloaded['format_id'], 'abc-cba')
264
265        # does not start with (!^=)
266        ydl = YDL({'format': '[format_id!^=abc]'})
267        ydl.process_ie_result(info_dict.copy())
268        downloaded = ydl.downloaded_info_dicts[0]
269        self.assertEqual(downloaded['format_id'], 'zxc-cxz')
270
271        ydl = YDL({'format': '[format_id!^=abc][format_id!^=zxc]'})
272        self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
273
274        # ends with ($=)
275        ydl = YDL({'format': '[format_id$=cba]'})
276        ydl.process_ie_result(info_dict.copy())
277        downloaded = ydl.downloaded_info_dicts[0]
278        self.assertEqual(downloaded['format_id'], 'abc-cba')
279
280        # does not end with (!$=)
281        ydl = YDL({'format': '[format_id!$=cba]'})
282        ydl.process_ie_result(info_dict.copy())
283        downloaded = ydl.downloaded_info_dicts[0]
284        self.assertEqual(downloaded['format_id'], 'zxc-cxz')
285
286        ydl = YDL({'format': '[format_id!$=cba][format_id!$=cxz]'})
287        self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
288
289        # contains (*=)
290        ydl = YDL({'format': '[format_id*=bc-cb]'})
291        ydl.process_ie_result(info_dict.copy())
292        downloaded = ydl.downloaded_info_dicts[0]
293        self.assertEqual(downloaded['format_id'], 'abc-cba')
294
295        # does not contain (!*=)
296        ydl = YDL({'format': '[format_id!*=bc-cb]'})
297        ydl.process_ie_result(info_dict.copy())
298        downloaded = ydl.downloaded_info_dicts[0]
299        self.assertEqual(downloaded['format_id'], 'zxc-cxz')
300
301        ydl = YDL({'format': '[format_id!*=abc][format_id!*=zxc]'})
302        self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
303
304        ydl = YDL({'format': '[format_id!*=-]'})
305        self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
306
307    def test_youtube_format_selection(self):
308        # FIXME: Rewrite in accordance with the new format sorting options
309        return
310
311        order = [
312            '38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13',
313            # Apple HTTP Live Streaming
314            '96', '95', '94', '93', '92', '132', '151',
315            # 3D
316            '85', '84', '102', '83', '101', '82', '100',
317            # Dash video
318            '137', '248', '136', '247', '135', '246',
319            '245', '244', '134', '243', '133', '242', '160',
320            # Dash audio
321            '141', '172', '140', '171', '139',
322        ]
323
324        def format_info(f_id):
325            info = YoutubeIE._formats[f_id].copy()
326
327            # XXX: In real cases InfoExtractor._parse_mpd_formats() fills up 'acodec'
328            # and 'vcodec', while in tests such information is incomplete since
329            # commit a6c2c24479e5f4827ceb06f64d855329c0a6f593
330            # test_YoutubeDL.test_youtube_format_selection is broken without
331            # this fix
332            if 'acodec' in info and 'vcodec' not in info:
333                info['vcodec'] = 'none'
334            elif 'vcodec' in info and 'acodec' not in info:
335                info['acodec'] = 'none'
336
337            info['format_id'] = f_id
338            info['url'] = 'url:' + f_id
339            return info
340        formats_order = [format_info(f_id) for f_id in order]
341
342        info_dict = _make_result(list(formats_order), extractor='youtube')
343        ydl = YDL({'format': 'bestvideo+bestaudio'})
344        yie = YoutubeIE(ydl)
345        yie._sort_formats(info_dict['formats'])
346        ydl.process_ie_result(info_dict)
347        downloaded = ydl.downloaded_info_dicts[0]
348        self.assertEqual(downloaded['format_id'], '248+172')
349        self.assertEqual(downloaded['ext'], 'mp4')
350
351        info_dict = _make_result(list(formats_order), extractor='youtube')
352        ydl = YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'})
353        yie = YoutubeIE(ydl)
354        yie._sort_formats(info_dict['formats'])
355        ydl.process_ie_result(info_dict)
356        downloaded = ydl.downloaded_info_dicts[0]
357        self.assertEqual(downloaded['format_id'], '38')
358
359        info_dict = _make_result(list(formats_order), extractor='youtube')
360        ydl = YDL({'format': 'bestvideo/best,bestaudio'})
361        yie = YoutubeIE(ydl)
362        yie._sort_formats(info_dict['formats'])
363        ydl.process_ie_result(info_dict)
364        downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
365        self.assertEqual(downloaded_ids, ['137', '141'])
366
367        info_dict = _make_result(list(formats_order), extractor='youtube')
368        ydl = YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'})
369        yie = YoutubeIE(ydl)
370        yie._sort_formats(info_dict['formats'])
371        ydl.process_ie_result(info_dict)
372        downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
373        self.assertEqual(downloaded_ids, ['137+141', '248+141'])
374
375        info_dict = _make_result(list(formats_order), extractor='youtube')
376        ydl = YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'})
377        yie = YoutubeIE(ydl)
378        yie._sort_formats(info_dict['formats'])
379        ydl.process_ie_result(info_dict)
380        downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
381        self.assertEqual(downloaded_ids, ['136+141', '247+141'])
382
383        info_dict = _make_result(list(formats_order), extractor='youtube')
384        ydl = YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'})
385        yie = YoutubeIE(ydl)
386        yie._sort_formats(info_dict['formats'])
387        ydl.process_ie_result(info_dict)
388        downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
389        self.assertEqual(downloaded_ids, ['248+141'])
390
391        for f1, f2 in zip(formats_order, formats_order[1:]):
392            info_dict = _make_result([f1, f2], extractor='youtube')
393            ydl = YDL({'format': 'best/bestvideo'})
394            yie = YoutubeIE(ydl)
395            yie._sort_formats(info_dict['formats'])
396            ydl.process_ie_result(info_dict)
397            downloaded = ydl.downloaded_info_dicts[0]
398            self.assertEqual(downloaded['format_id'], f1['format_id'])
399
400            info_dict = _make_result([f2, f1], extractor='youtube')
401            ydl = YDL({'format': 'best/bestvideo'})
402            yie = YoutubeIE(ydl)
403            yie._sort_formats(info_dict['formats'])
404            ydl.process_ie_result(info_dict)
405            downloaded = ydl.downloaded_info_dicts[0]
406            self.assertEqual(downloaded['format_id'], f1['format_id'])
407
408    def test_audio_only_extractor_format_selection(self):
409        # For extractors with incomplete formats (all formats are audio-only or
410        # video-only) best and worst should fallback to corresponding best/worst
411        # video-only or audio-only formats (as per
412        # https://github.com/ytdl-org/youtube-dl/pull/5556)
413        formats = [
414            {'format_id': 'low', 'ext': 'mp3', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL},
415            {'format_id': 'high', 'ext': 'mp3', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL},
416        ]
417        info_dict = _make_result(formats)
418
419        ydl = YDL({'format': 'best'})
420        ydl.process_ie_result(info_dict.copy())
421        downloaded = ydl.downloaded_info_dicts[0]
422        self.assertEqual(downloaded['format_id'], 'high')
423
424        ydl = YDL({'format': 'worst'})
425        ydl.process_ie_result(info_dict.copy())
426        downloaded = ydl.downloaded_info_dicts[0]
427        self.assertEqual(downloaded['format_id'], 'low')
428
429    def test_format_not_available(self):
430        formats = [
431            {'format_id': 'regular', 'ext': 'mp4', 'height': 360, 'url': TEST_URL},
432            {'format_id': 'video', 'ext': 'mp4', 'height': 720, 'acodec': 'none', 'url': TEST_URL},
433        ]
434        info_dict = _make_result(formats)
435
436        # This must fail since complete video-audio format does not match filter
437        # and extractor does not provide incomplete only formats (i.e. only
438        # video-only or audio-only).
439        ydl = YDL({'format': 'best[height>360]'})
440        self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
441
442    def test_format_selection_issue_10083(self):
443        # See https://github.com/ytdl-org/youtube-dl/issues/10083
444        formats = [
445            {'format_id': 'regular', 'height': 360, 'url': TEST_URL},
446            {'format_id': 'video', 'height': 720, 'acodec': 'none', 'url': TEST_URL},
447            {'format_id': 'audio', 'vcodec': 'none', 'url': TEST_URL},
448        ]
449        info_dict = _make_result(formats)
450
451        ydl = YDL({'format': 'best[height>360]/bestvideo[height>360]+bestaudio'})
452        ydl.process_ie_result(info_dict.copy())
453        self.assertEqual(ydl.downloaded_info_dicts[0]['format_id'], 'video+audio')
454
455    def test_invalid_format_specs(self):
456        def assert_syntax_error(format_spec):
457            self.assertRaises(SyntaxError, YDL, {'format': format_spec})
458
459        assert_syntax_error('bestvideo,,best')
460        assert_syntax_error('+bestaudio')
461        assert_syntax_error('bestvideo+')
462        assert_syntax_error('/')
463        assert_syntax_error('[720<height]')
464
465    def test_format_filtering(self):
466        formats = [
467            {'format_id': 'A', 'filesize': 500, 'width': 1000},
468            {'format_id': 'B', 'filesize': 1000, 'width': 500},
469            {'format_id': 'C', 'filesize': 1000, 'width': 400},
470            {'format_id': 'D', 'filesize': 2000, 'width': 600},
471            {'format_id': 'E', 'filesize': 3000},
472            {'format_id': 'F'},
473            {'format_id': 'G', 'filesize': 1000000},
474        ]
475        for f in formats:
476            f['url'] = 'http://_/'
477            f['ext'] = 'unknown'
478        info_dict = _make_result(formats)
479
480        ydl = YDL({'format': 'best[filesize<3000]'})
481        ydl.process_ie_result(info_dict)
482        downloaded = ydl.downloaded_info_dicts[0]
483        self.assertEqual(downloaded['format_id'], 'D')
484
485        ydl = YDL({'format': 'best[filesize<=3000]'})
486        ydl.process_ie_result(info_dict)
487        downloaded = ydl.downloaded_info_dicts[0]
488        self.assertEqual(downloaded['format_id'], 'E')
489
490        ydl = YDL({'format': 'best[filesize <= ? 3000]'})
491        ydl.process_ie_result(info_dict)
492        downloaded = ydl.downloaded_info_dicts[0]
493        self.assertEqual(downloaded['format_id'], 'F')
494
495        ydl = YDL({'format': 'best [filesize = 1000] [width>450]'})
496        ydl.process_ie_result(info_dict)
497        downloaded = ydl.downloaded_info_dicts[0]
498        self.assertEqual(downloaded['format_id'], 'B')
499
500        ydl = YDL({'format': 'best [filesize = 1000] [width!=450]'})
501        ydl.process_ie_result(info_dict)
502        downloaded = ydl.downloaded_info_dicts[0]
503        self.assertEqual(downloaded['format_id'], 'C')
504
505        ydl = YDL({'format': '[filesize>?1]'})
506        ydl.process_ie_result(info_dict)
507        downloaded = ydl.downloaded_info_dicts[0]
508        self.assertEqual(downloaded['format_id'], 'G')
509
510        ydl = YDL({'format': '[filesize<1M]'})
511        ydl.process_ie_result(info_dict)
512        downloaded = ydl.downloaded_info_dicts[0]
513        self.assertEqual(downloaded['format_id'], 'E')
514
515        ydl = YDL({'format': '[filesize<1MiB]'})
516        ydl.process_ie_result(info_dict)
517        downloaded = ydl.downloaded_info_dicts[0]
518        self.assertEqual(downloaded['format_id'], 'G')
519
520        ydl = YDL({'format': 'all[width>=400][width<=600]'})
521        ydl.process_ie_result(info_dict)
522        downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
523        self.assertEqual(downloaded_ids, ['D', 'C', 'B'])
524
525        ydl = YDL({'format': 'best[height<40]'})
526        try:
527            ydl.process_ie_result(info_dict)
528        except ExtractorError:
529            pass
530        self.assertEqual(ydl.downloaded_info_dicts, [])
531
532    def test_default_format_spec(self):
533        ydl = YDL({'simulate': True})
534        self.assertEqual(ydl._default_format_spec({}), 'bestvideo*+bestaudio/best')
535
536        ydl = YDL({})
537        self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
538
539        ydl = YDL({'simulate': True})
540        self.assertEqual(ydl._default_format_spec({'is_live': True}), 'bestvideo*+bestaudio/best')
541
542        ydl = YDL({'outtmpl': '-'})
543        self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
544
545        ydl = YDL({})
546        self.assertEqual(ydl._default_format_spec({}, download=False), 'bestvideo*+bestaudio/best')
547        self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
548
549
550class TestYoutubeDL(unittest.TestCase):
551    def test_subtitles(self):
552        def s_formats(lang, autocaption=False):
553            return [{
554                'ext': ext,
555                'url': 'http://localhost/video.%s.%s' % (lang, ext),
556                '_auto': autocaption,
557            } for ext in ['vtt', 'srt', 'ass']]
558        subtitles = dict((l, s_formats(l)) for l in ['en', 'fr', 'es'])
559        auto_captions = dict((l, s_formats(l, True)) for l in ['it', 'pt', 'es'])
560        info_dict = {
561            'id': 'test',
562            'title': 'Test',
563            'url': 'http://localhost/video.mp4',
564            'subtitles': subtitles,
565            'automatic_captions': auto_captions,
566            'extractor': 'TEST',
567            'webpage_url': 'http://example.com/watch?v=shenanigans',
568        }
569
570        def get_info(params={}):
571            params.setdefault('simulate', True)
572            ydl = YDL(params)
573            ydl.report_warning = lambda *args, **kargs: None
574            return ydl.process_video_result(info_dict, download=False)
575
576        result = get_info()
577        self.assertFalse(result.get('requested_subtitles'))
578        self.assertEqual(result['subtitles'], subtitles)
579        self.assertEqual(result['automatic_captions'], auto_captions)
580
581        result = get_info({'writesubtitles': True})
582        subs = result['requested_subtitles']
583        self.assertTrue(subs)
584        self.assertEqual(set(subs.keys()), set(['en']))
585        self.assertTrue(subs['en'].get('data') is None)
586        self.assertEqual(subs['en']['ext'], 'ass')
587
588        result = get_info({'writesubtitles': True, 'subtitlesformat': 'foo/srt'})
589        subs = result['requested_subtitles']
590        self.assertEqual(subs['en']['ext'], 'srt')
591
592        result = get_info({'writesubtitles': True, 'subtitleslangs': ['es', 'fr', 'it']})
593        subs = result['requested_subtitles']
594        self.assertTrue(subs)
595        self.assertEqual(set(subs.keys()), set(['es', 'fr']))
596
597        result = get_info({'writesubtitles': True, 'subtitleslangs': ['all', '-en']})
598        subs = result['requested_subtitles']
599        self.assertTrue(subs)
600        self.assertEqual(set(subs.keys()), set(['es', 'fr']))
601
602        result = get_info({'writesubtitles': True, 'subtitleslangs': ['en', 'fr', '-en']})
603        subs = result['requested_subtitles']
604        self.assertTrue(subs)
605        self.assertEqual(set(subs.keys()), set(['fr']))
606
607        result = get_info({'writesubtitles': True, 'subtitleslangs': ['-en', 'en']})
608        subs = result['requested_subtitles']
609        self.assertTrue(subs)
610        self.assertEqual(set(subs.keys()), set(['en']))
611
612        result = get_info({'writesubtitles': True, 'subtitleslangs': ['e.+']})
613        subs = result['requested_subtitles']
614        self.assertTrue(subs)
615        self.assertEqual(set(subs.keys()), set(['es', 'en']))
616
617        result = get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
618        subs = result['requested_subtitles']
619        self.assertTrue(subs)
620        self.assertEqual(set(subs.keys()), set(['es', 'pt']))
621        self.assertFalse(subs['es']['_auto'])
622        self.assertTrue(subs['pt']['_auto'])
623
624        result = get_info({'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
625        subs = result['requested_subtitles']
626        self.assertTrue(subs)
627        self.assertEqual(set(subs.keys()), set(['es', 'pt']))
628        self.assertTrue(subs['es']['_auto'])
629        self.assertTrue(subs['pt']['_auto'])
630
631    def test_add_extra_info(self):
632        test_dict = {
633            'extractor': 'Foo',
634        }
635        extra_info = {
636            'extractor': 'Bar',
637            'playlist': 'funny videos',
638        }
639        YDL.add_extra_info(test_dict, extra_info)
640        self.assertEqual(test_dict['extractor'], 'Foo')
641        self.assertEqual(test_dict['playlist'], 'funny videos')
642
643    outtmpl_info = {
644        'id': '1234',
645        'ext': 'mp4',
646        'width': None,
647        'height': 1080,
648        'title1': '$PATH',
649        'title2': '%PATH%',
650        'title3': 'foo/bar\\test',
651        'title4': 'foo "bar" test',
652        'title5': 'áéí ��',
653        'timestamp': 1618488000,
654        'duration': 100000,
655        'playlist_index': 1,
656        'playlist_autonumber': 2,
657        '_last_playlist_index': 100,
658        'n_entries': 10,
659        'formats': [{'id': 'id 1'}, {'id': 'id 2'}, {'id': 'id 3'}]
660    }
661
662    def test_prepare_outtmpl_and_filename(self):
663        def test(tmpl, expected, *, info=None, **params):
664            params['outtmpl'] = tmpl
665            ydl = YoutubeDL(params)
666            ydl._num_downloads = 1
667            self.assertEqual(ydl.validate_outtmpl(tmpl), None)
668
669            out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info)
670            fname = ydl.prepare_filename(info or self.outtmpl_info)
671
672            if not isinstance(expected, (list, tuple)):
673                expected = (expected, expected)
674            for (name, got), expect in zip((('outtmpl', out), ('filename', fname)), expected):
675                if callable(expect):
676                    self.assertTrue(expect(got), f'Wrong {name} from {tmpl}')
677                else:
678                    self.assertEqual(got, expect, f'Wrong {name} from {tmpl}')
679
680        # Side-effects
681        original_infodict = dict(self.outtmpl_info)
682        test('foo.bar', 'foo.bar')
683        original_infodict['epoch'] = self.outtmpl_info.get('epoch')
684        self.assertTrue(isinstance(original_infodict['epoch'], int))
685        test('%(epoch)d', int_or_none)
686        self.assertEqual(original_infodict, self.outtmpl_info)
687
688        # Auto-generated fields
689        test('%(id)s.%(ext)s', '1234.mp4')
690        test('%(duration_string)s', ('27:46:40', '27-46-40'))
691        test('%(resolution)s', '1080p')
692        test('%(playlist_index)s', '001')
693        test('%(playlist_autonumber)s', '02')
694        test('%(autonumber)s', '00001')
695        test('%(autonumber+2)03d', '005', autonumber_start=3)
696        test('%(autonumber)s', '001', autonumber_size=3)
697
698        # Escaping %
699        test('%', '%')
700        test('%%', '%')
701        test('%%%%', '%%')
702        test('%s', '%s')
703        test('%%%s', '%%s')
704        test('%d', '%d')
705        test('%abc%', '%abc%')
706        test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
707        test('%%%(height)s', '%1080')
708        test('%(width)06d.%(ext)s', 'NA.mp4')
709        test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
710        test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
711
712        # ID sanitization
713        test('%(id)s', '_abcd', info={'id': '_abcd'})
714        test('%(some_id)s', '_abcd', info={'some_id': '_abcd'})
715        test('%(formats.0.id)s', '_abcd', info={'formats': [{'id': '_abcd'}]})
716        test('%(id)s', '-abcd', info={'id': '-abcd'})
717        test('%(id)s', '.abcd', info={'id': '.abcd'})
718        test('%(id)s', 'ab__cd', info={'id': 'ab__cd'})
719        test('%(id)s', ('ab:cd', 'ab -cd'), info={'id': 'ab:cd'})
720        test('%(id.0)s', '-', info={'id': '--'})
721
722        # Invalid templates
723        self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%(title)'), ValueError))
724        test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder='none')
725        test('%(..)s', 'NA')
726
727        # Entire info_dict
728        def expect_same_infodict(out):
729            got_dict = json.loads(out)
730            for info_field, expected in self.outtmpl_info.items():
731                self.assertEqual(got_dict.get(info_field), expected, info_field)
732            return True
733
734        test('%()j', (expect_same_infodict, str))
735
736        # NA placeholder
737        NA_TEST_OUTTMPL = '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
738        test(NA_TEST_OUTTMPL, 'NA-NA-def-1234.mp4')
739        test(NA_TEST_OUTTMPL, 'none-none-def-1234.mp4', outtmpl_na_placeholder='none')
740        test(NA_TEST_OUTTMPL, '--def-1234.mp4', outtmpl_na_placeholder='')
741        test('%(non_existent.0)s', 'NA')
742
743        # String formatting
744        FMT_TEST_OUTTMPL = '%%(height)%s.%%(ext)s'
745        test(FMT_TEST_OUTTMPL % 's', '1080.mp4')
746        test(FMT_TEST_OUTTMPL % 'd', '1080.mp4')
747        test(FMT_TEST_OUTTMPL % '6d', '  1080.mp4')
748        test(FMT_TEST_OUTTMPL % '-6d', '1080  .mp4')
749        test(FMT_TEST_OUTTMPL % '06d', '001080.mp4')
750        test(FMT_TEST_OUTTMPL % ' 06d', ' 01080.mp4')
751        test(FMT_TEST_OUTTMPL % '   06d', ' 01080.mp4')
752        test(FMT_TEST_OUTTMPL % '0 6d', ' 01080.mp4')
753        test(FMT_TEST_OUTTMPL % '0   6d', ' 01080.mp4')
754        test(FMT_TEST_OUTTMPL % '   0   6d', ' 01080.mp4')
755
756        # Type casting
757        test('%(id)d', '1234')
758        test('%(height)c', '1')
759        test('%(ext)c', 'm')
760        test('%(id)d %(id)r', "1234 '1234'")
761        test('%(id)r %(height)r', "'1234' 1080")
762        test('%(ext)s-%(ext|def)d', 'mp4-def')
763        test('%(width|0)04d', '0000')
764        test('a%(width|)d', 'a', outtmpl_na_placeholder='none')
765
766        FORMATS = self.outtmpl_info['formats']
767        sanitize = lambda x: x.replace(':', ' -').replace('"', "'").replace('\n', ' ')
768
769        # Custom type casting
770        test('%(formats.:.id)l', 'id 1, id 2, id 3')
771        test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
772        test('%(ext)l', 'mp4')
773        test('%(formats.:.id) 18l', '  id 1, id 2, id 3')
774        test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS))))
775        test('%(formats)#j', (json.dumps(FORMATS, indent=4), sanitize(json.dumps(FORMATS, indent=4))))
776        test('%(title5).3B', 'á')
777        test('%(title5)U', 'áéí ��')
778        test('%(title5)#U', 'a\u0301e\u0301i\u0301 ��')
779        test('%(title5)+U', 'áéí A')
780        test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
781        test('%(height)D', '1K')
782        test('%(height)5.2D', ' 1.08K')
783        test('%(title4)#S', 'foo_bar_test')
784        test('%(title4).10S', ('foo \'bar\' ', 'foo \'bar\'' + ('#' if compat_os_name == 'nt' else ' ')))
785        if compat_os_name == 'nt':
786            test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'"))
787            test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', "'id 1' 'id 2' 'id 3'"))
788            test('%(formats.0.id)#q', ('"id 1"', "'id 1'"))
789        else:
790            test('%(title4)q', ('\'foo "bar" test\'', "'foo 'bar' test'"))
791            test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
792            test('%(formats.0.id)#q', "'id 1'")
793
794        # Internal formatting
795        test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
796        test('%(title|%)s %(title|%%)s', '% %%')
797        test('%(id+1-height+3)05d', '00158')
798        test('%(width+100)05d', 'NA')
799        test('%(formats.0) 15s', ('% 15s' % FORMATS[0], '% 15s' % sanitize(str(FORMATS[0]))))
800        test('%(formats.0)r', (repr(FORMATS[0]), sanitize(repr(FORMATS[0]))))
801        test('%(height.0)03d', '001')
802        test('%(-height.0)04d', '-001')
803        test('%(formats.-1.id)s', FORMATS[-1]['id'])
804        test('%(formats.0.id.-1)d', FORMATS[0]['id'][-1])
805        test('%(formats.3)s', 'NA')
806        test('%(formats.:2:-1)r', repr(FORMATS[:2:-1]))
807        test('%(formats.0.id.-1+id)f', '1235.000000')
808        test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
809
810        # Alternates
811        test('%(title,id)s', '1234')
812        test('%(width-100,height+20|def)d', '1100')
813        test('%(width-100,height+width|def)s', 'def')
814        test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
815
816        # Replacement
817        test('%(id&foo)s.bar', 'foo.bar')
818        test('%(title&foo)s.bar', 'NA.bar')
819        test('%(title&foo|baz)s.bar', 'baz.bar')
820
821        # Laziness
822        def gen():
823            yield from range(5)
824            raise self.assertTrue(False, 'LazyList should not be evaluated till here')
825        test('%(key.4)s', '4', info={'key': LazyList(gen())})
826
827        # Empty filename
828        test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
829        # test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4'))  # fixme
830        # test('%(foo|)s', ('', '_'))  # fixme
831
832        # Environment variable expansion for prepare_filename
833        compat_setenv('__yt_dlp_var', 'expanded')
834        envvar = '%__yt_dlp_var%' if compat_os_name == 'nt' else '$__yt_dlp_var'
835        test(envvar, (envvar, 'expanded'))
836        if compat_os_name == 'nt':
837            test('%s%', ('%s%', '%s%'))
838            compat_setenv('s', 'expanded')
839            test('%s%', ('%s%', 'expanded'))  # %s% should be expanded before escaping %s
840            compat_setenv('(test)s', 'expanded')
841            test('%(test)s%', ('NA%', 'expanded'))  # Environment should take priority over template
842
843        # Path expansion and escaping
844        test('Hello %(title1)s', 'Hello $PATH')
845        test('Hello %(title2)s', 'Hello %PATH%')
846        test('%(title3)s', ('foo/bar\\test', 'foo_bar_test'))
847        test('folder/%(title3)s', ('folder/foo/bar\\test', 'folder%sfoo_bar_test' % os.path.sep))
848
849    def test_format_note(self):
850        ydl = YoutubeDL()
851        self.assertEqual(ydl._format_note({}), '')
852        assertRegexpMatches(self, ydl._format_note({
853            'vbr': 10,
854        }), r'^\s*10k$')
855        assertRegexpMatches(self, ydl._format_note({
856            'fps': 30,
857        }), r'^30fps$')
858
859    def test_postprocessors(self):
860        filename = 'post-processor-testfile.mp4'
861        audiofile = filename + '.mp3'
862
863        class SimplePP(PostProcessor):
864            def run(self, info):
865                with open(audiofile, 'wt') as f:
866                    f.write('EXAMPLE')
867                return [info['filepath']], info
868
869        def run_pp(params, PP):
870            with open(filename, 'wt') as f:
871                f.write('EXAMPLE')
872            ydl = YoutubeDL(params)
873            ydl.add_post_processor(PP())
874            ydl.post_process(filename, {'filepath': filename})
875
876        run_pp({'keepvideo': True}, SimplePP)
877        self.assertTrue(os.path.exists(filename), '%s doesn\'t exist' % filename)
878        self.assertTrue(os.path.exists(audiofile), '%s doesn\'t exist' % audiofile)
879        os.unlink(filename)
880        os.unlink(audiofile)
881
882        run_pp({'keepvideo': False}, SimplePP)
883        self.assertFalse(os.path.exists(filename), '%s exists' % filename)
884        self.assertTrue(os.path.exists(audiofile), '%s doesn\'t exist' % audiofile)
885        os.unlink(audiofile)
886
887        class ModifierPP(PostProcessor):
888            def run(self, info):
889                with open(info['filepath'], 'wt') as f:
890                    f.write('MODIFIED')
891                return [], info
892
893        run_pp({'keepvideo': False}, ModifierPP)
894        self.assertTrue(os.path.exists(filename), '%s doesn\'t exist' % filename)
895        os.unlink(filename)
896
897    def test_match_filter(self):
898        class FilterYDL(YDL):
899            def __init__(self, *args, **kwargs):
900                super(FilterYDL, self).__init__(*args, **kwargs)
901                self.params['simulate'] = True
902
903            def process_info(self, info_dict):
904                super(YDL, self).process_info(info_dict)
905
906            def _match_entry(self, info_dict, incomplete=False):
907                res = super(FilterYDL, self)._match_entry(info_dict, incomplete)
908                if res is None:
909                    self.downloaded_info_dicts.append(info_dict)
910                return res
911
912        first = {
913            'id': '1',
914            'url': TEST_URL,
915            'title': 'one',
916            'extractor': 'TEST',
917            'duration': 30,
918            'filesize': 10 * 1024,
919            'playlist_id': '42',
920            'uploader': "變態妍字幕版 太妍 тест",
921            'creator': "тест ' 123 ' тест--",
922            'webpage_url': 'http://example.com/watch?v=shenanigans',
923        }
924        second = {
925            'id': '2',
926            'url': TEST_URL,
927            'title': 'two',
928            'extractor': 'TEST',
929            'duration': 10,
930            'description': 'foo',
931            'filesize': 5 * 1024,
932            'playlist_id': '43',
933            'uploader': "тест 123",
934            'webpage_url': 'http://example.com/watch?v=SHENANIGANS',
935        }
936        videos = [first, second]
937
938        def get_videos(filter_=None):
939            ydl = FilterYDL({'match_filter': filter_})
940            for v in videos:
941                ydl.process_ie_result(v, download=True)
942            return [v['id'] for v in ydl.downloaded_info_dicts]
943
944        res = get_videos()
945        self.assertEqual(res, ['1', '2'])
946
947        def f(v):
948            if v['id'] == '1':
949                return None
950            else:
951                return 'Video id is not 1'
952        res = get_videos(f)
953        self.assertEqual(res, ['1'])
954
955        f = match_filter_func('duration < 30')
956        res = get_videos(f)
957        self.assertEqual(res, ['2'])
958
959        f = match_filter_func('description = foo')
960        res = get_videos(f)
961        self.assertEqual(res, ['2'])
962
963        f = match_filter_func('description =? foo')
964        res = get_videos(f)
965        self.assertEqual(res, ['1', '2'])
966
967        f = match_filter_func('filesize > 5KiB')
968        res = get_videos(f)
969        self.assertEqual(res, ['1'])
970
971        f = match_filter_func('playlist_id = 42')
972        res = get_videos(f)
973        self.assertEqual(res, ['1'])
974
975        f = match_filter_func('uploader = "變態妍字幕版 太妍 тест"')
976        res = get_videos(f)
977        self.assertEqual(res, ['1'])
978
979        f = match_filter_func('uploader != "變態妍字幕版 太妍 тест"')
980        res = get_videos(f)
981        self.assertEqual(res, ['2'])
982
983        f = match_filter_func('creator = "тест \' 123 \' тест--"')
984        res = get_videos(f)
985        self.assertEqual(res, ['1'])
986
987        f = match_filter_func("creator = 'тест \\' 123 \\' тест--'")
988        res = get_videos(f)
989        self.assertEqual(res, ['1'])
990
991        f = match_filter_func(r"creator = 'тест \' 123 \' тест--' & duration > 30")
992        res = get_videos(f)
993        self.assertEqual(res, [])
994
995    def test_playlist_items_selection(self):
996        entries = [{
997            'id': compat_str(i),
998            'title': compat_str(i),
999            'url': TEST_URL,
1000        } for i in range(1, 5)]
1001        playlist = {
1002            '_type': 'playlist',
1003            'id': 'test',
1004            'entries': entries,
1005            'extractor': 'test:playlist',
1006            'extractor_key': 'test:playlist',
1007            'webpage_url': 'http://example.com',
1008        }
1009
1010        def get_downloaded_info_dicts(params):
1011            ydl = YDL(params)
1012            # make a deep copy because the dictionary and nested entries
1013            # can be modified
1014            ydl.process_ie_result(copy.deepcopy(playlist))
1015            return ydl.downloaded_info_dicts
1016
1017        def test_selection(params, expected_ids):
1018            results = [
1019                (v['playlist_autonumber'] - 1, (int(v['id']), v['playlist_index']))
1020                for v in get_downloaded_info_dicts(params)]
1021            self.assertEqual(results, list(enumerate(zip(expected_ids, expected_ids))))
1022
1023        test_selection({}, [1, 2, 3, 4])
1024        test_selection({'playlistend': 10}, [1, 2, 3, 4])
1025        test_selection({'playlistend': 2}, [1, 2])
1026        test_selection({'playliststart': 10}, [])
1027        test_selection({'playliststart': 2}, [2, 3, 4])
1028        test_selection({'playlist_items': '2-4'}, [2, 3, 4])
1029        test_selection({'playlist_items': '2,4'}, [2, 4])
1030        test_selection({'playlist_items': '10'}, [])
1031        test_selection({'playlist_items': '0'}, [])
1032
1033        # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
1034        test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
1035        test_selection({'playlist_items': '4,2'}, [4, 2])
1036
1037        # Tests for https://github.com/yt-dlp/yt-dlp/issues/720
1038        # https://github.com/yt-dlp/yt-dlp/issues/302
1039        test_selection({'playlistreverse': True}, [4, 3, 2, 1])
1040        test_selection({'playliststart': 2, 'playlistreverse': True}, [4, 3, 2])
1041        test_selection({'playlist_items': '2,4', 'playlistreverse': True}, [4, 2])
1042        test_selection({'playlist_items': '4,2'}, [4, 2])
1043
1044    def test_urlopen_no_file_protocol(self):
1045        # see https://github.com/ytdl-org/youtube-dl/issues/8227
1046        ydl = YDL()
1047        self.assertRaises(compat_urllib_error.URLError, ydl.urlopen, 'file:///etc/passwd')
1048
1049    def test_do_not_override_ie_key_in_url_transparent(self):
1050        ydl = YDL()
1051
1052        class Foo1IE(InfoExtractor):
1053            _VALID_URL = r'foo1:'
1054
1055            def _real_extract(self, url):
1056                return {
1057                    '_type': 'url_transparent',
1058                    'url': 'foo2:',
1059                    'ie_key': 'Foo2',
1060                    'title': 'foo1 title',
1061                    'id': 'foo1_id',
1062                }
1063
1064        class Foo2IE(InfoExtractor):
1065            _VALID_URL = r'foo2:'
1066
1067            def _real_extract(self, url):
1068                return {
1069                    '_type': 'url',
1070                    'url': 'foo3:',
1071                    'ie_key': 'Foo3',
1072                }
1073
1074        class Foo3IE(InfoExtractor):
1075            _VALID_URL = r'foo3:'
1076
1077            def _real_extract(self, url):
1078                return _make_result([{'url': TEST_URL}], title='foo3 title')
1079
1080        ydl.add_info_extractor(Foo1IE(ydl))
1081        ydl.add_info_extractor(Foo2IE(ydl))
1082        ydl.add_info_extractor(Foo3IE(ydl))
1083        ydl.extract_info('foo1:')
1084        downloaded = ydl.downloaded_info_dicts[0]
1085        self.assertEqual(downloaded['url'], TEST_URL)
1086        self.assertEqual(downloaded['title'], 'foo1 title')
1087        self.assertEqual(downloaded['id'], 'testid')
1088        self.assertEqual(downloaded['extractor'], 'testex')
1089        self.assertEqual(downloaded['extractor_key'], 'TestEx')
1090
1091    # Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
1092    def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self):
1093
1094        class _YDL(YDL):
1095            def __init__(self, *args, **kwargs):
1096                super(_YDL, self).__init__(*args, **kwargs)
1097
1098            def trouble(self, s, tb=None):
1099                pass
1100
1101        ydl = _YDL({
1102            'format': 'extra',
1103            'ignoreerrors': True,
1104        })
1105
1106        class VideoIE(InfoExtractor):
1107            _VALID_URL = r'video:(?P<id>\d+)'
1108
1109            def _real_extract(self, url):
1110                video_id = self._match_id(url)
1111                formats = [{
1112                    'format_id': 'default',
1113                    'url': 'url:',
1114                }]
1115                if video_id == '0':
1116                    raise ExtractorError('foo')
1117                if video_id == '2':
1118                    formats.append({
1119                        'format_id': 'extra',
1120                        'url': TEST_URL,
1121                    })
1122                return {
1123                    'id': video_id,
1124                    'title': 'Video %s' % video_id,
1125                    'formats': formats,
1126                }
1127
1128        class PlaylistIE(InfoExtractor):
1129            _VALID_URL = r'playlist:'
1130
1131            def _entries(self):
1132                for n in range(3):
1133                    video_id = compat_str(n)
1134                    yield {
1135                        '_type': 'url_transparent',
1136                        'ie_key': VideoIE.ie_key(),
1137                        'id': video_id,
1138                        'url': 'video:%s' % video_id,
1139                        'title': 'Video Transparent %s' % video_id,
1140                    }
1141
1142            def _real_extract(self, url):
1143                return self.playlist_result(self._entries())
1144
1145        ydl.add_info_extractor(VideoIE(ydl))
1146        ydl.add_info_extractor(PlaylistIE(ydl))
1147        info = ydl.extract_info('playlist:')
1148        entries = info['entries']
1149        self.assertEqual(len(entries), 3)
1150        self.assertTrue(entries[0] is None)
1151        self.assertTrue(entries[1] is None)
1152        self.assertEqual(len(ydl.downloaded_info_dicts), 1)
1153        downloaded = ydl.downloaded_info_dicts[0]
1154        self.assertEqual(entries[2], downloaded)
1155        self.assertEqual(downloaded['url'], TEST_URL)
1156        self.assertEqual(downloaded['title'], 'Video Transparent 2')
1157        self.assertEqual(downloaded['id'], '2')
1158        self.assertEqual(downloaded['extractor'], 'Video')
1159        self.assertEqual(downloaded['extractor_key'], 'Video')
1160
1161
1162if __name__ == '__main__':
1163    unittest.main()
1164