This is a music player based on [POE::Component::Player::Musicus] and [POE::Wheel::Curses]. You'll need to have [Musicus] installed because it's what PoCo::Player::Musicus uses. You'll then be able to play any file type that has an XMMS plugin.

To use the program, give a directory with music files as the first command line argument.

#!/usr/bin/perl
use strict;
use warnings;
use POE qw(Component::Player::Musicus Wheel::Curses);
use Curses;
my $dir = shift;
$dir = "." unless defined($dir);
die "$dir is not a directory" unless -d $dir;
opendir(DIR, $dir) or die $!;
my @music = grep { -f "$dir/$_" } sort grep /^[\x20-\x7E]+$/, readdir(DIR);
closedir DIR;
POE::Session->create(
  inline_states => {
    _start       => \&start,
    musicus_play => \&musicus_play,
    send_getpos  => \&send_getpos,
    play         => \&play,
    stop         => \&stop,
    pause        => \&pause,
    unpause      => \&unpause,
    getinfocurr  => \&getinfocurr,
    getpos       => \&getpos,
    setpos       => \&setpos,
    error        => \&error,
    getlength    => \&getlength,
    quit         => \&player_quit,
    update_all   => \&curses_refresh,
    got_input    => \&curses_input,
    update_song  => \&update_song,
    update_list  => \&update_list,
    update_time  => \&update_time,
  },
);
POE::Kernel->run;

sub start {
  my ($kernel, $heap) = @_[KERNEL, HEAP];
  $kernel->alias_set('console');

  # Initialize everything
  %$heap = (
    musicus => POE::Component::Player::Musicus->new(
      alias => 'console',
      delay => 100000
    ),
    songinfo => {
      length  => 0,
      pos     => 0,
      marker  => 0,
      artist  => '',
      title   => '',
      album   => '',
      track   => '',
      year    => '',
      date    => '',
      genre   => '',
      comment => '',
    },
    list_info => {
      top    => 0,
      cursor => 0,
    },
    playing => 0,
    paused  => 0,
    gotinfo => 0,
  );
  $heap->{curses} = POE::Wheel::Curses->new(InputEvent => 'got_input');
  $kernel->yield('update_all');
  $kernel->yield('musicus_play', 0);
}

# Musicus event handlers
sub play {
  my ($heap, $kernel, $plugin) = @_[HEAP, KERNEL, ARG0];
  $heap->{playing} = 1;
  $heap->{paused}  = 0;
  $kernel->yield('updatedisplaystatus');

  # Start getting position almost immediately, but put in a delay in case
  # we get a bunch of new songs started right after each other, then
  # there (hopefully) won't be a lot of extra getpos's
  $kernel->delay('send_getpos', .1);
}

sub stop {
  my ($heap, $kernel) = @_[HEAP, KERNEL];
  $kernel->delay('send_getpos');    # No need to get position while stopped
  $heap->{playing} = 0;
}

sub pause {
  my ($heap, $kernel) = @_[HEAP, KERNEL];
  $kernel->delay('send_getpos');    # No need to get position while paused
  $heap->{paused} = 1;
}

sub unpause {
  my ($heap, $kernel) = @_[HEAP, KERNEL];
  $heap->{paused} = 0;
  $kernel->yield('send_getpos');    # Start getting position again
}

sub getinfocurr {
  my ($heap, $kernel, $info) = @_[HEAP, KERNEL, ARG0];
  unless ($info->{title}) {

    # If the title is the same as the file name,
    # get rid of the extension and dir
    $info->{title} = $info->{file};
    $info->{title} =~ s/^$dir\///;
    $info->{title} =~ s/\.[^.]+$//;
  }

  # Some XMMS plugins give better info for this command than getinfocurr
  $heap->{musicus}->getlength;

  # Add in the new information while preserving what's already there
  $heap->{songinfo} = {%{$heap->{songinfo}}, %$info};
  $kernel->yield('update_song');
  $kernel->yield('update_time');
}

sub getlength {
  my ($heap, $length, $kernel) = @_[HEAP, ARG0, KERNEL];
  $heap->{songinfo}{length} = $length;
  $kernel->yield('update_time');
}

sub getpos {
  my ($heap, $kernel, $pos) = @_[HEAP, KERNEL, ARG0];

  # Don't attempt to get info until song is actually playing
  if (!$heap->{gotinfo} && $pos > 0) {
    $heap->{gotinfo} = 1;
    $heap->{musicus}->getinfocurr;
  }
  if ($pos >= 0) {

    # No use updating if it's not different
    if (int($pos / 1000) != int($heap->{songinfo}{pos} / 1000)) {
      $heap->{songinfo}{pos} = $pos;
      $heap->{songinfo}{marker} =
        int(($pos / $heap->{songinfo}{length}) * ($Curses::COLS - 1));
      $kernel->yield('update_time');
    }
    if ($heap->{playing} && !$heap->{paused}) {
      $kernel->delay('send_getpos', .1);
    }
  }
  else {

    # End of song
    $heap->{songinfo}{pos}    = $heap->{songinfo}{length};
    $heap->{songinfo}{marker} = $Curses::COLS - 1;
    $kernel->yield('update_time');
  }
}

# Events to send commands to musicus
sub send_getpos {
  my $heap = $_[HEAP];
  $heap->{musicus}->getpos;
}

sub musicus_play {
  my ($heap, $index) = @_[HEAP, ARG0];

  # No, we don't have the info yet, but here's something temporary
  # (also erases old info)
  $heap->{gotinfo}  = 0;
  $heap->{songinfo} = {
    title   => $music[$index],
    pos     => 0,
    marker  => 0,
    length  => 0,
    artist  => '',
    title   => '',
    album   => '',
    track   => '',
    year    => '',
    date    => '',
    genre   => '',
    comment => '',
  };
  $heap->{musicus}->stop if $heap->{playing};
  $heap->{musicus}->play($dir . '/' . $music[$index]);
}

# Events to handle Curses interaction
# Update all parts of the display.
sub curses_refresh {
  my $kernel = $_[KERNEL];
  $kernel->yield('update_song');
  $kernel->yield('update_time');
  $kernel->yield('update_list');
}

# Update the song list.  This sort of handles the highlight bar and
# scrolling, although it's really kind of cheezy.
sub update_list {
  my $heap      = $_[HEAP];
  my $list_info = $heap->{list_info};
  my $row       = 15;
  if ($list_info->{cursor} < 0) {
    $list_info->{cursor} = 0;
    $list_info->{top}-- if $list_info->{top} > 0;
  }
  elsif ($row + $list_info->{cursor} >= $Curses::LINES) {
    $list_info->{cursor} = $Curses::LINES - $row - 1;
    if ($list_info->{top} + ($Curses::LINES - $row) < @music) {
      $list_info->{top}++;
    }
  }
  elsif ($list_info->{top} + $list_info->{cursor} > @music - 1) {
    $list_info->{cursor} = @music - 1 - $list_info->{top};
  }
  my $offset = 0;
  my $index  = $list_info->{top};
  while ($row < $Curses::LINES) {
    move($row, 0);
    clrtoeol();
    if ($index < @music) {
      attrset(A_REVERSE) if $offset == $list_info->{cursor};
      addstr(sprintf("%5d: %s", $index, $music[$index]));
    }
    attrset(A_NORMAL) if $offset == $list_info->{cursor};
    $row++;
    $offset++;
    $index++;
  }
  move($row + $list_info->{cursor}, 0);
  noutrefresh();
  doupdate;
}

# Update the song information.  Title, album, genre, etc.
sub update_song {
  my $heap = $_[HEAP];
  my $song = $heap->{songinfo};
  move(0, 0);
  clrtoeol();
  addstr("Artist     : $song->{artist}");
  move(1, 0);
  clrtoeol();
  addstr("Album      : $song->{album}");
  move(2, 0);
  clrtoeol();
  addstr("Genre      : $song->{genre}");
  move(3, 0);
  clrtoeol();
  addstr("Year       : $song->{year}");
  move(4, 0);
  clrtoeol();
  addstr("Date       : $song->{date}");
  move(5, 0);
  clrtoeol();
  addstr("Title      : $song->{title}");
  move(6, 0);
  clrtoeol();
  addstr("Comment    : $song->{comment}");
  move(7, 0);
  clrtoeol();
  addstr("Track      : $song->{track}");
  move(8, 0);
  clrtoeol();
  move(9, 0);
  clrtoeol();
  move(10, 0);
  clrtoeol();
  addstr("Controls   : Up/Down to Select, Enter to Play, "
      . "Space to toggle Pause, Q to Quit");
  noutrefresh();
  doupdate;
}

# Update the time display.  This also moves the hash mark across the
# screen to show the song's progress.
sub update_time {
  my $heap = $_[HEAP];
  move(13, 0);
  clrtoeol();
  addstr(
    sprintf(
      "Time info  : seconds=\%.2f/\%.2f",
      $heap->{songinfo}{pos} / 1000,
      $heap->{songinfo}{length} / 1000
    )
  );
  move(14, 0);
  addstr('-' x $Curses::COLS);
  move(14, $heap->{songinfo}->{marker});
  addstr('#');
  move(14, $heap->{songinfo}->{marker});
  noutrefresh();
  doupdate;
}

# Got console input.  Don't just sit there, do something!
sub curses_input {
  my ($kernel, $heap, $keystroke) = @_[KERNEL, HEAP, ARG0];

  # Replace special keystrokes with the names they're known by to
  # Curses.
  $keystroke = uc(keyname($keystroke)) if $keystroke =~ /^\d{2,}$/;

  # Ctrl+L refreshes the display.
  if ($keystroke eq "\cL") {
    $kernel->yield('update_all');
    return;
  }

  # Emergency exit on C-c.
  if (($keystroke eq "\cC") or (lc($keystroke) eq 'q')) {
    $heap->{musicus}->quit();
    return;
  }

  # Navigate down the play list.
  if ($keystroke eq 'KEY_DOWN') {
    $heap->{list_info}->{cursor}++;
    $kernel->yield('update_list');
    noutrefresh();
    doupdate;
    return;
  }

  # Navigate up the play list.
  if ($keystroke eq 'KEY_UP') {
    $heap->{list_info}->{cursor}--;
    $kernel->yield('update_list');
    noutrefresh();
    doupdate;
    return;
  }

  # The "p" key plays.
  if (ord($keystroke) == 13) {
    $kernel->yield('musicus_play',
      $heap->{list_info}->{top} + $heap->{list_info}->{cursor});
    return;
  }

  # The spacebar toggles pause.
  if ($keystroke eq ' ') {
    if ($heap->{paused}) {
      $heap->{musicus}->unpause();
    }
    else {
      $heap->{musicus}->pause();
    }
    return;
  }
}

# The musicus player quit.  Shut down the console interface, which
# triggers the program's exit.
sub player_quit {
  my ($kernel, $heap) = @_[KERNEL, HEAP];
  move($Curses::LINES - 1, 0);
  clrtoeol();
  move($Curses::LINES - 2, 0);
  clrtoeol();
  addstr("Player has quit.  Bye!");
  noutrefresh();
  doupdate;
  $kernel->alias_remove("console");
  delete $heap->{curses};
}

# Written by Curtis "Mr_Person" Hawthorne
# Based on the MPEG Player from the POE Cookbook