def setUp(self): if not hasattr(self, 'assertRegex'): self.assertRegex = self.assertRegexpMatches self.assertNotRegex = self.assertNotRegexpMatches # make sure XDG_CONFIG_HOME doesn't interfere with our # change_home later if 'XDG_CONFIG_HOME' in os.environ: os.environ.pop('XDG_CONFIG_HOME') # create a mock srt subtitle file fd, self.srtfile = tempfile.mkstemp(prefix='storyboard-test-', suffix='.srt') os.close(fd) with open(self.srtfile, 'w') as fd: fd.write("1\n" "00:00:01,000 --> 00:00:02,000\n" "SubRip is the way to go\n") # create video file fd, self.videofile = tempfile.mkstemp(prefix='storyboard-test-', suffix='.mkv') os.close(fd) bins = fflocate.guess_bins() fflocate.check_bins(bins) # error if bins do not exist self.ffmpeg_bin, self.ffprobe_bin = bins with open(os.devnull, 'wb') as devnull: command = [ self.ffmpeg_bin, # video stream (320x180, pure pink) '-f', 'lavfi', '-i', 'color=c=pink:s=320x180:d=10', # audio stream (silent) '-f', 'lavfi', '-i', 'aevalsrc=0:d=10', # subtitle stream '-i', self.srtfile, # output option '-y', self.videofile ] subprocess.check_call(command, stdout=devnull, stderr=devnull)
def __init__(self, video, params=None): """Initialize the StoryBoard class. See the module docstring for parameters and exceptions. """ if params is None: params = {} if 'bins' in params and params['bins'] is not None: bins = params['bins'] assert isinstance(bins, tuple) and len(bins) == 2 else: bins = fflocate.guess_bins() frame_codec = _read_param(params, 'frame_codec', 'png') video_duration = _read_param(params, 'video_duration', None) print_progress = _read_param(params, 'print_progress', False) fflocate.check_bins(bins) # seek frame by frame if video duration is specially given # (indicating that normal input seeking may not work) self._seek_frame_by_frame = video_duration is not None self._bins = bins if isinstance(video, metadata.Video): self.video = video elif isinstance(video, str): self.video = metadata.Video(video, params={ 'ffprobe_bin': bins[1], 'video_duration': video_duration, 'print_progress': print_progress, }) else: raise ValueError("expected str or storyboard.metadata.Video " "for the video argument, got %s" % type(video).__name__) self.frames = [] self._frame_codec = frame_codec
def main(): """CLI interface.""" # pylint: disable=too-many-locals,too-many-statements,too-many-branches description = """Print video metadata. You may supply a list of videos, and the output for each video will be followed by a blank line to distinguish it from others. Below is the list of available options and their brief explanations. Some of the options can also be stored in a configuration file, $XDG_CONFIG_HOME/storyboard/storyboard.conf (or if $XDG_CONFIG_HOME is not defined, ~/.config/storyboard/storyboard.conf), under the "metadata-cli" section (the conf file format follows that described in https://docs.python.org/3/library/configparser.html). For more detailed explanations, see http://storyboard.rtfd.org/en/stable/metadata-cli.html (or replace "stable" with the version you are using). """ parser = argparse.ArgumentParser(description=description) parser.add_argument('videos', nargs='+', metavar='VIDEO', help="Path(s) to the video file(s).") parser.add_argument( '--ffprobe-bin', metavar='NAME', help="""The name/path of the ffprobe binary. The binay is guessed from OS type if this option is not specified.""") parser.add_argument('--include-sha1sum', '-s', action='store_const', const=True, help="Include SHA-1 digest of the video(s).") parser.add_argument('--exclude-sha1sum', action='store_true', help="""Exclude SHA-1 digest of the video(s). Overrides '--include-sha1sum'. This option is only useful if include_sha1sum is turned on by default in the config file.""") parser.add_argument( '--verbose', '-v', choices=['auto', 'on', 'off'], nargs='?', const='auto', help="""Whether to print progress information to stderr. Default is 'auto'.""") parser.add_argument('--version', action='version', version=version.__version__) cli_args = parser.parse_args() if 'XDG_CONFIG_HOME' in os.environ: config_file = os.path.join(os.environ['XDG_CONFIG_HOME'], 'storyboard/storyboard.conf') else: config_file = os.path.expanduser( '~/.config/storyboard/storyboard.conf') defaults = { 'ffprobe_bin': fflocate.guess_bins()[1], 'include_sha1sum': False, 'verbose': 'auto', } optreader = util.OptionReader( cli_args=cli_args, config_files=config_file, section='metadata-cli', defaults=defaults, ) ffprobe_bin = optreader.opt('ffprobe_bin') include_sha1sum = optreader.opt('include_sha1sum', opttype=bool) if cli_args.exclude_sha1sum: # force override include_sha1sum = False verbose = optreader.opt('verbose') if verbose == 'on': print_progress = True elif verbose == 'off': print_progress = False else: if verbose != 'auto': msg = ("warning: '%s' is a not a valid argument to --verbose; " "ignoring and using 'auto' instead\n" % verbose) sys.stderr.write(msg) if include_sha1sum and sys.stderr.isatty(): print_progress = True else: print_progress = False # test ffprobe_bin try: fflocate.check_bins((None, ffprobe_bin)) except OSError: msg = ("fatal error: '%s' does not exist on PATH or is corrupted " "(expected FFprobe)\n" % ffprobe_bin) sys.stderr.write(msg) exit(1) # real stuff happens from here returncode = 0 for video in cli_args.videos: # pylint: disable=invalid-name try: v = Video(video, params={ 'ffprobe_bin': ffprobe_bin, 'print_progress': print_progress, }) except OSError as err: sys.stderr.write("error: %s\n\n" % str(err)) returncode = 1 continue metadata_string = v.format_metadata(params={ 'include_sha1sum': include_sha1sum, 'print_progress': print_progress, }) if print_progress: # print one empty line to separate progress info and output # content sys.stderr.write("\n") print(metadata_string) print('') return returncode
def main(): """CLI interface.""" # pylint: disable=too-many-statements,too-many-branches description = """Generate video storyboards with metadata reports. You may supply a list of videos. For each video, the generated storyboard image will be saved to a secure temporary file, and its absolute path will be printed to stdout for further manipulations (permanent archiving, uploading to an image hosting website, etc). Note that stdout is guaranteed to only receive the image paths, one per line, so you may embed this program in a streamlined script; stderr, on the other hand, may receive progress information without guaranteed format (see the --print-progress option). Below is the list of available options and their brief explanations. The options can also be stored in a configuration file, $XDG_CONFIG_HOME/storyboard/storyboard.conf (or if $XDG_CONFIG_HOME is not defined, ~/.config/storyboard/storyboard.conf), under the "storyboard-cli" section (the conf file format follows that described in https://docs.python.org/3/library/configparser.html). Note that the storyboard is in fact much more customizable; see the API reference of storyboard.storyboard.StoryBoard.gen_storyboard. Those customization parameters are not exposed in the CLI, but you may easily write a wrapper script around the storyboard.storyboard if you'd like to. For more detailed explanations, see https://storyboard.readthedocs.io/en/stable/storyboard-cli.html (or replace "stable" with the version you are using). """ parser = argparse.ArgumentParser(description=description) parser.add_argument('videos', nargs='+', metavar='VIDEO', help="Path(s) to the video file(s).") parser.add_argument( '--ffmpeg-bin', metavar='NAME', help="""The name/path of the ffmpeg binary. The binay is guessed from OS type if this option is not specified.""") parser.add_argument( '--ffprobe-bin', metavar='NAME', help="""The name/path of the ffprobe binary. The binay is guessed from OS type if this option is not specified.""") parser.add_argument( '-f', '--output-format', choices=['jpeg', 'png'], help="Output format of the storyboard image. Default is JPEG.") parser.add_argument( '--quality', type=int, help="""Quality of the output image, should be an integer between 1 and 100. Only meaningful when the output format is JPEG. Default is 85.""") parser.add_argument( '--video-duration', type=float, metavar='SECONDS', help="""Video duration in seconds (float). By default the duration is extracted from container metadata, but in case it is not available or wrong, use this option to correct it and get a saner storyboard. Note however that this option activates output seeking (i.e., seeking the video frame by frame) in thumbnail generation, so it will be *infinitely* slower than without this option.""") parser.add_argument( '--exclude-sha1sum', '-s', action='store_const', const=True, help="Exclude SHA-1 digest of the video(s) from storyboard(s).") parser.add_argument('--include-sha1sum', action='store_true', help="""Include SHA-1 digest of the video(s). Overrides '--exclude-sha1sum'. This option is only useful if exclude_sha1sum is turned on by default in the config file.""") parser.add_argument( '--verbose', '-v', choices=['auto', 'on', 'off'], nargs='?', const='auto', help="""Whether to print progress information to stderr. Default is 'auto'.""") parser.add_argument('--version', action='version', version=version.__version__) cli_args = parser.parse_args() if 'XDG_CONFIG_HOME' in os.environ: config_file = os.path.join(os.environ['XDG_CONFIG_HOME'], 'storyboard/storyboard.conf') else: config_file = os.path.expanduser( '~/.config/storyboard/storyboard.conf') ffmpeg_bin_guessed, ffprobe_bin_guessed = fflocate.guess_bins() defaults = { 'ffmpeg_bin': ffmpeg_bin_guessed, 'ffprobe_bin': ffprobe_bin_guessed, 'output_format': 'jpeg', 'quality': 85, 'video_duration': None, 'exclude-sha1sum': False, 'verbose': 'auto', } optreader = util.OptionReader( cli_args=cli_args, config_files=config_file, section='storyboard-cli', defaults=defaults, ) bins = (optreader.opt('ffmpeg_bin'), optreader.opt('ffprobe_bin')) output_format = optreader.opt('output_format') if output_format not in ['jpeg', 'png']: msg = ("fatal error: output format should be either 'jpeg' or 'png'; " "'%s' received instead\n" % output_format) sys.stderr.write(msg) exit(1) suffix = '.jpg' if output_format == 'jpeg' else '.png' quality = optreader.opt('quality', opttype=int) video_duration = optreader.opt('video_duration', opttype=float) include_sha1sum = not optreader.opt('exclude_sha1sum', opttype=bool) if cli_args.include_sha1sum: # force override include_sha1sum = True verbose = optreader.opt('verbose') if verbose == 'on': print_progress = True elif verbose == 'off': print_progress = False else: if verbose != 'auto': msg = ("warning: '%s' is a not a valid argument to --verbose; " "ignoring and using 'auto' instead\n" % verbose) sys.stderr.write(msg) if sys.stderr.isatty(): print_progress = True else: print_progress = False # test bins try: fflocate.check_bins(bins) except OSError: msg = ("fatal error: at least one of '%s' and '%s' does not exist on " "PATH or is corrupted (expected ffmpeg and ffprobe)\n" % bins) sys.stderr.write(msg) exit(1) # real stuff happens from here returncode = 0 for video in cli_args.videos: try: storyboard_image = StoryBoard( video, params={ 'bins': bins, 'video_duration': video_duration, 'print_progress': print_progress, }).gen_storyboard( params={ 'include_sha1sum': include_sha1sum, 'print_progress': print_progress, }) except OSError as err: sys.stderr.write("error: %s\n\n" % str(err)) returncode = 1 continue tempfd, storyboard_file = tempfile.mkstemp(prefix='storyboard-', suffix=suffix) os.close(tempfd) if output_format == 'jpeg': storyboard_image.save(storyboard_file, 'jpeg', quality=quality, optimize=True, progressive=True) else: # 'png' storyboard_image.save(storyboard_file, 'png', optimize=True) if print_progress: sys.stderr.write("\n") sys.stderr.write("storyboard saved to: ") sys.stderr.flush() print(storyboard_file) sys.stderr.write("\n") else: print(storyboard_file) return returncode