@@ -338,6 +338,8 @@ class YoutubeDL(object):
338
338
_pps = []
339
339
_download_retcode = None
340
340
_num_downloads = None
341
+ _playlist_level = 0
342
+ _playlist_urls = set ()
341
343
_screen_file = None
342
344
343
345
def __init__ (self , params = None , auto_init = True ):
@@ -906,115 +908,23 @@ def process_ie_result(self, ie_result, download=True, extra_info={}):
906
908
return self .process_ie_result (
907
909
new_result , download = download , extra_info = extra_info )
908
910
elif result_type in ('playlist' , 'multi_video' ):
909
- # We process each entry in the playlist
910
- playlist = ie_result .get ('title' ) or ie_result .get ('id' )
911
- self .to_screen ('[download] Downloading playlist: %s' % playlist )
912
-
913
- playlist_results = []
914
-
915
- playliststart = self .params .get ('playliststart' , 1 ) - 1
916
- playlistend = self .params .get ('playlistend' )
917
- # For backwards compatibility, interpret -1 as whole list
918
- if playlistend == - 1 :
919
- playlistend = None
920
-
921
- playlistitems_str = self .params .get ('playlist_items' )
922
- playlistitems = None
923
- if playlistitems_str is not None :
924
- def iter_playlistitems (format ):
925
- for string_segment in format .split (',' ):
926
- if '-' in string_segment :
927
- start , end = string_segment .split ('-' )
928
- for item in range (int (start ), int (end ) + 1 ):
929
- yield int (item )
930
- else :
931
- yield int (string_segment )
932
- playlistitems = orderedSet (iter_playlistitems (playlistitems_str ))
933
-
934
- ie_entries = ie_result ['entries' ]
935
-
936
- def make_playlistitems_entries (list_ie_entries ):
937
- num_entries = len (list_ie_entries )
938
- return [
939
- list_ie_entries [i - 1 ] for i in playlistitems
940
- if - num_entries <= i - 1 < num_entries ]
941
-
942
- def report_download (num_entries ):
911
+ # Protect from infinite recursion due to recursively nested playlists
912
+ # (see https://github.com/ytdl-org/youtube-dl/issues/27833)
913
+ webpage_url = ie_result ['webpage_url' ]
914
+ if webpage_url in self ._playlist_urls :
943
915
self .to_screen (
944
- '[%s] playlist %s: Downloading %d videos' %
945
- (ie_result ['extractor' ], playlist , num_entries ))
946
-
947
- if isinstance (ie_entries , list ):
948
- n_all_entries = len (ie_entries )
949
- if playlistitems :
950
- entries = make_playlistitems_entries (ie_entries )
951
- else :
952
- entries = ie_entries [playliststart :playlistend ]
953
- n_entries = len (entries )
954
- self .to_screen (
955
- '[%s] playlist %s: Collected %d video ids (downloading %d of them)' %
956
- (ie_result ['extractor' ], playlist , n_all_entries , n_entries ))
957
- elif isinstance (ie_entries , PagedList ):
958
- if playlistitems :
959
- entries = []
960
- for item in playlistitems :
961
- entries .extend (ie_entries .getslice (
962
- item - 1 , item
963
- ))
964
- else :
965
- entries = ie_entries .getslice (
966
- playliststart , playlistend )
967
- n_entries = len (entries )
968
- report_download (n_entries )
969
- else : # iterable
970
- if playlistitems :
971
- entries = make_playlistitems_entries (list (itertools .islice (
972
- ie_entries , 0 , max (playlistitems ))))
973
- else :
974
- entries = list (itertools .islice (
975
- ie_entries , playliststart , playlistend ))
976
- n_entries = len (entries )
977
- report_download (n_entries )
978
-
979
- if self .params .get ('playlistreverse' , False ):
980
- entries = entries [::- 1 ]
981
-
982
- if self .params .get ('playlistrandom' , False ):
983
- random .shuffle (entries )
984
-
985
- x_forwarded_for = ie_result .get ('__x_forwarded_for_ip' )
986
-
987
- for i , entry in enumerate (entries , 1 ):
988
- self .to_screen ('[download] Downloading video %s of %s' % (i , n_entries ))
989
- # This __x_forwarded_for_ip thing is a bit ugly but requires
990
- # minimal changes
991
- if x_forwarded_for :
992
- entry ['__x_forwarded_for_ip' ] = x_forwarded_for
993
- extra = {
994
- 'n_entries' : n_entries ,
995
- 'playlist' : playlist ,
996
- 'playlist_id' : ie_result .get ('id' ),
997
- 'playlist_title' : ie_result .get ('title' ),
998
- 'playlist_uploader' : ie_result .get ('uploader' ),
999
- 'playlist_uploader_id' : ie_result .get ('uploader_id' ),
1000
- 'playlist_index' : playlistitems [i - 1 ] if playlistitems else i + playliststart ,
1001
- 'extractor' : ie_result ['extractor' ],
1002
- 'webpage_url' : ie_result ['webpage_url' ],
1003
- 'webpage_url_basename' : url_basename (ie_result ['webpage_url' ]),
1004
- 'extractor_key' : ie_result ['extractor_key' ],
1005
- }
1006
-
1007
- reason = self ._match_entry (entry , incomplete = True )
1008
- if reason is not None :
1009
- self .to_screen ('[download] ' + reason )
1010
- continue
916
+ '[download] Skipping already downloaded playlist: %s'
917
+ % ie_result .get ('title' ) or ie_result .get ('id' ))
918
+ return
1011
919
1012
- entry_result = self .__process_iterable_entry (entry , download , extra )
1013
- # TODO: skip failed (empty) entries?
1014
- playlist_results .append (entry_result )
1015
- ie_result ['entries' ] = playlist_results
1016
- self .to_screen ('[download] Finished downloading playlist: %s' % playlist )
1017
- return ie_result
920
+ self ._playlist_level += 1
921
+ self ._playlist_urls .add (webpage_url )
922
+ try :
923
+ return self .__process_playlist (ie_result , download )
924
+ finally :
925
+ self ._playlist_level -= 1
926
+ if not self ._playlist_level :
927
+ self ._playlist_urls .clear ()
1018
928
elif result_type == 'compat_list' :
1019
929
self .report_warning (
1020
930
'Extractor %s returned a compat_list result. '
@@ -1039,6 +949,118 @@ def _fixup(r):
1039
949
else :
1040
950
raise Exception ('Invalid result type: %s' % result_type )
1041
951
952
+ def __process_playlist (self , ie_result , download ):
953
+ # We process each entry in the playlist
954
+ playlist = ie_result .get ('title' ) or ie_result .get ('id' )
955
+
956
+ self .to_screen ('[download] Downloading playlist: %s' % playlist )
957
+
958
+ playlist_results = []
959
+
960
+ playliststart = self .params .get ('playliststart' , 1 ) - 1
961
+ playlistend = self .params .get ('playlistend' )
962
+ # For backwards compatibility, interpret -1 as whole list
963
+ if playlistend == - 1 :
964
+ playlistend = None
965
+
966
+ playlistitems_str = self .params .get ('playlist_items' )
967
+ playlistitems = None
968
+ if playlistitems_str is not None :
969
+ def iter_playlistitems (format ):
970
+ for string_segment in format .split (',' ):
971
+ if '-' in string_segment :
972
+ start , end = string_segment .split ('-' )
973
+ for item in range (int (start ), int (end ) + 1 ):
974
+ yield int (item )
975
+ else :
976
+ yield int (string_segment )
977
+ playlistitems = orderedSet (iter_playlistitems (playlistitems_str ))
978
+
979
+ ie_entries = ie_result ['entries' ]
980
+
981
+ def make_playlistitems_entries (list_ie_entries ):
982
+ num_entries = len (list_ie_entries )
983
+ return [
984
+ list_ie_entries [i - 1 ] for i in playlistitems
985
+ if - num_entries <= i - 1 < num_entries ]
986
+
987
+ def report_download (num_entries ):
988
+ self .to_screen (
989
+ '[%s] playlist %s: Downloading %d videos' %
990
+ (ie_result ['extractor' ], playlist , num_entries ))
991
+
992
+ if isinstance (ie_entries , list ):
993
+ n_all_entries = len (ie_entries )
994
+ if playlistitems :
995
+ entries = make_playlistitems_entries (ie_entries )
996
+ else :
997
+ entries = ie_entries [playliststart :playlistend ]
998
+ n_entries = len (entries )
999
+ self .to_screen (
1000
+ '[%s] playlist %s: Collected %d video ids (downloading %d of them)' %
1001
+ (ie_result ['extractor' ], playlist , n_all_entries , n_entries ))
1002
+ elif isinstance (ie_entries , PagedList ):
1003
+ if playlistitems :
1004
+ entries = []
1005
+ for item in playlistitems :
1006
+ entries .extend (ie_entries .getslice (
1007
+ item - 1 , item
1008
+ ))
1009
+ else :
1010
+ entries = ie_entries .getslice (
1011
+ playliststart , playlistend )
1012
+ n_entries = len (entries )
1013
+ report_download (n_entries )
1014
+ else : # iterable
1015
+ if playlistitems :
1016
+ entries = make_playlistitems_entries (list (itertools .islice (
1017
+ ie_entries , 0 , max (playlistitems ))))
1018
+ else :
1019
+ entries = list (itertools .islice (
1020
+ ie_entries , playliststart , playlistend ))
1021
+ n_entries = len (entries )
1022
+ report_download (n_entries )
1023
+
1024
+ if self .params .get ('playlistreverse' , False ):
1025
+ entries = entries [::- 1 ]
1026
+
1027
+ if self .params .get ('playlistrandom' , False ):
1028
+ random .shuffle (entries )
1029
+
1030
+ x_forwarded_for = ie_result .get ('__x_forwarded_for_ip' )
1031
+
1032
+ for i , entry in enumerate (entries , 1 ):
1033
+ self .to_screen ('[download] Downloading video %s of %s' % (i , n_entries ))
1034
+ # This __x_forwarded_for_ip thing is a bit ugly but requires
1035
+ # minimal changes
1036
+ if x_forwarded_for :
1037
+ entry ['__x_forwarded_for_ip' ] = x_forwarded_for
1038
+ extra = {
1039
+ 'n_entries' : n_entries ,
1040
+ 'playlist' : playlist ,
1041
+ 'playlist_id' : ie_result .get ('id' ),
1042
+ 'playlist_title' : ie_result .get ('title' ),
1043
+ 'playlist_uploader' : ie_result .get ('uploader' ),
1044
+ 'playlist_uploader_id' : ie_result .get ('uploader_id' ),
1045
+ 'playlist_index' : playlistitems [i - 1 ] if playlistitems else i + playliststart ,
1046
+ 'extractor' : ie_result ['extractor' ],
1047
+ 'webpage_url' : ie_result ['webpage_url' ],
1048
+ 'webpage_url_basename' : url_basename (ie_result ['webpage_url' ]),
1049
+ 'extractor_key' : ie_result ['extractor_key' ],
1050
+ }
1051
+
1052
+ reason = self ._match_entry (entry , incomplete = True )
1053
+ if reason is not None :
1054
+ self .to_screen ('[download] ' + reason )
1055
+ continue
1056
+
1057
+ entry_result = self .__process_iterable_entry (entry , download , extra )
1058
+ # TODO: skip failed (empty) entries?
1059
+ playlist_results .append (entry_result )
1060
+ ie_result ['entries' ] = playlist_results
1061
+ self .to_screen ('[download] Finished downloading playlist: %s' % playlist )
1062
+ return ie_result
1063
+
1042
1064
@__handle_extraction_exceptions
1043
1065
def __process_iterable_entry (self , entry , download , extra_info ):
1044
1066
return self .process_ie_result (
@@ -1226,6 +1248,8 @@ def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, ins
1226
1248
group = _parse_format_selection (tokens , inside_group = True )
1227
1249
current_selector = FormatSelector (GROUP , group , [])
1228
1250
elif string == '+' :
1251
+ if inside_merge :
1252
+ raise syntax_error ('Unexpected "+"' , start )
1229
1253
video_selector = current_selector
1230
1254
audio_selector = _parse_format_selection (tokens , inside_merge = True )
1231
1255
if not video_selector or not audio_selector :
@@ -1777,6 +1801,8 @@ def ensure_dir_exists(path):
1777
1801
os .makedirs (dn )
1778
1802
return True
1779
1803
except (OSError , IOError ) as err :
1804
+ if isinstance (err , OSError ) and err .errno == errno .EEXIST :
1805
+ return True
1780
1806
self .report_error ('unable to create directory ' + error_to_compat_str (err ))
1781
1807
return False
1782
1808
0 commit comments