diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index 6f2aba5ac..0121a7764 100755 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -198,6 +198,7 @@ class YoutubeDL(object): writeannotations: Write the video annotations to a .annotations.xml file writethumbnail: Write the thumbnail image to a file write_all_thumbnails: Write all thumbnail formats to files + writestoryboards: Write all storyboards (grid of video frames) to a file writesubtitles: Write the video subtitles to a file writeautomaticsub: Write the automatically generated subtitles to a file allsubtitles: Downloads all the subtitles of the video @@ -294,6 +295,7 @@ class YoutubeDL(object): [sleep_interval; max_sleep_interval]. listformats: Print an overview of available video formats and exit. list_thumbnails: Print a table of all thumbnails and exit. + list_storyboards: Print a table of all storyboards and exit. match_filter: A function that gets called with the info_dict of every video. If it returns a message, the video is ignored. @@ -1651,6 +1653,10 @@ class YoutubeDL(object): self.list_thumbnails(info_dict) return + if self.params.get('list_storyboards'): + self.list_thumbnails(info_dict, item_name='storyboards') + return + thumbnail = info_dict.get('thumbnail') if thumbnail: info_dict['thumbnail'] = sanitize_url(thumbnail) @@ -2431,17 +2437,27 @@ class YoutubeDL(object): '[info] Available formats for %s:\n%s' % (info_dict['id'], render_table(header_line, table))) - def list_thumbnails(self, info_dict): - thumbnails = info_dict.get('thumbnails') + def list_thumbnails(self, info_dict, item_name='thumbnails'): + thumbnails = info_dict.get(item_name) if not thumbnails: - self.to_screen('[info] No thumbnails present for %s' % info_dict['id']) + self.to_screen('[info] No %s present for %s' % (item_name, info_dict['id'])) return self.to_screen( - '[info] Thumbnails for %s:' % info_dict['id']) - self.to_screen(render_table( - ['ID', 'width', 'height', 'URL'], - [[t['id'], t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails])) + '[info] %s for %s:' % (item_name.title(), info_dict['id'])) + + columns = ['ID', 'width', 'height'] + if item_name == 'storyboards': + columns += ['cols', 'rows', 'frames'] + columns += ['URL'] + + table = [] + for t in thumbnails: + table.append([]) + for column in columns: + table[-1].append(t.get(column.lower(), 'unknown')) + + self.to_screen(render_table(columns, table)) def list_subtitles(self, video_id, subtitles, name='subtitles'): if not subtitles: @@ -2644,12 +2660,16 @@ class YoutubeDL(object): return def _write_thumbnails(self, info_dict, filename): + item_name = 'thumbnail' if self.params.get('writethumbnail', False): thumbnails = info_dict.get('thumbnails') if thumbnails: thumbnails = [thumbnails[-1]] elif self.params.get('write_all_thumbnails', False): thumbnails = info_dict.get('thumbnails') + elif self.params.get('writestoryboards', False): + thumbnails = info_dict.get('storyboards') + item_name = 'storyboard' else: return @@ -2659,22 +2679,28 @@ class YoutubeDL(object): for t in thumbnails: thumb_ext = determine_ext(t['url'], 'jpg') - suffix = '_%s' % t['id'] if len(thumbnails) > 1 else '' + if item_name == 'thumbnails': + suffix = '_%s' % t['id'] if len(thumbnails) > 1 else '' + else: + suffix = '_%s_%s' % (item_name, t['id']) thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else '' t['filename'] = thumb_filename = replace_extension(filename + suffix, thumb_ext, info_dict.get('ext')) if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)): - self.to_screen('[%s] %s: Thumbnail %sis already present' % - (info_dict['extractor'], info_dict['id'], thumb_display_id)) + self.to_screen('[%s] %s: %s %sis already present' % + (info_dict['extractor'], info_dict['id'], + item_name.title(), thumb_display_id)) else: - self.to_screen('[%s] %s: Downloading thumbnail %s...' % - (info_dict['extractor'], info_dict['id'], thumb_display_id)) + self.to_screen('[%s] %s: Downloading %s %s...' % + (info_dict['extractor'], info_dict['id'], + item_name, thumb_display_id)) try: uf = self.urlopen(t['url']) with open(encodeFilename(thumb_filename), 'wb') as thumbf: shutil.copyfileobj(uf, thumbf) - self.to_screen('[%s] %s: Writing thumbnail %sto: %s' % - (info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename)) + self.to_screen('[%s] %s: Writing %s %sto: %s' % + (info_dict['extractor'], info_dict['id'], + item_name, thumb_display_id, thumb_filename)) except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self.report_warning('Unable to download thumbnail "%s": %s' % - (t['url'], error_to_compat_str(err))) + self.report_warning('Unable to download %s "%s": %s' % + (t['url'], item_name, error_to_compat_str(err))) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index cc8285eba..37560cd68 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -370,6 +370,7 @@ def _real_main(argv=None): 'writeinfojson': opts.writeinfojson, 'writethumbnail': opts.writethumbnail, 'write_all_thumbnails': opts.write_all_thumbnails, + 'writestoryboards': opts.writestoryboards, 'writesubtitles': opts.writesubtitles, 'writeautomaticsub': opts.writeautomaticsub, 'allsubtitles': opts.allsubtitles, @@ -417,6 +418,7 @@ def _real_main(argv=None): 'max_sleep_interval': opts.max_sleep_interval, 'external_downloader': opts.external_downloader, 'list_thumbnails': opts.list_thumbnails, + 'list_storyboards': opts.list_storyboards, 'playlist_items': opts.playlist_items, 'xattr_set_filesize': opts.xattr_set_filesize, 'match_filter': match_filter, diff --git a/youtube_dl/extractor/common.py b/youtube_dl/extractor/common.py index 7fae9e57b..9f6b56897 100644 --- a/youtube_dl/extractor/common.py +++ b/youtube_dl/extractor/common.py @@ -239,6 +239,12 @@ class InfoExtractor(object): deprecated) * "filesize" (optional, int) thumbnail: Full URL to a video thumbnail image. + storyboards: A list of dictionaries representing storyboards. + A storyboard is an image grid made of frames from the video. + This has the same structure as the thumbnails list, plus: + * "cols" (optional, int) + * "rows" (optional, int) + * "frames" (optional, int) description: Full video description. uploader: Full name of the video uploader. license: License name the video is licensed under. diff --git a/youtube_dl/extractor/youtube.py b/youtube_dl/extractor/youtube.py index db840fc45..e1f04039b 100644 --- a/youtube_dl/extractor/youtube.py +++ b/youtube_dl/extractor/youtube.py @@ -9,6 +9,7 @@ import os.path import random import re import traceback +import math from .common import InfoExtractor, SearchInfoExtractor from ..compat import ( @@ -2258,6 +2259,58 @@ class YoutubeIE(YoutubeBaseInfoExtractor): if thumbnail: thumbnails = [{'url': thumbnail}] + storyboards = [] + sb_spec = try_get(player_response, + lambda x: x['storyboards']['playerStoryboardSpecRenderer']['spec'], + compat_str) + if sb_spec: + s_parts = sb_spec.split('|') + base_url = s_parts[0] + for i, params in enumerate(s_parts[1:]): + storyboard_attrib = params.split('#') + if len(storyboard_attrib) != 8: + self._downloader.report_warning('Unable to extract storyboard') + continue + + frame_width = int_or_none(storyboard_attrib[0]) + frame_height = int_or_none(storyboard_attrib[1]) + total_frames = int_or_none(storyboard_attrib[2]) + cols = int_or_none(storyboard_attrib[3]) + rows = int_or_none(storyboard_attrib[4]) + filename = storyboard_attrib[6] + sigh = storyboard_attrib[7] + + if frame_width and frame_height and cols and rows and total_frames: + frames = cols * rows + width, height = frame_width * cols, frame_height * rows + n_images = int(math.ceil(total_frames / float(cols * rows))) + else: + self._downloader.report_warning('Unable to extract storyboard') + continue + + storyboards_url = base_url.replace('$L', compat_str(i)) + '&' + for j in range(n_images): + url = storyboards_url.replace('$N', filename).replace('$M', compat_str(j)) + 'sigh=' + sigh + if j == n_images - 1: + remaining_frames = total_frames % (cols * rows) + if remaining_frames != 0: + frames = remaining_frames + rows = int(math.ceil(float(remaining_frames) / rows)) + height = rows * frame_height + if rows == 1: + cols = remaining_frames + width = cols * frame_width + + storyboards.append({ + 'id': 'L{0}-M{1}'.format(i, j), + 'width': width, + 'height': height, + 'cols': cols, + 'rows': rows, + 'frames': frames, + 'url': url + }) + category = microformat.get('category') or search_meta('genre') channel_id = self._extract_channel_id( webpage, videodetails=video_details, metadata=microformat) @@ -2309,6 +2362,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'categories': [category] if category else None, 'tags': keywords, 'is_live': is_live, + 'storyboards': storyboards, } pctr = try_get( diff --git a/youtube_dl/options.py b/youtube_dl/options.py index 434f520d3..5ce63eb39 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -785,6 +785,14 @@ def parseOpts(overrideArguments=None): '--list-thumbnails', action='store_true', dest='list_thumbnails', default=False, help='Simulate and list all available thumbnail formats') + thumbnail.add_option( + '--write-storyboards', + action='store_true', dest='writestoryboards', default=False, + help='Write all storyboards (grid of video frames) to disk') + thumbnail.add_option( + '--list-storyboards', + action='store_true', dest='list_storyboards', default=False, + help='Simulate and list all available storyboards') postproc = optparse.OptionGroup(parser, 'Post-processing Options') postproc.add_option(