sudo mkdir -p /usr/local/bin &>/dev/null ; sudo curl -fsSL -o /usr/local/bin/getframe "https://gist.githubusercontent.com/snown/a818afd18c9e2087731bdf7ce0a85195/raw/getframe.sh" && sudo chmod a+x /usr/local/bin/getframe
getframe install-folder-action
| #!/usr/bin/env bash | |
| ################################################################################ | |
| # Config | |
| ################################################################################ | |
| SEEK_TIME="00:00:03.000" | |
| VIDEO_FILE_TYPES=( | |
| mkv | |
| webm | |
| flv | |
| vob | |
| ogg | |
| ogv | |
| drc | |
| gifv | |
| mng | |
| avi | |
| mov | |
| qt | |
| wmv | |
| yuv | |
| rm | |
| rmvb | |
| asf | |
| amv | |
| mp4 | |
| m4v | |
| mp | |
| svi | |
| 3gp | |
| flv | |
| f4v | |
| ) | |
| ################################################################################ | |
| # Main Functions | |
| ################################################################################ | |
| function getframe::print_help { | |
| local script_name="$(basename "$0")" | |
| _pansi --reset | |
| here_printf <<-HELP | |
| $(_pansi --bold "USAGE:") | |
| ${script_name} [-h|--help] | |
| ${script_name} [-v|--verbose]... [-q|--quiet] [-f|--force] [-s|--seek <seek_time>] [-i|--input <video_file>]... <video_file> [<video_file>]... | |
| ${script_name} install-folder-action | |
| $(_pansi --bold "FLAGS:") | |
| -h, --help Print this help message. | |
| $(_pansi --bold "OPTIONS:") | |
| -v, --verbose How verbose logging should be. Can be passed multiple times. | |
| -q, --quiet Suppress extraneous printing to the terminal. | |
| Cancels any previous '-v' flags. | |
| -f, --force Force overwrite existing frame grabs. Skips files if not | |
| specified. | |
| -s, --seek Set how far into the video the frame should be grabbed from. | |
| Seek time can be specified in one of two ways: | |
| '$(_pansi --italic --underline "[HH:]MM:SS[.m...]")' Where '$(_pansi --italic HH)' is an optional number of hours, | |
| '$(_pansi --italic MM)' is the number of minutes, '$(_pansi --italic SS)' is number of seconds, | |
| and '$(_pansi --italic m)' is a decimal value of '$(_pansi --italic SS)' | |
| '$(_pansi --italic --underline "S+[.m...]")' Where '(_pansi --italic S)' expresses the number of seconds, with | |
| the optional decimal part '(_pansi --italic m)'. | |
| -i, --input A video file from which to grab a frame. | |
| Can be repeated any number of times | |
| <video_file> A video file from which to grab a frame. | |
| Can be repeated any number of times. | |
| At least one has to be specified, unless using '-i|--input' | |
| $(_pansi --bold "COMMANDS:") | |
| install-folder-action Installs a folder action on the system to be used with Finder. Will call this script anytime a file is added to the specified folder. | |
| HELP | |
| } | |
| VERBOSITY=1 | |
| function getframe { | |
| local passthrough_args=() | |
| if [[ $# -eq 0 ]]; then | |
| getframe::print_help | |
| exit 1 | |
| fi | |
| while [[ $# -gt 0 ]]; do | |
| local _key="$1" | |
| case "${_key}" in | |
| --install-watch-script|install-folder-action|install) | |
| getframe::install_watch_script | |
| exit 0 | |
| ;; | |
| -v|--verbose) | |
| VERBOSITY=$((VERBOSITY + 1)) | |
| ;; | |
| -q|--quiet) | |
| VERBOSITY=0 | |
| ;; | |
| -h|--help) | |
| getframe::print_help | |
| exit 0 | |
| ;; | |
| -v?*|-q?*|-h?*|-f?*) | |
| local _next="${_key:2}" | |
| local _current="${_key:0:2}" | |
| if [[ -n "${_next}" && "${_next}" != "${_key}" ]]; then | |
| _begins_with_short_option -o "vqhf" "${_next}" && shift && set -- "${_current}" "-${_next}" "$@" | |
| getframe::log -v 5 "Next arguments: $@" | |
| continue | |
| fi | |
| ;; | |
| *) | |
| getframe::log -v 5 "Passthrough Key: ${_key}" | |
| passthrough_args+=( "${_key}" ) | |
| ;; | |
| esac | |
| shift | |
| done | |
| # Perform image extraction | |
| getframe::make_image "${passthrough_args[@]}" | |
| } | |
| ################################################################################ | |
| # Helper Functions | |
| ################################################################################ | |
| function _pansi { | |
| if ! command -v pansi &>/dev/null; then | |
| local tmp_pansi="$(mktemp -t pansi)" | |
| curl -fsSL -o "${tmp_pansi}" "https://raw.githubusercontent.com/snown/pansi/master/pansi" | |
| source "${tmp_pansi}" | |
| fi | |
| pansi "$@" | |
| } | |
| function _ffmpeg { | |
| # Check for ffmpeg | |
| if ! command -v ffmpeg &>/dev/null; then | |
| # if no ffmpeg look for homebrew | |
| if ! command -v brew &>/dev/null; then | |
| # if no homebrew, install homebrew | |
| getframe::log -v 1 "Installing Homebrew..." | |
| ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" | |
| fi | |
| # Install ffmpeg | |
| getframe::log -v 1 "Installing FFMpeg..." | |
| brew install ffmpeg | |
| fi | |
| ffmpeg "$@" | |
| } | |
| # Find the absolute path of an exiting file or directory | |
| #------------------------------------------------------------------------------- | |
| function _abspath { | |
| if [[ -d "$1" ]] | |
| then | |
| pushd "$1" >/dev/null | |
| pwd | |
| popd >/dev/null | |
| elif [[ -e $1 ]] | |
| then | |
| pushd "$(dirname "$1")" >/dev/null | |
| echo "$(pwd)/$(basename "$1")" | |
| popd >/dev/null | |
| else | |
| echo "$1" does not exist! >&2 | |
| return 127 | |
| fi | |
| } | |
| function _scriptSudo { | |
| local SUDO_IS_ACTIVE | |
| SUDO_IS_ACTIVE=$(sudo -n uptime 2>&1|grep "load"|wc -l) | |
| if [[ ${SUDO_IS_ACTIVE} -le 0 ]]; then | |
| # Ask for the administrator password upfront | |
| sudo -v | |
| # Keep-alive: update existing `sudo` time stamp until `.osx` has finished | |
| while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & | |
| fi | |
| sudo "$@" | |
| } | |
| function _begins_with_short_option { | |
| local next_option | |
| local all_short_options="" | |
| while [[ $# -gt 0 ]]; do | |
| local _key="$1" | |
| case "${_key}" in | |
| -o|--options) | |
| all_short_options="${all_short_options}$2" | |
| shift | |
| ;; | |
| *) | |
| if [[ $# -gt 1 ]]; then | |
| shift && set -- "-o" "${_key}" "$@" | |
| continue | |
| fi | |
| next_option="${1:0:1}" | |
| ;; | |
| esac | |
| shift | |
| done | |
| test "${all_short_options}" = "${all_short_options/${next_option}/}" && return 1 || return 0 | |
| } | |
| function getframe::log { | |
| local print_level=0 | |
| if [[ "$1" == "-v" ]]; then | |
| print_level=$2 | |
| shift | |
| shift | |
| fi | |
| if [[ $print_level -le $VERBOSITY ]]; then | |
| for string in "$@"; do | |
| echo "${string}" > /dev/tty | |
| done | |
| fi | |
| } | |
| function getframe::is_video_file { | |
| local result=false | |
| local nocasematch_bu=$(shopt -p nocasematch; true) | |
| shopt -s nocasematch | |
| for file_ext in "${VIDEO_FILE_TYPES[@]}"; do | |
| if [[ "$1" == *"${file_ext}" ]]; then | |
| result=true | |
| break | |
| fi | |
| done | |
| # Restore case matching | |
| ${nocasematch_bu} | |
| ${result} | |
| } | |
| function getframe::install_watch_script { | |
| local username="$(whoami)" | |
| local script_path="$(_abspath "$0")" | |
| local tmp_script="$(mktemp -t getframe_watch_script)" | |
| local installation_path="/Library/Scripts/Folder Action Scripts/add - Still Frame from Videos.scpt" | |
| cat > "${tmp_script}" <<-APPLESCRIPT | |
| on adding folder items to theAttachedFolder after receiving theNewItems | |
| set posixDirectory to the quoted form of (POSIX path of (theAttachedFolder as alias)) | |
| try | |
| do shell script "sudo -u '${username}' -i getframe " & posixDirectory | |
| on error | |
| try | |
| do shell script "'${script_path}' " & posixDirectory | |
| on error | |
| tell application "Finder" | |
| display alert "Error" message "Could not find 'getframe' command" | |
| end tell | |
| end try | |
| end try | |
| end adding folder items to | |
| APPLESCRIPT | |
| _scriptSudo mkdir -p "$(dirname "${installation_path}")" | |
| if [[ -e "${installation_path}" ]]; then | |
| _scriptSudo rm "${installation_path}" | |
| fi | |
| _scriptSudo mv "${tmp_script}" "${installation_path}" | |
| } | |
| function getframe::make_image { | |
| local input_candidates=() | |
| local seeking="${SEEK_TIME}" | |
| local force=false | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| local _key="$1" | |
| case "${_key}" in | |
| -i|--input) | |
| local input="$2" | |
| shift | |
| local input_candidates=() | |
| if [[ ! -r "${input}" ]]; then | |
| getframe::log -v 2 "Cannot read input: ${input}" | |
| shift | |
| continue | |
| fi | |
| if [[ -d "${input}" ]]; then | |
| while IFS= read -d '' -r file; do input_candidates+=("$file"); done < <(find "${input}" -maxdepth 1 -type f -not -iname "*.png" -print0) | |
| elif [[ -f "${input}" ]]; then | |
| input_candidates=( "${input}" ) | |
| else | |
| getframe::log -v 2 "Input needs to be a file or directory: ${input}" | |
| shift | |
| continue | |
| fi | |
| ;; | |
| -s|-ss|--seek) | |
| seeking="$2" | |
| shift | |
| ;; | |
| -f|--force) | |
| getframe::log -v 5 "Should force" | |
| force=true | |
| ;; | |
| *) | |
| getframe::log -v 5 "Possible positional input: \"${_key}\"" | |
| if [[ -f "${_key}" || -d "${_key}" ]]; then | |
| getframe::log -v 5 "\"${_key}\" is either a file or directory" | |
| shift && set -- "--input" "${_key}" "$@" | |
| getframe::log -v 5 "Input params: $*" | |
| continue | |
| else | |
| getframe::log -v 1 "Unrecognized option: ${_key}" | |
| getframe::print_help | |
| exit 1 | |
| fi | |
| ;; | |
| esac | |
| shift | |
| done | |
| local video_files=() | |
| for file in "${input_candidates[@]}"; do | |
| if ! getframe::is_video_file "${file}"; then | |
| getframe::log -v 3 "Input was not recognized as a video file: ${file}" | |
| continue | |
| fi | |
| if [[ -e "${file%.*}.png" && "${force}" != true ]]; then | |
| getframe::log -v 3 "Frame grab already exists for file: ${file}" | |
| continue | |
| fi | |
| video_files+=( "${file}" ) | |
| done | |
| for file in "${video_files[@]}"; do | |
| _ffmpeg -y -ss "${seeking}" -i "${file}" -frames:v 1 "${file%.*}.png" | |
| done | |
| } | |
| ################################################################################ | |
| # Run | |
| ################################################################################ | |
| getframe "$@" |