]> jfr.im git - yt-dlp.git/blobdiff - yt_dlp/YoutubeDL.py
Field `additional_urls` to download additional videos from metadata
[yt-dlp.git] / yt_dlp / YoutubeDL.py
index 9fe591f75893f867cf84ee10b33086dde2606847..9a2d0abc27ee828a1ef47244a0d58902f10327b0 100644 (file)
@@ -48,7 +48,6 @@
     date_from_str,
     DateRange,
     DEFAULT_OUTTMPL,
-    OUTTMPL_TYPES,
     determine_ext,
     determine_protocol,
     DOT_DESKTOP_LINK_TEMPLATE,
@@ -57,8 +56,8 @@
     DownloadError,
     encode_compat_str,
     encodeFilename,
-    error_to_compat_str,
     EntryNotInPlaylist,
+    error_to_compat_str,
     ExistingVideoReached,
     expand_path,
     ExtractorError,
@@ -77,6 +76,7 @@
     MaxDownloadsReached,
     network_exceptions,
     orderedSet,
+    OUTTMPL_TYPES,
     PagedList,
     parse_filesize,
     PerRequestProxyHandler,
     PostProcessingError,
     preferredencoding,
     prepend_extension,
+    process_communicate_or_kill,
     random_uuidv4,
     register_socks_protocols,
+    RejectedVideoReached,
     render_table,
     replace_extension,
-    RejectedVideoReached,
     SameFileError,
     sanitize_filename,
     sanitize_path,
     YoutubeDLCookieProcessor,
     YoutubeDLHandler,
     YoutubeDLRedirectHandler,
-    process_communicate_or_kill,
 )
 from .cache import Cache
 from .extractor import (
@@ -1187,14 +1187,29 @@ def process_ie_result(self, ie_result, download=True, extra_info={}):
 
         if result_type == 'video':
             self.add_extra_info(ie_result, extra_info)
-            return self.process_video_result(ie_result, download=download)
+            ie_result = self.process_video_result(ie_result, download=download)
+            additional_urls = ie_result.get('additional_urls')
+            if additional_urls:
+                # TODO: Improve MetadataFromFieldPP to allow setting a list
+                if isinstance(additional_urls, compat_str):
+                    additional_urls = [additional_urls]
+                self.to_screen(
+                    '[info] %s: %d additional URL(s) requested' % (ie_result['id'], len(additional_urls)))
+                self.write_debug('Additional URLs: "%s"' % '", "'.join(additional_urls))
+                ie_result['additional_entries'] = [
+                    self.extract_info(
+                        url, download, extra_info,
+                        force_generic_extractor=self.params.get('force_generic_extractor'))
+                    for url in additional_urls
+                ]
+            return ie_result
         elif result_type == 'url':
             # We have to add extra_info to the results because it may be
             # contained in a playlist
-            return self.extract_info(ie_result['url'],
-                                     download,
-                                     ie_key=ie_result.get('ie_key'),
-                                     extra_info=extra_info)
+            return self.extract_info(
+                ie_result['url'], download,
+                ie_key=ie_result.get('ie_key'),
+                extra_info=extra_info)
         elif result_type == 'url_transparent':
             # Use the information from the embedding page
             info = self.extract_info(
@@ -1379,6 +1394,9 @@ def make_playlistitems_entries(list_ie_entries):
                     except (OSError, IOError):
                         self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
 
+            # TODO: This should be passed to ThumbnailsConvertor if necessary
+            self._write_thumbnails(ie_copy, self.prepare_filename(ie_copy, 'pl_thumbnail'))
+
             if self.params.get('writedescription', False):
                 descfn = self.prepare_filename(ie_copy, 'pl_description')
                 if not self._ensure_dir_exists(encodeFilename(descfn)):
@@ -1726,9 +1744,13 @@ def _check_formats(formats):
                     expand_path(paths.get('home', '').strip()),
                     expand_path(paths.get('temp', '').strip()),
                     'ytdl.%s.f%s.check-format' % (random_uuidv4(), f['format_id']))
-                dl, _ = self.dl(temp_file, f, test=True)
-                if os.path.exists(temp_file):
-                    os.remove(temp_file)
+                try:
+                    dl, _ = self.dl(temp_file, f, test=True)
+                except (ExtractorError, IOError, OSError, ValueError) + network_exceptions:
+                    dl = False
+                finally:
+                    if os.path.exists(temp_file):
+                        os.remove(temp_file)
                 if dl:
                     yield f
                 else:
@@ -1937,7 +1959,8 @@ def sanitize_numeric_fields(info):
                 t.get('preference') if t.get('preference') is not None else -1,
                 t.get('width') if t.get('width') is not None else -1,
                 t.get('height') if t.get('height') is not None else -1,
-                t.get('id') if t.get('id') is not None else '', t.get('url')))
+                t.get('id') if t.get('id') is not None else '',
+                t.get('url')))
             for i, t in enumerate(thumbnails):
                 t['url'] = sanitize_url(t['url'])
                 if t.get('width') and t.get('height'):
@@ -2081,6 +2104,9 @@ def is_wellformed(f):
             # element in the 'formats' field in info_dict is info_dict itself,
             # which can't be exported to json
             info_dict['formats'] = formats
+
+        info_dict, _ = self.pre_process(info_dict)
+
         if self.params.get('listformats'):
             if not info_dict.get('formats'):
                 raise ExtractorError('No video formats found', expected=True)
@@ -2128,14 +2154,13 @@ def is_wellformed(f):
                 self.report_warning('Requested format is not available')
         elif download:
             self.to_screen(
-                '[info] %s: Downloading format(s) %s'
-                % (info_dict['id'], ", ".join([f['format_id'] for f in formats_to_download])))
-            if len(formats_to_download) > 1:
-                self.to_screen(
-                    '[info] %s: Downloading video in %s formats'
-                    % (info_dict['id'], len(formats_to_download)))
+                '[info] %s: Downloading %d format(s): %s' % (
+                    info_dict['id'], len(formats_to_download),
+                    ", ".join([f['format_id'] for f in formats_to_download])))
             for fmt in formats_to_download:
                 new_info = dict(info_dict)
+                # Save a reference to the original info_dict so that it can be modified in process_info if needed
+                new_info['__original_infodict'] = info_dict
                 new_info.update(fmt)
                 self.process_info(new_info)
         # We update the info dict with the best quality format (backwards compatibility)
@@ -2299,8 +2324,6 @@ def process_info(self, info_dict):
         self.post_extract(info_dict)
         self._num_downloads += 1
 
-        info_dict, _ = self.pre_process(info_dict)
-
         # info_dict['_filename'] needs to be set for backward compatibility
         info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True)
         temp_filename = self.prepare_filename(info_dict, 'temp')
@@ -2395,7 +2418,7 @@ def process_info(self, info_dict):
                             self.dl(sub_filename, sub_info.copy(), subtitle=True)
                             sub_info['filepath'] = sub_filename
                             files_to_move[sub_filename] = sub_filename_final
-                        except tuple([ExtractorError, IOError, OSError, ValueError] + network_exceptions) as err:
+                        except (ExtractorError, IOError, OSError, ValueError) + network_exceptions as err:
                             self.report_warning('Unable to download subtitle for "%s": %s' %
                                                 (sub_lang, error_to_compat_str(err)))
                             continue
@@ -2738,6 +2761,7 @@ def download_with_info_file(self, info_filename):
 
     @staticmethod
     def filter_requested_info(info_dict, actually_filter=True):
+        info_dict.pop('__original_infodict', None)  # Always remove this
         if not actually_filter:
             info_dict['epoch'] = int(time.time())
             return info_dict
@@ -2782,13 +2806,14 @@ def actual_post_extract(info_dict):
                     actual_post_extract(video_dict or {})
                 return
 
-            if '__post_extractor' not in info_dict:
-                return
-            post_extractor = info_dict['__post_extractor']
-            if post_extractor:
-                info_dict.update(post_extractor().items())
-            del info_dict['__post_extractor']
-            return
+            post_extractor = info_dict.get('__post_extractor') or (lambda: {})
+            extra = post_extractor().items()
+            info_dict.update(extra)
+            info_dict.pop('__post_extractor', None)
+
+            original_infodict = info_dict.get('__original_infodict') or {}
+            original_infodict.update(extra)
+            original_infodict.pop('__post_extractor', None)
 
         actual_post_extract(info_dict or {})
 
@@ -3001,10 +3026,17 @@ def list_subtitles(self, video_id, subtitles, name='subtitles'):
             return
         self.to_screen(
             'Available %s for %s:' % (name, video_id))
+
+        def _row(lang, formats):
+            exts, names = zip(*((f['ext'], f.get('name', 'unknown')) for f in reversed(formats)))
+            if len(set(names)) == 1:
+                names = [] if names[0] == 'unknown' else names[:1]
+            return [lang, ', '.join(names), ', '.join(exts)]
+
         self.to_screen(render_table(
-            ['Language', 'formats'],
-            [[lang, ', '.join(f['ext'] for f in reversed(formats))]
-                for lang, formats in subtitles.items()]))
+            ['Language', 'Name', 'Formats'],
+            [_row(lang, formats) for lang, formats in subtitles.items()],
+            hideEmpty=True))
 
     def urlopen(self, req):
         """ Start an HTTP download """