#!/usr/bin/zsh # # Vacillating Utilitarian eXtemporizer # man vux for details # global options prog_name="vux" prog_ver="0.3.4" vux_dir="$HOME/.vux" vuxrc="$vux_dir/vuxrc" system_vuxrc="/etc/vuxrc" ogg_player="ogg123" ogg_player_options="" mp3_player="mpg321" mp3_player_options="-d oss" min_score=0 save_interval=30 # command line options playlist="$vux_dir/playlist" scorelist="$vux_dir/scorelist" agelist="$vux_dir/agelist" countlist="$vux_dir/countlist" missing_log="$vux_dir/missing" sound_device="/dev/dsp" default_score=50 default_count=10 above_mean_modifier=0 below_mean_modifier=0 max_score=100 increase_factor=10 decrease_factor=10 random_reprieve_factor=10 minimum_age="1h" age_bypass="sqrt" verbose_files=0 check_ratings=1 check_repeats=1 save_scorelist=1 save_agelist=1 save_countlist=1 update_rating_plays=1 update_repeat_plays=1 update_rating_skips=1 update_repeat_skips=1 use_missing=1 vux_action=play rating_method=bell repeat_method=age stats_method=: test_order=test_rating_first decrease_method=conservative_decrease increase_method=conservative_increase play_method=play_song noplay_skip=-1 print_method=print force_new=0 zmodload zsh/mathfunc setopt extendedglob unsetopt bgnice typeset -A rating typeset -A selection_rating typeset -A age typeset -A count typeset -F std_dev_counter typeset -F mean_counter typeset -F 4 mean typeset -F 4 threshold typeset -F 4 reprieve_threshold typeset -F 2 lower_sigma typeset -F 2 upper_sigma typeset -F 4 lower_area typeset -F 4 upper_area typeset -F 4 current_sigma typeset -F 4 probability typeset -F 4 area_sigma typeset -F 4 ratio_sigma typeset -F 4 float_random typeset -F 4 current_chance typeset -F 2 percent_prob typeset -F 4 std_dev_show bell=(0.0000 0.0987 0.1915 0.2734 0.3413 0.3944 0.4332 0.4599 0.4772 0.4878 0.4938 0.4970 0.4987 0.4994 0.4997 0.4999 0.5000) show_stats() { typeset -A rating_counter for i in $rating do rating_counter[$i]=$(( rating_counter[$i] + 1 )) done compute_stats print "score : count" for i in {$max_score..$min_score} do if [[ -n ${(k)rating_counter[$i]} ]] \ { print " "$i" : "$rating_counter[$i] } done print print " mean: "$mean print " standard_deviation: "$std_dev_show if [[ $rating_method == check_thresh ]] then high_song_counter=0 middle_song_counter=0 low_song_counter=0 for i in ${(k)rating_counter} do if [[ $i -ge $threshold ]] then high_song_counter=$(( high_song_counter + rating_counter[$i] )) elif [[ $i -ge $reprieve_threshold ]] then middle_song_counter=$(( middle_song_counter + rating_counter[$i] )) elif [[ $i -lt $reprieve_threshold ]] then low_song_counter=$(( low_song_counter + rating_counter[$i] )) fi done print print " total: "$#rating print " threshold: "$threshold print " reprieve threshold: "$reprieve_threshold print " above threshold: "$high_song_counter print " between thresholds: "$middle_song_counter print "below reprieve threshold: "$low_song_counter elif [[ $rating_method == check_bell ]] then for i in ${(k)rating_counter} do if [[ $i -gt $mean ]] then high_song_counter=$(( high_song_counter + rating_counter[$i] )) elif [[ $i -eq $mean ]] then middle_song_counter=$(( middle_song_counter + rating_counter[$i] )) else [[ $i -lt $mean ]] low_song_counter=$(( low_song_counter + rating_counter[$i] )) fi done print print " total: "$#rating print " above mean: "$high_song_counter print " equal to mean: "$middle_song_counter print " below mean: "$low_song_counter fi } prune_scorelist() { if [[ -r $playlist ]] then typeset -A temp_rating $print_method -n Pruning $scorelist from $playlist... for i in ${(f)"$(cat $playlist)"} do if [[ -n $rating[${(qqq)i}] ]] then temp_rating[${(qqq)i}]=$rating[${(qqq)i}] fi done $print_method "done." $print_method old song count : $#rating $print_method new song count : $#temp_rating : ${(AA)rating::=${(kv)temp_rating}} $write_scorelist_method "$scorelist" "rating" case $repeat_play in (update_age) typeset -A temp_age $print_method -n Pruning $agelist from $playlist... for i in ${(f)"$(cat $playlist)"} do if [[ -n $age[${(qqq)i}] ]] then temp_age[${(qqq)i}]=$age[${(qqq)i}] fi done $print_method "done." $print_method old song count : $#age $print_method new song count : $#temp_age : ${(AA)age::=${(kv)temp_age}} $write_agelist_method "$agelist" "age" ;; (update_count) typeset -A temp_count $print_method -n Pruning $countlist from $playlist... for i in ${(f)"$(cat $playlist)"} do if [[ -n $count[${(qqq)i}] ]] then temp_count[${(qqq)i}]=$count[${(qqq)i}] fi done $print_method "done." $print_method old song count : $#count $print_method new song count : $#temp_count : ${(AA)count::=${(kv)temp_count}} $write_countlist_method "$countlist" "count" ;; esac else print ERROR: $playlist is not readable. fi $stats_method } merge_playlists() { new_songs=0 skipped_songs=0 $print_method Merging $playlist with $scorelist ... for i in ${(f)"$(cat $playlist)"} do if [[ -n $rating[${(qqq)i}] ]] then (( skipped_songs++ )) else rating[${(qqq)i}]=$default_score (( new_songs++ )) fi done $print_method "skipped songs: "$skipped_songs $print_method "new songs: "$new_songs $print_method "total: "$(( skipped_songs + new_songs )) $write_scorelist_method "$scorelist" "rating" $stats_method } generate_new_scorelist() { for i in ${(f)"$(cat $playlist)"}; do rating[${(qqq)i}]=$default_score ; done $write_scorelist_method "$scorelist" "rating" $stats_method } save_file() { lockfile-create --retry 1 $1 || { print Aborting save. ; return } $print_method "Saving $1 ..." temp_file=`tempfile -d $vux_dir` lockfile-touch $1 & lock_pid=$! case $2 in ("rating") for i in ${(k)rating} do print ${(q)${(qqq)${(Q)i}}} $rating[$i] >> $temp_file done ;; ("age") for i in ${(k)age} do print ${(q)${(qqq)${(Q)i}}} $age[$i] >> $temp_file done ;; ("count") for i in ${(k)count} do print ${(q)${(qqq)${(Q)i}}} $count[$i] >> $temp_file done ;; (*) print "VUX: internal error. Unknown argument: $2" ;; esac [[ -e $1 ]] && cp $verbose_arg $1 $1.bak cp $verbose_arg $temp_file $1 rm $verbose_arg $temp_file kill $lock_pid lockfile-remove $1 $print_method 'done.' } read_file() { $print_method -n "Reading $1 ..." lockfile-create --retry 1 $1 || { print Aborting load. ; exit 1 } lockfile-touch $1 & lock_pid=$! case $2 in ("rating") : ${(AA)rating:=${(zf)"$(<$1)"}} ;; ("age") : ${(AA)age:=${(zf)"$(<$1)"}} ;; ("count") : ${(AA)count:=${(zf)"$(<$1)"}} ;; esac kill $lock_pid lockfile-remove $1 $print_method 'done.' } compute_stats() { mean_counter=0 std_dev_counter=0 for i in $rating do mean_counter=$(( mean_counter + i )) done # bugtest mean=$(( mean_counter / $#rating )) #pre_mean=$#rating #mean=$(( mean_counter / pre_mean )) for i in $rating do std_dev_counter=$(( std_dev_counter + ( ( i - mean ) ** 2 ) )) done std_dev=$(( sqrt( (( std_dev_counter / $#rating )) ) )) std_dev_show=$std_dev threshold=$(( std_dev + mean + $above_mean_modifier )) reprieve_threshold=$(( mean - std_dev + $below_mean_modifier )) } check_age() { aged_song=0 new_song=0 current_seconds=`print -P %D{%s}` if [[ ! -n $age[$current_pick] ]] then current_age=$current_seconds new_song=1 [[ $force_new == 1 ]] && $print_method -n "\-" else pre_age=$age[$current_pick] current_age=$(( current_seconds - pre_age )) fi if [[ $current_age -ge $minimum_age ]] then aged_song=1 elif [[ $age_bypass_count -eq 0 ]] then $print_method -n "~" aged_song=1 else (( age_bypass_count-- )) $print_method -n ":" fi } check_count() { aged_song=0 if [[ ! -n $count[$current_pick] ]] then aged_song=1 else pre_count=$count[$current_pick] count[$current_pick]=$(( pre_count - 1 )) if [[ $count[$current_pick] -le 0 ]] { unset "count[$current_pick]" } $print_method -n ":" fi } check_bell() { good_song=0 current_chance=$(( RANDOM % 10000 )) float_random=$(( current_chance /10000 )) if [[ $std_dev -eq 0 ]] then current_sigma=0 else current_sigma=$(( (current_rating - mean)/std_dev )) fi if [[ $current_sigma -lt 0 ]] then current_sigma=$(( current_sigma * -1 )) neg_sigma=1 else neg_sigma=0 fi if [[ $current_sigma -gt 4 ]] then area_sigma=0.5000 else lower_sigma=$(( int(current_sigma*4)/4.0 )) upper_sigma=$(( int(current_sigma*4)/4.0+0.25 )) lower_area=$bell[int(current_sigma*4)+1] upper_area=$bell[int(current_sigma*4)+2] ratio_sigma=$(( (upper_sigma - current_sigma) / \ (upper_sigma - lower_sigma) )) area_sigma=$(( lower_area * ratio_sigma + \ upper_area * (1-ratio_sigma) )) fi if [[ neg_sigma -eq 1 ]] then probability=$(( 0.5 - area_sigma - below_mean_modifier / 100.0 )) else probability=$(( 0.5 + area_sigma + above_mean_modifier / 100.0 )) fi if [[ $probability -ge $float_random ]] then good_song=1 $print_method -n "+" percent_prob=$(( probability * 100 )) else $print_method -n "." fi } check_threshold() { good_song=0 if [[ $current_rating -ge $threshold ]] then good_song=1 $print_method -n "+" elif [[ $current_rating -ge $reprieve_threshold ]] then if [[ $(( RANDOM % $random_reprieve_factor )) -eq 0 ]] then $print_method -n "!" good_song=1 else $print_method -n "." fi else $print_method -n "x" fi } dummy_repeat() { aged_song=1 } dummy_rating() { good_song=1 ; percent_prob=100 } test_rating_first() { $rating_method [[ $good_song == 1 ]] && $repeat_method } test_repeat_first() { $repeat_method if [[ $force_new == 1 && $new_song == 1 ]] then dummy_rating else [[ $aged_song == 1 ]] && $rating_method fi } find_all() { current_number=$(( RANDOM % $#rating + 1 )) current_pick=${${(k)rating}[$current_number]} current_rating=$rating[$current_pick] } find_selection() { current_number=$(( RANDOM % $#selection_rating + 1 )) current_pick=${${(k)selection_rating}[$current_number]} current_rating=$rating[$current_pick] } find_song() { good_song=0 aged_song=0 new_song=0 age_bypass_count=$age_bypass compute_stats $print_method while [[ $good_song == 0 || $aged_song == 0 ]] { $find_method $test_order } $print_method $print_rating_status $print_repeat_status $print_method } get_age_clock() { if [[ ! -n $age[$current_pick] ]] then age_clock="-" time_units="" else age_clock=$(( current_age / age_format )) time_units=$time_display fi } status_bell() { $print_method -n \ $rating[$current_pick]"/"$percent_prob"%/"$mean"/"$std_dev_show } status_thresh() { $print_method -n \ $rating[$current_pick]"/"$threshold"/"$mean"/"$reprieve_threshold } status_age() { get_age_clock $print_method -n "/"$age_clock$time_units } update_count() { count[$current_pick]=$default_count } update_age() { age[$current_pick]=`print -P %D{%s}` } update_rating() { pre_rating=$rating[$current_pick] rating[$current_pick]=$(( pre_rating + rating_delta )) if [[ $rating[$current_pick] -gt $max_score ]] \ { rating[$current_pick]=$max_score } if [[ $rating[$current_pick] -lt $min_score ]] \ { rating[$current_pick]=$min_score } $print_method $print_method "NEW RATING: "$rating[$current_pick] } conservative_decrease() { rating_delta=$(( int(current_rating / -decrease_factor) )) } conservative_increase() { rating_delta=$(( int( (max_score - current_rating ) / increase_factor ) )) } accelerated_decrease() { x=$(( current_rating - min_score )) y=$(( max_score - current_rating )) z=$(( x < y ? int(x/2) : int(y/2) )) rating_delta=$(( z > 1 ? -z : -1 )) } accelerated_increase() { x=$(( current_rating - min_score )) y=$(( max_score - current_rating )) z=$(( x < y ? int(x/2) : int(y/2) )) rating_delta=$(( z > 1 ? z : 1 )) } dummy_play() { was_skipped=$noplay_skip [[ $verbose_files == 1 ]] && $print_method $current_song } play_song() { case ${(L)current_song} in (*.mp3) player=$mp3_player player_options=$mp3_player_options print $current_song > $vux_dir/current_path print ${current_song:t} > $vux_dir/current_song ;; (*.ogg) player=$ogg_player player_options=$ogg_player_options print $current_song > $vux_dir/current_path print ${current_song:t} > $vux_dir/current_song ;; (*) $print_method ERROR: unrecognized format: $print_method $current_song $print_method was_skipped=1 ;; esac while ! : >$sound_device do $print_method Waiting... sleep 1 done if [[ ${(L)current_song} == http* || -r $current_song ]] then $player $player_options $current_song & player_pid=$! wait $player_pid && was_skipped=-1 unset player_pid else $missing_method $current_song >> $missing_log fi } playloop() { print $$ >> $vux_dir/vux.pid set player trap 'int_trap' INT trap 'hup_trap' HUP save_counter=0 while : do set player_pid set current_rating set current_pick find_song current_song=${(Q)current_pick} was_skipped=0 $play_method case $was_skipped in (1) $decrease_method $rating_skip $repeat_skip ;; (-1) $increase_method $rating_play $repeat_play ;; (0) $print_method ERROR PLAYING: $current_song ;; esac (( save_counter++ )) if [[ $save_counter -ge $save_interval ]] then $write_scorelist_method "$scorelist" "rating" $write_agelist_method "$agelist" "age" $write_countlist_method "$countlist" "count" save_counter=0 fi done } int_trap() { kill $player_pid $write_scorelist_method "$scorelist" "rating" $write_agelist_method "$agelist" "age" $write_countlist_method "$countlist" "count" $stats_method rm $verbose_arg $vux_dir/vux.pid exit 0 } hup_trap() { was_skipped=1 kill $player_pid } usage="usage: $prog_name [options] options: -x p|g|m|w|r Action: play|generate|merge|weed|ratings -s file Use file as scorelist -S Disable saving scorelist -a file Use file as agelist -A Disable saving agelist -z file Use file as countlist -Z Disable saving countlist -y file Use file as missing log -Y Disable missing log -p file Use file as playlist -c Disable rating check -w b|t Ratings: bell|thresh -d Disable rating updates on play -W a|c Repeats: age|count -l Disable rating updates on skip -G t|s|n Use age bypass method -j Disable repeat check -e pattern Use only matching songs -b Disable repeat updates on play -O device Check sound device -k Disable repeat updates on skip -M val Use minimum age val -n Disable player -t n Add n% to >=mean -J Accelerate decreases -T n Subtract n% from