3 # Allow direct execution
7 from unittest
.mock
import patch
9 sys
.path
.insert(0, os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
))))
16 from test
.helper
import FakeYDL
, assertRegexpMatches
, try_rm
17 from yt_dlp
import YoutubeDL
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_by_vcodec_sort(self
):
240 {'format_id': 'av1-format', 'ext': 'mp4', 'vcodec': 'av1', 'acodec': 'none', 'url': TEST_URL
},
241 {'format_id': 'vp9-hdr-format', 'ext': 'mp4', 'vcodec': 'vp09.02.50.10.01.09.18.09.00', 'acodec': 'none', 'url': TEST_URL
},
242 {'format_id': 'vp9-sdr-format', 'ext': 'mp4', 'vcodec': 'vp09.00.50.08', 'acodec': 'none', 'url': TEST_URL
},
243 {'format_id': 'h265-format', 'ext': 'mp4', 'vcodec': 'h265', 'acodec': 'none', 'url': TEST_URL
},
245 info_dict
= _make_result(formats
)
247 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9.2']})
248 ydl
.process_ie_result(info_dict
.copy())
249 downloaded
= ydl
.downloaded_info_dicts
[0]
250 self
.assertEqual(downloaded
['format_id'], 'vp9-hdr-format')
252 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9']})
253 ydl
.process_ie_result(info_dict
.copy())
254 downloaded
= ydl
.downloaded_info_dicts
[0]
255 self
.assertEqual(downloaded
['format_id'], 'vp9-sdr-format')
257 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9.2']})
258 ydl
.process_ie_result(info_dict
.copy())
259 downloaded
= ydl
.downloaded_info_dicts
[0]
260 self
.assertEqual(downloaded
['format_id'], 'vp9-hdr-format')
262 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9']})
263 ydl
.process_ie_result(info_dict
.copy())
264 downloaded
= ydl
.downloaded_info_dicts
[0]
265 self
.assertEqual(downloaded
['format_id'], 'vp9-sdr-format')
267 def test_format_selection_string_ops(self
):
269 {'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL
},
270 {'format_id': 'zxc-cxz', 'ext': 'webm', 'url': TEST_URL
},
272 info_dict
= _make_result(formats
)
275 ydl
= YDL({'format': '[format_id=abc-cba]'})
276 ydl
.process_ie_result(info_dict
.copy())
277 downloaded
= ydl
.downloaded_info_dicts
[0]
278 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
280 # does not equal (!=)
281 ydl
= YDL({'format': '[format_id!=abc-cba]'})
282 ydl
.process_ie_result(info_dict
.copy())
283 downloaded
= ydl
.downloaded_info_dicts
[0]
284 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
286 ydl
= YDL({'format': '[format_id!=abc-cba][format_id!=zxc-cxz]'})
287 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
290 ydl
= YDL({'format': '[format_id^=abc]'})
291 ydl
.process_ie_result(info_dict
.copy())
292 downloaded
= ydl
.downloaded_info_dicts
[0]
293 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
295 # does not start with (!^=)
296 ydl
= YDL({'format': '[format_id!^=abc]'})
297 ydl
.process_ie_result(info_dict
.copy())
298 downloaded
= ydl
.downloaded_info_dicts
[0]
299 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
301 ydl
= YDL({'format': '[format_id!^=abc][format_id!^=zxc]'})
302 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
305 ydl
= YDL({'format': '[format_id$=cba]'})
306 ydl
.process_ie_result(info_dict
.copy())
307 downloaded
= ydl
.downloaded_info_dicts
[0]
308 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
310 # does not end with (!$=)
311 ydl
= YDL({'format': '[format_id!$=cba]'})
312 ydl
.process_ie_result(info_dict
.copy())
313 downloaded
= ydl
.downloaded_info_dicts
[0]
314 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
316 ydl
= YDL({'format': '[format_id!$=cba][format_id!$=cxz]'})
317 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
320 ydl
= YDL({'format': '[format_id*=bc-cb]'})
321 ydl
.process_ie_result(info_dict
.copy())
322 downloaded
= ydl
.downloaded_info_dicts
[0]
323 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
325 # does not contain (!*=)
326 ydl
= YDL({'format': '[format_id!*=bc-cb]'})
327 ydl
.process_ie_result(info_dict
.copy())
328 downloaded
= ydl
.downloaded_info_dicts
[0]
329 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
331 ydl
= YDL({'format': '[format_id!*=abc][format_id!*=zxc]'})
332 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
334 ydl
= YDL({'format': '[format_id!*=-]'})
335 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
337 def test_youtube_format_selection(self
):
338 # FIXME: Rewrite in accordance with the new format sorting options
342 '38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13',
343 # Apple HTTP Live Streaming
344 '96', '95', '94', '93', '92', '132', '151',
346 '85', '84', '102', '83', '101', '82', '100',
348 '137', '248', '136', '247', '135', '246',
349 '245', '244', '134', '243', '133', '242', '160',
351 '141', '172', '140', '171', '139',
354 def format_info(f_id
):
355 info
= YoutubeIE
._formats
[f_id
].copy()
357 # XXX: In real cases InfoExtractor._parse_mpd_formats() fills up 'acodec'
358 # and 'vcodec', while in tests such information is incomplete since
359 # commit a6c2c24479e5f4827ceb06f64d855329c0a6f593
360 # test_YoutubeDL.test_youtube_format_selection is broken without
362 if 'acodec' in info
and 'vcodec' not in info
:
363 info
['vcodec'] = 'none'
364 elif 'vcodec' in info
and 'acodec' not in info
:
365 info
['acodec'] = 'none'
367 info
['format_id'] = f_id
368 info
['url'] = 'url:' + f_id
370 formats_order
= [format_info(f_id
) for f_id
in order
]
372 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
373 ydl
= YDL({'format': 'bestvideo+bestaudio'})
374 ydl
.sort_formats(info_dict
)
375 ydl
.process_ie_result(info_dict
)
376 downloaded
= ydl
.downloaded_info_dicts
[0]
377 self
.assertEqual(downloaded
['format_id'], '248+172')
378 self
.assertEqual(downloaded
['ext'], 'mp4')
380 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
381 ydl
= YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'})
382 ydl
.sort_formats(info_dict
)
383 ydl
.process_ie_result(info_dict
)
384 downloaded
= ydl
.downloaded_info_dicts
[0]
385 self
.assertEqual(downloaded
['format_id'], '38')
387 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
388 ydl
= YDL({'format': 'bestvideo/best,bestaudio'})
389 ydl
.sort_formats(info_dict
)
390 ydl
.process_ie_result(info_dict
)
391 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
392 self
.assertEqual(downloaded_ids
, ['137', '141'])
394 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
395 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'})
396 ydl
.sort_formats(info_dict
)
397 ydl
.process_ie_result(info_dict
)
398 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
399 self
.assertEqual(downloaded_ids
, ['137+141', '248+141'])
401 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
402 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'})
403 ydl
.sort_formats(info_dict
)
404 ydl
.process_ie_result(info_dict
)
405 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
406 self
.assertEqual(downloaded_ids
, ['136+141', '247+141'])
408 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
409 ydl
= YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'})
410 ydl
.sort_formats(info_dict
)
411 ydl
.process_ie_result(info_dict
)
412 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
413 self
.assertEqual(downloaded_ids
, ['248+141'])
415 for f1
, f2
in zip(formats_order
, formats_order
[1:]):
416 info_dict
= _make_result([f1
, f2
], extractor
='youtube')
417 ydl
= YDL({'format': 'best/bestvideo'})
418 ydl
.sort_formats(info_dict
)
419 ydl
.process_ie_result(info_dict
)
420 downloaded
= ydl
.downloaded_info_dicts
[0]
421 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
423 info_dict
= _make_result([f2
, f1
], extractor
='youtube')
424 ydl
= YDL({'format': 'best/bestvideo'})
425 ydl
.sort_formats(info_dict
)
426 ydl
.process_ie_result(info_dict
)
427 downloaded
= ydl
.downloaded_info_dicts
[0]
428 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
430 def test_audio_only_extractor_format_selection(self
):
431 # For extractors with incomplete formats (all formats are audio-only or
432 # video-only) best and worst should fallback to corresponding best/worst
433 # video-only or audio-only formats (as per
434 # https://github.com/ytdl-org/youtube-dl/pull/5556)
436 {'format_id': 'low', 'ext': 'mp3', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL
},
437 {'format_id': 'high', 'ext': 'mp3', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL
},
439 info_dict
= _make_result(formats
)
441 ydl
= YDL({'format': 'best'})
442 ydl
.process_ie_result(info_dict
.copy())
443 downloaded
= ydl
.downloaded_info_dicts
[0]
444 self
.assertEqual(downloaded
['format_id'], 'high')
446 ydl
= YDL({'format': 'worst'})
447 ydl
.process_ie_result(info_dict
.copy())
448 downloaded
= ydl
.downloaded_info_dicts
[0]
449 self
.assertEqual(downloaded
['format_id'], 'low')
451 def test_format_not_available(self
):
453 {'format_id': 'regular', 'ext': 'mp4', 'height': 360, 'url': TEST_URL
},
454 {'format_id': 'video', 'ext': 'mp4', 'height': 720, 'acodec': 'none', 'url': TEST_URL
},
456 info_dict
= _make_result(formats
)
458 # This must fail since complete video-audio format does not match filter
459 # and extractor does not provide incomplete only formats (i.e. only
460 # video-only or audio-only).
461 ydl
= YDL({'format': 'best[height>360]'})
462 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
464 def test_format_selection_issue_10083(self
):
465 # See https://github.com/ytdl-org/youtube-dl/issues/10083
467 {'format_id': 'regular', 'height': 360, 'url': TEST_URL
},
468 {'format_id': 'video', 'height': 720, 'acodec': 'none', 'url': TEST_URL
},
469 {'format_id': 'audio', 'vcodec': 'none', 'url': TEST_URL
},
471 info_dict
= _make_result(formats
)
473 ydl
= YDL({'format': 'best[height>360]/bestvideo[height>360]+bestaudio'})
474 ydl
.process_ie_result(info_dict
.copy())
475 self
.assertEqual(ydl
.downloaded_info_dicts
[0]['format_id'], 'video+audio')
477 def test_invalid_format_specs(self
):
478 def assert_syntax_error(format_spec
):
479 self
.assertRaises(SyntaxError, YDL
, {'format': format_spec
})
481 assert_syntax_error('bestvideo,,best')
482 assert_syntax_error('+bestaudio')
483 assert_syntax_error('bestvideo+')
484 assert_syntax_error('/')
485 assert_syntax_error('[720<height]')
487 def test_format_filtering(self
):
489 {'format_id': 'A', 'filesize': 500, 'width': 1000},
490 {'format_id': 'B', 'filesize': 1000, 'width': 500},
491 {'format_id': 'C', 'filesize': 1000, 'width': 400},
492 {'format_id': 'D', 'filesize': 2000, 'width': 600},
493 {'format_id': 'E', 'filesize': 3000},
495 {'format_id': 'G', 'filesize': 1000000},
498 f
['url'] = 'http://_/'
500 info_dict
= _make_result(formats
, _format_sort_fields
=('id', ))
502 ydl
= YDL({'format': 'best[filesize<3000]'})
503 ydl
.process_ie_result(info_dict
)
504 downloaded
= ydl
.downloaded_info_dicts
[0]
505 self
.assertEqual(downloaded
['format_id'], 'D')
507 ydl
= YDL({'format': 'best[filesize<=3000]'})
508 ydl
.process_ie_result(info_dict
)
509 downloaded
= ydl
.downloaded_info_dicts
[0]
510 self
.assertEqual(downloaded
['format_id'], 'E')
512 ydl
= YDL({'format': 'best[filesize <= ? 3000]'})
513 ydl
.process_ie_result(info_dict
)
514 downloaded
= ydl
.downloaded_info_dicts
[0]
515 self
.assertEqual(downloaded
['format_id'], 'F')
517 ydl
= YDL({'format': 'best [filesize = 1000] [width>450]'})
518 ydl
.process_ie_result(info_dict
)
519 downloaded
= ydl
.downloaded_info_dicts
[0]
520 self
.assertEqual(downloaded
['format_id'], 'B')
522 ydl
= YDL({'format': 'best [filesize = 1000] [width!=450]'})
523 ydl
.process_ie_result(info_dict
)
524 downloaded
= ydl
.downloaded_info_dicts
[0]
525 self
.assertEqual(downloaded
['format_id'], 'C')
527 ydl
= YDL({'format': '[filesize>?1]'})
528 ydl
.process_ie_result(info_dict
)
529 downloaded
= ydl
.downloaded_info_dicts
[0]
530 self
.assertEqual(downloaded
['format_id'], 'G')
532 ydl
= YDL({'format': '[filesize<1M]'})
533 ydl
.process_ie_result(info_dict
)
534 downloaded
= ydl
.downloaded_info_dicts
[0]
535 self
.assertEqual(downloaded
['format_id'], 'E')
537 ydl
= YDL({'format': '[filesize<1MiB]'})
538 ydl
.process_ie_result(info_dict
)
539 downloaded
= ydl
.downloaded_info_dicts
[0]
540 self
.assertEqual(downloaded
['format_id'], 'G')
542 ydl
= YDL({'format': 'all[width>=400][width<=600]'})
543 ydl
.process_ie_result(info_dict
)
544 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
545 self
.assertEqual(downloaded_ids
, ['D', 'C', 'B'])
547 ydl
= YDL({'format': 'best[height<40]'})
548 with contextlib
.suppress(ExtractorError
):
549 ydl
.process_ie_result(info_dict
)
550 self
.assertEqual(ydl
.downloaded_info_dicts
, [])
552 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', False)
553 def test_default_format_spec_without_ffmpeg(self
):
555 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
557 ydl
= YDL({'simulate': True})
558 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
561 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
563 ydl
= YDL({'simulate': True})
564 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
566 ydl
= YDL({'outtmpl': '-'})
567 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
570 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
571 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
573 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', True)
574 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.can_merge', lambda _
: True)
575 def test_default_format_spec_with_ffmpeg(self
):
577 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
579 ydl
= YDL({'simulate': True})
580 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
583 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
585 ydl
= YDL({'simulate': True})
586 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
588 ydl
= YDL({'outtmpl': '-'})
589 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
592 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
593 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
596 class TestYoutubeDL(unittest
.TestCase
):
597 def test_subtitles(self
):
598 def s_formats(lang
, autocaption
=False):
601 'url': f
'http://localhost/video.{lang}.{ext}',
602 '_auto': autocaption
,
603 } for ext
in ['vtt', 'srt', 'ass']]
604 subtitles
= {l
: s_formats(l
) for l
in ['en', 'fr', 'es']}
605 auto_captions
= {l
: s_formats(l
, True) for l
in ['it', 'pt', 'es']}
609 'url': 'http://localhost/video.mp4',
610 'subtitles': subtitles
,
611 'automatic_captions': auto_captions
,
613 'webpage_url': 'http://example.com/watch?v=shenanigans',
616 def get_info(params
={}):
617 params
.setdefault('simulate', True)
619 ydl
.report_warning
= lambda *args
, **kargs
: None
620 return ydl
.process_video_result(info_dict
, download
=False)
623 self
.assertFalse(result
.get('requested_subtitles'))
624 self
.assertEqual(result
['subtitles'], subtitles
)
625 self
.assertEqual(result
['automatic_captions'], auto_captions
)
627 result
= get_info({'writesubtitles': True})
628 subs
= result
['requested_subtitles']
629 self
.assertTrue(subs
)
630 self
.assertEqual(set(subs
.keys()), {'en'})
631 self
.assertTrue(subs
['en'].get('data') is None)
632 self
.assertEqual(subs
['en']['ext'], 'ass')
634 result
= get_info({'writesubtitles': True, 'subtitlesformat': 'foo/srt'})
635 subs
= result
['requested_subtitles']
636 self
.assertEqual(subs
['en']['ext'], 'srt')
638 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['es', 'fr', 'it']})
639 subs
= result
['requested_subtitles']
640 self
.assertTrue(subs
)
641 self
.assertEqual(set(subs
.keys()), {'es', 'fr'})
643 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['all', '-en']})
644 subs
= result
['requested_subtitles']
645 self
.assertTrue(subs
)
646 self
.assertEqual(set(subs
.keys()), {'es', 'fr'})
648 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['en', 'fr', '-en']})
649 subs
= result
['requested_subtitles']
650 self
.assertTrue(subs
)
651 self
.assertEqual(set(subs
.keys()), {'fr'})
653 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['-en', 'en']})
654 subs
= result
['requested_subtitles']
655 self
.assertTrue(subs
)
656 self
.assertEqual(set(subs
.keys()), {'en'})
658 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['e.+']})
659 subs
= result
['requested_subtitles']
660 self
.assertTrue(subs
)
661 self
.assertEqual(set(subs
.keys()), {'es', 'en'})
663 result
= get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
664 subs
= result
['requested_subtitles']
665 self
.assertTrue(subs
)
666 self
.assertEqual(set(subs
.keys()), {'es', 'pt'})
667 self
.assertFalse(subs
['es']['_auto'])
668 self
.assertTrue(subs
['pt']['_auto'])
670 result
= get_info({'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
671 subs
= result
['requested_subtitles']
672 self
.assertTrue(subs
)
673 self
.assertEqual(set(subs
.keys()), {'es', 'pt'})
674 self
.assertTrue(subs
['es']['_auto'])
675 self
.assertTrue(subs
['pt']['_auto'])
677 def test_add_extra_info(self
):
683 'playlist': 'funny videos',
685 YDL
.add_extra_info(test_dict
, extra_info
)
686 self
.assertEqual(test_dict
['extractor'], 'Foo')
687 self
.assertEqual(test_dict
['playlist'], 'funny videos')
697 'title3': 'foo/bar\\test',
698 'title4': 'foo "bar" test',
700 'timestamp': 1618488000,
703 'playlist_autonumber': 2,
704 '__last_playlist_index': 100,
707 {'id': 'id 1', 'height': 1080, 'width': 1920},
708 {'id': 'id 2', 'height': 720},
713 def test_prepare_outtmpl_and_filename(self
):
714 def test(tmpl
, expected
, *, info
=None, **params
):
715 params
['outtmpl'] = tmpl
716 ydl
= FakeYDL(params
)
717 ydl
._num
_downloads
= 1
718 self
.assertEqual(ydl
.validate_outtmpl(tmpl
), None)
720 out
= ydl
.evaluate_outtmpl(tmpl
, info
or self
.outtmpl_info
)
721 fname
= ydl
.prepare_filename(info
or self
.outtmpl_info
)
723 if not isinstance(expected
, (list, tuple)):
724 expected
= (expected
, expected
)
725 for (name
, got
), expect
in zip((('outtmpl', out
), ('filename', fname
)), expected
):
727 self
.assertTrue(expect(got
), f
'Wrong {name} from {tmpl}')
728 elif expect
is not None:
729 self
.assertEqual(got
, expect
, f
'Wrong {name} from {tmpl}')
732 original_infodict
= dict(self
.outtmpl_info
)
733 test('foo.bar', 'foo.bar')
734 original_infodict
['epoch'] = self
.outtmpl_info
.get('epoch')
735 self
.assertTrue(isinstance(original_infodict
['epoch'], int))
736 test('%(epoch)d', int_or_none
)
737 self
.assertEqual(original_infodict
, self
.outtmpl_info
)
739 # Auto-generated fields
740 test('%(id)s.%(ext)s', '1234.mp4')
741 test('%(duration_string)s', ('27:46:40', '27-46-40'))
742 test('%(resolution)s', '1080p')
743 test('%(playlist_index|)s', '001')
744 test('%(playlist_index&{}!)s', '1!')
745 test('%(playlist_autonumber)s', '02')
746 test('%(autonumber)s', '00001')
747 test('%(autonumber+2)03d', '005', autonumber_start
=3)
748 test('%(autonumber)s', '001', autonumber_size
=3)
757 test('%abc%', '%abc%')
758 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
759 test('%%%(height)s', '%1080')
760 test('%(width)06d.%(ext)s', 'NA.mp4')
761 test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
762 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
765 test('%(id)s', '_abcd', info
={'id': '_abcd'})
766 test('%(some_id)s', '_abcd', info
={'some_id': '_abcd'})
767 test('%(formats.0.id)s', '_abcd', info
={'formats': [{'id': '_abcd'}]})
768 test('%(id)s', '-abcd', info
={'id': '-abcd'})
769 test('%(id)s', '.abcd', info
={'id': '.abcd'})
770 test('%(id)s', 'ab__cd', info
={'id': 'ab__cd'})
771 test('%(id)s', ('ab:cd', 'ab:cd'), info
={'id': 'ab:cd'})
772 test('%(id.0)s', '-', info
={'id': '--'})
775 self
.assertTrue(isinstance(YoutubeDL
.validate_outtmpl('%(title)'), ValueError))
776 test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder
='none')
778 test('%(formats.{id)s', 'NA')
781 def expect_same_infodict(out
):
782 got_dict
= json
.loads(out
)
783 for info_field
, expected
in self
.outtmpl_info
.items():
784 self
.assertEqual(got_dict
.get(info_field
), expected
, info_field
)
787 test('%()j', (expect_same_infodict
, None))
790 NA_TEST_OUTTMPL
= '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
791 test(NA_TEST_OUTTMPL
, 'NA-NA-def-1234.mp4')
792 test(NA_TEST_OUTTMPL
, 'none-none-def-1234.mp4', outtmpl_na_placeholder
='none')
793 test(NA_TEST_OUTTMPL
, '--def-1234.mp4', outtmpl_na_placeholder
='')
794 test('%(non_existent.0)s', 'NA')
797 FMT_TEST_OUTTMPL
= '%%(height)%s.%%(ext)s'
798 test(FMT_TEST_OUTTMPL
% 's', '1080.mp4')
799 test(FMT_TEST_OUTTMPL
% 'd', '1080.mp4')
800 test(FMT_TEST_OUTTMPL
% '6d', ' 1080.mp4')
801 test(FMT_TEST_OUTTMPL
% '-6d', '1080 .mp4')
802 test(FMT_TEST_OUTTMPL
% '06d', '001080.mp4')
803 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
804 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
805 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
806 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
807 test(FMT_TEST_OUTTMPL
% ' 0 6d', ' 01080.mp4')
810 test('%(id)d', '1234')
811 test('%(height)c', '1')
813 test('%(id)d %(id)r', "1234 '1234'")
814 test('%(id)r %(height)r', "'1234' 1080")
815 test('%(title5)a %(height)a', (R
"'\xe1\xe9\xed \U0001d400' 1080", None))
816 test('%(ext)s-%(ext|def)d', 'mp4-def')
817 test('%(width|0)04d', '0')
818 test('a%(width|b)d', 'ab', outtmpl_na_placeholder
='none')
820 FORMATS
= self
.outtmpl_info
['formats']
822 # Custom type casting
823 test('%(formats.:.id)l', 'id 1, id 2, id 3')
824 test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
825 test('%(ext)l', 'mp4')
826 test('%(formats.:.id) 18l', ' id 1, id 2, id 3')
827 test('%(formats)j', (json
.dumps(FORMATS
), None))
828 test('%(formats)#j', (
829 json
.dumps(FORMATS
, indent
=4),
830 json
.dumps(FORMATS
, indent
=4).replace(':', ':').replace('"', '"').replace('\n', ' '),
832 test('%(title5).3B', 'á')
833 test('%(title5)U', 'áéí 𝐀')
834 test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
835 test('%(title5)+U', 'áéí A')
836 test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
837 test('%(height)D', '1k')
838 test('%(filesize)#D', '1Ki')
839 test('%(height)5.2D', ' 1.08k')
840 test('%(title4)#S', 'foo_bar_test')
841 test('%(title4).10S', ('foo "bar" ', 'foo "bar"' + ('#' if os
.name
== 'nt' else ' ')))
843 test('%(title4)q', ('"foo ""bar"" test"', None))
844 test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', None))
845 test('%(formats.0.id)#q', ('"id 1"', None))
847 test('%(title4)q', ('\'foo "bar" test\'', '\'foo "bar" test\''))
848 test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
849 test('%(formats.0.id)#q', "'id 1'")
851 # Internal formatting
852 test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
853 test('%(title|%)s %(title|%%)s', '% %%')
854 test('%(id+1-height+3)05d', '00158')
855 test('%(width+100)05d', 'NA')
856 test('%(filesize*8)d', '8192')
857 test('%(formats.0) 15s', ('% 15s' % FORMATS
[0], None))
858 test('%(formats.0)r', (repr(FORMATS
[0]), None))
859 test('%(height.0)03d', '001')
860 test('%(-height.0)04d', '-001')
861 test('%(formats.-1.id)s', FORMATS
[-1]['id'])
862 test('%(formats.0.id.-1)d', FORMATS
[0]['id'][-1])
863 test('%(formats.3)s', 'NA')
864 test('%(formats.:2:-1)r', repr(FORMATS
[:2:-1]))
865 test('%(formats.0.id.-1+id)f', '1235.000000')
866 test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
867 out
= json
.dumps([{'id': f
['id'], 'height.:2': str(f
['height'])[:2]}
868 if 'height' in f
else {'id': f
['id']}
870 test('%(formats.:.{id,height.:2})j', (out
, None))
871 test('%(formats.:.{id,height}.id)l', ', '.join(f
['id'] for f
in FORMATS
))
872 test('%(.{id,title})j', ('{"id": "1234"}', '{"id": "1234"}'))
875 test('%(title,id)s', '1234')
876 test('%(width-100,height+20|def)d', '1100')
877 test('%(width-100,height+width|def)s', 'def')
878 test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
881 test('%(id&foo)s.bar', 'foo.bar')
882 test('%(title&foo)s.bar', 'NA.bar')
883 test('%(title&foo|baz)s.bar', 'baz.bar')
884 test('%(x,id&foo|baz)s.bar', 'foo.bar')
885 test('%(x,title&foo|baz)s.bar', 'baz.bar')
886 test('%(id&a\nb|)s', ('a\nb', 'a b'))
887 test('%(id&hi {:>10} {}|)s', 'hi 1234 1234')
888 test(R
'%(id&{0} {}|)s', 'NA')
889 test(R
'%(id&{0.1}|)s', 'NA')
890 test('%(height&{:,d})S', '1,080')
895 raise self
.assertTrue(False, 'LazyList should not be evaluated till here')
896 test('%(key.4)s', '4', info
={'key': LazyList(gen())})
899 test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
900 # test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # FIXME: ?
901 # test('%(foo|)s', ('', '_')) # FIXME: ?
903 # Environment variable expansion for prepare_filename
904 os
.environ
['__yt_dlp_var'] = 'expanded'
905 envvar
= '%__yt_dlp_var%' if os
.name
== 'nt' else '$__yt_dlp_var'
906 test(envvar
, (envvar
, 'expanded'))
908 test('%s%', ('%s%', '%s%'))
909 os
.environ
['s'] = 'expanded'
910 test('%s%', ('%s%', 'expanded')) # %s% should be expanded before escaping %s
911 os
.environ
['(test)s'] = 'expanded'
912 test('%(test)s%', ('NA%', 'expanded')) # Environment should take priority over template
914 # Path expansion and escaping
915 test('Hello %(title1)s', 'Hello $PATH')
916 test('Hello %(title2)s', 'Hello %PATH%')
917 test('%(title3)s', ('foo/bar\\test', 'foo⧸bar⧹test'))
918 test('folder/%(title3)s', ('folder/foo/bar\\test', f
'folder{os.path.sep}foo⧸bar⧹test'))
920 def test_format_note(self
):
922 self
.assertEqual(ydl
._format
_note
({}), '')
923 assertRegexpMatches(self
, ydl
._format
_note
({
926 assertRegexpMatches(self
, ydl
._format
_note
({
930 def test_postprocessors(self
):
931 filename
= 'post-processor-testfile.mp4'
932 audiofile
= filename
+ '.mp3'
934 class SimplePP(PostProcessor
):
936 with
open(audiofile
, 'w') as f
:
938 return [info
['filepath']], info
940 def run_pp(params
, pp
):
941 with
open(filename
, 'w') as f
:
943 ydl
= YoutubeDL(params
)
944 ydl
.add_post_processor(pp())
945 ydl
.post_process(filename
, {'filepath': filename
})
947 run_pp({'keepvideo': True}, SimplePP
)
948 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
949 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
953 run_pp({'keepvideo': False}, SimplePP
)
954 self
.assertFalse(os
.path
.exists(filename
), f
'{filename} exists')
955 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
958 class ModifierPP(PostProcessor
):
960 with
open(info
['filepath'], 'w') as f
:
964 run_pp({'keepvideo': False}, ModifierPP
)
965 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
968 def test_match_filter(self
):
975 'filesize': 10 * 1024,
977 'uploader': '變態妍字幕版 太妍 тест',
978 'creator': "тест ' 123 ' тест--",
979 'webpage_url': 'http://example.com/watch?v=shenanigans',
987 'description': 'foo',
988 'filesize': 5 * 1024,
990 'uploader': 'тест 123',
991 'webpage_url': 'http://example.com/watch?v=SHENANIGANS',
993 videos
= [first
, second
]
995 def get_videos(filter_
=None):
996 ydl
= YDL({'match_filter': filter_
, 'simulate': True})
998 ydl
.process_ie_result(v
.copy(), download
=True)
999 return [v
['id'] for v
in ydl
.downloaded_info_dicts
]
1002 self
.assertEqual(res
, ['1', '2'])
1004 def f(v
, incomplete
):
1008 return 'Video id is not 1'
1010 self
.assertEqual(res
, ['1'])
1012 f
= match_filter_func('duration < 30')
1014 self
.assertEqual(res
, ['2'])
1016 f
= match_filter_func('description = foo')
1018 self
.assertEqual(res
, ['2'])
1020 f
= match_filter_func('description =? foo')
1022 self
.assertEqual(res
, ['1', '2'])
1024 f
= match_filter_func('filesize > 5KiB')
1026 self
.assertEqual(res
, ['1'])
1028 f
= match_filter_func('playlist_id = 42')
1030 self
.assertEqual(res
, ['1'])
1032 f
= match_filter_func('uploader = "變態妍字幕版 太妍 тест"')
1034 self
.assertEqual(res
, ['1'])
1036 f
= match_filter_func('uploader != "變態妍字幕版 太妍 тест"')
1038 self
.assertEqual(res
, ['2'])
1040 f
= match_filter_func('creator = "тест \' 123 \' тест--"')
1042 self
.assertEqual(res
, ['1'])
1044 f
= match_filter_func("creator = 'тест \\' 123 \\' тест--'")
1046 self
.assertEqual(res
, ['1'])
1048 f
= match_filter_func(r
"creator = 'тест \' 123 \' тест--' & duration > 30")
1050 self
.assertEqual(res
, [])
1052 def test_playlist_items_selection(self
):
1053 INDICES
, PAGE_SIZE
= list(range(1, 11)), 3
1055 def entry(i
, evaluated
):
1063 def pagedlist_entries(evaluated
):
1065 start
= PAGE_SIZE
* n
1066 for i
in INDICES
[start
: start
+ PAGE_SIZE
]:
1067 yield entry(i
, evaluated
)
1068 return OnDemandPagedList(page_func
, PAGE_SIZE
)
1071 return (i
+ PAGE_SIZE
- 1) // PAGE_SIZE
1073 def generator_entries(evaluated
):
1075 yield entry(i
, evaluated
)
1077 def list_entries(evaluated
):
1078 return list(generator_entries(evaluated
))
1080 def lazylist_entries(evaluated
):
1081 return LazyList(generator_entries(evaluated
))
1083 def get_downloaded_info_dicts(params
, entries
):
1085 ydl
.process_ie_result({
1086 '_type': 'playlist',
1088 'extractor': 'test:playlist',
1089 'extractor_key': 'test:playlist',
1090 'webpage_url': 'http://example.com',
1093 return ydl
.downloaded_info_dicts
1095 def test_selection(params
, expected_ids
, evaluate_all
=False):
1096 expected_ids
= list(expected_ids
)
1098 generator_eval
= pagedlist_eval
= INDICES
1099 elif not expected_ids
:
1100 generator_eval
= pagedlist_eval
= []
1102 generator_eval
= INDICES
[0: max(expected_ids
)]
1103 pagedlist_eval
= INDICES
[PAGE_SIZE
* page_num(min(expected_ids
)) - PAGE_SIZE
:
1104 PAGE_SIZE
* page_num(max(expected_ids
))]
1106 for name
, func
, expected_eval
in (
1107 ('list', list_entries
, INDICES
),
1108 ('Generator', generator_entries
, generator_eval
),
1109 # ('LazyList', lazylist_entries, generator_eval), # Generator and LazyList follow the exact same code path
1110 ('PagedList', pagedlist_entries
, pagedlist_eval
),
1113 entries
= func(evaluated
)
1114 results
= [(v
['playlist_autonumber'] - 1, (int(v
['id']), v
['playlist_index']))
1115 for v
in get_downloaded_info_dicts(params
, entries
)]
1116 self
.assertEqual(results
, list(enumerate(zip(expected_ids
, expected_ids
))), f
'Entries of {name} for {params}')
1117 self
.assertEqual(sorted(evaluated
), expected_eval
, f
'Evaluation of {name} for {params}')
1119 test_selection({}, INDICES
)
1120 test_selection({'playlistend': 20}, INDICES
, True)
1121 test_selection({'playlistend': 2}, INDICES
[:2])
1122 test_selection({'playliststart': 11}, [], True)
1123 test_selection({'playliststart': 2}, INDICES
[1:])
1124 test_selection({'playlist_items': '2-4'}, INDICES
[1:4])
1125 test_selection({'playlist_items': '2,4'}, [2, 4])
1126 test_selection({'playlist_items': '20'}, [], True)
1127 test_selection({'playlist_items': '0'}, [])
1129 # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
1130 test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
1131 test_selection({'playlist_items': '4,2'}, [4, 2])
1133 # Tests for https://github.com/yt-dlp/yt-dlp/issues/720
1134 # https://github.com/yt-dlp/yt-dlp/issues/302
1135 test_selection({'playlistreverse': True}, INDICES
[::-1])
1136 test_selection({'playliststart': 2, 'playlistreverse': True}, INDICES
[:0:-1])
1137 test_selection({'playlist_items': '2,4', 'playlistreverse': True}, [4, 2])
1138 test_selection({'playlist_items': '4,2'}, [4, 2])
1140 # Tests for --playlist-items start:end:step
1141 test_selection({'playlist_items': ':'}, INDICES
, True)
1142 test_selection({'playlist_items': '::1'}, INDICES
, True)
1143 test_selection({'playlist_items': '::-1'}, INDICES
[::-1], True)
1144 test_selection({'playlist_items': ':6'}, INDICES
[:6])
1145 test_selection({'playlist_items': ':-6'}, INDICES
[:-5], True)
1146 test_selection({'playlist_items': '-1:6:-2'}, INDICES
[:4:-2], True)
1147 test_selection({'playlist_items': '9:-6:-2'}, INDICES
[8:3:-2], True)
1149 test_selection({'playlist_items': '1:inf:2'}, INDICES
[::2], True)
1150 test_selection({'playlist_items': '-2:inf'}, INDICES
[-2:], True)
1151 test_selection({'playlist_items': ':inf:-1'}, [], True)
1152 test_selection({'playlist_items': '0-2:2'}, [2])
1153 test_selection({'playlist_items': '1-:2'}, INDICES
[::2], True)
1154 test_selection({'playlist_items': '0--2:2'}, INDICES
[1:-1:2], True)
1156 test_selection({'playlist_items': '10::3'}, [10], True)
1157 test_selection({'playlist_items': '-1::3'}, [10], True)
1158 test_selection({'playlist_items': '11::3'}, [], True)
1159 test_selection({'playlist_items': '-15::2'}, INDICES
[1::2], True)
1160 test_selection({'playlist_items': '-15::15'}, [], True)
1162 def test_do_not_override_ie_key_in_url_transparent(self
):
1165 class Foo1IE(InfoExtractor
):
1166 _VALID_URL
= r
'foo1:'
1168 def _real_extract(self
, url
):
1170 '_type': 'url_transparent',
1173 'title': 'foo1 title',
1177 class Foo2IE(InfoExtractor
):
1178 _VALID_URL
= r
'foo2:'
1180 def _real_extract(self
, url
):
1187 class Foo3IE(InfoExtractor
):
1188 _VALID_URL
= r
'foo3:'
1190 def _real_extract(self
, url
):
1191 return _make_result([{'url': TEST_URL
}], title
='foo3 title')
1193 ydl
.add_info_extractor(Foo1IE(ydl
))
1194 ydl
.add_info_extractor(Foo2IE(ydl
))
1195 ydl
.add_info_extractor(Foo3IE(ydl
))
1196 ydl
.extract_info('foo1:')
1197 downloaded
= ydl
.downloaded_info_dicts
[0]
1198 self
.assertEqual(downloaded
['url'], TEST_URL
)
1199 self
.assertEqual(downloaded
['title'], 'foo1 title')
1200 self
.assertEqual(downloaded
['id'], 'testid')
1201 self
.assertEqual(downloaded
['extractor'], 'testex')
1202 self
.assertEqual(downloaded
['extractor_key'], 'TestEx')
1204 # Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
1205 def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self
):
1208 def __init__(self
, *args
, **kwargs
):
1209 super().__init
__(*args
, **kwargs
)
1211 def trouble(self
, s
, tb
=None):
1216 'ignoreerrors': True,
1219 class VideoIE(InfoExtractor
):
1220 _VALID_URL
= r
'video:(?P<id>\d+)'
1222 def _real_extract(self
, url
):
1223 video_id
= self
._match
_id
(url
)
1225 'format_id': 'default',
1229 raise ExtractorError('foo')
1232 'format_id': 'extra',
1237 'title': f
'Video {video_id}',
1241 class PlaylistIE(InfoExtractor
):
1242 _VALID_URL
= r
'playlist:'
1248 '_type': 'url_transparent',
1249 'ie_key': VideoIE
.ie_key(),
1251 'url': f
'video:{video_id}',
1252 'title': f
'Video Transparent {video_id}',
1255 def _real_extract(self
, url
):
1256 return self
.playlist_result(self
._entries
())
1258 ydl
.add_info_extractor(VideoIE(ydl
))
1259 ydl
.add_info_extractor(PlaylistIE(ydl
))
1260 info
= ydl
.extract_info('playlist:')
1261 entries
= info
['entries']
1262 self
.assertEqual(len(entries
), 3)
1263 self
.assertTrue(entries
[0] is None)
1264 self
.assertTrue(entries
[1] is None)
1265 self
.assertEqual(len(ydl
.downloaded_info_dicts
), 1)
1266 downloaded
= ydl
.downloaded_info_dicts
[0]
1267 entries
[2].pop('requested_downloads', None)
1268 self
.assertEqual(entries
[2], downloaded
)
1269 self
.assertEqual(downloaded
['url'], TEST_URL
)
1270 self
.assertEqual(downloaded
['title'], 'Video Transparent 2')
1271 self
.assertEqual(downloaded
['id'], '2')
1272 self
.assertEqual(downloaded
['extractor'], 'Video')
1273 self
.assertEqual(downloaded
['extractor_key'], 'Video')
1275 def test_header_cookies(self
):
1276 from http
.cookiejar
import Cookie
1279 ydl
.report_warning
= lambda *_
, **__
: None
1281 def cookie(name
, value
, version
=None, domain
='', path
='', secure
=False, expires
=None):
1283 version
or 0, name
, value
, None, False,
1284 domain
, bool(domain
), bool(domain
), path
, bool(path
),
1285 secure
, expires
, False, None, None, rest
={})
1287 _test_url
= 'https://yt.dlp/test'
1289 def test(encoded_cookies
, cookies
, *, headers
=False, round_trip
=None, error_re
=None):
1291 ydl
.cookiejar
.clear()
1292 ydl
._load
_cookies
(encoded_cookies
, autoscope
=headers
)
1294 ydl
._apply
_header
_cookies
(_test_url
)
1295 data
= {'url': _test_url
}
1296 ydl
._calc
_headers
(data
)
1297 self
.assertCountEqual(
1298 map(vars, ydl
.cookiejar
), map(vars, cookies
),
1299 'Extracted cookiejar.Cookie is not the same')
1302 data
.get('cookies'), round_trip
or encoded_cookies
,
1303 'Cookie is not the same as round trip')
1304 ydl
.__dict
__['_YoutubeDL__header_cookies'] = []
1306 with self
.subTest(msg
=encoded_cookies
):
1310 with self
.assertRaisesRegex(Exception, error_re
):
1313 test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain
='.yt.dlp')])
1314 test('test=value', [cookie('test', 'value')], error_re
=r
'Unscoped cookies are not allowed')
1315 test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
1316 cookie('cookie1', 'value1', domain
='.yt.dlp', path
='/test'),
1317 cookie('cookie2', 'value2', domain
='.yt.dlp', path
='/')])
1318 test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
1319 cookie('test', 'value', domain
='.yt.dlp', path
='/test', secure
=True, expires
=9999999999)])
1320 test('test="value; "; path=/test; domain=.yt.dlp', [
1321 cookie('test', 'value; ', domain
='.yt.dlp', path
='/test')],
1322 round_trip
='test="value\\073 "; Domain=.yt.dlp; Path=/test')
1323 test('name=; Domain=.yt.dlp', [cookie('name', '', domain
='.yt.dlp')],
1324 round_trip
='name=""; Domain=.yt.dlp')
1326 test('test=value', [cookie('test', 'value', domain
='.yt.dlp')], headers
=True)
1327 test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers
=True, error_re
=r
'Invalid syntax')
1328 ydl
.deprecated_feature
= ydl
.report_error
1329 test('test=value', [], headers
=True, error_re
=r
'Passing cookies as a header is a potential security risk')
1331 def test_infojson_cookies(self
):
1332 TEST_FILE
= 'test_infojson_cookies.info.json'
1333 TEST_URL
= 'https://example.com/example.mp4'
1334 COOKIES
= 'a=b; Domain=.example.com; c=d; Domain=.example.com'
1335 COOKIE_HEADER
= {'Cookie': 'a=b; c=d'}
1338 ydl
.process_info
= lambda x
: ydl
._write
_info
_json
('test', x
, TEST_FILE
)
1340 def make_info(info_header_cookies
=False, fmts_header_cookies
=False, cookies_field
=False):
1341 fmt
= {'url': TEST_URL
}
1342 if fmts_header_cookies
:
1343 fmt
['http_headers'] = COOKIE_HEADER
1345 fmt
['cookies'] = COOKIES
1346 return _make_result([fmt
], http_headers
=COOKIE_HEADER
if info_header_cookies
else None)
1348 def test(initial_info
, note
):
1350 result
['processed'] = ydl
.process_ie_result(initial_info
)
1351 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1352 msg
=f
'No cookies set in cookiejar after initial process when {note}')
1353 ydl
.cookiejar
.clear()
1354 with
open(TEST_FILE
) as infojson
:
1355 result
['loaded'] = ydl
.sanitize_info(json
.load(infojson
), True)
1356 result
['final'] = ydl
.process_ie_result(result
['loaded'].copy(), download
=False)
1357 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1358 msg
=f
'No cookies set in cookiejar after final process when {note}')
1359 ydl
.cookiejar
.clear()
1360 for key
in ('processed', 'loaded', 'final'):
1363 traverse_obj(info
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False),
1364 msg
=f
'Cookie header not removed in {key} result when {note}')
1366 traverse_obj(info
, ((None, ('formats', 0)), 'cookies'), get_all
=False), COOKIES
,
1367 msg
=f
'No cookies field found in {key} result when {note}')
1369 test({'url': TEST_URL
, 'http_headers': COOKIE_HEADER
, 'id': '1', 'title': 'x'}, 'no formats field')
1370 test(make_info(info_header_cookies
=True), 'info_dict header cokies')
1371 test(make_info(fmts_header_cookies
=True), 'format header cookies')
1372 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True), 'info_dict and format header cookies')
1373 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True, cookies_field
=True), 'all cookies fields')
1374 test(make_info(cookies_field
=True), 'cookies format field')
1375 test({'url': TEST_URL
, 'cookies': COOKIES
, 'id': '1', 'title': 'x'}, 'info_dict cookies field only')
1379 def test_add_headers_cookie(self
):
1380 def check_for_cookie_header(result
):
1381 return traverse_obj(result
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False)
1383 ydl
= FakeYDL({'http_headers': {'Cookie': 'a=b'}})
1384 ydl
._apply
_header
_cookies
(_make_result([])['webpage_url']) # Scope to input webpage URL: .example.com
1386 fmt
= {'url': 'https://example.com/video.mp4'}
1387 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1388 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies in result info_dict')
1389 self
.assertEqual(result
.get('cookies'), 'a=b; Domain=.example.com', msg
='No cookies were set in cookies field')
1390 self
.assertIn('a=b', ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='No cookies were set in cookiejar')
1392 fmt
= {'url': 'https://wrong.com/video.mp4'}
1393 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1394 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies for wrong domain')
1395 self
.assertFalse(result
.get('cookies'), msg
='Cookies set in cookies field for wrong domain')
1396 self
.assertFalse(ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='Cookies set in cookiejar for wrong domain')
1399 if __name__
== '__main__':