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')
764 # Sanitization options
765 test('%(title3)s', (None, 'foo⧸bar⧹test'))
766 test('%(title5)s', (None, 'aei_A'), restrictfilenames
=True)
767 test('%(title3)s', (None, 'foo_bar_test'), windowsfilenames
=False, restrictfilenames
=True)
768 if sys
.platform
!= 'win32':
769 test('%(title3)s', (None, 'foo⧸bar\\test'), windowsfilenames
=False)
772 test('%(id)s', '_abcd', info
={'id': '_abcd'})
773 test('%(some_id)s', '_abcd', info
={'some_id': '_abcd'})
774 test('%(formats.0.id)s', '_abcd', info
={'formats': [{'id': '_abcd'}]})
775 test('%(id)s', '-abcd', info
={'id': '-abcd'})
776 test('%(id)s', '.abcd', info
={'id': '.abcd'})
777 test('%(id)s', 'ab__cd', info
={'id': 'ab__cd'})
778 test('%(id)s', ('ab:cd', 'ab:cd'), info
={'id': 'ab:cd'})
779 test('%(id.0)s', '-', info
={'id': '--'})
782 self
.assertTrue(isinstance(YoutubeDL
.validate_outtmpl('%(title)'), ValueError))
783 test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder
='none')
785 test('%(formats.{id)s', 'NA')
788 def expect_same_infodict(out
):
789 got_dict
= json
.loads(out
)
790 for info_field
, expected
in self
.outtmpl_info
.items():
791 self
.assertEqual(got_dict
.get(info_field
), expected
, info_field
)
794 test('%()j', (expect_same_infodict
, None))
797 NA_TEST_OUTTMPL
= '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
798 test(NA_TEST_OUTTMPL
, 'NA-NA-def-1234.mp4')
799 test(NA_TEST_OUTTMPL
, 'none-none-def-1234.mp4', outtmpl_na_placeholder
='none')
800 test(NA_TEST_OUTTMPL
, '--def-1234.mp4', outtmpl_na_placeholder
='')
801 test('%(non_existent.0)s', 'NA')
804 FMT_TEST_OUTTMPL
= '%%(height)%s.%%(ext)s'
805 test(FMT_TEST_OUTTMPL
% 's', '1080.mp4')
806 test(FMT_TEST_OUTTMPL
% 'd', '1080.mp4')
807 test(FMT_TEST_OUTTMPL
% '6d', ' 1080.mp4')
808 test(FMT_TEST_OUTTMPL
% '-6d', '1080 .mp4')
809 test(FMT_TEST_OUTTMPL
% '06d', '001080.mp4')
810 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
811 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
812 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
813 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
814 test(FMT_TEST_OUTTMPL
% ' 0 6d', ' 01080.mp4')
817 test('%(id)d', '1234')
818 test('%(height)c', '1')
820 test('%(id)d %(id)r', "1234 '1234'")
821 test('%(id)r %(height)r', "'1234' 1080")
822 test('%(title5)a %(height)a', (R
"'\xe1\xe9\xed \U0001d400' 1080", None))
823 test('%(ext)s-%(ext|def)d', 'mp4-def')
824 test('%(width|0)04d', '0')
825 test('a%(width|b)d', 'ab', outtmpl_na_placeholder
='none')
827 FORMATS
= self
.outtmpl_info
['formats']
829 # Custom type casting
830 test('%(formats.:.id)l', 'id 1, id 2, id 3')
831 test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
832 test('%(ext)l', 'mp4')
833 test('%(formats.:.id) 18l', ' id 1, id 2, id 3')
834 test('%(formats)j', (json
.dumps(FORMATS
), None))
835 test('%(formats)#j', (
836 json
.dumps(FORMATS
, indent
=4),
837 json
.dumps(FORMATS
, indent
=4).replace(':', ':').replace('"', '"').replace('\n', ' '),
839 test('%(title5).3B', 'á')
840 test('%(title5)U', 'áéí 𝐀')
841 test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
842 test('%(title5)+U', 'áéí A')
843 test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
844 test('%(height)D', '1k')
845 test('%(filesize)#D', '1Ki')
846 test('%(height)5.2D', ' 1.08k')
847 test('%(title4)#S', 'foo_bar_test')
848 test('%(title4).10S', ('foo "bar" ', 'foo "bar"' + ('#' if os
.name
== 'nt' else ' ')))
850 test('%(title4)q', ('"foo ""bar"" test"', None))
851 test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', None))
852 test('%(formats.0.id)#q', ('"id 1"', None))
854 test('%(title4)q', ('\'foo "bar" test\'', '\'foo "bar" test\''))
855 test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
856 test('%(formats.0.id)#q', "'id 1'")
858 # Internal formatting
859 test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
860 test('%(title|%)s %(title|%%)s', '% %%')
861 test('%(id+1-height+3)05d', '00158')
862 test('%(width+100)05d', 'NA')
863 test('%(filesize*8)d', '8192')
864 test('%(formats.0) 15s', ('% 15s' % FORMATS
[0], None))
865 test('%(formats.0)r', (repr(FORMATS
[0]), None))
866 test('%(height.0)03d', '001')
867 test('%(-height.0)04d', '-001')
868 test('%(formats.-1.id)s', FORMATS
[-1]['id'])
869 test('%(formats.0.id.-1)d', FORMATS
[0]['id'][-1])
870 test('%(formats.3)s', 'NA')
871 test('%(formats.:2:-1)r', repr(FORMATS
[:2:-1]))
872 test('%(formats.0.id.-1+id)f', '1235.000000')
873 test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
874 out
= json
.dumps([{'id': f
['id'], 'height.:2': str(f
['height'])[:2]}
875 if 'height' in f
else {'id': f
['id']}
877 test('%(formats.:.{id,height.:2})j', (out
, None))
878 test('%(formats.:.{id,height}.id)l', ', '.join(f
['id'] for f
in FORMATS
))
879 test('%(.{id,title})j', ('{"id": "1234"}', '{"id": "1234"}'))
882 test('%(title,id)s', '1234')
883 test('%(width-100,height+20|def)d', '1100')
884 test('%(width-100,height+width|def)s', 'def')
885 test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
888 test('%(id&foo)s.bar', 'foo.bar')
889 test('%(title&foo)s.bar', 'NA.bar')
890 test('%(title&foo|baz)s.bar', 'baz.bar')
891 test('%(x,id&foo|baz)s.bar', 'foo.bar')
892 test('%(x,title&foo|baz)s.bar', 'baz.bar')
893 test('%(id&a\nb|)s', ('a\nb', 'a b'))
894 test('%(id&hi {:>10} {}|)s', 'hi 1234 1234')
895 test(R
'%(id&{0} {}|)s', 'NA')
896 test(R
'%(id&{0.1}|)s', 'NA')
897 test('%(height&{:,d})S', '1,080')
902 raise self
.assertTrue(False, 'LazyList should not be evaluated till here')
903 test('%(key.4)s', '4', info
={'key': LazyList(gen())})
906 test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
907 # test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # FIXME: ?
908 # test('%(foo|)s', ('', '_')) # FIXME: ?
910 # Environment variable expansion for prepare_filename
911 os
.environ
['__yt_dlp_var'] = 'expanded'
912 envvar
= '%__yt_dlp_var%' if os
.name
== 'nt' else '$__yt_dlp_var'
913 test(envvar
, (envvar
, 'expanded'))
915 test('%s%', ('%s%', '%s%'))
916 os
.environ
['s'] = 'expanded'
917 test('%s%', ('%s%', 'expanded')) # %s% should be expanded before escaping %s
918 os
.environ
['(test)s'] = 'expanded'
919 test('%(test)s%', ('NA%', 'expanded')) # Environment should take priority over template
921 # Path expansion and escaping
922 test('Hello %(title1)s', 'Hello $PATH')
923 test('Hello %(title2)s', 'Hello %PATH%')
924 test('%(title3)s', ('foo/bar\\test', 'foo⧸bar⧹test'))
925 test('folder/%(title3)s', ('folder/foo/bar\\test', f
'folder{os.path.sep}foo⧸bar⧹test'))
927 def test_format_note(self
):
929 self
.assertEqual(ydl
._format
_note
({}), '')
930 assertRegexpMatches(self
, ydl
._format
_note
({
933 assertRegexpMatches(self
, ydl
._format
_note
({
937 def test_postprocessors(self
):
938 filename
= 'post-processor-testfile.mp4'
939 audiofile
= filename
+ '.mp3'
941 class SimplePP(PostProcessor
):
943 with
open(audiofile
, 'w') as f
:
945 return [info
['filepath']], info
947 def run_pp(params
, pp
):
948 with
open(filename
, 'w') as f
:
950 ydl
= YoutubeDL(params
)
951 ydl
.add_post_processor(pp())
952 ydl
.post_process(filename
, {'filepath': filename
})
954 run_pp({'keepvideo': True}, SimplePP
)
955 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
956 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
960 run_pp({'keepvideo': False}, SimplePP
)
961 self
.assertFalse(os
.path
.exists(filename
), f
'{filename} exists')
962 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
965 class ModifierPP(PostProcessor
):
967 with
open(info
['filepath'], 'w') as f
:
971 run_pp({'keepvideo': False}, ModifierPP
)
972 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
975 def test_match_filter(self
):
982 'filesize': 10 * 1024,
984 'uploader': '變態妍字幕版 太妍 тест',
985 'creator': "тест ' 123 ' тест--",
986 'webpage_url': 'http://example.com/watch?v=shenanigans',
994 'description': 'foo',
995 'filesize': 5 * 1024,
997 'uploader': 'тест 123',
998 'webpage_url': 'http://example.com/watch?v=SHENANIGANS',
1000 videos
= [first
, second
]
1002 def get_videos(filter_
=None):
1003 ydl
= YDL({'match_filter': filter_
, 'simulate': True})
1005 ydl
.process_ie_result(v
.copy(), download
=True)
1006 return [v
['id'] for v
in ydl
.downloaded_info_dicts
]
1009 self
.assertEqual(res
, ['1', '2'])
1011 def f(v
, incomplete
):
1015 return 'Video id is not 1'
1017 self
.assertEqual(res
, ['1'])
1019 f
= match_filter_func('duration < 30')
1021 self
.assertEqual(res
, ['2'])
1023 f
= match_filter_func('description = foo')
1025 self
.assertEqual(res
, ['2'])
1027 f
= match_filter_func('description =? foo')
1029 self
.assertEqual(res
, ['1', '2'])
1031 f
= match_filter_func('filesize > 5KiB')
1033 self
.assertEqual(res
, ['1'])
1035 f
= match_filter_func('playlist_id = 42')
1037 self
.assertEqual(res
, ['1'])
1039 f
= match_filter_func('uploader = "變態妍字幕版 太妍 тест"')
1041 self
.assertEqual(res
, ['1'])
1043 f
= match_filter_func('uploader != "變態妍字幕版 太妍 тест"')
1045 self
.assertEqual(res
, ['2'])
1047 f
= match_filter_func('creator = "тест \' 123 \' тест--"')
1049 self
.assertEqual(res
, ['1'])
1051 f
= match_filter_func("creator = 'тест \\' 123 \\' тест--'")
1053 self
.assertEqual(res
, ['1'])
1055 f
= match_filter_func(r
"creator = 'тест \' 123 \' тест--' & duration > 30")
1057 self
.assertEqual(res
, [])
1059 def test_playlist_items_selection(self
):
1060 INDICES
, PAGE_SIZE
= list(range(1, 11)), 3
1062 def entry(i
, evaluated
):
1070 def pagedlist_entries(evaluated
):
1072 start
= PAGE_SIZE
* n
1073 for i
in INDICES
[start
: start
+ PAGE_SIZE
]:
1074 yield entry(i
, evaluated
)
1075 return OnDemandPagedList(page_func
, PAGE_SIZE
)
1078 return (i
+ PAGE_SIZE
- 1) // PAGE_SIZE
1080 def generator_entries(evaluated
):
1082 yield entry(i
, evaluated
)
1084 def list_entries(evaluated
):
1085 return list(generator_entries(evaluated
))
1087 def lazylist_entries(evaluated
):
1088 return LazyList(generator_entries(evaluated
))
1090 def get_downloaded_info_dicts(params
, entries
):
1092 ydl
.process_ie_result({
1093 '_type': 'playlist',
1095 'extractor': 'test:playlist',
1096 'extractor_key': 'test:playlist',
1097 'webpage_url': 'http://example.com',
1100 return ydl
.downloaded_info_dicts
1102 def test_selection(params
, expected_ids
, evaluate_all
=False):
1103 expected_ids
= list(expected_ids
)
1105 generator_eval
= pagedlist_eval
= INDICES
1106 elif not expected_ids
:
1107 generator_eval
= pagedlist_eval
= []
1109 generator_eval
= INDICES
[0: max(expected_ids
)]
1110 pagedlist_eval
= INDICES
[PAGE_SIZE
* page_num(min(expected_ids
)) - PAGE_SIZE
:
1111 PAGE_SIZE
* page_num(max(expected_ids
))]
1113 for name
, func
, expected_eval
in (
1114 ('list', list_entries
, INDICES
),
1115 ('Generator', generator_entries
, generator_eval
),
1116 # ('LazyList', lazylist_entries, generator_eval), # Generator and LazyList follow the exact same code path
1117 ('PagedList', pagedlist_entries
, pagedlist_eval
),
1120 entries
= func(evaluated
)
1121 results
= [(v
['playlist_autonumber'] - 1, (int(v
['id']), v
['playlist_index']))
1122 for v
in get_downloaded_info_dicts(params
, entries
)]
1123 self
.assertEqual(results
, list(enumerate(zip(expected_ids
, expected_ids
))), f
'Entries of {name} for {params}')
1124 self
.assertEqual(sorted(evaluated
), expected_eval
, f
'Evaluation of {name} for {params}')
1126 test_selection({}, INDICES
)
1127 test_selection({'playlistend': 20}, INDICES
, True)
1128 test_selection({'playlistend': 2}, INDICES
[:2])
1129 test_selection({'playliststart': 11}, [], True)
1130 test_selection({'playliststart': 2}, INDICES
[1:])
1131 test_selection({'playlist_items': '2-4'}, INDICES
[1:4])
1132 test_selection({'playlist_items': '2,4'}, [2, 4])
1133 test_selection({'playlist_items': '20'}, [], True)
1134 test_selection({'playlist_items': '0'}, [])
1136 # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
1137 test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
1138 test_selection({'playlist_items': '4,2'}, [4, 2])
1140 # Tests for https://github.com/yt-dlp/yt-dlp/issues/720
1141 # https://github.com/yt-dlp/yt-dlp/issues/302
1142 test_selection({'playlistreverse': True}, INDICES
[::-1])
1143 test_selection({'playliststart': 2, 'playlistreverse': True}, INDICES
[:0:-1])
1144 test_selection({'playlist_items': '2,4', 'playlistreverse': True}, [4, 2])
1145 test_selection({'playlist_items': '4,2'}, [4, 2])
1147 # Tests for --playlist-items start:end:step
1148 test_selection({'playlist_items': ':'}, INDICES
, True)
1149 test_selection({'playlist_items': '::1'}, INDICES
, True)
1150 test_selection({'playlist_items': '::-1'}, INDICES
[::-1], True)
1151 test_selection({'playlist_items': ':6'}, INDICES
[:6])
1152 test_selection({'playlist_items': ':-6'}, INDICES
[:-5], True)
1153 test_selection({'playlist_items': '-1:6:-2'}, INDICES
[:4:-2], True)
1154 test_selection({'playlist_items': '9:-6:-2'}, INDICES
[8:3:-2], True)
1156 test_selection({'playlist_items': '1:inf:2'}, INDICES
[::2], True)
1157 test_selection({'playlist_items': '-2:inf'}, INDICES
[-2:], True)
1158 test_selection({'playlist_items': ':inf:-1'}, [], True)
1159 test_selection({'playlist_items': '0-2:2'}, [2])
1160 test_selection({'playlist_items': '1-:2'}, INDICES
[::2], True)
1161 test_selection({'playlist_items': '0--2:2'}, INDICES
[1:-1:2], True)
1163 test_selection({'playlist_items': '10::3'}, [10], True)
1164 test_selection({'playlist_items': '-1::3'}, [10], True)
1165 test_selection({'playlist_items': '11::3'}, [], True)
1166 test_selection({'playlist_items': '-15::2'}, INDICES
[1::2], True)
1167 test_selection({'playlist_items': '-15::15'}, [], True)
1169 def test_do_not_override_ie_key_in_url_transparent(self
):
1172 class Foo1IE(InfoExtractor
):
1173 _VALID_URL
= r
'foo1:'
1175 def _real_extract(self
, url
):
1177 '_type': 'url_transparent',
1180 'title': 'foo1 title',
1184 class Foo2IE(InfoExtractor
):
1185 _VALID_URL
= r
'foo2:'
1187 def _real_extract(self
, url
):
1194 class Foo3IE(InfoExtractor
):
1195 _VALID_URL
= r
'foo3:'
1197 def _real_extract(self
, url
):
1198 return _make_result([{'url': TEST_URL
}], title
='foo3 title')
1200 ydl
.add_info_extractor(Foo1IE(ydl
))
1201 ydl
.add_info_extractor(Foo2IE(ydl
))
1202 ydl
.add_info_extractor(Foo3IE(ydl
))
1203 ydl
.extract_info('foo1:')
1204 downloaded
= ydl
.downloaded_info_dicts
[0]
1205 self
.assertEqual(downloaded
['url'], TEST_URL
)
1206 self
.assertEqual(downloaded
['title'], 'foo1 title')
1207 self
.assertEqual(downloaded
['id'], 'testid')
1208 self
.assertEqual(downloaded
['extractor'], 'testex')
1209 self
.assertEqual(downloaded
['extractor_key'], 'TestEx')
1211 # Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
1212 def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self
):
1215 def __init__(self
, *args
, **kwargs
):
1216 super().__init
__(*args
, **kwargs
)
1218 def trouble(self
, s
, tb
=None):
1223 'ignoreerrors': True,
1226 class VideoIE(InfoExtractor
):
1227 _VALID_URL
= r
'video:(?P<id>\d+)'
1229 def _real_extract(self
, url
):
1230 video_id
= self
._match
_id
(url
)
1232 'format_id': 'default',
1236 raise ExtractorError('foo')
1239 'format_id': 'extra',
1244 'title': f
'Video {video_id}',
1248 class PlaylistIE(InfoExtractor
):
1249 _VALID_URL
= r
'playlist:'
1255 '_type': 'url_transparent',
1256 'ie_key': VideoIE
.ie_key(),
1258 'url': f
'video:{video_id}',
1259 'title': f
'Video Transparent {video_id}',
1262 def _real_extract(self
, url
):
1263 return self
.playlist_result(self
._entries
())
1265 ydl
.add_info_extractor(VideoIE(ydl
))
1266 ydl
.add_info_extractor(PlaylistIE(ydl
))
1267 info
= ydl
.extract_info('playlist:')
1268 entries
= info
['entries']
1269 self
.assertEqual(len(entries
), 3)
1270 self
.assertTrue(entries
[0] is None)
1271 self
.assertTrue(entries
[1] is None)
1272 self
.assertEqual(len(ydl
.downloaded_info_dicts
), 1)
1273 downloaded
= ydl
.downloaded_info_dicts
[0]
1274 entries
[2].pop('requested_downloads', None)
1275 self
.assertEqual(entries
[2], downloaded
)
1276 self
.assertEqual(downloaded
['url'], TEST_URL
)
1277 self
.assertEqual(downloaded
['title'], 'Video Transparent 2')
1278 self
.assertEqual(downloaded
['id'], '2')
1279 self
.assertEqual(downloaded
['extractor'], 'Video')
1280 self
.assertEqual(downloaded
['extractor_key'], 'Video')
1282 def test_header_cookies(self
):
1283 from http
.cookiejar
import Cookie
1286 ydl
.report_warning
= lambda *_
, **__
: None
1288 def cookie(name
, value
, version
=None, domain
='', path
='', secure
=False, expires
=None):
1290 version
or 0, name
, value
, None, False,
1291 domain
, bool(domain
), bool(domain
), path
, bool(path
),
1292 secure
, expires
, False, None, None, rest
={})
1294 _test_url
= 'https://yt.dlp/test'
1296 def test(encoded_cookies
, cookies
, *, headers
=False, round_trip
=None, error_re
=None):
1298 ydl
.cookiejar
.clear()
1299 ydl
._load
_cookies
(encoded_cookies
, autoscope
=headers
)
1301 ydl
._apply
_header
_cookies
(_test_url
)
1302 data
= {'url': _test_url
}
1303 ydl
._calc
_headers
(data
)
1304 self
.assertCountEqual(
1305 map(vars, ydl
.cookiejar
), map(vars, cookies
),
1306 'Extracted cookiejar.Cookie is not the same')
1309 data
.get('cookies'), round_trip
or encoded_cookies
,
1310 'Cookie is not the same as round trip')
1311 ydl
.__dict
__['_YoutubeDL__header_cookies'] = []
1313 with self
.subTest(msg
=encoded_cookies
):
1317 with self
.assertRaisesRegex(Exception, error_re
):
1320 test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain
='.yt.dlp')])
1321 test('test=value', [cookie('test', 'value')], error_re
=r
'Unscoped cookies are not allowed')
1322 test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
1323 cookie('cookie1', 'value1', domain
='.yt.dlp', path
='/test'),
1324 cookie('cookie2', 'value2', domain
='.yt.dlp', path
='/')])
1325 test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
1326 cookie('test', 'value', domain
='.yt.dlp', path
='/test', secure
=True, expires
=9999999999)])
1327 test('test="value; "; path=/test; domain=.yt.dlp', [
1328 cookie('test', 'value; ', domain
='.yt.dlp', path
='/test')],
1329 round_trip
='test="value\\073 "; Domain=.yt.dlp; Path=/test')
1330 test('name=; Domain=.yt.dlp', [cookie('name', '', domain
='.yt.dlp')],
1331 round_trip
='name=""; Domain=.yt.dlp')
1333 test('test=value', [cookie('test', 'value', domain
='.yt.dlp')], headers
=True)
1334 test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers
=True, error_re
=r
'Invalid syntax')
1335 ydl
.deprecated_feature
= ydl
.report_error
1336 test('test=value', [], headers
=True, error_re
=r
'Passing cookies as a header is a potential security risk')
1338 def test_infojson_cookies(self
):
1339 TEST_FILE
= 'test_infojson_cookies.info.json'
1340 TEST_URL
= 'https://example.com/example.mp4'
1341 COOKIES
= 'a=b; Domain=.example.com; c=d; Domain=.example.com'
1342 COOKIE_HEADER
= {'Cookie': 'a=b; c=d'}
1345 ydl
.process_info
= lambda x
: ydl
._write
_info
_json
('test', x
, TEST_FILE
)
1347 def make_info(info_header_cookies
=False, fmts_header_cookies
=False, cookies_field
=False):
1348 fmt
= {'url': TEST_URL
}
1349 if fmts_header_cookies
:
1350 fmt
['http_headers'] = COOKIE_HEADER
1352 fmt
['cookies'] = COOKIES
1353 return _make_result([fmt
], http_headers
=COOKIE_HEADER
if info_header_cookies
else None)
1355 def test(initial_info
, note
):
1357 result
['processed'] = ydl
.process_ie_result(initial_info
)
1358 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1359 msg
=f
'No cookies set in cookiejar after initial process when {note}')
1360 ydl
.cookiejar
.clear()
1361 with
open(TEST_FILE
) as infojson
:
1362 result
['loaded'] = ydl
.sanitize_info(json
.load(infojson
), True)
1363 result
['final'] = ydl
.process_ie_result(result
['loaded'].copy(), download
=False)
1364 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1365 msg
=f
'No cookies set in cookiejar after final process when {note}')
1366 ydl
.cookiejar
.clear()
1367 for key
in ('processed', 'loaded', 'final'):
1370 traverse_obj(info
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False),
1371 msg
=f
'Cookie header not removed in {key} result when {note}')
1373 traverse_obj(info
, ((None, ('formats', 0)), 'cookies'), get_all
=False), COOKIES
,
1374 msg
=f
'No cookies field found in {key} result when {note}')
1376 test({'url': TEST_URL
, 'http_headers': COOKIE_HEADER
, 'id': '1', 'title': 'x'}, 'no formats field')
1377 test(make_info(info_header_cookies
=True), 'info_dict header cokies')
1378 test(make_info(fmts_header_cookies
=True), 'format header cookies')
1379 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True), 'info_dict and format header cookies')
1380 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True, cookies_field
=True), 'all cookies fields')
1381 test(make_info(cookies_field
=True), 'cookies format field')
1382 test({'url': TEST_URL
, 'cookies': COOKIES
, 'id': '1', 'title': 'x'}, 'info_dict cookies field only')
1386 def test_add_headers_cookie(self
):
1387 def check_for_cookie_header(result
):
1388 return traverse_obj(result
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False)
1390 ydl
= FakeYDL({'http_headers': {'Cookie': 'a=b'}})
1391 ydl
._apply
_header
_cookies
(_make_result([])['webpage_url']) # Scope to input webpage URL: .example.com
1393 fmt
= {'url': 'https://example.com/video.mp4'}
1394 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1395 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies in result info_dict')
1396 self
.assertEqual(result
.get('cookies'), 'a=b; Domain=.example.com', msg
='No cookies were set in cookies field')
1397 self
.assertIn('a=b', ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='No cookies were set in cookiejar')
1399 fmt
= {'url': 'https://wrong.com/video.mp4'}
1400 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1401 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies for wrong domain')
1402 self
.assertFalse(result
.get('cookies'), msg
='Cookies set in cookies field for wrong domain')
1403 self
.assertFalse(ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='Cookies set in cookiejar for wrong domain')
1406 if __name__
== '__main__':