# beet playlist. use beetag with a playlist name
bpl() {
- eval beetag -r "$@" "${bpla[${@: -1}]}"
+ local playlist playlist_regex
+ playlist="${*: -1}"
+ playlist_regex='[a-z0-9_]'
+ if [[ ! $playlist =~ $playlist_regex ]]; then
+ echo "bpl: error unexpected chars in playlist: $playlist"
+ return 1
+ fi
+ # all but last arg as options
+ eval beetag -r "${*:1:$# - 1}" "${bpla[$playlist]}"
}
complete -W "${!bpla[*]}" bpl
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
+ beetag-nostatus 1
+}
+
+kill-bg-quiet() {
+ # https://stackoverflow.com/a/5722874
+ kill %% 2>/dev/null ||:; wait %% 2>/dev/null ||:
+}
+
+# debug variables
+dv() {
+ for arg; do
+ printf "%s=%s " "$arg" "${!arg}"
+ done
+ echo
}
# Must be called from beetag for variables to be setup
beetag-help() {
+ 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_map[@]}; i++)); do
- if (( i % 3 == 2 )); then
- printf "%s %s\n" ${buttons[i]} ${button_map[i]}
+ 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[i]} ${button_map[i]}
+ printf "%s %-15s" ${buttons[j]} ${button_map[j]}
fi
done
- echo
cat <<'EOF'
-y other genres z fg player ' = toggle play
-; previous _ = delete -/+ volume ->/<- skip
+
+
+y other genres z fg player ' = toggle play 1-5 rate
+; 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
+ 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 ||:
+}
+
# tag with beets.
# usage: beetag [-r] [-s] QUERY
# it lists the query, reads an input char for tagging one by one.
# 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 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 help line lsout tmp ls_line skip_lookback
- 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 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
+ local new_random pl_seed_path seed_num seed_file
+ local -a pl_tags 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
-
+ erasable_line=false
escape_char=$(printf "\u1b")
scrolled=999 # more than any $LINES
### begin arg processing ###
random=false
+ new_random=false
case $1 in
-r)
random=true
random=false
shift
;;
+ -x)
+ new_random=true
+ shift
+ ;;
esac
if (( ! $# )); then
echo beetag: error expected a query arg >&2
volume=70
read_wait=2
doplay=true
- # because we were destined to run out of single key buttons.
- rare_genres=(
- jazz
- musical
- noise
- skit
- spoken-w
- )
last_genre_i=$(( ${#common_genres[@]} - 1 ))
- buttons=( {a..p} {r..w} 0 {6..9} , . / )
+ buttons=( {a..p} {r..w} {6..8} , . / - "=")
button_map=(${common_genres[@]} ${pl_tags[@]})
fstring=
for tag in "${pl_tags[@]}"; 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.
+ # 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=/i/info/pl-state
+ if [[ $playlist ]]; then
+ pl_state_dir=$pl_state_dir/nopl
+ else
+ pl_state_dir=$pl_state_dir/$playlist
+ 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 # obvious reason
- mapfile -t initial_ls < <(beet ls -f '%ifdef{rating,$rating }'"$fstring"'$genre | $artist - $album - $title $length $id' "$@" | { if $random; then sort -R; else cat; fi; } )
- j=0
- # i only care to see the head of the list.
- head_count=$(( LINES - 20 ))
+ mapfile -t initial_ls < <(beet ls -f '%ifdef{rating,$rating }'"$fstring"'$genre | $artist - $album - $title $length $id PijokVipiotOzeph $path' "$@" | { if $random; then sort -R --random-source=$pl_seed_path; else cat; fi; } )
+ id_count=${#initial_ls[@]}
for line in "${initial_ls[@]}"; do
- id="${line##* }"
+ 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%% |*}"
- ls_line="$(printf %-11s "$right_pad")${line#$right_pad}"
+ right_pad="${line_no_path%% |*}"
+ ls_line="$(printf %-11s "$right_pad")${line_no_path#"$right_pad"}"
ls_lines+=("$ls_line")
- if (( j < head_count )); then
+ 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
- j=$(( j+1 ))
done
- id_count=${#ids[@]}
- for (( j=0; j < id_count; j++ )); do
+
+ while true; do
id=${ids[j]}
+ path="${paths[$j]}"
lsout="${ls_lines[j]}"
tags=( ${lsout%%,*} )
beetag-help
printf "██ %s\n" "$lsout"
- scrolled+=1
+ beetag-nostatus 1
if $doplay; then
# https://stackoverflow.com/a/7687716
# note: duplicated down below
- { beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
+ # 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
+ erasable_line=false
fi
while true; do
char=
# Automatically skip to the next song if this one ends, unless
# we turn off the autoplay.
if (( ret == 142 )) || [[ ! $char ]]; then
- if bg %% &>/dev/null; then
+ if jobs -p | grep -q . &>/dev/null; then
continue
else
break
beetag-help
if [[ $char == $'\n' ]]; then
# https://stackoverflow.com/a/5722874
- kill %%; wait %% 2>/dev/null ||:
+ kill-bg-quiet
break
fi
case $char in
";")
- kill %%; wait %% 2>/dev/null ||:
+ kill-bg-quiet
j=$(( j - 2 ))
break
;;
doplay=false
else
doplay=true
- kill %%; wait %% 2>/dev/null ||:
- { beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
+ kill-bg-quiet
+ { mpv --profile=a --volume=$volume "$path" 2>&1 & } 2>/dev/null
+ #{ beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
+ erasable_line=false
fi
- scrolled+=1
+ beetag-nostatus 1
continue
;;
_)
- kill %%; wait %% 2>/dev/null ||:
+ kill-bg-quiet
m beet rm --delete --force "id:$id"
- scrolled+=4 # guessing. dont want to test atm
+ 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
- echo volume=$volume
- scrolled+=1
- continue
- ;;
- q)
- kill %%; wait %% 2>/dev/null ||:
- return
- ;;
- +)
+ ;;&
+ 0)
volume+=5
if (( volume > 130 )); then
volume=130
fi
+ ;;&
+ 0|9)
+ mpvrpc '{ "command": ["set_property", "volume", '$volume'] }'
+ beetag-status
echo volume=$volume
- scrolled+=1
continue
;;
+ q)
+ kill-bg-quiet
+ return
+ ;;
y)
if $do_rare_genres; then
do_rare_genres=false
continue
;;
z)
- scrolled+=3
+ 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
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
- '[D')
- skip_lookback=5
+ # up char
+ '[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
line_int+=1
done
} | less -F
- scrolled+=$(( id_count - skip_start - 1 ))
;;
- '[C')
+ # 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++ )); do
+ 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 ))
echo "$line_int | $ls_line"
line_int+=1
done
- scrolled+=$(( id_count - skip_start - 1 ))
+ ;;
+ # 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
+ case $skip_input in
q)
- kill %%; wait %% 2>/dev/null ||:
+ kill-bg-quiet
return
;;
esac
j=$(( j - 1 ))
fi
fi
- kill %%; wait %% 2>/dev/null ||:
+ kill-bg-quiet
break
fi
;;
fi
fi
done
+ if (( j < id_count - 1 )); then
+ j+=1
+ else
+ j=0
+ fi
+ if [[ $playlist ]]; then
+ echo $j >$pl_state_path
+ fi
done
}