: ########################################################################## # Shellscript: playrand - play random file # Author : Heiner Steven # Date : 2001-04-09 # Requires : [busy], mpg123, mp3info, shuffle, [stdbuf], sum, [xtitle] # Category : Music # SCCS-Id. : @(#) playrand 2.10 18/05/16 ########################################################################## # Description # o "playrand Cranberries" only plays songs having # "Cranberries" in path name # o Maintains multiple play lists for different song directories # # Notes # o Directories with whitespace characters in the MP3PATH are not # handled correctly # # ToDo # o The creation of the main file list should run in the background # o list_proctree() should be able to use different "ps" variants ########################################################################## PN=`basename "$0"` # Program name VER='2.10' : ${MP3PATH:=/usr/local/share/mp3} : ${PLAYLISTDIR:=$HOME/.$PN} # Set the following variables to disable the automatic search for optional # programs. #: ${PROGRESSINDICATOR:=busy} #: ${LINEBUFFER:=stdbuf} set -u ############################################################################### # searchprog - search program using search PATH # usage: searchprog program ############################################################################### searchprog () { _search=$1; shift for _dir in `echo "$PATH" | sed "s/^:/.:/;s/:\$/:./;s/:/ /g"` do [ -x "$_dir/$_search" ] || continue echo "$_dir/$_search" return 0 done return 1 } ############################################################################### : ${LINEBUFFER:=`searchprog stdbuf`} : ${LINEBUFFEROPTS=-o L} : ${PROGRESSINDICATOR:=`searchprog busy`} # MP3 player player=mpg123 playerflags= progressindicator=$PROGRESSINDICATOR maxlistcnt=10 findopts="-L" findargs="-type f -follow -name '*.mp*'" usage () { echo >&2 "$PN - play random MP3 files, $VER usage: $PN [-Fl] [-d dirs] [-f playlist] [-m idtag=value [...]] [pattern ...] -d: directories with MP3 files (default: MP3PATH=$MP3PATH) -f: name of file with MP3 songs to play -F: force re-creation of playlist -l: list matching mp3 path names (do not play them) -m: MP3 ID tag criteria, e.g. \"genre=pop\" Example: $PN -m genre=pop -m year=200[0-3]" exit 1 } msg () { echo >&2 "$PN: $*"; } fatal () { msg "$@"; exit 1; } # string2hash - create a unique "hash" value for a string string2hash () { echo "$@" | sum | sed 's/[^0-9][^0-9]*/_/g' } ############################################################################### # Return all descendant processes (child, grand-child, ..) given a parent # process id. ############################################################################### list_proctree () { [ $# -eq 1 ] || fatal "usage: list_proctree pid" ps -e -o pid,ppid | # Output example: # PID PPID # 2500 2388 # 23209 2500 # 23220 23209 # 23222 23220 # 19904 2500 awk ' BEGIN { ppid = '"$1"' } NF >= 2 { if ( childof[$2] != "" ) { childof[$2] = childof[$2] " " } childof[$2] = childof[$2] $1 } END { if ( ppid in childof ) { # Create a list of descendant processes descendants = "" # Result list parent_cnt = 0 # Length of working list parents[++parent_cnt] = ppid while ( parent_cnt > 0 ) { ppid = parents[parent_cnt--] if ( childof[ppid] != "" ) { if ( descendants != "" ) { descendants = descendants " " } descendants = descendants childof[ppid] } # Check for grand-children n = split(childof[ppid], children) for ( i=1; i<=n; ++i ) { parents[parent_cnt+i] = children[i] } parent_cnt = parent_count+n } print descendants } } ' } ############################################################################### # addfilter - match files by ID3 tags # # Convert names line "genre=" to a mp3info(1) format string returning the # corresponding ID3 tag information. ############################################################################### addfilter () { for expr do case "$expr" in file=*) fmt="%f";; path=*) fmt="%F";; size=*) fmt="%k";; artist=*) fmt="%a";; comment=*) fmt="%c";; genre=*) fmt="%g";; genrenumber=*) fmt="%G";; album=*) fmt="%l";; tracknumber=*) fmt="%n";; title=*) fmt="%t";; year=*) fmt="%y";; stereo=*) fmt="%o";; bitrate=*) fmt="%r";; frequency=*) fmt="%Q";; *) fatal "invalid filter expression: $expr" esac value=`echo "$expr" | cut -d= -f2-` case "$value" in *%*) fatal "invalid character: %" esac fmtstring=${fmtstring:+$fmtstring/}$fmt matchstring=${matchstring:+$matchstring/}$value done #echo >&2 "DEBUG: fmtstring=<$fmtstring>" #echo >&2 "DEBUG: matchstring=<$matchstring>" } progress_indicator_start () { if [ -n "$progressindicator" ] then $progressindicator & busypid=$! fi } progress_indicator_stop () { if [ -n "$progressindicator" ] then if [ -n "$busypid" ] then kill $busypid >/dev/null 2>&1 wait $busypid >/dev/null 2>&1 busypid= fi fi } # unbuffer - ensure lines are printed immediately unbuffer () { if [ -n "$LINEBUFFER" ] then $LINEBUFFER $LINEBUFFEROPTS "$@" else cat "$@" fi } ########################################################################## Mp3Path= Playlist= ForceCreate=false ListOnly=false while [ $# -gt 0 ] do case "$1" in -d) Mp3Path=$2; shift;; -F) ForceCreate=true;; -f) Playlist=$2; shift;; -l) ListOnly=true;; -m) addfilter "$2"; shift;; --) shift; break;; -h) usage;; -*) usage;; *) break;; # First file name esac shift done [ $# -lt 1 ] && set -- '.*' # Silently create directory for the playlists [ -d "$PLAYLISTDIR" ] || mkdir -p "$PLAYLISTDIR" || fatal "cannot create directory: $PLAYLISTDIR" if [ -n "$DISPLAY$WINDOWID" ] then isxwindows=: # true oldtitle=`xtitle -g w` else isxwindows=false fi ########################################################################## if [ -z "$Playlist" ] then # The user either has to specify directory(s) containing MP3 files, # or set the MP3PATH environment variable. searchpath=`echo "${Mp3Path:-$MP3PATH}" | tr : ' '` [ -n "$searchpath" ] || fatal "specify MP3 search path either by using -d or by setting the MP3PATH environment variable" hashval=`string2hash "$searchpath"` [ -n "$hashval" ] || fatal "cannot create unique id: $searchpath" Playlist=$PLAYLISTDIR/$hashval if [ -s "$Playlist" ] && [ $ForceCreate = false ] then msg "using existing playlist: $Playlist" else msg "Song directory(s): $searchpath" \ "creating playlist $Playlist; please be patient" progress_indicator_start # We do not want partial playlists: trap 'rm -f "$Playlist" >/dev/null 2>&1' 1 2 3 13 15 eval find $findopts $searchpath $findargs -print > "$Playlist" || fatal "could not create file list: $Playlist" trap 1 2 3 13 15 progress_indicator_stop fi if [ -s "$Playlist" ] then msg "list contains" `wc -l < "$Playlist"` "audio files" else fatal "found no songs: $searchpath" fi fi #echo >&2 "DEBUG: Playlist=<$Playlist>" #ls -ld "$Playlist" >&2 [ -s "$Playlist" ] || fatal "play list does not exist or is empty: $Playlist" ########################################################################## # Pre-process our playlist if the user specified mp3 filters based # on ID tags. : ${filterpid:=} : ${matchstring:=} if [ -n "$matchstring" ] then hashval=`string2hash ":$matchstring:$MP3PATH"` newlist=$PLAYLISTDIR/$hashval if [ -s "$newlist" ] && [ $ForceCreate = false ] then msg "MP3 selection criteria playlist: $newlist" msg "INFO: list contains" `wc -l < "$newlist"` "files" Playlist=$newlist else msg "preparing playlist for MP3 selection criteria." \ "(list will be saved for faster startup)" # Run the filter generator in the background for faster startup. ( #msg "DEBUG: start creating filter" interrupted=false #trap 'set -x; echo >&2 filter: SIGNAL # progress_indicator_stop # rm -f "$newlist" >/dev/null 2>&1' 0 #trap "set -x; echo >&2 filter: SIGTERM; interrupted=true; exit 2" 15 trap "echo >&2 filter: SIGNAL; interrupted=true" 15 trap '' 1 2 3 progress_indicator_start while read path do [ "$interrupted" = true ] && break unbuffer mp3info -p "$fmtstring%%%F\n" "$path" 2>/dev/null done < "$Playlist" | unbuffer egrep -i "^$matchstring%" | unbuffer cut -d% -f2- > "$newlist" progress_indicator_stop if [ -s "$newlist" ] && [ "$interrupted" = "false" ] then lines=`wc -l < "$newlist"` error=0 state=finished else rm -f "$newlist" error=1 state=aborted fi msg "INFO: $state creating filter, ${lines:-0} lines." trap 1 2 3 15 exit $error ) & filterpid=$! # Special handling: wait until the play list created in the # backround contains at least one matching song before # continuing. # Terminate filtering process in case of errors (or user interrupts), # quit the program afterwards. : ${interrupted:=} trap "echo >&2 SIGNAL waiter; interrupted=true" 1 2 3 15 printinfo=true # Print info message at most one time. until [ -s "$newlist" ] && egrep -l -i "$*" "$newlist" >/dev/null do #msg "DEBUG: checking if filter is still running" if [ "$interrupted" = true ] then kill $filterpid `list_proctree $filterpid` >/dev/null 2>&1 fatal "aborted" fi if kill -0 $filterpid >/dev/null 2>&1 then if [ "$printinfo" = true ] then msg "INFO: waiting for the first matching entry..." printinfo=false fi #sleep 1 sleep 5 else # Process has finished. # Note that the result list may still be empty. wait $filterpid >/dev/null 2>&1 || fatal "ERROR: could not create list of matching files" filterpid= trap 1 2 3 15 break fi done trap 1 2 3 15 msg "DEBUG: end waiting for new list" Playlist=$newlist fi fi if [ "$ListOnly" = "true" ] then if [ -n "${filterpid:=}" ] && kill -0 $filterpid >/dev/null 2>&1 then msg "INFO: waiting for selection criteria processing to complete. Please be patient; this can take some time..." wait $filterpid fi exec egrep -i -- "$*" "$Playlist" fi # The interrupt (trap) handling is complicated by having to deal # with both ksh and the Bourne shell (sh). # o ksh runs the "while read file" loop in the current process, and # therefore changing a variable from within the loop has an effect # on the surrounding loop, too. # o the Bourne shell runs the "while" loop in a subshell, and changing # environment interrupts cannot terminate the surrounding "until" # loop. # Instead of variables, we therefore use signals to let the inner loop # terminate the outer one. stopreading=false # inner loop was terminated by a signal interrupted=false # outer loop should terminate until $interrupted do # Ignore SIGHUP SIGINT SIGQUIT. We will receive SIGTERM (15) # from the inner loop to notify us that the user wishes to terminate # the program. trap 'echo >&2 SIGNAL player' 1 2 3 trap "echo >&2 SIGTERM player; interrupted=true" 15 $interrupted && break egrep -i "$*" "$Playlist" | { egrep . || { msg "no matching files: $*"; kill -15 $$; } } | shuffle 2>/dev/null | while read file do trap "stopreading=true" 1 2 3 sleep 1 $stopreading && { kill -15 $$; break; } [ -n "$file" ] || fatal "cannot find song matching \"$*\"" if [ -f "$file" ] then $isxwindows && xtitle "`basename \"$file\"`" # Relay all interrupts to the $player program, but # continue execution nevertheless. Note that "mpg123" # catches two interrupts: the first terminates the song, # the second terminates the program. A user therefore # has to enter three interrupts to leave this script. trap : 1 2 3 #player=echo "$player" $playerflags "$file" || exit $? fi done done trap 1 2 3 15 # Restore default trap handling # Are there still filter processes running? [ -n "$filterpid" ] && kill -0 "$filterpid" >/dev/null 2>&1 && # Yes: terminate filter process and all descendent processes kill $filterpid `list_proctree "$filterpid"` >/dev/null 2>&1 ########################################################################## # So the user really let us get here. Take advantage of that rare event # and use it to clean up old playlists. exitvalue=$? # save return code msg "cleaning up..." xtitle "$oldtitle" cd -- "$PLAYLISTDIR" || exit $exitvalue # Keep the $maxlistcnt most recent files, remove the rest. # We do not use "| xargs rm -f" because this would not handle whitespace # within file names. ls -t | sed "1,${maxlistcnt}d" | while read path do rm -f -- "$path" >/dev/null 2>&1 || break done wait # ...for all background processes to finish exit $exitvalue