3 # Allow direct execution
8 sys
.path
.insert(0, os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
))))
15 from test
.helper
import FakeYDL
, assertRegexpMatches
, try_rm
16 from yt_dlp
import YoutubeDL
17 from yt_dlp
.compat
import compat_os_name
18 from yt_dlp
.extractor
import YoutubeIE
19 from yt_dlp
.extractor
.common
import InfoExtractor
20 from yt_dlp
.postprocessor
.common
import PostProcessor
21 from yt_dlp
.utils
import (
28 from yt_dlp
.utils
.traversal
import traverse_obj
30 TEST_URL
= 'http://localhost/sample.mp4'
34 def __init__(self
, *args
, **kwargs
):
35 super().__init
__(*args
, **kwargs
)
36 self
.downloaded_info_dicts
= []
39 def process_info(self
, info_dict
):
40 self
.downloaded_info_dicts
.append(info_dict
.copy())
42 def to_screen(self
, msg
, *args
, **kwargs
):
45 def dl(self
, *args
, **kwargs
):
46 assert False, 'Downloader must not be invoked for test_YoutubeDL'
49 def _make_result(formats
, **kwargs
):
53 'title': 'testttitle',
54 'extractor': 'testex',
55 'extractor_key': 'TestEx',
56 'webpage_url': 'http://example.com/watch?v=shenanigans',
62 class TestFormatSelection(unittest
.TestCase
):
63 def test_prefer_free_formats(self
):
64 # Same resolution => download webm
66 ydl
.params
['prefer_free_formats'] = True
68 {'ext': 'webm', 'height': 460, 'url': TEST_URL}
,
69 {'ext': 'mp4', 'height': 460, 'url': TEST_URL}
,
71 info_dict
= _make_result(formats
)
72 ydl
.sort_formats(info_dict
)
73 ydl
.process_ie_result(info_dict
)
74 downloaded
= ydl
.downloaded_info_dicts
[0]
75 self
.assertEqual(downloaded
['ext'], 'webm')
77 # Different resolution => download best quality (mp4)
79 ydl
.params
['prefer_free_formats'] = True
81 {'ext': 'webm', 'height': 720, 'url': TEST_URL}
,
82 {'ext': 'mp4', 'height': 1080, 'url': TEST_URL}
,
84 info_dict
['formats'] = formats
85 ydl
.sort_formats(info_dict
)
86 ydl
.process_ie_result(info_dict
)
87 downloaded
= ydl
.downloaded_info_dicts
[0]
88 self
.assertEqual(downloaded
['ext'], 'mp4')
90 # No prefer_free_formats => prefer mp4 and webm
92 ydl
.params
['prefer_free_formats'] = False
94 {'ext': 'webm', 'height': 720, 'url': TEST_URL}
,
95 {'ext': 'mp4', 'height': 720, 'url': TEST_URL}
,
96 {'ext': 'flv', 'height': 720, 'url': TEST_URL}
,
98 info_dict
['formats'] = formats
99 ydl
.sort_formats(info_dict
)
100 ydl
.process_ie_result(info_dict
)
101 downloaded
= ydl
.downloaded_info_dicts
[0]
102 self
.assertEqual(downloaded
['ext'], 'mp4')
105 ydl
.params
['prefer_free_formats'] = False
107 {'ext': 'flv', 'height': 720, 'url': TEST_URL}
,
108 {'ext': 'webm', 'height': 720, 'url': TEST_URL}
,
110 info_dict
['formats'] = formats
111 ydl
.sort_formats(info_dict
)
112 ydl
.process_ie_result(info_dict
)
113 downloaded
= ydl
.downloaded_info_dicts
[0]
114 self
.assertEqual(downloaded
['ext'], 'webm')
116 def test_format_selection(self
):
118 {'format_id': '35', 'ext': 'mp4', 'preference': 0, 'url': TEST_URL}
,
119 {'format_id': 'example-with-dashes', 'ext': 'webm', 'preference': 1, 'url': TEST_URL}
,
120 {'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': TEST_URL}
,
121 {'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': TEST_URL}
,
122 {'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': TEST_URL}
,
124 info_dict
= _make_result(formats
)
126 def test(inp
, *expected
, multi
=False):
129 'allow_multiple_video_streams': multi
,
130 'allow_multiple_audio_streams': multi
,
132 ydl
.process_ie_result(info_dict
.copy())
133 downloaded
= [x
['format_id'] for x
in ydl
.downloaded_info_dicts
]
134 self
.assertEqual(downloaded
, list(expected
))
137 test('20/71/worst', '35')
139 test('webm/mp4', '47')
140 test('3gp/40/mp4', '35')
141 test('example-with-dashes', 'example-with-dashes')
142 test('all', '2', '47', '45', 'example-with-dashes', '35')
143 test('mergeall', '2+47+45+example-with-dashes+35', multi
=True)
144 # See: https://github.com/yt-dlp/yt-dlp/pulls/8797
145 test('7_a/worst', '35')
147 def test_format_selection_audio(self
):
149 {'format_id': 'audio-low', 'ext': 'webm', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL}
,
150 {'format_id': 'audio-mid', 'ext': 'webm', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL}
,
151 {'format_id': 'audio-high', 'ext': 'flv', 'preference': 3, 'vcodec': 'none', 'url': TEST_URL}
,
152 {'format_id': 'vid', 'ext': 'mp4', 'preference': 4, 'url': TEST_URL}
,
154 info_dict
= _make_result(formats
)
156 ydl
= YDL({'format': 'bestaudio'}
)
157 ydl
.process_ie_result(info_dict
.copy())
158 downloaded
= ydl
.downloaded_info_dicts
[0]
159 self
.assertEqual(downloaded
['format_id'], 'audio-high')
161 ydl
= YDL({'format': 'worstaudio'}
)
162 ydl
.process_ie_result(info_dict
.copy())
163 downloaded
= ydl
.downloaded_info_dicts
[0]
164 self
.assertEqual(downloaded
['format_id'], 'audio-low')
167 {'format_id': 'vid-low', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL}
,
168 {'format_id': 'vid-high', 'ext': 'mp4', 'preference': 2, 'url': TEST_URL}
,
170 info_dict
= _make_result(formats
)
172 ydl
= YDL({'format': 'bestaudio/worstaudio/best'}
)
173 ydl
.process_ie_result(info_dict
.copy())
174 downloaded
= ydl
.downloaded_info_dicts
[0]
175 self
.assertEqual(downloaded
['format_id'], 'vid-high')
177 def test_format_selection_audio_exts(self
):
179 {'format_id': 'mp3-64', 'ext': 'mp3', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'}
,
180 {'format_id': 'ogg-64', 'ext': 'ogg', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'}
,
181 {'format_id': 'aac-64', 'ext': 'aac', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'}
,
182 {'format_id': 'mp3-32', 'ext': 'mp3', 'abr': 32, 'url': 'http://_', 'vcodec': 'none'}
,
183 {'format_id': 'aac-32', 'ext': 'aac', 'abr': 32, 'url': 'http://_', 'vcodec': 'none'}
,
186 info_dict
= _make_result(formats
)
187 ydl
= YDL({'format': 'best', 'format_sort': ['abr', 'ext']}
)
188 ydl
.sort_formats(info_dict
)
189 ydl
.process_ie_result(copy
.deepcopy(info_dict
))
190 downloaded
= ydl
.downloaded_info_dicts
[0]
191 self
.assertEqual(downloaded
['format_id'], 'aac-64')
193 ydl
= YDL({'format': 'mp3'}
)
194 ydl
.sort_formats(info_dict
)
195 ydl
.process_ie_result(copy
.deepcopy(info_dict
))
196 downloaded
= ydl
.downloaded_info_dicts
[0]
197 self
.assertEqual(downloaded
['format_id'], 'mp3-64')
199 ydl
= YDL({'prefer_free_formats': True, 'format_sort': ['abr', 'ext']}
)
200 ydl
.sort_formats(info_dict
)
201 ydl
.process_ie_result(copy
.deepcopy(info_dict
))
202 downloaded
= ydl
.downloaded_info_dicts
[0]
203 self
.assertEqual(downloaded
['format_id'], 'ogg-64')
205 def test_format_selection_video(self
):
207 {'format_id': 'dash-video-low', 'ext': 'mp4', 'preference': 1, 'acodec': 'none', 'url': TEST_URL}
,
208 {'format_id': 'dash-video-high', 'ext': 'mp4', 'preference': 2, 'acodec': 'none', 'url': TEST_URL}
,
209 {'format_id': 'vid', 'ext': 'mp4', 'preference': 3, 'url': TEST_URL}
,
211 info_dict
= _make_result(formats
)
213 ydl
= YDL({'format': 'bestvideo'}
)
214 ydl
.process_ie_result(info_dict
.copy())
215 downloaded
= ydl
.downloaded_info_dicts
[0]
216 self
.assertEqual(downloaded
['format_id'], 'dash-video-high')
218 ydl
= YDL({'format': 'worstvideo'}
)
219 ydl
.process_ie_result(info_dict
.copy())
220 downloaded
= ydl
.downloaded_info_dicts
[0]
221 self
.assertEqual(downloaded
['format_id'], 'dash-video-low')
223 ydl
= YDL({'format': 'bestvideo[format_id^=dash][format_id$=low]'}
)
224 ydl
.process_ie_result(info_dict
.copy())
225 downloaded
= ydl
.downloaded_info_dicts
[0]
226 self
.assertEqual(downloaded
['format_id'], 'dash-video-low')
229 {'format_id': 'vid-vcodec-dot', 'ext': 'mp4', 'preference': 1, 'vcodec': 'avc1.123456', 'acodec': 'none', 'url': TEST_URL}
,
231 info_dict
= _make_result(formats
)
233 ydl
= YDL({'format': 'bestvideo[vcodec=avc1.123456]'}
)
234 ydl
.process_ie_result(info_dict
.copy())
235 downloaded
= ydl
.downloaded_info_dicts
[0]
236 self
.assertEqual(downloaded
['format_id'], 'vid-vcodec-dot')
238 def test_format_selection_string_ops(self
):
240 {'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL}
,
241 {'format_id': 'zxc-cxz', 'ext': 'webm', 'url': TEST_URL}
,
243 info_dict
= _make_result(formats
)
246 ydl
= YDL({'format': '[format_id=abc-cba]'}
)
247 ydl
.process_ie_result(info_dict
.copy())
248 downloaded
= ydl
.downloaded_info_dicts
[0]
249 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
251 # does not equal (!=)
252 ydl
= YDL({'format': '[format_id!=abc-cba]'}
)
253 ydl
.process_ie_result(info_dict
.copy())
254 downloaded
= ydl
.downloaded_info_dicts
[0]
255 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
257 ydl
= YDL({'format': '[format_id!=abc-cba][format_id!=zxc-cxz]'}
)
258 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
261 ydl
= YDL({'format': '[format_id^=abc]'}
)
262 ydl
.process_ie_result(info_dict
.copy())
263 downloaded
= ydl
.downloaded_info_dicts
[0]
264 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
266 # does not start with (!^=)
267 ydl
= YDL({'format': '[format_id!^=abc]'}
)
268 ydl
.process_ie_result(info_dict
.copy())
269 downloaded
= ydl
.downloaded_info_dicts
[0]
270 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
272 ydl
= YDL({'format': '[format_id!^=abc][format_id!^=zxc]'}
)
273 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
276 ydl
= YDL({'format': '[format_id$=cba]'}
)
277 ydl
.process_ie_result(info_dict
.copy())
278 downloaded
= ydl
.downloaded_info_dicts
[0]
279 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
281 # does not end with (!$=)
282 ydl
= YDL({'format': '[format_id!$=cba]'}
)
283 ydl
.process_ie_result(info_dict
.copy())
284 downloaded
= ydl
.downloaded_info_dicts
[0]
285 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
287 ydl
= YDL({'format': '[format_id!$=cba][format_id!$=cxz]'}
)
288 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
291 ydl
= YDL({'format': '[format_id*=bc-cb]'}
)
292 ydl
.process_ie_result(info_dict
.copy())
293 downloaded
= ydl
.downloaded_info_dicts
[0]
294 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
296 # does not contain (!*=)
297 ydl
= YDL({'format': '[format_id!*=bc-cb]'}
)
298 ydl
.process_ie_result(info_dict
.copy())
299 downloaded
= ydl
.downloaded_info_dicts
[0]
300 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
302 ydl
= YDL({'format': '[format_id!*=abc][format_id!*=zxc]'}
)
303 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
305 ydl
= YDL({'format': '[format_id!*=-]'}
)
306 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
308 def test_youtube_format_selection(self
):
309 # FIXME: Rewrite in accordance with the new format sorting options
313 '38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13',
314 # Apple HTTP Live Streaming
315 '96', '95', '94', '93', '92', '132', '151',
317 '85', '84', '102', '83', '101', '82', '100',
319 '137', '248', '136', '247', '135', '246',
320 '245', '244', '134', '243', '133', '242', '160',
322 '141', '172', '140', '171', '139',
325 def format_info(f_id
):
326 info
= YoutubeIE
._formats
[f_id
].copy()
328 # XXX: In real cases InfoExtractor._parse_mpd_formats() fills up 'acodec'
329 # and 'vcodec', while in tests such information is incomplete since
330 # commit a6c2c24479e5f4827ceb06f64d855329c0a6f593
331 # test_YoutubeDL.test_youtube_format_selection is broken without
333 if 'acodec' in info
and 'vcodec' not in info
:
334 info
['vcodec'] = 'none'
335 elif 'vcodec' in info
and 'acodec' not in info
:
336 info
['acodec'] = 'none'
338 info
['format_id'] = f_id
339 info
['url'] = 'url:' + f_id
341 formats_order
= [format_info(f_id
) for f_id
in order
]
343 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
344 ydl
= YDL({'format': 'bestvideo+bestaudio'}
)
345 ydl
.sort_formats(info_dict
)
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')
351 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
352 ydl
= YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'}
)
353 ydl
.sort_formats(info_dict
)
354 ydl
.process_ie_result(info_dict
)
355 downloaded
= ydl
.downloaded_info_dicts
[0]
356 self
.assertEqual(downloaded
['format_id'], '38')
358 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
359 ydl
= YDL({'format': 'bestvideo/best,bestaudio'}
)
360 ydl
.sort_formats(info_dict
)
361 ydl
.process_ie_result(info_dict
)
362 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
363 self
.assertEqual(downloaded_ids
, ['137', '141'])
365 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
366 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'}
)
367 ydl
.sort_formats(info_dict
)
368 ydl
.process_ie_result(info_dict
)
369 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
370 self
.assertEqual(downloaded_ids
, ['137+141', '248+141'])
372 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
373 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'}
)
374 ydl
.sort_formats(info_dict
)
375 ydl
.process_ie_result(info_dict
)
376 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
377 self
.assertEqual(downloaded_ids
, ['136+141', '247+141'])
379 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
380 ydl
= YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'}
)
381 ydl
.sort_formats(info_dict
)
382 ydl
.process_ie_result(info_dict
)
383 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
384 self
.assertEqual(downloaded_ids
, ['248+141'])
386 for f1
, f2
in zip(formats_order
, formats_order
[1:]):
387 info_dict
= _make_result([f1
, f2
], extractor
='youtube')
388 ydl
= YDL({'format': 'best/bestvideo'}
)
389 ydl
.sort_formats(info_dict
)
390 ydl
.process_ie_result(info_dict
)
391 downloaded
= ydl
.downloaded_info_dicts
[0]
392 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
394 info_dict
= _make_result([f2
, f1
], extractor
='youtube')
395 ydl
= YDL({'format': 'best/bestvideo'}
)
396 ydl
.sort_formats(info_dict
)
397 ydl
.process_ie_result(info_dict
)
398 downloaded
= ydl
.downloaded_info_dicts
[0]
399 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
401 def test_audio_only_extractor_format_selection(self
):
402 # For extractors with incomplete formats (all formats are audio-only or
403 # video-only) best and worst should fallback to corresponding best/worst
404 # video-only or audio-only formats (as per
405 # https://github.com/ytdl-org/youtube-dl/pull/5556)
407 {'format_id': 'low', 'ext': 'mp3', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL}
,
408 {'format_id': 'high', 'ext': 'mp3', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL}
,
410 info_dict
= _make_result(formats
)
412 ydl
= YDL({'format': 'best'}
)
413 ydl
.process_ie_result(info_dict
.copy())
414 downloaded
= ydl
.downloaded_info_dicts
[0]
415 self
.assertEqual(downloaded
['format_id'], 'high')
417 ydl
= YDL({'format': 'worst'}
)
418 ydl
.process_ie_result(info_dict
.copy())
419 downloaded
= ydl
.downloaded_info_dicts
[0]
420 self
.assertEqual(downloaded
['format_id'], 'low')
422 def test_format_not_available(self
):
424 {'format_id': 'regular', 'ext': 'mp4', 'height': 360, 'url': TEST_URL}
,
425 {'format_id': 'video', 'ext': 'mp4', 'height': 720, 'acodec': 'none', 'url': TEST_URL}
,
427 info_dict
= _make_result(formats
)
429 # This must fail since complete video-audio format does not match filter
430 # and extractor does not provide incomplete only formats (i.e. only
431 # video-only or audio-only).
432 ydl
= YDL({'format': 'best[height>360]'}
)
433 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
435 def test_format_selection_issue_10083(self
):
436 # See https://github.com/ytdl-org/youtube-dl/issues/10083
438 {'format_id': 'regular', 'height': 360, 'url': TEST_URL}
,
439 {'format_id': 'video', 'height': 720, 'acodec': 'none', 'url': TEST_URL}
,
440 {'format_id': 'audio', 'vcodec': 'none', 'url': TEST_URL}
,
442 info_dict
= _make_result(formats
)
444 ydl
= YDL({'format': 'best[height>360]/bestvideo[height>360]+bestaudio'}
)
445 ydl
.process_ie_result(info_dict
.copy())
446 self
.assertEqual(ydl
.downloaded_info_dicts
[0]['format_id'], 'video+audio')
448 def test_invalid_format_specs(self
):
449 def assert_syntax_error(format_spec
):
450 self
.assertRaises(SyntaxError, YDL
, {'format': format_spec}
)
452 assert_syntax_error('bestvideo,,best')
453 assert_syntax_error('+bestaudio')
454 assert_syntax_error('bestvideo+')
455 assert_syntax_error('/')
456 assert_syntax_error('[720<height]')
458 def test_format_filtering(self
):
460 {'format_id': 'A', 'filesize': 500, 'width': 1000}
,
461 {'format_id': 'B', 'filesize': 1000, 'width': 500}
,
462 {'format_id': 'C', 'filesize': 1000, 'width': 400}
,
463 {'format_id': 'D', 'filesize': 2000, 'width': 600}
,
464 {'format_id': 'E', 'filesize': 3000}
,
466 {'format_id': 'G', 'filesize': 1000000}
,
469 f
['url'] = 'http://_/'
471 info_dict
= _make_result(formats
, _format_sort_fields
=('id', ))
473 ydl
= YDL({'format': 'best[filesize<3000]'}
)
474 ydl
.process_ie_result(info_dict
)
475 downloaded
= ydl
.downloaded_info_dicts
[0]
476 self
.assertEqual(downloaded
['format_id'], 'D')
478 ydl
= YDL({'format': 'best[filesize<=3000]'}
)
479 ydl
.process_ie_result(info_dict
)
480 downloaded
= ydl
.downloaded_info_dicts
[0]
481 self
.assertEqual(downloaded
['format_id'], 'E')
483 ydl
= YDL({'format': 'best[filesize <= ? 3000]'}
)
484 ydl
.process_ie_result(info_dict
)
485 downloaded
= ydl
.downloaded_info_dicts
[0]
486 self
.assertEqual(downloaded
['format_id'], 'F')
488 ydl
= YDL({'format': 'best [filesize = 1000] [width>450]'}
)
489 ydl
.process_ie_result(info_dict
)
490 downloaded
= ydl
.downloaded_info_dicts
[0]
491 self
.assertEqual(downloaded
['format_id'], 'B')
493 ydl
= YDL({'format': 'best [filesize = 1000] [width!=450]'}
)
494 ydl
.process_ie_result(info_dict
)
495 downloaded
= ydl
.downloaded_info_dicts
[0]
496 self
.assertEqual(downloaded
['format_id'], 'C')
498 ydl
= YDL({'format': '[filesize>?1]'}
)
499 ydl
.process_ie_result(info_dict
)
500 downloaded
= ydl
.downloaded_info_dicts
[0]
501 self
.assertEqual(downloaded
['format_id'], 'G')
503 ydl
= YDL({'format': '[filesize<1M]'}
)
504 ydl
.process_ie_result(info_dict
)
505 downloaded
= ydl
.downloaded_info_dicts
[0]
506 self
.assertEqual(downloaded
['format_id'], 'E')
508 ydl
= YDL({'format': '[filesize<1MiB]'}
)
509 ydl
.process_ie_result(info_dict
)
510 downloaded
= ydl
.downloaded_info_dicts
[0]
511 self
.assertEqual(downloaded
['format_id'], 'G')
513 ydl
= YDL({'format': 'all[width>=400][width<=600]'}
)
514 ydl
.process_ie_result(info_dict
)
515 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
516 self
.assertEqual(downloaded_ids
, ['D', 'C', 'B'])
518 ydl
= YDL({'format': 'best[height<40]'}
)
519 with contextlib
.suppress(ExtractorError
):
520 ydl
.process_ie_result(info_dict
)
521 self
.assertEqual(ydl
.downloaded_info_dicts
, [])
523 def test_default_format_spec(self
):
524 ydl
= YDL({'simulate': True}
)
525 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
528 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}
), 'best/bestvideo+bestaudio')
530 ydl
= YDL({'simulate': True}
)
531 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}
), 'bestvideo*+bestaudio/best')
533 ydl
= YDL({'outtmpl': '-'}
)
534 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
537 self
.assertEqual(ydl
._default
_format
_spec
({}, download
=False), 'bestvideo*+bestaudio/best')
538 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}
), 'best/bestvideo+bestaudio')
541 class TestYoutubeDL(unittest
.TestCase
):
542 def test_subtitles(self
):
543 def s_formats(lang
, autocaption
=False):
546 'url': f
'http://localhost/video.{lang}.{ext}',
547 '_auto': autocaption
,
548 } for ext
in ['vtt', 'srt', 'ass']]
549 subtitles
= {l: s_formats(l) for l in ['en', 'fr', 'es']}
550 auto_captions
= {l: s_formats(l, True) for l in ['it', 'pt', 'es']}
554 'url': 'http://localhost/video.mp4',
555 'subtitles': subtitles
,
556 'automatic_captions': auto_captions
,
558 'webpage_url': 'http://example.com/watch?v=shenanigans',
561 def get_info(params
={}):
562 params
.setdefault('simulate', True)
564 ydl
.report_warning
= lambda *args
, **kargs
: None
565 return ydl
.process_video_result(info_dict
, download
=False)
568 self
.assertFalse(result
.get('requested_subtitles'))
569 self
.assertEqual(result
['subtitles'], subtitles
)
570 self
.assertEqual(result
['automatic_captions'], auto_captions
)
572 result
= get_info({'writesubtitles': True}
)
573 subs
= result
['requested_subtitles']
574 self
.assertTrue(subs
)
575 self
.assertEqual(set(subs
.keys()), {'en'}
)
576 self
.assertTrue(subs
['en'].get('data') is None)
577 self
.assertEqual(subs
['en']['ext'], 'ass')
579 result
= get_info({'writesubtitles': True, 'subtitlesformat': 'foo/srt'}
)
580 subs
= result
['requested_subtitles']
581 self
.assertEqual(subs
['en']['ext'], 'srt')
583 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['es', 'fr', 'it']}
)
584 subs
= result
['requested_subtitles']
585 self
.assertTrue(subs
)
586 self
.assertEqual(set(subs
.keys()), {'es', 'fr'}
)
588 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['all', '-en']}
)
589 subs
= result
['requested_subtitles']
590 self
.assertTrue(subs
)
591 self
.assertEqual(set(subs
.keys()), {'es', 'fr'}
)
593 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['en', 'fr', '-en']}
)
594 subs
= result
['requested_subtitles']
595 self
.assertTrue(subs
)
596 self
.assertEqual(set(subs
.keys()), {'fr'}
)
598 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['-en', 'en']}
)
599 subs
= result
['requested_subtitles']
600 self
.assertTrue(subs
)
601 self
.assertEqual(set(subs
.keys()), {'en'}
)
603 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['e.+']}
)
604 subs
= result
['requested_subtitles']
605 self
.assertTrue(subs
)
606 self
.assertEqual(set(subs
.keys()), {'es', 'en'}
)
608 result
= get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']}
)
609 subs
= result
['requested_subtitles']
610 self
.assertTrue(subs
)
611 self
.assertEqual(set(subs
.keys()), {'es', 'pt'}
)
612 self
.assertFalse(subs
['es']['_auto'])
613 self
.assertTrue(subs
['pt']['_auto'])
615 result
= get_info({'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']}
)
616 subs
= result
['requested_subtitles']
617 self
.assertTrue(subs
)
618 self
.assertEqual(set(subs
.keys()), {'es', 'pt'}
)
619 self
.assertTrue(subs
['es']['_auto'])
620 self
.assertTrue(subs
['pt']['_auto'])
622 def test_add_extra_info(self
):
628 'playlist': 'funny videos',
630 YDL
.add_extra_info(test_dict
, extra_info
)
631 self
.assertEqual(test_dict
['extractor'], 'Foo')
632 self
.assertEqual(test_dict
['playlist'], 'funny videos')
642 'title3': 'foo/bar\\test',
643 'title4': 'foo "bar" test',
645 'timestamp': 1618488000,
648 'playlist_autonumber': 2,
649 '__last_playlist_index': 100,
652 {'id': 'id 1', 'height': 1080, 'width': 1920}
,
653 {'id': 'id 2', 'height': 720}
,
658 def test_prepare_outtmpl_and_filename(self
):
659 def test(tmpl
, expected
, *, info
=None, **params
):
660 params
['outtmpl'] = tmpl
661 ydl
= FakeYDL(params
)
662 ydl
._num
_downloads
= 1
663 self
.assertEqual(ydl
.validate_outtmpl(tmpl
), None)
665 out
= ydl
.evaluate_outtmpl(tmpl
, info
or self
.outtmpl_info
)
666 fname
= ydl
.prepare_filename(info
or self
.outtmpl_info
)
668 if not isinstance(expected
, (list, tuple)):
669 expected
= (expected
, expected
)
670 for (name
, got
), expect
in zip((('outtmpl', out
), ('filename', fname
)), expected
):
672 self
.assertTrue(expect(got
), f
'Wrong {name} from {tmpl}')
673 elif expect
is not None:
674 self
.assertEqual(got
, expect
, f
'Wrong {name} from {tmpl}')
677 original_infodict
= dict(self
.outtmpl_info
)
678 test('foo.bar', 'foo.bar')
679 original_infodict
['epoch'] = self
.outtmpl_info
.get('epoch')
680 self
.assertTrue(isinstance(original_infodict
['epoch'], int))
681 test('%(epoch)d', int_or_none
)
682 self
.assertEqual(original_infodict
, self
.outtmpl_info
)
684 # Auto-generated fields
685 test('%(id)s.%(ext)s', '1234.mp4')
686 test('%(duration_string)s', ('27:46:40', '27-46-40'))
687 test('%(resolution)s', '1080p')
688 test('%(playlist_index|)s', '001')
689 test('%(playlist_index&{}!)s', '1!')
690 test('%(playlist_autonumber)s', '02')
691 test('%(autonumber)s', '00001')
692 test('%(autonumber+2)03d', '005', autonumber_start
=3)
693 test('%(autonumber)s', '001', autonumber_size
=3)
702 test('%abc%', '%abc%')
703 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
704 test('%%%(height)s', '%1080')
705 test('%(width)06d.%(ext)s', 'NA.mp4')
706 test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
707 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
710 test('%(id)s', '_abcd', info
={'id': '_abcd'}
)
711 test('%(some_id)s', '_abcd', info
={'some_id': '_abcd'}
)
712 test('%(formats.0.id)s', '_abcd', info
={'formats': [{'id': '_abcd'}
]})
713 test('%(id)s', '-abcd', info
={'id': '-abcd'}
)
714 test('%(id)s', '.abcd', info
={'id': '.abcd'}
)
715 test('%(id)s', 'ab__cd', info
={'id': 'ab__cd'}
)
716 test('%(id)s', ('ab:cd', 'ab:cd'), info
={'id': 'ab:cd'}
)
717 test('%(id.0)s', '-', info
={'id': '--'}
)
720 self
.assertTrue(isinstance(YoutubeDL
.validate_outtmpl('%(title)'), ValueError))
721 test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder
='none')
723 test('%(formats.{id)s', 'NA')
726 def expect_same_infodict(out
):
727 got_dict
= json
.loads(out
)
728 for info_field
, expected
in self
.outtmpl_info
.items():
729 self
.assertEqual(got_dict
.get(info_field
), expected
, info_field
)
732 test('%()j', (expect_same_infodict
, None))
735 NA_TEST_OUTTMPL
= '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
736 test(NA_TEST_OUTTMPL
, 'NA-NA-def-1234.mp4')
737 test(NA_TEST_OUTTMPL
, 'none-none-def-1234.mp4', outtmpl_na_placeholder
='none')
738 test(NA_TEST_OUTTMPL
, '--def-1234.mp4', outtmpl_na_placeholder
='')
739 test('%(non_existent.0)s', 'NA')
742 FMT_TEST_OUTTMPL
= '%%(height)%s.%%(ext)s'
743 test(FMT_TEST_OUTTMPL
% 's', '1080.mp4')
744 test(FMT_TEST_OUTTMPL
% 'd', '1080.mp4')
745 test(FMT_TEST_OUTTMPL
% '6d', ' 1080.mp4')
746 test(FMT_TEST_OUTTMPL
% '-6d', '1080 .mp4')
747 test(FMT_TEST_OUTTMPL
% '06d', '001080.mp4')
748 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
749 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
750 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
751 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
752 test(FMT_TEST_OUTTMPL
% ' 0 6d', ' 01080.mp4')
755 test('%(id)d', '1234')
756 test('%(height)c', '1')
758 test('%(id)d %(id)r', "1234 '1234'")
759 test('%(id)r %(height)r', "'1234' 1080")
760 test('%(title5)a %(height)a', (R
"'\xe1\xe9\xed \U0001d400' 1080", None))
761 test('%(ext)s-%(ext|def)d', 'mp4-def')
762 test('%(width|0)04d', '0')
763 test('a%(width|b)d', 'ab', outtmpl_na_placeholder
='none')
765 FORMATS
= self
.outtmpl_info
['formats']
767 # Custom type casting
768 test('%(formats.:.id)l', 'id 1, id 2, id 3')
769 test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
770 test('%(ext)l', 'mp4')
771 test('%(formats.:.id) 18l', ' id 1, id 2, id 3')
772 test('%(formats)j', (json
.dumps(FORMATS
), None))
773 test('%(formats)#j', (
774 json
.dumps(FORMATS
, indent
=4),
775 json
.dumps(FORMATS
, indent
=4).replace(':', ':').replace('"', '"').replace('\n', ' '),
777 test('%(title5).3B', 'á')
778 test('%(title5)U', 'áéí 𝐀')
779 test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
780 test('%(title5)+U', 'áéí A')
781 test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
782 test('%(height)D', '1k')
783 test('%(filesize)#D', '1Ki')
784 test('%(height)5.2D', ' 1.08k')
785 test('%(title4)#S', 'foo_bar_test')
786 test('%(title4).10S', ('foo "bar" ', 'foo "bar"' + ('#' if compat_os_name
== 'nt' else ' ')))
787 if compat_os_name
== 'nt':
788 test('%(title4)q', ('"foo ""bar"" test"', None))
789 test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', None))
790 test('%(formats.0.id)#q', ('"id 1"', None))
792 test('%(title4)q', ('\'foo "bar" test\'', '\'foo "bar" test\''))
793 test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
794 test('%(formats.0.id)#q', "'id 1'")
796 # Internal formatting
797 test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
798 test('%(title|%)s %(title|%%)s', '% %%')
799 test('%(id+1-height+3)05d', '00158')
800 test('%(width+100)05d', 'NA')
801 test('%(filesize*8)d', '8192')
802 test('%(formats.0) 15s', ('% 15s' % FORMATS
[0], None))
803 test('%(formats.0)r', (repr(FORMATS
[0]), None))
804 test('%(height.0)03d', '001')
805 test('%(-height.0)04d', '-001')
806 test('%(formats.-1.id)s', FORMATS
[-1]['id'])
807 test('%(formats.0.id.-1)d', FORMATS
[0]['id'][-1])
808 test('%(formats.3)s', 'NA')
809 test('%(formats.:2:-1)r', repr(FORMATS
[:2:-1]))
810 test('%(formats.0.id.-1+id)f', '1235.000000')
811 test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
812 out
= json
.dumps([{'id': f['id'], 'height.:2': str(f['height'])[:2]}
813 if 'height' in f
else {'id': f['id']}
815 test('%(formats.:.{id,height.:2})j', (out
, None))
816 test('%(formats.:.{id,height}.id)l', ', '.join(f
['id'] for f
in FORMATS
))
817 test('%(.{id,title})j', ('{"id": "1234"}', '{"id": "1234"}'))
820 test('%(title,id)s', '1234')
821 test('%(width-100,height+20|def)d', '1100')
822 test('%(width-100,height+width|def)s', 'def')
823 test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
826 test('%(id&foo)s.bar', 'foo.bar')
827 test('%(title&foo)s.bar', 'NA.bar')
828 test('%(title&foo|baz)s.bar', 'baz.bar')
829 test('%(x,id&foo|baz)s.bar', 'foo.bar')
830 test('%(x,title&foo|baz)s.bar', 'baz.bar')
831 test('%(id&a\nb|)s', ('a\nb', 'a b'))
832 test('%(id&hi {:>10} {}|)s', 'hi 1234 1234')
833 test(R
'%(id&{0} {}|)s', 'NA')
834 test(R
'%(id&{0.1}|)s', 'NA')
835 test('%(height&{:,d})S', '1,080')
840 raise self
.assertTrue(False, 'LazyList should not be evaluated till here')
841 test('%(key.4)s', '4', info
={'key': LazyList(gen())}
)
844 test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
845 # test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # FIXME: ?
846 # test('%(foo|)s', ('', '_')) # FIXME: ?
848 # Environment variable expansion for prepare_filename
849 os
.environ
['__yt_dlp_var'] = 'expanded'
850 envvar
= '%__yt_dlp_var%' if compat_os_name
== 'nt' else '$__yt_dlp_var'
851 test(envvar
, (envvar
, 'expanded'))
852 if compat_os_name
== 'nt':
853 test('%s%', ('%s%', '%s%'))
854 os
.environ
['s'] = 'expanded'
855 test('%s%', ('%s%', 'expanded')) # %s% should be expanded before escaping %s
856 os
.environ
['(test)s'] = 'expanded'
857 test('%(test)s%', ('NA%', 'expanded')) # Environment should take priority over template
859 # Path expansion and escaping
860 test('Hello %(title1)s', 'Hello $PATH')
861 test('Hello %(title2)s', 'Hello %PATH%')
862 test('%(title3)s', ('foo/bar\\test', 'foo⧸bar⧹test'))
863 test('folder/%(title3)s', ('folder/foo/bar\\test', f
'folder{os.path.sep}foo⧸bar⧹test'))
865 def test_format_note(self
):
867 self
.assertEqual(ydl
._format
_note
({}), '')
868 assertRegexpMatches(self
, ydl
._format
_note
({
871 assertRegexpMatches(self
, ydl
._format
_note
({
875 def test_postprocessors(self
):
876 filename
= 'post-processor-testfile.mp4'
877 audiofile
= filename
+ '.mp3'
879 class SimplePP(PostProcessor
):
881 with open(audiofile
, 'w') as f
:
883 return [info
['filepath']], info
885 def run_pp(params
, pp
):
886 with open(filename
, 'w') as f
:
888 ydl
= YoutubeDL(params
)
889 ydl
.add_post_processor(pp())
890 ydl
.post_process(filename
, {'filepath': filename}
)
892 run_pp({'keepvideo': True}
, SimplePP
)
893 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
894 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
898 run_pp({'keepvideo': False}
, SimplePP
)
899 self
.assertFalse(os
.path
.exists(filename
), f
'{filename} exists')
900 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
903 class ModifierPP(PostProcessor
):
905 with open(info
['filepath'], 'w') as f
:
909 run_pp({'keepvideo': False}
, ModifierPP
)
910 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
913 def test_match_filter(self
):
920 'filesize': 10 * 1024,
922 'uploader': '變態妍字幕版 太妍 тест',
923 'creator': "тест ' 123 ' тест--",
924 'webpage_url': 'http://example.com/watch?v=shenanigans',
932 'description': 'foo',
933 'filesize': 5 * 1024,
935 'uploader': 'тест 123',
936 'webpage_url': 'http://example.com/watch?v=SHENANIGANS',
938 videos
= [first
, second
]
940 def get_videos(filter_
=None):
941 ydl
= YDL({'match_filter': filter_, 'simulate': True}
)
943 ydl
.process_ie_result(v
.copy(), download
=True)
944 return [v
['id'] for v
in ydl
.downloaded_info_dicts
]
947 self
.assertEqual(res
, ['1', '2'])
949 def f(v
, incomplete
):
953 return 'Video id is not 1'
955 self
.assertEqual(res
, ['1'])
957 f
= match_filter_func('duration < 30')
959 self
.assertEqual(res
, ['2'])
961 f
= match_filter_func('description = foo')
963 self
.assertEqual(res
, ['2'])
965 f
= match_filter_func('description =? foo')
967 self
.assertEqual(res
, ['1', '2'])
969 f
= match_filter_func('filesize > 5KiB')
971 self
.assertEqual(res
, ['1'])
973 f
= match_filter_func('playlist_id = 42')
975 self
.assertEqual(res
, ['1'])
977 f
= match_filter_func('uploader = "變態妍字幕版 太妍 тест"')
979 self
.assertEqual(res
, ['1'])
981 f
= match_filter_func('uploader != "變態妍字幕版 太妍 тест"')
983 self
.assertEqual(res
, ['2'])
985 f
= match_filter_func('creator = "тест \' 123 \' тест--"')
987 self
.assertEqual(res
, ['1'])
989 f
= match_filter_func("creator = 'тест \\' 123 \\' тест--'")
991 self
.assertEqual(res
, ['1'])
993 f
= match_filter_func(r
"creator = 'тест \' 123 \' тест--' & duration > 30")
995 self
.assertEqual(res
, [])
997 def test_playlist_items_selection(self
):
998 INDICES
, PAGE_SIZE
= list(range(1, 11)), 3
1000 def entry(i
, evaluated
):
1008 def pagedlist_entries(evaluated
):
1010 start
= PAGE_SIZE
* n
1011 for i
in INDICES
[start
: start
+ PAGE_SIZE
]:
1012 yield entry(i
, evaluated
)
1013 return OnDemandPagedList(page_func
, PAGE_SIZE
)
1016 return (i
+ PAGE_SIZE
- 1) // PAGE_SIZE
1018 def generator_entries(evaluated
):
1020 yield entry(i
, evaluated
)
1022 def list_entries(evaluated
):
1023 return list(generator_entries(evaluated
))
1025 def lazylist_entries(evaluated
):
1026 return LazyList(generator_entries(evaluated
))
1028 def get_downloaded_info_dicts(params
, entries
):
1030 ydl
.process_ie_result({
1031 '_type': 'playlist',
1033 'extractor': 'test:playlist',
1034 'extractor_key': 'test:playlist',
1035 'webpage_url': 'http://example.com',
1038 return ydl
.downloaded_info_dicts
1040 def test_selection(params
, expected_ids
, evaluate_all
=False):
1041 expected_ids
= list(expected_ids
)
1043 generator_eval
= pagedlist_eval
= INDICES
1044 elif not expected_ids
:
1045 generator_eval
= pagedlist_eval
= []
1047 generator_eval
= INDICES
[0: max(expected_ids
)]
1048 pagedlist_eval
= INDICES
[PAGE_SIZE
* page_num(min(expected_ids
)) - PAGE_SIZE
:
1049 PAGE_SIZE
* page_num(max(expected_ids
))]
1051 for name
, func
, expected_eval
in (
1052 ('list', list_entries
, INDICES
),
1053 ('Generator', generator_entries
, generator_eval
),
1054 # ('LazyList', lazylist_entries, generator_eval), # Generator and LazyList follow the exact same code path
1055 ('PagedList', pagedlist_entries
, pagedlist_eval
),
1058 entries
= func(evaluated
)
1059 results
= [(v
['playlist_autonumber'] - 1, (int(v
['id']), v
['playlist_index']))
1060 for v
in get_downloaded_info_dicts(params
, entries
)]
1061 self
.assertEqual(results
, list(enumerate(zip(expected_ids
, expected_ids
))), f
'Entries of {name} for {params}')
1062 self
.assertEqual(sorted(evaluated
), expected_eval
, f
'Evaluation of {name} for {params}')
1064 test_selection({}, INDICES
)
1065 test_selection({'playlistend': 20}
, INDICES
, True)
1066 test_selection({'playlistend': 2}
, INDICES
[:2])
1067 test_selection({'playliststart': 11}
, [], True)
1068 test_selection({'playliststart': 2}
, INDICES
[1:])
1069 test_selection({'playlist_items': '2-4'}
, INDICES
[1:4])
1070 test_selection({'playlist_items': '2,4'}
, [2, 4])
1071 test_selection({'playlist_items': '20'}
, [], True)
1072 test_selection({'playlist_items': '0'}
, [])
1074 # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
1075 test_selection({'playlist_items': '2-4,3-4,3'}
, [2, 3, 4])
1076 test_selection({'playlist_items': '4,2'}
, [4, 2])
1078 # Tests for https://github.com/yt-dlp/yt-dlp/issues/720
1079 # https://github.com/yt-dlp/yt-dlp/issues/302
1080 test_selection({'playlistreverse': True}
, INDICES
[::-1])
1081 test_selection({'playliststart': 2, 'playlistreverse': True}
, INDICES
[:0:-1])
1082 test_selection({'playlist_items': '2,4', 'playlistreverse': True}
, [4, 2])
1083 test_selection({'playlist_items': '4,2'}
, [4, 2])
1085 # Tests for --playlist-items start:end:step
1086 test_selection({'playlist_items': ':'}
, INDICES
, True)
1087 test_selection({'playlist_items': '::1'}
, INDICES
, True)
1088 test_selection({'playlist_items': '::-1'}
, INDICES
[::-1], True)
1089 test_selection({'playlist_items': ':6'}
, INDICES
[:6])
1090 test_selection({'playlist_items': ':-6'}
, INDICES
[:-5], True)
1091 test_selection({'playlist_items': '-1:6:-2'}
, INDICES
[:4:-2], True)
1092 test_selection({'playlist_items': '9:-6:-2'}
, INDICES
[8:3:-2], True)
1094 test_selection({'playlist_items': '1:inf:2'}
, INDICES
[::2], True)
1095 test_selection({'playlist_items': '-2:inf'}
, INDICES
[-2:], True)
1096 test_selection({'playlist_items': ':inf:-1'}
, [], True)
1097 test_selection({'playlist_items': '0-2:2'}
, [2])
1098 test_selection({'playlist_items': '1-:2'}
, INDICES
[::2], True)
1099 test_selection({'playlist_items': '0--2:2'}
, INDICES
[1:-1:2], True)
1101 test_selection({'playlist_items': '10::3'}
, [10], True)
1102 test_selection({'playlist_items': '-1::3'}
, [10], True)
1103 test_selection({'playlist_items': '11::3'}
, [], True)
1104 test_selection({'playlist_items': '-15::2'}
, INDICES
[1::2], True)
1105 test_selection({'playlist_items': '-15::15'}
, [], True)
1107 def test_do_not_override_ie_key_in_url_transparent(self
):
1110 class Foo1IE(InfoExtractor
):
1111 _VALID_URL
= r
'foo1:'
1113 def _real_extract(self
, url
):
1115 '_type': 'url_transparent',
1118 'title': 'foo1 title',
1122 class Foo2IE(InfoExtractor
):
1123 _VALID_URL
= r
'foo2:'
1125 def _real_extract(self
, url
):
1132 class Foo3IE(InfoExtractor
):
1133 _VALID_URL
= r
'foo3:'
1135 def _real_extract(self
, url
):
1136 return _make_result([{'url': TEST_URL}
], title
='foo3 title')
1138 ydl
.add_info_extractor(Foo1IE(ydl
))
1139 ydl
.add_info_extractor(Foo2IE(ydl
))
1140 ydl
.add_info_extractor(Foo3IE(ydl
))
1141 ydl
.extract_info('foo1:')
1142 downloaded
= ydl
.downloaded_info_dicts
[0]
1143 self
.assertEqual(downloaded
['url'], TEST_URL
)
1144 self
.assertEqual(downloaded
['title'], 'foo1 title')
1145 self
.assertEqual(downloaded
['id'], 'testid')
1146 self
.assertEqual(downloaded
['extractor'], 'testex')
1147 self
.assertEqual(downloaded
['extractor_key'], 'TestEx')
1149 # Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
1150 def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self
):
1153 def __init__(self
, *args
, **kwargs
):
1154 super().__init
__(*args
, **kwargs
)
1156 def trouble(self
, s
, tb
=None):
1161 'ignoreerrors': True,
1164 class VideoIE(InfoExtractor
):
1165 _VALID_URL
= r
'video:(?P<id>\d+)'
1167 def _real_extract(self
, url
):
1168 video_id
= self
._match
_id
(url
)
1170 'format_id': 'default',
1174 raise ExtractorError('foo')
1177 'format_id': 'extra',
1182 'title': f
'Video {video_id}',
1186 class PlaylistIE(InfoExtractor
):
1187 _VALID_URL
= r
'playlist:'
1193 '_type': 'url_transparent',
1194 'ie_key': VideoIE
.ie_key(),
1196 'url': f
'video:{video_id}',
1197 'title': f
'Video Transparent {video_id}',
1200 def _real_extract(self
, url
):
1201 return self
.playlist_result(self
._entries
())
1203 ydl
.add_info_extractor(VideoIE(ydl
))
1204 ydl
.add_info_extractor(PlaylistIE(ydl
))
1205 info
= ydl
.extract_info('playlist:')
1206 entries
= info
['entries']
1207 self
.assertEqual(len(entries
), 3)
1208 self
.assertTrue(entries
[0] is None)
1209 self
.assertTrue(entries
[1] is None)
1210 self
.assertEqual(len(ydl
.downloaded_info_dicts
), 1)
1211 downloaded
= ydl
.downloaded_info_dicts
[0]
1212 entries
[2].pop('requested_downloads', None)
1213 self
.assertEqual(entries
[2], downloaded
)
1214 self
.assertEqual(downloaded
['url'], TEST_URL
)
1215 self
.assertEqual(downloaded
['title'], 'Video Transparent 2')
1216 self
.assertEqual(downloaded
['id'], '2')
1217 self
.assertEqual(downloaded
['extractor'], 'Video')
1218 self
.assertEqual(downloaded
['extractor_key'], 'Video')
1220 def test_header_cookies(self
):
1221 from http
.cookiejar
import Cookie
1224 ydl
.report_warning
= lambda *_
, **__
: None
1226 def cookie(name
, value
, version
=None, domain
='', path
='', secure
=False, expires
=None):
1228 version
or 0, name
, value
, None, False,
1229 domain
, bool(domain
), bool(domain
), path
, bool(path
),
1230 secure
, expires
, False, None, None, rest
={})
1232 _test_url
= 'https://yt.dlp/test'
1234 def test(encoded_cookies
, cookies
, *, headers
=False, round_trip
=None, error_re
=None):
1236 ydl
.cookiejar
.clear()
1237 ydl
._load
_cookies
(encoded_cookies
, autoscope
=headers
)
1239 ydl
._apply
_header
_cookies
(_test_url
)
1240 data
= {'url': _test_url}
1241 ydl
._calc
_headers
(data
)
1242 self
.assertCountEqual(
1243 map(vars, ydl
.cookiejar
), map(vars, cookies
),
1244 'Extracted cookiejar.Cookie is not the same')
1247 data
.get('cookies'), round_trip
or encoded_cookies
,
1248 'Cookie is not the same as round trip')
1249 ydl
.__dict
__['_YoutubeDL__header_cookies'] = []
1251 with self
.subTest(msg
=encoded_cookies
):
1255 with self
.assertRaisesRegex(Exception, error_re
):
1258 test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain
='.yt.dlp')])
1259 test('test=value', [cookie('test', 'value')], error_re
=r
'Unscoped cookies are not allowed')
1260 test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
1261 cookie('cookie1', 'value1', domain
='.yt.dlp', path
='/test'),
1262 cookie('cookie2', 'value2', domain
='.yt.dlp', path
='/')])
1263 test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
1264 cookie('test', 'value', domain
='.yt.dlp', path
='/test', secure
=True, expires
=9999999999)])
1265 test('test="value; "; path=/test; domain=.yt.dlp', [
1266 cookie('test', 'value; ', domain
='.yt.dlp', path
='/test')],
1267 round_trip
='test="value\\073 "; Domain=.yt.dlp; Path=/test')
1268 test('name=; Domain=.yt.dlp', [cookie('name', '', domain
='.yt.dlp')],
1269 round_trip
='name=""; Domain=.yt.dlp')
1271 test('test=value', [cookie('test', 'value', domain
='.yt.dlp')], headers
=True)
1272 test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers
=True, error_re
=r
'Invalid syntax')
1273 ydl
.deprecated_feature
= ydl
.report_error
1274 test('test=value', [], headers
=True, error_re
=r
'Passing cookies as a header is a potential security risk')
1276 def test_infojson_cookies(self
):
1277 TEST_FILE
= 'test_infojson_cookies.info.json'
1278 TEST_URL
= 'https://example.com/example.mp4'
1279 COOKIES
= 'a=b; Domain=.example.com; c=d; Domain=.example.com'
1280 COOKIE_HEADER
= {'Cookie': 'a=b; c=d'}
1283 ydl
.process_info
= lambda x
: ydl
._write
_info
_json
('test', x
, TEST_FILE
)
1285 def make_info(info_header_cookies
=False, fmts_header_cookies
=False, cookies_field
=False):
1286 fmt
= {'url': TEST_URL}
1287 if fmts_header_cookies
:
1288 fmt
['http_headers'] = COOKIE_HEADER
1290 fmt
['cookies'] = COOKIES
1291 return _make_result([fmt
], http_headers
=COOKIE_HEADER
if info_header_cookies
else None)
1293 def test(initial_info
, note
):
1295 result
['processed'] = ydl
.process_ie_result(initial_info
)
1296 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1297 msg
=f
'No cookies set in cookiejar after initial process when {note}')
1298 ydl
.cookiejar
.clear()
1299 with open(TEST_FILE
) as infojson
:
1300 result
['loaded'] = ydl
.sanitize_info(json
.load(infojson
), True)
1301 result
['final'] = ydl
.process_ie_result(result
['loaded'].copy(), download
=False)
1302 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1303 msg
=f
'No cookies set in cookiejar after final process when {note}')
1304 ydl
.cookiejar
.clear()
1305 for key
in ('processed', 'loaded', 'final'):
1308 traverse_obj(info
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False),
1309 msg
=f
'Cookie header not removed in {key} result when {note}')
1311 traverse_obj(info
, ((None, ('formats', 0)), 'cookies'), get_all
=False), COOKIES
,
1312 msg
=f
'No cookies field found in {key} result when {note}')
1314 test({'url': TEST_URL, 'http_headers': COOKIE_HEADER, 'id': '1', 'title': 'x'}
, 'no formats field')
1315 test(make_info(info_header_cookies
=True), 'info_dict header cokies')
1316 test(make_info(fmts_header_cookies
=True), 'format header cookies')
1317 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True), 'info_dict and format header cookies')
1318 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True, cookies_field
=True), 'all cookies fields')
1319 test(make_info(cookies_field
=True), 'cookies format field')
1320 test({'url': TEST_URL, 'cookies': COOKIES, 'id': '1', 'title': 'x'}
, 'info_dict cookies field only')
1324 def test_add_headers_cookie(self
):
1325 def check_for_cookie_header(result
):
1326 return traverse_obj(result
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False)
1328 ydl
= FakeYDL({'http_headers': {'Cookie': 'a=b'}
})
1329 ydl
._apply
_header
_cookies
(_make_result([])['webpage_url']) # Scope to input webpage URL: .example.com
1331 fmt
= {'url': 'https://example.com/video.mp4'}
1332 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1333 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies in result info_dict')
1334 self
.assertEqual(result
.get('cookies'), 'a=b; Domain=.example.com', msg
='No cookies were set in cookies field')
1335 self
.assertIn('a=b', ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='No cookies were set in cookiejar')
1337 fmt
= {'url': 'https://wrong.com/video.mp4'}
1338 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1339 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies for wrong domain')
1340 self
.assertFalse(result
.get('cookies'), msg
='Cookies set in cookies field for wrong domain')
1341 self
.assertFalse(ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='Cookies set in cookiejar for wrong domain')
1344 if __name__
== '__main__':