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 "$@" |