Skip to content

Instantly share code, notes, and snippets.

@snown
Last active April 28, 2020 19:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save snown/a818afd18c9e2087731bdf7ce0a85195 to your computer and use it in GitHub Desktop.
Save snown/a818afd18c9e2087731bdf7ce0a85195 to your computer and use it in GitHub Desktop.
Grab a frame of a video file (Specifically for Mac)

Installing the script

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

Install the folder action

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 "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment