|
#!/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 "$@" |