#!/usr/bin/perl

use strict;
use File::Find;
use File::stat;
use Getopt::Long;
use Date::Calc qw/Today Delta_Days/;
use Date::Manip qw/UnixDate/;
use Pod::Usage;
use Term::ReadKey;
use POSIX ":sys_wait_h";
use MP3::Info;
use Ogg::Vorbis::Header;
use Time::localtime;

$| = 1;

# make sure we put the terminal back in a good state no matter how we quit
END {              
    ReadMode 1;
};

my $version = "v0.60";

my %player;
my $extensions;

my @played;
my $played_position=0;
my @files;
my $help = 0;
my $man = 0;
my $random = 1;
my $verbose = 0;
my $weight_favorites = 1;
my $quiet = 0;
my $days = 10;
my $delay = '1';
my $history_file = $ENV{"HOME"} . "/.randomplay_history";
my @search_path;
my %history;
my %favorite;
my $regexp = "";
my $regexp_if_statement;
my $names = 0;
my $tracks = -1;
my $remember = 1;
my $useutf8 = 1;
my $maxsize = 0;
my $maxtime = 0;
my $zero = 0;
my $last_played = 0;
my $basedir = "";
my $clear_history = 0;
my $version_only = 0;
my $announce_song = "";
my $newer_than = "";
my $older_than = "";
$announce_song = "osd_cat -p bottom -l 2 -A right -c white -d 5 -f '-etl-fixed-medium-r-*-*-24-*-*-*-*-*-*-*'" if ( -x "/usr/bin/osd_cat" || -x "/usr/local/bin/osd_cat" );
my %key; 

sub shuffle {
  my $array = shift;
  my $i;
  for ($i = @$array; --$i; ) {
    my $j = int rand ($i+1);
    next if $i == $j;
    @$array[$i,$j] = @$array[$j,$i];
  }
}

sub PlayMusic {
  $_ = $File::Find::fullname;
  eval $regexp_if_statement;
}

# KillOrPausePlayer recursively signals the requested process ID and all of its descendents
# This is a bit of a hack--there should be a better way to do this, but I haven't found one.
# Normally, kill HUP => -$$ should do the job, except this doesn't work when randomplay itself
# is called from a shell script and all output from child processes is redirected to null.
# This subroutine assumes that shelling out to ps is going to work, which may not be totally
# safe--at the very least, it reduces the portability of the script.
# Any suggestions for a better way to do this are welcome.
sub KillOrPausePlayer {
  my $pid = shift;
  my $sig = shift || "HUP";
  local $SIG{$sig} = 'IGNORE';   # don't want to kill ourselves here--shouldn't happen, but just in case.
  if (open PS, "ps -o pid,ppid |") {
     eof PS; # Force ps to run now. Otherwise it would run at the first read operation.
     kill $sig => $pid;
     foreach my $line (<PS>) {
       KillOrPausePlayer($1, $sig) if ($line =~ /^\s*(\d+)\s*(\d+)\s*$/ and ($2 eq $pid));
     }
     close PS;
  } else {                      # if we can't read the process table, just try killing the child process group
    kill $sig => -$$;
  }
}

# check to make sure players are actually executable
sub CheckPlayers {
  my %executable;
  $executable{$_} = 0 foreach (keys %player);
  foreach my $dir (split ':', $ENV{PATH}) { 
    foreach (keys %executable) {
       $executable{$_} = 1 if ( -x $dir . '/' . $player{$_} );
    }
  }
  foreach (keys %executable) {
    unless ($executable{$_}) {
       print "Error: can't find player for extension $_ ($player{$_}) in path!  Will not play $_ files.\n";
       delete $player{$_};
    }
  }
  die "None of the specified players are available, quitting.\n" unless keys %player;
}

# read in settings from ~/.randomplayrc, if any; be as flexible as possible in interpreting lines
if (open IN, $ENV{"HOME"} . "/.randomplayrc") {
   while (<IN>) {
      /^\s*days[\s=]*(.*)$/i and $days = $1;
      /^\s*history[\s=]*(.*)$/i and $history_file = $1;
      /^\s*random/i and $random = 1;
      /^\s*norandom/i || /^\s*random[\s=]*(no|off|false)/i and $random = 0;
      /^\s*pause[\s=]*(.*)$/i and $delay = $1;
      if (my ($ext,$prog) = /^\s*player\s*[\{\[\(]([A-Za-z0-9]+)[\}\]\)][\s=]*(.*)$/i) {
         $ext =~ tr/A-Z/a-z/;
         $player{$ext} = $prog;
      }
# this doesn't check to make sure the keystroke you are trying to define actually
# corresponds to a real keystroke, but there's no harm to defining a nonexistent keystroke
      if (my ($func,$keystroke) = /^\s*keys?\s*[\{\[\(]([A-Za-z]+)[\}\]\)]\s*[\s=]\s*(.*)$/i) {
         $func =~ tr/A-Z/a-z/;
         $func =~ s/[^a-z]//g;
         $key{$func} = $keystroke;
      }
      /^\s*quiet/ and $quiet = 1;
      /^\s*noquiet/ || /^\s*quiet[\s=]*(no|off|false)/ and $quiet = 0;
      /^\s*verbose/ and $verbose = 1;
      /^\s*noverbose/ || /^\s*verbose[\s=]*(no|off|false)/ and $verbose = 0;
      /^\s*remember/ and $remember = 1;
      /^\s*noremember/i || /^\s*remember[\s=]*(no|off|false)/i and $remember = 0;
      /^\s*utf8/i and $useutf8 = 1;
      /^\s*noutf8/i || /^\s*utf8[\s=]*(no|off|false)$/i and $useutf8 = 0;
      /^\s*weight/i and $weight_favorites = 1;
      /^\s*noweight/i || /^\s*weight[\s=]*(no|off|false)/i and $weight_favorites = 0;
      /^\s*tracks[\s=]*(.*)$/i and $tracks = $1;
      /^\s*maxsize[\s=]*(.*)$/i and $maxsize = $1;
      /^\s*maxtime[\s=]*(.*)$/i and $maxtime = $1;
      /^\s*basedir[\s=]*(.*)$/i and $basedir = $1;
      /^\s*announce[\s=]*(.*)$/i and $announce_song = $1;
      /^\s*newer(?:[_-]than)?[\s=]*(.*)$/i and $newer_than = $1;
      /^\s*older(?:[_-]than)?[\s=]*(.*)$/i and $older_than = $1;
      /^\s*names$/i and $names = 1;
   }
   close IN;
}

# read in command-line options, if any; can override ~/.randomplayrc settings
GetOptions(
            'announce|a=s' => \$announce_song,
            'days=s' => \$days,
            'history=s' => \$history_file,
            'random!' => \$random,
            'help|?' => \$help,
            'man' => \$man,
            'pause=s' => \$delay,
            'quiet!' => \$quiet,
            'regexp|regex|r=s' => \$regexp,
            'names-only|n' => \$names,
            'tracks=i' => \$tracks,
            'maxsize=s' => \$maxsize,
            'maxtime=s' => \$maxtime,
            'remember!' => \$remember,
            'utf8!' => \$useutf8,
            'last=i' => \$last_played,
            'basedir=s' => \$basedir,
            'clear' => \$clear_history,
            'version' => \$version_only,
            'verbose|v!' => \$verbose,
            'weight|w!' => \$weight_favorites,
            'player=s' => \%player,
	    'newer-than|newer_than|newer=s' => \$newer_than,
	    'older-than|older_than|older=s' => \$older_than,
            'key=s' => \%key,
            'keys=s' => \%key,
            '0' => \$zero
);

$key{'like'} = "+" unless exists $key{'like'};
$key{'dislike'} = "-" unless exists $key{'dislike'};
$key{'next'} = "NnFf" unless exists $key{'next'};
$key{'back'} = "Bb" unless exists $key{'back'};
$key{'quit'} = "Qq" unless exists $key{'quit'};
$key{'last'} = "Ll" unless exists $key{'last'};
$key{'reset'} = "0=" unless exists $key{'reset'};
$key{'pause'} = "Pp" unless exists $key{'pause'};
$key{'help'} = "Hh?" unless exists $key{'help'};

# define default music file players if none defined
unless (keys %player) {
   $player{'ogg'} = 'ogg123';
   $player{'mp3'} = 'mpg321';
#   $player{'wav'} = 'play-sample';
#   actually, most people don't want to play WAVs, and this will cause randomplay not to work 
#   with default configuration if you don't have sound-recorder installed (which is most people)
}

print "randomplay $version\n" unless ($quiet or $names);

exit(0) if $version_only;
pod2usage(1) if $help;
pod2usage(-exitstatus => 0, -verbose => 2) if $man;

&CheckPlayers;

$delay =~ s/[^0-9]//g;
$delay = '1' unless length $delay;

$days = 0 if ($zero or $last_played);        # -0 is shortcut for --days=0, assume -0 when we ask for last x played songs
$remember = 0 if ($last_played);             # --last implies no remember
$random = 0 if ($last_played);               # --last implies no shuffle
$weight_favorites = 0 unless ($random);      # if the user requests nonrandom list, then don't do preferential weighting

# convert newer_than, older_than and days value into actual days
for ($newer_than, $older_than, $days) {
    # if date is in date (rather than delta format), calculate the delta
    if (m{/}) {
       my $x = localtime(UnixDate($_,"%s"));
       my $y = Delta_Days($x->year()+1900,$x->mon()+1,$x->mday(),Today());
       $_    = $y if $y;
    } else {
      # otherwise, convert d, m, w, y to absolute days
      s/days?/d/gi;
      s/weeks?/w/gi;
      s/months?/m/gi;
      s/years?/y/gi;
      s/[^0-9dwmy]//gi;             
      if (/y/i) {
         s/[^0-9]//g;
         $_ = $_ * 365;
      } elsif (/m/i) {
         s/[^0-9]//g;
         $_ = $_ * 31;
      } elsif (m/w/i) {
         s/[^0-9]//g;
         $_ = $_ * 7;
      } else {
         s/[^0-9]//g;
      }
    }
}

$extensions = '\.' . join('$|',keys(%player)) . '$';
$extensions =~ s/\|/\|\\./g;
$basedir =~ s/~/$ENV{"HOME"}/;               # expand tilde in basedir
$basedir =~ s{([^/])$}{$1/}g if $basedir;    # add a trailing slash to basedir if not specified

print "Warning: base directory $basedir does not exist or is not readable!\n" if (not $quiet and $basedir and not -d $basedir);

if ($regexp) {
   $regexp_if_statement = "if (/$extensions/i && /" . join ('/i && /', split / /, $regexp) . '/i) {';
} else {
   $regexp_if_statement = "if (/$extensions/i) {";
}

# this seems to be a very slow way to check for file dates, but it's a first stab.
# more efficient suggestions welcome.
$regexp_if_statement .= '
     my $date_string = localtime(stat($_)->mtime);
     if ((! $newer_than) || (Delta_Days($date_string->year()+1900,$date_string->mon()+1,$date_string->mday(),Today())) < $newer_than ) {
     if ((! $older_than) || (Delta_Days($date_string->year()+1900,$date_string->mon()+1,$date_string->mday(),Today())) > $older_than ) {
     if (defined $history{$_} and $days) {
        if (Delta_Days(@{$history{$_}},Today()) > $days)  {
           push @files, $_;
        }
     } else {
       push @files, $_;
     }
     }
     }
  }
';

if ($maxsize) {                              # convert maxsize into raw bytes
   $_ = $maxsize;
   s/[^0-9]//g;
   if ($maxsize =~ /m$/i) {
     $maxsize = $_ * 1048576;
   } elsif ($maxsize =~ /k$/i) {
     $maxsize = $_ * 1024;
   } else {
     $maxsize = $_;
   }
}

if ($maxtime) {                              # convert maxtime into raw seconds
   $_ = $maxtime;
   s/[^0-9]//g;
   if ($maxtime =~ /m$/i) {
     $maxtime = $_ * 60;
   } elsif ($maxtime =~ /h$/i) {
     $maxtime = $_ * 3600;
   } elsif ($maxtime =~ /d$/i) {
     $maxtime = $_ * 86400;
   } else {
     $maxtime = $_;
   }
}

$history_file =~ s/~/$ENV{"HOME"}/;
unlink $history_file if ($clear_history and -e $history_file);

if (open IN, $history_file) {                # read in history file
  my $line_count = 0;
  if ($last_played) {                        # need to count lines in history if there is a request for last x songs
     $line_count += tr/\n/\n/ while sysread(IN, $_, 2 ** 16);
     seek IN,0,0;
  }
  while (<IN>) {                             # parse history file into filenames and last played dates
    if (my ($filename, $date, $favorite) = /^([^\t]*)\t([^\t]*)\t?(.*)?$/) {
      $history{$filename} = [ split "/", $date ];
      $favorite{$filename} = $favorite if ($favorite ne "");
      push @files, $filename if ($last_played and $line_count-- <= $last_played);
    }
  }
  close IN;
} elsif (-e $history_file) {
  print STDERR "Warning: could not open history file $history_file, check file permissions.\n";
}

my $files_specified = 0;

foreach (@ARGV) {                            # parse filenames and directory names from the command line
  s/^~/$ENV{"HOME"}/;                        # expand ~ as home directory
  s/^=/$basedir/;                            # = directories are relative to the basedir
  if (-d $_) {
     push @search_path, $_;
  } elsif ($basedir and -d $basedir . $_) {
     push @search_path, $basedir . $_;
  } elsif (-f $_) {
     $files_specified = 1;
     $_ = $ENV{"PWD"} . "/" . $_ unless (/^\//);
     if (defined $history{$_} and $days) {
        if (Delta_Days(@{$history{$_}},Today()) > $days)  {
           push @files, $_;
        }
     } else {
       push @files, $_;
     }
  }
}

push @search_path, ($basedir ? $basedir : ".") unless (@search_path or @files or $last_played or $files_specified);

find( { follow => 1, follow_skip => 2, wanted => \&PlayMusic }, @search_path) if (@search_path and not $last_played);

shuffle(\@files) if ($random and scalar(@files) > 2);

@files = sort(@files) unless $random;     # put in alphabetical order if nonrandom requested

my $bytes_played = 0;
my $start_time = time;
my $index = 0;
my $any_left = 1;
my $announce_pid = 0;

MP3::Info::use_mp3_utf8($useutf8);                    # enable/disable UTF-8 printing for MP3 tags

my $next_song;

while (@files or $played_position < scalar @played) {
  if ($played_position < scalar @played) {
    $next_song = $played[$played_position];
  } else {
    last unless $tracks--;                              # finished playing specified tracks, stop playing
    last if $maxtime and time - $start_time > $maxtime; # finished playing specified number of seconds, stop playing
    if ($weight_favorites) {                            # random weighting, we flip a coin but make it easier or harder to
       $next_song = "";                                 # get "heads" (rand(1)>0.5) based on the song's score
       until ($next_song) {                             # score = 5 means always play
          my $rand = rand(1);                           # score = -5 means never play unless there are (almost) no other songs left
          if ($rand > (0.5 - (exists $favorite{$files[$index]} ? $favorite{$files[$index]} : 0) * 0.1)) {
            $any_left = 1;                              # if we found a song, that proves that there are some less with
            $next_song = $files[$index];                # preference > -5
            splice @files, $index, 1; 
            $index = @files-1 if ($index >= @files);
          } else {
            if (++$index >= @files) {
               unless ($any_left) {                     # if we've been all the way through and haven't picked any songs
                 $next_song = shift @files;                     # grab the next one in the list
               }
               $index = 0;
               $any_left = 0;
            }
          }
       }
    } else {                                            # otherwise just play the next song in the list
      $next_song = shift @files;
    }
  }
  if ($maxsize) {                                     # finished playing specified number of bytes
     $bytes_played += stat($next_song)->size;
     last if $bytes_played > $maxsize;
  }
  $next_song =~ s/`/\\`/gi;                           # escape backticks to avoid shenanigans with filenames (could this even happen? Probably not unless/until we have a prerecorded index file.)
  my $song_title = "";
  my $song_artist = "";
  if ($names) {
     print $next_song . "\n";
  } else {                                            # grab whatever mp3/ogg tags we can read
    my $song_info = "";                               # at some point we might want to be more selective
    if ($next_song =~ /mp3$/i) {                      # and smarter with presentation and support other tags
      my $tag = get_mp3tag($next_song);
      while (my ($k, $v) = each %{$tag}) {
         $song_info .= $k . ": " . $v . "\n" if $v;
         $song_title = $v if ($k =~ /title/i);
         $song_artist = $v if ($k =~ /artist/i);
      }
    } elsif ($next_song =~ /ogg$/i) {
      my $tag = Ogg::Vorbis::Header->new($next_song);
      foreach ($tag->comment_tags) {
         $song_info .= $_ . ": " . join(" ", (defined $tag->comment($_) ? $tag->comment($_) : "")) . "\n";
         $song_title = join(" ", (defined $tag->comment($_) ? $tag->comment($_) : "")) if (/title/i);
         $song_artist = join(" ", (defined $tag->comment($_) ? $tag->comment($_) : "")) if (/artist/i);
      }
    }
    KillOrPausePlayer $announce_pid if $announce_pid;
    if ($next_song and $announce_song) {
       unless ($announce_pid = fork()) {
          # we need to close STDIN so the announce program doesn't hold into it
          close STDIN;
          if ($song_artist and $song_title) {
             if (length ($song_title . $song_artist) < 40) {
                $song_title = "$song_artist / $song_title";
             } else {
                $song_title = "$song_artist\n$song_title";
             }
          } elsif ($song_artist) {
             $song_title = "$song_artist";
          } elsif (not $song_title) {
             $song_title = $next_song;
             # remove path and extension for screen display
             $song_title =~ s{^.*/}{};
             $song_title =~ s{\..*?$}{};
          }
          if (open ANNOUNCE, "|$announce_song") {
             print ANNOUNCE $song_title;
             close ANNOUNCE;
          }
          exit;
       }
    }
    $song_info = "Playing ${next_song}...\n" unless $song_info;
    $song_info .= "Weight: " . ($favorite{$next_song} ? $favorite{$next_song} : 0)  . "\n";
    print "---------------------\n" . $song_info unless $quiet;
    my $pid;
    unless ($pid = fork()) {             # we fork; either the child plays the whole song or is killed by us
      foreach my $ext (keys %player) {
        $next_song =~ m/$ext$/i and exec($player{$ext} . " \"" . $next_song . "\"" . ($verbose ? "" : " > /dev/null 2>&1"));
      }
    }
# add the currently played song to the played history in case user decides to go back
    push @played, $next_song unless ($played_position < scalar @played);
    $played_position++;
    my $kid;
    ReadMode 3;       # in case the music player changes the terminal mode, put it back in something where
    my $paused = 0;   # start each song assuming we are *not* paused
    do {              # we can still catch keystrokes; ReadMode 3 will allow user to suspend with ctrl-z, however
      my $keystroke = ReadKey(0.01);
#      $kid = waitpid(-1,WNOHANG) || 0;
#      print "waiting for $pid\n";
      $kid = waitpid($pid,WNOHANG) || 0;
#      print "got $kid and $keystroke\n";
      if (not defined $keystroke) {
      } elsif ($key{'pause'} =~ /\Q$keystroke\E/) {
         &KillOrPausePlayer($pid, ($paused) ? 'CONT' : 'STOP');
         $paused = !$paused;
         print (($paused) ? "Paused... (hit " . substr($key{'pause'},0,1) . " to continue)\n" : "Continuing...\n") unless $quiet;
      } elsif ($key{'next'} =~ /\Q$keystroke\E/) {
         print "Skipping to next song...\n" unless $quiet;
         &KillOrPausePlayer($pid);
      } elsif ($key{'back'} =~ /\Q$keystroke\E/) {
         if ($played_position > 1) {
            print "Skipping to previous song...\n" unless $quiet;
            $played_position -= 2;
            &KillOrPausePlayer($pid);
         } else {
            print "Already on first song, can't go back...\n" unless $quiet;
         }
      } elsif ($key{'quit'} =~ /\Q$keystroke\E/) {
         print "Quitting...\n" unless $quiet;
         &KillOrPausePlayer($pid);
         last;
      } elsif ($key{'last'} =~ /\Q$keystroke\E/) {
         print "Quitting after this song...\n" unless $quiet;
         @files=qw//;                           # clear song playlist; at the end of this loop there will be no songs to play
      } elsif ($key{'like'} =~ /\Q$keystroke\E/) {
         $favorite{$next_song} = 5 if (++$favorite{$next_song} > 5);
         print "Increasing preference to " . $favorite{$next_song} . ".\n" unless $quiet;
      } elsif ($key{'dislike'} =~ /\Q$keystroke\E/) {
         $favorite{$next_song} = -5 if (--$favorite{$next_song} < -5);
         print "Decreasing preference to " . $favorite{$next_song} . ".\n" unless $quiet;
      } elsif ($key{'reset'} =~ /\Q$keystroke\E/) {
         $favorite{$next_song} = 0;
         print "Resetting preference to 0.\n" unless $quiet;
      } elsif ($key{'help'} =~ /\Q$keystroke\E/) {
         printf "%-15s %-30s\n", join(", ",split(//,$key{'help'})), "show keystrokes";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'like'})), "increase the currently playing song's random weight";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'dislike'})), "decrease the currently playing song's random weight";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'reset'})), "reset the currently playing song's randow weight to 0";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'next'})), "skip to the next song";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'back'})), "skip to the previous song";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'pause'})), "pause the playback (toggle)";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'last'})), "quit randomplay after the current song is finished";
         printf "%-15s %-30s\n", join(", ",split(//,$key{'quit'})), "quit randomplay";
      }
    } until ($kid > 0);
    ReadMode 1;          # return terminal mode back to what the user probably wants, in case they cancel here
    sleep $delay if $delay;
  }
  next unless $remember;        # don't record this track in history if we are in 'noremember' mode
  if (open OUT, ">>$history_file") {
     print OUT $next_song . "\t" . join('/',Today()) . (exists $favorite{$next_song} ? "\t" . $favorite{$next_song} : "" ) . "\n";
     close OUT;
  } else {
    print STDERR "Warning: could not open history file $history_file for writing, check file permissions and disk space.\n";
  }
}

__END__

=head1 NAME

randomplay - command-line based shuffle music player that remembers songs between sessions

=head1 SYNOPSIS

randomplay [options] [directory ...] [file ...]

 Options:
   --help                            brief help message
   --man                             full man page
   --version                         show the version number and exit                                     
   -q, --quiet                       quiet output: don't report name of each song as it is played
   -v, --verbose                     verbose output: include output from player program on console

   -d n, --days=n                    minimum days since track was last played for it to be considered (default 10 days)
                                     n can be number of days, or add suffix (W = week, M = month, Y= year), or can be an absolute date
   -0                                special case to ignore history entirely (equivalent to --days=0)
   --random                          shuffle track listing (default random)
   --norandom                        
   --remember                        remember tracks played for future sessions (default remember)
   --noremember                      
   --utf8                            use UTF-8 encoding for output (default utf8)
   --noutf8
   -w, --weight                      weight random shuffle to prefer songs rated as favorites (default weight)
   --noweight

   -r, --regexp=regexp string        only play files where filename matches regexp string
   -n, --names-only                  don't play tracks, only give list of filenames
   -t n, --tracks=n                  how many tracks to play (or display) total (default unlimited)
   --maxsize=number of bytes         maximum number of bytes of music to play or display (default unlimited)
   --maxtime=duration                how long to play before stopping (minutes or hours) (default unlimited)
   -l n, --last=n                    play last X songs in same order as they were played before
   --newer-than n, --newer n         only play songs newer than n days old (e.g., W = week, M = month, Y = year, default days) or newer than date n
   --older-than n, --older n         only play songs older than n days old or older than date n

   --basedir=directory               directory to which all other directory specifications will be relative
   --history=filename                file to use for randomplay history (default ~/.randomplay_history)
   --clear                           permanently clear (delete) the history file, start over with new history

   --pause=n                         delay between songs in seconds (default one second)
   --announce=command                program to run to announce each new track
                                     track information is piped into program
                                     set to a blank string if you don't want any on screen announcements
                                     (default is xosd if it is installed)
   --player extension=command        play files with extension with specified command 
                                     (default mp3=mpg321, ogg=ogg123)
   --key function=keystroke          define alternative keystrokes for player actions (defaults below)

 Default keys while playing:
   h, ?                              show keystrokes
                                     (set with --key help=keystrokes)
   +                                 increase the currently playing song's random weight
                                     (set with --key like=keystrokes)
   -                                 decrease the currently playing song's random weight
                                     (set with --key dislike=keystrokes)
   0, =                              reset the currently playing song's randow weight to 0
                                     (set with --key reset=keystrokes)
   f                                 skip to the next song
                                     (set with --key next=keystrokes)
   b                                 skip to the previous song
                                     (set with --key back=keystrokes)
   p                                 pause the playback (toggle)
                                     (set with --key pause=keystrokes)
   l                                 quit randomplay after the current song is finished
                                     (set with --key quit=keystrokes)
   q                                 quit randomplay
                                     (set with --key last=keystrokes)

=head1 DESCRIPTION

randomplay is a shuffle music file player with a memory across sessions.  randomplay also can remember which songs you like better and weight them more heavily in the random shuffle.  It can also be used to play a directory or set of directories nonrandomly, remembering where you left off between seessions.  You can specify any number of directories to be recursively searched for music files, as well as how long since a track was last played for it to re-enter the rotation.  You can also keep track of different playlist histories (for example, for different users of the same account) by specifying a different history filename. All options can also be kept in ~/.randomplayrc, with one option per line, option name followed by a space followed by the option value. Command line settings will override settings specified in ~/.randomplayrc; later settings will override earlier settings. There is a default randomplayrc that comes with this package and includes all possible settings.

=head1 EXAMPLE

Play all ogg files in dir1 and dir2 under your home directory, and dir3 under the base directory specified in ~/.randomplayrc, which have not been played for 15 days in random order with 5 seconds between songs:

randomplay --days=15 --pause=5 --player ogg=ogg123 ~/dir1 ~/dir2 =dir3

Play all ogg, wav, and mp3 files under the current directory (or base directory, if specified in .randomplayrc file) which have not been played for 10 days in alphabetical order, switch the 'skip to next song' keystroke to 'G' or 'g' and 'quit' to 'q' or 'c':

randomplay --norandom --key next=Gg --key quit=qc

Play all files under the current directory with the strings "frisell" and "bill" in the filename, in any order, (saves having to hunt down a file in a hierarchy), ignore whether the file has been played recently, but stop playing after 15 minutes:

randomplay --regexp 'frisell bill' -0 --maxtime=15m

Display 100M worth of music files, randomly sorted, without recording the history of tracks, using the default music directory (or the current directory if not specified):

randomplay --maxsize=100M --noremember --names-only

Play the last 10 songs played over again:

randomplay --last=10

Play songs test.ogg, test2.ogg, test3.ogg, and all files in musicdir in random order without weighting preferred songs:

randomplay --noweight test.ogg test2.ogg test3.ogg musicdir

Copy 128M of songs into a Neuros Audio Player, using positron:

positron add `randomplay --names-only --maxsize=128M`

Pick a random jpeg or png file that has not been displayed in the last week from the 'images' directory and display it with ImageMagick 'display' command:

randomplay --player jpg=display --player gif=display --days 7 ~/images

=head1 NOTES

Most options can be abbreviated as a single letter; e.g., -t for --tracks, -d for --days, -r for --regexp. You can also omit the "=" sign between the option and the value if you prefer. If many options start with the same letter (e.g., --maxtime, --maxsize), then you must use the full name for the option.

For maxsize, you can use "k" for kilobytes and "m" for megabytes, or just enter the number of bytes.

For maxtime, you can use "s" for seconds (default units), "m" for minutes, "h" for hours, or "d" for days.

You might want to specify a "basedir" setting in your .randomplayrc, if all your music is under a certain directory. Then you can just give the relative names of the directories on the command line--e.g., you have "basedir=~/music" in ~/.randomplayrc, and rock is a directory under ~/music, so just enter "randomplay rock". 

Randomplay will search for music in specified subdirectories first under the current working directory, and then relative to the basedir setting, if any.  If the specified directory name begins with =, it will be searched relative to the basedir setting, even if the specified directory also exists under the current working directory.

Spaces in regexps are turned into "ands," making it more convenient to find the files you want.  For example, --regexp 'thelonious monk blues' will play all songs with the words thelonious, monk, and blues in the filename, regardless of the order in which those words occur.  Regexp searching is case-insensitive.

If you specify an extension and a player for that extension, either in ~/.randomplayrc or on the command-line, only files with that extension will be played.  You can, however, specify multiple players/extensions in either place, and all extensions listed will be played.

The --last option implies --norandom and --noremember.

=head1 COPYRIGHT

Copyright (c) 2003-2006 Adam Rosi-Kessel.
This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, to the extent permitted by law.

=head1 BUGS

Your history file will grow indefinitely. This could either be a bug or a feature. You may want to clear the history at some point. It might also compromise your privacy if other people can see your history file and you are concerned about people knowing which songs you have listened to.

The system used to kill the music player when you decide to skip to the next track looks up the process IDs using 'ps'; this may not work on all platforms.

The --maxtime option only works if you actually play the songs; it calculates actual time elapsed, rather than totalling the length of the tracks.  This may or may not be the behavior you expect.

=head1 AUTHOR

Adam Rosi-Kessel, L<ajkessel@debian.org|mailto:ajkessel@debian.org>

=cut


syntax highlighted by Code2HTML, v. 0.9.1