begin refactoring beetag to be more maintainable
authorIan Kelling <ian@iankelling.org>
Sat, 1 Mar 2025 18:56:06 +0000 (13:56 -0500)
committerIan Kelling <ian@iankelling.org>
Sat, 1 Mar 2025 18:56:06 +0000 (13:56 -0500)
beet-data
beetag [new file with mode: 0755]
brc2

index 7e9dc732a86856f89c14a6c3595a5570b4d07df3..cccc6550051467fdea3bc9881870edf33343072e 100644 (file)
--- a/beet-data
+++ b/beet-data
@@ -42,14 +42,13 @@ nav_tags=(
   run
 )
 
-
+# playlist tags
 pl_tags=(
   "${nav_tags[@]}"
   # alternate version of a song we already have which isn't as good
   lesser_version
 )
 
-nav_convert_query="^genre:spoken-w ^genre:skit ^lesser_version:t rating:3..5"
 
 
 common_genres=(
@@ -122,15 +121,6 @@ tags=(
   sad
 )
 
-declare -A bpla # beet playlist associative array
-beetapl() { # beet add playlist
-  local name
-  name="$1"
-  shift
-  bpla[$name]="${*@Q}"
-}
-
-
 # this function is just so we can have some local vars
 # and not mess with the global var namespace.
 beet-gen-global-vars() {
diff --git a/beetag b/beetag
new file mode 100755 (executable)
index 0000000..05da2f3
--- /dev/null
+++ b/beetag
@@ -0,0 +1,575 @@
+#!/bin/bash
+# I, Ian Kelling, follow the GNU license recommendations at
+# https://www.gnu.org/licenses/license-recommendations.en.html. They
+# recommend that small programs, < 300 lines, be licensed under the
+# Apache License 2.0. This file contains or is part of one or more small
+# programs. If a small program grows beyond 300 lines, I plan to change
+# to a recommended GPL license.
+
+# Copyright 2024 Ian Kelling
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+. /a/c/fsf-script-lib
+
+
+# Must be called from beetag for variables to be setup
+beetag-help() {
+  source /a/bin/ds/beet-data
+  local -i i j col_total row col button_total row_total remainder_cols remainder_term
+  col_total=4
+  button_total=${#button_map[@]}
+  row_total=$(( button_total / col_total ))
+  remainder_cols=$(( button_total % col_total ))
+  # for debugging
+  #dv button_total row_total remainder_cols
+  beetag-nostatus
+  #   - 3 is just a constant that helps things work in practice.
+  if [[ $LINES ]] && (( LINES - 3 < scrolled )); then
+    hr
+    for (( i=0; i<button_total; i++)); do
+      row=$(( i / col_total ))
+      col=$(( i % col_total ))
+      remainder_term=$remainder_cols
+      if (( col < remainder_term )); then
+        remainder_term=$col
+      fi
+      j=$(( col * row_total + row + remainder_term ))
+      # avoid double newline when we have exactly row * col buttons
+      if (( i == button_total - 1 )); then
+        printf "%s %s" ${buttons[j]} ${button_map[j]}
+      elif (( i % col_total == col_total -1 )); then
+        printf "%s %s\n" ${buttons[j]} ${button_map[j]}
+      else
+        printf "%s %-15s" ${buttons[j]} ${button_map[j]}
+      fi
+    done
+    cat <<'EOF'
+
+
+y other genres   z fg player      ' = toggle play  1-5 rate    ] repeat1
+; previous       _ = delete       up/down skip     mpv vol,pause,seek
+EOF
+    hr
+    scrolled=10
+  fi
+}
+
+# Must be called from beetag for variables to be setup
+beetag-nostatus() {
+  if (( $# )); then
+    scrolled=$(( scrolled + $1 ))
+  fi
+  if $erasable_line; then
+    # https://stackoverflow.com/a/71286261
+    # erase line / delete line in terminal
+    printf '\033[1A\033[K'
+  fi
+  erasable_line=false
+}
+# meant to be called from beetag
+beetag-status() {
+  if $erasable_line; then
+    # https://stackoverflow.com/a/71286261
+    printf '\033[1A\033[K'
+  fi
+  erasable_line=true
+}
+
+# meant to be called from beetag
+mpvrpc() {
+  if jobs -p | grep -q . &>/dev/null; then
+    printf "%s\n" "$*" | socat - /tmp/mpvsock >/dev/null ||:
+  fi
+}
+# meant to be called from beetag
+# o for get output
+mpvrpco() {
+  # note: testing for background jobs will output nothing if we are in a pipeline
+  printf "%s\n" "$*" | socat - /tmp/mpvsock ||:
+}
+
+# meant to be called from beetag
+mpvrpc-percent-pos() {
+  mpvrpco '{ "command": ["get_property", "percent-pos"] }' | jq .data | sed 's/\..*/%/' 2>/dev/null ||:
+}
+
+# Call from beetag.
+#
+# Sets variables: pl_state_path pl_seed_path. Does mkdir of their directory.
+#
+# Input vars: random
+beetag-pl-state-init() {
+  local seed_num seed_file pl_state_dir pl_state_file
+    # note: this structure of files is rather haphazard.
+  seed_num=1 # later we might want a few
+  seed_file=seed$seed_num
+
+  pl_state_file=sorted
+  if $random; then
+    pl_state_file=$seed_num
+  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
+  mkdir -p $pl_state_dir
+
+  pl_state_path=$pl_state_dir/$pl_state_file
+  pl_seed_path=$pl_state_dir/$seed_file
+
+  }
+
+# 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()  {
+  source /a/bin/ds/beet-data
+  local last_genre_i tag_query tag id char new_item char_i tag remove doplay i j random path
+  local do_rare_genres read_wait line lsout ls_line skip_lookback
+  local escape_char escaped_input expected_input skip_input_regex right_pad erasable_line seek_sec
+  local pl_state_path tmpstr
+  local new_random pl_seed_path 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 ###
+
+  readonly -a buttons=( {a..p} {r..w} {6..8} , . / - "=")
+
+  # 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 ))
+
+  button_map=(${common_genres[@]} ${pl_tags[@]})
+
+  for (( i=0; i<${#buttons[@]}; i++ )); do
+    button_i[${buttons[i]}]=$i
+  done
+
+  beetag-pl-state-init
+
+  if $new_random || [[ ! -r $pl_seed_path ]]; then
+    { base64 < /dev/urandom | head -c 200 ||:; echo; } > $pl_seed_path
+  fi
+
+  tag_query=
+  for tag in "${pl_tags[@]}"; do
+    tag_query+="%ifdef{$tag,$tag }"
+  done
+  # note: PijokVipiotOzeph is just a random string for a delimiter
+  # shellcheck disable=SC2016 # false positive
+  fmt='%ifdef{rating,$rating }'"$tag_query"'$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"
+            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')
+              echo ok >>/tmp/x
+
+              # 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
+}
+
+beetag "$@"
diff --git a/brc2 b/brc2
index 02b00d030d44e2761005e4cc56dbaa01a3a7dc16..82fb271a3c13a6e63a1c3ba5ded90efe40d2110b 100644 (file)
--- a/brc2
+++ b/brc2
@@ -561,12 +561,13 @@ _iki-convert() {
   esac
 }
 
+source /a/bin/ds/beet-data
+
 
 # Generate beet smartplaylists for navidrome.
 # for going in the reverse direction, run
 # /b/ds/navidrome-playlist-export
 beetsmartplaylists() {
-  source /a/bin/ds/beet-data
   install -m 0700 -d /tmp/ianbeetstmp
   beet splupdate
   # kill off any playlists we deleted. they will still need manual
@@ -618,9 +619,10 @@ update annotation set rating = $rating
   done
 }
 
+nav_convert_query="^genre:spoken-w ^genre:skit ^lesser_version:t rating:3..5"
+
 # Export beets ratings into navidrome
 beetrating() {
-  source /a/bin/ds/beet-data
   local ssh_prefix
   source /p/c/domain-info
   if [[ $HOSTNAME != "$d_host" ]]; then
@@ -632,7 +634,6 @@ beetrating() {
 
 # Do transcoding and hardlinking of audio files for navidrome.
 beetconvert() {
-  source /a/bin/ds/beet-data
   local tmpf
   tmpf="$(mktemp)"
   # a bunch of effort to ignore output we dont care about...
@@ -644,7 +645,6 @@ beetconvert() {
 # This deletes files in the converted directory which should no longer
 # be there due to a rename of the unconverted file.
 beetconvert-rm-extras() {
-  source /a/bin/ds/beet-data
   local l tmpf
   local -A paths
   tmpf="$(mktemp)"
@@ -670,8 +670,16 @@ beetconvert-rm-extras() {
   rm "$tmpf"
 }
 
+declare -A bpla # beet playlist associative array
+beetapl() { # beet add playlist
+  local name
+  name="$1"
+  shift
+  bpla[$name]="${*@Q}"
+}
+
+
 beets-gen-playlists() {
-  source /a/bin/ds/beet-data
   local i str
   local -a query_array query_str
   for i in "${!bpla[@]}"; do
@@ -686,7 +694,6 @@ beets-gen-playlists() {
 EOF
   done
 }
-source /a/bin/ds/beet-data
 
 # beet playlist. use beetag with a playlist name
 bpl() {
@@ -714,7 +721,6 @@ complete -W "${!bpla[*]}" bpl
 
 # beet modify quietly
 beetmq() {
-  source /a/bin/ds/beet-data
   local tmpf
   tmpf="$(mktemp)"
   # a bunch of effort to ignore output we dont care about...
@@ -737,86 +743,6 @@ dv() {
   echo
 }
 
-# Must be called from beetag for variables to be setup
-beetag-help() {
-  source /a/bin/ds/beet-data
-  local -i i j col_total row col button_total row_total remainder_cols remainder_term
-  col_total=4
-  button_total=${#button_map[@]}
-  row_total=$(( button_total / col_total ))
-  remainder_cols=$(( button_total % col_total ))
-  # for debugging
-  #dv button_total row_total remainder_cols
-  beetag-nostatus
-  #   - 3 is just a constant that helps things work in practice.
-  if [[ $LINES ]] && (( LINES - 3 < scrolled )); then
-    hr
-    for (( i=0; i<button_total; i++)); do
-      row=$(( i / col_total ))
-      col=$(( i % col_total ))
-      remainder_term=$remainder_cols
-      if (( col < remainder_term )); then
-        remainder_term=$col
-      fi
-      j=$(( col * row_total + row + remainder_term ))
-      # avoid double newline when we have exactly row * col buttons
-      if (( i == button_total - 1 )); then
-        printf "%s %s" ${buttons[j]} ${button_map[j]}
-      elif (( i % col_total == col_total -1 )); then
-        printf "%s %s\n" ${buttons[j]} ${button_map[j]}
-      else
-        printf "%s %-15s" ${buttons[j]} ${button_map[j]}
-      fi
-    done
-    cat <<'EOF'
-
-
-y other genres   z fg player      ' = toggle play  1-5 rate    ] repeat1
-; previous       _ = delete       up/down skip     mpv vol,pause,seek
-EOF
-    hr
-    scrolled=10
-  fi
-}
-
-# Must be called from beetag for variables to be setup
-beetag-nostatus() {
-  if (( $# )); then
-    scrolled=$(( scrolled + $1 ))
-  fi
-  if $erasable_line; then
-    # https://stackoverflow.com/a/71286261
-    # erase line / delete line in terminal
-    printf '\033[1A\033[K'
-  fi
-  erasable_line=false
-}
-# meant to be called from beetag
-beetag-status() {
-  if $erasable_line; then
-    # https://stackoverflow.com/a/71286261
-    printf '\033[1A\033[K'
-  fi
-  erasable_line=true
-}
-
-# meant to be called from beetag
-mpvrpc() {
-  if jobs -p | grep -q . &>/dev/null; then
-    printf "%s\n" "$*" | socat - /tmp/mpvsock >/dev/null ||:
-  fi
-}
-# meant to be called from beetag
-# o for get output
-mpvrpco() {
-  # note: testing for background jobs will output nothing if we are in a pipeline
-  printf "%s\n" "$*" | socat - /tmp/mpvsock ||:
-}
-
-# meant to be called from beetag
-mpvrpc-percent-pos() {
-  mpvrpco '{ "command": ["get_property", "percent-pos"] }' | jq .data | sed 's/\..*/%/' 2>/dev/null ||:
-}
 
 # run if not running.
 #
@@ -873,463 +799,6 @@ mpvrpc-loadfile() {
   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()  {
-  source /a/bin/ds/beet-data
-  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"
-            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')
-              echo ok >>/tmp/x
-
-              # 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
@@ -1350,7 +819,6 @@ beetadd() {
 
 # update navidrome music data after doing beets tagging
 beet2nav() {
-  source /a/bin/ds/beet-data
   m beetpull
   m beetconvert
   m beetrating
@@ -1363,7 +831,6 @@ beet2nav() {
 
 # pull in beets library locally
 beetpull() {
-  source /a/bin/ds/beet-data
   local sshfs_host sshfs_cmd
   sshfs_host=b8.nz
   source /p/c/domain-info
@@ -1383,7 +850,6 @@ beetpull() {
 # remove all playlists in navidrome, for when I make big
 # playlist name changes and just want to scrap everything.
 nav-rm-plists() {
-  source /a/bin/ds/beet-data
   local tmpf id
   tmpf=$(mktemp)
   source /p/c/domain-info
@@ -1415,7 +881,6 @@ er() {
 # "artist:" it is used as the artist instead of each artist in QUERY.
 #
 beegenre() {
-  source /a/bin/ds/beet-data
   local count artist artregex genre singleartist tmpf tmpf2
   local -a artists genres
   singleartist=false