Files
shell-scripting-templates/bin/convertMedia
Nathaniel Landau a715e23547 minor updates
2016-01-19 13:19:36 -05:00

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