summaryrefslogtreecommitdiff
path: root/dotfiles/common/.local/bin
diff options
context:
space:
mode:
Diffstat (limited to 'dotfiles/common/.local/bin')
-rwxr-xr-xdotfiles/common/.local/bin/AAXtoMP3908
-rwxr-xr-xdotfiles/common/.local/bin/ai-assistants45
-rwxr-xr-xdotfiles/common/.local/bin/any2flac44
-rwxr-xr-xdotfiles/common/.local/bin/any2opus102
-rwxr-xr-xdotfiles/common/.local/bin/build-emacs.sh213
-rwxr-xr-xdotfiles/common/.local/bin/clobberall20
-rw-r--r--dotfiles/common/.local/bin/cron/README.md11
-rwxr-xr-xdotfiles/common/.local/bin/cron/checkup17
-rwxr-xr-xdotfiles/common/.local/bin/cron/crontog6
-rwxr-xr-xdotfiles/common/.local/bin/dab72
-rwxr-xr-xdotfiles/common/.local/bin/ec2
-rwxr-xr-xdotfiles/common/.local/bin/em2
-rwxr-xr-xdotfiles/common/.local/bin/et2
-rwxr-xr-xdotfiles/common/.local/bin/extractaudio2
-rwxr-xr-xdotfiles/common/.local/bin/get-arch-iso.sh78
-rwxr-xr-xdotfiles/common/.local/bin/gitconfig_defaults5
-rwxr-xr-xdotfiles/common/.local/bin/ifinstalled12
-rwxr-xr-xdotfiles/common/.local/bin/linkhandler26
-rwxr-xr-xdotfiles/common/.local/bin/mkplaylist173
-rwxr-xr-xdotfiles/common/.local/bin/mpd_play_yt_stream14
-rwxr-xr-xdotfiles/common/.local/bin/msmtp-enqueue.sh44
-rwxr-xr-xdotfiles/common/.local/bin/msmtp-listqueue.sh8
-rwxr-xr-xdotfiles/common/.local/bin/msmtp-runqueue.sh69
-rwxr-xr-xdotfiles/common/.local/bin/open-file-in-eww2
-rwxr-xr-xdotfiles/common/.local/bin/opus2mp33
-rwxr-xr-xdotfiles/common/.local/bin/org-capture.sh159
-rwxr-xr-xdotfiles/common/.local/bin/org-protocol-setup9
-rwxr-xr-xdotfiles/common/.local/bin/ps-mem28
-rwxr-xr-xdotfiles/common/.local/bin/refresharchkeys6
-rwxr-xr-xdotfiles/common/.local/bin/ssh-createkeys3
-rwxr-xr-xdotfiles/common/.local/bin/timezone-change68
-rwxr-xr-xdotfiles/common/.local/bin/timezone-set16
-rwxr-xr-xdotfiles/common/.local/bin/torwrap7
-rwxr-xr-xdotfiles/common/.local/bin/updatemirrors20
-rwxr-xr-xdotfiles/common/.local/bin/warpinator-start11
35 files changed, 2207 insertions, 0 deletions
diff --git a/dotfiles/common/.local/bin/AAXtoMP3 b/dotfiles/common/.local/bin/AAXtoMP3
new file mode 100755
index 0000000..adc91ef
--- /dev/null
+++ b/dotfiles/common/.local/bin/AAXtoMP3
@@ -0,0 +1,908 @@
+#!/usr/bin/env bash
+
+
+# ========================================================================
+# Command Line Options
+
+# Usage Synopsis.
+usage=$'\nUsage: AAXtoMP3 [--flac] [--aac] [--opus ] [--single] [--level <COMPRESSIONLEVEL>]
+[--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [--authcode <AUTHCODE>] [--no-clobber]
+[--target_dir <PATH>] [--complete_dir <PATH>] [--validate] [--loglevel <LOGLEVEL>]
+[--keep-author <N>] [--author <AUTHOR>] [--{dir,file,chapter}-naming-scheme <STRING>]
+[--use-audible-cli-data] [--continue <CHAPTERNUMBER>] {FILES}\n'
+codec=libmp3lame # Default encoder.
+extension=mp3 # Default encoder extension.
+level=-1 # Compression level. Can be given for mp3, flac and opus. -1 = default/not specified.
+mode=chaptered # Multi file output
+auth_code= # Required to be set via file or option.
+targetdir= # Optional output location. Note default is basedir of AAX file.
+dirNameScheme= # Custom directory naming scheme, default is $genre/$author/$title
+customDNS=0
+fileNameScheme= # Custom file naming scheme, default is $title
+customFNS=0
+chapterNameScheme= # Custom chapter naming scheme, default is '$title-$(printf %0${#chaptercount}d $chapternum) $chapter' (BookTitle-01 Chapter 1)
+customCNS=0
+completedir= # Optional location to move aax files once the decoding is complete.
+container=mp3 # Just in case we need to change the container. Used for M4A to M4B
+VALIDATE=0 # Validate the input aax file(s) only. No Transcoding of files will occur
+loglevel=1 # Loglevel: 0: Show progress only; 1: default; 2: a little more information, timestamps; 3: debug
+noclobber=0 # Default off, clobber only if flag is enabled
+continue=0 # Default off, If set Transcoding is skipped and chapter splitting starts at chapter continueAt.
+continueAt=1 # Optional chapter to continue splitting the chapters.
+keepArtist=-1 # Default off, if set change author metadata to use the passed argument as field
+authorOverride= # Override the author, ignoring the metadata
+audibleCli=0 # Default off, Use additional data gathered from mkb79/audible-cli
+aaxc_key= # Initialize variables, in case we need them in debug_vars
+aaxc_iv= # Initialize variables, in case we need them in debug_vars
+
+# -----
+# Code tip Do not have any script above this point that calls a function or a binary. If you do
+# the $1 will no longer be a ARGV element. So you should only do basic variable setting above here.
+#
+# Process the command line options. This allows for un-ordered options. Sorta like a getops style
+while true; do
+ case "$1" in
+ # Flac encoding
+ -f | --flac ) codec=flac; extension=flac; mode=single; container=flac; shift ;;
+ # Apple m4a music format.
+ -a | --aac ) codec=copy; extension=m4a; mode=single; container=m4a; shift ;;
+ # Ogg Format
+ -o | --opus ) codec=libopus; extension=opus; container=ogg; shift ;;
+ # If appropriate use only a single file output.
+ -s | --single ) mode=single; shift ;;
+ # If appropriate use only a single file output.
+ -c | --chaptered ) mode=chaptered; shift ;;
+ # This is the same as --single option.
+ -e:mp3 ) codec=libmp3lame; extension=mp3; mode=single; container=mp3; shift ;;
+ # Identical to --acc option.
+ -e:m4a ) codec=copy; extension=m4a; mode=single; container=mp4; shift ;;
+ # Similar to --aac but specific to audio books
+ -e:m4b ) codec=copy; extension=m4b; mode=single; container=mp4; shift ;;
+ # Change the working dir from AAX directory to what you choose.
+ -t | --target_dir ) targetdir="$2"; shift 2 ;;
+ # Use a custom directory naming scheme, with variables.
+ -D | --dir-naming-scheme ) dirNameScheme="$2"; customDNS=1; shift 2 ;;
+ # Use a custom file naming scheme, with variables.
+ -F | --file-naming-scheme ) fileNameScheme="$2"; customFNS=1; shift 2 ;;
+ # Use a custom chapter naming scheme, with variables.
+ --chapter-naming-scheme ) chapterNameScheme="$2"; customCNS=1; shift 2 ;;
+ # Move the AAX file to a new directory when decoding is complete.
+ -C | --complete_dir ) completedir="$2"; shift 2 ;;
+ # Authorization code associate with the AAX file(s)
+ -A | --authcode ) auth_code="$2"; shift 2 ;;
+ # Don't overwrite the target directory if it already exists
+ -n | --no-clobber ) noclobber=1; shift ;;
+ # Extremely verbose output.
+ -d | --debug ) loglevel=3; shift ;;
+ # Set loglevel.
+ -l | --loglevel ) loglevel="$2"; shift 2 ;;
+ # Validate ONLY the aax file(s) No transcoding occurs
+ -V | --validate ) VALIDATE=1; shift ;;
+ # continue splitting chapters at chapter continueAt
+ --continue ) continueAt="$2"; continue=1; shift 2 ;;
+ # Use additional data got with mkb79/audible-cli
+ --use-audible-cli-data ) audibleCli=1; shift ;;
+ # Compression level
+ --level ) level="$2"; shift 2 ;;
+ # Keep author number n
+ --keep-author ) keepArtist="$2"; shift 2 ;;
+ # Author override
+ --author ) authorOverride="$2"; shift 2 ;;
+ # Command synopsis.
+ -h | --help ) printf "$usage" $0 ; exit ;;
+ # Standard flag signifying the end of command line processing.
+ -- ) shift; break ;;
+ # Anything else stops command line processing.
+ * ) break ;;
+
+ esac
+done
+
+# -----
+# Empty argv means we have nothing to do so lets bark some help.
+if [ "$#" -eq 0 ]; then
+ printf "$usage" $0
+ exit 1
+fi
+
+# Setup safer bash script defaults.
+set -o errexit -o noclobber -o nounset -o pipefail
+
+# ========================================================================
+# Utility Functions
+
+# -----
+# debug
+# debug "Some longish message"
+debug() {
+ if [ $loglevel == 3 ] ; then
+ echo "$(date "+%F %T%z") DEBUG ${1}"
+ fi
+}
+
+# -----
+# debug dump contents of a file to STDOUT
+# debug "<full path to file>"
+debug_file() {
+ if [ $loglevel == 3 ] ; then
+ echo "$(date "+%F %T%z") DEBUG"
+ echo "=Start=========================================================================="
+ cat "${1}"
+ echo "=End============================================================================"
+ fi
+}
+
+# -----
+# debug dump a list of internal script variables to STDOUT
+# debug_vars "Some Message" var1 var2 var3 var4 var5
+debug_vars() {
+ if [ $loglevel == 3 ] ; then
+ msg="$1"; shift ; # Grab the message
+ args=("$@") # Grab the rest of the args
+
+ # determine the length of the longest key
+ l=0
+ for (( n=0; n<${#args[@]}; n++ )) ; do
+ (( "${#args[$n]}" > "$l" )) && l=${#args[$n]}
+ done
+
+ # Print the Debug Message
+ echo "$(date "+%F %T%z") DEBUG ${msg}"
+ echo "=Start=========================================================================="
+
+ # Using the max length of a var name we dynamically create the format.
+ fmt="%-"${l}"s = %s\n"
+
+ for (( n=0; n<${#args[@]}; n++ )) ; do
+ eval val="\$${args[$n]}" ; # We save off the value of the var in question for ease of coding.
+
+ echo "$(printf "${fmt}" ${args[$n]} "${val}" )"
+ done
+ echo "=End============================================================================"
+ fi
+}
+
+# -----
+# log
+log() {
+ if [ "$((${loglevel} > 1))" == "1" ] ; then
+ echo "$(date "+%F %T%z") ${1}"
+ else
+ echo "${1}"
+ fi
+}
+
+# -----
+#progressbar produces a progressbar in the style of
+# process: |####### | XX% (part/total unit)
+# which is gonna be overwritten by the next line.
+
+progressbar() {
+ #get input
+ part=${1}
+ total=${2}
+
+ #compute percentage and make print_percentage the same length regardless of the number of digits.
+ percentage=$((part*100/total))
+ if [ "$((percentage<10))" = "1" ]; then print_percentage=" $percentage"
+ elif [ "$((percentage<100))" = "1" ]; then print_percentage=" $percentage"
+ else print_percentage="$percentage"; fi
+
+ #draw progressbar with one # for every 5% and blank spaces for the missing part.
+ progressbar=""
+ for (( n=0; n<(percentage/5); n++ )) ; do progressbar="$progressbar#"; done
+ for (( n=0; n<(20-(percentage/5)); n++ )) ; do progressbar="$progressbar "; done
+
+ #print progressbar
+ echo -ne "Chapter splitting: |$progressbar| $print_percentage% ($part/$total chapters)\r"
+}
+# Print out what we have already after command line processing.
+debug_vars "Command line options as set" codec extension mode container targetdir completedir auth_code keepArtist authorOverride audibleCli
+
+# ========================================================================
+# Variable validation
+
+if [ $(uname) = 'Linux' ]; then
+ GREP="grep"
+ FIND="find"
+ SED="sed"
+else
+ GREP="ggrep"
+ FIND="gfind"
+ SED="gsed"
+fi
+
+
+# -----
+# Detect which annoying version of grep we have
+if ! [[ $(type -P "$GREP") ]]; then
+ echo "$GREP (GNU grep) is not in your PATH"
+ echo "Without it, this script will break."
+ echo "On macOS, you may want to try: brew install grep"
+ exit 1
+fi
+
+# -----
+# Detect which annoying version of find we have
+if ! [[ $(type -P "$FIND") ]]; then
+ echo "$FIND (GNU find) is not in your PATH"
+ echo "Without it, this script will break."
+ echo "On macOS, you may want to try: brew install findutils"
+ exit 1
+fi
+
+# -----
+# Detect which annoying version of sed we have
+if ! [[ $(type -P "$SED") ]]; then
+ echo "$SED (GNU sed) is not in your PATH"
+ echo "Without it, this script will break."
+ echo "On macOS, you may want to try: brew install gnu-sed"
+ exit 1
+fi
+
+# -----
+# Detect ffmpeg and ffprobe
+if [[ "x$(type -P ffmpeg)" == "x" ]]; then
+ echo "ERROR ffmpeg was not found on your env PATH variable"
+ echo "Without it, this script will break."
+ echo "INSTALL:"
+ echo "MacOS: brew install ffmpeg"
+ echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc"
+ echo "Ubuntu (20.04): sudo apt-get update; sudo apt-get install ffmpeg x264 x265 bc"
+ echo "RHEL: yum install ffmpeg"
+ exit 1
+fi
+
+# -----
+# Detect ffmpeg and ffprobe
+if [[ "x$(type -P ffprobe)" == "x" ]]; then
+ echo "ERROR ffprobe was not found on your env PATH variable"
+ echo "Without it, this script will break."
+ echo "INSTALL:"
+ echo "MacOS: brew install ffmpeg"
+ echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc"
+ echo "RHEL: yum install ffmpeg"
+ exit 1
+fi
+
+
+# -----
+# Detect if we need mp4art for cover additions to m4a & m4b files.
+if [[ "x${container}" == "xmp4" && "x$(type -P mp4art)" == "x" ]]; then
+ echo "WARN mp4art was not found on your env PATH variable"
+ echo "Without it, this script will not be able to add cover art to"
+ echo "m4b files. Note if there are no other errors the AAXtoMP3 will"
+ echo "continue. However no cover art will be added to the output."
+ echo "INSTALL:"
+ echo "MacOS: brew install mp4v2"
+ echo "Ubuntu: sudo apt-get install mp4v2-utils"
+fi
+
+# -----
+# Detect if we need mp4chaps for adding chapters to m4a & m4b files.
+if [[ "x${container}" == "xmp4" && "x$(type -P mp4chaps)" == "x" ]]; then
+ echo "WARN mp4chaps was not found on your env PATH variable"
+ echo "Without it, this script will not be able to add chapters to"
+ echo "m4a/b files. Note if there are no other errors the AAXtoMP3 will"
+ echo "continue. However no chapter data will be added to the output."
+ echo "INSTALL:"
+ echo "MacOS: brew install mp4v2"
+ echo "Ubuntu: sudo apt-get install mp4v2-utils"
+fi
+
+# -----
+# Detect if we need mediainfo for adding description and narrator
+if [[ "x$(type -P mediainfo)" == "x" ]]; then
+ echo "WARN mediainfo was not found on your env PATH variable"
+ echo "Without it, this script will not be able to add the narrator"
+ echo "and description tags. Note if there are no other errors the AAXtoMP3"
+ echo "will continue. However no such tags will be added to the output."
+ echo "INSTALL:"
+ echo "MacOS: brew install mediainfo"
+ echo "Ubuntu: sudo apt-get install mediainfo"
+fi
+
+# -----
+# Obtain the authcode from either the command line, local directory or home directory.
+# See Readme.md for details on how to acquire your personal authcode for your personal
+# audible AAX files.
+if [ -z $auth_code ]; then
+ if [ -r .authcode ]; then
+ auth_code=`head -1 .authcode`
+ elif [ -r ~/.authcode ]; then
+ auth_code=`head -1 ~/.authcode`
+ fi
+fi
+
+# -----
+# Check the target dir for if set if it is writable
+if [[ "x${targetdir}" != "x" ]]; then
+ if [[ ! -w "${targetdir}" || ! -d "${targetdir}" ]] ; then
+ echo "ERROR Target Directory does not exist or is not writable: \"$targetdir\""
+ echo "$usage"
+ exit 1
+ fi
+fi
+
+# -----
+# Check the completed dir for if set if it is writable
+if [[ "x${completedir}" != "x" ]]; then
+ if [[ ! -w "${completedir}" || ! -d "${completedir}" ]] ; then
+ echo "ERROR Complete Directory does not exist or is not writable: \"$completedir\""
+ echo "$usage"
+ exit 1
+ fi
+fi
+
+# -----
+# Check whether the loglevel is valid
+if [ "$((${loglevel} < 0 || ${loglevel} > 3 ))" = "1" ]; then
+ echo "ERROR loglevel has to be in the range from 0 to 3!"
+ echo " 0: Show progress only"
+ echo " 1: default"
+ echo " 2: a little more information, timestamps"
+ echo " 3: debug"
+ echo "$usage"
+ exit 1
+fi
+# -----
+# If a compression level is given, check whether the given codec supports compression level specifiers and whether the level is valid.
+if [ "${level}" != "-1" ]; then
+ if [ "${codec}" == "flac" ]; then
+ if [ "$((${level} < 0 || ${level} > 12 ))" = "1" ]; then
+ echo "ERROR Flac compression level has to be in the range from 0 to 12!"
+ echo "$usage"
+ exit 1
+ fi
+ elif [ "${codec}" == "libopus" ]; then
+ if [ "$((${level} < 0 || ${level} > 10 ))" = "1" ]; then
+ echo "ERROR Opus compression level has to be in the range from 0 to 10!"
+ echo "$usage"
+ exit 1
+ fi
+ elif [ "${codec}" == "libmp3lame" ]; then
+ if [ "$((${level} < 0 || ${level} > 9 ))" = "1" ]; then
+ echo "ERROR MP3 compression level has to be in the range from 0 to 9!"
+ echo "$usage"
+ exit 1
+ fi
+ else
+ echo "ERROR This codec doesnt support compression levels!"
+ echo "$usage"
+ exit 1
+ fi
+fi
+
+# -----
+# Clean up if someone hits ^c or the script exits for any reason.
+trap 'rm -r -f "${working_directory}"' EXIT
+
+# -----
+# Set up some basic working files ASAP. Note the trap will clean this up no matter what.
+working_directory=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
+metadata_file="${working_directory}/metadata.txt"
+
+# -----
+# Validate the AAX and extract the metadata associated with the file.
+validate_aax() {
+ local media_file
+ media_file="$1"
+
+ # Test for existence
+ if [[ ! -r "${media_file}" ]] ; then
+ log "ERROR File NOT Found: ${media_file}"
+ return 1
+ else
+ if [[ "${VALIDATE}" == "1" ]]; then
+ log "Test 1 SUCCESS: ${media_file}"
+ fi
+ fi
+
+ # Clear the errexit value we want to capture the output of the ffprobe below.
+ set +e errexit
+
+ # Take a look at the aax file and see if it is valid. If the source file is aaxc, we give ffprobe additional flags
+ output="$(ffprobe -loglevel warning ${decrypt_param} -i "${media_file}" 2>&1)"
+
+ # If invalid then say something.
+ if [[ $? != "0" ]] ; then
+ # No matter what lets bark that something is wrong.
+ log "ERROR: Invalid File: ${media_file}"
+ elif [[ "${VALIDATE}" == "1" ]]; then
+ # If the validate option is present then lets at least state what is valid.
+ log "Test 2 SUCCESS: ${media_file}"
+ fi
+
+ # This is a big test only performed when the --validate switch is passed.
+ if [[ "${VALIDATE}" == "1" ]]; then
+ output="$(ffmpeg -hide_banner ${decrypt_param} -i "${media_file}" -vn -f null - 2>&1)"
+ if [[ $? != "0" ]] ; then
+ log "ERROR: Invalid File: ${media_file}"
+ else
+ log "Test 3 SUCCESS: ${media_file}"
+ fi
+ fi
+
+ # Dump the output of the ffprobe command.
+ debug "$output"
+
+ # Turn it back on. ffprobe is done.
+ set -e errexit
+}
+
+validate_extra_files() {
+ local extra_media_file extra_find_command
+ extra_media_file="$1"
+ # Bash trick to delete, non greedy, from the end up until the first '-'
+ extra_title="${extra_media_file%-*}"
+
+ # Using this is not ideal, because if the naming scheme is changed then
+ # this part of the script will break
+ # AAX file: BookTitle-LC_128_44100_stereo.aax
+ # Cover file: BookTitle_(1215).jpg
+ # Chapter file: BookTitle-chapters.json
+
+ # Chapter
+ extra_chapter_file="${extra_title}-chapters.json"
+
+ # Cover
+ extra_dirname="$(dirname "${extra_media_file}")"
+ extra_find_command='$FIND "${extra_dirname}" -maxdepth 1 -regex ".*/${extra_title##*/}_([0-9]+)\.jpg"'
+ # We want the output of the find command, we will turn errexit on later
+ set +e errexit
+ extra_cover_file="$(eval ${extra_find_command})"
+ extra_eval_comm="$(eval echo ${extra_find_command})"
+ set -e errexit
+
+ if [[ "${aaxc}" == "1" ]]; then
+ # bash trick to get file w\o extention (delete from end to the first '.')
+ extra_voucher="${extra_media_file%.*}.voucher"
+ if [[ ! -r "${extra_voucher}" ]] ; then
+ log "ERROR File NOT Found: ${extra_voucher}"
+ return 1
+ fi
+ aaxc_key=$(jq -r '.content_license.license_response.key' "${extra_voucher}")
+ aaxc_iv=$(jq -r '.content_license.license_response.iv' "${extra_voucher}")
+ fi
+
+ debug_vars "Audible-cli variables" extra_media_file extra_title extra_chapter_file extra_cover_file extra_find_command extra_eval_comm extra_dirname extra_voucher aaxc_key aaxc_iv
+
+ # Test for chapter file existence
+ if [[ ! -r "${extra_chapter_file}" ]] ; then
+ log "ERROR File NOT Found: ${extra_chapter_file}"
+ return 1
+ fi
+ if [[ "x${extra_cover_file}" == "x" ]] ; then
+ log "ERROR Cover File NOT Found"
+ return 1
+ fi
+
+ debug "All expected audible-cli related file are here"
+}
+
+# -----
+# Inspect the AAX and extract the metadata associated with the file.
+save_metadata() {
+ local media_file
+ media_file="$1"
+ ffprobe -i "$media_file" 2> "$metadata_file"
+ if [[ $(type -P mediainfo) ]]; then
+ echo "Mediainfo data START" >> "$metadata_file"
+ # Mediainfo output is structured like ffprobe, so we append it to the metadata file and then parse it with get_metadata_value()
+ # mediainfo "$media_file" >> "$metadata_file"
+ # Or we only get the data we are intrested in:
+ # Description
+ echo "Track_More :" "$(mediainfo --Inform="General;%Track_More%" "$media_file")" >> "$metadata_file"
+ # Narrator
+ echo "nrt :" "$(mediainfo --Inform="General;%nrt%" "$media_file")" >> "$metadata_file"
+ # Publisher
+ echo "pub :" "$(mediainfo --Inform="General;%pub%" "$media_file")" >> "$metadata_file"
+ echo "Mediainfo data END" >> "$metadata_file"
+ fi
+ if [[ "${audibleCli}" == "1" ]]; then
+ # If we use data we got with audible-cli, we delete conflicting chapter infos
+ $SED -i '/^ Chapter #/d' "${metadata_file}"
+ # Some magic: we parse the .json generated by audible-cli.
+ # to get the output structure like the one generated by ffprobe,
+ # we use some characters (#) as placeholder, add some new lines,
+ # put a ',' after the start value, we calculate the end of each chapter
+ # as start+length, and we convert (divide) the time stamps from ms to s.
+ # Then we delete all ':' since they make a filename invalid.
+ jq -r '.content_metadata.chapter_info.chapters[] | "Chapter # start: \(.start_offset_ms/1000), end: \((.start_offset_ms+.length_ms)/1000) \n#\n# Title: \(.title)"' "${extra_chapter_file}" \
+ | tr -d ':' >> "$metadata_file"
+ fi
+ debug "Metadata file $metadata_file"
+ debug_file "$metadata_file"
+}
+
+# -----
+# Reach into the meta data and extract a specific value.
+# This is a long pipe of transforms.
+# This finds the first occurrence of the key : value pair.
+get_metadata_value() {
+ local key
+ key="$1"
+ # Find the key in the meta data file # Extract field value # Remove the following /'s "(Unabridged) <blanks> at start end and multiples.
+ echo "$($GREP --max-count=1 --only-matching "${key} *: .*" "$metadata_file" | cut -d : -f 2- | $SED -e 's#/##g;s/ (Unabridged)//;s/^[[:blank:]]\+//g;s/[[:blank:]]\+$//g' | $SED 's/[[:blank:]]\+/ /g')"
+}
+
+# -----
+# specific variant of get_metadata_value bitrate is important for transcoding.
+get_bitrate() {
+ get_metadata_value bitrate | $GREP --only-matching '[0-9]\+'
+}
+
+# Save the original value, since in the for loop we overwrite
+# $audibleCli in case the file is aaxc. If the file is the
+# old aax, reset the variable to be the one passed by the user
+originalAudibleCliVar=$audibleCli
+# ========================================================================
+# Main Transcode Loop
+for aax_file
+do
+ # If the file is in aaxc format, set the proper variables
+ if [[ ${aax_file##*.} == "aaxc" ]]; then
+ # File is the new .aaxc
+ aaxc=1
+ audibleCli=1
+ else
+ # File is the old .aax
+ aaxc=0
+ # If some previous file in the loop are aaxc, the $audibleCli variable has been overwritten, so we reset it to the original one
+ audibleCli=$originalAudibleCliVar
+ fi
+
+ debug_vars "Variables set based on file extention" aaxc originalAudibleCliVar audibleCli
+
+ # No point going on if no authcode found and the file is aax.
+ # If we use aaxc as input, we do not need it
+ # if the string $auth_code is null and the format is not aaxc; quit. We need the authcode
+ if [ -z $auth_code ] && [ "${aaxc}" = "0" ]; then
+ echo "ERROR Missing authcode, can't decode $aax_file"
+ echo "$usage"
+ exit 1
+ fi
+
+ # Validate the input aax file. Note this happens no matter what.
+ # It's just that if the validate option is set then we skip to next file.
+ # If however validate is not set and we proceed with the script any errors will
+ # case the script to stop.
+
+ # If the input file is aaxc, we need to first get the audible_key and audible_iv
+ # We get them in the function validate_extra_files
+
+ if [[ ${audibleCli} == "1" ]] ; then
+ # If we have additional files (obtained via audible-cli), be sure that they
+ # exists and they are in the correct location.
+ validate_extra_files "${aax_file}"
+ fi
+
+ # Set the needed params to decrypt the file. Needed in all command that require ffprobe or ffmpeg
+ # After validate_extra_files, since the -audible_key and -audible_iv are read in that function
+ if [[ ${aaxc} == "1" ]] ; then
+ decrypt_param="-audible_key ${aaxc_key} -audible_iv ${aaxc_iv}"
+ else
+ decrypt_param="-activation_bytes ${auth_code}"
+ fi
+
+ validate_aax "${aax_file}"
+ if [[ ${VALIDATE} == "1" ]] ; then
+ # Don't bother doing anything else with this file.
+ continue
+ fi
+
+ # -----
+ # Make sure everything is a variable. Simplifying Command interpretation
+ save_metadata "${aax_file}"
+ genre=$(get_metadata_value genre)
+ if [ "x${authorOverride}" != "x" ]; then
+ #Manual Override
+ artist="${authorOverride}"
+ album_artist="${authorOverride}"
+ else
+ if [ "${keepArtist}" != "-1" ]; then
+ # Choose artist from the one that are present in the metadata. Comma separated list of names
+ # remove leading space; 'C. S. Lewis' -> 'C.S. Lewis'
+ artist="$(get_metadata_value artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')"
+ album_artist="$(get_metadata_value album_artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')"
+ else
+ # The default
+ artist=$(get_metadata_value artist)
+ album_artist="$(get_metadata_value album_artist)"
+ fi
+ fi
+ title=$(get_metadata_value title)
+ title=${title:0:128}
+ bitrate="$(get_bitrate)k"
+ album="$(get_metadata_value album)"
+ album_date="$(get_metadata_value date)"
+ copyright="$(get_metadata_value copyright)"
+
+ # Get more tags with mediainfo
+ if [[ $(type -P mediainfo) ]]; then
+ narrator="$(get_metadata_value nrt)"
+ description="$(get_metadata_value Track_More)"
+ publisher="$(get_metadata_value pub)"
+ else
+ narrator=""
+ description=""
+ publisher=""
+ fi
+
+ # Define the output_directory
+ if [ "${customDNS}" == "1" ]; then
+ currentDirNameScheme="$(eval echo "${dirNameScheme}")"
+ else
+ # The Default
+ currentDirNameScheme="${genre}/${artist}/${title}"
+ fi
+
+ # If we defined a target directory, use it. Otherwise use the location of the AAX file
+ if [ "x${targetdir}" != "x" ] ; then
+ output_directory="${targetdir}/${currentDirNameScheme}/"
+ else
+ output_directory="$(dirname "${aax_file}")/${currentDirNameScheme}/"
+ fi
+
+ # Define the output_file
+ if [ "${customFNS}" == "1" ]; then
+ currentFileNameScheme="$(eval echo "${fileNameScheme}")"
+ else
+ # The Default
+ currentFileNameScheme="${title}"
+ fi
+ output_file="${output_directory}/${currentFileNameScheme}.${extension}"
+
+ if [[ "${noclobber}" = "1" ]] && [[ -d "${output_directory}" ]]; then
+ log "Noclobber enabled but directory '${output_directory}' exists. Exiting to avoid overwriting"
+ exit 0
+ fi
+ mkdir -p "${output_directory}"
+
+ if [ "$((${loglevel} > 0))" = "1" ]; then
+ # Fancy declaration of which book we are decoding. Including the AUTHCODE.
+ dashline="----------------------------------------------------"
+ log "$(printf '\n----Decoding---%s%s--%s--' "${title}" "${dashline:${#title}}" "${auth_code}")"
+ log "Source: ${aax_file}"
+ fi
+
+ # Big long DEBUG output. Fully describes the settings used for transcoding.
+ # Note this is a long debug command. It's not critical to operation. It's purely for people debugging
+ # and coders wanting to extend the script.
+ debug_vars "Book and Variable values" title auth_code aaxc aaxc_key aaxc_iv mode aax_file container codec bitrate artist album_artist album album_date genre copyright narrator description publisher currentDirNameScheme output_directory currentFileNameScheme output_file metadata_file working_directory
+
+
+ # Display the total length of the audiobook in format hh:mm:ss
+ # 10#$var force base-10 interpretation. By default it's base-8, so values like 08 or 09 are not octal numbers
+ total_length="$(ffprobe -v error ${decrypt_param} -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${aax_file}" | cut -d . -f 1)"
+ hours="$((total_length/3600))"
+ if [ "$((hours<10))" = "1" ]; then hours="0$hours"; fi
+ minutes="$((total_length/60-60*10#$hours))"
+ if [ "$((minutes<10))" = "1" ]; then minutes="0$minutes"; fi
+ seconds="$((total_length-3600*10#$hours-60*10#$minutes))"
+ if [ "$((seconds<10))" = "1" ]; then seconds="0$seconds"; fi
+ log "Total length: $hours:$minutes:$seconds"
+
+ # If level != -1 specify a compression level in ffmpeg.
+ compression_level_param=""
+ if [ "${level}" != "-1" ]; then
+ compression_level_param="-compression_level ${level}"
+ fi
+
+ # -----
+ if [ "${continue}" == "0" ]; then
+ # This is the main work horse command. This is the primary transcoder.
+ # This is the primary transcode. All the heavy lifting is here.
+ debug 'ffmpeg -loglevel error -stats ${decrypt_param} -i "${aax_file}" -vn -codec:a "${codec}" -ab ${bitrate} -map_metadata -1 -metadata title="${title}" -metadata artist="${artist}" -metadata album_artist="${album_artist}" -metadata album="${album}" -metadata date="${album_date}" -metadata track="1/1" -metadata genre="${genre}" -metadata copyright="${copyright}" "${output_file}"'
+ </dev/null ffmpeg -loglevel error \
+ -stats \
+ ${decrypt_param} \
+ -i "${aax_file}" \
+ -vn \
+ -codec:a "${codec}" \
+ ${compression_level_param} \
+ -ab ${bitrate} \
+ -map_metadata -1 \
+ -metadata title="${title}" \
+ -metadata artist="${artist}" \
+ -metadata album_artist="${album_artist}" \
+ -metadata album="${album}" \
+ -metadata date="${album_date}" \
+ -metadata track="1/1" \
+ -metadata genre="${genre}" \
+ -metadata copyright="${copyright}" \
+ -metadata description="${description}" \
+ -metadata composer="${narrator}" \
+ -metadata publisher="${publisher}" \
+ -f ${container} \
+ "${output_file}"
+ if [ "$((${loglevel} > 0))" == "1" ]; then
+ log "Created ${output_file}."
+ fi
+ # -----
+ fi
+ # Grab the cover art if available.
+ cover_file="${output_directory}/cover.jpg"
+ extra_crop_cover=''
+ if [ "${continue}" == "0" ]; then
+ if [ "${audibleCli}" == "1" ]; then
+ # We have a better quality cover file, copy it.
+ if [ "$((${loglevel} > 1))" == "1" ]; then
+ log "Copy cover file to ${cover_file}..."
+ fi
+ cp "${extra_cover_file}" "${cover_file}"
+
+ # We now set a variable, ${extra_crop_cover}, which contains an additional
+ # ffmpeg flag. It crops the cover so the width and the height is divisible by two.
+ # Since the standard (in the aax file) image resolution is 512, we set the flag
+ # only if we use a custom cover art.
+ extra_crop_cover='-vf crop=trunc(iw/2)*2:trunc(ih/2)*2'
+ else
+ # Audible-cli not used, extract the cover from the aax file
+ if [ "$((${loglevel} > 1))" == "1" ]; then
+ log "Extracting cover into ${cover_file}..."
+ fi
+ </dev/null ffmpeg -loglevel error -activation_bytes "${auth_code}" -i "${aax_file}" -an -codec:v copy "${cover_file}"
+ fi
+ fi
+
+ # -----
+ # If mode=chaptered, split the big converted file by chapter and remove it afterwards.
+ # Not all audio encodings make sense with multiple chapter outputs (see options section)
+ if [ "${mode}" == "chaptered" ]; then
+ # Playlist m3u support
+ playlist_file="${output_directory}/${currentFileNameScheme}.m3u"
+ if [ "${continue}" == "0" ]; then
+ if [ "$((${loglevel} > 0))" == "1" ]; then
+ log "Creating PlayList ${currentFileNameScheme}.m3u"
+ fi
+ echo '#EXTM3U' > "${playlist_file}"
+ fi
+
+ # Determine the number of chapters.
+ chaptercount=$($GREP -Pc "Chapter.*start.*end" $metadata_file)
+ if [ "$((${loglevel} > 0))" == "1" ]; then
+ log "Extracting ${chaptercount} chapter files from ${output_file}..."
+ if [ "${continue}" == "1" ]; then
+ log "Continuing at chapter ${continueAt}:"
+ fi
+ fi
+ chapternum=1
+ #start progressbar for loglevel 0 and 1
+ if [ "$((${loglevel} < 2))" == "1" ]; then
+ progressbar 0 ${chaptercount}
+ fi
+ # We pipe the metadata_file in read.
+ # Example of the section that we are interested in:
+ #
+ # Chapter #0:0: start 0.000000, end 1928.231474
+ # Metadata:
+ # title : Chapter 1
+ #
+ # Then read the line in these variables:
+ # first Chapter
+ # _ #0:0:
+ # _ start
+ # chapter_start 0.000000,
+ # _ end
+ # chapter_end 1928.231474
+ while read -r -u9 first _ _ chapter_start _ chapter_end
+ do
+ # Do things only if the line starts with 'Chapter'
+ if [[ "${first}" = "Chapter" ]]; then
+ # The next line (Metadata:...) gets discarded
+ read -r -u9 _
+ # From the line 'title : Chapter 1' we save the third field and those after in chapter
+ read -r -u9 _ _ chapter
+
+ # The formatting of the chapters names and the file names.
+ # Chapter names are used in a few place.
+ # Define the chapter_file
+ if [ "${customCNS}" == "1" ]; then
+ chapter_title="$(eval echo "${chapterNameScheme}")"
+ else
+ # The Default
+ chapter_title="${title}-$(printf %0${#chaptercount}d $chapternum) ${chapter}"
+ fi
+ chapter_file="${output_directory}/${chapter_title}.${extension}"
+
+ # Since the .aax file allready got converted we can use
+ # -acodec copy, which is much faster than a reencodation.
+ # Since there is an issue when using copy on flac, where
+ # the duration of the chapters gets shown as if they where
+ # as long as the whole audiobook.
+ chapter_codec=""
+ if test "${extension}" = "flac"; then
+ chapter_codec="flac "${compression_level_param}""
+ else
+ chapter_codec="copy"
+ fi
+
+ #Since there seems to be a bug in some older versions of ffmpeg, which makes, that -ss and -to
+ #have to be apllied to the output file, this makes, that -ss and -to get applied on the input for
+ #ffmpeg version 4+ and on the output for all older versions.
+ split_input=""
+ split_output=""
+ if [ "$(($(ffmpeg -version | $SED -E 's/[^0-9]*([0-9]).*/\1/g;1q') > 3))" = "1" ]; then
+ split_input="-ss ${chapter_start%?} -to ${chapter_end}"
+ else
+ split_output="-ss ${chapter_start%?} -to ${chapter_end}"
+ fi
+
+ # Big Long chapter debug
+ debug_vars "Chapter Variables:" cover_file chapter_start chapter_end chapternum chapter chapterNameScheme chapter_title chapter_file
+ if [ "$((${continueAt} > ${chapternum}))" = "0" ]; then
+ # Extract chapter by time stamps start and finish of chapter.
+ # This extracts based on time stamps start and end.
+ if [ "$((${loglevel} > 1))" == "1" ]; then
+ log "Splitting chapter ${chapternum}/${chaptercount} start:${chapter_start%?}(s) end:${chapter_end}(s)"
+ fi
+ </dev/null ffmpeg -loglevel quiet \
+ -nostats \
+ ${split_input} \
+ -i "${output_file}" \
+ -i "${cover_file}" \
+ ${extra_crop_cover} \
+ ${split_output} \
+ -map 0:0 \
+ -map 1:0 \
+ -acodec ${chapter_codec} \
+ -metadata:s:v title="Album cover" \
+ -metadata:s:v comment="Cover (Front)" \
+ -metadata track="${chapternum}" \
+ -metadata title="${chapter}" \
+ -metadata:s:a title="${chapter}" \
+ -metadata:s:a track="${chapternum}" \
+ -map_chapters -1 \
+ -f ${container} \
+ "${chapter_file}"
+ # -----
+ if [ "$((${loglevel} < 2))" == "1" ]; then
+ progressbar ${chapternum} ${chaptercount}
+ fi
+ # OK lets get what need for the next chapter in the Playlist m3u file.
+ # Playlist creation.
+ duration=$(echo "${chapter_end} - ${chapter_start%?}" | bc)
+ echo "#EXTINF:${duration%.*},${title} - ${chapter}" >> "${playlist_file}"
+ echo "${chapter_title}.${extension}" >> "${playlist_file}"
+ fi
+ chapternum=$((chapternum + 1 ))
+ fi
+ done 9< "$metadata_file"
+
+ # Clean up of working directory stuff.
+ rm "${output_file}"
+ if [ "$((${loglevel} > 1))" == "1" ]; then
+ log "Done creating chapters for ${output_directory}."
+ else
+ #ending progress bar
+ echo ""
+ fi
+ else
+ # Perform file tasks on output file.
+ # ----
+ # ffmpeg seems to copy only chapter position, not chapter names.
+ if [[ ${container} == "mp4" && $(type -P mp4chaps) ]]; then
+ ffprobe -i "${aax_file}" -print_format csv -show_chapters 2>/dev/null | awk -F "," '{printf "CHAPTER%02d=%02d:%02d:%02.3f\nCHAPTER%02dNAME=%s\n", NR, $5/60/60, $5/60%60, $5%60, NR, $8}' > "${output_directory}/${currentFileNameScheme}.chapters.txt"
+ mp4chaps -i "${output_file}"
+ fi
+ fi
+
+ # -----
+ # Announce that we have completed the transcode
+ if [ "$((${loglevel} > 0))" == "1" ]; then
+ log "Complete ${title}"
+ fi
+ # Lastly get rid of any extra stuff.
+ rm "${metadata_file}"
+
+ # Move the aax file if the decode is completed and the --complete_dir is set to a valid location.
+ # Check the target dir for if set if it is writable
+ if [[ "x${completedir}" != "x" ]]; then
+ if [ "$((${loglevel} > 0))" == "1" ]; then
+ log "Moving Transcoded ${aax_file} to ${completedir}"
+ fi
+ mv "${aax_file}" "${completedir}"
+ fi
+
+done
diff --git a/dotfiles/common/.local/bin/ai-assistants b/dotfiles/common/.local/bin/ai-assistants
new file mode 100755
index 0000000..51028fe
--- /dev/null
+++ b/dotfiles/common/.local/bin/ai-assistants
@@ -0,0 +1,45 @@
+#!/bin/bash
+# Launch tmux session with Claude AI assistants for multiple projects
+
+SESSION="ai-assistants"
+
+# If session exists, attach to it
+if tmux has-session -t "$SESSION" 2>/dev/null; then
+ tmux attach-session -t "$SESSION"
+ exit 0
+fi
+
+# Define projects: name and directory
+projects=(
+ "health:~/projects/health"
+ "finances:~/projects/finances"
+ "danneel:~/projects/danneel"
+ "jr-estate:~/projects/jr-estate"
+ "kit:~/projects/kit"
+ "homelab:~/projects/homelab"
+ "career:~/projects/career"
+)
+
+# Claude command to run in each window
+CLAUDE_CMD='claude "Read docs/protocols.org and docs/NOTES.org, follow instructions exactly, then begin the session-start workflow"'
+
+# Create session with first project
+first="${projects[0]}"
+name="${first%%:*}"
+dir="${first#*:}"
+dir="${dir/#\~/$HOME}"
+tmux new-session -d -s "$SESSION" -n "$name" -c "$dir"
+tmux send-keys -t "$SESSION:$name" "$CLAUDE_CMD" Enter
+
+# Create remaining windows
+for project in "${projects[@]:1}"; do
+ name="${project%%:*}"
+ dir="${project#*:}"
+ dir="${dir/#\~/$HOME}"
+ tmux new-window -t "$SESSION" -n "$name" -c "$dir"
+ tmux send-keys -t "$SESSION:$name" "$CLAUDE_CMD" Enter
+done
+
+# Select first window and attach
+tmux select-window -t "$SESSION:health"
+tmux attach-session -t "$SESSION"
diff --git a/dotfiles/common/.local/bin/any2flac b/dotfiles/common/.local/bin/any2flac
new file mode 100755
index 0000000..c2cc0a7
--- /dev/null
+++ b/dotfiles/common/.local/bin/any2flac
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# Craig Jennings <c@cjennings.net>
+
+
+function print_help {
+ echo "Converts an audio or video to flac audio and removes all metadata tags."
+ echo "This script requires ffmpeg and metaflac."; echo ""
+ echo "Usage: any2flac [filename]"
+ echo "Parameter: filename - name of the file to convert."
+}
+
+# Check for help argument
+if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
+ print_help
+ exit 0
+fi
+
+# Check if filename is passed
+if [ $# -eq 0 ]; then
+ echo "Must name an audio file to convert. Exiting."
+ exit 1
+fi
+
+# Check for the existence of ffmpeg
+if ! command -v ffmpeg &> /dev/null; then
+ echo "ffmpeg could not be found. Please install it first."
+ exit 1
+fi
+
+# Check for the existence of metaflac
+if ! command -v metaflac &> /dev/null; then
+ echo "metaflac could not be found. Please install it first."
+ exit 1
+fi
+
+echo "Converting to flac format. This may take a while..."
+# convert to flac
+ffmpeg -i "$1" -vn -c:a flac "${1%.*}.flac"
+
+echo "Removing all tags."
+# remove all tags
+metaflac --remove-all-tags "${1%.*}.flac"
+
+echo ""; echo "Done."
diff --git a/dotfiles/common/.local/bin/any2opus b/dotfiles/common/.local/bin/any2opus
new file mode 100755
index 0000000..c5a7dbd
--- /dev/null
+++ b/dotfiles/common/.local/bin/any2opus
@@ -0,0 +1,102 @@
+#!/bin/env bash
+# Craig Jennings <c@cjennings.net>
+# convenience utility to convert all media files not in opus format in current directory to opus.
+# flags for recursive or specific filename supported.
+
+# Default values of arguments
+delete_after_conversion=false
+filename=""
+recursive=false
+
+# Usage info
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [-h] [-d] [-r] [-f FILE]
+Convert audio/video files to the Opus format.
+
+ -h display this help and exit
+ -d delete source files after conversion
+ -r convert files recursively in current and subdirectories
+ -f FILE convert a specific file.
+
+Note: The '-f' and '-r' options are mutually exclusive.
+ If no file is given, works on all non-opus media files in the current directory.
+ Requires the ffmpeg installation.
+EOF
+}
+
+# Check for the existence of ffmpeg
+if ! command -v ffmpeg &> /dev/null; then
+ echo "Cannot proceed: ffmpeg not found on path."
+ exit 1
+fi
+
+# Identify arguments; quit if invalid argument
+while getopts ":df:rh" opt; do
+ case ${opt} in
+ d)
+ delete_after_conversion=true
+ ;;
+ f)
+ filename=$OPTARG
+ ;;
+ r)
+ recursive=true
+ ;;
+ h)
+ show_help
+ exit 0
+ ;;
+ \?)
+ echo "Invalid option: -$OPTARG" >&2
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+# Display error and quit if incompatible arguments
+if [ "$recursive" = true ] && [ -n "$filename" ]
+then
+ echo "The -f and -r options cannot be used together."
+ show_help
+ exit 1
+fi
+
+# Convert function checks mime-type and converts/deletes a single file
+convert() {
+ f=$1
+ # Check the mime-type of the file
+ mime_type=$(file -b --mime-type "$f")
+ # If the file is an audio or video file and not already in opus format then convert it to opus
+ if [[ $mime_type == video/* || $mime_type == audio/* ]] && [[ $f != *.opus ]]; then
+ ffmpeg -i "$f" "${f%.*}.opus"
+ # If the '-d' option was specified, delete the original file after conversion
+ if $delete_after_conversion; then
+ rm "$f"
+ fi
+ fi
+}
+
+# Use above convert function based on user's intended set of files
+if [ "$recursive" = true ]
+then
+ # convert all media files in current and child directories
+ find . -type f -exec bash -c 'convert "$0"' {} \;
+elif [[ -n $filename ]]
+then
+ # if filename is not empty, convert only this file
+ convert $filename
+else
+ # Convert all
+ echo "All non-opus media files in the current directory will be converted. Originals are kept."
+ read -p "Proceed? (Y/n) " -n 1 -r
+ echo
+ if [[ $REPLY =~ ^[Yy]$ ]]
+ then
+ # Iterate over each file in the directory and convert
+ for f in *; do
+ convert $f
+ done
+ fi
+fi
diff --git a/dotfiles/common/.local/bin/build-emacs.sh b/dotfiles/common/.local/bin/build-emacs.sh
new file mode 100755
index 0000000..4e47ff9
--- /dev/null
+++ b/dotfiles/common/.local/bin/build-emacs.sh
@@ -0,0 +1,213 @@
+#!/usr/bin/env bash
+# Craig Jennings <c@cjennings.net>
+# Build & install Emacs from source
+# safe, user-local, fast, versioned.
+
+set -Eeuo pipefail
+IFS=$'\n\t'
+
+# ------------------------ Config (overwrite via ENV) -----------------------
+
+SRC_DIR="${SRC_DIR:-$HOME/code/emacs-src}"
+EMACS_REPO="${EMACS_REPO:-https://git.savannah.gnu.org/git/emacs.git}"
+CHECKOUT_REF="${CHECKOUT_REF:-emacs-30.2}"
+# CHECKOUT_REF="${CHECKOUT_REF:-636f166cfc8}"
+PREFIX_BASE="${PREFIX_BASE:-$HOME/.local/src/emacs}"
+PREFIX="${PREFIX:-$PREFIX_BASE/${CHECKOUT_REF}}"
+# LOG_DIR="${LOG_DIR:-$HOME/.cache/emacs-build-logs}"
+LOG_DIR="${LOG_DIR:-$HOME/}"
+ENABLE_NATIVE="${ENABLE_NATIVE:-1}"
+# WITH_PGTK="${WITH_PGTK:-auto}"
+WITH_PGTK="${WITH_PGTK:-0}"
+JOBS="${JOBS:-auto}"
+EXTRA_CONFIG="${EXTRA_CONFIG:-}"
+
+# --------------------------------- Preflight ---------------------------------
+
+umask 022
+mkdir -p "$LOG_DIR" "$PREFIX_BASE" "$HOME/.local/bin"
+: "${LOGFILE:=$LOG_DIR/emacs-build-$(date +%Y%m%d-%H%M%S)-${CHECKOUT_REF}.log}"
+
+say() { printf '>>> %s\n' "$*" | tee -a "$LOGFILE" ; }
+run() {
+ say "+ $*"
+ if ! eval "$@" >>"$LOGFILE" 2>&1; then
+ echo "ERROR: Command failed: $*" >&2
+ echo "Last 20 lines of output:" >&2
+ tail -n 20 "$LOGFILE" >&2
+ return 1
+ fi
+}
+
+on_err(){
+ ec=$?
+ echo "ERROR [$ec] - Full log at: $LOGFILE" >&2
+ echo "Last 100 lines of log:" >&2
+ tail -n 100 "$LOGFILE" >&2 || true
+ exit "$ec"
+}
+
+trap on_err ERR
+
+# -------------------------- Arch Linux Dependencies --------------------------
+
+if [[ -r /etc/os-release ]]; then
+ . /etc/os-release
+fi
+
+if [[ "${ID:-}" == "arch" || "${ID_LIKE:-}" == *arch* ]]; then
+ say "Arch Linux detected; checking required build/runtime packages"
+
+ # Base packages needed for the requested configure flags
+ pkgs=(jansson tree-sitter imagemagick mailutils harfbuzz cairo gnutls libxml2 texinfo gtk3)
+
+ # Only if native-compilation is enabled
+ if [[ "${ENABLE_NATIVE:-1}" == "1" ]]; then
+ pkgs+=(libgccjit)
+ fi
+
+ # List packages that are not installed
+ missing="$(pacman -T "${pkgs[@]}" || true)"
+
+ if [[ -z "$missing" ]]; then
+ say "All required packages are already installed."
+ else
+ say "Missing packages: $missing"
+ if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
+ run "sudo -n pacman -Sy --needed --noconfirm $missing"
+ else
+ say "sudo (passwordless) not available; please install missing packages manually:"
+ echo " sudo pacman -Sy --needed $missing"
+ exit 70
+ fi
+ fi
+else
+ say "Non-Arch system detected; skipping Arch-specific dependency check."
+fi
+
+# ----------------------------- Clone And Update ----------------------------
+
+if [[ -d "$SRC_DIR/.git" ]]; then
+ run "git -C '$SRC_DIR' fetch --tags --prune"
+else
+ mkdir -p "$(dirname "$SRC_DIR")"
+ run "git clone --origin origin '$EMACS_REPO' '$SRC_DIR'"
+fi
+
+run "git -C '$SRC_DIR' reset --hard HEAD"
+run "git -C '$SRC_DIR' clean -fdx"
+run "git -C '$SRC_DIR' checkout -f '$CHECKOUT_REF'"
+run "git -C '$SRC_DIR' submodule update --init --recursive || true"
+
+# ---------------------------------- Autogen ----------------------------------
+
+if [[ -x "$SRC_DIR/autogen.sh" ]]; then
+ run "cd '$SRC_DIR' && ./autogen.sh"
+fi
+
+# ------------------------------ Configure Flags ------------------------------
+
+conf_flags=(
+ "--prefix=${PREFIX}"
+ "--with-json"
+ "--with-modules"
+)
+
+# Wayland/X choice
+if [[ "$WITH_PGTK" == "auto" ]]; then
+ if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then WITH_PGTK="yes"; else WITH_PGTK="no"; fi
+fi
+if [[ "$WITH_PGTK" == "yes" ]]; then
+ conf_flags+=("--with-pgtk")
+else
+ conf_flags+=("--with-x-toolkit=gtk3")
+fi
+
+# Native-compilation
+if [[ "$ENABLE_NATIVE" == "1" ]]; then
+ conf_flags+=("--with-native-compilation=yes")
+else
+ conf_flags+=("--with-native-compilation=no")
+fi
+
+# Useful extras (disable later via EXTRA_CONFIG if missing deps)
+conf_flags+=(
+ "--with-cairo"
+ "--with-harfbuzz"
+ "--with-tree-sitter"
+ "--with-imagemagick"
+ "--with-mailutils"
+)
+
+# Optional extra flags from env
+if [[ -n "$EXTRA_CONFIG" ]]; then
+ # shellcheck disable=SC2206
+ conf_flags+=($EXTRA_CONFIG)
+fi
+
+# ----------------------------- Build And Install -----------------------------
+
+mkdir -p "$PREFIX"
+
+# Temporarily change IFS to space for configure argument expansion
+old_ifs="$IFS"
+IFS=' '
+run "cd '$SRC_DIR' && ./configure ${conf_flags[*]}"
+IFS="$old_ifs"
+
+if [[ "$JOBS" == "auto" ]]; then
+ if command -v nproc >/dev/null 2>&1; then JOBS=$(nproc); else JOBS=4; fi
+fi
+run "cd '$SRC_DIR' && make -j$JOBS"
+
+# Build documentation (info files)
+say "...building info files"
+(
+ cd "$SRC_DIR"
+ run "make info"
+)
+
+run "cd '$SRC_DIR' && make install"
+run "cd '$SRC_DIR' && make install-info"
+
+# --------------------------------- Symlinks --------------------------------
+
+run "ln -sfn '$PREFIX' '$PREFIX_BASE/emacs-current'"
+
+# Remove old symlinks first, then create new ones
+for exe in emacs emacsclient; do
+ target="$PREFIX/bin/$exe"
+ link="$HOME/.local/bin/$exe"
+ if [[ -x "$target" ]]; then
+ run "rm -f '$link'"
+ run "ln -s '$target' '$link'"
+ fi
+done
+
+# ---------------------------------- Wrap Up ----------------------------------
+
+command -v emacs >/dev/null && emacs --version | head -n1 || true
+command -v emacsclient >/dev/null && emacsclient --version | head -n1 || true
+
+# ----------------------------- Show Build Features ----------------------------
+
+say "Launching Emacs to display version and build features..."
+"$HOME/.local/bin/emacs" -Q --batch --eval '
+(progn
+ (princ (format "Emacs version: %s\n" emacs-version))
+ (princ (format "Build configuration:\n"))
+ (princ (format " Native compilation: %s\n"
+ (if (and (fboundp (quote native-comp-available-p))
+ (native-comp-available-p))
+ "yes" "no")))
+ (princ (format " PGTK: %s\n" (if (featurep (quote pgtk)) "yes" "no")))
+ (princ (format " Tree-sitter: %s\n"
+ (if (fboundp (quote treesit-available-p)) "yes" "no")))
+ (princ (format " JSON: %s\n" (if (fboundp (quote json-parse-string)) "yes" "no")))
+ (princ (format " ImageMagick: %s\n" (if (image-type-available-p (quote imagemagick)) "yes" "no")))
+ (princ (format " Cairo: %s\n" (if (featurep (quote cairo)) "yes" "no")))
+ (princ (format "\nFull system configuration:\n%s\n" system-configuration))
+ (princ (format "\nConfigured features:\n%s\n" system-configuration-features)))
+' 2>&1 | tee -a "$LOGFILE"
+
+echo "Done. See $LOGFILE"
diff --git a/dotfiles/common/.local/bin/clobberall b/dotfiles/common/.local/bin/clobberall
new file mode 100755
index 0000000..a570d4e
--- /dev/null
+++ b/dotfiles/common/.local/bin/clobberall
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# clobberall
+# Repeatedly kills all processes with the given name until none remain
+# Usage: clobberall <process_name>
+#
+# Craig Jennings <c@cjennings.net>
+
+if [ $# -eq 0 ]; then
+ echo "Usage: clobberall <process_name>"
+ echo "Example: clobberall emacs"
+ exit 1
+fi
+
+process_name="$1"
+
+while sudo killall "$process_name" 2>/dev/null; do
+ sleep 0.1
+done
+
+echo "All '$process_name' processes terminated."
diff --git a/dotfiles/common/.local/bin/cron/README.md b/dotfiles/common/.local/bin/cron/README.md
new file mode 100644
index 0000000..fa0c354
--- /dev/null
+++ b/dotfiles/common/.local/bin/cron/README.md
@@ -0,0 +1,11 @@
+# Important Note
+
+These cronjobs have components that require information about your current display to display notifications correctly.
+
+When you add them as cronjobs, I recommend you precede the command with commands as those below:
+
+```
+export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u $USER)/bus; export DISPLAY=:0; . $HOME/.zprofile; then_command_goes_here
+```
+
+This ensures that notifications will display, xdotool commands will function and environmental variables will work as well.
diff --git a/dotfiles/common/.local/bin/cron/checkup b/dotfiles/common/.local/bin/cron/checkup
new file mode 100755
index 0000000..bd3c634
--- /dev/null
+++ b/dotfiles/common/.local/bin/cron/checkup
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Syncs repositories and downloads updates, meant to be run as a cronjob.
+
+notify-send "πŸ“¦ Repository Sync" "Checking for package updates..."
+
+sudo pacman -Syyuw --noconfirm || notify-send "Error downloading updates.
+
+Check your internet connection, if pacman is already running, or run update manually to see errors."
+pkill -RTMIN+8 "${STATUSBAR:-dwmblocks}"
+
+if pacman -Qu | grep -v "\[ignored\]"
+then
+ notify-send "🎁 Repository Sync" "Updates available. Click statusbar icon (πŸ“¦) for update."
+else
+ notify-send "πŸ“¦ Repository Sync" "Sync complete. No new packages for update."
+fi
diff --git a/dotfiles/common/.local/bin/cron/crontog b/dotfiles/common/.local/bin/cron/crontog
new file mode 100755
index 0000000..5aba5e6
--- /dev/null
+++ b/dotfiles/common/.local/bin/cron/crontog
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+# Toggles all cronjobs off/on.
+# Stores disabled crontabs in ~/.consaved until restored.
+
+([ -f "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved ] && crontab - < "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && rm "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && notify-send "πŸ•“ Cronjobs re-enabled.") || ( crontab -l > "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && crontab -r && notify-send "πŸ•“ Cronjobs saved and disabled.")
diff --git a/dotfiles/common/.local/bin/dab b/dotfiles/common/.local/bin/dab
new file mode 100755
index 0000000..e7d0fae
--- /dev/null
+++ b/dotfiles/common/.local/bin/dab
@@ -0,0 +1,72 @@
+#!/bin/env bash
+# dab - delete all but
+# Craig Jennings <c@cjennings.net>
+# deletes all files in current directory that are NOT of a specific extension
+
+# default switch values
+recurse=""
+del_dirs=""
+
+# help function
+show_help() {
+ echo "Usage: $0 [extension] [OPTION]"
+ echo "Deletes all files that do not match the given extension from the current directory."
+ echo ""
+ echo "Options:"
+ echo "-r Delete files in directories recursively."
+ echo "-d Delete directories if they become empty after deletions."
+ echo "-h Display this help message and exit."
+ echo ""
+ echo "Example: $0 txt -r -d"
+ echo "This would delete all non-text files in the current directory and subdirectories. Any directories emptied by this process will also be deleted."
+ echo ""
+ echo "Note: Directories that were previously empty will remain untouched."
+}
+
+# if help was requested, show it and exit
+if [[ $1 = "-h" ]] || [[ $2 = "-h" ]] || [[ $3 = "-h" ]]; then
+ show_help
+ exit
+fi
+
+# error if nothing or * was passed
+if [[ -z $1 ]] || [[ $1 = "*" ]]; then
+ echo "ERROR: No file extension was provided or invalid extension ('*')."
+ show_help
+ exit
+fi
+
+# identify rest of arguments
+ext=$1
+if [[ $2 == *"-r"* ]] || [[ $3 == *"-r"* ]]; then
+ recurse="-R"
+fi
+
+if [[ $2 == *"-d"* ]] || [[ $3 == *"-d"* ]]; then
+ del_dirs="true"
+fi
+
+# Save empty directories into an array before deleting the files
+empty_dirs=()
+while IFS= read -r -d $'\0'; do
+ empty_dirs+=("$REPLY")
+done < <(find . -type d -empty -print0)
+
+shopt -s globstar nullglob extglob
+
+# remove non-matching files
+for file in **; do
+ if [[ -f "$file" ]] && [[ "$file" != *".$ext" ]]; then
+ rm "$file"
+ fi
+done
+
+# remove directories emptied by this operation
+if [[ $del_dirs == "true" ]]; then
+ for dir in **/; do
+ if [[ ! -n "$(ls -A $dir)" ]] && [[ ! "${empty_dirs[*]}" =~ $dir ]]; then
+ rmdir "$dir"
+ fi
+ done
+fi
+
diff --git a/dotfiles/common/.local/bin/ec b/dotfiles/common/.local/bin/ec
new file mode 100755
index 0000000..b409195
--- /dev/null
+++ b/dotfiles/common/.local/bin/ec
@@ -0,0 +1,2 @@
+#!/bin/sh
+emacsclient -c -a "" $1 $2 $3 $4 &
diff --git a/dotfiles/common/.local/bin/em b/dotfiles/common/.local/bin/em
new file mode 100755
index 0000000..b409195
--- /dev/null
+++ b/dotfiles/common/.local/bin/em
@@ -0,0 +1,2 @@
+#!/bin/sh
+emacsclient -c -a "" $1 $2 $3 $4 &
diff --git a/dotfiles/common/.local/bin/et b/dotfiles/common/.local/bin/et
new file mode 100755
index 0000000..1c3c4a0
--- /dev/null
+++ b/dotfiles/common/.local/bin/et
@@ -0,0 +1,2 @@
+#!/bin/sh
+emacsclient -c -nw --alternate-editor="" $1 $2 $3 $4 &
diff --git a/dotfiles/common/.local/bin/extractaudio b/dotfiles/common/.local/bin/extractaudio
new file mode 100755
index 0000000..a665451
--- /dev/null
+++ b/dotfiles/common/.local/bin/extractaudio
@@ -0,0 +1,2 @@
+#!/bin/sh
+ffmpeg -i $1 -q:a 0 -map a $1.mp3
diff --git a/dotfiles/common/.local/bin/get-arch-iso.sh b/dotfiles/common/.local/bin/get-arch-iso.sh
new file mode 100755
index 0000000..635034a
--- /dev/null
+++ b/dotfiles/common/.local/bin/get-arch-iso.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+# fetch-arch-iso.sh
+# Downloads the latest Arch ISO + signature, checks GPG key, verifies the download.
+
+set -u
+set -o pipefail
+
+# CONFIGURATION
+BASE_DIR="${HOME}/downloads/isos"
+ISO_NAME="archlinux-x86_64.iso"
+SIG_NAME="${ISO_NAME}.sig"
+ISO_URL="https://geo.mirror.pkgbuild.com/iso/latest/${ISO_NAME}"
+SIG_URL="https://geo.mirror.pkgbuild.com/iso/latest/${SIG_NAME}"
+# The β€œArch Linux Master Key” is what signs the ISO. We look for its name in your keyring.
+ARCH_KEY_SEARCH="Arch Linux Master Key"
+
+# 1) Build target directory, e.g. ~/downloads/isos/archlinux.2025.08.22
+today=$(date +%Y.%m.%d)
+TARGET_DIR="${BASE_DIR}/archlinux.${today}"
+
+mkdir -p "${TARGET_DIR}" || {
+ echo "Error: could not create ${TARGET_DIR}" >&2
+ exit 1
+}
+
+# 2) A small helper to download with one retry
+download_with_retry() {
+ local url=$1 out=$2
+ echo " -> Downloading ${url} to ${out}"
+ if ! wget -q --show-progress -O "${out}" "${url}"; then
+ echo " First attempt failed; retrying once..."
+ if ! wget -q --show-progress -O "${out}" "${url}"; then
+ echo "Error: failed to download ${url} after 2 tries."
+ echo " Please check your network connectivity."
+ exit 1
+ fi
+ fi
+}
+
+# 3) Make sure GPG is installed (we assume gpg binary exists)
+if ! command -v gpg >/dev/null; then
+ echo "Error: gpg is not installed. Please install it and re-run."
+ exit 1
+fi
+
+# 4) Check for the Arch Linux signing key
+if ! gpg --list-keys "${ARCH_KEY_SEARCH}" >/dev/null 2>&1; then
+ echo "Warning: Arch Linux signing key not found in your keyring."
+ read -p "Install archlinux-keyring package now? [y/N] " ans
+ ans=${ans,,} # tolower
+ if [[ "${ans}" == "y" || "${ans}" == "yes" ]]; then
+ sudo pacman -Sy --needed archlinux-keyring || {
+ echo "Error: could not install archlinux-keyring." >&2
+ exit 1
+ }
+ else
+ echo "Cannot verify ISO without the Arch key. Aborting."
+ exit 1
+ fi
+fi
+
+# 5) Download the ISO and its .sig
+download_with_retry "${ISO_URL}" "${TARGET_DIR}/${ISO_NAME}"
+download_with_retry "${SIG_URL}" "${TARGET_DIR}/${SIG_NAME}"
+
+# 6) Verify the ISO against the signature
+echo " -> Verifying the ISO with GPG..."
+if gpg --verify "${TARGET_DIR}/${SIG_NAME}" "${TARGET_DIR}/${ISO_NAME}"; then
+ echo
+ echo "SUCCESS: The ISO signature is valid."
+ echo "You can now burn or mount ${TARGET_DIR}/${ISO_NAME} with confidence."
+ exit 0
+else
+ echo
+ echo "ERROR: GPG signature verification failed!"
+ echo " The downloaded ISO may be corrupted or tampered with."
+ exit 1
+fi
diff --git a/dotfiles/common/.local/bin/gitconfig_defaults b/dotfiles/common/.local/bin/gitconfig_defaults
new file mode 100755
index 0000000..c2f18ae
--- /dev/null
+++ b/dotfiles/common/.local/bin/gitconfig_defaults
@@ -0,0 +1,5 @@
+git config --global user.email "craigmartinjennings@gmail.com"
+git config --global user.name "Craig Jennings""
+git config --global merge.tool meld
+git config --global core.editor "emacsclient -c -a''"
+git config --global fetch.prune true
diff --git a/dotfiles/common/.local/bin/ifinstalled b/dotfiles/common/.local/bin/ifinstalled
new file mode 100755
index 0000000..c192eba
--- /dev/null
+++ b/dotfiles/common/.local/bin/ifinstalled
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+# Some optional functions in LARBS require programs not installed by default. I
+# use this little script to check to see if a command exists and if it doesn't
+# it informs the user that they need that command to continue. This is used in
+# various other scripts for clarity's sake.
+
+for x in "$@"; do
+ if ! which "$x" >/dev/null 2>&1 && ! pacman -Qq "$x" >/dev/null 2>&1; then
+ notify-send "πŸ“¦ $x" "must be installed for this function." && exit 1 ;
+ fi
+done
diff --git a/dotfiles/common/.local/bin/linkhandler b/dotfiles/common/.local/bin/linkhandler
new file mode 100755
index 0000000..cc971fc
--- /dev/null
+++ b/dotfiles/common/.local/bin/linkhandler
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+# Feed script a url or file location.
+# If an image, it will view in sxiv,
+# if a video or gif, it will view in mpv
+# if a music file or pdf, it will download,
+# otherwise it opens link in browser.
+
+if [ -z "$1" ]; then
+ url="$(xclip -o)"
+else
+ url="$1"
+fi
+
+case "$url" in
+ *mkv|*webm|*mp4|*youtube.com/watch*|*youtube.com/playlist*|*youtu.be*|*hooktube.com*|*bitchute.com*|*videos.lukesmith.xyz*|*odysee.com*)
+ setsid -f mpv -quiet "$url" >/dev/null 2>&1 ;;
+ *png|*jpg|*jpe|*jpeg|*gif)
+ curl -sL "$url" > "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" && sxiv -a "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" >/dev/null 2>&1 & ;;
+ *pdf|*cbz|*cbr)
+ curl -sL "$url" > "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" && zathura "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" >/dev/null 2>&1 & ;;
+ *mp3|*flac|*opus|*mp3?source*)
+ qndl "$url" 'curl -LO' >/dev/null 2>&1 ;;
+ *)
+ [ -f "$url" ] && setsid -f "$TERMINAL" -e "$EDITOR" "$url" >/dev/null 2>&1 || setsid -f "$BROWSER" "$url" >/dev/null 2>&1
+esac
diff --git a/dotfiles/common/.local/bin/mkplaylist b/dotfiles/common/.local/bin/mkplaylist
new file mode 100755
index 0000000..66b6e9c
--- /dev/null
+++ b/dotfiles/common/.local/bin/mkplaylist
@@ -0,0 +1,173 @@
+#!/usr/bin/env bash
+# Craig Jennings <c@cjennings.net>
+# Basically just a bash wrapper around a find/grep/awk command pipe
+# to generate m3u playlists from video or audio files in a directory.
+
+# One m3u playlist will be placed in the MUSIC_DIR, and another
+# will be placed inside each playlist directory.
+# It also converts .opus and .ogg files to .m4a for Android playback.
+
+# Note:
+# This script requires the following utilities to be on the path:
+# mid3v2 (aur package: python-mutagen)
+# tageditor (aur package: tageditor)
+# metaflac (aur package: flac)
+
+set -e
+
+MUSIC_DIR="$HOME/music"
+# REQUIRED_TOOLS=("mid3v2" "tageditor")
+REQUIRED_TOOLS=("mid3v2" "metaflac" "tageditor")
+
+# ---------------------------- Functions ----------------------------
+
+usage () {
+ printf "\nUsage: mkplaylist <playlist_name>\n\n"
+ printf "mkplaylist - creates an m3u playlist in the $MUSIC_DIR directory\n"
+ printf "based the music and video files in directory which m3uplaylist is called.\n\n"
+ printf " - this script should be run in the directory containing the music or video files\n"
+ printf " - <playlist_name> is mandatory and shouldn't end with '.m3u' extension\n"
+ printf " - change the destination ($MUSIC_DIR) by editing this script\n\n"
+}
+
+tag_music_file() {
+ while IFS= read -r file; do
+ filename=$(basename "$file")
+ extension="${filename##*.}"
+ artist=$(basename "$file" | cut -d '-' -f 1)
+ title=$(basename "$file" | cut -d '-' -f 2- | cut -d '.' -f 1)
+ outputfile="$(dirname "$file")/$title.flac"
+
+ # If file is not already flac, convert it
+ if [ "$extension" != "flac" ]; then
+
+ # Delete all tags using mid3v2
+ mid3v2 --delete-all "$file"
+ ffmpeg -i "$file" -vn -c:a flac "$outputfile"
+ file="$outputfile" # Now we're working with the new FLAC file
+
+ fi
+
+ # Set artist and song title tags using metaflac
+ metaflac --set-tag="ARTIST=$artist" --set-tag="TITLE=$title" "$file"
+
+ done
+}
+
+# tag_music_file() {
+# while IFS= read -r file; do
+# filename=$(basename "$file")
+# extension="${filename##*.}"
+# artist=$(basename "$file" | cut -d '-' -f 1)
+# title=$(basename "$file" | cut -d '-' -f 2- | cut -d '.' -f 1)
+# outputfile="$(dirname "$file")/$title.flac"
+
+# # Delete all tags using mid3v2
+# mid3v2 --delete-all "$file"
+
+# # If file is not already flac, convert it
+# if [ "$extension" != "flac" ]; then
+# ffmpeg -i "$file" -vn -c:a flac "$outputfile"
+# file="$outputfile" # Now we're working with the new FLAC file
+# fi
+
+# # Set artist and song title tags using metaflac
+# metaflac --set-tag="ARTIST=$artist" --set-tag="TITLE=$title" "$file"
+# done
+# }
+
+# tag_music_file() {
+# while IFS= read -r file; do
+# # Extract artist and song title from filename
+# artist=$(basename "$file" | cut -d '-' -f 1)
+# title=$(basename "$file" | cut -d '-' -f 2- | cut -d '.' -f 1)
+# outputfile="$(dirname "$file")/$title.flac"
+
+# # Delete all tags using mid3v2
+# mid3v2 --delete-all "$file"
+
+# # Convert to flac and save to new file
+# ffmpeg -i "$file" -vn -c:a flac "$outputfile"
+
+# # Set artist and song title tags using metaflac
+# metaflac --set-tag="ARTIST=$artist" --set-tag="TITLE=$title" "$outputfile"
+# done
+# }
+
+# tag_music_file() {
+# while IFS= read -r file; do
+# # Extract artist and song title from filename
+# artist=$(basename "$file" | cut -d '-' -f 1)
+# title=$(basename "$file" | cut -d '-' -f 2 | cut -d '.' -f 1)
+# outputfile="$(dirname "$file")/$title.flac"
+
+# # Delete all tags using mid3v2
+# mid3v2 --delete-all "$file"
+
+# # Set artist and song title tags using mid3v2
+# mid3v2 --artist="$artist" --song="$title" "$file"
+# # Convert to flac and save to new file
+# ffmpeg -i "$file" -vn -c:a flac "$outputfile"
+# done
+# }
+
+# tag_music_file() {
+# while IFS= read -r file; do
+# # Extract artist and song title from filename
+# artist=$(basename "$file" | cut -d '-' -f 1)
+# title=$(basename "$file" | cut -d '-' -f 2 | cut -d '.' -f 1)
+
+# # Delete all tags using mid3v2
+# mid3v2 --delete-all "$file"
+
+# # Set artist and song title tags using mid3v2
+# mid3v2 --artist="$artist" --song="$title" "$file"
+# done
+# }
+
+ generate_music_m3u() {
+ printf "retagging music files....\n"
+ find "$(pwd)" -print | file -if - | grep -E '(audio)' | awk -F: '{print $1}' | tag_music_file
+
+ printf "generating playlist.'%s'...\n" "$LOCAL_PLAYLIST"
+ find "$(pwd)" -print | file -if - | grep -E '(video|audio)' |
+ awk -F: '{print $1}' | while read -r line; do basename "$line"; done > "$LOCAL_PLAYLIST"
+ printf "generating playlist '%s'....\n" "$MUSIC_PLAYLIST"
+ find "$(pwd)" -print | file -if - | grep -E '(video|audio)' | awk -F: '{print $1}' > "$MUSIC_PLAYLIST"
+ printf "Done.\n\n"
+ }
+
+ # ----------------------------- Script ----------------------------
+
+ # display usage if user specifically requests it
+ TYPE=$(tr '[a-z]' '[A-Z]' <<< "$@");
+ [ "$TYPE" = "HELP" ] && usage && exit 1
+ [ "$TYPE" = "-H" ] && usage&& exit 1
+
+ # check that all necessary tools are installed
+ for tool in ${REQUIRED_TOOLS[@]}; do
+ if ! type "$tool" >/dev/null 2>&1; then
+ printf "ERROR: The script requires '%s' but it is not installed or not in PATH.\n" "$tool"
+ exit 1
+ fi
+ done
+
+ # use directory name for playlist name when parameter doesn't exist
+ if [ $# -eq 0 ]
+ then
+ set -- "$(basename "$PWD")"
+ echo "no playlist name entered, so using directory name: '$(basename "$PWD")'"
+ fi
+
+ # ask to overwrite if the playlist already exists
+ MUSIC_PLAYLIST="$MUSIC_DIR/$@.m3u"
+ LOCAL_PLAYLIST="./$@.m3u"
+
+ if [ -f "$MUSIC_PLAYLIST" ]; then
+ read -p "$MUSIC_PLAYLIST exists. Overwrite (y/n) " yn
+ if [ "$yn" != "y" ] && [ "$yn" != "Y" ]; then
+ exit 0
+ fi
+ fi
+
+ generate_music_m3u
diff --git a/dotfiles/common/.local/bin/mpd_play_yt_stream b/dotfiles/common/.local/bin/mpd_play_yt_stream
new file mode 100755
index 0000000..b53f298
--- /dev/null
+++ b/dotfiles/common/.local/bin/mpd_play_yt_stream
@@ -0,0 +1,14 @@
+#!/bin/bash
+#
+MYHOST='127.0.0.1' # or your MPD host
+
+mpduri="$(yt-dlp -f best -g $1)#"
+# mpduri="$(yt-dlp -g $1)#"
+# TAG=$(yt-dlp -i --get-filename $1)
+# cadena="{\"title\":\"$TAG\"}"
+# echo "$cadena"
+# mpduri="$mpduri$cadena"
+# echo "$mpduri"
+mpc insert "$mpduri"
+mpc next
+mpc play
diff --git a/dotfiles/common/.local/bin/msmtp-enqueue.sh b/dotfiles/common/.local/bin/msmtp-enqueue.sh
new file mode 100755
index 0000000..c9beaca
--- /dev/null
+++ b/dotfiles/common/.local/bin/msmtp-enqueue.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env sh
+
+QUEUEDIR=$HOME/.msmtpqueue
+
+# Set secure permissions on created directories and files
+umask 077
+
+# Change to queue directory (create it if necessary)
+if [ ! -d "$QUEUEDIR" ]; then
+ mkdir -p "$QUEUEDIR" || exit 1
+fi
+cd "$QUEUEDIR" || exit 1
+
+# Create new unique filenames of the form
+# MAILFILE: ccyy-mm-dd-hh.mm.ss[-x].mail
+# MSMTPFILE: ccyy-mm-dd-hh.mm.ss[-x].msmtp
+# where x is a consecutive number only appended if you send more than one
+# mail per second.
+BASE="$(date +%Y-%m-%d-%H.%M.%S)"
+if [ -f "$BASE.mail" ] || [ -f "$BASE.msmtp" ]; then
+ TMP="$BASE"
+ i=1
+ while [ -f "$TMP-$i.mail" ] || [ -f "$TMP-$i.msmtp" ]; do
+ i=$((i + 1))
+ done
+ BASE="$BASE-$i"
+fi
+MAILFILE="$BASE.mail"
+MSMTPFILE="$BASE.msmtp"
+
+# Write command line to $MSMTPFILE
+echo "$@" > "$MSMTPFILE" || exit 1
+
+# Write the mail to $MAILFILE
+cat > "$MAILFILE" || exit 1
+
+# If we are online, run the queue immediately.
+# Replace the test with something suitable for your site.
+#ping -c 1 -w 2 SOME-IP-ADDRESS > /dev/null
+#if [ $? -eq 0 ]; then
+# msmtp-runqueue.sh > /dev/null &
+#fi
+
+exit 0
diff --git a/dotfiles/common/.local/bin/msmtp-listqueue.sh b/dotfiles/common/.local/bin/msmtp-listqueue.sh
new file mode 100755
index 0000000..cc97c58
--- /dev/null
+++ b/dotfiles/common/.local/bin/msmtp-listqueue.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+
+QUEUEDIR=$HOME/.msmtpqueue
+
+for i in $QUEUEDIR/*.mail; do
+ grep -E -s --colour -h '(^From:|^To:|^Subject:)' "$i" || echo "No mail in queue";
+ echo " "
+done
diff --git a/dotfiles/common/.local/bin/msmtp-runqueue.sh b/dotfiles/common/.local/bin/msmtp-runqueue.sh
new file mode 100755
index 0000000..1200610
--- /dev/null
+++ b/dotfiles/common/.local/bin/msmtp-runqueue.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env sh
+
+QUEUEDIR="$HOME/.msmtpqueue"
+LOCKFILE="$QUEUEDIR/.lock"
+MAXWAIT=120
+
+OPTIONS=$*
+
+# eat some options that would cause msmtp to return 0 without sendmail mail
+case "$OPTIONS" in
+ *--help*)
+ echo "$0: send mails in $QUEUEDIR"
+ echo "Options are passed to msmtp"
+ exit 0
+ ;;
+ *--version*)
+ echo "$0: unknown version"
+ exit 0
+ ;;
+esac
+
+# wait for a lock that another instance has set
+WAIT=0
+while [ -e "$LOCKFILE" ] && [ "$WAIT" -lt "$MAXWAIT" ]; do
+ sleep 1
+ WAIT="$((WAIT + 1))"
+done
+if [ -e "$LOCKFILE" ]; then
+ echo "Cannot use $QUEUEDIR: waited $MAXWAIT seconds for"
+ echo "lockfile $LOCKFILE to vanish, giving up."
+ echo "If you are sure that no other instance of this script is"
+ echo "running, then delete the lock file."
+ exit 1
+fi
+
+# change into $QUEUEDIR
+cd "$QUEUEDIR" || exit 1
+
+# check for empty queuedir
+if [ "$(echo ./*.mail)" = './*.mail' ]; then
+ echo "No mails in $QUEUEDIR"
+ exit 0
+fi
+
+# lock the $QUEUEDIR
+touch "$LOCKFILE" || exit 1
+
+# process all mails
+for MAILFILE in *.mail; do
+ MSMTPFILE="$(echo $MAILFILE | sed -e 's/mail/msmtp/')"
+ echo "*** Sending $MAILFILE to $(sed -e 's/^.*-- \(.*$\)/\1/' $MSMTPFILE) ..."
+ if [ ! -f "$MSMTPFILE" ]; then
+ echo "No corresponding file $MSMTPFILE found"
+ echo "FAILURE"
+ continue
+ fi
+ msmtp $OPTIONS $(cat "$MSMTPFILE") < "$MAILFILE"
+ if [ $? -eq 0 ]; then
+ rm "$MAILFILE" "$MSMTPFILE"
+ echo "$MAILFILE sent successfully"
+ else
+ echo "FAILURE"
+ fi
+done
+
+# remove the lock
+rm -f "$LOCKFILE"
+
+exit 0
diff --git a/dotfiles/common/.local/bin/open-file-in-eww b/dotfiles/common/.local/bin/open-file-in-eww
new file mode 100755
index 0000000..e77899e
--- /dev/null
+++ b/dotfiles/common/.local/bin/open-file-in-eww
@@ -0,0 +1,2 @@
+#!/bin/sh
+emacsclient --eval "(eww-open-file \"$1\")"
diff --git a/dotfiles/common/.local/bin/opus2mp3 b/dotfiles/common/.local/bin/opus2mp3
new file mode 100755
index 0000000..eef37ed
--- /dev/null
+++ b/dotfiles/common/.local/bin/opus2mp3
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Craig Jennings Monday, April 25, 2022
+for f in *.opus; do ffmpeg -i "$f" -codec:v copy -codec:a libmp3lame -q:a 2 "${f%.opus}.mp3"; done
diff --git a/dotfiles/common/.local/bin/org-capture.sh b/dotfiles/common/.local/bin/org-capture.sh
new file mode 100755
index 0000000..1e63177
--- /dev/null
+++ b/dotfiles/common/.local/bin/org-capture.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+
+# * Defaults
+
+heading=" "
+protocol="capture-html"
+template="w"
+
+# * Functions
+
+function debug {
+ if [[ -n $debug ]]
+ then
+ function debug {
+ echo "DEBUG: $@" >&2
+ }
+ debug "$@"
+ else
+ function debug {
+ true
+ }
+ fi
+}
+function die {
+ echo "$@" >&2
+ exit 1
+}
+function usage {
+ cat <<EOF
+$0 [OPTIONS] [HTML]
+html | $0 [OPTIONS]
+
+Send HTML to Emacs through org-protocol, passing it through Pandoc to
+convert HTML to Org-mode. HTML may be passed as an argument or
+through STDIN. If only URL is given, it will be downloaded and its
+contents used.
+
+Options:
+ -h, --heading HEADING Heading
+ -r, --readability Capture web page article with python-readability
+ -t, --template TEMPLATE org-capture template key (default: w)
+ -u, --url URL URL
+
+ --debug Print debug info
+ --help I need somebody!
+EOF
+}
+
+function urlencode {
+ python -c "
+from __future__ import print_function
+try:
+ from urllib import quote # Python 2
+except ImportError:
+ from urllib.parse import quote # Python 3
+import sys
+
+print(quote(sys.stdin.read()[:-1], safe=''))"
+}
+
+# * Args
+
+args=$(getopt -n "$0" -o dh:rt:u: -l debug,help,heading:,readability,template:,url: -- "$@") \
+ || die "Unable to parse args. Is getopt installed?"
+eval set -- "$args"
+
+while true
+do
+ case "$1" in
+ -d|--debug)
+ debug=true
+ debug "Debugging on"
+ ;;
+ --help)
+ usage
+ exit
+ ;;
+ -h|--heading)
+ shift
+ heading="$1"
+ ;;
+ -r|--readability)
+ protocol="capture-eww-readable"
+ readability=true
+ ;;
+ -t|--template)
+ shift
+ template="$1"
+ ;;
+ -u|--url)
+ shift
+ url="$1"
+ ;;
+ --)
+ # Remaining args
+ shift
+ rest=("$@")
+ break
+ ;;
+ esac
+
+ shift
+done
+
+debug "ARGS: $args"
+debug "Remaining args: ${rest[@]}"
+
+# * Main
+
+# ** Get HTML
+
+if [[ -n $@ ]]
+then
+ debug "HTML from args"
+
+ html="$@"
+
+elif ! [[ -t 0 ]]
+then
+ debug "HTML from STDIN"
+
+ html=$(cat)
+
+elif [[ -n $url && ! -n $readability ]]
+then
+ debug "Only URL given; downloading..."
+
+ # Download URL
+ html=$(curl "$url") || die "Unable to download $url"
+
+ # Get HTML title for heading
+ heading=$(sed -nr '/<title>/{s|.*<title>([^<]+)</title>.*|\1|i;p;q};' <<<"$html") || heading="A web page with no name"
+
+ debug "Using heading: $heading"
+
+elif [[ -n $readability ]]
+then
+ debug "Using readability"
+
+else
+ usage
+ echo
+ die "I need somethin' ta go on, Cap'n!"
+fi
+
+# ** Check URL
+# The URL shouldn't be empty
+
+[[ -n $url ]] || url="http://example.com"
+
+# ** URL-encode html
+
+heading=$(urlencode <<<"$heading") || die "Unable to urlencode heading."
+url=$(urlencode <<<"$url") || die "Unable to urlencode URL."
+html=$(urlencode <<<"$html") || die "Unable to urlencode HTML."
+
+# ** Send to Emacs
+
+emacsclient "org-protocol://$protocol?template=$template&url=$url&title=$heading&body=$html"
diff --git a/dotfiles/common/.local/bin/org-protocol-setup b/dotfiles/common/.local/bin/org-protocol-setup
new file mode 100755
index 0000000..5ed86a7
--- /dev/null
+++ b/dotfiles/common/.local/bin/org-protocol-setup
@@ -0,0 +1,9 @@
+#!/bin/sh
+# org-protocol-setup
+# Craig Jennings <c@cjennings.net>
+# Register org-protocol scheme handler for Emacs capture
+
+xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol
+update-desktop-database ~/.local/share/applications/
+
+echo "org-protocol handler registered for Emacs"
diff --git a/dotfiles/common/.local/bin/ps-mem b/dotfiles/common/.local/bin/ps-mem
new file mode 100755
index 0000000..b24b003
--- /dev/null
+++ b/dotfiles/common/.local/bin/ps-mem
@@ -0,0 +1,28 @@
+#!/bin/bash
+# Craig Jennings <c@cjennings.net>
+# Outputs a process's memory usage in multiple size units.
+
+# Get a list of all processes
+procs=$(ps aux --sort=-%mem | awk '{print $2, $4, $11}' | fzf)
+
+# Check if a process was selected
+if [ -z "$procs" ]; then
+ echo "No process selected."
+ exit 1
+fi
+
+# Get the PID of the selected process (first field)
+PID=$(echo $procs | awk '{print $1}')
+
+# Get the process name
+PROCNAME=$(ps -p $PID -o comm=)
+
+# Get the memory usage
+KB=$(pmap -x $PID | grep total | awk '{print $4}')
+
+# Convert to MB and GB
+MB=$(echo "scale=2; $KB / 1024" | bc)
+GB=$(echo "scale=2; $MB / 1024" | bc)
+
+# Print the memory usage
+printf "$PROCNAME (pid $PID) mem usage: $KB KB | $MB MB | $GB GB\n\n"
diff --git a/dotfiles/common/.local/bin/refresharchkeys b/dotfiles/common/.local/bin/refresharchkeys
new file mode 100755
index 0000000..db1e755
--- /dev/null
+++ b/dotfiles/common/.local/bin/refresharchkeys
@@ -0,0 +1,6 @@
+#!/bin/sh
+sudo rm -R /etc/pacman.d/gnupg
+sudo pacman-key --init
+sudo pacman-key --populate archlinux
+sudo pacman -Sy archlinux-keyring
+sudo pacman -Syu
diff --git a/dotfiles/common/.local/bin/ssh-createkeys b/dotfiles/common/.local/bin/ssh-createkeys
new file mode 100755
index 0000000..a1c14b6
--- /dev/null
+++ b/dotfiles/common/.local/bin/ssh-createkeys
@@ -0,0 +1,3 @@
+#!/bin/sh
+# use strong passwords
+ssh-keygen -t ed25519 -a 100
diff --git a/dotfiles/common/.local/bin/timezone-change b/dotfiles/common/.local/bin/timezone-change
new file mode 100755
index 0000000..c5a4e5a
--- /dev/null
+++ b/dotfiles/common/.local/bin/timezone-change
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+# Craig Jennings <c@cjennings.net>
+
+# Convenience script since I can't ever remember the two part TZ identifier to
+# change timezones.
+
+case $1 in
+ "eastern"|"ny"|"nyc"|"boston"|"dc")
+ sudo timedatectl set-timezone "US/Eastern"
+ ;;
+ "central"|"nola"|"home"|"chicago")
+ sudo timedatectl set-timezone "US/Central"
+ ;;
+ "mountain")
+ sudo timedatectl set-timezone "US/Mountain"
+ ;;
+ "pacific"|"sf"|"oakland"|"berkeley"|"hb"|"california")
+ sudo timedatectl set-timezone "US/Pacific"
+ ;;
+ "london"|"england"|"britain"|"gb")
+ sudo timedatectl set-timezone "Europe/London"
+ ;;
+ "hawaii")
+ sudo timedatectl set-timezone "US/Hawaii"
+ ;;
+ "armenia"|"yerevan")
+ sudo timedatectl set-timezone "Asia/Yerevan"
+ ;;
+ "greece"|"athens")
+ sudo timedatectl set-timezone "Europe/Athens"
+ ;;
+ "germany"|"berlin")
+ sudo timedatectl set-timezone "Europe/Berlin"
+ ;;
+ "turkey"|"istanbul")
+ sudo timedatectl set-timezone "Europe/Istanbul"
+ ;;
+ "portugal"|"lisbon")
+ sudo timedatectl set-timezone "Europe/Portugal"
+ ;;
+ "spain"|"madrid")
+ sudo timedatectl set-timezone "Europe/Madrid"
+ ;;
+ "france"|"paris")
+ sudo timedatectl set-timezone "Europe/Paris"
+ ;;
+ "italy"|"rome"|"naples"|"ischia")
+ sudo timedatectl set-timezone "Europe/Rome"
+ ;;
+ "austria"|"vienna")
+ sudo timedatectl set-timezone "Europe/Vienna"
+ ;;
+ "japan"|"tokyo")
+ sudo timedatectl set-timezone "Asia/Tokyo"
+ ;;
+ "jamaica")
+ sudo timedatectl set-timezone "America/Jamaica"
+ ;;
+ "st_lucia"|"grenadines"|"st_vincent"|"nevis"|"st_kitts"|"puerto_rico")
+ sudo timedatectl set-timezone "America/Puerto_Rico"
+ ;;
+ *)
+ echo
+ "Invalid option chosen."
+ echo
+ "Some valid options are: eastern, central, pacific, rome, london, st_lucia, italy, france, spain ."
+ ;;
+esac
diff --git a/dotfiles/common/.local/bin/timezone-set b/dotfiles/common/.local/bin/timezone-set
new file mode 100755
index 0000000..1fe7370
--- /dev/null
+++ b/dotfiles/common/.local/bin/timezone-set
@@ -0,0 +1,16 @@
+#!/bin/sh
+# Craig Jennings <c@cjennings.net>
+
+# sets timezone based on the ip-address when there's network connectivity
+
+# Check network status
+if ping -q -c 1 -W 1 google.com >/dev/null; then
+ NEW_TIMEZONE="$(curl --fail --silent https://ipapi.co/timezone)"
+ if sudo timedatectl set-timezone "$NEW_TIMEZONE"; then
+ notify-send "Setting timezone to $NEW_TIMEZONE successul."
+ else
+ notify-send "Attempt to set timezone failed."
+ fi
+else
+ notify-send "No network connection detected. Cannot set timezone automatically."
+fi
diff --git a/dotfiles/common/.local/bin/torwrap b/dotfiles/common/.local/bin/torwrap
new file mode 100755
index 0000000..8b20ad4
--- /dev/null
+++ b/dotfiles/common/.local/bin/torwrap
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+ifinstalled tremc transmission-cli || exit
+
+! pidof transmission-daemon >/dev/null && transmission-daemon && notify-send "Starting torrent daemon..."
+
+$TERMINAL -e tremc; pkill -RTMIN+7 "${STATUSBAR:-dwmblocks}"
diff --git a/dotfiles/common/.local/bin/updatemirrors b/dotfiles/common/.local/bin/updatemirrors
new file mode 100755
index 0000000..3ba4f7f
--- /dev/null
+++ b/dotfiles/common/.local/bin/updatemirrors
@@ -0,0 +1,20 @@
+#!/bin/sh
+# cjennings generates arch linux mirror list
+echo "Updating mirrorlist. Please be patient. This may take a few minutes...."
+echo " "
+
+sudo reflector \
+ --connection-timeout 3 \
+ --download-timeout 3 \
+ --protocol https \
+ --country US \
+ --age 18 \
+ --latest 20 \
+ --score 10 \
+ --fastest 5 \
+ --sort score \
+ --save /etc/pacman.d/mirrorlist > /dev/null 2>&1
+
+cat /etc/pacman.d/mirrorlist
+echo " "
+echo "Done."
diff --git a/dotfiles/common/.local/bin/warpinator-start b/dotfiles/common/.local/bin/warpinator-start
new file mode 100755
index 0000000..2d1798c
--- /dev/null
+++ b/dotfiles/common/.local/bin/warpinator-start
@@ -0,0 +1,11 @@
+#!/bin/sh
+# attempts to launch warpinator
+
+logdir="$HOME/.local/var/log/"
+[ -d $logdir ] || mkdir -p "$logdir"
+logfile="$logdir/$(date +%Y-%m-%d_%H-%M-%S-%3N.warpinator.log)"
+
+echo "$(date): Starting warpinator" >> "$logfile" 2>&1
+sleep 20 && warpinator >> "$logfile" 2>&1
+echo "$(date): Starting warpinator again" >> "$logfile" 2>&1
+sleep 20 && warpinator >> "$logfile" 2>&1