# beet playlist. use beetag with a playlist name
bpl() {
- eval beetag "${bpla[$1]}"
+ eval beetag -r "$@" "${bpla[${@: -1}]}"
}
complete -W "${!bpla[*]}" bpl
+# beet modify quietly
+beetmq() {
+ local tmpf
+ tmpf="$(mktemp)"
+ # a bunch of effort to ignore output we dont care about...
+ sed 's/^format_item:.*/format_item: ignore_this/' ~/.config/beets/config.yaml >$tmpf
+ beet -c $tmpf modify -y "$@" > >(grep -vFx -e 'ignore_this' -e 'Modifying 1 items.' ||:)
+ rm "$tmpf"
+ scrolled+=1
+}
+
+# Must be called from beetag for variables to be setup
+beetag-help() {
+ # - 3 is just a constant that helps things work in practice.
+ if [[ $LINES ]] && (( LINES - 3 < scrolled )); then
+ hr
+ for (( i=0; i<${#button_map[@]}; i++)); do
+ if (( i % 3 == 2 )); then
+ printf "%s %s\n" ${buttons[i]} ${button_map[i]}
+ else
+ printf "%s %-15s" ${buttons[i]} ${button_map[i]}
+ fi
+ done
+ echo
+ cat <<'EOF'
+y other genres z fg player ' = toggle play
+; previous _ = delete -/+ volume ->/<- skip
+EOF
+ hr
+ scrolled=10
+ fi
+}
+
# tag with beets.
-# usage: beetag QUERY
+# usage: beetag [-r] [-s] QUERY
# it lists the query, reads an input char for tagging one by one.
-# 1-5 = set rating
-# a-x 0 6-9 / . , = set genre/playlist. (available buttons: ` \ ) ] [
-# q = quit
-# y = toggle to setting rare genres
-# z = put the player in the foreground
-# enter = next song
-# ' = toggle playing of songs, also replays current song if hit twice
-# ; = go to previous song
-# _ = delete file, remove from library
-# -/+ = decrease / increase volume
#
# 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
beetag() {
local last_genre_i fstring tag id char new_item char_i genre tag remove doplay i j random
- local do_rare_genres read_wait
- local -a pl_tags buttons button_map ids tags rare_genres tmp_tags
+ local do_rare_genres read_wait help line lsout tmp ls_line
+ local escape_char escaped_input expected_input skip_input_regex
+ local -a pl_tags buttons button_map ids tags rare_genres tmp_tags initial_ls ls_lines
local -A button_i
- local -i volume
+ local -i i j volume scrolled id_count line_int skip_start pre_j_count head_count
+
+ escape_char=$(printf "\u1b")
+ scrolled=999 # more than any $LINES
### begin arg processing ###
random=false
case $1 in
random=true
shift
;;
+ -s)
+ random=false
+ shift
+ ;;
esac
if (( ! $# )); then
echo beetag: error expected a query arg >&2
for (( i=0; i<${#buttons[@]}; i++ )); do
button_i[${buttons[i]}]=$i
done
+
+ # TODO: use shuf --random-source=FILE to save the random sort order
+ # for reusing later, so we can pickup where we left off in a playlist.
+
# shellcheck disable=SC2016 # obvious reason
- beet ls -f '%ifdef{rating,$rating }'"$fstring"', $genre $artist - $album - $title' "$@" | head -n 100 ||:
- # shellcheck disable=SC2016 # obvious reason
- mapfile -t ids < <(beet ls -f '$id' "$@" | { if $random; then sort -R; else cat; fi; } )
- for (( j=0; j<${#ids[@]}; j++ )); do
- hr
+ mapfile -t initial_ls < <(beet ls -f '$id %ifdef{rating,$rating }'"$fstring"'$genre | $artist - $album - $title $length' "$@" | { if $random; then sort -R; else cat; fi; } )
+ j=0
+ # i only care to see the head of the list.
+ head_count=$(( LINES - 14 ))
+ for line in "${initial_ls[@]}"; do
+ id="${line%% *}"
+ ids+=("$id")
+ ls_line="${line#* }"
+ ls_line="$ls_line $id"
+ ls_lines+=("$ls_line")
+ if (( j < head_count )); then
+ echo "$ls_line"
+ fi
+ j=$(( j+1 ))
+ done
+ id_count=${#ids[@]}
+ for (( j=0; j < id_count; j++ )); do
id=${ids[j]}
- # shellcheck disable=SC2016 # obvious reason
- lsout="$(beet ls -f '%ifdef{rating,$rating }'"$fstring"', $genre $id $artist - $album - $title' "id:$id")"
+ lsout="${ls_lines[j]}"
tags=( ${lsout%%,*} )
- printf "%s\n" "$lsout"
- for (( i=0; i<${#button_map[@]}; i++)); do
- if (( i % 3 == 2 )); then
- printf "%s %s\n" ${buttons[i]} ${button_map[i]}
- else
- printf "%s %-15s" ${buttons[i]} ${button_map[i]}
- fi
- done
+ beetag-help
+ printf "██ %s\n" "$lsout"
+ scrolled+=1
if $doplay; then
- beet play --args=--volume=$volume "id:$id" &
+ # https://stackoverflow.com/a/7687716
+ # note: duplicated down below
+ { beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
fi
while true; do
char=
if $doplay; then
ret=0
- read -r -N 1 -s -t $read_wait char || ret=$?
+ 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.
fi
fi
else
- read -r -N 1 -s char
+ read -rsN1 char
fi
+ beetag-help
if [[ $char == $'\n' ]]; then
- kill %% ||: &>/dev/null
+ # https://stackoverflow.com/a/5722874
+ kill %%; wait %% 2>/dev/null ||:
break
fi
case $char in
";")
- kill %% ||: &>/dev/null
+ kill %%; wait %% 2>/dev/null ||:
j=$(( j - 2 ))
break
;;
"'")
if $doplay; then
+ echo "play toggled off"
doplay=false
else
doplay=true
- kill %% ||: &>/dev/null
- beet play --args=--volume=$volume "id:$id" &
+ kill %%; wait %% 2>/dev/null ||:
+ { beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
fi
+ scrolled+=1
continue
;;
_)
- kill %% ||: &>/dev/null
+ kill %%; wait %% 2>/dev/null ||:
m beet rm --delete --force "id:$id"
+ scrolled+=4 # guessing. dont want to test atm
break
;;
[1-5])
- beet modify -y "id:$id" rating=$char
+ beetmq "id:$id" rating=$char
continue
;;
-)
volume=0
fi
echo volume=$volume
+ scrolled+=1
continue
;;
q)
- kill %% ||: &>/dev/null
+ kill %%; wait %% 2>/dev/null ||:
return
;;
+)
volume=130
fi
echo volume=$volume
+ scrolled+=1
continue
;;
y)
continue
;;
z)
+ scrolled+=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
fi
continue
;;
+ "$escape_char")
+ expected_input=true
+ read -rsn2 escaped_input
+ skip_input_regex="^[0-9]+$"
+ skip_back=false
+ case $escaped_input in
+ '[D')
+ # skip backward
+ if (( j == 0 )); then
+ echo "no earlier songs"
+ continue
+ fi
+ skip_back=true
+ {
+ line_int=0
+ for (( i=j-1; i >= 0; i-- )); do
+ echo "$line_int | ${ls_lines[i]}"
+ line_int+=1
+ done
+ } | less -F
+ scrolled+=$j
+ ;;
+ '[C')
+ # skip forward, but show the last few songs anyways.
+ skip_start=0
+ if (( j - 3 > skip_start )); then
+ skip_start=$(( j - 3 ))
+ fi
+ {
+ 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
+ scrolled+=$(( id_count - skip_start - 1 ))
+ ;;
+ *)
+ expected_input=false
+ ;;
+ esac
+ if $expected_input; then
+ read -r skip_input
+ if [[ $skip_input =~ $skip_input_regex ]]; then
+ if $skip_back; then
+ j=$(( j - skip_input - 2 ))
+ else
+ 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
+ kill %%; wait %% 2>/dev/null ||:
+ break
+ fi
+ fi
+ ;;
esac
char_i=${button_i[$char]}
new_item=${button_map[$char_i]}
continue
fi
if (( char_i <= last_genre_i )); then
- m beet modify -y "id:$id" genre=$new_item
+ m beetmq "id:$id" genre=$new_item
else
remove=false
tmp_tags=()
done
if $remove; then
tags=("${tags[@]}")
- m beet modify -y "id:$id" "$new_item!"
+ m beetmq "id:$id" "$new_item!"
else
tags+=("$new_item")
- m beet modify -y "id:$id" $new_item=t
+ m beetmq "id:$id" $new_item=t
fi
fi
done