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
.compat
import compat_os_name
19 from yt_dlp
.extractor
import YoutubeIE
20 from yt_dlp
.extractor
.common
import InfoExtractor
21 from yt_dlp
.postprocessor
.common
import PostProcessor
22 from yt_dlp
.utils
import (
29 from yt_dlp
.utils
.traversal
import traverse_obj
31 TEST_URL
= 'http://localhost/sample.mp4'
35 def __init__(self
, *args
, **kwargs
):
36 super().__init
__(*args
, **kwargs
)
37 self
.downloaded_info_dicts
= []
40 def process_info(self
, info_dict
):
41 self
.downloaded_info_dicts
.append(info_dict
.copy())
43 def to_screen(self
, msg
, *args
, **kwargs
):
46 def dl(self
, *args
, **kwargs
):
47 assert False, 'Downloader must not be invoked for test_YoutubeDL'
50 def _make_result(formats
, **kwargs
):
54 'title': 'testttitle',
55 'extractor': 'testex',
56 'extractor_key': 'TestEx',
57 'webpage_url': 'http://example.com/watch?v=shenanigans',
63 class TestFormatSelection(unittest
.TestCase
):
64 def test_prefer_free_formats(self
):
65 # Same resolution => download webm
67 ydl
.params
['prefer_free_formats'] = True
69 {'ext': 'webm', 'height': 460, 'url': TEST_URL
},
70 {'ext': 'mp4', 'height': 460, 'url': TEST_URL
},
72 info_dict
= _make_result(formats
)
73 ydl
.sort_formats(info_dict
)
74 ydl
.process_ie_result(info_dict
)
75 downloaded
= ydl
.downloaded_info_dicts
[0]
76 self
.assertEqual(downloaded
['ext'], 'webm')
78 # Different resolution => download best quality (mp4)
80 ydl
.params
['prefer_free_formats'] = True
82 {'ext': 'webm', 'height': 720, 'url': TEST_URL
},
83 {'ext': 'mp4', 'height': 1080, 'url': TEST_URL
},
85 info_dict
['formats'] = formats
86 ydl
.sort_formats(info_dict
)
87 ydl
.process_ie_result(info_dict
)
88 downloaded
= ydl
.downloaded_info_dicts
[0]
89 self
.assertEqual(downloaded
['ext'], 'mp4')
91 # No prefer_free_formats => prefer mp4 and webm
93 ydl
.params
['prefer_free_formats'] = False
95 {'ext': 'webm', 'height': 720, 'url': TEST_URL
},
96 {'ext': 'mp4', 'height': 720, 'url': TEST_URL
},
97 {'ext': 'flv', 'height': 720, 'url': TEST_URL
},
99 info_dict
['formats'] = formats
100 ydl
.sort_formats(info_dict
)
101 ydl
.process_ie_result(info_dict
)
102 downloaded
= ydl
.downloaded_info_dicts
[0]
103 self
.assertEqual(downloaded
['ext'], 'mp4')
106 ydl
.params
['prefer_free_formats'] = False
108 {'ext': 'flv', 'height': 720, 'url': TEST_URL
},
109 {'ext': 'webm', 'height': 720, 'url': TEST_URL
},
111 info_dict
['formats'] = formats
112 ydl
.sort_formats(info_dict
)
113 ydl
.process_ie_result(info_dict
)
114 downloaded
= ydl
.downloaded_info_dicts
[0]
115 self
.assertEqual(downloaded
['ext'], 'webm')
117 def test_format_selection(self
):
119 {'format_id': '35', 'ext': 'mp4', 'preference': 0, 'url': TEST_URL
},
120 {'format_id': 'example-with-dashes', 'ext': 'webm', 'preference': 1, 'url': TEST_URL
},
121 {'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': TEST_URL
},
122 {'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': TEST_URL
},
123 {'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': TEST_URL
},
125 info_dict
= _make_result(formats
)
127 def test(inp
, *expected
, multi
=False):
130 'allow_multiple_video_streams': multi
,
131 'allow_multiple_audio_streams': multi
,
133 ydl
.process_ie_result(info_dict
.copy())
134 downloaded
= [x
['format_id'] for x
in ydl
.downloaded_info_dicts
]
135 self
.assertEqual(downloaded
, list(expected
))
138 test('20/71/worst', '35')
140 test('webm/mp4', '47')
141 test('3gp/40/mp4', '35')
142 test('example-with-dashes', 'example-with-dashes')
143 test('all', '2', '47', '45', 'example-with-dashes', '35')
144 test('mergeall', '2+47+45+example-with-dashes+35', multi
=True)
145 # See: https://github.com/yt-dlp/yt-dlp/pulls/8797
146 test('7_a/worst', '35')
148 def test_format_selection_audio(self
):
150 {'format_id': 'audio-low', 'ext': 'webm', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL
},
151 {'format_id': 'audio-mid', 'ext': 'webm', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL
},
152 {'format_id': 'audio-high', 'ext': 'flv', 'preference': 3, 'vcodec': 'none', 'url': TEST_URL
},
153 {'format_id': 'vid', 'ext': 'mp4', 'preference': 4, 'url': TEST_URL
},
155 info_dict
= _make_result(formats
)
157 ydl
= YDL({'format': 'bestaudio'})
158 ydl
.process_ie_result(info_dict
.copy())
159 downloaded
= ydl
.downloaded_info_dicts
[0]
160 self
.assertEqual(downloaded
['format_id'], 'audio-high')
162 ydl
= YDL({'format': 'worstaudio'})
163 ydl
.process_ie_result(info_dict
.copy())
164 downloaded
= ydl
.downloaded_info_dicts
[0]
165 self
.assertEqual(downloaded
['format_id'], 'audio-low')
168 {'format_id': 'vid-low', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL
},
169 {'format_id': 'vid-high', 'ext': 'mp4', 'preference': 2, 'url': TEST_URL
},
171 info_dict
= _make_result(formats
)
173 ydl
= YDL({'format': 'bestaudio/worstaudio/best'})
174 ydl
.process_ie_result(info_dict
.copy())
175 downloaded
= ydl
.downloaded_info_dicts
[0]
176 self
.assertEqual(downloaded
['format_id'], 'vid-high')
178 def test_format_selection_audio_exts(self
):
180 {'format_id': 'mp3-64', 'ext': 'mp3', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'},
181 {'format_id': 'ogg-64', 'ext': 'ogg', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'},
182 {'format_id': 'aac-64', 'ext': 'aac', 'abr': 64, 'url': 'http://_', 'vcodec': 'none'},
183 {'format_id': 'mp3-32', 'ext': 'mp3', 'abr': 32, 'url': 'http://_', 'vcodec': 'none'},
184 {'format_id': 'aac-32', 'ext': 'aac', 'abr': 32, 'url': 'http://_', 'vcodec': 'none'},
187 info_dict
= _make_result(formats
)
188 ydl
= YDL({'format': 'best', 'format_sort': ['abr', 'ext']})
189 ydl
.sort_formats(info_dict
)
190 ydl
.process_ie_result(copy
.deepcopy(info_dict
))
191 downloaded
= ydl
.downloaded_info_dicts
[0]
192 self
.assertEqual(downloaded
['format_id'], 'aac-64')
194 ydl
= YDL({'format': 'mp3'})
195 ydl
.sort_formats(info_dict
)
196 ydl
.process_ie_result(copy
.deepcopy(info_dict
))
197 downloaded
= ydl
.downloaded_info_dicts
[0]
198 self
.assertEqual(downloaded
['format_id'], 'mp3-64')
200 ydl
= YDL({'prefer_free_formats': True, 'format_sort': ['abr', 'ext']})
201 ydl
.sort_formats(info_dict
)
202 ydl
.process_ie_result(copy
.deepcopy(info_dict
))
203 downloaded
= ydl
.downloaded_info_dicts
[0]
204 self
.assertEqual(downloaded
['format_id'], 'ogg-64')
206 def test_format_selection_video(self
):
208 {'format_id': 'dash-video-low', 'ext': 'mp4', 'preference': 1, 'acodec': 'none', 'url': TEST_URL
},
209 {'format_id': 'dash-video-high', 'ext': 'mp4', 'preference': 2, 'acodec': 'none', 'url': TEST_URL
},
210 {'format_id': 'vid', 'ext': 'mp4', 'preference': 3, 'url': TEST_URL
},
212 info_dict
= _make_result(formats
)
214 ydl
= YDL({'format': 'bestvideo'})
215 ydl
.process_ie_result(info_dict
.copy())
216 downloaded
= ydl
.downloaded_info_dicts
[0]
217 self
.assertEqual(downloaded
['format_id'], 'dash-video-high')
219 ydl
= YDL({'format': 'worstvideo'})
220 ydl
.process_ie_result(info_dict
.copy())
221 downloaded
= ydl
.downloaded_info_dicts
[0]
222 self
.assertEqual(downloaded
['format_id'], 'dash-video-low')
224 ydl
= YDL({'format': 'bestvideo[format_id^=dash][format_id$=low]'})
225 ydl
.process_ie_result(info_dict
.copy())
226 downloaded
= ydl
.downloaded_info_dicts
[0]
227 self
.assertEqual(downloaded
['format_id'], 'dash-video-low')
230 {'format_id': 'vid-vcodec-dot', 'ext': 'mp4', 'preference': 1, 'vcodec': 'avc1.123456', 'acodec': 'none', 'url': TEST_URL
},
232 info_dict
= _make_result(formats
)
234 ydl
= YDL({'format': 'bestvideo[vcodec=avc1.123456]'})
235 ydl
.process_ie_result(info_dict
.copy())
236 downloaded
= ydl
.downloaded_info_dicts
[0]
237 self
.assertEqual(downloaded
['format_id'], 'vid-vcodec-dot')
239 def test_format_selection_by_vcodec_sort(self
):
241 {'format_id': 'av1-format', 'ext': 'mp4', 'vcodec': 'av1', 'acodec': 'none', 'url': TEST_URL
},
242 {'format_id': 'vp9-hdr-format', 'ext': 'mp4', 'vcodec': 'vp09.02.50.10.01.09.18.09.00', 'acodec': 'none', 'url': TEST_URL
},
243 {'format_id': 'vp9-sdr-format', 'ext': 'mp4', 'vcodec': 'vp09.00.50.08', 'acodec': 'none', 'url': TEST_URL
},
244 {'format_id': 'h265-format', 'ext': 'mp4', 'vcodec': 'h265', 'acodec': 'none', 'url': TEST_URL
},
246 info_dict
= _make_result(formats
)
248 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9.2']})
249 ydl
.process_ie_result(info_dict
.copy())
250 downloaded
= ydl
.downloaded_info_dicts
[0]
251 self
.assertEqual(downloaded
['format_id'], 'vp9-hdr-format')
253 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9']})
254 ydl
.process_ie_result(info_dict
.copy())
255 downloaded
= ydl
.downloaded_info_dicts
[0]
256 self
.assertEqual(downloaded
['format_id'], 'vp9-sdr-format')
258 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9.2']})
259 ydl
.process_ie_result(info_dict
.copy())
260 downloaded
= ydl
.downloaded_info_dicts
[0]
261 self
.assertEqual(downloaded
['format_id'], 'vp9-hdr-format')
263 ydl
= YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9']})
264 ydl
.process_ie_result(info_dict
.copy())
265 downloaded
= ydl
.downloaded_info_dicts
[0]
266 self
.assertEqual(downloaded
['format_id'], 'vp9-sdr-format')
268 def test_format_selection_string_ops(self
):
270 {'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL
},
271 {'format_id': 'zxc-cxz', 'ext': 'webm', 'url': TEST_URL
},
273 info_dict
= _make_result(formats
)
276 ydl
= YDL({'format': '[format_id=abc-cba]'})
277 ydl
.process_ie_result(info_dict
.copy())
278 downloaded
= ydl
.downloaded_info_dicts
[0]
279 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
281 # does not equal (!=)
282 ydl
= YDL({'format': '[format_id!=abc-cba]'})
283 ydl
.process_ie_result(info_dict
.copy())
284 downloaded
= ydl
.downloaded_info_dicts
[0]
285 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
287 ydl
= YDL({'format': '[format_id!=abc-cba][format_id!=zxc-cxz]'})
288 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
291 ydl
= YDL({'format': '[format_id^=abc]'})
292 ydl
.process_ie_result(info_dict
.copy())
293 downloaded
= ydl
.downloaded_info_dicts
[0]
294 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
296 # does not start with (!^=)
297 ydl
= YDL({'format': '[format_id!^=abc]'})
298 ydl
.process_ie_result(info_dict
.copy())
299 downloaded
= ydl
.downloaded_info_dicts
[0]
300 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
302 ydl
= YDL({'format': '[format_id!^=abc][format_id!^=zxc]'})
303 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
306 ydl
= YDL({'format': '[format_id$=cba]'})
307 ydl
.process_ie_result(info_dict
.copy())
308 downloaded
= ydl
.downloaded_info_dicts
[0]
309 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
311 # does not end with (!$=)
312 ydl
= YDL({'format': '[format_id!$=cba]'})
313 ydl
.process_ie_result(info_dict
.copy())
314 downloaded
= ydl
.downloaded_info_dicts
[0]
315 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
317 ydl
= YDL({'format': '[format_id!$=cba][format_id!$=cxz]'})
318 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
321 ydl
= YDL({'format': '[format_id*=bc-cb]'})
322 ydl
.process_ie_result(info_dict
.copy())
323 downloaded
= ydl
.downloaded_info_dicts
[0]
324 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
326 # does not contain (!*=)
327 ydl
= YDL({'format': '[format_id!*=bc-cb]'})
328 ydl
.process_ie_result(info_dict
.copy())
329 downloaded
= ydl
.downloaded_info_dicts
[0]
330 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
332 ydl
= YDL({'format': '[format_id!*=abc][format_id!*=zxc]'})
333 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
335 ydl
= YDL({'format': '[format_id!*=-]'})
336 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
338 def test_youtube_format_selection(self
):
339 # FIXME: Rewrite in accordance with the new format sorting options
343 '38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13',
344 # Apple HTTP Live Streaming
345 '96', '95', '94', '93', '92', '132', '151',
347 '85', '84', '102', '83', '101', '82', '100',
349 '137', '248', '136', '247', '135', '246',
350 '245', '244', '134', '243', '133', '242', '160',
352 '141', '172', '140', '171', '139',
355 def format_info(f_id
):
356 info
= YoutubeIE
._formats
[f_id
].copy()
358 # XXX: In real cases InfoExtractor._parse_mpd_formats() fills up 'acodec'
359 # and 'vcodec', while in tests such information is incomplete since
360 # commit a6c2c24479e5f4827ceb06f64d855329c0a6f593
361 # test_YoutubeDL.test_youtube_format_selection is broken without
363 if 'acodec' in info
and 'vcodec' not in info
:
364 info
['vcodec'] = 'none'
365 elif 'vcodec' in info
and 'acodec' not in info
:
366 info
['acodec'] = 'none'
368 info
['format_id'] = f_id
369 info
['url'] = 'url:' + f_id
371 formats_order
= [format_info(f_id
) for f_id
in order
]
373 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
374 ydl
= YDL({'format': 'bestvideo+bestaudio'})
375 ydl
.sort_formats(info_dict
)
376 ydl
.process_ie_result(info_dict
)
377 downloaded
= ydl
.downloaded_info_dicts
[0]
378 self
.assertEqual(downloaded
['format_id'], '248+172')
379 self
.assertEqual(downloaded
['ext'], 'mp4')
381 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
382 ydl
= YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'})
383 ydl
.sort_formats(info_dict
)
384 ydl
.process_ie_result(info_dict
)
385 downloaded
= ydl
.downloaded_info_dicts
[0]
386 self
.assertEqual(downloaded
['format_id'], '38')
388 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
389 ydl
= YDL({'format': 'bestvideo/best,bestaudio'})
390 ydl
.sort_formats(info_dict
)
391 ydl
.process_ie_result(info_dict
)
392 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
393 self
.assertEqual(downloaded_ids
, ['137', '141'])
395 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
396 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'})
397 ydl
.sort_formats(info_dict
)
398 ydl
.process_ie_result(info_dict
)
399 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
400 self
.assertEqual(downloaded_ids
, ['137+141', '248+141'])
402 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
403 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'})
404 ydl
.sort_formats(info_dict
)
405 ydl
.process_ie_result(info_dict
)
406 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
407 self
.assertEqual(downloaded_ids
, ['136+141', '247+141'])
409 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
410 ydl
= YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'})
411 ydl
.sort_formats(info_dict
)
412 ydl
.process_ie_result(info_dict
)
413 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
414 self
.assertEqual(downloaded_ids
, ['248+141'])
416 for f1
, f2
in zip(formats_order
, formats_order
[1:]):
417 info_dict
= _make_result([f1
, f2
], extractor
='youtube')
418 ydl
= YDL({'format': 'best/bestvideo'})
419 ydl
.sort_formats(info_dict
)
420 ydl
.process_ie_result(info_dict
)
421 downloaded
= ydl
.downloaded_info_dicts
[0]
422 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
424 info_dict
= _make_result([f2
, f1
], extractor
='youtube')
425 ydl
= YDL({'format': 'best/bestvideo'})
426 ydl
.sort_formats(info_dict
)
427 ydl
.process_ie_result(info_dict
)
428 downloaded
= ydl
.downloaded_info_dicts
[0]
429 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
431 def test_audio_only_extractor_format_selection(self
):
432 # For extractors with incomplete formats (all formats are audio-only or
433 # video-only) best and worst should fallback to corresponding best/worst
434 # video-only or audio-only formats (as per
435 # https://github.com/ytdl-org/youtube-dl/pull/5556)
437 {'format_id': 'low', 'ext': 'mp3', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL
},
438 {'format_id': 'high', 'ext': 'mp3', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL
},
440 info_dict
= _make_result(formats
)
442 ydl
= YDL({'format': 'best'})
443 ydl
.process_ie_result(info_dict
.copy())
444 downloaded
= ydl
.downloaded_info_dicts
[0]
445 self
.assertEqual(downloaded
['format_id'], 'high')
447 ydl
= YDL({'format': 'worst'})
448 ydl
.process_ie_result(info_dict
.copy())
449 downloaded
= ydl
.downloaded_info_dicts
[0]
450 self
.assertEqual(downloaded
['format_id'], 'low')
452 def test_format_not_available(self
):
454 {'format_id': 'regular', 'ext': 'mp4', 'height': 360, 'url': TEST_URL
},
455 {'format_id': 'video', 'ext': 'mp4', 'height': 720, 'acodec': 'none', 'url': TEST_URL
},
457 info_dict
= _make_result(formats
)
459 # This must fail since complete video-audio format does not match filter
460 # and extractor does not provide incomplete only formats (i.e. only
461 # video-only or audio-only).
462 ydl
= YDL({'format': 'best[height>360]'})
463 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
465 def test_format_selection_issue_10083(self
):
466 # See https://github.com/ytdl-org/youtube-dl/issues/10083
468 {'format_id': 'regular', 'height': 360, 'url': TEST_URL
},
469 {'format_id': 'video', 'height': 720, 'acodec': 'none', 'url': TEST_URL
},
470 {'format_id': 'audio', 'vcodec': 'none', 'url': TEST_URL
},
472 info_dict
= _make_result(formats
)
474 ydl
= YDL({'format': 'best[height>360]/bestvideo[height>360]+bestaudio'})
475 ydl
.process_ie_result(info_dict
.copy())
476 self
.assertEqual(ydl
.downloaded_info_dicts
[0]['format_id'], 'video+audio')
478 def test_invalid_format_specs(self
):
479 def assert_syntax_error(format_spec
):
480 self
.assertRaises(SyntaxError, YDL
, {'format': format_spec
})
482 assert_syntax_error('bestvideo,,best')
483 assert_syntax_error('+bestaudio')
484 assert_syntax_error('bestvideo+')
485 assert_syntax_error('/')
486 assert_syntax_error('[720<height]')
488 def test_format_filtering(self
):
490 {'format_id': 'A', 'filesize': 500, 'width': 1000},
491 {'format_id': 'B', 'filesize': 1000, 'width': 500},
492 {'format_id': 'C', 'filesize': 1000, 'width': 400},
493 {'format_id': 'D', 'filesize': 2000, 'width': 600},
494 {'format_id': 'E', 'filesize': 3000},
496 {'format_id': 'G', 'filesize': 1000000},
499 f
['url'] = 'http://_/'
501 info_dict
= _make_result(formats
, _format_sort_fields
=('id', ))
503 ydl
= YDL({'format': 'best[filesize<3000]'})
504 ydl
.process_ie_result(info_dict
)
505 downloaded
= ydl
.downloaded_info_dicts
[0]
506 self
.assertEqual(downloaded
['format_id'], 'D')
508 ydl
= YDL({'format': 'best[filesize<=3000]'})
509 ydl
.process_ie_result(info_dict
)
510 downloaded
= ydl
.downloaded_info_dicts
[0]
511 self
.assertEqual(downloaded
['format_id'], 'E')
513 ydl
= YDL({'format': 'best[filesize <= ? 3000]'})
514 ydl
.process_ie_result(info_dict
)
515 downloaded
= ydl
.downloaded_info_dicts
[0]
516 self
.assertEqual(downloaded
['format_id'], 'F')
518 ydl
= YDL({'format': 'best [filesize = 1000] [width>450]'})
519 ydl
.process_ie_result(info_dict
)
520 downloaded
= ydl
.downloaded_info_dicts
[0]
521 self
.assertEqual(downloaded
['format_id'], 'B')
523 ydl
= YDL({'format': 'best [filesize = 1000] [width!=450]'})
524 ydl
.process_ie_result(info_dict
)
525 downloaded
= ydl
.downloaded_info_dicts
[0]
526 self
.assertEqual(downloaded
['format_id'], 'C')
528 ydl
= YDL({'format': '[filesize>?1]'})
529 ydl
.process_ie_result(info_dict
)
530 downloaded
= ydl
.downloaded_info_dicts
[0]
531 self
.assertEqual(downloaded
['format_id'], 'G')
533 ydl
= YDL({'format': '[filesize<1M]'})
534 ydl
.process_ie_result(info_dict
)
535 downloaded
= ydl
.downloaded_info_dicts
[0]
536 self
.assertEqual(downloaded
['format_id'], 'E')
538 ydl
= YDL({'format': '[filesize<1MiB]'})
539 ydl
.process_ie_result(info_dict
)
540 downloaded
= ydl
.downloaded_info_dicts
[0]
541 self
.assertEqual(downloaded
['format_id'], 'G')
543 ydl
= YDL({'format': 'all[width>=400][width<=600]'})
544 ydl
.process_ie_result(info_dict
)
545 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
546 self
.assertEqual(downloaded_ids
, ['D', 'C', 'B'])
548 ydl
= YDL({'format': 'best[height<40]'})
549 with contextlib
.suppress(ExtractorError
):
550 ydl
.process_ie_result(info_dict
)
551 self
.assertEqual(ydl
.downloaded_info_dicts
, [])
553 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', False)
554 def test_default_format_spec_without_ffmpeg(self
):
556 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
558 ydl
= YDL({'simulate': True})
559 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
562 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
564 ydl
= YDL({'simulate': True})
565 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
567 ydl
= YDL({'outtmpl': '-'})
568 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
571 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
572 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
574 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', True)
575 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.can_merge', lambda _
: True)
576 def test_default_format_spec_with_ffmpeg(self
):
578 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
580 ydl
= YDL({'simulate': True})
581 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
584 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
586 ydl
= YDL({'simulate': True})
587 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
589 ydl
= YDL({'outtmpl': '-'})
590 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
593 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
594 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
597 class TestYoutubeDL(unittest
.TestCase
):
598 def test_subtitles(self
):
599 def s_formats(lang
, autocaption
=False):
602 'url': f
'http://localhost/video.{lang}.{ext}',
603 '_auto': autocaption
,
604 } for ext
in ['vtt', 'srt', 'ass']]
605 subtitles
= {l
: s_formats(l
) for l
in ['en', 'fr', 'es']}
606 auto_captions
= {l
: s_formats(l
, True) for l
in ['it', 'pt', 'es']}
610 'url': 'http://localhost/video.mp4',
611 'subtitles': subtitles
,
612 'automatic_captions': auto_captions
,
614 'webpage_url': 'http://example.com/watch?v=shenanigans',
617 def get_info(params
={}):
618 params
.setdefault('simulate', True)
620 ydl
.report_warning
= lambda *args
, **kargs
: None
621 return ydl
.process_video_result(info_dict
, download
=False)
624 self
.assertFalse(result
.get('requested_subtitles'))
625 self
.assertEqual(result
['subtitles'], subtitles
)
626 self
.assertEqual(result
['automatic_captions'], auto_captions
)
628 result
= get_info({'writesubtitles': True})
629 subs
= result
['requested_subtitles']
630 self
.assertTrue(subs
)
631 self
.assertEqual(set(subs
.keys()), {'en'})
632 self
.assertTrue(subs
['en'].get('data') is None)
633 self
.assertEqual(subs
['en']['ext'], 'ass')
635 result
= get_info({'writesubtitles': True, 'subtitlesformat': 'foo/srt'})
636 subs
= result
['requested_subtitles']
637 self
.assertEqual(subs
['en']['ext'], 'srt')
639 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['es', 'fr', 'it']})
640 subs
= result
['requested_subtitles']
641 self
.assertTrue(subs
)
642 self
.assertEqual(set(subs
.keys()), {'es', 'fr'})
644 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['all', '-en']})
645 subs
= result
['requested_subtitles']
646 self
.assertTrue(subs
)
647 self
.assertEqual(set(subs
.keys()), {'es', 'fr'})
649 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['en', 'fr', '-en']})
650 subs
= result
['requested_subtitles']
651 self
.assertTrue(subs
)
652 self
.assertEqual(set(subs
.keys()), {'fr'})
654 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['-en', 'en']})
655 subs
= result
['requested_subtitles']
656 self
.assertTrue(subs
)
657 self
.assertEqual(set(subs
.keys()), {'en'})
659 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['e.+']})
660 subs
= result
['requested_subtitles']
661 self
.assertTrue(subs
)
662 self
.assertEqual(set(subs
.keys()), {'es', 'en'})
664 result
= get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
665 subs
= result
['requested_subtitles']
666 self
.assertTrue(subs
)
667 self
.assertEqual(set(subs
.keys()), {'es', 'pt'})
668 self
.assertFalse(subs
['es']['_auto'])
669 self
.assertTrue(subs
['pt']['_auto'])
671 result
= get_info({'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
672 subs
= result
['requested_subtitles']
673 self
.assertTrue(subs
)
674 self
.assertEqual(set(subs
.keys()), {'es', 'pt'})
675 self
.assertTrue(subs
['es']['_auto'])
676 self
.assertTrue(subs
['pt']['_auto'])
678 def test_add_extra_info(self
):
684 'playlist': 'funny videos',
686 YDL
.add_extra_info(test_dict
, extra_info
)
687 self
.assertEqual(test_dict
['extractor'], 'Foo')
688 self
.assertEqual(test_dict
['playlist'], 'funny videos')
698 'title3': 'foo/bar\\test',
699 'title4': 'foo "bar" test',
701 'timestamp': 1618488000,
704 'playlist_autonumber': 2,
705 '__last_playlist_index': 100,
708 {'id': 'id 1', 'height': 1080, 'width': 1920},
709 {'id': 'id 2', 'height': 720},
714 def test_prepare_outtmpl_and_filename(self
):
715 def test(tmpl
, expected
, *, info
=None, **params
):
716 params
['outtmpl'] = tmpl
717 ydl
= FakeYDL(params
)
718 ydl
._num
_downloads
= 1
719 self
.assertEqual(ydl
.validate_outtmpl(tmpl
), None)
721 out
= ydl
.evaluate_outtmpl(tmpl
, info
or self
.outtmpl_info
)
722 fname
= ydl
.prepare_filename(info
or self
.outtmpl_info
)
724 if not isinstance(expected
, (list, tuple)):
725 expected
= (expected
, expected
)
726 for (name
, got
), expect
in zip((('outtmpl', out
), ('filename', fname
)), expected
):
728 self
.assertTrue(expect(got
), f
'Wrong {name} from {tmpl}')
729 elif expect
is not None:
730 self
.assertEqual(got
, expect
, f
'Wrong {name} from {tmpl}')
733 original_infodict
= dict(self
.outtmpl_info
)
734 test('foo.bar', 'foo.bar')
735 original_infodict
['epoch'] = self
.outtmpl_info
.get('epoch')
736 self
.assertTrue(isinstance(original_infodict
['epoch'], int))
737 test('%(epoch)d', int_or_none
)
738 self
.assertEqual(original_infodict
, self
.outtmpl_info
)
740 # Auto-generated fields
741 test('%(id)s.%(ext)s', '1234.mp4')
742 test('%(duration_string)s', ('27:46:40', '27-46-40'))
743 test('%(resolution)s', '1080p')
744 test('%(playlist_index|)s', '001')
745 test('%(playlist_index&{}!)s', '1!')
746 test('%(playlist_autonumber)s', '02')
747 test('%(autonumber)s', '00001')
748 test('%(autonumber+2)03d', '005', autonumber_start
=3)
749 test('%(autonumber)s', '001', autonumber_size
=3)
758 test('%abc%', '%abc%')
759 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
760 test('%%%(height)s', '%1080')
761 test('%(width)06d.%(ext)s', 'NA.mp4')
762 test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
763 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
766 test('%(id)s', '_abcd', info
={'id': '_abcd'})
767 test('%(some_id)s', '_abcd', info
={'some_id': '_abcd'})
768 test('%(formats.0.id)s', '_abcd', info
={'formats': [{'id': '_abcd'}]})
769 test('%(id)s', '-abcd', info
={'id': '-abcd'})
770 test('%(id)s', '.abcd', info
={'id': '.abcd'})
771 test('%(id)s', 'ab__cd', info
={'id': 'ab__cd'})
772 test('%(id)s', ('ab:cd', 'ab:cd'), info
={'id': 'ab:cd'})
773 test('%(id.0)s', '-', info
={'id': '--'})
776 self
.assertTrue(isinstance(YoutubeDL
.validate_outtmpl('%(title)'), ValueError))
777 test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder
='none')
779 test('%(formats.{id)s', 'NA')
782 def expect_same_infodict(out
):
783 got_dict
= json
.loads(out
)
784 for info_field
, expected
in self
.outtmpl_info
.items():
785 self
.assertEqual(got_dict
.get(info_field
), expected
, info_field
)
788 test('%()j', (expect_same_infodict
, None))
791 NA_TEST_OUTTMPL
= '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
792 test(NA_TEST_OUTTMPL
, 'NA-NA-def-1234.mp4')
793 test(NA_TEST_OUTTMPL
, 'none-none-def-1234.mp4', outtmpl_na_placeholder
='none')
794 test(NA_TEST_OUTTMPL
, '--def-1234.mp4', outtmpl_na_placeholder
='')
795 test('%(non_existent.0)s', 'NA')
798 FMT_TEST_OUTTMPL
= '%%(height)%s.%%(ext)s'
799 test(FMT_TEST_OUTTMPL
% 's', '1080.mp4')
800 test(FMT_TEST_OUTTMPL
% 'd', '1080.mp4')
801 test(FMT_TEST_OUTTMPL
% '6d', ' 1080.mp4')
802 test(FMT_TEST_OUTTMPL
% '-6d', '1080 .mp4')
803 test(FMT_TEST_OUTTMPL
% '06d', '001080.mp4')
804 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
805 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
806 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
807 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
808 test(FMT_TEST_OUTTMPL
% ' 0 6d', ' 01080.mp4')
811 test('%(id)d', '1234')
812 test('%(height)c', '1')
814 test('%(id)d %(id)r', "1234 '1234'")
815 test('%(id)r %(height)r', "'1234' 1080")
816 test('%(title5)a %(height)a', (R
"'\xe1\xe9\xed \U0001d400' 1080", None))
817 test('%(ext)s-%(ext|def)d', 'mp4-def')
818 test('%(width|0)04d', '0')
819 test('a%(width|b)d', 'ab', outtmpl_na_placeholder
='none')
821 FORMATS
= self
.outtmpl_info
['formats']
823 # Custom type casting
824 test('%(formats.:.id)l', 'id 1, id 2, id 3')
825 test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
826 test('%(ext)l', 'mp4')
827 test('%(formats.:.id) 18l', ' id 1, id 2, id 3')
828 test('%(formats)j', (json
.dumps(FORMATS
), None))
829 test('%(formats)#j', (
830 json
.dumps(FORMATS
, indent
=4),
831 json
.dumps(FORMATS
, indent
=4).replace(':', ':').replace('"', '"').replace('\n', ' '),
833 test('%(title5).3B', 'á')
834 test('%(title5)U', 'áéí 𝐀')
835 test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
836 test('%(title5)+U', 'áéí A')
837 test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
838 test('%(height)D', '1k')
839 test('%(filesize)#D', '1Ki')
840 test('%(height)5.2D', ' 1.08k')
841 test('%(title4)#S', 'foo_bar_test')
842 test('%(title4).10S', ('foo "bar" ', 'foo "bar"' + ('#' if compat_os_name
== 'nt' else ' ')))
843 if compat_os_name
== 'nt':
844 test('%(title4)q', ('"foo ""bar"" test"', None))
845 test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', None))
846 test('%(formats.0.id)#q', ('"id 1"', None))
848 test('%(title4)q', ('\'foo "bar" test\'', '\'foo "bar" test\''))
849 test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
850 test('%(formats.0.id)#q', "'id 1'")
852 # Internal formatting
853 test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
854 test('%(title|%)s %(title|%%)s', '% %%')
855 test('%(id+1-height+3)05d', '00158')
856 test('%(width+100)05d', 'NA')
857 test('%(filesize*8)d', '8192')
858 test('%(formats.0) 15s', ('% 15s' % FORMATS
[0], None))
859 test('%(formats.0)r', (repr(FORMATS
[0]), None))
860 test('%(height.0)03d', '001')
861 test('%(-height.0)04d', '-001')
862 test('%(formats.-1.id)s', FORMATS
[-1]['id'])
863 test('%(formats.0.id.-1)d', FORMATS
[0]['id'][-1])
864 test('%(formats.3)s', 'NA')
865 test('%(formats.:2:-1)r', repr(FORMATS
[:2:-1]))
866 test('%(formats.0.id.-1+id)f', '1235.000000')
867 test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
868 out
= json
.dumps([{'id': f
['id'], 'height.:2': str(f
['height'])[:2]}
869 if 'height' in f
else {'id': f
['id']}
871 test('%(formats.:.{id,height.:2})j', (out
, None))
872 test('%(formats.:.{id,height}.id)l', ', '.join(f
['id'] for f
in FORMATS
))
873 test('%(.{id,title})j', ('{"id": "1234"}', '{"id": "1234"}'))
876 test('%(title,id)s', '1234')
877 test('%(width-100,height+20|def)d', '1100')
878 test('%(width-100,height+width|def)s', 'def')
879 test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
882 test('%(id&foo)s.bar', 'foo.bar')
883 test('%(title&foo)s.bar', 'NA.bar')
884 test('%(title&foo|baz)s.bar', 'baz.bar')
885 test('%(x,id&foo|baz)s.bar', 'foo.bar')
886 test('%(x,title&foo|baz)s.bar', 'baz.bar')
887 test('%(id&a\nb|)s', ('a\nb', 'a b'))
888 test('%(id&hi {:>10} {}|)s', 'hi 1234 1234')
889 test(R
'%(id&{0} {}|)s', 'NA')
890 test(R
'%(id&{0.1}|)s', 'NA')
891 test('%(height&{:,d})S', '1,080')
896 raise self
.assertTrue(False, 'LazyList should not be evaluated till here')
897 test('%(key.4)s', '4', info
={'key': LazyList(gen())})
900 test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
901 # test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # FIXME: ?
902 # test('%(foo|)s', ('', '_')) # FIXME: ?
904 # Environment variable expansion for prepare_filename
905 os
.environ
['__yt_dlp_var'] = 'expanded'
906 envvar
= '%__yt_dlp_var%' if compat_os_name
== 'nt' else '$__yt_dlp_var'
907 test(envvar
, (envvar
, 'expanded'))
908 if compat_os_name
== 'nt':
909 test('%s%', ('%s%', '%s%'))
910 os
.environ
['s'] = 'expanded'
911 test('%s%', ('%s%', 'expanded')) # %s% should be expanded before escaping %s
912 os
.environ
['(test)s'] = 'expanded'
913 test('%(test)s%', ('NA%', 'expanded')) # Environment should take priority over template
915 # Path expansion and escaping
916 test('Hello %(title1)s', 'Hello $PATH')
917 test('Hello %(title2)s', 'Hello %PATH%')
918 test('%(title3)s', ('foo/bar\\test', 'foo⧸bar⧹test'))
919 test('folder/%(title3)s', ('folder/foo/bar\\test', f
'folder{os.path.sep}foo⧸bar⧹test'))
921 def test_format_note(self
):
923 self
.assertEqual(ydl
._format
_note
({}), '')
924 assertRegexpMatches(self
, ydl
._format
_note
({
927 assertRegexpMatches(self
, ydl
._format
_note
({
931 def test_postprocessors(self
):
932 filename
= 'post-processor-testfile.mp4'
933 audiofile
= filename
+ '.mp3'
935 class SimplePP(PostProcessor
):
937 with
open(audiofile
, 'w') as f
:
939 return [info
['filepath']], info
941 def run_pp(params
, pp
):
942 with
open(filename
, 'w') as f
:
944 ydl
= YoutubeDL(params
)
945 ydl
.add_post_processor(pp())
946 ydl
.post_process(filename
, {'filepath': filename
})
948 run_pp({'keepvideo': True}, SimplePP
)
949 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
950 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
954 run_pp({'keepvideo': False}, SimplePP
)
955 self
.assertFalse(os
.path
.exists(filename
), f
'{filename} exists')
956 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
959 class ModifierPP(PostProcessor
):
961 with
open(info
['filepath'], 'w') as f
:
965 run_pp({'keepvideo': False}, ModifierPP
)
966 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
969 def test_match_filter(self
):
976 'filesize': 10 * 1024,
978 'uploader': '變態妍字幕版 太妍 тест',
979 'creator': "тест ' 123 ' тест--",
980 'webpage_url': 'http://example.com/watch?v=shenanigans',
988 'description': 'foo',
989 'filesize': 5 * 1024,
991 'uploader': 'тест 123',
992 'webpage_url': 'http://example.com/watch?v=SHENANIGANS',
994 videos
= [first
, second
]
996 def get_videos(filter_
=None):
997 ydl
= YDL({'match_filter': filter_
, 'simulate': True})
999 ydl
.process_ie_result(v
.copy(), download
=True)
1000 return [v
['id'] for v
in ydl
.downloaded_info_dicts
]
1003 self
.assertEqual(res
, ['1', '2'])
1005 def f(v
, incomplete
):
1009 return 'Video id is not 1'
1011 self
.assertEqual(res
, ['1'])
1013 f
= match_filter_func('duration < 30')
1015 self
.assertEqual(res
, ['2'])
1017 f
= match_filter_func('description = foo')
1019 self
.assertEqual(res
, ['2'])
1021 f
= match_filter_func('description =? foo')
1023 self
.assertEqual(res
, ['1', '2'])
1025 f
= match_filter_func('filesize > 5KiB')
1027 self
.assertEqual(res
, ['1'])
1029 f
= match_filter_func('playlist_id = 42')
1031 self
.assertEqual(res
, ['1'])
1033 f
= match_filter_func('uploader = "變態妍字幕版 太妍 тест"')
1035 self
.assertEqual(res
, ['1'])
1037 f
= match_filter_func('uploader != "變態妍字幕版 太妍 тест"')
1039 self
.assertEqual(res
, ['2'])
1041 f
= match_filter_func('creator = "тест \' 123 \' тест--"')
1043 self
.assertEqual(res
, ['1'])
1045 f
= match_filter_func("creator = 'тест \\' 123 \\' тест--'")
1047 self
.assertEqual(res
, ['1'])
1049 f
= match_filter_func(r
"creator = 'тест \' 123 \' тест--' & duration > 30")
1051 self
.assertEqual(res
, [])
1053 def test_playlist_items_selection(self
):
1054 INDICES
, PAGE_SIZE
= list(range(1, 11)), 3
1056 def entry(i
, evaluated
):
1064 def pagedlist_entries(evaluated
):
1066 start
= PAGE_SIZE
* n
1067 for i
in INDICES
[start
: start
+ PAGE_SIZE
]:
1068 yield entry(i
, evaluated
)
1069 return OnDemandPagedList(page_func
, PAGE_SIZE
)
1072 return (i
+ PAGE_SIZE
- 1) // PAGE_SIZE
1074 def generator_entries(evaluated
):
1076 yield entry(i
, evaluated
)
1078 def list_entries(evaluated
):
1079 return list(generator_entries(evaluated
))
1081 def lazylist_entries(evaluated
):
1082 return LazyList(generator_entries(evaluated
))
1084 def get_downloaded_info_dicts(params
, entries
):
1086 ydl
.process_ie_result({
1087 '_type': 'playlist',
1089 'extractor': 'test:playlist',
1090 'extractor_key': 'test:playlist',
1091 'webpage_url': 'http://example.com',
1094 return ydl
.downloaded_info_dicts
1096 def test_selection(params
, expected_ids
, evaluate_all
=False):
1097 expected_ids
= list(expected_ids
)
1099 generator_eval
= pagedlist_eval
= INDICES
1100 elif not expected_ids
:
1101 generator_eval
= pagedlist_eval
= []
1103 generator_eval
= INDICES
[0: max(expected_ids
)]
1104 pagedlist_eval
= INDICES
[PAGE_SIZE
* page_num(min(expected_ids
)) - PAGE_SIZE
:
1105 PAGE_SIZE
* page_num(max(expected_ids
))]
1107 for name
, func
, expected_eval
in (
1108 ('list', list_entries
, INDICES
),
1109 ('Generator', generator_entries
, generator_eval
),
1110 # ('LazyList', lazylist_entries, generator_eval), # Generator and LazyList follow the exact same code path
1111 ('PagedList', pagedlist_entries
, pagedlist_eval
),
1114 entries
= func(evaluated
)
1115 results
= [(v
['playlist_autonumber'] - 1, (int(v
['id']), v
['playlist_index']))
1116 for v
in get_downloaded_info_dicts(params
, entries
)]
1117 self
.assertEqual(results
, list(enumerate(zip(expected_ids
, expected_ids
))), f
'Entries of {name} for {params}')
1118 self
.assertEqual(sorted(evaluated
), expected_eval
, f
'Evaluation of {name} for {params}')
1120 test_selection({}, INDICES
)
1121 test_selection({'playlistend': 20}, INDICES
, True)
1122 test_selection({'playlistend': 2}, INDICES
[:2])
1123 test_selection({'playliststart': 11}, [], True)
1124 test_selection({'playliststart': 2}, INDICES
[1:])
1125 test_selection({'playlist_items': '2-4'}, INDICES
[1:4])
1126 test_selection({'playlist_items': '2,4'}, [2, 4])
1127 test_selection({'playlist_items': '20'}, [], True)
1128 test_selection({'playlist_items': '0'}, [])
1130 # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
1131 test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
1132 test_selection({'playlist_items': '4,2'}, [4, 2])
1134 # Tests for https://github.com/yt-dlp/yt-dlp/issues/720
1135 # https://github.com/yt-dlp/yt-dlp/issues/302
1136 test_selection({'playlistreverse': True}, INDICES
[::-1])
1137 test_selection({'playliststart': 2, 'playlistreverse': True}, INDICES
[:0:-1])
1138 test_selection({'playlist_items': '2,4', 'playlistreverse': True}, [4, 2])
1139 test_selection({'playlist_items': '4,2'}, [4, 2])
1141 # Tests for --playlist-items start:end:step
1142 test_selection({'playlist_items': ':'}, INDICES
, True)
1143 test_selection({'playlist_items': '::1'}, INDICES
, True)
1144 test_selection({'playlist_items': '::-1'}, INDICES
[::-1], True)
1145 test_selection({'playlist_items': ':6'}, INDICES
[:6])
1146 test_selection({'playlist_items': ':-6'}, INDICES
[:-5], True)
1147 test_selection({'playlist_items': '-1:6:-2'}, INDICES
[:4:-2], True)
1148 test_selection({'playlist_items': '9:-6:-2'}, INDICES
[8:3:-2], True)
1150 test_selection({'playlist_items': '1:inf:2'}, INDICES
[::2], True)
1151 test_selection({'playlist_items': '-2:inf'}, INDICES
[-2:], True)
1152 test_selection({'playlist_items': ':inf:-1'}, [], True)
1153 test_selection({'playlist_items': '0-2:2'}, [2])
1154 test_selection({'playlist_items': '1-:2'}, INDICES
[::2], True)
1155 test_selection({'playlist_items': '0--2:2'}, INDICES
[1:-1:2], True)
1157 test_selection({'playlist_items': '10::3'}, [10], True)
1158 test_selection({'playlist_items': '-1::3'}, [10], True)
1159 test_selection({'playlist_items': '11::3'}, [], True)
1160 test_selection({'playlist_items': '-15::2'}, INDICES
[1::2], True)
1161 test_selection({'playlist_items': '-15::15'}, [], True)
1163 def test_do_not_override_ie_key_in_url_transparent(self
):
1166 class Foo1IE(InfoExtractor
):
1167 _VALID_URL
= r
'foo1:'
1169 def _real_extract(self
, url
):
1171 '_type': 'url_transparent',
1174 'title': 'foo1 title',
1178 class Foo2IE(InfoExtractor
):
1179 _VALID_URL
= r
'foo2:'
1181 def _real_extract(self
, url
):
1188 class Foo3IE(InfoExtractor
):
1189 _VALID_URL
= r
'foo3:'
1191 def _real_extract(self
, url
):
1192 return _make_result([{'url': TEST_URL
}], title
='foo3 title')
1194 ydl
.add_info_extractor(Foo1IE(ydl
))
1195 ydl
.add_info_extractor(Foo2IE(ydl
))
1196 ydl
.add_info_extractor(Foo3IE(ydl
))
1197 ydl
.extract_info('foo1:')
1198 downloaded
= ydl
.downloaded_info_dicts
[0]
1199 self
.assertEqual(downloaded
['url'], TEST_URL
)
1200 self
.assertEqual(downloaded
['title'], 'foo1 title')
1201 self
.assertEqual(downloaded
['id'], 'testid')
1202 self
.assertEqual(downloaded
['extractor'], 'testex')
1203 self
.assertEqual(downloaded
['extractor_key'], 'TestEx')
1205 # Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
1206 def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self
):
1209 def __init__(self
, *args
, **kwargs
):
1210 super().__init
__(*args
, **kwargs
)
1212 def trouble(self
, s
, tb
=None):
1217 'ignoreerrors': True,
1220 class VideoIE(InfoExtractor
):
1221 _VALID_URL
= r
'video:(?P<id>\d+)'
1223 def _real_extract(self
, url
):
1224 video_id
= self
._match
_id
(url
)
1226 'format_id': 'default',
1230 raise ExtractorError('foo')
1233 'format_id': 'extra',
1238 'title': f
'Video {video_id}',
1242 class PlaylistIE(InfoExtractor
):
1243 _VALID_URL
= r
'playlist:'
1249 '_type': 'url_transparent',
1250 'ie_key': VideoIE
.ie_key(),
1252 'url': f
'video:{video_id}',
1253 'title': f
'Video Transparent {video_id}',
1256 def _real_extract(self
, url
):
1257 return self
.playlist_result(self
._entries
())
1259 ydl
.add_info_extractor(VideoIE(ydl
))
1260 ydl
.add_info_extractor(PlaylistIE(ydl
))
1261 info
= ydl
.extract_info('playlist:')
1262 entries
= info
['entries']
1263 self
.assertEqual(len(entries
), 3)
1264 self
.assertTrue(entries
[0] is None)
1265 self
.assertTrue(entries
[1] is None)
1266 self
.assertEqual(len(ydl
.downloaded_info_dicts
), 1)
1267 downloaded
= ydl
.downloaded_info_dicts
[0]
1268 entries
[2].pop('requested_downloads', None)
1269 self
.assertEqual(entries
[2], downloaded
)
1270 self
.assertEqual(downloaded
['url'], TEST_URL
)
1271 self
.assertEqual(downloaded
['title'], 'Video Transparent 2')
1272 self
.assertEqual(downloaded
['id'], '2')
1273 self
.assertEqual(downloaded
['extractor'], 'Video')
1274 self
.assertEqual(downloaded
['extractor_key'], 'Video')
1276 def test_header_cookies(self
):
1277 from http
.cookiejar
import Cookie
1280 ydl
.report_warning
= lambda *_
, **__
: None
1282 def cookie(name
, value
, version
=None, domain
='', path
='', secure
=False, expires
=None):
1284 version
or 0, name
, value
, None, False,
1285 domain
, bool(domain
), bool(domain
), path
, bool(path
),
1286 secure
, expires
, False, None, None, rest
={})
1288 _test_url
= 'https://yt.dlp/test'
1290 def test(encoded_cookies
, cookies
, *, headers
=False, round_trip
=None, error_re
=None):
1292 ydl
.cookiejar
.clear()
1293 ydl
._load
_cookies
(encoded_cookies
, autoscope
=headers
)
1295 ydl
._apply
_header
_cookies
(_test_url
)
1296 data
= {'url': _test_url
}
1297 ydl
._calc
_headers
(data
)
1298 self
.assertCountEqual(
1299 map(vars, ydl
.cookiejar
), map(vars, cookies
),
1300 'Extracted cookiejar.Cookie is not the same')
1303 data
.get('cookies'), round_trip
or encoded_cookies
,
1304 'Cookie is not the same as round trip')
1305 ydl
.__dict
__['_YoutubeDL__header_cookies'] = []
1307 with self
.subTest(msg
=encoded_cookies
):
1311 with self
.assertRaisesRegex(Exception, error_re
):
1314 test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain
='.yt.dlp')])
1315 test('test=value', [cookie('test', 'value')], error_re
=r
'Unscoped cookies are not allowed')
1316 test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
1317 cookie('cookie1', 'value1', domain
='.yt.dlp', path
='/test'),
1318 cookie('cookie2', 'value2', domain
='.yt.dlp', path
='/')])
1319 test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
1320 cookie('test', 'value', domain
='.yt.dlp', path
='/test', secure
=True, expires
=9999999999)])
1321 test('test="value; "; path=/test; domain=.yt.dlp', [
1322 cookie('test', 'value; ', domain
='.yt.dlp', path
='/test')],
1323 round_trip
='test="value\\073 "; Domain=.yt.dlp; Path=/test')
1324 test('name=; Domain=.yt.dlp', [cookie('name', '', domain
='.yt.dlp')],
1325 round_trip
='name=""; Domain=.yt.dlp')
1327 test('test=value', [cookie('test', 'value', domain
='.yt.dlp')], headers
=True)
1328 test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers
=True, error_re
=r
'Invalid syntax')
1329 ydl
.deprecated_feature
= ydl
.report_error
1330 test('test=value', [], headers
=True, error_re
=r
'Passing cookies as a header is a potential security risk')
1332 def test_infojson_cookies(self
):
1333 TEST_FILE
= 'test_infojson_cookies.info.json'
1334 TEST_URL
= 'https://example.com/example.mp4'
1335 COOKIES
= 'a=b; Domain=.example.com; c=d; Domain=.example.com'
1336 COOKIE_HEADER
= {'Cookie': 'a=b; c=d'}
1339 ydl
.process_info
= lambda x
: ydl
._write
_info
_json
('test', x
, TEST_FILE
)
1341 def make_info(info_header_cookies
=False, fmts_header_cookies
=False, cookies_field
=False):
1342 fmt
= {'url': TEST_URL
}
1343 if fmts_header_cookies
:
1344 fmt
['http_headers'] = COOKIE_HEADER
1346 fmt
['cookies'] = COOKIES
1347 return _make_result([fmt
], http_headers
=COOKIE_HEADER
if info_header_cookies
else None)
1349 def test(initial_info
, note
):
1351 result
['processed'] = ydl
.process_ie_result(initial_info
)
1352 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1353 msg
=f
'No cookies set in cookiejar after initial process when {note}')
1354 ydl
.cookiejar
.clear()
1355 with
open(TEST_FILE
) as infojson
:
1356 result
['loaded'] = ydl
.sanitize_info(json
.load(infojson
), True)
1357 result
['final'] = ydl
.process_ie_result(result
['loaded'].copy(), download
=False)
1358 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1359 msg
=f
'No cookies set in cookiejar after final process when {note}')
1360 ydl
.cookiejar
.clear()
1361 for key
in ('processed', 'loaded', 'final'):
1364 traverse_obj(info
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False),
1365 msg
=f
'Cookie header not removed in {key} result when {note}')
1367 traverse_obj(info
, ((None, ('formats', 0)), 'cookies'), get_all
=False), COOKIES
,
1368 msg
=f
'No cookies field found in {key} result when {note}')
1370 test({'url': TEST_URL
, 'http_headers': COOKIE_HEADER
, 'id': '1', 'title': 'x'}, 'no formats field')
1371 test(make_info(info_header_cookies
=True), 'info_dict header cokies')
1372 test(make_info(fmts_header_cookies
=True), 'format header cookies')
1373 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True), 'info_dict and format header cookies')
1374 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True, cookies_field
=True), 'all cookies fields')
1375 test(make_info(cookies_field
=True), 'cookies format field')
1376 test({'url': TEST_URL
, 'cookies': COOKIES
, 'id': '1', 'title': 'x'}, 'info_dict cookies field only')
1380 def test_add_headers_cookie(self
):
1381 def check_for_cookie_header(result
):
1382 return traverse_obj(result
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False)
1384 ydl
= FakeYDL({'http_headers': {'Cookie': 'a=b'}})
1385 ydl
._apply
_header
_cookies
(_make_result([])['webpage_url']) # Scope to input webpage URL: .example.com
1387 fmt
= {'url': 'https://example.com/video.mp4'}
1388 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1389 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies in result info_dict')
1390 self
.assertEqual(result
.get('cookies'), 'a=b; Domain=.example.com', msg
='No cookies were set in cookies field')
1391 self
.assertIn('a=b', ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='No cookies were set in cookiejar')
1393 fmt
= {'url': 'https://wrong.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 for wrong domain')
1396 self
.assertFalse(result
.get('cookies'), msg
='Cookies set in cookies field for wrong domain')
1397 self
.assertFalse(ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='Cookies set in cookiejar for wrong domain')
1400 if __name__
== '__main__':