mirror of
https://github.com/natelandau/shell-scripting-templates.git
synced 2025-11-11 06:23:47 -05:00
966 lines
34 KiB
Bash
Executable File
966 lines
34 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# ##################################################
|
|
# My Generic BASH script template
|
|
#
|
|
version="2.2.0" # Sets version variable for this script
|
|
#
|
|
scriptTemplateVersion="1.5.0" # Version of scriptTemplate.sh that this script is based on
|
|
#
|
|
#
|
|
# HISTORY:
|
|
#
|
|
# * 2015-03-31 - v1.0.0 - First creation
|
|
# * 2015-04-07 - v1.1.0 - Added support for music files
|
|
# * 2015-05-26 - v1.1.1 - Fixed log level on downsize720
|
|
# * 2015-08-30 - v1.2.0 - Support for recursive directory conversions
|
|
# * 2016-01-03 - v1.2.1 - Fixed XLD audio conversion which had been broken
|
|
# * 2016-01-05 - v2.0.0 - Major refactoring of code.
|
|
# - Better JSON parsing via JQ
|
|
# - MIME type discovery based on file metadata (rather than user input)
|
|
# - Better error handling
|
|
# - Better renaming of existing files
|
|
# * 2016-01-09 - v2.0.1 - Fixed bug on in video preset section where 'videoPreset' was malformed
|
|
# * 2016-01-09 - v2.1.0 - Support for audiobooks with .m4b
|
|
# - Support for concatenating multiple audio files
|
|
# - Support for --probe function to output ffprobe data in JSON
|
|
# * 2016-01-13 - v2.1.1 - Fixed bug with concatenating files
|
|
# * 2016-01-14 - v2.2.0 - Added 'spoken word' audio profiles
|
|
#
|
|
# ##################################################
|
|
|
|
# Provide a variable with the location of this script.
|
|
scriptPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
|
|
# Source Scripting Utilities
|
|
# -----------------------------------
|
|
# These shared utilities provide many functions which are needed to provide
|
|
# the functionality in this boilerplate. This script will fail if they can
|
|
# not be found.
|
|
# -----------------------------------
|
|
|
|
utilsLocation="${scriptPath}/../lib/utils.sh" # Update this path to find the utilities.
|
|
|
|
if [ -f "${utilsLocation}" ]; then
|
|
source "${utilsLocation}"
|
|
else
|
|
echo "Please find the file util.sh and add a reference to it in this script. Exiting."
|
|
exit 1
|
|
fi
|
|
|
|
# trapCleanup Function
|
|
# -----------------------------------
|
|
# Any actions that should be taken if the script is prematurely
|
|
# exited. Always call this function at the top of your script.
|
|
# -----------------------------------
|
|
function trapCleanup() {
|
|
echo ""
|
|
# Delete temp files, if any
|
|
if is_dir "${tmpDir}"; then
|
|
rm -r "${tmpDir}"
|
|
fi
|
|
die "Exit trapped." # Edit this if you like.
|
|
}
|
|
|
|
# Set Flags
|
|
# -----------------------------------
|
|
# Flags which can be overridden by user input.
|
|
# Default values are below
|
|
# -----------------------------------
|
|
quiet=false
|
|
printLog=false
|
|
verbose=false
|
|
recursive=false
|
|
force=false
|
|
strict=false
|
|
debug=false
|
|
safeRun=0
|
|
downsize720=0
|
|
deleteOriginal=false
|
|
newFileFlag=false
|
|
videoFoundFlag=false
|
|
audioFoundFlag=false
|
|
spokenWord=false
|
|
probe=false
|
|
concat=false
|
|
XLD=0
|
|
args=()
|
|
|
|
# Set Temp Directory
|
|
# -----------------------------------
|
|
# Create temp directory with three random numbers and the process ID
|
|
# in the name. This directory is removed automatically at exit.
|
|
# -----------------------------------
|
|
tmpDir="/tmp/${scriptName}.${RANDOM}.${RANDOM}.${RANDOM}.${$}"
|
|
(umask 077 && mkdir "${tmpDir}") || {
|
|
die "Could not create temporary directory! Exiting."
|
|
}
|
|
|
|
# Logging
|
|
# -----------------------------------
|
|
# Log is only used when the '-l' flag is set.
|
|
#
|
|
# To never save a logfile change variable to '/dev/null'
|
|
# Save to Desktop use: $HOME/Desktop/${scriptBasename}.log
|
|
# Save to standard user log location use: $HOME/Library/Logs/${scriptBasename}.log
|
|
# -----------------------------------
|
|
logFile="${HOME}/Library/Logs/${scriptBasename}.log"
|
|
|
|
# Check for Dependencies
|
|
# -----------------------------------
|
|
# Arrays containing package dependencies needed to execute this script.
|
|
# The script will fail if dependencies are not installed. For Mac users,
|
|
# most dependencies can be installed automatically using the package
|
|
# manager 'Homebrew'. Mac applications will be installed using
|
|
# Homebrew Casks. Ruby and gems via RVM.
|
|
# -----------------------------------
|
|
homebrewDependencies=(ffmpeg jq rename)
|
|
caskDependencies=(xld)
|
|
gemDependencies=()
|
|
|
|
function mainScript() {
|
|
############## Begin Script Here ###################
|
|
####################################################
|
|
|
|
# File extension mappings - ie - are we working with a video or music file?
|
|
videoTypes=(mp4 mov avi mkv wmv flv ogg m4p m4v 3gp divx h264)
|
|
audioTypes=(mp3 m4a aiff aac m4p wav wma flac m4b)
|
|
|
|
function identifyFiles() {
|
|
local item
|
|
local maxSearchDepth
|
|
local fileType
|
|
local fileMatch
|
|
|
|
# Assign user specified files
|
|
if [ -n "${userFile}" ]; then
|
|
filesToConvert+=("${userFile}")
|
|
fi
|
|
if [ -n "${args}" ]; then
|
|
filesToConvert+=("${args[@]}")
|
|
fi
|
|
|
|
# Respect recursive file searches when requested
|
|
if ${recursive}; then
|
|
maxSearchDepth=4
|
|
else
|
|
maxSearchDepth=1
|
|
fi
|
|
|
|
# Assign files based on MEDIATYPE extension
|
|
if [ -n "${MEDIATYPE}" ]; then
|
|
while read item; do
|
|
filesToConvert+=("${item}")
|
|
done < <(find . -name "*.${MEDIATYPE}" -type f -maxdepth ${maxSearchDepth})
|
|
fi
|
|
|
|
# Figure out what to do if a user doesn't specify any files or input types
|
|
if [ ${#filesToConvert[@]} -eq 0 ]; then
|
|
notice "You didn't specify a specific file."
|
|
seek_confirmation "Do you want to convert all VIDEO files?"
|
|
if is_confirmed; then
|
|
for fileType in "${videoTypes[@]}"; do
|
|
while read fileMatch; do
|
|
filesToConvert+=("${fileMatch}")
|
|
done < <(find . -name "*.${fileType}" -type f -maxdepth ${maxSearchDepth})
|
|
done
|
|
else
|
|
seek_confirmation "Do you want to convert all AUDIO files?"
|
|
if is_confirmed; then
|
|
for fileType in "${audioTypes[@]}"; do
|
|
while read fileMatch; do
|
|
filesToConvert+=("${fileMatch}")
|
|
done < <(find . -name "*.${fileType}" -type f -maxdepth ${maxSearchDepth})
|
|
done
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Error handling: Ensure the count of elements in ${filesToConvert[@]} is not zero.
|
|
if [ ${#filesToConvert[@]} -eq 0 ]; then
|
|
error "We couldn't find any files to act on. Exiting."
|
|
safeExit
|
|
fi
|
|
|
|
# debug
|
|
verbose "filesToConvert=${filesToConvert[@]}"
|
|
}
|
|
|
|
function testFiles() {
|
|
# This function ensures that the specified files exist and can be converted
|
|
local extension
|
|
|
|
# Ensure we have a valid file
|
|
if [ ! -f "${fileToTest}" ]; then die "We can't find '${fileToTest}' or it is not valid."; fi
|
|
|
|
# Ensure the extension is known to this script
|
|
extension="${fileToTest##*.}" # Grab file extension of input file
|
|
extension="$(echo "${extension}" | tr '[:upper:]' '[:lower:]')" # Lowercase the extension if needed
|
|
|
|
# See if the extension is in the specified extension mappings
|
|
if [[ ! "${videoTypes[*]}" =~ "${extension}" ]] && [[ ! "${audioTypes[*]}" =~ "${extension}" ]]; then
|
|
error "The extension of '${extension} ' was not recognized."
|
|
die "We don't know what to do with ${fileToTest}."
|
|
fi
|
|
}
|
|
|
|
function convertToFormat() {
|
|
|
|
# Do things on video files
|
|
if [[ "${videoTypes[*]}" =~ "${file##*.}" ]]; then
|
|
# if you wanted a default target format for a specific input format,
|
|
# you would put it here.
|
|
# Set the default video conversion format to mp4
|
|
case "${format}" in
|
|
'Matroska / WebM') outputFormat='mp4' ;;
|
|
*) outputFormat='mp4' ;;
|
|
esac
|
|
fi
|
|
|
|
# Do things on audio files
|
|
if [[ "${audioTypes[*]}" =~ "${file##*.}" ]]; then
|
|
|
|
# Ensure a user sets an output format since we don't have a default.
|
|
if [[ -z ${userOutput} ]]; then
|
|
warning "Please specify an output audio format using '-o, --output'. Exiting"
|
|
safeExit
|
|
fi
|
|
|
|
# Confirm if a user wants to convert audio to it's own format
|
|
# if [[ "${file##*.}" == "${userOutput}" ]]; then
|
|
# warning "You are attempting to convert a ${file##*.} file to ${userOutput}."
|
|
# seek_confirmation "Do you want to proceed?"
|
|
# if is_not_confirmed; then
|
|
# continue
|
|
# fi
|
|
# fi
|
|
fi
|
|
|
|
# Reads user input for format (-o, --output)
|
|
if [ -n "${userOutput}" ]; then
|
|
outputFormat="${userOutput,,}" #&& verbose "outputFormat=${outputFormat}"
|
|
fi
|
|
}
|
|
|
|
function concatFiles() {
|
|
if ${concat}; then
|
|
# Create variable for ffmpeg to concacenate the files
|
|
concatConvert="concat:$(join "|" "${filesToConvert[@]}")"
|
|
# Create the variable for the deleteOriginalFile function
|
|
concatDelete="$(join " " ${filesToConvert[@]})"
|
|
|
|
# Ask the user for the name of the newly created file
|
|
input "Please enter the name of the new file to be created. [ENTER]: "
|
|
read concatOutput
|
|
|
|
fi
|
|
}
|
|
|
|
function parseJSON() {
|
|
local ii
|
|
local videoFoundFlag
|
|
local audioFoundFlag
|
|
local informationFile
|
|
|
|
# Create the temporary JSON file
|
|
informationFile="${tmpDir}/${file////}.json"
|
|
|
|
verbose "Reading audio data and writing JSON to tmp"
|
|
verbose "-> ${informationFile}"
|
|
|
|
# Output a JSON file for each video asset being parsed.
|
|
ffprobe -v quiet -print_format json -show_format -show_streams "${file}" >> "${informationFile}"
|
|
|
|
# Debugging: Uncomment either (or both) of these lines
|
|
# ffprobe -v quiet -print_format json -show_format -show_streams "${file}" >> "${file}.json"
|
|
# cat "${informationFile}"
|
|
|
|
# If the '--probe' flag is set, we print the media file information onto the screen.
|
|
if "${probe}"; then
|
|
cat "${informationFile}"
|
|
return 0
|
|
fi
|
|
|
|
# Read the necessary information from the JSON
|
|
format="$(jq -r ".format.format_long_name" "${informationFile}")"
|
|
formatName="$(jq -r ".format.format_name" "${informationFile}")"
|
|
jsonFilename="$(jq -r ".format.filename" "${informationFile}")"
|
|
formatBit_Rate="$(jq -r ".format.bit_rate" "${informationFile}")"
|
|
numStreams="$(jq -r ".format.nb_streams" "${informationFile}")"
|
|
|
|
# Iterate over results
|
|
ii=0
|
|
while [ ${ii} -lt ${numStreams} ]; do
|
|
if [[ $(jq -r --arg ii ${ii} ".streams[$ii | tonumber].codec_type" "${informationFile}") == "video" ]]; then
|
|
videoHeight="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].height" "${informationFile}")"
|
|
videoWidth="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].width" "${informationFile}")"
|
|
videoCodec="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].codec_name" "${informationFile}")"
|
|
videoCodecLong="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].codec_long_name" "${informationFile}")"
|
|
|
|
videoFoundFlag=true # Used for error handling to confirm we found a video stream
|
|
fi
|
|
if [ $(jq -r --arg ii ${ii} ".streams[$ii | tonumber].codec_type" "${informationFile}") == "audio" ]; then
|
|
audioCodec="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].codec_name" "${informationFile}")"
|
|
audioCodecLong="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].codec_long_name" "${informationFile}")"
|
|
audioSampleRate="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].sample_rate" "${informationFile}")"
|
|
audioBitRate="$(jq -r --arg ii ${ii} ".streams[$ii | tonumber].bit_rate" "${informationFile}")"
|
|
|
|
audioFoundFlag=true # Used for error handling to confirm we found an audio stream
|
|
fi
|
|
|
|
# Increase loop count
|
|
ii=$[$ii+1]
|
|
done
|
|
|
|
# Error Handling for video files
|
|
if [[ "${videoTypes[*]}" =~ "${file##*.}" ]]; then
|
|
if [ "${videoFoundFlag}" = "false" ]; then
|
|
warning "Missing video stream for '${file}'."
|
|
seek_confirmation "Inspect the file with ffprobe?"
|
|
if is_confirmed; then
|
|
ffprobe -v quiet -print_format json -show_format -show_streams "${file}"
|
|
safeExit
|
|
else
|
|
notice "Exiting"
|
|
safeExit
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Error Handling for audio files
|
|
if [[ "${audioTypes[*]}" =~ "${file##*.}" ]]; then
|
|
if [ "${audioFoundFlag}" = "false" ]; then
|
|
warning "Missing audio stream for '${file}'."
|
|
seek_confirmation "Inspect the file with ffprobe?"
|
|
if is_confirmed; then
|
|
ffprobe -v quiet -print_format json -show_format -show_streams "${file}"
|
|
safeExit
|
|
else
|
|
notice "Exiting"
|
|
safeExit
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function convertVideo() {
|
|
|
|
verbose "Running 'convertVideo' function"
|
|
|
|
# SET AUDIO INFORMATION
|
|
# Copy audio in compatible formats. Re-encode audio when needed
|
|
################################################################
|
|
|
|
# Pick the best aac audio encoder
|
|
if $(ffmpeg -version | grep enable-libfdk-aac >/dev/null); then
|
|
aacEncoder="libfdk_aac"
|
|
else
|
|
aacencoder="libfaac"
|
|
fi
|
|
|
|
supportedAudioCodecs=(aac ac3 eac3)
|
|
if [[ "${supportedAudioCodecs[*]}" =~ "${audioCodec}" ]]; then
|
|
videoAudioCommand="-c:a copy"
|
|
else
|
|
videoAudioCommand="-c:a ${aacEncoder} -b:a 160k"
|
|
fi
|
|
|
|
# SET VIDEO INFORMATION
|
|
# Copy video in compatible formats. Re-encode audio when needed.
|
|
# Set resizing options
|
|
################################################################
|
|
|
|
# Is input video a known preset size?
|
|
if [[ "${videoWidth}" == "1920" && "${videoHeight}" == "1080" ]] || [[ "${videoWidth}" == "1920" && "${videoHeight}" == "816" ]]; then
|
|
videoPreset="1080p" && verbose "Input video has preset: 1080p"
|
|
fi
|
|
if [[ "${videoWidth}" == "1280" && "${videoHeight}" == "720" ]] || [[ "${videoWidth}" == "1280" && "$videoHeight" == "544" ]]; then
|
|
videoPreset="720p" && verbose "Input video has preset: 720p"
|
|
fi
|
|
if [[ "${videoWidth}" == "720" && "${videoHeight}" == "576" ]]; then
|
|
videoPreset="DVPAL" && verbose "Input video has preset: DVPAL"
|
|
fi
|
|
|
|
# Enable resizing of videos
|
|
# #############################
|
|
|
|
# Fail if user sets more than one value
|
|
if [[ -n "${videoSize}" ]] && [[ -n "${height}" || -n "${width}" ]]; then
|
|
die "We can't set a 'height', 'width', and a 'size' at the same time."
|
|
fi
|
|
|
|
# if user sets both a height and width, run that as a videoSize variable
|
|
if [[ -n "${width}" ]] && [[ -n "${height}" ]]; then
|
|
videoSize="${width}x${height}" && verbose "videoSize=${width}x${height}"
|
|
unset width
|
|
unset height
|
|
fi
|
|
|
|
# downsize720
|
|
# Commonly used function to downsize 1080p to 720p
|
|
if [[ "${downsize720}" == "1" ]]; then
|
|
if [[ "${videoPreset}" == "1080p" ]]; then
|
|
videoSize="hd720" && verbose "videoSize=hd720"
|
|
else
|
|
notice "Skipping ${file}. It's not 1080p"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
# Do something when a user specifies a size
|
|
if [[ -n "${videoSize}" ]]; then
|
|
|
|
# Don't resize videos to their same size
|
|
if [[ "${videoSize}" == "hd720" ]] || [[ "${videoSize}" == "720" || "${videoSize}" == "1280x720" ]]; then
|
|
if [[ "$videoPreset" == "720p" ]]; then
|
|
notice "${file} is already 720p. Skipping...."
|
|
continue
|
|
fi
|
|
elif [[ "${videoSize}" == "hd1080" || "${videoSize}" == "1080" || "${videoSize}" == "1920x1080" ]]; then
|
|
if [[ "$videoPreset" == "1080p" ]]; then
|
|
notice "${file} is already 1080p. Skipping...."
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
# Confirm if user wants to upscale their video
|
|
if [[ "${videoSize}" == "hd1080" || "${videoSize}" == "1080" ]]; then
|
|
userWidth="1920"
|
|
userHeight="1080"
|
|
elif [[ "${videoSize}" == "hd720" ]] || [[ "${videoSize}" == "720" ]]; then
|
|
userWidth="1280"
|
|
userHeight="720"
|
|
else
|
|
# break user's video size into a height and width
|
|
userWidth=$(echo ${videoSize} | cut -f1 -dx)
|
|
userHeight=$(echo ${videoSize} | cut -f2 -dx)
|
|
if [ "${userWidth}" -gt "${videoWidth}" ] || [ "${userHeight}" -gt "${videoHeight}" ]; then
|
|
seek_confirmation "Upscale "${file}" to "${videoSize}"? It is already "${videoWidth}"x"${videoHeight}"."
|
|
if is_not_confirmed; then
|
|
continue
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Finally, set the resize variable
|
|
videoResize="-vf scale=${videoSize}" && verbose "videoResize='-vf scale=${videoSize}'"
|
|
fi
|
|
|
|
# Scaling variables
|
|
# ####################################
|
|
if is_not_empty "${height}"; then
|
|
if [ "${height}" -gt "${videoHeight}" ]; then
|
|
seek_confirmation "Upscale "${file}" to height "${height}"? It is already "${videoWidth}"x"${videoHeight}"."
|
|
if is_not_confirmed; then
|
|
continue
|
|
fi
|
|
fi
|
|
videoResize="-vf scale=-1:${height}" && verbose "videoResize='-vf scale=-1:${height}'"
|
|
fi
|
|
if is_not_empty "${width}"; then
|
|
if [ "${width}" -gt "${videoWidth}" ]; then
|
|
seek_confirmation "Upscale "${file}" to width "${width}"? It is already "${videoWidth}"x"${videoHeight}"."
|
|
if is_not_confirmed; then
|
|
continue
|
|
fi
|
|
fi
|
|
videoResize="-vf scale=${width}:-1" && verbose "videoResize='-vf scale=${width}:-1'"
|
|
fi
|
|
|
|
# Copy h264 when possible
|
|
# Save precious time by not re-encoding files that are already H264.
|
|
# ###########################
|
|
if [[ "${videoCodec}" == "h264" ]] && [[ -z "${videoResize}" ]]; then
|
|
videoCommand="-c:v copy" && verbose "videoCommand='-c:v copy'"
|
|
else
|
|
videoCommand="-c:v libx264 -crf 18 -preset slow" && verbose "videoCommand'-c:v libx264 -crf 18 -preset slow'"
|
|
fi
|
|
|
|
# Don't convert to self if no other options set
|
|
if [[ -z ${height} && -z ${width} && -z ${videoSize} && ${downsize720} == "0" && "${outputFormat}" == "${file##*.}" ]]; then
|
|
error "Can't convert a '"${file##*.}"' file to itself. Skipping ${file}"
|
|
continue
|
|
fi
|
|
}
|
|
|
|
function convertAudio() {
|
|
|
|
verbose "Running 'convertAudio' function"
|
|
# note on XLD: If you have XLD installed and configured, lossless audio conversion
|
|
# will be run using built-in XLD profiles. You can disable this by
|
|
# changing ensuring that XLD=0 in sections below.
|
|
#
|
|
# #############################################################
|
|
|
|
# Build the Conversion Command
|
|
# ########################################
|
|
|
|
# Set mono/64k conversion for audiobooks
|
|
if ${spokenWord}; then
|
|
monoConversion="-ab 96k -ac 1"
|
|
fi
|
|
|
|
if [[ "${userOutput,,}" == "alac" ]]; then
|
|
if type_exists "xld"; then
|
|
XLD=1
|
|
#audioConvertCommand="--profile FLACtoALAC"
|
|
audioConvertCommand="-f alac"
|
|
outputFormat="m4a"
|
|
else
|
|
audioConvertCommand="-acodec alac"
|
|
fi
|
|
elif [[ "${userOutput,,}" == "flac" ]]; then
|
|
if type_exists "xld"; then
|
|
XLD=1
|
|
audioConvertCommand="-f flac"
|
|
else
|
|
audioConvertCommand="-c:a flac ${monoConversion}"
|
|
fi
|
|
elif [[ "${userOutput,,}" == "aac" || "${userOutput,,}" == "m4a" ]]; then
|
|
outputFormat="m4a"
|
|
# Pick the best aac audio encoder
|
|
if $(ffmpeg -version | grep enable-libfdk-aac >/dev/null); then
|
|
# set variable bit rate to '5', the highest unless we are doing spoken word
|
|
if ${spokenWord}; then
|
|
aacEncoder='libfdk_aac'
|
|
else
|
|
aacEncoder='libfdk_aac -vbr 5'
|
|
fi
|
|
else
|
|
aacEncoder='libfaac -q:a 400'
|
|
fi
|
|
if type_exists "xlds"; then
|
|
XLD=1
|
|
audioConvertCommand="-f aac" && verbose "using xld. audioConvertCommand = -f aac "
|
|
else
|
|
audioConvertCommand="-acodec ${aacEncoder} ${monoConversion}"
|
|
fi
|
|
elif [[ "${userOutput,,}" == "mp3" ]]; then
|
|
# Can we convert to mp3? Do we have an ffmpeg encoder?
|
|
if $(ffmpeg -version | grep enable-libmp3lame >/dev/null); then
|
|
mp3Encoder='libmp3lame'
|
|
# else
|
|
# warning "No workable ffmpeg mp3 encoder. Skipping ${f}..."
|
|
# continue
|
|
fi
|
|
# Take user specified bitrate
|
|
if [ -n "$bitrate" ]; then
|
|
bitrate="${bitrate%k}k" # Ensure 'k' is at the end of any bitrate sent to ffmpeg
|
|
ffmpegBitrate="-ab ${bitrate}"
|
|
else
|
|
ffmpegBitrate="-qscale:a 0"
|
|
fi
|
|
# Set mono conversion for audiobooks
|
|
if ${spokenWord}; then
|
|
ffmpegBitrate="${monoConversion}"
|
|
fi
|
|
audioConvertCommand="-acodec ${mp3Encoder} ${ffmpegBitrate} -map_metadata 0 -id3v2_version 3"
|
|
elif [[ "${userOutput,,}" == "m4b" ]]; then
|
|
# m4b is exactly the same as m4a except it tells Apple that the file is an audiobook.
|
|
# so we use m4a conversion here and then rename the output file to m4b
|
|
# The main difference here is that we make the assumption that audiobooks don't
|
|
# need high fidelity or stereo so we make them mono and low bit-rate.
|
|
|
|
# Pick the best aac audio encoder
|
|
if $(ffmpeg -version | grep enable-libfdk-aac >/dev/null); then
|
|
# set variable bit rate to '5', the highest
|
|
aacEncoder="libfdk_aac ${monoConversion} -f m4a"
|
|
else
|
|
aacEncoder="libfaac ${monoConversion} -f m4a"
|
|
fi
|
|
audioConvertCommand="-acodec ${aacEncoder}"
|
|
else
|
|
warning "We don't know what to do with audio format: '${outputFormat}'."
|
|
warning "Exiting"
|
|
safeExit
|
|
fi
|
|
}
|
|
|
|
function setOutputDirectory() {
|
|
if ${verbose}; then v="-v" ; fi
|
|
|
|
# Use the user's specified directory to save the new file in if specified.
|
|
# default to use the location of the original file.
|
|
if [[ -n "${saveDir}" ]]; then
|
|
if [[ -e "${saveDir}" && ! -d "${saveDir}" ]]; then
|
|
die "${saveDir} exists but is not a directory"
|
|
fi
|
|
if [[ ! -d "${saveDir}" ]]; then
|
|
seek_confirmation "${saveDir} does not exist. Create?"
|
|
if is_confirmed; then
|
|
mkdir ${v} "${saveDir}"
|
|
else
|
|
die "Can't run without a place to save the files."
|
|
fi
|
|
fi
|
|
# remove trailing slash if included. Add back to ensure it's always there.
|
|
outputDir="${saveDir%/}/"
|
|
else
|
|
outputDir=$(dirname "${file}")
|
|
outputDir="${outputDir#*/}"
|
|
outputDir="${outputDir%/}/"
|
|
fi
|
|
}
|
|
|
|
function setOutputFile() {
|
|
|
|
# This function creates the name of new file to be generated by the conversion
|
|
# ##################
|
|
|
|
# Set output filename
|
|
output="$(basename "${file%.*}").${outputFormat}"
|
|
# Add the output directory
|
|
output="${outputDir}${output}"
|
|
|
|
#Override the output file to the user's input if we are concatenating files
|
|
if [ -n "$concatOutput" ]; then
|
|
output="${outputDir}${concatOutput%.*}.${outputFormat}"
|
|
success "output=${output}"
|
|
fi
|
|
|
|
# Confirm we're not overwriting an existing file
|
|
# If we are, append '.new' to the name
|
|
fileFlag=0
|
|
while [[ fileFlag -eq 0 ]]; do
|
|
if [ -e "${output}" ]; then
|
|
# output="$(basename "${file%.*}").new."${outputFormat}""
|
|
output="$(basename "${output%.*}").new.${outputFormat}"
|
|
# Add the directory back to the file
|
|
output="${outputDir}${output}"
|
|
# Set a flag so that we can rename this file back to the original name if
|
|
# a user elects to delete the original file
|
|
newFileFlag=true
|
|
else
|
|
fileFlag=1
|
|
fi
|
|
done
|
|
|
|
verbose "new file name will be: ${output}"
|
|
}
|
|
|
|
function doConvert() {
|
|
verbose "running doConvert function"
|
|
|
|
if ${verbose}; then v="-v" ; fi
|
|
|
|
# Respect the 'Quiet' flag
|
|
if "${quiet}"; then
|
|
verbose "running in quiet mode"
|
|
ffquiet="-loglevel quiet"
|
|
fi
|
|
|
|
# When concatenating files, we need a different $file variable
|
|
if ${concat}; then
|
|
file="${concatConvert}"
|
|
fi
|
|
|
|
# Respect the 'logfile' flag
|
|
if ${printLog}; then ffmpegLog=">> ${logFile}"; fi
|
|
|
|
# Use XLD for audio file conversion if available
|
|
if [[ "${XLD}" == "1" && "${spokenWord}" == "false" ]]; then
|
|
verbose "Running XLD commands for audio. No FFMPEG."
|
|
# Respect --safe flag.
|
|
if [[ "${safeRun}" == "1" ]]; then
|
|
notice "xld -o "${output}" ${audioConvertCommand} "${file}""
|
|
else
|
|
verbose "xld -o "${output}" ${audioConvertCommand} "${file}""
|
|
xld -o "${output}" ${audioConvertCommand} "${file}"
|
|
fi
|
|
else # Use ffmpeg when XLD is set to 0
|
|
# Respect --safe flag.
|
|
if [[ "${safeRun}" == "1" ]]; then
|
|
notice "ffmpeg -i "${file}" ${videoResize} ${videoCommand} ${videoAudioCommand} ${audioConvertCommand} "${output}" ${ffquiet}"
|
|
else
|
|
verbose "ffmpeg -i "${file}" ${videoResize} ${videoCommand} ${videoAudioCommand} ${audioConvertCommand} "${output}" ${ffquiet}"
|
|
ffmpeg -i "${file}" ${videoResize} ${videoCommand} ${videoAudioCommand} ${audioConvertCommand} "${output}" ${ffquiet}
|
|
fi
|
|
fi
|
|
|
|
# Unset variables to get ready for the next file
|
|
unset jsonFilename
|
|
unset formatBit_Rate
|
|
unset numStreams
|
|
unset videoCodec
|
|
unset videoCodecLong
|
|
unset format
|
|
unset formatName
|
|
unset videoHeight
|
|
unset videoWidth
|
|
unset videoPreset
|
|
unset audioCodec
|
|
unset audioCodecLong
|
|
unset audioSampleRate
|
|
unset audioBitRate
|
|
}
|
|
|
|
function deleteOriginalFile() {
|
|
if ${verbose}; then v="-v" ; fi
|
|
|
|
# first, ensure we don't delete the originals if we're in a safe rune
|
|
if [[ "${safeRun}" == "0" ]]; then
|
|
if ${deleteOriginal}; then
|
|
if ${concat}; then
|
|
for fileToDelete in "${filesToConvert[@]}"; do
|
|
rm -f ${v} "${fileToDelete}"
|
|
done
|
|
else
|
|
rm -f ${v} "${file}"
|
|
if [[ "${file##*.}" == "${outputFormat}" ]]; then
|
|
mv ${v} "${output}" "${outputDir}${file}"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# break the loop if we're concatenating files.
|
|
if ${concat}; then
|
|
break
|
|
fi
|
|
}
|
|
|
|
## RUN THE SCRIPT ##
|
|
####################################
|
|
|
|
# First we need to identify the files
|
|
identifyFiles
|
|
|
|
# Then we test that all files can be operated upon
|
|
for fileToTest in "${filesToConvert[@]}"; do testFiles; done
|
|
|
|
# Then we respect the '--probe' option if requested
|
|
if "${probe}"; then
|
|
for file in "${filesToConvert[@]}"; do
|
|
info "Probing ${file}"
|
|
verbose "ffprobe -v quiet -print_format json -show_format -show_streams ${file}"
|
|
parseJSON
|
|
done
|
|
safeExit
|
|
fi
|
|
|
|
# Then we work on the individual files. This is the fun part.
|
|
for file in "${filesToConvert[@]}"; do
|
|
if ! ${concat}; then info "Working on ${file}"; fi
|
|
# First we grab the metadata of the file and assign it to variables
|
|
parseJSON
|
|
# Then we set the expected output format
|
|
convertToFormat
|
|
# Then we set the appropriate conversion commands
|
|
if [[ "${videoTypes[*]}" =~ "$(echo ${file##*.} | tr '[:upper:]' '[:lower:]')" ]]; then convertVideo; fi
|
|
if [[ "${audioTypes[*]}" =~ "$(echo ${file##*.} | tr '[:upper:]' '[:lower:]')" ]]; then convertAudio; fi
|
|
# Then we tell the script where to output the file
|
|
setOutputDirectory
|
|
# Then we check if we are supposed to concatenate the files
|
|
concatFiles
|
|
# Then we generate the name for the new file
|
|
setOutputFile
|
|
# Then we actually do the conversion
|
|
doConvert
|
|
# Then we delete the original file (if requested)
|
|
deleteOriginalFile
|
|
done
|
|
|
|
|
|
####################################################
|
|
############### End Script Here ####################
|
|
}
|
|
|
|
|
|
usage() {
|
|
# Print usage
|
|
echo -n "${scriptName} ${version} [OPTION]... [ARGUMENT]...
|
|
|
|
${bold}DESCRIPTION${reset}
|
|
This is a media conversion script which converts audio and video into many
|
|
different formats. It was written to eliminate the need to remember specific
|
|
FFMPEG commands. All conversions can be performed with ffmpeg. XLD on a mac
|
|
is used for audio conversions when available.
|
|
|
|
${BOLD}DEPENDENCIES${reset}
|
|
This script makes heavy use of shared functions contained in ${bold}lib/utils.sh${reset} which are
|
|
available as part of the same Github repository. It will fail if these are not found.
|
|
|
|
This script relies on ${bold}ffmpeg${reset} for video and audio conversion as well as ${bold}jq${reset}
|
|
for parsing JSON files. These must be installed prior to usage. If run on a
|
|
mac, the script will attempt to help you install these packages using ${bold}Homebrew${reset}.
|
|
|
|
${bold}General Options:${reset}
|
|
${bold}-h, --help${reset} Display this help and exit
|
|
${bold}-d, --debug${reset} Runs script in BASH debug mode ('set -x')
|
|
${bold}--force${reset} Skip all user interaction. Implied 'Yes' to all actions.
|
|
${bold}-l, --log${reset} Print log to file
|
|
${bold}-q, --quiet${reset} Quiet (no output)
|
|
${bold}-v, --verbose${reset} Output more information. (Items echoed to 'verbose')
|
|
${bold} --safe${reset} Runs the script without actually invoking FFMPEG. Will simply print
|
|
the FFMPEG commands to the terminal
|
|
${bold}--version${reset} Output version information and exit
|
|
${bold}--recursive${reset} Will search recursively through directories
|
|
${bold}--probe${reset} Outputs file metadata via ffprobe in JSON format. Does no coversion.
|
|
${bold}--concat${reset} Will concatenate audio files together into a single output. Good for audiobooks.
|
|
${bold}--spoken${reset} Will convert audio files to mono, 64k. Good for audiobooks
|
|
|
|
|
|
${bold}File Options:${reset}
|
|
${bold}-f, --file${reset} Specify a specific file to take actions on.
|
|
${bold}-i, --input${reset} Specify the specific media type to search for and take action
|
|
on. (mov', 'mp4', 'mp3')
|
|
${bold}-o, --output${reset} Specify the output format for the file(s) to be converted to.
|
|
('mkv', 'mp4', 'm4a')
|
|
${bold}--delete ${reset} Delete the original file after conversion.
|
|
${bold}--saveDir${reset} Specify a folder for the converted files to be saved to. Defaults to
|
|
the directory the script is invoked in.
|
|
|
|
${bold}Video Specific Options:${reset}
|
|
For video files, if no output format is specified, the script will attempt to convert every
|
|
video file it finds in the directory into h264 .mp4 format.
|
|
|
|
${bold}--size${reset} Set the size of the target file. Can be one of 'hd1080', 'hd720',
|
|
or 'HeightxWidth' (ie. 1920x1080)
|
|
${bold}--height${reset} Set a height in pixels to scale the target file.
|
|
${bold}--width${reset} Set a width in pixels to scale the target file.
|
|
${bold}--downsize720${reset} Searches for 1080p videos and downsizes them to 720p. No need
|
|
for other scaling options. Used to reduce the size of video collections
|
|
when quality is not the primary need.
|
|
|
|
${bold}Audio Specific Options:${reset}
|
|
${bold}--bitrate${reset} Set a bit rate for audio conversions. Note, this does not
|
|
effect video conversions.
|
|
|
|
${bold}EXAMPLES:${reset}
|
|
Search for all *.flac files in a directory and convert them to
|
|
Apple Lossless (alac). Once the conversion is complete, original files
|
|
will be deleted.
|
|
|
|
$ convertMedia -i flac -o alac --delete
|
|
|
|
Search for all 1080p files in a directory and downsize them to 720p.
|
|
|
|
$ convertMedia --downsize720
|
|
|
|
Convert a Windows Media file (file.wmv) to h264 (mp4).
|
|
|
|
$ convertMedia -o mp4 file.wmv
|
|
|
|
Do a recursive search for all directories beneath the current one. In each
|
|
directory, search for .avi files and convert them to .mp4
|
|
|
|
$ convertMedia -o mp4 -i avi --recursive
|
|
|
|
Create an Apple audiobook (m4b) from all mp3 files in a directory
|
|
|
|
$ convertMedia -i mp3 -o m4b --concat
|
|
|
|
"
|
|
}
|
|
|
|
# Iterate over options breaking -ab into -a -b when needed and --foo=bar into
|
|
# --foo bar
|
|
optstring=h
|
|
unset options
|
|
while (($#)); do
|
|
case $1 in
|
|
# If option is of type -ab
|
|
-[!-]?*)
|
|
# Loop over each character starting with the second
|
|
for ((i=1; i < ${#1}; i++)); do
|
|
c=${1:i:1}
|
|
|
|
# Add current char to options
|
|
options+=("-$c")
|
|
|
|
# If option takes a required argument, and it's not the last char make
|
|
# the rest of the string its argument
|
|
if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then
|
|
options+=("${1:i+1}")
|
|
break
|
|
fi
|
|
done
|
|
;;
|
|
|
|
# If option is of type --foo=bar
|
|
--?*=*) options+=("${1%%=*}" "${1#*=}") ;;
|
|
# add --endopts for --
|
|
--) options+=(--endopts) ;;
|
|
# Otherwise, nothing special
|
|
*) options+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
set -- "${options[@]}"
|
|
unset options
|
|
|
|
# Print help if no arguments were passed.
|
|
# Uncomment to force arguments when invoking the script
|
|
# [[ $# -eq 0 ]] && set -- "--help"
|
|
|
|
# Read the options and set stuff
|
|
while [[ ${1} = -?* ]]; do
|
|
case ${1} in
|
|
|
|
-f|--file) shift; userFile="$1" ;;
|
|
-i|--input) shift; MEDIATYPE="$1" ;;
|
|
-o|--output) shift; userOutput="$1" ;;
|
|
--safe) safeRun=1 ;;
|
|
--size) shift; videoSize="$1" ;;
|
|
--height) shift; height="$1" ;;
|
|
--width) shift; width="$1" ;;
|
|
--probe) probe=true; safeRun=1 ;;
|
|
--downsize720) downsize720=1 ;;
|
|
--recursive) recursive=true ;;
|
|
--delete) deleteOriginal=true ;;
|
|
--concat) concat=true; ;;
|
|
--spoken) spokenWord=true ;;
|
|
--saveDir) shift; saveDir="$1" ;;
|
|
--bitrate) shift; bitrate="$1" ;;
|
|
-h|--help) usage >&2; safeExit ;;
|
|
--force) force=true ;;
|
|
--version) echo "$(basename $0) $version"; safeExit ;;
|
|
-v|--verbose) verbose=true ;;
|
|
-l|--log) printLog=true ;;
|
|
-q|--quiet) quiet=true ;;
|
|
-d|--debug) debug=true;;
|
|
--endopts) shift; break ;;
|
|
*) die "invalid option: '$1'." ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Store the remaining part as arguments.
|
|
args+=("$@")
|
|
|
|
# ############# ############# #############
|
|
# ## TIME TO RUN THE SCRIPT ##
|
|
# ## ##
|
|
# ## You shouldn't need to edit anything ##
|
|
# ## beneath this line ##
|
|
# ## ##
|
|
# ############# ############# #############
|
|
|
|
# Trap bad exits with your cleanup function
|
|
trap trapCleanup EXIT INT TERM
|
|
|
|
# Set IFS to preferred implementation
|
|
IFS=$' \n\t'
|
|
|
|
# Exit on error. Append '||true' when you run the script if you expect an error.
|
|
set -o errexit
|
|
|
|
# Run in debug mode, if set
|
|
if ${debug}; then set -x ; fi
|
|
|
|
# Exit on empty variable
|
|
if ${strict}; then set -o nounset ; fi
|
|
|
|
# Bash will remember & return the highest exitcode in a chain of pipes.
|
|
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`, for example.
|
|
set -o pipefail
|
|
|
|
# Invoke the checkDependenices function to test for Bash packages. Uncomment if needed.
|
|
checkDependencies
|
|
|
|
# Run your script
|
|
mainScript
|
|
|
|
# Exit cleanly
|
|
safeExit
|