diff options
Diffstat (limited to 'dotfiles/common/.local/bin')
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 |
