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_string_ops(self
):
241 {'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL
},
242 {'format_id': 'zxc-cxz', 'ext': 'webm', 'url': TEST_URL
},
244 info_dict
= _make_result(formats
)
247 ydl
= YDL({'format': '[format_id=abc-cba]'})
248 ydl
.process_ie_result(info_dict
.copy())
249 downloaded
= ydl
.downloaded_info_dicts
[0]
250 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
252 # does not equal (!=)
253 ydl
= YDL({'format': '[format_id!=abc-cba]'})
254 ydl
.process_ie_result(info_dict
.copy())
255 downloaded
= ydl
.downloaded_info_dicts
[0]
256 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
258 ydl
= YDL({'format': '[format_id!=abc-cba][format_id!=zxc-cxz]'})
259 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
262 ydl
= YDL({'format': '[format_id^=abc]'})
263 ydl
.process_ie_result(info_dict
.copy())
264 downloaded
= ydl
.downloaded_info_dicts
[0]
265 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
267 # does not start with (!^=)
268 ydl
= YDL({'format': '[format_id!^=abc]'})
269 ydl
.process_ie_result(info_dict
.copy())
270 downloaded
= ydl
.downloaded_info_dicts
[0]
271 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
273 ydl
= YDL({'format': '[format_id!^=abc][format_id!^=zxc]'})
274 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
277 ydl
= YDL({'format': '[format_id$=cba]'})
278 ydl
.process_ie_result(info_dict
.copy())
279 downloaded
= ydl
.downloaded_info_dicts
[0]
280 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
282 # does not end with (!$=)
283 ydl
= YDL({'format': '[format_id!$=cba]'})
284 ydl
.process_ie_result(info_dict
.copy())
285 downloaded
= ydl
.downloaded_info_dicts
[0]
286 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
288 ydl
= YDL({'format': '[format_id!$=cba][format_id!$=cxz]'})
289 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
292 ydl
= YDL({'format': '[format_id*=bc-cb]'})
293 ydl
.process_ie_result(info_dict
.copy())
294 downloaded
= ydl
.downloaded_info_dicts
[0]
295 self
.assertEqual(downloaded
['format_id'], 'abc-cba')
297 # does not contain (!*=)
298 ydl
= YDL({'format': '[format_id!*=bc-cb]'})
299 ydl
.process_ie_result(info_dict
.copy())
300 downloaded
= ydl
.downloaded_info_dicts
[0]
301 self
.assertEqual(downloaded
['format_id'], 'zxc-cxz')
303 ydl
= YDL({'format': '[format_id!*=abc][format_id!*=zxc]'})
304 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
306 ydl
= YDL({'format': '[format_id!*=-]'})
307 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
309 def test_youtube_format_selection(self
):
310 # FIXME: Rewrite in accordance with the new format sorting options
314 '38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13',
315 # Apple HTTP Live Streaming
316 '96', '95', '94', '93', '92', '132', '151',
318 '85', '84', '102', '83', '101', '82', '100',
320 '137', '248', '136', '247', '135', '246',
321 '245', '244', '134', '243', '133', '242', '160',
323 '141', '172', '140', '171', '139',
326 def format_info(f_id
):
327 info
= YoutubeIE
._formats
[f_id
].copy()
329 # XXX: In real cases InfoExtractor._parse_mpd_formats() fills up 'acodec'
330 # and 'vcodec', while in tests such information is incomplete since
331 # commit a6c2c24479e5f4827ceb06f64d855329c0a6f593
332 # test_YoutubeDL.test_youtube_format_selection is broken without
334 if 'acodec' in info
and 'vcodec' not in info
:
335 info
['vcodec'] = 'none'
336 elif 'vcodec' in info
and 'acodec' not in info
:
337 info
['acodec'] = 'none'
339 info
['format_id'] = f_id
340 info
['url'] = 'url:' + f_id
342 formats_order
= [format_info(f_id
) for f_id
in order
]
344 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
345 ydl
= YDL({'format': 'bestvideo+bestaudio'})
346 ydl
.sort_formats(info_dict
)
347 ydl
.process_ie_result(info_dict
)
348 downloaded
= ydl
.downloaded_info_dicts
[0]
349 self
.assertEqual(downloaded
['format_id'], '248+172')
350 self
.assertEqual(downloaded
['ext'], 'mp4')
352 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
353 ydl
= YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'})
354 ydl
.sort_formats(info_dict
)
355 ydl
.process_ie_result(info_dict
)
356 downloaded
= ydl
.downloaded_info_dicts
[0]
357 self
.assertEqual(downloaded
['format_id'], '38')
359 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
360 ydl
= YDL({'format': 'bestvideo/best,bestaudio'})
361 ydl
.sort_formats(info_dict
)
362 ydl
.process_ie_result(info_dict
)
363 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
364 self
.assertEqual(downloaded_ids
, ['137', '141'])
366 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
367 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'})
368 ydl
.sort_formats(info_dict
)
369 ydl
.process_ie_result(info_dict
)
370 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
371 self
.assertEqual(downloaded_ids
, ['137+141', '248+141'])
373 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
374 ydl
= YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'})
375 ydl
.sort_formats(info_dict
)
376 ydl
.process_ie_result(info_dict
)
377 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
378 self
.assertEqual(downloaded_ids
, ['136+141', '247+141'])
380 info_dict
= _make_result(list(formats_order
), extractor
='youtube')
381 ydl
= YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'})
382 ydl
.sort_formats(info_dict
)
383 ydl
.process_ie_result(info_dict
)
384 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
385 self
.assertEqual(downloaded_ids
, ['248+141'])
387 for f1
, f2
in zip(formats_order
, formats_order
[1:]):
388 info_dict
= _make_result([f1
, f2
], extractor
='youtube')
389 ydl
= YDL({'format': 'best/bestvideo'})
390 ydl
.sort_formats(info_dict
)
391 ydl
.process_ie_result(info_dict
)
392 downloaded
= ydl
.downloaded_info_dicts
[0]
393 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
395 info_dict
= _make_result([f2
, f1
], extractor
='youtube')
396 ydl
= YDL({'format': 'best/bestvideo'})
397 ydl
.sort_formats(info_dict
)
398 ydl
.process_ie_result(info_dict
)
399 downloaded
= ydl
.downloaded_info_dicts
[0]
400 self
.assertEqual(downloaded
['format_id'], f1
['format_id'])
402 def test_audio_only_extractor_format_selection(self
):
403 # For extractors with incomplete formats (all formats are audio-only or
404 # video-only) best and worst should fallback to corresponding best/worst
405 # video-only or audio-only formats (as per
406 # https://github.com/ytdl-org/youtube-dl/pull/5556)
408 {'format_id': 'low', 'ext': 'mp3', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL
},
409 {'format_id': 'high', 'ext': 'mp3', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL
},
411 info_dict
= _make_result(formats
)
413 ydl
= YDL({'format': 'best'})
414 ydl
.process_ie_result(info_dict
.copy())
415 downloaded
= ydl
.downloaded_info_dicts
[0]
416 self
.assertEqual(downloaded
['format_id'], 'high')
418 ydl
= YDL({'format': 'worst'})
419 ydl
.process_ie_result(info_dict
.copy())
420 downloaded
= ydl
.downloaded_info_dicts
[0]
421 self
.assertEqual(downloaded
['format_id'], 'low')
423 def test_format_not_available(self
):
425 {'format_id': 'regular', 'ext': 'mp4', 'height': 360, 'url': TEST_URL
},
426 {'format_id': 'video', 'ext': 'mp4', 'height': 720, 'acodec': 'none', 'url': TEST_URL
},
428 info_dict
= _make_result(formats
)
430 # This must fail since complete video-audio format does not match filter
431 # and extractor does not provide incomplete only formats (i.e. only
432 # video-only or audio-only).
433 ydl
= YDL({'format': 'best[height>360]'})
434 self
.assertRaises(ExtractorError
, ydl
.process_ie_result
, info_dict
.copy())
436 def test_format_selection_issue_10083(self
):
437 # See https://github.com/ytdl-org/youtube-dl/issues/10083
439 {'format_id': 'regular', 'height': 360, 'url': TEST_URL
},
440 {'format_id': 'video', 'height': 720, 'acodec': 'none', 'url': TEST_URL
},
441 {'format_id': 'audio', 'vcodec': 'none', 'url': TEST_URL
},
443 info_dict
= _make_result(formats
)
445 ydl
= YDL({'format': 'best[height>360]/bestvideo[height>360]+bestaudio'})
446 ydl
.process_ie_result(info_dict
.copy())
447 self
.assertEqual(ydl
.downloaded_info_dicts
[0]['format_id'], 'video+audio')
449 def test_invalid_format_specs(self
):
450 def assert_syntax_error(format_spec
):
451 self
.assertRaises(SyntaxError, YDL
, {'format': format_spec
})
453 assert_syntax_error('bestvideo,,best')
454 assert_syntax_error('+bestaudio')
455 assert_syntax_error('bestvideo+')
456 assert_syntax_error('/')
457 assert_syntax_error('[720<height]')
459 def test_format_filtering(self
):
461 {'format_id': 'A', 'filesize': 500, 'width': 1000},
462 {'format_id': 'B', 'filesize': 1000, 'width': 500},
463 {'format_id': 'C', 'filesize': 1000, 'width': 400},
464 {'format_id': 'D', 'filesize': 2000, 'width': 600},
465 {'format_id': 'E', 'filesize': 3000},
467 {'format_id': 'G', 'filesize': 1000000},
470 f
['url'] = 'http://_/'
472 info_dict
= _make_result(formats
, _format_sort_fields
=('id', ))
474 ydl
= YDL({'format': 'best[filesize<3000]'})
475 ydl
.process_ie_result(info_dict
)
476 downloaded
= ydl
.downloaded_info_dicts
[0]
477 self
.assertEqual(downloaded
['format_id'], 'D')
479 ydl
= YDL({'format': 'best[filesize<=3000]'})
480 ydl
.process_ie_result(info_dict
)
481 downloaded
= ydl
.downloaded_info_dicts
[0]
482 self
.assertEqual(downloaded
['format_id'], 'E')
484 ydl
= YDL({'format': 'best[filesize <= ? 3000]'})
485 ydl
.process_ie_result(info_dict
)
486 downloaded
= ydl
.downloaded_info_dicts
[0]
487 self
.assertEqual(downloaded
['format_id'], 'F')
489 ydl
= YDL({'format': 'best [filesize = 1000] [width>450]'})
490 ydl
.process_ie_result(info_dict
)
491 downloaded
= ydl
.downloaded_info_dicts
[0]
492 self
.assertEqual(downloaded
['format_id'], 'B')
494 ydl
= YDL({'format': 'best [filesize = 1000] [width!=450]'})
495 ydl
.process_ie_result(info_dict
)
496 downloaded
= ydl
.downloaded_info_dicts
[0]
497 self
.assertEqual(downloaded
['format_id'], 'C')
499 ydl
= YDL({'format': '[filesize>?1]'})
500 ydl
.process_ie_result(info_dict
)
501 downloaded
= ydl
.downloaded_info_dicts
[0]
502 self
.assertEqual(downloaded
['format_id'], 'G')
504 ydl
= YDL({'format': '[filesize<1M]'})
505 ydl
.process_ie_result(info_dict
)
506 downloaded
= ydl
.downloaded_info_dicts
[0]
507 self
.assertEqual(downloaded
['format_id'], 'E')
509 ydl
= YDL({'format': '[filesize<1MiB]'})
510 ydl
.process_ie_result(info_dict
)
511 downloaded
= ydl
.downloaded_info_dicts
[0]
512 self
.assertEqual(downloaded
['format_id'], 'G')
514 ydl
= YDL({'format': 'all[width>=400][width<=600]'})
515 ydl
.process_ie_result(info_dict
)
516 downloaded_ids
= [info
['format_id'] for info
in ydl
.downloaded_info_dicts
]
517 self
.assertEqual(downloaded_ids
, ['D', 'C', 'B'])
519 ydl
= YDL({'format': 'best[height<40]'})
520 with contextlib
.suppress(ExtractorError
):
521 ydl
.process_ie_result(info_dict
)
522 self
.assertEqual(ydl
.downloaded_info_dicts
, [])
524 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', False)
525 def test_default_format_spec_without_ffmpeg(self
):
527 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
529 ydl
= YDL({'simulate': True})
530 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
533 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
535 ydl
= YDL({'simulate': True})
536 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
538 ydl
= YDL({'outtmpl': '-'})
539 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
542 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
543 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
545 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', True)
546 @patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.can_merge', lambda _
: True)
547 def test_default_format_spec_with_ffmpeg(self
):
549 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
551 ydl
= YDL({'simulate': True})
552 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
555 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
557 ydl
= YDL({'simulate': True})
558 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
560 ydl
= YDL({'outtmpl': '-'})
561 self
.assertEqual(ydl
._default
_format
_spec
({}), 'best/bestvideo+bestaudio')
564 self
.assertEqual(ydl
._default
_format
_spec
({}), 'bestvideo*+bestaudio/best')
565 self
.assertEqual(ydl
._default
_format
_spec
({'is_live': True}), 'best/bestvideo+bestaudio')
568 class TestYoutubeDL(unittest
.TestCase
):
569 def test_subtitles(self
):
570 def s_formats(lang
, autocaption
=False):
573 'url': f
'http://localhost/video.{lang}.{ext}',
574 '_auto': autocaption
,
575 } for ext
in ['vtt', 'srt', 'ass']]
576 subtitles
= {l
: s_formats(l
) for l
in ['en', 'fr', 'es']}
577 auto_captions
= {l
: s_formats(l
, True) for l
in ['it', 'pt', 'es']}
581 'url': 'http://localhost/video.mp4',
582 'subtitles': subtitles
,
583 'automatic_captions': auto_captions
,
585 'webpage_url': 'http://example.com/watch?v=shenanigans',
588 def get_info(params
={}):
589 params
.setdefault('simulate', True)
591 ydl
.report_warning
= lambda *args
, **kargs
: None
592 return ydl
.process_video_result(info_dict
, download
=False)
595 self
.assertFalse(result
.get('requested_subtitles'))
596 self
.assertEqual(result
['subtitles'], subtitles
)
597 self
.assertEqual(result
['automatic_captions'], auto_captions
)
599 result
= get_info({'writesubtitles': True})
600 subs
= result
['requested_subtitles']
601 self
.assertTrue(subs
)
602 self
.assertEqual(set(subs
.keys()), {'en'})
603 self
.assertTrue(subs
['en'].get('data') is None)
604 self
.assertEqual(subs
['en']['ext'], 'ass')
606 result
= get_info({'writesubtitles': True, 'subtitlesformat': 'foo/srt'})
607 subs
= result
['requested_subtitles']
608 self
.assertEqual(subs
['en']['ext'], 'srt')
610 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['es', 'fr', 'it']})
611 subs
= result
['requested_subtitles']
612 self
.assertTrue(subs
)
613 self
.assertEqual(set(subs
.keys()), {'es', 'fr'})
615 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['all', '-en']})
616 subs
= result
['requested_subtitles']
617 self
.assertTrue(subs
)
618 self
.assertEqual(set(subs
.keys()), {'es', 'fr'})
620 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['en', 'fr', '-en']})
621 subs
= result
['requested_subtitles']
622 self
.assertTrue(subs
)
623 self
.assertEqual(set(subs
.keys()), {'fr'})
625 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['-en', 'en']})
626 subs
= result
['requested_subtitles']
627 self
.assertTrue(subs
)
628 self
.assertEqual(set(subs
.keys()), {'en'})
630 result
= get_info({'writesubtitles': True, 'subtitleslangs': ['e.+']})
631 subs
= result
['requested_subtitles']
632 self
.assertTrue(subs
)
633 self
.assertEqual(set(subs
.keys()), {'es', 'en'})
635 result
= get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
636 subs
= result
['requested_subtitles']
637 self
.assertTrue(subs
)
638 self
.assertEqual(set(subs
.keys()), {'es', 'pt'})
639 self
.assertFalse(subs
['es']['_auto'])
640 self
.assertTrue(subs
['pt']['_auto'])
642 result
= get_info({'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
643 subs
= result
['requested_subtitles']
644 self
.assertTrue(subs
)
645 self
.assertEqual(set(subs
.keys()), {'es', 'pt'})
646 self
.assertTrue(subs
['es']['_auto'])
647 self
.assertTrue(subs
['pt']['_auto'])
649 def test_add_extra_info(self
):
655 'playlist': 'funny videos',
657 YDL
.add_extra_info(test_dict
, extra_info
)
658 self
.assertEqual(test_dict
['extractor'], 'Foo')
659 self
.assertEqual(test_dict
['playlist'], 'funny videos')
669 'title3': 'foo/bar\\test',
670 'title4': 'foo "bar" test',
672 'timestamp': 1618488000,
675 'playlist_autonumber': 2,
676 '__last_playlist_index': 100,
679 {'id': 'id 1', 'height': 1080, 'width': 1920},
680 {'id': 'id 2', 'height': 720},
685 def test_prepare_outtmpl_and_filename(self
):
686 def test(tmpl
, expected
, *, info
=None, **params
):
687 params
['outtmpl'] = tmpl
688 ydl
= FakeYDL(params
)
689 ydl
._num
_downloads
= 1
690 self
.assertEqual(ydl
.validate_outtmpl(tmpl
), None)
692 out
= ydl
.evaluate_outtmpl(tmpl
, info
or self
.outtmpl_info
)
693 fname
= ydl
.prepare_filename(info
or self
.outtmpl_info
)
695 if not isinstance(expected
, (list, tuple)):
696 expected
= (expected
, expected
)
697 for (name
, got
), expect
in zip((('outtmpl', out
), ('filename', fname
)), expected
):
699 self
.assertTrue(expect(got
), f
'Wrong {name} from {tmpl}')
700 elif expect
is not None:
701 self
.assertEqual(got
, expect
, f
'Wrong {name} from {tmpl}')
704 original_infodict
= dict(self
.outtmpl_info
)
705 test('foo.bar', 'foo.bar')
706 original_infodict
['epoch'] = self
.outtmpl_info
.get('epoch')
707 self
.assertTrue(isinstance(original_infodict
['epoch'], int))
708 test('%(epoch)d', int_or_none
)
709 self
.assertEqual(original_infodict
, self
.outtmpl_info
)
711 # Auto-generated fields
712 test('%(id)s.%(ext)s', '1234.mp4')
713 test('%(duration_string)s', ('27:46:40', '27-46-40'))
714 test('%(resolution)s', '1080p')
715 test('%(playlist_index|)s', '001')
716 test('%(playlist_index&{}!)s', '1!')
717 test('%(playlist_autonumber)s', '02')
718 test('%(autonumber)s', '00001')
719 test('%(autonumber+2)03d', '005', autonumber_start
=3)
720 test('%(autonumber)s', '001', autonumber_size
=3)
729 test('%abc%', '%abc%')
730 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
731 test('%%%(height)s', '%1080')
732 test('%(width)06d.%(ext)s', 'NA.mp4')
733 test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
734 test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
737 test('%(id)s', '_abcd', info
={'id': '_abcd'})
738 test('%(some_id)s', '_abcd', info
={'some_id': '_abcd'})
739 test('%(formats.0.id)s', '_abcd', info
={'formats': [{'id': '_abcd'}]})
740 test('%(id)s', '-abcd', info
={'id': '-abcd'})
741 test('%(id)s', '.abcd', info
={'id': '.abcd'})
742 test('%(id)s', 'ab__cd', info
={'id': 'ab__cd'})
743 test('%(id)s', ('ab:cd', 'ab:cd'), info
={'id': 'ab:cd'})
744 test('%(id.0)s', '-', info
={'id': '--'})
747 self
.assertTrue(isinstance(YoutubeDL
.validate_outtmpl('%(title)'), ValueError))
748 test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder
='none')
750 test('%(formats.{id)s', 'NA')
753 def expect_same_infodict(out
):
754 got_dict
= json
.loads(out
)
755 for info_field
, expected
in self
.outtmpl_info
.items():
756 self
.assertEqual(got_dict
.get(info_field
), expected
, info_field
)
759 test('%()j', (expect_same_infodict
, None))
762 NA_TEST_OUTTMPL
= '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
763 test(NA_TEST_OUTTMPL
, 'NA-NA-def-1234.mp4')
764 test(NA_TEST_OUTTMPL
, 'none-none-def-1234.mp4', outtmpl_na_placeholder
='none')
765 test(NA_TEST_OUTTMPL
, '--def-1234.mp4', outtmpl_na_placeholder
='')
766 test('%(non_existent.0)s', 'NA')
769 FMT_TEST_OUTTMPL
= '%%(height)%s.%%(ext)s'
770 test(FMT_TEST_OUTTMPL
% 's', '1080.mp4')
771 test(FMT_TEST_OUTTMPL
% 'd', '1080.mp4')
772 test(FMT_TEST_OUTTMPL
% '6d', ' 1080.mp4')
773 test(FMT_TEST_OUTTMPL
% '-6d', '1080 .mp4')
774 test(FMT_TEST_OUTTMPL
% '06d', '001080.mp4')
775 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
776 test(FMT_TEST_OUTTMPL
% ' 06d', ' 01080.mp4')
777 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
778 test(FMT_TEST_OUTTMPL
% '0 6d', ' 01080.mp4')
779 test(FMT_TEST_OUTTMPL
% ' 0 6d', ' 01080.mp4')
782 test('%(id)d', '1234')
783 test('%(height)c', '1')
785 test('%(id)d %(id)r', "1234 '1234'")
786 test('%(id)r %(height)r', "'1234' 1080")
787 test('%(title5)a %(height)a', (R
"'\xe1\xe9\xed \U0001d400' 1080", None))
788 test('%(ext)s-%(ext|def)d', 'mp4-def')
789 test('%(width|0)04d', '0')
790 test('a%(width|b)d', 'ab', outtmpl_na_placeholder
='none')
792 FORMATS
= self
.outtmpl_info
['formats']
794 # Custom type casting
795 test('%(formats.:.id)l', 'id 1, id 2, id 3')
796 test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
797 test('%(ext)l', 'mp4')
798 test('%(formats.:.id) 18l', ' id 1, id 2, id 3')
799 test('%(formats)j', (json
.dumps(FORMATS
), None))
800 test('%(formats)#j', (
801 json
.dumps(FORMATS
, indent
=4),
802 json
.dumps(FORMATS
, indent
=4).replace(':', ':').replace('"', '"').replace('\n', ' '),
804 test('%(title5).3B', 'á')
805 test('%(title5)U', 'áéí 𝐀')
806 test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
807 test('%(title5)+U', 'áéí A')
808 test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
809 test('%(height)D', '1k')
810 test('%(filesize)#D', '1Ki')
811 test('%(height)5.2D', ' 1.08k')
812 test('%(title4)#S', 'foo_bar_test')
813 test('%(title4).10S', ('foo "bar" ', 'foo "bar"' + ('#' if compat_os_name
== 'nt' else ' ')))
814 if compat_os_name
== 'nt':
815 test('%(title4)q', ('"foo ""bar"" test"', None))
816 test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', None))
817 test('%(formats.0.id)#q', ('"id 1"', None))
819 test('%(title4)q', ('\'foo "bar" test\'', '\'foo "bar" test\''))
820 test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
821 test('%(formats.0.id)#q', "'id 1'")
823 # Internal formatting
824 test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
825 test('%(title|%)s %(title|%%)s', '% %%')
826 test('%(id+1-height+3)05d', '00158')
827 test('%(width+100)05d', 'NA')
828 test('%(filesize*8)d', '8192')
829 test('%(formats.0) 15s', ('% 15s' % FORMATS
[0], None))
830 test('%(formats.0)r', (repr(FORMATS
[0]), None))
831 test('%(height.0)03d', '001')
832 test('%(-height.0)04d', '-001')
833 test('%(formats.-1.id)s', FORMATS
[-1]['id'])
834 test('%(formats.0.id.-1)d', FORMATS
[0]['id'][-1])
835 test('%(formats.3)s', 'NA')
836 test('%(formats.:2:-1)r', repr(FORMATS
[:2:-1]))
837 test('%(formats.0.id.-1+id)f', '1235.000000')
838 test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
839 out
= json
.dumps([{'id': f
['id'], 'height.:2': str(f
['height'])[:2]}
840 if 'height' in f
else {'id': f
['id']}
842 test('%(formats.:.{id,height.:2})j', (out
, None))
843 test('%(formats.:.{id,height}.id)l', ', '.join(f
['id'] for f
in FORMATS
))
844 test('%(.{id,title})j', ('{"id": "1234"}', '{"id": "1234"}'))
847 test('%(title,id)s', '1234')
848 test('%(width-100,height+20|def)d', '1100')
849 test('%(width-100,height+width|def)s', 'def')
850 test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
853 test('%(id&foo)s.bar', 'foo.bar')
854 test('%(title&foo)s.bar', 'NA.bar')
855 test('%(title&foo|baz)s.bar', 'baz.bar')
856 test('%(x,id&foo|baz)s.bar', 'foo.bar')
857 test('%(x,title&foo|baz)s.bar', 'baz.bar')
858 test('%(id&a\nb|)s', ('a\nb', 'a b'))
859 test('%(id&hi {:>10} {}|)s', 'hi 1234 1234')
860 test(R
'%(id&{0} {}|)s', 'NA')
861 test(R
'%(id&{0.1}|)s', 'NA')
862 test('%(height&{:,d})S', '1,080')
867 raise self
.assertTrue(False, 'LazyList should not be evaluated till here')
868 test('%(key.4)s', '4', info
={'key': LazyList(gen())})
871 test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
872 # test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # FIXME: ?
873 # test('%(foo|)s', ('', '_')) # FIXME: ?
875 # Environment variable expansion for prepare_filename
876 os
.environ
['__yt_dlp_var'] = 'expanded'
877 envvar
= '%__yt_dlp_var%' if compat_os_name
== 'nt' else '$__yt_dlp_var'
878 test(envvar
, (envvar
, 'expanded'))
879 if compat_os_name
== 'nt':
880 test('%s%', ('%s%', '%s%'))
881 os
.environ
['s'] = 'expanded'
882 test('%s%', ('%s%', 'expanded')) # %s% should be expanded before escaping %s
883 os
.environ
['(test)s'] = 'expanded'
884 test('%(test)s%', ('NA%', 'expanded')) # Environment should take priority over template
886 # Path expansion and escaping
887 test('Hello %(title1)s', 'Hello $PATH')
888 test('Hello %(title2)s', 'Hello %PATH%')
889 test('%(title3)s', ('foo/bar\\test', 'foo⧸bar⧹test'))
890 test('folder/%(title3)s', ('folder/foo/bar\\test', f
'folder{os.path.sep}foo⧸bar⧹test'))
892 def test_format_note(self
):
894 self
.assertEqual(ydl
._format
_note
({}), '')
895 assertRegexpMatches(self
, ydl
._format
_note
({
898 assertRegexpMatches(self
, ydl
._format
_note
({
902 def test_postprocessors(self
):
903 filename
= 'post-processor-testfile.mp4'
904 audiofile
= filename
+ '.mp3'
906 class SimplePP(PostProcessor
):
908 with
open(audiofile
, 'w') as f
:
910 return [info
['filepath']], info
912 def run_pp(params
, pp
):
913 with
open(filename
, 'w') as f
:
915 ydl
= YoutubeDL(params
)
916 ydl
.add_post_processor(pp())
917 ydl
.post_process(filename
, {'filepath': filename
})
919 run_pp({'keepvideo': True}, SimplePP
)
920 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
921 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
925 run_pp({'keepvideo': False}, SimplePP
)
926 self
.assertFalse(os
.path
.exists(filename
), f
'{filename} exists')
927 self
.assertTrue(os
.path
.exists(audiofile
), f
'{audiofile} doesn\'t exist')
930 class ModifierPP(PostProcessor
):
932 with
open(info
['filepath'], 'w') as f
:
936 run_pp({'keepvideo': False}, ModifierPP
)
937 self
.assertTrue(os
.path
.exists(filename
), f
'{filename} doesn\'t exist')
940 def test_match_filter(self
):
947 'filesize': 10 * 1024,
949 'uploader': '變態妍字幕版 太妍 тест',
950 'creator': "тест ' 123 ' тест--",
951 'webpage_url': 'http://example.com/watch?v=shenanigans',
959 'description': 'foo',
960 'filesize': 5 * 1024,
962 'uploader': 'тест 123',
963 'webpage_url': 'http://example.com/watch?v=SHENANIGANS',
965 videos
= [first
, second
]
967 def get_videos(filter_
=None):
968 ydl
= YDL({'match_filter': filter_
, 'simulate': True})
970 ydl
.process_ie_result(v
.copy(), download
=True)
971 return [v
['id'] for v
in ydl
.downloaded_info_dicts
]
974 self
.assertEqual(res
, ['1', '2'])
976 def f(v
, incomplete
):
980 return 'Video id is not 1'
982 self
.assertEqual(res
, ['1'])
984 f
= match_filter_func('duration < 30')
986 self
.assertEqual(res
, ['2'])
988 f
= match_filter_func('description = foo')
990 self
.assertEqual(res
, ['2'])
992 f
= match_filter_func('description =? foo')
994 self
.assertEqual(res
, ['1', '2'])
996 f
= match_filter_func('filesize > 5KiB')
998 self
.assertEqual(res
, ['1'])
1000 f
= match_filter_func('playlist_id = 42')
1002 self
.assertEqual(res
, ['1'])
1004 f
= match_filter_func('uploader = "變態妍字幕版 太妍 тест"')
1006 self
.assertEqual(res
, ['1'])
1008 f
= match_filter_func('uploader != "變態妍字幕版 太妍 тест"')
1010 self
.assertEqual(res
, ['2'])
1012 f
= match_filter_func('creator = "тест \' 123 \' тест--"')
1014 self
.assertEqual(res
, ['1'])
1016 f
= match_filter_func("creator = 'тест \\' 123 \\' тест--'")
1018 self
.assertEqual(res
, ['1'])
1020 f
= match_filter_func(r
"creator = 'тест \' 123 \' тест--' & duration > 30")
1022 self
.assertEqual(res
, [])
1024 def test_playlist_items_selection(self
):
1025 INDICES
, PAGE_SIZE
= list(range(1, 11)), 3
1027 def entry(i
, evaluated
):
1035 def pagedlist_entries(evaluated
):
1037 start
= PAGE_SIZE
* n
1038 for i
in INDICES
[start
: start
+ PAGE_SIZE
]:
1039 yield entry(i
, evaluated
)
1040 return OnDemandPagedList(page_func
, PAGE_SIZE
)
1043 return (i
+ PAGE_SIZE
- 1) // PAGE_SIZE
1045 def generator_entries(evaluated
):
1047 yield entry(i
, evaluated
)
1049 def list_entries(evaluated
):
1050 return list(generator_entries(evaluated
))
1052 def lazylist_entries(evaluated
):
1053 return LazyList(generator_entries(evaluated
))
1055 def get_downloaded_info_dicts(params
, entries
):
1057 ydl
.process_ie_result({
1058 '_type': 'playlist',
1060 'extractor': 'test:playlist',
1061 'extractor_key': 'test:playlist',
1062 'webpage_url': 'http://example.com',
1065 return ydl
.downloaded_info_dicts
1067 def test_selection(params
, expected_ids
, evaluate_all
=False):
1068 expected_ids
= list(expected_ids
)
1070 generator_eval
= pagedlist_eval
= INDICES
1071 elif not expected_ids
:
1072 generator_eval
= pagedlist_eval
= []
1074 generator_eval
= INDICES
[0: max(expected_ids
)]
1075 pagedlist_eval
= INDICES
[PAGE_SIZE
* page_num(min(expected_ids
)) - PAGE_SIZE
:
1076 PAGE_SIZE
* page_num(max(expected_ids
))]
1078 for name
, func
, expected_eval
in (
1079 ('list', list_entries
, INDICES
),
1080 ('Generator', generator_entries
, generator_eval
),
1081 # ('LazyList', lazylist_entries, generator_eval), # Generator and LazyList follow the exact same code path
1082 ('PagedList', pagedlist_entries
, pagedlist_eval
),
1085 entries
= func(evaluated
)
1086 results
= [(v
['playlist_autonumber'] - 1, (int(v
['id']), v
['playlist_index']))
1087 for v
in get_downloaded_info_dicts(params
, entries
)]
1088 self
.assertEqual(results
, list(enumerate(zip(expected_ids
, expected_ids
))), f
'Entries of {name} for {params}')
1089 self
.assertEqual(sorted(evaluated
), expected_eval
, f
'Evaluation of {name} for {params}')
1091 test_selection({}, INDICES
)
1092 test_selection({'playlistend': 20}, INDICES
, True)
1093 test_selection({'playlistend': 2}, INDICES
[:2])
1094 test_selection({'playliststart': 11}, [], True)
1095 test_selection({'playliststart': 2}, INDICES
[1:])
1096 test_selection({'playlist_items': '2-4'}, INDICES
[1:4])
1097 test_selection({'playlist_items': '2,4'}, [2, 4])
1098 test_selection({'playlist_items': '20'}, [], True)
1099 test_selection({'playlist_items': '0'}, [])
1101 # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
1102 test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
1103 test_selection({'playlist_items': '4,2'}, [4, 2])
1105 # Tests for https://github.com/yt-dlp/yt-dlp/issues/720
1106 # https://github.com/yt-dlp/yt-dlp/issues/302
1107 test_selection({'playlistreverse': True}, INDICES
[::-1])
1108 test_selection({'playliststart': 2, 'playlistreverse': True}, INDICES
[:0:-1])
1109 test_selection({'playlist_items': '2,4', 'playlistreverse': True}, [4, 2])
1110 test_selection({'playlist_items': '4,2'}, [4, 2])
1112 # Tests for --playlist-items start:end:step
1113 test_selection({'playlist_items': ':'}, INDICES
, True)
1114 test_selection({'playlist_items': '::1'}, INDICES
, True)
1115 test_selection({'playlist_items': '::-1'}, INDICES
[::-1], True)
1116 test_selection({'playlist_items': ':6'}, INDICES
[:6])
1117 test_selection({'playlist_items': ':-6'}, INDICES
[:-5], True)
1118 test_selection({'playlist_items': '-1:6:-2'}, INDICES
[:4:-2], True)
1119 test_selection({'playlist_items': '9:-6:-2'}, INDICES
[8:3:-2], True)
1121 test_selection({'playlist_items': '1:inf:2'}, INDICES
[::2], True)
1122 test_selection({'playlist_items': '-2:inf'}, INDICES
[-2:], True)
1123 test_selection({'playlist_items': ':inf:-1'}, [], True)
1124 test_selection({'playlist_items': '0-2:2'}, [2])
1125 test_selection({'playlist_items': '1-:2'}, INDICES
[::2], True)
1126 test_selection({'playlist_items': '0--2:2'}, INDICES
[1:-1:2], True)
1128 test_selection({'playlist_items': '10::3'}, [10], True)
1129 test_selection({'playlist_items': '-1::3'}, [10], True)
1130 test_selection({'playlist_items': '11::3'}, [], True)
1131 test_selection({'playlist_items': '-15::2'}, INDICES
[1::2], True)
1132 test_selection({'playlist_items': '-15::15'}, [], True)
1134 def test_do_not_override_ie_key_in_url_transparent(self
):
1137 class Foo1IE(InfoExtractor
):
1138 _VALID_URL
= r
'foo1:'
1140 def _real_extract(self
, url
):
1142 '_type': 'url_transparent',
1145 'title': 'foo1 title',
1149 class Foo2IE(InfoExtractor
):
1150 _VALID_URL
= r
'foo2:'
1152 def _real_extract(self
, url
):
1159 class Foo3IE(InfoExtractor
):
1160 _VALID_URL
= r
'foo3:'
1162 def _real_extract(self
, url
):
1163 return _make_result([{'url': TEST_URL
}], title
='foo3 title')
1165 ydl
.add_info_extractor(Foo1IE(ydl
))
1166 ydl
.add_info_extractor(Foo2IE(ydl
))
1167 ydl
.add_info_extractor(Foo3IE(ydl
))
1168 ydl
.extract_info('foo1:')
1169 downloaded
= ydl
.downloaded_info_dicts
[0]
1170 self
.assertEqual(downloaded
['url'], TEST_URL
)
1171 self
.assertEqual(downloaded
['title'], 'foo1 title')
1172 self
.assertEqual(downloaded
['id'], 'testid')
1173 self
.assertEqual(downloaded
['extractor'], 'testex')
1174 self
.assertEqual(downloaded
['extractor_key'], 'TestEx')
1176 # Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
1177 def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self
):
1180 def __init__(self
, *args
, **kwargs
):
1181 super().__init
__(*args
, **kwargs
)
1183 def trouble(self
, s
, tb
=None):
1188 'ignoreerrors': True,
1191 class VideoIE(InfoExtractor
):
1192 _VALID_URL
= r
'video:(?P<id>\d+)'
1194 def _real_extract(self
, url
):
1195 video_id
= self
._match
_id
(url
)
1197 'format_id': 'default',
1201 raise ExtractorError('foo')
1204 'format_id': 'extra',
1209 'title': f
'Video {video_id}',
1213 class PlaylistIE(InfoExtractor
):
1214 _VALID_URL
= r
'playlist:'
1220 '_type': 'url_transparent',
1221 'ie_key': VideoIE
.ie_key(),
1223 'url': f
'video:{video_id}',
1224 'title': f
'Video Transparent {video_id}',
1227 def _real_extract(self
, url
):
1228 return self
.playlist_result(self
._entries
())
1230 ydl
.add_info_extractor(VideoIE(ydl
))
1231 ydl
.add_info_extractor(PlaylistIE(ydl
))
1232 info
= ydl
.extract_info('playlist:')
1233 entries
= info
['entries']
1234 self
.assertEqual(len(entries
), 3)
1235 self
.assertTrue(entries
[0] is None)
1236 self
.assertTrue(entries
[1] is None)
1237 self
.assertEqual(len(ydl
.downloaded_info_dicts
), 1)
1238 downloaded
= ydl
.downloaded_info_dicts
[0]
1239 entries
[2].pop('requested_downloads', None)
1240 self
.assertEqual(entries
[2], downloaded
)
1241 self
.assertEqual(downloaded
['url'], TEST_URL
)
1242 self
.assertEqual(downloaded
['title'], 'Video Transparent 2')
1243 self
.assertEqual(downloaded
['id'], '2')
1244 self
.assertEqual(downloaded
['extractor'], 'Video')
1245 self
.assertEqual(downloaded
['extractor_key'], 'Video')
1247 def test_header_cookies(self
):
1248 from http
.cookiejar
import Cookie
1251 ydl
.report_warning
= lambda *_
, **__
: None
1253 def cookie(name
, value
, version
=None, domain
='', path
='', secure
=False, expires
=None):
1255 version
or 0, name
, value
, None, False,
1256 domain
, bool(domain
), bool(domain
), path
, bool(path
),
1257 secure
, expires
, False, None, None, rest
={})
1259 _test_url
= 'https://yt.dlp/test'
1261 def test(encoded_cookies
, cookies
, *, headers
=False, round_trip
=None, error_re
=None):
1263 ydl
.cookiejar
.clear()
1264 ydl
._load
_cookies
(encoded_cookies
, autoscope
=headers
)
1266 ydl
._apply
_header
_cookies
(_test_url
)
1267 data
= {'url': _test_url
}
1268 ydl
._calc
_headers
(data
)
1269 self
.assertCountEqual(
1270 map(vars, ydl
.cookiejar
), map(vars, cookies
),
1271 'Extracted cookiejar.Cookie is not the same')
1274 data
.get('cookies'), round_trip
or encoded_cookies
,
1275 'Cookie is not the same as round trip')
1276 ydl
.__dict
__['_YoutubeDL__header_cookies'] = []
1278 with self
.subTest(msg
=encoded_cookies
):
1282 with self
.assertRaisesRegex(Exception, error_re
):
1285 test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain
='.yt.dlp')])
1286 test('test=value', [cookie('test', 'value')], error_re
=r
'Unscoped cookies are not allowed')
1287 test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
1288 cookie('cookie1', 'value1', domain
='.yt.dlp', path
='/test'),
1289 cookie('cookie2', 'value2', domain
='.yt.dlp', path
='/')])
1290 test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
1291 cookie('test', 'value', domain
='.yt.dlp', path
='/test', secure
=True, expires
=9999999999)])
1292 test('test="value; "; path=/test; domain=.yt.dlp', [
1293 cookie('test', 'value; ', domain
='.yt.dlp', path
='/test')],
1294 round_trip
='test="value\\073 "; Domain=.yt.dlp; Path=/test')
1295 test('name=; Domain=.yt.dlp', [cookie('name', '', domain
='.yt.dlp')],
1296 round_trip
='name=""; Domain=.yt.dlp')
1298 test('test=value', [cookie('test', 'value', domain
='.yt.dlp')], headers
=True)
1299 test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers
=True, error_re
=r
'Invalid syntax')
1300 ydl
.deprecated_feature
= ydl
.report_error
1301 test('test=value', [], headers
=True, error_re
=r
'Passing cookies as a header is a potential security risk')
1303 def test_infojson_cookies(self
):
1304 TEST_FILE
= 'test_infojson_cookies.info.json'
1305 TEST_URL
= 'https://example.com/example.mp4'
1306 COOKIES
= 'a=b; Domain=.example.com; c=d; Domain=.example.com'
1307 COOKIE_HEADER
= {'Cookie': 'a=b; c=d'}
1310 ydl
.process_info
= lambda x
: ydl
._write
_info
_json
('test', x
, TEST_FILE
)
1312 def make_info(info_header_cookies
=False, fmts_header_cookies
=False, cookies_field
=False):
1313 fmt
= {'url': TEST_URL
}
1314 if fmts_header_cookies
:
1315 fmt
['http_headers'] = COOKIE_HEADER
1317 fmt
['cookies'] = COOKIES
1318 return _make_result([fmt
], http_headers
=COOKIE_HEADER
if info_header_cookies
else None)
1320 def test(initial_info
, note
):
1322 result
['processed'] = ydl
.process_ie_result(initial_info
)
1323 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1324 msg
=f
'No cookies set in cookiejar after initial process when {note}')
1325 ydl
.cookiejar
.clear()
1326 with
open(TEST_FILE
) as infojson
:
1327 result
['loaded'] = ydl
.sanitize_info(json
.load(infojson
), True)
1328 result
['final'] = ydl
.process_ie_result(result
['loaded'].copy(), download
=False)
1329 self
.assertTrue(ydl
.cookiejar
.get_cookies_for_url(TEST_URL
),
1330 msg
=f
'No cookies set in cookiejar after final process when {note}')
1331 ydl
.cookiejar
.clear()
1332 for key
in ('processed', 'loaded', 'final'):
1335 traverse_obj(info
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False),
1336 msg
=f
'Cookie header not removed in {key} result when {note}')
1338 traverse_obj(info
, ((None, ('formats', 0)), 'cookies'), get_all
=False), COOKIES
,
1339 msg
=f
'No cookies field found in {key} result when {note}')
1341 test({'url': TEST_URL
, 'http_headers': COOKIE_HEADER
, 'id': '1', 'title': 'x'}, 'no formats field')
1342 test(make_info(info_header_cookies
=True), 'info_dict header cokies')
1343 test(make_info(fmts_header_cookies
=True), 'format header cookies')
1344 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True), 'info_dict and format header cookies')
1345 test(make_info(info_header_cookies
=True, fmts_header_cookies
=True, cookies_field
=True), 'all cookies fields')
1346 test(make_info(cookies_field
=True), 'cookies format field')
1347 test({'url': TEST_URL
, 'cookies': COOKIES
, 'id': '1', 'title': 'x'}, 'info_dict cookies field only')
1351 def test_add_headers_cookie(self
):
1352 def check_for_cookie_header(result
):
1353 return traverse_obj(result
, ((None, ('formats', 0)), 'http_headers', 'Cookie'), casesense
=False, get_all
=False)
1355 ydl
= FakeYDL({'http_headers': {'Cookie': 'a=b'}})
1356 ydl
._apply
_header
_cookies
(_make_result([])['webpage_url']) # Scope to input webpage URL: .example.com
1358 fmt
= {'url': 'https://example.com/video.mp4'}
1359 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1360 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies in result info_dict')
1361 self
.assertEqual(result
.get('cookies'), 'a=b; Domain=.example.com', msg
='No cookies were set in cookies field')
1362 self
.assertIn('a=b', ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='No cookies were set in cookiejar')
1364 fmt
= {'url': 'https://wrong.com/video.mp4'}
1365 result
= ydl
.process_ie_result(_make_result([fmt
]), download
=False)
1366 self
.assertIsNone(check_for_cookie_header(result
), msg
='http_headers cookies for wrong domain')
1367 self
.assertFalse(result
.get('cookies'), msg
='Cookies set in cookies field for wrong domain')
1368 self
.assertFalse(ydl
.cookiejar
.get_cookie_header(fmt
['url']), msg
='Cookies set in cookiejar for wrong domain')
1371 if __name__
== '__main__':