play6/play6
2024-12-20 18:44:16 +05:00

348 lines
8.8 KiB
Bash
Executable file

#!/bin/sh
#
# The Playlist Jonkler
#
# usage: play6 <command> <playlist file>
# commands: tell: list files in playlist
# tell_w: list playlist entry weights
# tell_l: list playlist entry lengths
# gen: output shuffled playlist
# play: play music with mpv
#
# playlist file is shell commands.
# you shall run the `collect` command inside the playlist file.
#
# collect <weight> <file> [...]
#
# for example:
#
# #!/usr/bin/env -S play6 play
# collect \
# 100 epic_music.mp3 \
# 50 less_epic_music.webm
#
# if the playlist file does anything else, including, but not limited to:
#
# - executing commands that are not `collect`
# - setting any variable
# - referencing any variable
#
# the behavior is undefined.
#
# the program will assign a probability to each entry.
# the probability is proportional to the entry's weight and
# inversely proportional to the file's duration.
#
# the program will generate a shuffled playlist like this:
#
# 1. pick a random entry
# 2. if it's 1th last entry output, return to #1 with 100% probability
# (unless there is only one entry in the playlist)
# 3. if it's 2th last entry output, return to #1 with 91% probability
# 4. if it's 3th last entry output, return to #1 with 70% probability
# 5. output filename of chosen entry
#
# dependency : mpv (only for `play music with mpv`)
# ffprobe
# POSIX.1-2017 compatible system
# /dev/urandom or /dev/random
#
# this program is godawful and if it explodes
# its your fault for being foolish enough to use it
#
mktfifo() {
{ mkfifo "$(printf "$(test -z "$TMPDIR" && printf '/tmp' ||
printf "%s" "$TMPDIR")/pipe.$(rand).$(rand)$1" | tee /dev/fd/3)"; } 3>&1
}
dyn_set() { eval "$(printf '_dyn__fn() { %s="$1"; }\n' "$1")";
_dyn__fn "$2"; }
dyn_get() { eval "$(printf \
'_dyn__fn() { test ! -z "${%s+x}" && %s="$%s"; }\n' "$2" "$1" "$2")";
_dyn__fn; }
dyn_unset() { eval "$(printf '_dyn__fn() { unset %s; }\n' "$1")";
_dyn__fn; }
_list_id=0
list_mk() {
_list_id=$((_list_id+1))
_list_mk_id="_arr_$_list_id"
dyn_set "$1" "$_list_mk_id"
shift 1
dyn_set ${_list_mk_id}_len $#
_list_mk_i=0
for _list_mk_arg; do
_list_mk_i=$((_list_mk_i+1))
dyn_set ${_list_mk_id}_${_list_mk_i} "$_list_mk_arg"
done
}
list_unmk() {
_list_unmk_i=0
dyn_get _list_unmk_id $1 || return
dyn_get _list_unmk_len ${_list_unmk_id}_len || return
while [ $_list_unmk_i != $_list_unmk_len ]; do
_list_unmk_i=$((_list_unmk_i+1))
dyn_unset ${_list_unmk_id}_${list_unmk_i}
done
dyn_unset ${_list_unmk_id}_len
}
list_get() {
dyn_get _list_get_id "$1" || return
[ "$2" -eq "$2" ] || return
dyn_get "$3" "${_list_get_id}_$2"
}
list_len() {
dyn_get _list_len_id $1 || return
dyn_get _list_len_len ${_list_len_id}_len || return
printf '%s\n' $_list_len_len
}
list_resize() {
dyn_get _list_resize_id $1 || return
[ "$2" -eq "$2" ] || return
[ $2 -ge 0 ] || return
dyn_get _list_resize_len ${_list_resize_id}_len || return
_list_resize_i=$_list_resize_len
while [ "$_list_resize_i" -gt "$2" ]; do
dyn_unset ${_list_resize_id}_${_list_resize_i}
_list_resize_i=$((_list_resize_i-1))
done
_list_resize_i=$((_list_resize_len+1))
while [ "$_list_resize_i" -le "$2" ]; do
dyn_set ${_list_resize_id}_${_list_resize_i} ""
_list_resize_i=$((_list_resize_i+1))
done
dyn_set ${_list_resize_id}_len "$2"
}
list_set() {
dyn_get _list_set_id "$1" || return
[ "$2" -eq "$2" ] || return
dyn_get _list_set_len ${_list_set_id}_len || return
if [ "$2" -gt "$_list_set_len" ]; then
list_resize "$1" "$2" || return
fi
dyn_set "${_list_set_id}_$2" "$3"
}
list_push() {
list_resize "$1" "$(("$(list_len $1)" +1))" || return
list_set "$1" "$(list_len $1)" "$2"
}
list_pop() {
list_get "$1" "$(list_len $1)" "$2" || return
list_resize "$1" "$(($(list_len $1)-1))"
}
collect() {
_collect_fifo_lens="$(mktfifo)"
_collect_gen() {
i=0;
{ while :; do
{ ! test -z ${1+x}; } || break
{ ! test -z ${2+x}; } || return
_name="$2"
_weight="$1"
shift 2
i=$((i+1)); (
printf "%d %.0f\n" $i \
"$(if test -e "$_name"
then
printf 'info: probe "%s"\n' "$_name" >&2
ffprobe -loglevel 8 -of flat -show_entries format=duration "$_name" |
sed 's/^[^"]*"\([^"]*\)"$/\1/'
printf 'info: DONE probe "%s"\n' "$_name" >&2
else
printf 'warn: "%s": file not found\n' "$_name" >&2
echo -1
fi)" ) &
done; wait; } | sort -n | sed 's/^[0-9]* *//'
}
_collect_gen "$@" > "$_collect_fifo_lens" &
list_mk list
while true; do
{ ! test -z ${1+x}; } || break
{ ! test -z ${2+x}; } || exit
read -r _collect_len
list_mk _collect_entry "$2" "$1" "$_collect_len"
list_push list "$_collect_entry"
test -z "$_collect_lpi" && { _collect_lpi=1; printf 'info: collecting' >&2; }
printf '.' >&2
shift 2
done < "$_collect_fifo_lens"
printf '\n' >&2
rm "$_collect_fifo_lens"
if ! test "$(list_len list)" -gt 0; then
printf 'fatal: no music\n' >&2
exit 1
fi
for i in $(seq 1 $(list_len list) ); do
list_get list $i entry
list_get entry 1 name
list_get entry 2 weight
list_get entry 3 len
printf 'info: (%s): %sx %ss\n' "$name" "$weight" "$len" >&2;
done
}
if test -e /dev/urandom; then
rand() { echo $(head -c4 /dev/urandom | od -An -t u); }
elif test -e /dev/random; then
rand() { echo $(head -c4 /dev/random | od -An -t u); }
else
printf 'fatal: no random\n' >&2;
exit 1
fi
randn() {
(while true; do
if test -z $1; then printf 'randn: bad argument\n' >&2; exit 1; fi
_rand=$(rand || exit)
if ! [ $_rand -lt $(((1<<32) % $1)) ]; then
break;
fi
done
printf $(($_rand % $1)))
}
gen() (
_len="$(list_len list)"
echo count: $_len >&2
_get_el() {
list_get list "$1" _entry || return
list_get _entry 3 "$2"
}
_get_el 1 _maxll || return
for i in $(seq 2 $(list_len list)); do
_get_el $i _mll
if [ "$_mll" -gt "$_maxll" ]; then
_maxll="$_mll"
fi
done
echo maxlen: $_maxll >&2
__wei=0
for i in $(seq 1 $(list_len list)); do
list_get list $i _entry
list_get _entry 2 _vwei
list_get _entry 3 _vlen
__prevwei=$__wei
__wei=$((__wei+(((10000 * _maxll) / _vlen) * _vwei)))
list_set _entry 4 $__wei
done
maxwei=$__wei
set -- "" "" ""
_binget() {
list_get "$1" "$2" _binget_
list_get _binget_ 4 "$3"
}
_bini() (
t="$1"; v="$2";
i="${3:-1}"; j="${4:-$(list_len t)}"
bf="$4"
mid=$((i+(j-i)/2))
_binget t $mid _vmid || return
if { test -z $bf || { _binget t $bf _vbf || return; test \
"$_vbf" -ge "$_vmid"; }; } && test "$_vmid" -ge "$v"
then
bf=$mid
fi
if test $i = $j; then
echo $bf; test ! -z $bf; return
fi
if test $_vmid -ge $v; then
if test $mid -eq $i; then
echo $bf; test ! -z $bf; return
fi
_bini "$t" $v $i $((mid-1)) $bf
else
if test $mid -eq $j; then
echo $bf; test ! -z $bf; return
fi
_bini "$t" $v $((mid+1)) $j $bf
fi
)
while true; do
num=$(randn $maxwei || exit)
i=$(_bini "$list" $num) || exit
if { test "$i" != "$3" || test $_len = 1; } &&
{ test "$i" != "$2" || test $(randn 100) -lt 9; } &&
{ test "$i" != "$1" || test $(randn 100) -lt 30; }
then
list_get list $i _entry
list_get _entry 1 _name
dd bs=1 count=1 >/dev/null 2>/dev/null || exit
printf "%d\n%s\n" "$(printf '%s' "$_name" | wc -c)" "$_name" || exit
shift 1
set -- "$@" "$i"
fi
done
)
gen2() {
gen | while read len; do
dd bs=$len count=1 2>/dev/null; printf '\0'
dd bs=1 count=1 2>/dev/null >/dev/null
done
}
play() {
_fifo_playl="$(mktfifo .m3u8)"
_fifo_playctl="$(mktfifo .ctl)"
_fifo_script="$(mktfifo .lua)"
{ cat <<'EOF'
local f = assert(io.open("/dev/fd/4"))
local ctl = assert(io.open("/dev/fd/5",'w'))
function spawn()
assert(ctl:write('.')) assert(ctl:flush())
local sngi = assert(tonumber(f:read()))
local song = f:read(sngi)
f:read()
print("PLAYING: "..song)
mp.commandv("loadfile",song,"replace")
mp.set_property_bool("pause", false)
end
mp.observe_property("eof-reached", "bool", function(name, value)
if value then
spawn()
end
end)
spawn()
EOF
} >"$_fifo_script" &
(gen >"$_fifo_playl" <"$_fifo_playctl") &
curpid=$$
clrn(){
rm "$_fifo_playl"
rm "$_fifo_script"
rm "$_fifo_playctl"
}
( while kill -0 $curpid 2>/dev/null; do sleep 0.05; done; clrn ) &
job="$(jobs -p | tail -n1)"
{
mpv --keep-open --no-video \
--volume=69 --script="$_fifo_script" --idle
} 4<"$_fifo_playl" 5>"$_fifo_playctl"
kill $job;
clrn
}
for _i in "tell 1" "tell_w 2" "tell_l 3"; do
_inset() { _fn=$1; _j=$2; }
_inset $_i
eval "$( (cat <<EOF
$_fn() {
for i in \$(seq 1 \$(list_len list)); do
list_get list \$i _entry
list_get _entry $_j _val
printf '%s\0' "\$_val";
done
}
EOF
) | tee /dev/stderr)"
done
. "$2" || exit
if test -z "$list"; then
printf 'fatal: bad playlist\n' >&2
exit 1
fi
if test "$1" = tell; then tell;
elif test "$1" = tell_w; then tell_w;
elif test "$1" = tell_l; then tell_l;
elif test "$1" = gen; then gen2;
elif test "$1" = play; then play;
else printf "play6: invalid argument\n" >&2; exit 1
fi