+ # note: logic duplicated in beetpull
+ local remote_p=true
+ if [[ $HOSTNAME == kd ]]; then
+ remote_p=false
+ fi
+
+ if $remote_p; then
+ finalpath="$cachedir${path#/i/m}"
+ rowir rsync --partial -a --inplace --mkpath "b8.nz:$path" "$finalpath"
+ finalnextpath="$cachedir${nextpath#/i/m}"
+ count=$(pgrep -a -f "^rsync --partial -a --inplace --mkpath $cachedir" || [[ $? == 1 ]] )
+ # allow us to start 2 rsyncs in the background
+ if [[ $count == [01] ]]; then
+ rinr rsync --partial -a --inplace --mkpath "b8.nz:$nextpath" "$finalnextpath" &
+ fi
+ else
+ finalpath="$path"
+ fi
+ mpvrpc '{ "command": ["loadfile", "'"$finalpath"'"] }'
+}
+
+# tag with beets.
+# usage: beetag [-r] [-s] QUERY
+# it lists the query, reads an input char for tagging one by one.
+#
+# note, you may want to change the play command for doing rapid taging
+# by immediately jumping forward into the song. this is set in the beets
+# config yaml.
+#
+# (available buttons: ` \ ) ] [ and non-printing chars, see
+# https://stackoverflow.com/questions/10679188/casing-arrow-keys-in-bash
+#
+#
+# note: after foregrounding the player, must quit it to get back. can't ctrl-c.
+#
+# keys I dont need help to remember:
+# 1-5 rate
+# q quit
+# ret next
+#
+# todo: enter should also unpause
+beetag() {
+ local last_genre_i fstring tag id char new_item char_i genre tag remove doplay i j random path
+ local do_rare_genres read_wait line lsout tmp ls_line skip_lookback
+ local escape_char escaped_input expected_input skip_input_regex right_pad erasable_line seek_sec
+ local pl_state_path pl_state_dir pl_state_file tmpstr
+ local new_random pl_seed_path seed_num seed_file fmt first_play repeat1
+ local -a buttons button_map ids tags tmp_tags initial_ls ls_lines paths
+ local -A button_i
+ local -i i j volume scrolled id_count line_int skip_start pre_j_count head_count skip_lookback
+ local -i overflow_lines overflow
+
+ first_play=true
+ erasable_line=false
+ escape_char=$(printf "\u1b")
+ scrolled=999 # more than any $LINES
+ ### begin arg processing ###
+ random=false
+ repeat1=false
+ new_random=false
+ case $1 in
+ -r)
+ random=true
+ shift
+ ;;
+ -s)
+ random=false
+ shift
+ ;;
+ -x)
+ new_random=true
+ shift
+ ;;
+ esac
+ if (( ! $# )); then
+ echo beetag: error expected a query arg >&2
+ return 1
+ fi
+ ### end arg processing ###
+
+ # note: I used to do beetpull here, but mpv + ssfs on slowish
+ # connection leads to bad/buggy result.
+
+ do_rare_genres=false
+ volume=70
+ read_wait=2
+ doplay=true
+
+ last_genre_i=$(( ${#common_genres[@]} - 1 ))
+ buttons=( {a..p} {r..w} {6..8} , . / - "=")
+ button_map=(${common_genres[@]} ${pl_tags[@]})
+ fstring=
+ for tag in "${pl_tags[@]}"; do
+ fstring+="%ifdef{$tag,$tag }"
+ done
+
+ for (( i=0; i<${#buttons[@]}; i++ )); do
+ button_i[${buttons[i]}]=$i
+ done
+
+ # note: this structure of files is rather haphazard.
+ seed_num=1 # later we might want a few
+ seed_file=seed$seed_num
+ if $random; then
+ pl_state_file=$seed_num
+ else
+ pl_state_file=sorted
+ fi
+ pl_state_dir=/b/data/pl-state
+ if [[ $playlist ]]; then
+ pl_state_dir=$pl_state_dir/$playlist
+ else
+ pl_state_dir=$pl_state_dir/nopl
+ fi
+ pl_state_path=$pl_state_dir/$pl_state_file
+ pl_seed_path=$pl_state_dir/$seed_file
+
+
+ if $new_random || [[ ! -r $pl_seed_path ]]; then
+ mkdir -p $pl_state_dir
+ { base64 < /dev/urandom | head -c 200 ||:; echo; } > $pl_seed_path
+ fi
+
+ # PijokVipiotOzeph is just a random string for a delimiter
+ # shellcheck disable=SC2016 # false positive
+ fmt='%ifdef{rating,$rating }'"$fstring"'$genre | $title - $artist - $album $length $id PijokVipiotOzeph $path'
+ # shellcheck disable=SC2016 # obvious reason
+ tmpstr=$(beet ls -f "$fmt" "$@" | { if $random; then sort -R --random-source=$pl_seed_path; else cat; fi; } )
+ mapfile -t initial_ls <<<"$tmpstr"
+ if [[ ! ${initial_ls[0]} ]]; then
+ echo "beetag: error: no result from beet ls $*"
+ return 1
+ fi
+ id_count=${#initial_ls[@]}
+ for line in "${initial_ls[@]}"; do
+ path="${line#*PijokVipiotOzeph }"
+ # https://github.com/koalaman/shellcheck/issues/2171
+ # shellcheck disable=SC2190 # bug in shellcheck, looking at paths from an earlier function
+ paths+=("$path")
+ line_no_path="${line% PijokVipiotOzeph*}"
+ id="${line_no_path##* }"
+ ids+=("$id")
+ right_pad="${line_no_path%% |*}"
+ ls_line="$(printf %-11s "$right_pad")${line_no_path#"$right_pad"}"
+ ls_lines+=("$ls_line")
+ i=$(( i+1 ))
+ done
+
+
+
+
+ j=0
+ if [[ $playlist ]]; then
+ if [[ -r $pl_state_path ]]; then
+ j=$(cat $pl_state_path)
+ fi
+ fi
+
+ # i only care to see a smallish portion of the list when starting.
+ head_count=$(( LINES - 20 ))
+ head_start=$(( j - head_count / 2 ))
+ if (( head_start < 0 )); then
+ head_start=0
+ fi
+ for (( i=head_start; i < head_count && i < id_count; i++ )); do
+ ls_line="${ls_lines[$i]}"
+ if (( i == j )); then
+ echo "* $ls_line"
+ else
+ echo "$ls_line"
+ fi
+ done
+ if $doplay; then
+ #{ mpv --profile=a --volume=$volume --idle 2>&1 & } 2>/dev/null
+ mpv --profile=a --volume=$volume --idle &
+ # if we dont sleep, can expect an error like this:
+ # socat[1103381] E connect(5, AF=1 "/tmp/mpvsock", 14): Connection refused
+ sleep .1
+ fi
+
+ while true; do
+ id=${ids[j]}
+ path="${paths[$j]}"
+ lsout="${ls_lines[j]}"
+ tags=( ${lsout%%,*} )
+ beetag-help
+ printf "██ %s\n" "$lsout"
+ beetag-nostatus 1
+ if $doplay; then
+ # https://stackoverflow.com/a/7687716
+ # note: duplicated down below
+ #
+ # notes on old method of invoking mpv each time:
+ # https://superuser.com/questions/305933/preventing-bash-from-displaying-done-when-a-background-command-finishes-execut
+ # we can't disown or run in a subshell or set +m because all that
+ # disabled job control from working properly in ways we want.
+ # todo: figure out some kind of answer to this. I think the solution
+ # is that we are waiting in 2 second intervals and checking if the
+ # background job exists. Instead, we should make mpv just idle
+ # when it is done with a song and then send it a command to play a new track.
+ #{ mpv --profile=a --volume=$volume "$path" 2>&1 & } 2>/dev/null
+ # old
+ #{ beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
+
+ # on slow systems, we may need to wait like .3 seconds before mpv
+ # is ready. so impatiently check until it is ready
+ if $first_play; then
+ first_play=false
+ for (( i=0; i<20; i++ )); do
+ if [[ $(mpvrpco '{ "command": ["get_property", "idle-active"] }' 2>/dev/null | jq .data) == true ]]; then
+ mpvrpc-loadfile "$path" 2>/dev/null
+ break
+ fi
+ sleep .1
+ done
+ else
+ mpvrpc-loadfile "$path"
+ fi
+ erasable_line=false
+ fi
+ while true; do
+ char=
+ if $doplay; then
+ ret=0
+ read -rsN1 -t $read_wait char || ret=$?
+ read_wait=2
+ # Automatically skip to the next song if this one ends, unless
+ # we turn off the autoplay.
+ if (( ret == 142 )) || [[ ! $char ]]; then
+ if jobs -p | grep -q . &>/dev/null && \
+ [[ $(mpvrpco '{ "command": ["get_property", "idle-active"] }' | jq .data) == false ]]; then
+ continue
+ else
+ break
+ fi
+ fi
+ else
+ read -rsN1 char
+ fi
+ beetag-help
+ if [[ $char == $'\n' ]]; then
+ break
+ fi
+ case $char in
+ ";")
+ j=$(( j - 2 ))
+ break
+ ;;
+ "'")
+ if $doplay; then
+ echo "play toggled off"
+ doplay=false
+ else
+ doplay=true
+ mpvrpc-loadfile "$path"
+ erasable_line=false
+ fi
+ beetag-nostatus 1
+ continue
+ ;;
+ _)
+ m beet rm --delete --force "id:$id"
+ beetag-nostatus 4 # guessing. dont want to test atm
+ break
+ ;;
+ [1-5])
+ beetmq "id:$id" rating=$char
+ continue
+ ;;
+ 9)
+ volume=$(( volume - 5 ))
+ if (( volume < 0 )); then
+ volume=0
+ fi
+ ;;&
+ 0)
+ volume+=5
+ if (( volume > 130 )); then
+ volume=130
+ fi
+ ;;&
+ 0|9)
+ mpvrpc '{ "command": ["set_property", "volume", '$volume'] }'
+ beetag-status
+ echo volume=$volume
+ continue
+ ;;
+ ']')
+ if $repeat1; then
+ repeat1=false
+ else
+ repeat1=true
+ fi
+ echo repeat1=$repeat1
+ continue
+ ;;
+ q)
+ kill-bg-quiet
+ return
+ ;;
+ y)
+ if $do_rare_genres; then
+ do_rare_genres=false
+ button_map=(${common_genres[@]} ${pl_tags[@]})
+ last_genre_i=$(( ${#rare_genres[@]} - 1 ))
+ else
+ do_rare_genres=true
+ button_map=(${rare_genres[@]} ${pl_tags[@]})
+ last_genre_i=$(( ${#rare_genres[@]} - 1 ))
+ fi
+ local -A button_i
+ for (( i=0; i<${#buttons[@]}; i++ )); do
+ button_i[${buttons[i]}]=$i
+ done
+ for (( i=0; i<${#button_map[@]}; i++ )); do
+ echo ${buttons[i]} ${button_map[i]}
+ done
+ continue
+ ;;
+ z)
+ beetag-nostatus 3
+ # if we ctrl-z, it will put the whole function into sleep. so
+ # basically, we can't return from a foregrounded mpv like we
+ # would like to without some strange mechanism I can't think
+ # of. So, instead, detect ctrl-c and wait a while for prompt
+ # input. One idea would be to use a music player like mpd where
+ # we can send it messages.
+ if ! fg; then
+ read_wait=10
+ fi
+ continue
+ ;;
+
+ #
+ " ")
+ # output time if we aren't already paused
+ if [[ $(mpvrpco '{ "command": ["get_property", "pause"] }' | jq .data) == false ]]; then
+ # minutes/seconds
+ #date -d @"$(mpvrpco '{ "command": ["get_property", "playback-time"] }' | jq .data)" +%M:%S ||:
+ beetag-status
+ mpvrpc-percent-pos
+ fi
+ # originally found this solution, which worked fine.
+ #kill -STOP %% &>/dev/null
+ #
+ mpvrpc '{ "command": ["cycle", "pause"] }'
+ continue
+ ;;
+ "$escape_char")
+ expected_input=true
+ read -rsn2 escaped_input
+ skip_input_regex="^[0-9]+$"
+ case $escaped_input in
+ # up char: show all the songs, use less
+ '[A')
+ skip_start=0
+ skip_lookback=5
+ if (( j - skip_lookback > skip_start )); then
+ skip_start=$(( j - skip_lookback ))
+ fi
+ beetag-nostatus $(( id_count - skip_start - 1 ))
+ {
+ line_int=0
+ for (( i=skip_start; i < id_count; i++ )); do
+ if (( i == j )); then
+ echo " * ${ls_lines[i]}"
+ continue
+ fi
+ echo "$line_int | ${ls_lines[i]}"
+ line_int+=1
+ done
+ } | less -F
+ ;;
+ # down char
+ '[B')
+ # skip forward, but show the last few songs anyways.
+ skip_start=0
+ skip_lookback=3
+ if (( j - skip_lookback > skip_start )); then
+ skip_start=$(( j - skip_lookback ))
+ fi
+ beetag-nostatus $(( id_count - skip_start - 1 ))
+
+ line_int=0
+ overflow_lines=$LINES
+ for (( i=skip_start; i < overflow_lines - 1 && i < id_count; i++ )); do
+ ls_line="${ls_lines[i]}"
+ overflow=$(( ${#ls_line} / ( COLUMNS - 1 ) ))
+ overflow_lines=$(( overflow_lines - overflow ))
+ if (( i == j )); then
+ echo " * $ls_line"
+ continue
+ fi
+ echo "$line_int | $ls_line"
+ line_int+=1
+ done
+ ;;
+ # left key
+ '[D')
+ seek_sec=-8
+ ;;&
+ # right key
+ '[C')
+ seek_sec=8
+ ;;&
+ '[C'|'[D')
+ beetag-status
+ mpvrpc-percent-pos
+ erasable_line=true
+ mpvrpc '{ "command": ["seek", "'$seek_sec'"] }'
+ continue
+ ;;
+ *)
+ expected_input=false
+ ;;
+ esac
+ if $expected_input; then
+ read -r skip_input
+ case $skip_input in
+ q)
+ kill-bg-quiet
+ return
+ ;;
+ esac
+ if [[ $skip_input =~ $skip_input_regex ]]; then
+ pre_j_count=$(( j - skip_start ))
+ j=$(( j + skip_input - pre_j_count ))
+ if (( skip_input < pre_j_count )); then
+ j=$(( j - 1 ))
+ fi
+ fi
+ break
+ fi
+ ;;
+ esac
+ char_i=${button_i[$char]}
+ new_item=${button_map[$char_i]}
+ if [[ ! $char_i || ! $new_item ]]; then
+ echo "error: no mapping of input: $char found, try again"
+ continue
+ fi
+ if (( char_i <= last_genre_i )); then
+ m beetmq "id:$id" genre=$new_item
+ else
+ remove=false
+ tmp_tags=()
+ for tag in ${tags[@]}; do
+ if [[ $new_item == "$tag" ]]; then
+ remove=true
+ else
+ tmp_tags+=("$tag")
+ fi
+ done
+ if $remove; then
+ tags=("${tags[@]}")
+ m beetmq "id:$id" "$new_item!"
+ else
+ tags+=("$new_item")
+ m beetmq "id:$id" $new_item=t
+ fi
+ fi
+ done
+ if ! $repeat1; then
+ if (( j < id_count - 1 )); then
+ j+=1
+ else
+ j=0
+ fi
+ fi
+ if [[ $playlist ]]; then
+ echo $j >$pl_state_path
+ fi
+ done
+}
+
+# usage: FILE|ALBUM_DIR [GENRE]
+beetadd() {
+ local import_path genre_arg single_track_arg
+ import_path="$1"
+ if [[ ! -e $import_path ]]; then
+ echo "beetadd error: path does not exist"
+ fi
+ if [[ $2 ]]; then
+ genre_arg="--set genre=$2"
+ fi
+ if [[ -f $import_path ]]; then
+ single_track_arg=-s
+ fi
+ beet import --set totag=t $single_track_arg $genre_arg "$import_path"
+ beetag totag:t
+ beet modify -y totag:t "totag!"
+}
+
+# update navidrome music data after doing beets tagging
+beet2nav() {
+ m beetpull
+ m beetconvert
+ m beetrating
+ # this function would naturally just be part of beetconvert,
+ # but we want beetrating to happen sooner so that our ssh auth dialog
+ # happens earlier. Currently 17 seconds for that.
+ m beetconvert-rm-extras
+ m beetsmartplaylists
+}
+
+# pull in beets library locally
+beetpull() {
+ local sshfs_host sshfs_cmd
+ sshfs_host=b8.nz
+ if [[ $HOSTNAME == kd ]]; then
+ return 0
+ fi
+ if [[ ! -e /i ]]; then
+ s mkdir /i
+ s chown iank:iank /i
+ fi
+ sshfs_cmd="sshfs -o ServerAliveInterval=15,reconnect $sshfs_host:/i /i"
+ if ! pgrep -f "^$sshfs_cmd$" >/dev/null; then
+ m $sshfs_cmd
+ fi
+}
+
+# remove all playlists in navidrome, for when I make big
+# playlist name changes and just want to scrap everything.
+nav-rm-plists() {
+ local tmpf id
+ tmpf=$(mktemp)
+ if [[ $HOSTNAME != kd ]]; then
+ echo "error: run on kd"
+ return 1
+ fi
+ sqlite3 /i/navidrome/navidrome.db "select id from playlist" >$tmpf
+ while read -r id; do
+
+ curl --http1.1 --user "iank:$navidrome_pw" "https://b8.nz/rest/deletePlaylist.view?u=iank&s=sb219dvv7egnoe4i47k75cli0m&t=1c8f5575cd0fdf03deb971187c9c88b1&v=1.2.0&c=DSub&id=$id"
+ done <$tmpf
+ rm $tmpf
+}
+
+# escape regex.
+#
+# This is not perfect but generally good enough. It escapes all
+# metachars listed man 3 pcrepattern.
+er() {
+ sed 's/[]\\^$.[|()?*+{}]/[&]/g; s/\^/\\^/g' <<<"$*"
+}
+
+# usage beegenre QUERY
+#
+# beet set genre for QUERY based on existing artist most used genre on
+#
+# inverse of query for each artist found in QUERY. If query starts with
+# "artist:" it is used as the artist instead of each artist in QUERY.
+#
+beegenre() {
+ local count artist artregex genre singleartist tmpf tmpf2
+ local -a artists genres
+ singleartist=false
+ case $1 in
+ artist:*)
+ singleartist=true
+ artist="$1"
+ shift
+ ;;
+ esac
+ tmpf=$(mktemp)
+ tmpf2=$(mktemp)
+ if $singleartist; then
+ # shellcheck disable=SC2016 # obvious reason
+ beet ls -f '$genre' "$artist" "${@/#/^}" | sort | uniq -c | sort -n | tail -n1 >$tmpf
+ read -r count genre <$tmpf ||:
+ beet modify "$artist" "$@" genre=$genre
+ else
+ # shellcheck disable=SC2016 # obvious reason
+ beet ls -f '$artist' "$@" | sort -u >$tmpf
+ while read -r artist; do
+ artregex=$(er "$artist")
+ # shellcheck disable=SC2016 # obvious reason
+ beet ls -f '$genre' "artist::^$artregex$" "${@/#/^}" | sort | uniq -c | sort -n | tail -n1 >$tmpf2
+ read -r count genre <$tmpf2 || continue
+ if [[ $count ]]; then
+ artists+=("$artregex")
+ genres+=("$genre")
+ echo "beet modify -y $* \"artist::^$artist$\" genre=$genre # $count"
+ fi
+ done <$tmpf
+ read -r -N 1 -s -p "Y/n " char
+ case $char in
+ [Yy$'\n'])
+ for (( i=0; i<${#artists[@]}; i++ )); do
+ beet modify -y "$@" "artist::^${artists[i]}$" genre=${genre[i]}
+ done
+ ;;
+ esac
+ fi
+ rm $tmpf
+}
+
+# prettify the date
+btrbk-date() {
+ local indate
+ indate="$1"
+ shift
+ date +%F_%T%:::z -d "$(sed -r 's/(.{4})(..)(.{5})(..)(.*)/\1-\2-\3:\4:\5/' <<<"$indate")" "$@"
+}
+btrbk-undate() {
+ # fudCaHougfirp is a random string
+ { if [[ $1 ]]; then
+ echo "$1"
+ else
+ cat
+ fi
+ } | sed -r 's/-0([45])( |$)/fudCaHougfirp0\100/;s/_/T/;s/[:-]//g;s/fudCaHougfirp/-/'
+
+}
+btrbk-date-sed() {
+ local line
+ while read -r line; do
+ if [[ $line == *20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00* ]]; then
+ pre="${line%%20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00*}"
+ post="${line##*20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00}"
+ mid="${line:${#pre}:22}"
+ echo "$pre$(btrbk-date "$mid")$post"
+ else
+ echo "$line"
+ fi
+ done
+}
+jrbtrbk() {
+ jr -u btrbk-run -u btrbk -u switch-mail-host "$@"
+}
+
+# internal function
+btrbk-host-debug-show-host() {
+ for f; do
+ snaphost=
+ for host in $remote $alt local; do
+ if line=$(grep -P "\S*$f" /tmp/b/s/$host.log); then
+ if [[ $snaphost ]]; then
+ e error: snaphost=$snaphost, host=$host line="$line"
+ fi
+ if [[ $line == ssh* ]]; then
+ tmp="${line#ssh://}"
+ snaphost="${tmp%%/*}"
+ else
+ snaphost=$host
+ fi
+ fi
+ done
+ echo $snaphost $f | btrbk-date-sed
+ done
+}
+
+# If we get a btrfs receive error like this:
+# ERROR: ... clone: did not find source subvol
+# running this command will help track down the problem.
+# Alter remote= and alt=. When I used it, remote is
+# the host having the error when I push a snapshot.
+# Alt is just the other host that takes snapshots
+# besides the local host.
+btrbk-host-debug() {
+
+ remote=b8.nz
+ alt=sywg.b8.nz
+
+ mkdir -p /tmp/b/s
+ for host in $remote $alt; do
+ h=$(ssh $host hostname)
+ rsync -a /var/log/btrbk $host:/var/log/btrbk /var/log/btrbk/$h
+ grr '\bsnapshot success' /var/log/btrbk/$h >/tmp/b/$h.log
+
+ ## this takes a while, we only want to do it on 1st run
+ # if [[ -s /tmp/b/$host.log ]]; then continue; fi
+ # ssh $host journalctl -u btrbk-run -u btrbk -u switch-mail-host >/tmp/b/$host.log
+ done
+ gr '\bsnapshot success' /var/log/btrbk/*.log >/tmp/b/local.log
+ cd /tmp/b
+ for f in *.log; do
+ gr '\bsnapshot success' $f >s/$f
+ done
+ cd /mnt/root/btrbk
+ localq=(q.*)
+ declare -A localq_a
+ for f in "${localq[@]}"; do
+ localq_a[$f]=t
+ done
+
+ remoteq=()
+ for f in $(ssh $remote "cd /mnt/root/btrbk; echo q.*"); do
+ if [[ ! ${localq_a[$f]} ]]; then
+ remoteq+=($f)
+ fi
+ done
+ btrbk-host-debug-show-host "${localq[@]}"
+ if (( ${#remoteq[@]} >= 1 )); then
+ echo "=== $remote only ===="
+ btrbk-host-debug-show-host ${remoteq[@]}
+ fi
+
+}
+
+# note, to check for glue records
+# First, find some the .org nameservers:
+# dig +trace iankelling.org
+# then, query one:
+# dig ns1.iankelling.org @b0.org.afilias-nst.org.
+
+# Now, compare for a domain that does have glue records setup (note the A
+# and AAAA records in ADDITIONAL SECTION, those are glue records like the
+# one I'm asking for):
+
+# $ dig ns1.gnu.org @b0.org.afilias-nst.org.
+
+bbk() { # btrbk wrapper
+ local ret=0
+ c /
+ local active=true
+ systemctl is-active btrbk.timer || active=false
+ if $active; then
+ ser stop btrbk.timer
+ fi
+ btrbk_is_active=$(systemctl is-active btrbk.service ||:)
+ case $btrbk_is_active in
+ inactive|failed) : ;;
+ *)
+ echo "bbk: error: systemctl is-active btrbk.service output: $btrbk_is_active"
+ if $active; then ser start btrbk.timer; fi
+ return 1
+ ;;
+ esac
+ # todo: consider changing this to srun and having the args come
+ # from a file like /etc/default/btrbk, like is done in exim
+ s jdo btrbk-run "$@"
+ if $active; then
+ if (( ret )); then
+ echo bbk: WARNING: btrbk.timer not restarted due to failure
+ else
+ ser start btrbk.timer
+ fi
+ fi
+ return $ret
+}
+
+faimon() {
+ fai-monitor | pee cat "fai-monitor-gui -"
+}
+
+bfg() { java -jar /a/opt/bfg-1.12.14.jar "$@"; }
+
+bigclock() {
+ xclock -digital -update 1 -face 'arial black-80:bold'
+}
+
+nnn() { /a/opt/nnn -H "$@"; }
+
+locat() { # log-once cat
+ local files
+ ngset
+ files=(/var/local/cron-errors/* /home/iank/cron-errors/* /sysd-mail-once-state/*)
+ case ${#files[@]} in
+ 0) : ;;
+ 1)
+ echo ${files[0]}
+ head ${files[0]}
+ ;;
+ *)
+ head ${files[@]}
+ ;;
+ esac
+ ngreset
+}
+
+scr() {
+ screen -RD "$@"
+}
+
+# usage: first get an adb shell on the phone.
+#
+# just followed instructions in readme at
+# https://github.com/Yuubi-san/ceb-tools
+# tried to use ceb2txt but it failed because of schema
+# slightly different than what it expected.
+cheogram-get-logs() {
+ #adb shell rm -r /storage/emulated/0/Download/Cheogram/Backup
+ read -r -p "do cheogram backup on phone, do not enable extra cheogram data. press any key when done"
+ cd /p/cheogram
+ rm -rf Backup b
+ adb pull /storage/emulated/0/Download/Cheogram/Backup
+ sqlite3 b </a/opt/ceb-tools/schema.sql
+ echo "note: the next step took 39 seconds last time i measured"
+ # expected failure: Error: near line 1: in prepare, table accounts has no column named pinned_mechanism (1)
+ # the sql needs an update
+ /a/opt/ceb-tools/ceb2sqlgz Backup/iank@fsf.org.ceb <pas | gunzip | sqlite3 b ||:
+ rm -r Backup
+}
+
+# usage: cheologs [DAYS_LIMIT]
+# default days is 100
+cheologs() {
+ local days q
+ days=${1:-100}
+ q="
+select
+ datetime(substr(timeSent,0,11), 'unixepoch'),
+ replace(replace(counterpart,'@fsf.org',''),
+ '@conference.fsf.org',''),
+ body
+from messages
+where timeSent > $(( (EPOCHSECONDS - days * 60 * 60 * 24) * 1000 ))
+order by timeSent;"
+ sqlite3 /p/cheogram/b ".mode tabs" "$q" | less
+}
+
+mycheologs() {
+ local days q
+ days=${1:-16}
+ # timezone compared to utc. note: this takes the current offset, so if daylight savings change
+ # happened in the looking back period, this won't account for it.
+ zone_offset=$(( $( date +%z | sed 's/[^1-9-]*//g' ) * 60 * 60))
+ case $zone_offset in
+ -*) : ;;
+ *) zone_offset="+ $zone_offset"
+ esac
+ echo zone_offset=$zone_offset
+ q="
+select
+ datetime(substr(timeSent,0,11) $zone_offset, 'unixepoch'),
+ body
+from messages
+where timeSent > $(( (EPOCHSECONDS - days * 60 * 60 * 24) * 1000 ))
+and counterpart = 'office@conference.fsf.org/iank'
+order by timeSent;"
+ sqlite3 /p/cheogram/b ".mode tabs" "$q" | sed 's/ /./' | less
+}
+
+# version of jdo for my non-root user
+jdo() {
+ # comparison of alternative logging methods:
+ #
+ # systemd-run command (what this function does)
+ #
+ # If there is a user prompt, the program will detect that it is not
+ # connected to a terminal and act in a non-interactive way, skipping
+ # the prompt. This has the benefit that you know exactly how the
+ # program will act if you want to move it into a service that runs
+ # automatically.
+ #
+ # If run with sudo and command is a shell script which does a sleep,
+ # it can (sometimes?) output some extra whitespace in front of
+ # messages, more for each subsequent message. This can be avoided by
+ # becoming root first.
+ #
+ # It logs the command's pid and exit code, which is nice.
+ #
+ #
+ ### command |& ts | tee file.log
+ #
+ # If there is a user prompt, like "read -p prompt var", it will hang
+ # without outputting the prompt.
+ #
+ # I've had a few times where ts had an error and I wasn't totally sure
+ # if it was really the command or ts having the problem.
+ #
+ # Sometimes some output will get hidden until you hit enter.
+ #
+ #
+ ### command |& pee cat logger
+ #
+ # This seems to work. I need to test more.
+ #
+ #
+ ### command |& logger -s
+ #
+ # User prompts get confusingly prefixed to earlier output, and all log
+ # entries get prefixed with annoying priority level.
+ #
+ #
+ ### systemd-cat
+ #
+ # Had a few problems. One major one is that it exited in the middle of
+ # a command on systemctl daemon-reload
+ #
+ # Related commands which can log a whole session: script, sudo, screen
+ local cmd cmd_name jr_pid ret
+ ret=0
+ cmd="$1"
+ shift
+ cmd_name=${cmd##*/}
+ if [[ $cmd != /* ]]; then
+ cmd=$(type -P "$cmd")
+ fi
+ #note date format for since is date '+%F %T'
+ # -q = quiet
+ journalctl --since=now -qn2 -f -u "$cmd_name" &
+ jr_pid=$!
+ # note, we could have a version that does system --user, but if for example
+ # it does sudo ssh, that will leave a process around that we can't kill
+ # and it will leave the unit hanging around in a failed state needing manual
+ # killing of the process.
+ s systemd-run --uid "$(id -u)" --gid "$(id -g)" \
+ -E SSH_AUTH_SOCK=/run/openssh_agent \
+ --unit "$cmd_name" --wait --collect "$cmd" "$@" || ret=$?
+ # The sleep lets the journal output its last line
+ # before the prompt comes up.
+ sleep .5
+ kill $jr_pid &>/dev/null ||:
+ unset jr_pid
+ fg &>/dev/null ||:
+ # this avoids any err-catch
+ (( ret == 0 )) || return $ret
+}
+
+# service run, and watch the output
+srun() {
+ local unit
+ ret=0
+ unit=$1
+ journalctl -qn2 -f -u $unit &
+ systemctl start $unit
+ sleep 2
+ kill $jr_pid &>/dev/null ||:
+ unset jr_pid
+ fg &>/dev/null ||:
+}
+
+sm() { # switch mail host
+ local tmp keyhash
+ c /
+ # run latest
+ keyhash=$(s ssh-keygen -lf /root/.ssh/home | awk '{print $2}')
+ tmp=$(s ssh-add -l | awk '$2 == "'$keyhash'"' ||:)
+ if [[ ! $tmp ]]; then
+ s ssh-add /root/.ssh/home
+ fi
+ s jdo switch-mail-host "$@"
+ return $ret
+}
+sh2() { # switch host2
+ local tmp keyhash
+ c /
+ # run latest
+ keyhash=$(s ssh-keygen -lf /root/.ssh/home | awk '{print $2}')
+ tmp=$(s ssh-add -l | awk '$2 == "'$keyhash'"')
+ if [[ ! $tmp ]]; then
+ s ssh-add /root/.ssh/home
+ fi
+ install-my-scripts
+ s jdo switch-host2 "$@"
+ return $ret
+}
+
+# shellcheck disable=SC2120
+lipush() {
+ # note, i had --delete-excluded, but that deletes all files in --exclude-from on
+ # the remote site, which doesn't make sense, so not sure why i had it.
+ local p a
+ # excluding emacs for now
+ #p=(/a/opt/{emacs-debian11{,-nox},mu,emacs} /a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts})
+ p=(
+ /a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts}
+ /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter
+ /a/opt/fpaste
+ /p/c/user-specific/www-data/icecast-fsf{,-tech}-htpasswd
+ /p/c/icecast.xml
+ )
+ a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
+ ret=0
+ for h in li je bk; do
+ m s rsync "$@" $a ${p[@]} /p/c/machine_specific/$h root@$h.b8.nz:/
+ ## only li is debian11
+ #p[0]=/a/opt/emacs-trisuqel10
+ #p[1]=/a/opt/emacs-trisquel10-nox
+ done
+ m s rsync "$@" -ahviSAXPH root@li.b8.nz:/a/h/proposed-comments/ /a/h/proposed-comments || ret=$?
+ return $ret
+}
+bkpush() { # no emacs. for running faster.
+ p=(/a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts} /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter)
+ a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
+ ret=0
+ m rsync "$@" $a ${p[@]} /p/c/machine_specific/bk root@bk.b8.nz:/ || ret=$?
+ return $ret
+}
+jepush() { # no emacs. for running faster.
+ p=(/a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts} /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter)
+ a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
+ ret=0
+ m rsync "$@" $a ${p[@]} /p/c/machine_specific/je root@je.b8.nz:/ || ret=$?
+ return $ret
+}
+
+bindpush() {
+ dsign iankelling.org expertpathologyreview.com zroe.org amnimal.ninja
+ lipush
+ for h in li bk; do
+ m ssh iank@$h.b8.nz dnsup
+ done
+}
+bindpushb8() {
+ lipush
+ for h in li bk; do
+ m ssh $h.b8.nz dnsb8
+ done
+}
+
+dnsup() {
+ conflink -f
+ m ser reload named
+}
+dnsb8() {
+ local f=/var/lib/bind/db.b8.nz
+ m ser stop named
+ # jbk is like a temp file. dunno if removing it helps
+
+ i=0
+ while pgrep '^named$' &>/dev/null; do
+ sleep .5
+ i=$(( i + 1 ))
+ if (( i > 100 )); then
+ echo "dnsb8: error: timeout waiting for named to exit"
+ return 1
+ fi
+ done
+ m sudo rm -fv $f.jnl $f.signed.jnl $f.jbk
+ m sudo install -m 644 -o bind -g bind /p/c/machine_specific/vps/bind-initial/db.b8.nz $f
+ m ser restart named
+}
+dnsecgen() {
+ # keys generated like this
+ # because of https://ftp.isc.org/isc/dnssec-guide/dnssec-guide.pdf
+ # https://blog.apnic.net/2019/05/23/how-to-deploying-dnssec-with-bind-and-ubuntu-server/
+
+ # key length is longer than that guide because
+ # we are using those at fsf and when old key lengths
+ # become insecure, I want some extra time to update.
+ # dnsecgen (in brc2)
+
+ local zone=$1
+ dnssec-keygen -a RSASHA256 -b 2048 $zone
+ dnssec-keygen -f KSK -a RSASHA256 -b 4096 $zone
+ for f in K"$zone".*.key; do
+ # eg Kb8.nz.+008+47995.key tag=47995
+ # in dnsimple, you add the long string from this.
+ # in gandi, you add the long string from the .key file,
+ # then see that the digest matches the ds.
+ echo "tag is the number after DS"
+ dnssec-dsfromkey -a SHA-256 $f
+ done
+ # For b8.nz, we let bind read the keys and sign, and
+ # right now they have root ownership, so let them
+ # get group read.
+ chmod g+r ./*.private
+}
+dsign() {
+ # create .signed file
+ # note: full paths probably not needed.
+ local arg
+ for arg; do
+ local zone=${arg#db.}
+ local dir=/p/c/machine_specific/vps/filesystem/var/lib/bind
+ dnssec-signzone -S -e +31536000 -o $zone -K $dir -d $dir $dir/db.$zone
+ done
+}
+
+# set day start for use in other programs.
+# expected to do be in a format like 830, or 800 or 1300.
+ds() {
+ local regex
+ regex='[0-9]?[0-9]?[0-9][0-9]'
+ if [[ $1 ]]; then
+ if [[ ! $1 =~ $regex ]]; then
+ echo "ds: error. expected \$1 to match $regex, got \$1: $1"
+ return 1
+ fi
+ echo $1 >/b/data/daystart
+ else
+ cat /b/data/daystart
+ fi
+}
+
+#### begin bitcoin related things
+btc() {
+ local f=/etc/bitcoin/bitcoin.conf