#!/usr/bin/perl
use strict;
use Tk;
use Tk::ROText;
use Tk::Pane;
use Tk::MListbox;
use POSIX;
use Socket;
use File::Copy qw(mv);
use Getopt::Long;
use Pod::Usage;
use DBI;
use MP3::Tag;
use MP3::Info qw(:all);
use Ogg::Vorbis::Header;
use Encode;
use utf8;
use open ':std', ':encoding(UTF-8)';


my $pjam_version	= '1.13.0';
my $pjam_desc		= "\nPerlJammer version $pjam_version\nA Perl music jukebox\n";

my $cfgdir		= sprintf('%s/.pjam', $ENV{HOME});
my $cfgfile		= sprintf('%s/config', $cfgdir);

my ($SOCKET, $STATESOCKET, $PLAYER);
my (%opts, $playing, $active, $elapsed, $paused, $player_pid,
    $app, $listbox, $timebox, $playbutton, $waitbutton,
    $dragging, $drag_start, $click_row, $iconfile);


if (GetOptions(\%opts,
               'autoplay|a',
               'debug|d=i',
               'geometry|g=s',
               'help|usage|?|h|u',
               'log|l',
               'man|m',
               'nonet|n',
               'version|v'))
{
    if ($opts{version})
    {
        $opts{nonet} = 1;
        pod2usage(-message => $pjam_desc,
                  -exitstatus => 0,
                  -verbose => 0);
    }
    elsif ($opts{help})
    {
        $opts{nonet} = 1;
        pod2usage(-message => $pjam_desc,
                  -exitstatus => 0,
                  -verbose => 1);
    }
    elsif ($opts{man})
    {
        $opts{nonet} = 1;
        pod2usage(-exitstatus => 0,
                  -verbose => 2);
    }
    else
    {
        MP3::Tag->config(write_v24 => 1, prohibit_v24 => 0);
        use_winamp_genres();
        jam();
    }
}
else
{
    $opts{nonet} = 1;
    pod2usage(-message => $pjam_desc,
              -exitstatus => -1,
              -verbose => 1);
}

exit (0);


sub quit
{
    close ($SOCKET) if ($SOCKET && !$opts{nonet});
    stop() if ($opts{stop_on_quit});
    close (STDOUT) if ($opts{log});

    exit(0);
}


sub jam
{
    if ($opts{log})
    {
        my $logfile = sprintf('%s/perljammer.log', $ENV{HOME});
        close (STDOUT);
        open (STDOUT, ">$logfile") || die "Cannot open logfile $logfile as STDOUT";
        select((select(STDOUT), $| = 1)[0]);
    }

    unless (-d $cfgdir)
    {
        print "No $cfgdir directory was found.\n";
        if (mkdir ($cfgdir, oct('0700')))
        {
            print "It has been created for you.\n";
        }
        else
        {
            print "Additionally, it could not be created.  Unable to continue.\nPlease investigate this problem.\n";
            quit();
        }
    }

    if (-f $cfgfile)
    {
        %opts = get_prefs($cfgfile, %opts);
    }
    else
    {
        print "No config file ($cfgfile) was found.\n";
        write_cfgfile();

        if (-f $cfgfile)
        {
            print "A default one has been created for you.  Please edit it\nappropriately and restart PerlJammer.\n";
        }
        else
        {
            print "Additionally, the file could not be created.  Unable to continue.\nPlease investigate this problem.\n";
        }
        quit();
    }

    apply_defaults(\%opts);
    $iconfile = sprintf('%s/PerlJammer.xpm', $opts{datadir});
    list_options(\%opts) if ($opts{debug});

    if (defined $opts{sqldb})
    {
        ($playing, $active, $elapsed, $paused) = (-1, 0, 0, 0);
        create_UI();
        create_playlist();
        $app->repeat(50   => \&event_loop);
        $app->repeat(100  => \&state_loop) unless ($opts{nonet});
        $app->repeat(1000 => \&display_elapsed);
        play() if ($opts{autoplay});
    }
    else
    {
        cfg_error_warn();
    }

    unless ($opts{nonet})
    {
        my $port = $opts{remoteport} || 16384;	# belt and braces
        $SOCKET = open_remote_socket(\$SOCKET, $port);
        $STATESOCKET = open_remote_socket(\$STATESOCKET, $port+1);
    }

    MainLoop();

    quit();
}


sub open_remote_socket
{
    my ($socket, $port) = @_;

    my $proto = getprotobyname('tcp');

    socket($$socket, PF_INET, SOCK_STREAM, $proto)                || die "socket: $!";
    setsockopt($$socket, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))  || die "setsockopt: $!";
    bind($$socket, sockaddr_in($port, INADDR_ANY))                || die "bind: $!";
    listen($$socket, SOMAXCONN)                                   || die "listen: $!";

    return ($$socket);
}


sub create_playlist
{
    my ($sth, $dbh, $query, %songlist, @playlist, $id, $next_id, $prev_id, @ids, $field, $column, $db_version);
    my $i = 0;

    $dbh = open_db() || die "Could not open the database";

    $query = "SELECT db_version FROM db_info";
    $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    my $ref = $sth->fetchrow_arrayref;
    $db_version = $$ref[0];
    $sth->finish();

    $query = sprintf("SELECT s.id, s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj, s.year, s.next_id, s.prev_id
                      FROM song s
                      JOIN disc d ON s.discid = d.id
                      JOIN artist a ON s.artistid = a.id
                      WHERE s.disable & '%s' = 0",
                      $opts{disable_bitmask});

    $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    while (my $ref = $sth->fetchrow_arrayref)
    {
        $songlist{$$ref[0]} = [ $$ref[1], $$ref[2], $$ref[3], $$ref[4], $$ref[5], $$ref[6], $$ref[7], $$ref[8], $$ref[9], $$ref[10] ];
    }
    $sth->finish();
    $dbh->disconnect();

    @ids = keys(%songlist);
    srand(time()^($$+($$<<15)));
    while ($i < $opts{songlimit} && scalar (keys %songlist))
    {
        $id = (keys %songlist)[int(rand(scalar @ids))];
        if (defined $songlist{$id})
        {
            $next_id = $songlist{$id}[8];
            $prev_id = $songlist{$id}[9];

            while ($prev_id > 0)
            {
                $id = $prev_id;
                $next_id = $songlist{$id}[8];
                $prev_id = $songlist{$id}[9];
            }
            
            if (defined $songlist{$id})
            {
                $playlist[$i] = $songlist{$id};
                delete ($songlist{$id});
                $i++;

                while ($next_id > 0 && defined $songlist{$next_id})
                {
                    $id = $next_id;
                    $playlist[$i] = $songlist{$id};
                    $next_id = $songlist{$id}[8];
                    delete ($songlist{$id});
                    $i++;
                }
            }
        }
    }
    
    for ($id = 0; $id < (scalar @playlist); $id++)
    {
        for ($field = 0; $field < 8; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert('end',
                                                $field == 4 ? sprintf(" %02d", $playlist[$id][$field])
                                                            : $playlist[$id][$field]);

        }
        if ($playlist[$id][8] > 0 || $playlist[$id][9] > 0)
        {
            for (my $i = 1; $i < 6; $i++)
            {
                $listbox->columnGet($i)->Subwidget("listbox")->itemconfigure('end', -background => 'lightgoldenrodyellow');
            }
        }
    }

    $listbox->selectionSet(0);
}


sub get_prefs
{
    my ($cfgfile, %opts) = @_;
    my (%o, @cfg);

    open(PREFS, $cfgfile);
    @cfg = grep(/^\s*[^#]\S+.*$/, <PREFS>);
    close (PREFS);

    foreach $_ (@cfg)
    {
        if (/^(\S+)\s*[:=]\s+(yes|true)$/i)
        {
            $o{$1} = 1;
        }
        elsif (/^(\S+)\s*[:=]\s+(no|false)$/i)
        {
            $o{$1} = 0;
        }
        elsif (/^(\S+)\s*[:=]\s+"?([^"]+)"\s*$/)        # " reset
        {
            $o{$1} = $2;
        }
        elsif (/^(\S+)\s*[:=]\s+(\S.*\S)\s*$/)
        {
            $o{$1} = $2;
        }
        elsif (/^(\S+)\s*[:=]\s+(\d+)\s*$/)
        {
            $o{$1} = $2;
        }
    }

    foreach my $k (keys %opts)
    {
        $o{$k} = $opts{$k};
    }

    return(%o);
}


sub apply_defaults
{
    my $o = $_[0];
    my $home = $ENV{'HOME'};

    $o->{datadir} = '/usr/share/perljammer' unless (defined $o->{datadir});

    if (defined $o->{scrollbar})
    {
        die "Invalid scrollbar position" unless ($o->{scrollbar} eq 'left' || $o->{scrollbar} eq 'right');
    }
    else
    {
        $o->{scrollbar} = 'left';
    }

    if (defined $o->{action_at_end})
    {
       die "Invalid action_at_end setting" unless ($o->{action_at_end} eq 'stop'
                                                || $o->{action_at_end} eq 'loop'
                                                || $o->{action_at_end} eq 'new'
                                                || $o->{action_at_end} eq 'quit');
    }
    else
    {
        $o->{action_at_end} = 'stop';
    }

    if (defined $o->{autorepeat})
    {
        mv ($cfgfile, "$cfgfile.old");
        write_cfgfile();

        print "The 'autorepeat' directive is obsolete, and has been replaced by\n"
            . "'action_at_end'.  Your config file has been renamed to config.old,\n"
            . "and a new config file has been written for you.  Please update the\n"
            . "new file with your saved settings before restarting PerlJammer.\n";
        quit();
    }

    if (defined $o->{columnPack})
    {
        my @columns = split(/,\s*/, $o->{columnPack});
        unless (scalar @columns == 5
            && $columns[0] eq int($columns[0]) && $columns[0] > 1
            && $columns[1] eq int($columns[1]) && $columns[1] > 1
            && $columns[2] eq int($columns[2]) && $columns[2] > 1
            && $columns[3] eq int($columns[3]) && $columns[3] > 1
            && $columns[4] eq int($columns[4]) && $columns[4] > 1 )
        {
            die "Badly formed columnPack directive";
        }
    }

    if (defined $o->{controls})
    {
        die "Invalid control position" unless ($o->{controls} eq 'top' || $o->{controls} eq 'bottom');
    }
    else
    {
        $o->{controls} = 'bottom';
    }

    if (defined $o->{disable_id})
    {
        die "Disable ID out of range" if ($o->{disable_id} < 0 || $o->{disable_id} > 64);
        die "Invalid disable ID" if ($o->{disable_id} != int($o->{disable_id}));
    }
    else
    {
        $o->{disable_id} = 1;
    }

    $o->{disable_bitmask} = 2**($o->{disable_id}-1) unless (defined $o->{disable_bitmask});

    $o->{font}            = '-adobe-helvetica-medium-r-normal--*-120-*-*-p-*-iso10646-1' unless (defined $o->{font});
    (($o->{boldfont}      = $o->{font}) =~ s/medium/bold/) unless (defined $o->{boldfont});
    (($o->{smallfont}     = $o->{font}) =~ s/120/100/) unless (defined $o->{smallfont});
    $o->{errorfont}       = $o->{boldfont} unless (defined $o->{errorfont});

    $o->{geometry}        = '800x600' unless (defined $o->{geometry});
    $o->{activerow}       = 5 unless (defined $o->{activerow});
    $o->{songlimit}       = 500 unless (defined $o->{songlimit});
    $o->{mp3playercmd}    = '/usr/bin/madplay -Q -G %s' unless (defined $o->{mp3playercmd});
    $o->{oggplayercmd}    = '/usr/bin/ogg123 %s' unless (defined $o->{oggplayercmd});
    $o->{remoteport}      = 16384 unless (defined $o->{remoteport});
    $o->{sqlgroup}        = 'pjam' unless (defined $o->{sqlgroup});
    $o->{useWMicon}       = 0 unless (defined $o->{useWMicon});
    $o->{timebox_padding} = 10 unless (defined $o->{timebox_padding});
    $o->{update_stats}    = 1 unless (defined $o->{update_stats});

    $o->{UIforeground}    = 'MidnightBlue' unless (defined $o->{UIforeground});
    $o->{UIbackground}    = 'MidnightBlue' unless (defined $o->{UIbackground});

    $o->{UIforeground}    = '#666699' if ($o->{UIforeground} eq 'SunBlue');		# Easter egg!
    $o->{UIbackground}    = '#666699' if ($o->{UIbackground} eq 'SunBlue');

    if (defined $o->{sqlcfg})
    {
        $o->{sqlcfg} =~ s/~/$home/;
    }
    else
    {
        $o->{sqlcfg} = $home.'/.my.cnf';
    }
}


sub list_options
{
    my $o = $_[0];

    print "Options:\n";
    foreach my $key (sort keys %$o)
    {
        printf("\t%-16s : %s\n",
               $key,
               $o->{$key});
    }
}


sub open_db
{
    my ($dsn, $dbh, $user, $password);

    $dsn = "DBI:mysql:%s;"
         . "mysql_read_default_group=%s;"
         . "mysql_read_default_file=%s;"
         . "mysql_mysql_client_found_rows=TRUE;"
         . "mysql_mysql_ssl=TRUE";


    $dsn = sprintf($dsn,
                   $opts{sqldb},
                   $opts{sqlgroup},
                   $opts{sqlcfg});

    $dbh = DBI->connect($dsn, undef, undef,
                        {RaiseError => 1, AutoCommit => 1, mysql_enable_utf8 => 1});

    my $sth = $dbh->prepare('SET NAMES utf8mb4') || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    return ($dbh);
}


sub get_sql_data
{
    
    my ($sth, $dbh, $query, @result);

    $query = sprintf("SELECT s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj, s.year
                      FROM song s
                      JOIN disc d ON s.discid = d.id
                      JOIN artist a ON s.artistid = a.id
                      WHERE s.filename = '%s'
                      LIMIT 1",
                     $_[0]);
                      
    $dbh = open_db();
    $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    while (my $ref = $sth->fetchrow_arrayref)
    {
        push(@result, $$ref[0], $$ref[1], $$ref[2], $$ref[3], $$ref[4], $$ref[5], $$ref[6], $$ref[7]);
    }
    $sth->finish();
    $dbh->disconnect();

    return (@result);
}


sub get_mp3_or_ogg_data
{
    my $file = $_[0];
    my @data = get_sql_data($file);
    unless (scalar @data)
    {
        if ($file =~ /\.mp3/)
        {
            my $mp3 = MP3::Tag->new($file);
            $mp3->get_tags;
            my ($title, $track, $artist, $disc, $comment, $year, $genreID) = $mp3->autoinfo;
            my $info = get_mp3info($file);
            my $play_time = sprintf("%02d:%02d", $info->{MM}, $info->{SS});
            push (@data, $file, $title, $artist, $disc, $track, $play_time, $year);
            $mp3->close;
        }
        elsif ($file =~ /\.ogg/)
        {
            my $ogg = Ogg::Vorbis::Header->new($file);
            my $length = $ogg->info('length');
            push (@data, $file,
                  $ogg->comment('title'),
                  $ogg->comment('artist'),
                  $ogg->comment('album'),
                  $ogg->comment('tracknumber'),
                  sprintf("%02d:%02d", $length / 60, $length % 60),
                  $ogg->comment('date'));
        }
    }

    return (@data);
}


sub event_loop
{
    if ($player_pid && -e "/proc/$player_pid/status")
    {
        open(STAT, "/proc/$player_pid/status");
        my $stat = (grep(/^State:/, <STAT>))[0];
        close(STAT);
        if ($stat =~ /zombie/)
        {
            kill ('TERM', $player_pid);
            if ($opts{gainctl})
            {
                if ($opts{resetcmd})
                {
                    reset_volume();
                }
                else
                {
                    my $vol = ($listbox->getRow($playing))[6];
                    change_volume(-$vol) if ($vol != 0);
                }
            }
            $player_pid = 0;
            close($PLAYER);
            clear_selections();

            if ($active)
            {
                if ($playing < $listbox->size()-1)
                {
                    play_song($playing+1);

			# if end action is 'new', we generate a new playlist in the background
			# during the final song of the current playlist

                    new_list() if ($playing == $listbox->size()-1 && $opts{action_at_end} eq 'new');
                }
                else
                {
			# end of playlist reached

                    if ($opts{action_at_end} eq 'loop')
                    {
                        play_song(0);
                    }
                    else   # ($opts{action_at_end} eq 'stop' || $opts{action_at_end} eq 'quit')
                    {
                        $active = $elapsed = 0;
                        $playbutton->configure(-bitmap     => 'playbutton',
                                               -foreground => $opts{UIforeground});

                        quit() if ($opts{action_at_end} eq 'quit');
                    }
                }
            }
            else
            {
                if ($playing < $listbox->size()-1)
                {
                    $listbox->selectionSet($playing+1);
                }
                $playing = -1;
                $elapsed = 0;
                $timebox->Contents('elapsed: 00:00:00');
                $playbutton->configure(-bitmap     => 'playbutton',
                                       -foreground => $opts{UIforeground});
                $waitbutton->configure(-bitmap	   => 'waitbutton',
                                       -foreground => 'black');
            }
        }
    }
    socket_handler() unless ($opts{nonet});
}


sub socket_handler
{
    my $connections = 1;
    my ($rin, $rout, $data);

    while ($connections)
    {
        $rin = '';
        vec($rin, fileno($SOCKET), 1) = 1;
        select($rout = $rin, undef, undef, 0.001);
        if (vec($rout, fileno($SOCKET), 1))
        {
            if (accept(REMOTE, $SOCKET))
            {
                chomp($data = <REMOTE>);

                my @data = split(/ /, $data);
                $data[0] =~ tr/A-Z/a-z/;
                $data = join(' ', @data);
                print STDERR "COMMAND: '$data', PLAYING: $playing\n" if ($opts{debug}>1);

                if ($data[0] eq 'nowplaying' || $data[0] eq 'status')
                {
                    my (@row, $p);
                    $p = ($playing == -1)
                         ? $listbox->curselection()->[0]
                         : $playing;
                    @row = $listbox->getRow($p);
                    print STDERR "PREFETCH: $row[1]\n" if ($opts{debug}>1);

                    if ($active == 0 && !defined $data[1])
                    {
                        print STDERR "CASE 1\t" if ($opts{debug}>1);
                        if ($row[1] =~ /PAUSE PLAYLIST/)
                        {
                            print REMOTE "PAUSED\n";
                        }
                        else
                        {   
                            print REMOTE "Inactive\n";
                        }
                    }
                    elsif ($data[1] == 1 && $active == 0)
                    {
                        print STDERR "CASE 2\t" if ($opts{debug}>1);
                        if ($row[1] =~ /PAUSE PLAYLIST/)
                        {
                            @row = $listbox->getRow($p+1);
                            printf STDERR ("NEXT: %s\n", $row[1]) if ($opts{debug}>1);
                        }
                        $data = sprintf("%s :: %s :: %s (%d, %s)\n",
                                        $row[2],
                                        $row[3],
                                        $row[1],
                                        $row[7],
                                        $row[5]);
                        print REMOTE $data;
                    }
                    elsif ($data[1] > 0)
                    {
                        print STDERR "CASE 3\t" if ($opts{debug}>1);
                        my $p = ($playing == -1)
                                ? $listbox->curselection()->[0]
                                : $playing + 1;

                        for (my $i = 0; $i < $data[1]; $i++)
                        {
                            last if ($p + $i > $listbox->size()-1);
                            @row = $listbox->getRow($p + $i);
                            print STDERR "LOOKAHEAD: $row[1]\n" if ($opts{debug}>1);
                            if ($row[1] =~ /PAUSE PLAYLIST/)
                            {
                                print REMOTE "-- PAUSE PLAYLIST --\n";
                            }
                            else
                            {
                                $data = sprintf("%s :: %s :: %s (%d, %s)\n",
                                                $row[2],
                                                $row[3],
                                                $row[1],
                                                $row[7],
                                                $row[5]);
                                print REMOTE $data;
                            }
                        }
                    }
                    elsif ($playing != -1)
                    {
                        print STDERR "CASE 4\t" if ($opts{debug}>1);
                        if ($row[1] =~ /PAUSE PLAYLIST/)
                        {
                            @row = $listbox->getRow($playing+1);
                            print STDERR "LOOKAHEAD: $row[1]\n" if ($opts{debug}>1);
                        }
                        $data = sprintf("%s :: %s :: %s (%d, %s)\n",
                                        $row[2],
                                        $row[3],
                                        $row[1],
                                        $row[7],
                                        $row[5]);
                        print REMOTE $data;
                    }
                    else
                    {
                        print STDERR "CASE 5\t" if ($opts{debug}>1);
                        if ($row[1] =~ /PAUSE PLAYLIST/)
                        {
                            print REMOTE "PAUSED\n";
                        }
                        else
                        {   
                            print REMOTE "Inactive\n";
                        }
                    }
                    close (REMOTE);
                }
                else
                {
                    close (REMOTE);
                    handle_remote_cmd($data);
                }
            }
        }
        else
        {
            $connections = 0;
        }
    }
}


sub state_loop
{
    my $connections = 1;
    my ($rin, $rout, $data);

    while ($connections)
    {
        $rin = '';
        vec($rin, fileno($STATESOCKET), 1) = 1;
        select($rout = $rin, undef, undef, 0.001);
        if (vec($rout, fileno($STATESOCKET), 1))
        {
            if (accept(STATE, $STATESOCKET))
            {
                printf STATE ("%d %d %d",
                              ($playing == -1) ? 0 : 1,
                              ($playing >= 0 && !$active) ? 1 : 0,
                              $paused);
                close (STATE);
            }
        }
        else
        {
            $connections = 0;
        }
    }
}


sub clear_selections
{
    foreach my $p ($listbox->curselection())
    {
        $listbox->selectionClear($p);
    }
}


sub handle_remote_cmd
{
    my $data = $_[0];
    print "Remote command: $data\n" if ($opts{debug});

    if ($data eq 'quit')
    {
        quit();
    }
    elsif ($data eq 'stop')
    {
        stop();
    }
    elsif ($data eq 'wait')
    {
        stop_after_current();
    }
    elsif ($data eq 'play')
    {
        play();
    }
    elsif ($data eq 'pause')
    {
        pause_play();
    }
    elsif ($data eq 'unpause' || $data eq 'resume')
    {
        resume_play();
    }
    elsif ($data eq 'play_or_pause')
    {
        play_or_pause();
    }
    elsif ($data eq 'next' || $data eq 'next_song')
    {
        next_song();
    }
    elsif ($data eq 'prev' || $data eq 'previous' || $data eq 'prev_song')
    {
        prev_song();
    }
    elsif ($data eq 'restart' || $data eq 'restart_song')
    {
        stop();
        play();
    }
    elsif ($data eq 'first' || $data eq 'first_song' || $data eq 'start')
    {
        first_song();
    }
    elsif ($data eq 'last' || $data eq 'last_song' || $data eq 'end')
    {
        last_song();
    }
    elsif ($data eq 'delete' || $data eq 'drop' || $data eq 'drop_song')
    {
        drop_song();
    }
    elsif ($data eq 'disable' || $data eq 'disable_song')
    {
        disable_song();
    }
    elsif ($data eq 'disc' || $data eq 'play_disc')
    {
        insert_disc();
    }
    elsif ($data eq 'insert_next')
    {
        insert_next();
    }
    elsif ($data eq 'new_list')
    {
        new_list();
    }
    elsif ($data =~ /^insert\s+(.+)$/)
    {
        my @list = split(/\s+/, $1);
        add_song(1, @list) if (scalar @list);
    }
    elsif ($data =~ /^instant\s+(.+)$/)
    {
        my @list = split(/\s+/, $1);
        add_song(0, @list) if (scalar @list);
    }
    elsif ($data =~ /^replace\s+(\S+)$/)
    {
        replace_song($1);
    }

    $data = '';
}
    

sub play_song
{
    my $sel = $_[0];
    my ($cmd, $player, @row);

    clear_selections();
    @row = $listbox->getRow($sel);
    printf ("play_song(): New row is '%s'\n", $row[0]) if ($opts{debug});
        
    if ($row[0] eq '')
    {
        suspend($sel);
    }
    else
    {
        
        $cmd = ($row[0] =~ /.mp3$/)
             ? sprintf($opts{mp3playercmd}, $row[0])
             : ($row[0] =~ /.ogg$/)
             ? sprintf($opts{oggplayercmd}, $row[0])
             : '';                      # This SHOULDN'T ever happen

        if (length($cmd))
        {
            ($player = $cmd) =~ s/^(\S+)\s.*$/$1/;

            if ($player_pid)
            {
                kill ('TERM', $player_pid);
            }

            $listbox->selectionSet($sel);
            $listbox->yview($sel < $opts{activerow} ? 0 : $sel - $opts{activerow});
            $playing = $sel;
            $elapsed = 0;

            if ($opts{timebox_countdown})
            {
                my $time = $row[5];
                my @times = split(/:/, $time);
                for (my $i = 0; $i < scalar @times; $i++)
                {
                    $elapsed += $times[$i] * 60**(scalar @times - ($i+1));
                }
            }

            if ($opts{gainctl})
            {
                my $vol = $row[6];
                change_volume($vol) if ($vol != 0);
            }

            if ($opts{update_stats})
            {
                update_play_stats();
            }

            if ($opts{debug}>2)
            {
                printf("Playing index %d :: %s\n",
                       $sel,
                       join(" : ",
                            $row[2],
                            $row[3],
                            $row[1]));
            }
            elsif ($opts{debug})
            {
                print "Playing index $playing\n";
            }
            $player_pid = open($PLAYER, "|$cmd");
        }
        else
        {
            # Need to display a "No player for this file type" message here
        }
    }
}


sub play_or_pause
{
    if ($player_pid > 0)
    {
        if ($paused)
        {
            kill ('CONT', $player_pid);
            $paused = 0;
            $playbutton->configure(-bitmap     => 'pausebutton',
                                   -foreground => $opts{UIforeground});
            $timebox->configure(-foreground => 'blue');
        print "Resuming index $playing\n" if ($opts{debug});
        }
        else
        {
            kill ('STOP', $player_pid);
            $paused = 1;
            $playbutton->configure(-bitmap     => 'playbutton',
                                   -foreground => 'red');
            $timebox->configure(-foreground => 'red');
        print "Paused at index $playing\n" if ($opts{debug});
        }
    }
    else
    {
        $active = 1;
        play_song($listbox->curselection()->[0]);
        $playbutton->configure(-bitmap     => 'pausebutton',
                               -foreground => $opts{UIforeground});
    }
}


sub play
{
    if ($paused)
    {
        kill ('CONT', $player_pid);
        $paused = 0;
        $timebox->configure(-foreground => 'blue');
    }
    else
    {
        $active = 1;
        my $s = $listbox->curselection()->[0];
        if ($s eq '')
        {
            suspend();
        }
        else
        {
            play_song($s);
        }
    }

    $playbutton->configure(-bitmap     => 'pausebutton',
                           -foreground => $opts{UIforeground});
}


sub stop
{
    if ($playing != -1)
    {
        resume_play() if ($paused);
        kill ('TERM', $player_pid);
        if ($opts{gainctl})
        {
            if ($opts{resetcmd})
            {
                reset_volume();
            }
            else
            {
                my $vol = ($listbox->getRow($playing))[6];
                change_volume(-$vol) if ($vol != 0);
            }
        }

        close($PLAYER);
        print "Stopped playing index $playing\n" if ($opts{debug});
        $player_pid = 0;
        $active = 0;
        $playing = -1;
        $playbutton->configure(-bitmap     => 'playbutton',
                               -foreground => $opts{UIforeground});
        $waitbutton->configure(-bitmap     => 'waitbutton',
                               -foreground => 'black');
        $timebox->Contents('elapsed: 00:00:00');
    }
}


sub suspend
{
    my $index = $_[0];
    
    print "Suspend() called: Breakpoint at index $playing\n" if ($opts{debug});
    kill ('TERM', $player_pid) if ($player_pid);
    $player_pid = 0;
    $active = 0;
    $playing = -1;
    $timebox->Contents('elapsed: 00:00:00');

    clear_selections();
    print "Selection cleared\n" if ($opts{debug});
    
    if ($index < $listbox->size()-2)
    {
        $listbox->selectionSet($index+1);
        printf ("Selection set to %d\n", $index+1) if ($opts{debug});
    }
    
    $listbox->yview($index < ($opts{activerow}-1) ? 0 : $index - ($opts{activerow}-1));
    $playbutton->configure(-bitmap     => 'playbutton',
                           -foreground => $opts{UIforeground});
    $waitbutton->configure(-bitmap     => 'waitbutton',
                           -foreground => 'black');
    $timebox->Contents('elapsed: 00:00:00');
}


sub stop_after_current
{
    $active = 0;
    $waitbutton->configure(-bitmap	=> 'waitbutton2',
                           -foreground	=> 'red');
    print "Playing index $playing until completion\n" if ($opts{debug});
}


sub pause_play
{
    if ($playing != -1 && !$paused)
    {
        kill ('STOP', $player_pid);
        $paused = 1;
        $playbutton->configure(-bitmap     => 'playbutton',
                               -foreground => 'red');
        $timebox->configure(-foreground => 'red');
        print "Paused at index $playing\n" if ($opts{debug});
    }
}


sub resume_play
{
    if ($paused)
    {
        kill ('CONT', $player_pid);
        $paused = 0;
        $playbutton->configure(-bitmap => 'pausebutton',
                               -foreground => $opts{UIforeground});
        $timebox->configure(-foreground => 'blue');
        print "Resuming index $playing\n" if ($opts{debug});
    }
}


sub prev_song
{
    my $p = $player_pid > 0
          ? $playing
          : $listbox->curselection()->[0];
    if ($p > 0)
    {
        my $a = ($player_pid > 0);
        stop() if ($a);
        clear_selections();
        $listbox->selectionSet($p-1);
        $listbox->yview($p < ($opts{activerow}+1) ? 0 : $p - ($opts{activerow}+1));
        play() if ($a);
    }
}


sub next_song
{
    my $p = $player_pid > 0
          ? $playing
          : $listbox->curselection()->[0];
    if ($p < $listbox->size()-1)
    {
        my $a = ($player_pid > 0);
        stop() if ($a);
        clear_selections();
        $listbox->selectionSet($p+1);
        $listbox->yview($p < ($opts{activerow}-1) ? 0 : $p - ($opts{activerow}-1));
        play() if ($a);
    }
}


sub first_song
{
    clear_selections();
    my $a = ($player_pid > 0);
    stop() if ($a);
    $listbox->selectionSet(0);
    $listbox->see(0);
    play() if ($a);
}


sub last_song
{
    clear_selections();
    my $a = ($player_pid > 0);
    stop() if ($a);
    $listbox->selectionSet('end');
    $listbox->see('end');
    play() if ($a);
}


sub drop_song
{
    my $p = $listbox->curselection()->[0];
    my $a = ($p == $playing && $player_pid > 0);
    stop() if ($a);
    $listbox->selectionClear($p);
    $listbox->delete($p);
    $listbox->selectionSet($p == $listbox->size() ? 'end' : $p);
    if ($a)
    {
        $listbox->yview($p < $opts{activerow} ? 0 : $p - $opts{activerow});
        play();
    }
    elsif ($p < $playing)
    {
        $playing--;
        print "Index adjusted to $playing\n" if ($opts{debug});
    }
}


sub disable_song
{
    my $p = $listbox->curselection()->[0];
    my $a = ($p == $playing && $player_pid > 0);
    stop() if ($a);

    my $filename = ($listbox->getRow($p))[0];

    my $dbh = open_db();
    $dbh->do(sprintf("UPDATE song SET disable = (disable | b'%s') WHERE filename = '%s'",
                     $opts{disable_bitmask},
                     $filename));
    $dbh->do('COMMIT');
    $dbh->disconnect();

    $listbox->selectionClear($p);
    $listbox->delete($p);
    $listbox->selectionSet($p == $listbox->size() ? 'end' : $p);
    if ($a)
    {
        $listbox->yview($p < $opts{activerow} ? 0 : $p - $opts{activerow});
        play();
    }
    elsif ($p < $playing)
    {
        $playing--;
        print "Index adjusted to $playing\n" if ($opts{debug});
    }
}


sub replace_song
{
    my $file = $_[0];
    my @data = get_mp3_or_ogg_data($file);

    my $p = $listbox->curselection()->[0];
    my $l = $listbox->size()-1;
    my $a = ($p == $playing);
    stop() if ($a);
    clear_selections();
    $listbox->delete($p);
    for (my $field = 0; $field < 8; $field++)
    {
        $listbox->columnGet($field)->Subwidget("listbox")
                                   ->insert($p < $l ? $p : 'end',
                                            $field == 4 ? sprintf(" %02d", $data[$field])
                                                        : $data[$field]);
    }
    $listbox->selectionSet($p == $listbox->size() ? 'end' : $p);
    $listbox->yview($p < $opts{activerow} ? 0 : $p - $opts{activerow});
    play() if ($a);
}


sub add_song
{
    my $offset = shift(@_);
    my ($file, @rows, @data, $a, $p, $pp);

    foreach $file (@_)
    {
        if ($file eq 'STOP')
        {
            push (@rows, ['','    ---------- PAUSE PLAYLIST ----------','','','','','','','']);
        }
        else
        {
            push (@rows, [get_mp3_or_ogg_data($file)]) if (-e $file);
        }
    }

    $p = ($listbox->curselection()->[0]);
    $pp = $p + $offset;
    $a = ($pp == $playing);

    stop() if ($a);
    clear_selections();

    for (my $i = 0; $i <scalar @rows; $i++)
    {
        for (my $field = 0; $field < 8; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($pp < $listbox->size()-1 
                                                    ? $pp + $i
                                                    : 'end',
                                                $field == 4 ? sprintf(" %02d", $rows[$i][$field])
                                                            : $rows[$i][$field]);
            if ($rows[$i][0] eq '')
            {
                $listbox->columnGet($field)->Subwidget("listbox")->itemconfigure($pp < $listbox->size()-1
                                                                                     ? $pp + $i
                                                                                     : 'end',
                                                                                 -background => 'black', 
                                                                                 -foreground => 'gold');
            }
        }
    }


    $listbox->selectionSet($p);
    $listbox->yview($p < $opts{activerow} ? 0 : $p - $opts{activerow});

    if ($a)
    {
        play()
    }
    elsif ($p < $playing)
    {
        $playing++;
        print "Index adjusted to $playing\n" if ($opts{debug});
    }
}


sub insert_breakpoint
{
    my $p = ($listbox->curselection()->[0]);
    my @row = ('','    ---------- PAUSE PLAYLIST ----------','','','','','','','');
    
    for (my $i = 0; $i < 8; $i++)
    {
        $listbox->columnGet($i)->Subwidget("listbox")
                               ->insert($p+1,
                                        $i == 4 ? sprintf(" %02d", $row[$i])
                                                : $row[$i]);
        $listbox->columnGet($i)->Subwidget("listbox")->itemconfigure($p+1, -background => 'black', -foreground => 'gold');
    }
}


sub insert_next
{
    my ($sth, $dbh, $query, $id, @ids, $field,
        @row, $p);
   
    $p = ($listbox->curselection()->[0]);
    @row = $listbox->getRow($p);

    $query = sprintf("SELECT s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj, s.year
                      FROM song as s, disc as d, artist as a
                      WHERE s.artistid = a.id
                      AND s.discid = d.id
                      AND a.artist = \"%s\"
                      AND d.disc = \"%s\"
                      AND track_num = %d",
                     $row[2],
                     $row[3],
                     $row[4]+1);

    $dbh = open_db();
    $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    if (my $ref = $sth->fetchrow_arrayref)
    {
        for ($field = 0; $field < 8; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($p+1,
                                                $field == 4 ? sprintf(" %02d", $$ref[$field])
                                                            : $$ref[$field]);
        }

        clear_selections();
        $listbox->selectionSet($p+1);
        if ($p < $playing)
        {
            $playing++;
            print "Index adjusted to $playing\n" if ($opts{debug});
        }
    }

    $sth->finish();
    $dbh->disconnect();
}


sub insert_disc
{
    my ($sth, $dbh, $query, $id, @ids, $field,
        @row, $seamless, $i, $p, $active);
   
    $p = ($listbox->curselection()->[0]);
    $active = ($player_pid > 0 && $p == $playing);

    @row = $listbox->getRow($p);

    if ($active && ($listbox->getRow($p))[4] == 1)
    {
        $seamless = 1;
    }
    else
    {
        stop() if ($active);
        $seamless = 0;
        drop_song();
        clear_selections();
    }

    $query = sprintf("SELECT s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj, s.year
                      FROM song s
                      JOIN disc d ON s.discid = d.id
                      JOIN artist a ON s.artistid = a.id
                      WHERE a.artist = \"%s\"
                      AND d.disc = \"%s\"
                      ORDER BY s.track_num",
                     $row[2],
                     $row[3]);

    $dbh = open_db();
    $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    $i = 0;
    while (my $ref = $sth->fetchrow_arrayref)
    {
        next if ($seamless && $$ref[4] == 1);
        for ($field = 0; $field < 8; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($p + $i + $seamless,
                                                $field == 4 ? sprintf(" %02d", $$ref[$field])
                                                            : $$ref[$field]);
        }
        $i++;
        if ($p + $i + $seamless < $playing)
        {
            $playing++;
            print "Index adjusted to $playing\n" if ($opts{debug});
        }
    }
    $sth->finish();
    $dbh->disconnect();

    $listbox->selectionSet($p);
    $listbox->yview($p < $opts{activerow} ? 0 : $p - $opts{activerow});
    play() if ($active && !$seamless);
}


sub link_to_next
{
    my ($sth, $dbh, $query, $id, @ids, $field,
        @row, @row2, $p, $ref, $this_id, $next_id);
   
    $dbh = open_db();

    $p = ($listbox->curselection()->[0]);
    @row = $listbox->getRow($p);
    @row2 = $listbox->getRow($p+1);

    $query = sprintf("SELECT s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj, s.year, s.id
                      FROM song s
                      JOIN disc d ON s.discid = d.id
                      JOIN artist a ON s.artistid = a.id
                      WHERE a.artist = \"%s\"
                      AND d.disc = \"%s\"
                      AND s.track_num = %d",
                     $row[2],
                     $row[3],
                     $row[4]);

    $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    if ($ref = $sth->fetchrow_arrayref)
    {
        $this_id = $$ref[8];
        $sth->finish();

        $query = sprintf("SELECT s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj, s.year, s.id
                          FROM song s
                          JOIN disc d ON s.discid = d.id
                          JOIN artist a ON s.artistid = a.id
                          WHERE a.artist = \"%s\"
                          AND d.disc = \"%s\"
                          AND s.track_num = %d",
                         $row2[2],
                         $row2[3],
                         $row2[4]);
        
        $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
        $sth->execute || die "Error:" . $dbh->errstr . "\n";

        if ($ref = $sth->fetchrow_arrayref)
        {
            $next_id = $$ref[8];
            $sth->finish();

            $query = sprintf("UPDATE song
                              SET next_id = %d
                              WHERE id = %d",
                             $next_id,
                             $this_id);

            $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
            $sth->execute || die "Error:" . $dbh->errstr . "\n";
            $sth->finish();

            $query = sprintf("UPDATE song
                              SET prev_id = %d
                              WHERE id = %d",
                             $this_id,
                             $next_id);

            $sth = $dbh->prepare($query) || die "Error:" . $dbh->errstr . "\n";
            $sth->execute || die "Error:" . $dbh->errstr . "\n";
            $sth->finish();
            $dbh->do("COMMIT");

            for (my $i = 1; $i < 6; $i++)
            {
                $listbox->columnGet($i)->Subwidget("listbox")->itemconfigure($p, -background => 'lightgoldenrodyellow');
                $listbox->columnGet($i)->Subwidget("listbox")->itemconfigure($p+1, -background => 'lightgoldenrodyellow');
            }
        }
    }

    $dbh->disconnect();
}


sub new_list
{
    my $p = $listbox->curselection()->[0];
    if ($active)
    {
        $listbox->delete($playing+1, 'end') if ($playing < $listbox->size()-1);
        $listbox->delete(0, $playing-1) if ($playing > 0);
        $playing = 0;
        print "Index adjusted to $playing\n" if ($opts{debug});
    }
    else
    {
        $listbox->delete(0,'end');
    }

    clear_selections();
    create_playlist();
    $listbox->see(0);
}


sub update_play_stats
{
    my (@rowdata, $dbh);

    @rowdata = $listbox->getRow($playing);

    $dbh = open_db() || print "Could not open DB\n";
    $dbh->do(sprintf("UPDATE song SET num_plays = COALESCE(num_plays, 0)+1, last_play = NOW() WHERE filename = '%s'",
                     $rowdata[0]));
    $dbh->do('COMMIT');
    $dbh->disconnect();
}


sub change_gain
{
    my $change = $_[0];

    if ($playing >= 0)
    {
        my (@rowdata, $sel, $dbh);

        change_volume($change);
        $sel = $listbox->curselection()->[0];
        @rowdata = $listbox->getRow($playing);
        $rowdata[6] += $change;

        # There is apparently no way to set a single field; we have
        # to delete the entire row and re-insert the new values

        $listbox->delete($playing);
        my $l = $listbox->size()-1;
        for (my $field = 0; $field < 7; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($playing < $l ? $playing : 'end',
                                                $field == 4 ? sprintf(" %02d", $rowdata[$field])
                                                            : $rowdata[$field]);
        }
        $listbox->selectionSet($sel);

        $dbh = open_db() || print "Could not open DB\n";
        $dbh->do(sprintf("UPDATE song SET gain_adj = %d WHERE filename = '%s'",
                         ($listbox->getRow($playing))[6],
                         $rowdata[0]));
        $dbh->do('COMMIT');
        $dbh->disconnect();
    }
}


sub change_volume
{
    my $steps = $_[0];

    system(split(/\s+/, sprintf($opts{volumecmd}, $steps * $opts{stepsize})));
}


sub reset_volume
{
    system(split(/\s+/, $opts{resetcmd}));
}


sub increase_gain
{
    change_gain(1);
}


sub decrease_gain
{
    change_gain(-1);
}


###  End of action functions
###############################################
###  Interface maintenance functions begin here


sub display_elapsed
{
    my ($t, $m);

    if (!($playing == -1 || $paused))
    {
        if ($opts{timebox_countdown})
        {
            $elapsed--;
            $m = 'remains';
        }
        else
        {
            $elapsed++;
            $m = 'elapsed';
        }
        $t = sprintf("%s: %02d:%02d:%02d",
                     $m,
                     int($elapsed/3600),
                     int($elapsed/60) - 60*int($elapsed/3600),
                     $elapsed % 60);

        $timebox->Contents($t);
    }
}


sub listbox_click_event
{
    my ($w, $infoHR) = @_;
    $click_row = $infoHR->{-row};
    my @row = $listbox->getRow( $listbox->curselection()->[0]);

    printf("You selected index %d :: %s\n",
           $click_row,
           join(" : ",
                $row[2],
                $row[3],
                $row[1])) if ($opts{debug}>2);
}


sub listbox_dclick_event
{
    my ($w, $infoHR) = @_;
    my @row = $listbox->getRow( $listbox->curselection()->[0]);
    stop() if ($paused);
    printf("Playing %s\n",
           $infoHR->{-row},
           join(" : ",
                $row[2],
                $row[3],
                $row[1])) if ($opts{debug});
    $paused = 0;
    $active = 1;
    play_song($infoHR->{-row});
}


sub listbox_drag_event
{
    my ($w, $infoHR) = @_;
    $drag_start = $click_row unless ($dragging);
    $dragging = 1;
    my $now = $infoHR->{-row};
    if ($now != $drag_start)
    {
        drag_from($drag_start, $now);
        if ($playing != -1)
        {
            if ($drag_start > $playing && $now == $playing)
            {
                $playing++;
            }
            elsif ($drag_start < $playing && $now == $playing)
            {
                $playing--;
            }
            elsif ($drag_start == $playing)
            {
                $playing = $now;
            }
            print "Index adjusted to $playing\n" if ($opts{debug});
        }
        $drag_start = $now;
    }
}


sub listbox_button_up_event
{
    my ($w, $infoHR) = @_;

    $dragging = 0;
}


sub drag_from
{
    my ($start, $end) = @_;
    my (@dragged, @entry, $color);

    $color = $listbox->columnGet(1)->Subwidget("listbox")->itemcget($start, -background);
    $listbox->columnGet(0)->Subwidget("listbox")->selectionClear($end);

    @dragged = $listbox->getRow($start);
    printf("You dragged %s from row %d to row %d\n",
           join(" : ",
                $dragged[2],
                $dragged[3],
                $dragged[1]),
           $start,
           $end) if ($opts{debug}>2);
    $listbox->delete($start, $start);
    $listbox->insert($end, [ @dragged ]);

    if ($color eq 'lightgoldenrodyellow')
    {
        for (my $i = 1; $i < 6; $i++)
        {
            $listbox->columnGet($i)->Subwidget("listbox")->itemconfigure($end,
                                                                         -background => $color);
        }
    }
    elsif ($color eq 'black')
    {
        for (my $i = 1; $i < 6; $i++)
        {
            $listbox->columnGet($i)->Subwidget("listbox")->itemconfigure($end,
                                                                         -background => $color,
                                                                         -foreground => 'gold');
        }
    }
    $listbox->columnGet(0)->Subwidget("listbox")->selectionSet($end);
}


### User interface code begins here



sub create_UI
{
    my ($stopbutton, $resumebutton, $prevbutton, $nextbutton,
        $firstbutton, $lastbutton, $dropbutton, $killbutton,
        $newbutton, $discbutton, $quitbutton, $helpbutton,
        $upbutton, $downbutton, $addnextbutton, $linkbutton,
        @bar_top, @bar_bottom, @list_top, @list_bottom,
        @widths, @sizes, $width, $i);

    $app = MainWindow->new();
    $app -> geometry($opts{geometry});
    $app -> configure(-background => 'white');
    $app -> title("PerlJammer");
    $app -> formGrid(68,30);

    unless ($opts{useWMicon})
    {
        $app -> Icon(-image => $app->Pixmap(-file => $iconfile));
    }

    create_button_bitmaps($app);

    if ($opts{controls} eq 'top')
    {
        @bar_top = ['%0',2];
        @bar_bottom = ['%0',36];
        @list_top = ['%0',38];
        @list_bottom = ['%30',-2];
    }
    else
    {
        @bar_top = ['%30',-36];
        @bar_bottom = ['%30',-2];
        @list_top = ['%0',2];
        @list_bottom = ['%30',-38];
    }

    ($listbox = $app->Scrolled(qw/MListbox -background white
                                           -selectbackground LightSkyBlue
                                           -separatorcolor grey90
                                           -separatorwidth 2
                                           -selectmode browse
                                           -moveable 0
                                           -sortable 0/)) -> form(-in		=> $app,
                                                                  -fill		=> 'both',
                                                                  -left		=> ['%0',2],
                                                                  -right	=> ['%68',-2],
                                                                  -top		=> @list_top,
                                                                  -bottom	=> @list_bottom);

    $listbox->configure(-scrollbars => $opts{scrollbar} eq 'left' ? 'w' : 'e');

    $listbox->columnInsert('end', -text => 'Filename', -font => $opts{font});
    $listbox->columnHide(0,0);


    if ($opts{columnPack})
    {
        # We found user-specified column packing, so use it.
        my @width = split(/,\s*/, $opts{columnPack});
        
        $listbox->columnInsert('end', -text => 'Title',    -font => $opts{font}, -width => $width[0]);
        $listbox->columnInsert('end', -text => 'Artist',   -font => $opts{font}, -width => $width[1]);
        $listbox->columnInsert('end', -text => 'Disc',     -font => $opts{font}, -width => $width[2]);
        $listbox->columnInsert('end', -text => 'Track',    -font => $opts{font}, -width => $width[3]);
        $listbox->columnInsert('end', -text => 'Time',     -font => $opts{font}, -width => $width[4]);
        
    }
    elsif ($width > 1500)
    {
        # This column-packing code calculates reasonable-seeming column
        # widths in pixels, but listbox->columnPack() only vaguely honors
        # them.  Column 4 will always be grossly over-wide, and trying to
        # make column 4 narrower will instead make column *5* narrower.
        
        my $geom = $app->cget('-geometry');     # this call appears not to work
        print "Detected geometry: $geom\n" if ($opts{debug});
        $width = $1 if ($opts{geometry} =~ /^(\d+)x\d+/);
        print "Width: $width\n" if ($opts{debug});

        @widths = (0.33, 0.22, 0.32, 0.02, 0.02);
        $width -= 25;
        for ($i = 0; $i < 5; $i++)
        {
            $sizes[$i] = sprintf("%d:%d",
                                $i+1,
                                int($width * $widths[$i]));
            printf("Column %s\n",$sizes[$i]) if ($opts{debug});
        }
    
        $listbox->columnInsert('end', -text => 'Title',    -font => $opts{font});
        $listbox->columnInsert('end', -text => 'Artist',   -font => $opts{font});
        $listbox->columnInsert('end', -text => 'Disc',     -font => $opts{font});
        $listbox->columnInsert('end', -text => 'Track',    -font => $opts{font});
        $listbox->columnInsert('end', -text => 'Time',     -font => $opts{font});
        $listbox->columnPack(@sizes);
    }
    else
    {
        # For windows <1500 pixels wide, columnPack doesn't work worth shit,
        # so we have to hardcode the column widths if the user didn't specify.

        $listbox->columnInsert('end', -text => 'Title',    -font => $opts{font}, -width => 36);
        $listbox->columnInsert('end', -text => 'Artist',   -font => $opts{font}, -width => 25);
        $listbox->columnInsert('end', -text => 'Disc',     -font => $opts{font}, -width => 36);
        $listbox->columnInsert('end', -text => 'Track',    -font => $opts{font}, -width => 4);
        $listbox->columnInsert('end', -text => 'Time',     -font => $opts{font}, -width => 5);
    }
    
    $listbox->columnInsert('end', -text => 'Gain', -font => $opts{font});
    $listbox->columnInsert('end', -text => 'Year', -font => $opts{font});
    $listbox->columnHide(7,7);
    $listbox->columnHide(6,6);


    $listbox->bindRows('<ButtonPress-1>', \&listbox_click_event); 
    $listbox->bindRows('<Double-1>', \&listbox_dclick_event);
    $listbox->bindRows('<B1-Motion>', \&listbox_drag_event); 
    $listbox->bindRows('<ButtonRelease-1>', \&listbox_button_up_event); 


    for (my $i = 0; $i<6; $i++)
    {
        $listbox->columnGet($i)->Subwidget("heading")
                               ->configure(-background=>'gainsboro',
                                           -font => $opts{font});
    }

    ($helpbutton = $app->Button(-text		=> 'Help',
                                -font		=> $opts{boldfont},
                                -command	=> sub { help(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%0',1],
                                                               -right	=> ['%4',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($timebox = $app->ROText(-width		=> 30, 
                             -height		=> 2,
                             -padx		=> $opts{timebox_padding}, 
                             -pady		=> 0, 
                             -relief		=> 'groove',
                             -foreground	=> 'blue', 
                             -background	=> 'white', 
                             -selectforeground	=> 'blue', 
                             -selectbackground	=> 'white', 
                             -font		=> $opts{smallfont},
                             -wrap		=> 'none')) -> form(-in		=> $app,
                                                                    -fill	=> 'both',
                                                                    -left	=> ['%5',1],
                                                                    -right	=> ['%15',-1],
                                                                    -top	=> @bar_top,
                                                                    -bottom	=> @bar_bottom);

    $timebox->Contents('elapsed: 00:00:00');


    ($firstbutton = $app->Button(-bitmap	=> 'startbutton',
                                 -command	=> sub { first_song(); },
                                 -background	=> 'gainsboro',
                                 -foreground	=> $opts{UIforeground},
                                 -activeforeground	=> 'green2',
                                 -anchor	=> 'center',
                                 -relief	=> 'raised',
                                 -width		=> 2,
                                 -height	=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%16',1],
                                                               -right	=> ['%19',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($prevbutton = $app->Button(-bitmap		=> 'prevbutton',
                                -command	=> sub { prev_song(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%19',1],
                                                               -right	=> ['%22',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($playbutton = $app->Button(-bitmap		=> 'playbutton',
                                -command	=> sub { play_or_pause(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%22',1],
                                                               -right	=> ['%25',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($stopbutton = $app->Button(-bitmap		=> 'stopbutton',
                                -command	=> sub { stop(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'black',
                                -activeforeground	=> 'red',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%25',1],
                                                               -right	=> ['%28',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($waitbutton = $app->Button(-bitmap		=> 'waitbutton',
                                -command	=> sub { stop_after_current(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'black',
                                -activeforeground	=> 'red',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%28',1],
                                                               -right	=> ['%31',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($nextbutton = $app->Button(-bitmap		=> 'nextbutton',
                                -command	=> sub { next_song(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%31',1],
                                                               -right	=> ['%34',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($lastbutton = $app->Button(-bitmap		=> 'endbutton',
                                -command	=> sub { last_song(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%34',1],
                                                               -right	=> ['%37',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);


    if ($opts{gainctl})
    {
        ($upbutton = $app->Button(-bitmap		=> 'upbutton',
                                  -command		=> sub { increase_gain(); },
                                  -background		=> 'yellow',
                                  -foreground		=> $opts{UIforeground},
                                  -activebackground	=> 'black',
                                  -activeforeground	=> 'green2',
                                  -anchor		=> 'center',
                                  -relief		=> 'raised',
                                  -width		=> 2,
                                  -height		=> 1)) -> form(-in	=> $app,
                                  				       -fill	=> 'both',
                                    				       -left	=> ['%37',9],
                                    				       -right	=> ['%40',-9],
                                    				       -top	=> $opts{controls} eq 'top'
 										 ? ['%0',2]
 										 : ['%30',-25],
                                    				       -bottom	=> $opts{controls} eq 'top'
 										 ? ['%0',14]
 										 : ['%30',-13]);

        ($downbutton = $app->Button(-bitmap		=> 'downbutton',
                                    -command		=> sub { decrease_gain(); },
                                    -background		=> 'yellow',
                                    -foreground		=> $opts{UIforeground},
                                    -activebackground	=> 'black',
                                    -activeforeground	=> 'green2',
                                    -anchor		=> 'center',
                                    -relief		=> 'raised',
                                    -width		=> 2,
                                    -height		=> 1)) -> form(-in	=> $app,
                                    				       -fill	=> 'both',
                                    				       -left	=> ['%37',9],
                                    				       -right	=> ['%40',-9],
                                    				       -top	=> $opts{controls} eq 'top'
										 ? ['%0',15]
										 : ['%30',-14],
                                    				       -bottom	=> $opts{controls} eq 'top'
 										 ? ['%0',25]
										 : ['%30',-2]);
    }


    ($addnextbutton = $app->Button(-bitmap	=> 'breakpoint',
                                   -command	=> sub { insert_breakpoint(); },
                                   -background	=> 'gainsboro',
                                   -foreground	=> $opts{UIforeground},
                                   -activeforeground	=> 'red',
                                   -anchor	=> 'center',
                                   -relief	=> 'raised',
                                   -width	=> 2,
                                   -height	=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%40',1],
                                                               -right	=> ['%43',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);


    ($addnextbutton = $app->Button(-bitmap	=> 'addnextbutton',
                                   -command	=> sub { insert_next(); },
                                   -background	=> 'gainsboro',
                                   -foreground	=> $opts{UIforeground},
                                   -activeforeground	=> 'green2',
                                   -anchor	=> 'center',
                                   -relief	=> 'raised',
                                   -width	=> 2,
                                   -height	=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%43',1],
                                                               -right	=> ['%46',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);


    ($discbutton = $app->Button(-bitmap		=> 'discbutton',
                                -command	=> sub { insert_disc(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%46',1],
                                                               -right	=> ['%49',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);


    ($linkbutton = $app->Button(-bitmap		=> 'linkbutton',
                                -command	=> sub { link_to_next(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'gold3',
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%49',1],
                                                               -right	=> ['%52',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);


    ($dropbutton = $app->Button(-bitmap		=> 'dropbutton',
                                -command	=> sub { drop_song(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'black',
                                -activeforeground	=> 'red',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%52',1],
                                                               -right	=> ['%55',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($killbutton = $app->Button(-bitmap		=> 'killbutton',
                                -command	=> sub { confirm_disable(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'black',
                                -activeforeground	=> 'red',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%55',1],
                                                               -right	=> ['%58',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($newbutton = $app->Button(-text		=> 'NEW',
                                -font		=> $opts{boldfont},
                                -command	=> sub { new_list(); },
                                -background	=> 'gainsboro',
                                -foreground	=> $opts{UIforeground},
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%58',1],
                                                               -right	=> ['%62',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    ($quitbutton = $app->Button(-bitmap		=> 'quitbutton',
                                -command	=> sub { quit(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'black',
                                -activeforeground	=> 'red',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%64',1],
                                                               -right	=> ['%68',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

    $app->bind('<Alt-h>'	=> \&help);
    $app->bind('<Alt-s>'	=> \&stop);
    $app->bind('<Alt-n>'	=> \&new_list);
    $app->bind('<Home>'		=> \&first_song);
    $app->bind('<Left>'		=> \&prev_song);
    $app->bind('<space>'	=> \&play_or_pause);
    $app->bind('<Right>'	=> \&next_song);
    $app->bind('<End>'		=> \&last_song);
    $app->bind('<Delete>'	=> \&drop_song);
    $app->bind('<Up>'		=> \&increase_gain);
    $app->bind('<Down>'		=> \&decrease_gain);
    $app->bind('<Alt-q>'	=> \&quit);
    $app->bind('<Control-q>'	=> \&quit);
}


sub help
{
    my ($dialog, $title, $helptext, $okbutton, $credit, $timer);
    $dialog = MainWindow->new();
    $dialog -> title("About PerlJammer");
    $dialog -> configure(-background => 'white');
    $dialog -> geometry("600x500");
    $dialog -> formGrid(10,10);

    ($helptext = $dialog->Scrolled('ROText',
                                   -scrollbars		=> 'oe',
                                   -width		=> 30, 
                                   -height		=> 2,
                                   -padx		=> 5, 
                                   -pady		=> 2, 
                                   -relief		=> 'flat',
                                   -spacing3		=> 10,
                                   -foreground		=> $opts{UIforeground}, 
                                   -background		=> 'white', 
                                   -selectforeground	=> 'blue', 
                                   -selectbackground	=> 'white', 
                                   -font		=> $opts{font},
                                   -wrap		=> 'word')) -> form(-in		=> $dialog,
                                                                            -fill	=> 'both',
                                                                            -left	=> ['%0',5],
                                                                            -right	=> ['%10',-5],
                                                                            -top	=> ['%0',5],
                                                                            -bottom	=> ['%10',-70]);

    $helptext->insert('1.0', sprintf(
"This is version %s of PerlJammer, a Perl replacement for Mike Oliphant's abandoned MP3 jukebox application"
." DigitalDJ.  It generates and plays random playlists from a DDJ-compatible music database, and accepts remote"
." control commands via its network socket.  A pjam-remote companion program is provided for sending remote"
." commands via the network.  A new playlist is randomly generated each time PerlJammer is started.\n"
."PerlJammer can dynamically modify the generated playlist while playing via inserts, drops, and instant-replaces,"
." or generate a new playlist on demand.  (Inserts and in-place replacements can only be performed via the remote"
." interface.  New playlists can only be generated from the GUI.)  Songs can be rearranged in the playlist via"
." drag-and-drop.  (Keep in mind that drag-and-drop actions that change the position in the playlist of the"
." currently playing song may have unanticipated side effects.)  Additionally, PerlJammer can automatically"
." replace any single track in the playlist with the entire disc that it came from, in track order.\n"
."PerlJammer will use only songs found in the SQL database when generating playlists, but will accept inserts of"
." files not in the database and will read MP3 tag information for them.\n"
."\nPerlJammer Controls:\n"
."PerlJammer's main panel contains fourteen buttons in two groups.  From left to right, the first group of buttons"
." perform the following actions:  START, PREVIOUS, PLAY/PAUSE, STOP, WAIT, NEXT, END.  Most of these buttons"
." should be obvious.  The PLAY/PAUSE button toggles between PLAY and PAUSE functions as appropriate, and turns"
." red when a song is paused.  In general, buttons which start or change things have blue icons which highlight"
." green when moused over; buttons which stop or delete things have black icons which highlight red when moused"
." over.\n"
."The WAIT button is a new function in PerlJammer.  It finishes playing the currently playing song, then advances"
." to the next song and stops the playlist, ready to resume play at any time.  The button turns red and changes"
." its appearance while waiting for the playing selection to end.\n"
."The second main group also contains seven buttons.  The first button in this group, BREAKPOINT, inserts a"
." breakpoint into the playlist at the position after the currently selected song,  This does not change the"
." selection or the song playing.  When PerlJammer reaches this breakpoint, it will pause playback and the"
." selection will be set to the first song after the breakpoint.  Multiple breakpoints are allowed in the playlist.\n"
."The second button, INSERT NEXT, identifies the disc containing the current selection, which may be"
." but need not necessarily be the current playing song; finds the next track on the disc, if the selected song is"
." not the last track on the disc; and inserts that song into the playlist after the current selection, then sets"
." selection to the song just added.\n"
."The third button, PLAY DISC, also finds the disc containing the selected song, but inserts all tracks from"
." that disc, in track order, at the selected point in the playlist.  If PerlJammer detects that the selected song"
." is playing and is the first track of the disc, this operation is performed seamlessly without stopping and"
." restarting play.  Likewise, if the selection is not the current playing song, the operation is of course"
." seamless.  (Of course, if you insert a disc before the playing song, this will change the position in the"
." playlist of the playing song.  See the caveat above.)  If the selected song when the disc insert operation begins"
." is the currently playing song, and is NOT the first track of the disc, then PerlJammer will stop playback and"
." restart from the first track of the disc after the insert.\n"
."The fourth button, LINK WITH NEXT, marks the currently selected song and the next song in the playlist to always"
." be kept together.  Once so marked, the songs will always be loaded together when generating new playlists.  Linked"
." songs will be shown in the playlist with a pale goldenrod background.\n"
."The fifth button, DROP, deletes the selected song from the current playlist.  If the selected song is playing"
." when dropped, playback immediately resumes from the next song.\n"
."The sixth button, DISABLE, not only drops the song from the current playlist, but marks it to be excluded from"
." all future playlists you generate.  (When you click DISABLE, a dialog will pop up asking whether you're sure"
." you want to disable the song, with buttons to either confirm the action or change your mind.)\n"
."The seventh button, NEW, generates a complete new playlist and resets the selection to the start of the new playlist.\n"
."The Quit button should require no explanation, and clearly you already figured out the Help button.\n"
."\nPersistent Gain Control:\n"
."If you enabled the gainctl option, which you should do ONLY AFTER setting up a working volume change script for"
." your sound system, there will be a yellow gain control button (split into upper and lower halves) in between"
." the two main control groups.  Each click on the upper half will persistently INCREASE the playback gain for"
." that song by the amount you have defined as one 'step'.  Each click on the lower half will persistently DECREASE"
." the playback gain for the song.  PerlJammer will remember this gain adjustment from session to session, and"
." dynamically adjust the volume from track to track to keep output volume at your preferred level.\n"
."\nPerlJammer Hot Keys:\n"
."From PerlJammer v1.7.0 on, the list of active hot heys has been revised.  PerlJammer's UI now responds to the"
." followng hot keys:\n"
."Home key - Go to the first song in the playlist\n"
."Left arrow - Skip back one song in the playlist\n"
."Right arrow - Skip forward one song in the playlist\n"
."End key - Go to the last song in the playlist\n"
."Space bar - Start play at the current selection, or pause/resume playback if already playing\n"
."Alt-S - Stop playing\n"
."Delete - Drop the current selection from thwe playlist\n"
."Up arrow - Increase volume one step, if volume control has been configured\n"
."Down arrow - Decrease volume one step, if volume control has been configured\n"
."Alt-N - Regenerate the playlist\n"
."Alt-Q or Ctrl-Q - Quit PerlJammer.\n"
."\nPerlJammer Remote Control:\n"
."The pjam-remote or pjam-gremote tools can be used to send remote commands to PerlJammer via a network socket."
."  Available remote control commands and their synonyms are:  START, PREVIOUS/PREV_SONG, PLAY/PLAY_SONG, PAUSE,"
." UNPAUSE/RESUME, PLAY_OR_PAUSE, STOP, WAIT, NEXT/NEXT_SONG, START/FIRST_SONG,END/LAST_SONG, INSERT_NEXT,"
." DISC/PLAY_DISC, INSERT_NEXT, DROP/DROP_SONG, DISABLE/DISABLE_SONG, INSERT, INSTANT, REPLACE, and STATUS/NOWPLAYING."
."  All of these commands are case insensitive.  Most of them are obvious, performing the same functions as the"
." buttons of the same name.  PLAY_OR_PAUSE is a toggle that starts play if nothing is playing, and otherwise"
." pauses or unpauses play according to whether or not playback is currently paused.\n"
."The REPLACE command takes a full path to a single MP3 filename as an argument, and replaces the currently"
." playing selection with that file.  The INSERT and INSTANT commands both take a list of pathnames, and insert"
." those files into the playlist.  The difference between the two is that INSERT inserts the list after the"
." current selection, while INSTANT stops play and inserts the new songs before the previously playing selection"
." so that they are played instantly.  These three commands are currently available only through the console"
." pjam-remote program.\n"
."The STATUS command causes PerlJammer to send back a reply consisting of the artist, disc title, song title, year,"
." and play time of the currently playing selection.  If nothing is playing, the reply is the single word 'Inactive'."
."  If a single integer argument is given, PerlJammer will send back the track information for that many of the"
." next upcoming songs in the current playlist.\n"
."PerlJammer is written to use madplay to play MP3s with, but should work with other command-line players that obey"
." SIGSTOP and SIGCONT.  Ogg Vorbis support will be implemented later.\n"
."\nPerlJammer Configuration:\n"
."PerlJammer obtains many runtime settings from its config file ($cfgfile).  Many of these settings have sensible"
." and safe defaults, but some do not, or have defaults that may be incorrect.  These must be set before running"
." PerlJammer."
."If no config directory or config file are found the first time PerlJammer is started, it will create them.  It will"
." also save a copy of its desktop icon into its config directory in case you wish to have your window manager handle"
." binding the icon instead of perlJammer doing it itself.  (This may be desirable if you use fvwm2, which may display"
." the icon better than Tk does, as Tk does not handle bounding transparency well.)  It will then quit and let you edit"
." the config file according to your needs before you restart it.\n"
."The following configuration options are REQUIRED to be set to correct values:\n"
."sqldb - The name of your DDJ-compatible MySQL music database.  THIS PARAMETER IS REQUIRED AND HAS NO DEFAULT."
."  PerlJammer WILL NOT START if this configuration item is not set, and will not be able to open the SQL DB if"
." it is not set correctly.\n"
."sqlcfg - The location of your MySQL config file.  If not specified, it defaults to HOME/.my.cnf.\n"
."sqlgroup - The name of the group entry in your MySQL config file.  The entry must contain the correct username,"
." password, and (if not localhost) hostname and port to access your music database.\n"
."mp3playercmd - the external command-line executable, with any required options, to be used to play MP3 files."
."  (PerlJammer is a jukebox, not a player per se, and relies upon an external player.)  Place a %s at the appropriate"
." point in the command line for the song file.  If not specified, it defaults to 'madplay -Q %s'.\n"
."Example:\nmp3playercmd: /usr/local/bin/madplay -o /dev/dsp6 -Q %s\n"
."oggplayercmd - the external command-line executable, with any required options, to be used to play Ogg Vorbis files."
."  If not specified, it defaults to 'ogg123 %s'.\n"
."PerlJammer also has several optional behaviors that can be controlled from its config file, including"
." the window geometry, the size of generated playlists, whether the controls are placed at the top or bottom of"
." the window, whether or not PerlJammer will automatically repeat when it reaches the end of the playlist, whether"
." or not PerlJammer should stop playing the current selection when it exits, and the network port on which it should"
." listen for commands (port 16384, by default).\n"
."\nKNOWN BUGS:\n"
."• The columns in the playlist do not pack properly into the available window width if geometry width is not 800 pixels,"
." because I have so far been unable to get Tk::MListbox->pack() or listbox->columnPack() to work properly.  I believe"
." this is a Tk::MListbox bug.  As a workaround for this, a new config file option 'columnPack' has been added.  This"
." should be set to a list of five integers separated by commas and optional spaces.  Try columnPack = 36,24,36,5,5"
." as a starting point.\n"
."• If you accidentally click and drag right on a column entry longer than the width of the column, it will scroll the"
." column.  You can click again and drag left to scroll it back.  There is no scrollbar to do this, because Tk::MListbox"
." provides no way to attack a horizontal scroll bar to a column.",
$pjam_version));

    ($okbutton = $dialog->Button(-text			=> 'OK',
                                 -command		=> sub {
                                                                   $timer = $okbutton->after(10, sub { $dialog->destroy(); } );
                                                               },
                                 -background		=> 'gainsboro',
                                 -anchor		=> 'center',
                                 -relief		=> 'raised',
                                 -width			=> 4,
                                 -height		=> 1)) -> form(-in	=> $dialog,
                                                                       -fill	=> 'both',
                                                                       -left	=> ['%4',5],
                                                                       -right	=> ['%6',-5],
                                                                       -top	=> ['%10',-60],
                                                                       -bottom	=> ['%10',-30]);

    ($credit = $dialog->Label(-text		=> sprintf('PerlJammer v%s, 2014-2020, Phil V. Stracchino <alaric@caerllewys.net>',
                                                           $pjam_version),
                              -font		=> defined $opts{smallfont} ? $opts{smallfont} : $opts{font},
                              -foreground	=> 'blue', 
                              -background	=> 'white', 
                              -anchor		=> 'center', 
                              -relief		=> 'flat', 
                              -width		=> 50, 
                              -height		=> 1)) -> form(-in	=> $dialog,
                                                               -fill	=> 'both',
                                                               -left	=> ['%0',5],
                                                               -right	=> ['%10',-5],
                                                               -top	=> ['%10',-25],
                                                               -bottom	=> ['%10',-5]);

    $dialog->bind('<Control-w>' => sub { $dialog->destroy(); });
}


sub confirm_disable
{
    my ($dialog, $title, $error, $errortext, $okbutton, $cancelbutton, $timer);
    $dialog = MainWindow->new();
    $dialog -> title("Confirm Disable Song");
    $dialog -> configure(-background => 'white');
    $dialog -> geometry("300x200");
    $dialog -> formGrid(10,1);

    ($errortext = $dialog->ROText(-width		=> 30,
                                  -height		=> 2,
                                  -padx			=> 5, 
                                  -pady			=> 5, 
                                  -relief		=> 'flat',
                                  -spacing3		=> 10,
                                  -foreground		=> 'black', 
                                  -background		=> 'gold', 
                                  -font			=> $opts{errorfont},
                                  -wrap			=> 'word')) -> form(-in		=> $dialog,
                                                                            -fill	=> 'both',
                                                                            -left	=> ['%0',15],
                                                                            -right	=> ['%10',-15],
                                                                            -top	=> ['%0',15],
                                                                            -bottom	=> ['%1',-60]);

    my @row = $listbox->getRow($listbox->curselection()->[0]);

    $errortext->insert('1.0', sprintf("Do you really want to ban '%s', by %s, from appearing in any future playlists?",
                                      $row[1],
                                      $row[2]));

    ($okbutton = $dialog->Button(-text			=> "Yes, I'm sure",
                                 -font			=> $opts{boldfont},
                                 -command		=> sub {
                                                                   $timer = $okbutton->after(10, sub {
                                                                                                         disable_song();
                                                                                                         $dialog->destroy();
                                                                                                     } );
                                                               },
                                 -background		=> 'gainsboro',
                                 -activeforeground	=> 'red',
                                 -anchor		=> 'center',
                                 -relief		=> 'raised',
                                 -width			=> 4,
                                 -height		=> 1)) -> form(-in	=> $dialog,
                                                                       -fill	=> 'both',
                                                                       -left	=> ['%0',15],
                                                                       -right	=> ['%5',-10],
                                                                       -top	=> ['%1',-45],
                                                                       -bottom	=> ['%1',-15]);

    ($cancelbutton = $dialog->Button(-text		=> 'No, never mind',
                                     -font		=> $opts{boldfont},
                                     -command		=> sub {
                                                                   $timer = $cancelbutton->after(10, sub {
                                                                                                             $dialog->destroy();
                                                                                                         } );
                                                               },
                                     -background	=> 'gainsboro',
                                     -activeforeground	=> 'blue',
                                     -anchor		=> 'center',
                                     -relief		=> 'raised',
                                     -width		=> 4,
                                     -height		=> 1)) -> form(-in	=> $dialog,
                                                                       -fill	=> 'both',
                                                                       -left	=> ['%5',10],
                                                                       -right	=> ['%10',-15],
                                                                       -top	=> ['%1',-45],
                                                                       -bottom	=> ['%1',-15]);

    $dialog->bind('<Control-w>' => sub { $dialog->destroy(); });
}


sub cfg_error_warn
{
    my ($dialog, $title, $error, $errortext, $okbutton, $timer);
    $dialog = MainWindow->new();
    $dialog -> title("Configuration failed!");
    $dialog -> configure(-background => 'white');
    $dialog -> geometry("400x300");
    $dialog -> formGrid(10,10);

    ($error = $dialog->Label(-text		=> '*** CONFIGURATION FAILURE! ***',
                             -font		=> $opts{errorfont},
                             -foreground	=> 'red', 
                             -background	=> 'gold', 
                             -anchor		=> 'center', 
                             -relief		=> 'flat', 
                             -width		=> 50, 
                             -height		=> 1)) -> form(-in	=> $dialog,
                                                               -fill	=> 'both',
                                                               -left	=> ['%0',5],
                                                               -right	=> ['%10',-5],
                                                               -top	=> ['%0',5],
                                                               -bottom	=> ['%0',25]);
    ($errortext = $dialog->ROText(-width		=> 30, 
                                  -height		=> 2,
                                  -padx			=> 5, 
                                  -pady			=> 5, 
                                  -relief		=> 'flat',
                                  -spacing3		=> 10,
                                  -foreground		=> 'black', 
                                  -background		=> 'gold', 
                                  -font			=> $opts{errorfont}.
                                  -wrap			=> 'word')) -> form(-in		=> $dialog,
                                                                            -fill	=> 'both',
                                                                            -left	=> ['%0',5],
                                                                            -right	=> ['%10',-5],
                                                                            -top	=> ['%0',25],
                                                                            -bottom	=> ['%10',-50]);

    $errortext->insert('1.0',
"PerlJammer's configuration file in your home directory, .pjam/config, does not contain a valid"
." sqldb setting.  Without this information, PerlJammer will be unable to read your music database"
." to generate playlists.\n"
."Please edit your .pjam/config file and make the appropriate changes, then restart PerlJammer.\n"
."If you do not HAVE a SQL music database compatible with DigitalDJ and PerlJammer, you will be"
." unable to use PerlJammer.");

    ($okbutton = $dialog->Button(-text			=> 'OK, let me fix it...',
                                 -command		=> sub {
                                                                   $timer = $okbutton->after(10, sub {
                                                                                                         $dialog->destroy();
                                                                                                     } );
                                                               },
                                 -background		=> 'gainsboro',
                                 -anchor		=> 'center',
                                 -relief		=> 'raised',
                                 -width			=> 4,
                                 -height		=> 1)) -> form(-in	=> $dialog,
                                                                       -fill	=> 'both',
                                                                       -left	=> ['%3',5],
                                                                       -right	=> ['%7',-5],
                                                                       -top	=> ['%10',-40],
                                                                       -bottom	=> ['%10',-10]);

    $dialog->bind('<Control-w>' => sub { $dialog->destroy(); });
}


sub create_button_bitmaps
{
    my $app = $_[0];

    my $playbuttonbits = pack("b12" x 12,
                 "11..........",
                 "1111........",
                 "111111......",
                 "11111111....",
                 "1111111111..",
                 "111111111111",
                 "111111111111",
                 "1111111111..",
                 "11111111....",
                 "111111......",
                 "1111........",
                 "11..........");
    $app->DefineBitmap('playbutton' => 12, 12, $playbuttonbits);

    my $stopbuttonbits = pack("b12" x 12,
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111",
                 "111111111111");
    $app->DefineBitmap('stopbutton' => 12, 12, $stopbuttonbits);

    my $pausebuttonbits = pack("b12" x 12,
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..",
                 "..111..111..");
    $app->DefineBitmap('pausebutton' => 12, 12, $pausebuttonbits);

    my $waitbuttonbits = pack("b24" x 12,
                 "............111111111111",
                 "11..........111111111111",
                 "1111........111111111111",
                 "111111......111111111111",
                 "11111111....111111111111",
                 "1111111111..111111111111",
                 "1111111111..111111111111",
                 "11111111....111111111111",
                 "111111......111111111111",
                 "1111........111111111111",
                 "11..........111111111111",
                 "............111111111111");
    $app->DefineBitmap('waitbutton' => 24, 12, $waitbuttonbits);

    my $waitbutton2bits = pack("b18" x 12,
                 "......111111111111",
                 "11......1111111111",
                 "1111..11..11111111",
                 "111111..11..111111",
                 "11111111..11..1111",
                 "1111111111..11..11",
                 "1111111111..11..11",
                 "11111111..11..1111",
                 "111111..11..111111",
                 "1111..11..11111111",
                 "11......1111111111",
                 "......111111111111");
    $app->DefineBitmap('waitbutton2' => 18, 12, $waitbutton2bits);

    my $startbuttonbits = pack("b20" x 12,
                 "....................",
                 "....................",
                 "11.......11.......11",
                 "11.....1111.....1111",
                 "11...111111...111111",
                 "11.11111111.11111111",
                 "11.11111111.11111111",
                 "11...111111...111111",
                 "11.....1111.....1111",
                 "11.......11.......11",
                 "....................",
                 "....................");
    $app->DefineBitmap('startbutton' => 20, 12, $startbuttonbits);

    my $prevbuttonbits = pack("b18" x 12,
                 "..................",
                 "..................",
                 ".......11.......11",
                 ".....1111.....1111",
                 "...111111...111111",
                 ".11111111.11111111",
                 ".11111111.11111111",
                 "...111111...111111",
                 ".....1111.....1111",
                 ".......11.......11",
                 "..................",
                 "..................");
    $app->DefineBitmap('prevbutton' => 18, 12, $prevbuttonbits);

    my $nextbuttonbits = pack("b18" x 12,
                 "..................",
                 "..................",
                 "11.......11.......",
                 "1111.....1111.....",
                 "111111...111111...",
                 "11111111.11111111.",
                 "11111111.11111111.",
                 "1111111..111111...",
                 "11111....1111.....",
                 "11.......11.......",
                 "..................",
                 "..................");
    $app->DefineBitmap('nextbutton' => 18, 12, $nextbuttonbits);

    my $endbuttonbits = pack("b20" x 12,
                 "....................",
                 "....................",
                 "11.......11.......11",
                 "1111.....1111.....11",
                 "111111...111111...11",
                 "11111111.11111111.11",
                 "11111111.11111111.11",
                 "1111111..111111...11",
                 "11111....1111.....11",
                 "11.......11.......11",
                 "....................",
                 "....................");
    $app->DefineBitmap('endbutton' => 20, 12, $endbuttonbits);

    my $discbuttonbits = pack("b20" x 12,
                 "....................",
                 "...1111.............",
                 ".11111111..11.......",
                 ".11111111..1111.....",
                 "1111111111.111111...",
                 "1111111111.11111111.",
                 "1111111111.11111111.",
                 "1111111111.111111...",
                 ".11111111..1111.....",
                 ".11111111..11.......",
                 "...1111.............",
                 "....................");
    $app->DefineBitmap('discbutton' => 20, 12, $discbuttonbits);

    my $breakpointbits = pack("b20" x 12,
                 "....................",
                 "....................",
                 "....................",
                 "....................",
                 "....................",
                 "11111111111111111111",
                 "11111111111111111111",
                 "....................",
                 "....................",
                 "....................",
                 "....................",
                 "....................");
    $app->DefineBitmap('breakpoint' => 20, 12, $breakpointbits);

    my $addnextbuttonbits = pack("b20" x 12,
                 "....................",
                 "....................",
                 "....11....11........",
                 "....1111..1111......",
                 "....111111..1111....",
                 "....11111111..1111..",
                 "....11111111..1111..",
                 "....111111..1111....",
                 "....1111..1111......",
                 "....11....11........",
                 "....................",
                 "....................");
    $app->DefineBitmap('addnextbutton' => 20, 12, $addnextbuttonbits);

    my $linkbuttonbits = pack("b20" x 12,
                 "....................",
                 "...1111..11111111...",
                 ".11111.111111111111.",
                 ".111...111......111.",
                 "111...111..11....111",
                 "111...111..111...111",
                 "111...111..111...111",
                 "111....11..111...111",
                 ".111......111...111.",
                 ".111111111111.11111.",
                 "...11111111..1111...",
                 "....................");
    $app->DefineBitmap('linkbutton' => 20, 12, $linkbuttonbits);

    my $dropbuttonbits = pack("b20" x 12,
                 "....................",
                 "....111......111....",
                 ".....111....111.....",
                 "......111..111......",
                 ".......111111.......",
                 "111111..1111..111111",
                 "111111..1111..111111",
                 ".......111111.......",
                 "......111..111......",
                 ".....111....111.....",
                 "....111......111....",
                 "....................");
    $app->DefineBitmap('dropbutton' => 20, 12, $dropbuttonbits);

    my $killbuttonbits = pack("b20" x 12,
                 "........1111..111...",
                 ".....111111..111....",
                 "...1111111..111.....",
                 "...111111..111..1...",
                 "..111111..111..111..",
                 "..11111..111..1111..",
                 "..1111..111..11111..",
                 "..111..111..111111..",
                 "...1..111..111111...",
                 ".....111..1111111...",
                 "....111..111111.....",
                 "...111..1111........");
    $app->DefineBitmap('killbutton' => 20, 12, $killbuttonbits);

    my $quitbuttonbits = pack("b12" x 12,
                 "....1111....",
                 "..11111111..",
                 ".111....111.",
                 "111......111",
                 "11........11",
                 "11...11...11",
                 "11...11...11",
                 "111..11..111",
                 ".111.11.111.",
                 "..11.11.11..",
                 ".....11.....",
                 ".....11.....");
    $app->DefineBitmap('quitbutton' => 12, 12, $quitbuttonbits);

    my $upbuttonbits = pack("b12" x 6,
                 ".....11.....",
                 "....1111....",
                 "...111111...",
                 "..11111111..",
                 ".1111111111.",
                 "1111....1111");
    $app->DefineBitmap('upbutton' => 12, 6, $upbuttonbits);

    my $downbuttonbits = pack("b12" x 6,
                 "1111....1111",
                 ".1111111111.",
                 "..11111111..",
                 "...111111...",
                 "....1111....",
                 ".....11.....");
    $app->DefineBitmap('downbutton' => 12, 6, $downbuttonbits);
}


sub write_cfgfile
{
    open (CFG, ">$cfgfile");
    select((select(CFG), $| = 1)[0]);
    print CFG <<'END';
# PerlJammer Configuration File

# Player commands must be set to a valid command containing
# a %s placeholder for the filename.  PerlJammer defaults to
# using madplay for MP3 playback if not specified, as this
# is the preferred player application for PerlJammer.
# (mpg123 is not recommended, as its output rendering is
# inferior and it stutters very badly under high system I/O
# loads.)
# The default player for Ogg Vorbis files is ogg123.
#
# Examples:
# mp3playercmd = "/usr/bin/madplay -b 32 -Q -G %s"
# mp3playercmd = "/usr/bin/mpg123 -q %s"
# oggplayercmd = "/usr/bin/ogg123 %s"

mp3playercmd = "/usr/bin/madplay -b 32 -Q -G %s"
oggplayercmd = "/usr/bin/ogg123 %s"

# datadir is where all your systemwide shared RemoteJammer skins,
# PerlJammer's icon, etc. reside.  If unset, it defaults to
# /usr/share/perljammer.  You should not need to set
# this unless you have a non-standard install.

# datadir = /usr/share/perljammer

# sqlcfg defaults to $HOME/.my.cnf.  The sqlgroup defaults
# to pjam.  The sqldb name HAS NO DEFAULT AND MUST BE CORRECT.
# The group entry must contain a correct SQL host, port,
# username, and password.

sqlcfg = ~/.my.cnf
sqlgroup = pjam
sqldb = perljammer

# basedir is used only by the id3sort tool.  If defined, it is used
# to set the default base directory that mp3 files should be organized
# under by id3sort.

basedir =

# PERSISTENT GAIN CONTROL
# Volume change command template and step size
# The gainctl setting enables or disables database-based gain
# adjustment.  (The command line switch -gainctl overrides
# the setting gainctl: no.)
# The volumecmd is the command which, if invoked with a positive
# integer value, will increase the playback volume by a constant
# whatever it is currently set to (unless already at the maximum).
# The stepsize is the argument which, passed to that volume command,
# will result in a volume increase of what you consider one "step".
# (This need not be constant from one machine to another.)  You
# decide how large of a volume change is a "step" to you.
# The resetcmd is a shell command which will reset playback volume
# to whatever level you consider 'default'.

gainctl = yes
volumecmd = "/root/bin/chvol %d"
stepsize =  5
resetcmd =  /root/bin/sreset

# Geometry defaults to 800x600 if unspecified

geometry = 800x600

# Playlist size defaults to 500

songlimit = 500

# Active row controls how many rows down from the top of the
# playlist window ParlJammer maintains the active selected row.
# If unset, the default is 5.

activerow = 10

# Font defaults to Helvetica Medium 12pt (for latin1 charset,
# change to iso8859-15)
font = -adobe-helvetica-medium-r-normal--*-120-*-*-p-*-iso10646-1

# Bold font defaults to bolded version of Font
boldfont = -adobe-helvetica-bold-r-normal--*-120-*-*-p-*-iso10646-1

# Small font defaults to 10pt version of Font
smallfont = -adobe-helvetica-medium-r-normal--*-100-*-*-p-*-iso10646-1

# Remote font defaults to Helvetica Medium 12pt
remotefont = -adobe-helvetica-medium-r-normal--*-120-*-*-p-*-iso10646-1

# Remote bold font defaults to bolded version of remotefont
remoteboldfont = -adobe-helvetica-bold-r-normal--*-120-*-*-p-*-iso10646-1

# Remote small font defaults to 10pt version of remotefont
remotesmallfont = -adobe-helvetica-bold-r-normal--*-100-*-*-p-*-iso10646-1

# Control location defaults to bottom.  Options are bottom or top.

controls = bottom

# Scrollbar location defaults to left.  Options are left or right.

scrollbar = left

# Tk::Listbox's columnPack function doesn't seem to work worth shit for
# total window widths <1500px.  For windows more than 1500px wide, it
# works TOLERABLY well, but column 4 will always be excessively wide,
# and trying to make column 4 narrower will make column 5 narrower
# instead.
# We therefore provide an option to manually tune column widths.  Uncomment
# the following directive and edit it as seems appropriate:
#
# columnPack = 36, 25, 36, 4, 5

# The timebox_padding setting controls how many pixels of 'padding'
# to use on either side of the contents of the elapsed-time meter.

timebox_padding = 8

# The remote_timebox_padding setting controls how many pixels of 'padding'
# to use on either side of the contents of the elapsed-time meter in
# remotejammer.

remote_timebox_padding = 8

# The elapsed time meter runs by default in count-up mode.  Turning
# this option on will make it run in count-down mode instead.

timebox_countdown = no

# Stop on quit defaults to off (0 or 'no').  If set, the playing
# song will be stopped immediately when PerlJammer quits.

stop_on_quit = no

# The action_at_end directive has four allowed values:  'stop',
# the default), 'loop', 'new', or 'quit'.  When set to 'stop',
# PerlJammer will stop playing when it reaches the end of the
# playlist, and wait for input or commands.  When set to 'loop',
# PerlJammer will resume play from the beginning of the existing
# playlist.  When set to 'new', PerlJammer will generate a new
# playlist while the final selection of the existing playlist is
# playing, then seamlessly continue play from the beginning of
# the new playlist.  When set to 'quit', PerlJammer will stop and
# exit when it reaches the end of the playlist.
#
# This directive obsoletes the 'autorepeat' directive, which is
# equivalent to 'action_at_end: loop' or 'action_at_end: stop'.

action_at_end = stop

# Autostart defaults to off.  If set, PerlJammer will automatically
# start playing as soon as it has loaded its playlist.

autostart = no

# Update play stats defaults to on.  The information is not used yet,
# but may be used in the future to "prefer" songs played less often or
# less recently.  Disabling it will save a small amount of database
# traffic and one database connection per song played.  You should
# probably leave it on unless you have a specific reason to want to
# turn it off.

update_stats = yes

# The useWMicon setting (default: 'no') controls whether PerlJammer binds
# its own icon, or leaves it to the window manager.  With some window
# managers, such as fvwm2, the icon may look better on the desktop if
# bound in the window manager rather than by PerlJammer, since Tk's
# own icon binding does not handle bounding transparency properly.

useWMicon = no

# Network port for remote controls defaults to 16384

remoteport = 16384

# Remote host for remote controls defaults to localhost.  This does not
# restrict host allowed to connect to the remote port; it sets which host
# remote-control tools will attempt to connect to.  This should always
# be set to the host upon which PerlJammer is running.  It is ignored by
# Perljammer itself.

remotehost = localhost

# RemoteJammer skins are 162x302 images in PNG, JPEG, GIF or XPM format.
# Default: /usr/share/perljammer/skins/bluecurve.png
# This can also be set to 'random' to select a random skin from PerlJammer's
# shared system data directory.

remoteskin = /usr/share/perljammer/skins/bluecurve.png

# remotelayout controls which type of UI RemoteJammer creates.  Blank
# or 'default' gives the normal skinned remote-control-like UI.
# 'horizontal' creates a currently unskinned UI with all the normal
# controls and now-playing and elapsed-time displays.  'vertical'
# creates a vertical control strip only.

remotelayout = default

# remoteposition is an X11 geometry position specification for
# RemoteJammer.  Use it if you want RemoteJammer to start in a fixed
# position on screen.
# Example: -0+0 starts RemoteJammer in the top right corner.
# Default: none

# remoteposition = -0+0

# Colors for foreground and background UI elements not part of a
# skin (control strip backgrounds, button foregrounds)

UIforeground = MidnightBlue
UIbackground = DarkSlateBlue

# IMPORTANT:
# The following functionality is available ONLY on a database which
# has been either created, or updated to version 3, by pjam-dbmaker.
# 
# The disable_bitmask and disable_id settings are used to control
# which songs in the database are skipped when building a playlist
# for this user.
# The disable_id setting is a numerid "user id" in the range 1-64,
# and indicates which bit will be set in a song's disable bitmask
# to indicate that the user with this id does not want it to be
# selected into playlists.  It defaults to 1.

disable_id = 1

# The disable_bitmask setting, which also defaults to 1, selects
# which bits in a song's disable bitmask will be taken into account
# when deciding whether to skip a song.  By default, this is left
# blank and set internally by PerlJammer with only the bit
# corresponding to the user's disable_id set.  However, it can be
# explicitly set to also honor other users' disable bits.  For
# example, the following:
# disable_bitmask: 1000110
# will tell PerlJammer to skip songs marked as disabled by any of
# the users with numeric disable_ids 2, 3, or 7.

disable_bitmask =

END
    close (CFG);
}


sub write_icon
{
    open(ICON, ">$iconfile");
    select((select(ICON), $| = 1)[0]);
    print ICON <<'END';
/* XPM */
static char * PerlJammer_xpm[] = {
"96 96 252 2",
"  	c None",
". 	c #FFFFFF",
"+ 	c #F0F0F0",
"@ 	c #FAFAFA",
"# 	c #E2E2E2",
"$ 	c #EDEDED",
"% 	c #BEBEBE",
"& 	c #B2B2B2",
"* 	c #E4E4E4",
"= 	c #A1A1A1",
"- 	c #868686",
"; 	c #7E7E7E",
"> 	c #7D7D7D",
", 	c #808080",
"' 	c #818181",
") 	c #7B7B7B",
"! 	c #7A7A7A",
"~ 	c #797979",
"{ 	c #777777",
"] 	c #787878",
"^ 	c #7C7C7C",
"/ 	c #7F7F7F",
"( 	c #878787",
"_ 	c #979797",
": 	c #B7B7B7",
"< 	c #E1E1E1",
"[ 	c #D9D9D9",
"} 	c #8F8F8F",
"| 	c #BCBCBC",
"1 	c #D4D4D4",
"2 	c #585858",
"3 	c #202020",
"4 	c #1B1B1B",
"5 	c #252525",
"6 	c #2C2C2C",
"7 	c #2E2E2E",
"8 	c #2F2F2F",
"9 	c #2B2B2B",
"0 	c #2D2D2D",
"a 	c #343434",
"b 	c #3F3F3F",
"c 	c #555555",
"d 	c #ACACAC",
"e 	c #AFAFAF",
"f 	c #686868",
"g 	c #575757",
"h 	c #A4A4A4",
"i 	c #F7F7F7",
"j 	c #EBEBEB",
"k 	c #919191",
"l 	c #262626",
"m 	c #151515",
"n 	c #494949",
"o 	c #595959",
"p 	c #5E5E5E",
"q 	c #5D5D5D",
"r 	c #5C5C5C",
"s 	c #5B5B5B",
"t 	c #5A5A5A",
"u 	c #606060",
"v 	c #5F5F5F",
"w 	c #646464",
"x 	c #717171",
"y 	c #848484",
"z 	c #9D9D9D",
"A 	c #A8A8A8",
"B 	c #616161",
"C 	c #989898",
"D 	c #888888",
"E 	c #757575",
"F 	c #9C9C9C",
"G 	c #ADADAD",
"H 	c #B3B3B3",
"I 	c #B0B0B0",
"J 	c #AEAEAE",
"K 	c #B1B1B1",
"L 	c #B4B4B4",
"M 	c #AAAAAA",
"N 	c #A9A9A9",
"O 	c #A6A6A6",
"P 	c #A7A7A7",
"Q 	c #B5B5B5",
"R 	c #C1C1C1",
"S 	c #C6C6C6",
"T 	c #747474",
"U 	c #4B4B4B",
"V 	c #939393",
"W 	c #F8F8F8",
"X 	c #959595",
"Y 	c #4C4C4C",
"Z 	c #676767",
"` 	c #CDCDCD",
" .	c #D7D7D7",
"..	c #DBDBDB",
"+.	c #D8D8D8",
"@.	c #D5D5D5",
"#.	c #D3D3D3",
"$.	c #D2D2D2",
"%.	c #CFCFCF",
"&.	c #CECECE",
"*.	c #D0D0D0",
"=.	c #D6D6D6",
"-.	c #D1D1D1",
";.	c #DADADA",
">.	c #CBCBCB",
",.	c #CCCCCC",
"'.	c #C9C9C9",
").	c #CACACA",
"!.	c #C8C8C8",
"~.	c #C5C5C5",
"{.	c #DEDEDE",
"].	c #E6E6E6",
"^.	c #E5E5E5",
"/.	c #898989",
"(.	c #515151",
"_.	c #FEFEFE",
":.	c #9E9E9E",
"<.	c #BDBDBD",
"[.	c #E3E3E3",
"}.	c #DDDDDD",
"|.	c #DCDCDC",
"1.	c #DFDFDF",
"2.	c #E0E0E0",
"3.	c #ECECEC",
"4.	c #F1F1F1",
"5.	c #999999",
"6.	c #828282",
"7.	c #E8E8E8",
"8.	c #EFEFEF",
"9.	c #A0A0A0",
"0.	c #858585",
"a.	c #C7C7C7",
"b.	c #EEEEEE",
"c.	c #F6F6F6",
"d.	c #EAEAEA",
"e.	c #F5F5F5",
"f.	c #F2F2F2",
"g.	c #FDFDFD",
"h.	c #9F9F9F",
"i.	c #E7E7E7",
"j.	c #F3F3F3",
"k.	c #C3C3C3",
"l.	c #969696",
"m.	c #FBFBFB",
"n.	c #C0C0C0",
"o.	c #949494",
"p.	c #BFBFBF",
"q.	c #9A9A9A",
"r.	c #A3A3A3",
"s.	c #929292",
"t.	c #9B9B9B",
"u.	c #BABABA",
"v.	c #BBBBBB",
"w.	c #909090",
"x.	c #F4F4F4",
"y.	c #B6B6B6",
"z.	c #E9E9E9",
"A.	c #545454",
"B.	c #565656",
"C.	c #B9B9B9",
"D.	c #C2C2C2",
"E.	c #8D8D8D",
"F.	c #535353",
"G.	c #A5A5A5",
"H.	c #C4C4C4",
"I.	c #8E8E8E",
"J.	c #525252",
"K.	c #4D4D4D",
"L.	c #6D6D6D",
"M.	c #8C8C8C",
"N.	c #4A4A4A",
"O.	c #6A6A6A",
"P.	c #737373",
"Q.	c #8B8B8B",
"R.	c #505050",
"S.	c #4F4F4F",
"T.	c #8A8A8A",
"U.	c #383838",
"V.	c #6E6E6E",
"W.	c #B8B8B8",
"X.	c #353535",
"Y.	c #434343",
"Z.	c #414141",
"`.	c #3D3D3D",
" +	c #373737",
".+	c #363636",
"++	c #3B3B3B",
"@+	c #707070",
"#+	c #ABABAB",
"$+	c #838383",
"%+	c #767676",
"&+	c #313131",
"*+	c #3E3E3E",
"=+	c #424242",
"-+	c #303030",
";+	c #212121",
">+	c #1A1A1A",
",+	c #191919",
"'+	c #4E4E4E",
")+	c #393939",
"!+	c #3A3A3A",
"~+	c #484848",
"{+	c #333333",
"]+	c #292929",
"^+	c #282828",
"/+	c #232323",
"(+	c #1C1C1C",
"_+	c #323232",
":+	c #242424",
"<+	c #272727",
"[+	c #404040",
"}+	c #464646",
"|+	c #1F1F1F",
"1+	c #474747",
"2+	c #222222",
"3+	c #696969",
"4+	c #727272",
"5+	c #2A2A2A",
"6+	c #444444",
"7+	c #6B6B6B",
"8+	c #454545",
"9+	c #1E1E1E",
"0+	c #171717",
"a+	c #161616",
"b+	c #1D1D1D",
"c+	c #6F6F6F",
"d+	c #141414",
"e+	c #626262",
"f+	c #121212",
"g+	c #0D0D0D",
"h+	c #111111",
"i+	c #0E0E0E",
"j+	c #131313",
"k+	c #070707",
"l+	c #0C0C0C",
"m+	c #090909",
"n+	c #0F0F0F",
"o+	c #181818",
"p+	c #6C6C6C",
"q+	c #636363",
"r+	c #101010",
"s+	c #A2A2A2",
"t+	c #0A0A0A",
"u+	c #666666",
"v+	c #3C3C3C",
"w+	c #656565",
"x+	c #080808",
"y+	c #020202",
"z+	c #030303",
"A+	c #0B0B0B",
"B+	c #F9F9F9",
"C+	c #050505",
"      . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +       ",
"    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ #     ",
"  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . $ % &   ",
". . . . * = - ; > , ' ; ; ) ! ~ { ] ~ ~ ~ ~ ! ! ) ! ) ^ / ; ; > ; > , / ; , ' , , ; / , / ; , ' ' ' , ; ; ' , , , / ; ; , / / ; / ^ ^ > ! ! { ] ! ^ ) ^ > ; ; ^ > ; / / / , , ( _ : < . [ } > | ",
". . . 1 2 3 4 5 6 7 8 6 9 9 9 6 6 6 6 0 6 6 0 6 6 6 6 0 7 7 7 8 0 0 7 7 7 7 7 7 7 7 7 8 7 8 8 8 7 8 7 0 7 7 7 8 8 7 7 7 7 7 7 7 7 0 6 0 0 6 6 9 6 0 6 0 0 7 7 0 0 6 9 0 0 7 8 a b c > d e f g h ",
". i j k l m 6 n o p q r s s t t t s s s t s s s s s s s q r q r q q q r p q u p p p p p p v q q p q p q q r q r q r s q s s s s s s 2 s t 2 g 2 g 2 2 t s r s s t s q q r q r p w x y z A B n C ",
". i $ D a b E F G H & I I I e G d J e J J J e I G J J J K e & I K & K e & I L & & & & & K K J & & & & & K e I e d G K J d d d d J d M d M M N O O N P M d J J G J e e I I K K e & Q | R S T U V ",
". W W X Y Z M `  ...+.@.1 #.$.$.%.&.*.$.1 1 =.=.-.$.$.#. .@.+.=.=.+.+.1  .=.[ +.=. . . .1 ;.+.[ +.[ +.+.@.=.1 1 >.,.-.&.*.*.*.*.,.,.,.'.).>.!.~.).,.).,.&.$.1 $.#.$.*.*.&.-.#.$.=.{.].# ^./.(.X ",
". _.. :.o ) <.;.< [.< }.[ +.+. .[  . ...[ ;...|.[ [ [ ;.1.}.1.}.}.1.1.|.}.{.{.1.}.}.}.{.[ }.|.< 2.1.< 1.|.{.;.|.[ +.1 +.@.@.@.@.#.#.=.%.@.-.%.%.#.1 -.$.1 +..... .+.+.+.@.[ |.;.{.3.4.+ i X c 5.",
". . . = r 6.~.{.# 7.< |.....;.[ @.1 [  .[ }.}.;.........1.{.}.|.}...}...}.}.}.}.|.{.}.[ ;.}...{.1.}.}.1.;.[ }...=.@.@. .[ +.=.@.&.$.-.` -.%.&.&.,.*.-.%.#...[  . .@.@.[ =.[ [  .# 8.W i W C t z ",
". . . 9.r 0.a.2.[.].< }.....;.[  . .[ [ [ ....[ [ [ ;.[ }.|...;.}.;.}.;.}.}.}.}.}.1.{...;.....}.}.....}.[ [ ..;. .[ ;.+.[ [ ;...#.1 -.` $.&.,.%.%.-.#.#.-. .@.#.+. .#.@.=.1 1  .1.b.W c.W C o F ",
". _.. 9.r 0.a.2.^.[.< }.....[ [ +.[  . .[  . .[ ..< [ 1.1.{.}.}.|.;.|.;.........|.}.|.[ [ [ ......+.+.;. .[ +.[ +.;...[ [ [ ....$.1 -.>.#.-.*.%.-.-.$.1 @.[  .[ $.=.@. .@.#.1 =.}.d.e.f.i C g 5.",
". g.. h.q 0.~.}.i.< < }...;.[ [ =. .$.1 +.@.@.[ ;.{.[ }.}.|...;.|.[ |.[ ........[ ;.[ @.[  .|.;.[ =.=.+.=.[ =.[ +.=. .[ ;.[  .=.-.>.>.-.>.%.&.!.*.&.%.#.#.@.$.=.#. .1 $.@.+.+.=.;.7.j.+ c.C c C ",
". _.. 9.q y k.|.[.# # < }.;.@.$.1 1 1 1 #.=.1 =.[ [ [ [ }.|.....[ [ [ +.[ =. .;.;.[  .@. .@.[  . .=.@.=.#. .[ +.}.+.=. .+. . .=.$.%.` ).%.%.&.&.` %.-.$.$.&.@.-.-.#.@.@.@.@.@.1 =.i.j.j 8.l.g X ",
". g.. 9.q y k.}.< < < < |.;.=.#.#.#.1 1 #.=.#.=.=. .;...[ [ +. .[ +.+. .@.@.@. .1 1 1 @.1 $.@.1 @.-.#.=.@.1 =.[ ;.+. . .+.+. .=.-.-.%.&.*.&.>.'.%.-.#.1 -.%.@.$.$.-.$.=. . . .=.=.d.f.$ e.5.o C ",
". m.. h.s 6.R ;.2.1.1.1...[  .@.#.#.#.#.#.@.#.=.[ [ +. .+.+. .=. . . . .#.@.=.@.@.@.@.@.*.&.-.*.%.%.-.1 1 #.@.+.=.+.[  .[ +. . . .@.-.&.-.%.` >.` &.*.-.-.*.$.$.&.$.#.$. . .=.@.|.3.m.+ + _ g l.",
". @ . :.r 6.R ;.1.1.{.}.;.[  . .$.#.#.#.#.@.#.=. .+.[ ;.;.[ [ +. . .=.=.@.+.[ =.1 #.*.%.` >.%.` !.%.$.*.-.@. .@.1 +.[  .[ +. . . .@.#.$.-.-.-.-.%.*.$.#.*.-.-.$.%.%.-.#.@.#.#.#.+.j e.b.+ l.g l.",
". e._.z s ' n.;.1.}.|...[  .#.-.@.@.@.@.@.$.-.#.-. .}.+...[ ..[  . .=.=.=.=.=.=.-.%.&.&.&.` >.>.,.,.&.#.#.+. . . .=.=.@.+...+.-.1 #.-.-.-.%.%.$.-.*.*.*.*.-.$.$.$.-.1 @.#.#.@.@...j + 3.f.X c l.",
"i i _.5.o , n.|.{.}...;.[  .@.#.@.@.@.@.=.#.$.1 $. ...+. .=. .=. . . .=.+.=. .@.-.-.%.` %.,.,.>.` '.%.%.` >.-.1 @.@.@.@.@.#.$.-.-.*.%.` *.-.%.>.*.*.*.*.*.-.-.$.*.=.1 @.$.#.1 @.+.7.3.i.$ o.c V ",
"g.e.W z o , p.;.}...;.+.[ +. . .@.@.=.=. .1 1 @.@. . .+.=.@.=.@.+.+. . ... .[ 1 *.#.-.` %.&.>.,.` G &.*.1 *.#.#.1 #.#.#.$.%.%.-.-.*.&.` -.-.%.,.*.*.*.*.%.*.-.-.%.,.*.$.-.$.#.#.=.].d.^.j.o.c l.",
"c.i i q.t , <.=.|...[  .[ [ [ [ @.@.=.=.[ @.@. .@. .1  . .=. .=.+.+. . .{. .;.#.*.1 #.` *.` ` ,.>.r.&.,.` $.-.#.#.#.$.-.*.%.-.$.#.$.*.%.#.-.-.1 *.*.*.%.%.*.-.-.` *.-.-.*.-.$.#. .i.j ].$ V c s.",
"c.+ c.t.o > u.@.;. .=.+. .$.#.+.$.@.@.$.=.=.@.@.$.1 @.#.1 @.=. . .@. .[ [ +. . .#.#.#.#.$.%.-.*._ 5.` ` &.$.1 #.-.1 %.,.-.,.1 1 $.$.$.$.-.1 -.1 #.@.@.#.*.%.%.&.#.%.%.#.$.$.$.$.+.^.$ 7.8.o.c V ",
"e.b.j.5.o > v.=... . .[ 1 *.*.#.@.@.@.@.@.@.@.@.@.@.1 -.#.1 @.@. .@.=.[ +.+. .=.#.#.$.$.-.` ` -.w.p.&.%.$.1 #.-.%.#.*.-.%.1 $.-.-.-.-.-.#.@.#.@.#.@.@.#.%.%.&.` -.%.%.-.-.-.-.-.@.].b.^.3.s.c X ",
"x.3.8.X g ! y.*.@.#.-.#.1 -.%.%.@.#.#.@.@.1 1 1 *.#.1 #.$.#.1 @. .@.=.[  . .=.@.#.$.-.-.%.$.%.%./ -.%.%.%.-.-.&.>.$.%.` | ` ` ` %.%.%.%.%.-.%.-.#.@.@.$.%.&.` ` -.%.%.-.%.%.*.*. .[.j i.d.s.c o.",
"j.z.3.o.g ) : -.@.#.$.#.=.@.$.%.$.%.%.$.1 1 #.#.-.#.$.%.-.-.#.#. .@.@.[  .=.@.@.$.$.-.-.*.$.` -.] -.` ,.!.,.&.` ` >.).'.L '.>.'.` ` ` ` *.$.*.#.#.@.@.-.&.&.` ` -.` ` -.%.%.%.%.@.* 3.^.$ s.A.k ",
"+ i.$ o.B.! : %.1 %.#.#.*.$.-.%.#.-.*.%.1 -.#.-.$.=.#.-.1 $.#.+.+. . .=. .=. .C.G : %.1  .%.).!.] !.'.!.~.D.%.>.'.'.'.E.h.a.~.!.~.).'.&.@.-.-.#.-.-.*.%.%.-.%.,.%.` ` %.&.` *.).-.# j [.j } F.s.",
"3.[.d.k c ! : -.#.*.-.#.#.*.*.-.%.*.-.#.1 -.#.*.-.=.%.$.#.#.#.#.=.@.@.@. .;.@.1 k.:.- g p - X G.o.-.` ~.H.% ~.~.S k.~.^ | D.!.S S '.>.&.#.-.-.-.-.%.%.%.&.%.` >.&.>.>.` ,.,.,.'.%.1.z.< i.I.J.k ",
"d.< ].} B.! Q ,.$.-.%.#.#.-.*.$.%.*.-.#.#.-.#.*.$.-.%.*.@.%.%.#.#.#.$.$.$.$.&.*.,.` #. .V (.C Y K.g g B.9.p.` a.S D.!.L.H.k.~.S a.!.` ` -.$.-.%.%.&.` ` &.,.>.>.,.'.'.>.'.'.S !.)...* |.^.M.(.I.",
"d.< i.w.A.~ Q &.-.-.&.#.-.#.#.*.#.-.*.%.#.*.$.%.-.*.>.-.#.-.%.%.-.-.*.*.>.` >.!.!.!.` ~.a.N p.N.I R w.N w.) O.P.9.<.~.P.n.p.p.R !.a.` ` %.$.-.&.` ` ,.,.` ).'.>.).a.a.'.!.a.k.a.a.[ # [ [.Q.R.E.",
"[.2.^.I.F.{ H ,.-.-.1 *.&.*.$.#.-.-.-.-.-.-.*.%.-.&.` %.%.%.` ` %.@.&.).u.p.'.a.'.'.,.&.!.H.R s e L <., - _ H R | <.G y % R <.R ~.~.'.%.-.*.%.` ` ` ` &.).'.a.S a.~.H.S ~.~.R H.a.#.# +.{.( S.T.",
"* }.[.I.A.E K ` $.%.-.#.%.*.-.$.%.%.%.%.-.*.%.%.-.` ` %.%.%.*.-.$.-.$.` r.0.U.U N.t V.; /.h.N t u.u.<.p.W.G.' t o ; { T.L H.R ~.v.n.a.` ` ` ,.>.&.` >.).~.~.~.~.S k.k.~.H.H.n.p.n.$.;.$. .D K.( ",
"2.{.* M.F.E I ,.-.*.%.#.#.#.$.-.-.-.-.-.*.*.%.%.-.` ` %.-.*.&.` ,.1 ).~.k.& X.Y.a Z.`. +8 .+++++b ++Y p @+5.L R e G.U.L.Z F.0 @+#+n.,.a.` ` ` ` ,.>.'.!.'.a.~.k.H.R R D.D.R p.C.n.&.[ -.-.y U $+",
"< |.< E.J.%+K ).%.#.-.-.1 #.-.-.&.&.&.&.%.%.%.%.*.` ` %.>.>.,.,.'.,.'.k.a.' &+++X.Z.*+Z.U..+X.`.=+X.-+8 ;+>+,+0 =+L.8 h G.h.H.P X 0.T.~.'.'.>.,.a.!.'.'.H.k.k.D.D.p.p.R ~.| v.W.| '.,.-.-./ N.' ",
";.|.{.Q.R.T I ).%.-.-.&.$.*.%.$.&.%.%.%.%.%.%.%.-.` >.` ` ).'.).>.>.~.R ,.'+X.)+!+U. +8 7 0 5 9 ~+{+]+0 ^+9 /+(+_+) )+M K C.& : C.D.R D.~.!.S !.~.k.~.k.p.p.n.R R p.p.R p.u.: : : H.*.S a.! ~+/ ",
"[ ..{.T.(.E J a.>.%.#.#.*.` ` %.` &.%.-.%.%.%.&.*.` >.,.` ).!.'.a.'.~.k.% _+X.X.{+X.8 5 ]+:+ + +v Z.l 0 <+ +^+:+<+6 _+[+`.s / 5.e p.n.K y.p.a.R H.D.H.D.R n.n.R n.n.<.C.| W.y.: v.R !.n.D.~ ~+! ",
"+.[ |./.(.T G ~.&.-.#.-.*.` ` %.$.*.` >.&.&.` ` %.,.).>.,.'.a.'.!.k.a.k.( 7 &+&+-+-+7 ]+6 }+(.g s ~+5 |+3 1+0 <+/+^+U ++_+.+Z.t R.p P.T.G R D.k.~.k.~.k.n.p.<.<.<.<.u.Q W.Q Q : W.n.'.H.!.~ 1+) ",
"@.+.;.D R.P.e '.>.*.1 @.$.*.%.$.*.%.` ,.` ` ` ` %.,.).>.>.!.a.!.D.k.k.>.U.6 &+7 .+]+<+<+6 {+U 0 X.F.<+5 /+]+5 ]+2+0 K.B 1+0 .+3+p B.1+M N p.~.~.n.% n.% <.| C.: C.: : C.Q H H : & p.'.<.k.] }+{ ",
"$.=.{.D S.4+J '.>.%.-.*.*.*.*.*.*.,.>.` ` ` ` ` ,.>.).).>.'.a.~.H.n.~.D :+l ^+^+8 3 5+5 0 N.n 5 3 *+/+/+l U.;+/+/+&+b Y.Y.8 7 6+7+F.B.R % R | p.% % C.<.<.u.C.u.W.: L & H e e K I u.D.<.R ] 8+~ ",
"1 $.;.D S.x N n.).&.*.%.%.%.` ` ,.>.>.,.&.&.&.&.>.>.).).>.,.a.a.p.p.H ^+3 |+2+9+3 (+0+a+3 -+5 3 ;+5+(+(+b+{+4 |+]+o g 1+Y.)+Z.q v v c <.C.W.u.u.v.% v.C.Q W.C.W.Q Q H & H I G d M & v.| R ] 8+~ ",
"1 -.+.- K.c+N R '.` %.&.,.>.a.S !.).>.>.` ` ` ` >.>.).'.>.a.!.H.~.'.}+d+l *+r > E.o.s./.e+|+5+3 4 ;+3 b+4 :+9+;+6 X.6+|+[+_+Y.A.o (.A.W.K L Q : : W.W.Q K L Q L K K K K G J G d G W.% W.u.P.Y.T ",
"$.#.+.$+K.c+M k.'.,.&.` *.` ).a.~.'.>.'.'.'.'.'.>.>.).'.a.S k.R D.r.0.W.'.>.~.H.k.<.p.H.k /+! o f+g+h+i+j+4 4 (+5+0+f+4 5 ;+/+-+}+.+K.y.K I W.y.L e K K H I e I e e I K G J d P P I : : u.P.Y.T ",
"&.&.1 $+K.V.O % ~.a.'.).a.a.a.S !.!.a.a.'.'.'.!.'.'.!.a.'.H.n.A s A.T ( _ J L p.% % C.e E./+B.k+l+m+0+4 b+,+n+f+(+j+n+j+(+o+(+|+9 8 1+Q H K I & K I e J N d e K J I e d G #+N P A I W.C.W.x Y.c+",
").'.*.' Y p+r.% ~.S '.).a.S ~.~.k.k.H.H.S S S ~.S S S S k.~.v.R y `.`.=+!+)+)+F.q+f 0.5.P.l F.y V r.K y.W.H p+n+i+)+Z.,+i+r+(+>+|+<+ +Z ! /.C P #+#+#+#+N #+G e J #+M G N A A A h I : & C.P.=+@+",
",.!.%.' Y O.h.v.R k.~.S S n.% p.R D.H.~.k.D.D.D.R D.k.~.n.u.| p.K.b  +U.!+`.6+`.=+8+X.-+_+U.'+Z T ( 9.N N & I / m+)+Q ! T.2 3 g+>+;+8 9.d G.X h.O P A N N N M #+G O O #+G.G.G.G.s+d & G K V.Y.x ",
").H.)., N.O.h.: | <.p.R D.u.: C.v.<.n.k.n.p.p.p.<.p.D.k.R v.y.9.8 8 &+0 {+8  +.+a ++b 0 -+&+.+0 -+{+a X.`.Y q w 0 P.9.' 0.t.G ) 5 n+4 / P P O G.G.O N M A N N N N G.G.P O G.s+9.s+A #+#+J L.[+L.",
"S k.'.> n Z F H C.y.u.C.C.C.: : : v.<.v.u.<.p.<.p.p.R | v.C.C.q &+&+8 8 8 -+&+a X.)+7 8 ]+7 ]+7 8 {+a .+)+9 ^+^+l ,+]+U p+- I.E.P 7+m B.l.= O r.= = A A O P P A P h h = h.h.t.q.q.P e N G L.*+O.",
"D.n.S ) 1+Z q.J L H Q L Q Q L H L y.W.W.: u.v.C.v.% C.W.W.e F <+<+6 l 9 0 ]+9 ^+a {+]+:+2+<+2+<+/+_+5+=+b }+]+U.{+]+5+7 0 9 a ++J.P.T.s C Q.y l.= s+s+N r.h G.G.A G.h = = F :.5.:.P G O N 7+*+p+",
"<.u.p.] 8+w _ N G J G e I e J J K & H y.H Q : Q W.v.: : K G X.|+;+b+b+;+-+4 ;+5 l 8 2+9+b+3 4 /+2+0 7 B.s J.g 1+&+a 6 {+++ + +-+b |+4 _+2 I.E.x h.G.r.r.r.r.h h N O G.s+= s+t.z s+P #+d G 7+b V.",
"W.: | E 6+u s.P A #+A #+G d #+M M A M e e H H & K & L I = w `.c t q+B B q U Y.!+8 8 m m j+>+m 0+]+`.-+B.U.b `.8 <+0 ^+a U.}+Y U K.~+7 s > ( /.o.s+9.s+= h G.G.O N O G.r.r.9.h.z 9.e : e y.V.b c+",
"K I : 4+Y.v M.A N N P A P G.P M A N #+d M J K I J J J J E.c+U 3+0.5.O d G K J G I P x m t+l+g+h+]+b+/+;+5 '+U.2+6 0 6 ++8+q+v v f Y.)+R.V.) 0.w.= r.G.P P O N G #+G P G.= = h.h.r.d y.& H c+[+c+",
"I J L @+Z.r Q.r.r.G.r.r.r.s+r.G.P P O G.P #+G d G G G G M :.*+b b ++n c c E o.h.q.x 1+|+R.p c }+`./+i+h+9+)+3 3 2+5 9 0 a _+ +c Y.5+0 B.(.q %+X r.G.O P A P N d G e N A r.= = 9.r.G K J H c+[+x ",
"e d e L.Z.r M.s+h G.= = h.= s+= G.h h r.G.N #+#+#+#+#+#+O Q._+*+[+=+[+=+Y.Z.b '+K.F. +;+9 F.@+0.o.A t.v 8 r+0+0+,+;+b+>+;+<+/+7 <+]+8 f = q c _ r.A N P N M M N G e M A = 9.h.h.= M e G K c+Z.P.",
"G N #+O.b t Q.F 5.z h.r.:.s+r.= r.h O P O M G d N N N N d V.X.++Z.Z.Z.*+a [+B.o b Y.U S.S.b .+X.^+6 K.Z $+p ]+n+0+3 |+9+0+0+;+]+3 ;+8 u+#+d c+> r.M d P N d #+P #+G A O z z t.t.:.G.J e I c+Z.T ",
"d N J L.*+o - q.h.z z t.h.9.z h.9.= h O P O N A P M A r.e *+6 _+U.&+a U.{+a Q.[+a .+*+!+[+n 1+S.K.K.U.l  +8+6+`.,+2+<+l &+&+]+(+o+4 <+7 1+{ :.5.G.h N #+A A N N N N P h 9.h.F 5.h.N & J L c+[+4+",
"A G.#+O.*+o - q.z t.z t.z :.t.z = s+s+= r.G.h O A P A #+C |+2+5 b+(+:+/+4 s C 9+b+^+5+]+{+a U.++U.++}+++b )+)+)+N.=+g (.~+{+{+.+9 <+,+ +K.Y ++Z.E.N N O #+N P P N P G.G.= z F z F N J G u.x [+P.",
"O h A f `.2 y _ q.5.z F z :.t.z r.P P r.r.G.h G.N O M A '+8+O.0._ _ h.:.C C Q.E.u b+a+|+2+/+6 5+ +U.b X.)+`.`..+|+v+Y.S.S.n B.c (.Y.8+{+g 4+R.q+h 9.P A P A N N P A O r.q.h.s+= 9.N Q J H x =+@+",
"P G.N O.`.2 0.C C C F F h.9.z h.:.G.A h G.G.P P N M.D o.h Q G.= h.q.z s+5.X ( r.c g+a+g+m 0+(+3 /+{+{+8 .+ + +!+=+!+v+Z.`.b =+~+S.S.8+!+U 3+*+, r.r.= O N N A P h #+N 9.= 9.h.9.:.M I J K x Y.%+",
"O r.A 3+`.B.' l.t.t.F z z z z z h.h r.r.r.P N P M h > Z =+K.7+> I.F h t.X _ ~ > ++_+o+S.T._ G./.p _+Y.n ^+)+8 0 ++;+0 -+ +7 v+b Z.6+K.0 }+U.Y./.= 9.r.h N N N N P G.A G.s+r.s+h.= d & J Q x =+E ",
"h r.P f v+c , X 5.q.q.t.z z z z :.h.r.h.= G.O G.N M A = ( &+l =+`..+n Q.q.q.w+*+5+j+0+:+U ' t.t.k }+n {+5 -+6 7 b 2+/+:+/+:+{+++v+[+'+] 2+^+0 l.G.h.9.r.O P A A P G.N P = r.s+h.s+G : & L x Y.{ ",
"r.= O f ++A.; s._ C 5.5.t.t.q.q.t.F h.F :.s+r.r.N N P h.z o.p s S.g V.t._ l.U X.n S.v+8+S._+4 5 b *+g /.n 6 b+5 X.a 5 ;+2+9+5 7 {+b q+s.J v 4 z h.= r.G.s+h O A P r.s+z 9.s+9.z 9.G : L W.P.Y.%+",
"s+= G.Z !+F.> k l._ _ C _ _ l.l._ F q.t.z 9.s+9.9.h P F 5.9.N A z L./.l.o.( )+0+:+6 b o n E } p+++++c v+m 4 >+o+7+= O 3+0 {+:+(+]+8 A.}+c ~ 6.z r.= h.h h.= G.P O r.r.h.h.= 9.z z M Q & : P.6+] ",
"h.:.G.f !+A.> M.V V X l.C C o.w.o.V o._ F z h.h.= r.= h.h.q.= z t.M.w.M.E.E X.8+;+h+)+q 2 1+]+++`.&+Y U a 0+4 d+5 ;+(.^+]+9 E p+Y.b+!+++'+S.!+p h.s+F 9.r.= r.r.G.= r.9.h.h.z :.:.G y.e y.P.6+T ",
"_ C h.u+U.J.) T.X X o.V X s.} w.M.Q.M.} I.s._ t.9.9.z :.= r.= :.Q.q.I.D ' g K.U E b+9+0 &+1+'+|+`.U {+0 U.++n 6+3 f+9+|+2+,+7 (.5.Q.++8 F.8+n z s+s+s+s+= h.= 9.r.s+h h.h.9.:.h.z #+H J H P.Y.%+",
"5._ z q+U.J.^ E.I.} k V X s.k V T./.T.E.E.E.I.I.X V w.V o.5._ E.q.h.:.~ ~ 1+Z.F./ j+~ 8+> r.J.6 /+<+&+-+8 ;+a+]+'+U 2+9 8 8+s v )+8 v (.U.q+1+G.r.r.s+r.r.= r.r.s+:.h.:.z :.t.F F P e G Q x Y.4+",
"l.V l.r .+R.! T.Q.Q.Q.M.I.w.w.I.( - ( Q.I.E.E.E.} Q./.E.Q.T.D q.F h.5.- c+Y.X.) q -+V S.I.~ 8 /+/+_+t { b !+|+^ s.J.|+9 &+++6 q+s+P.q.! ' &+Y.O 9.h :.= = h.= = z t.z 5.5.q.C 5.t.h #+d K x =+T ",
"s.V o.r .+K.P.6.( /.T.Q.T.Q.Q.D - 0.- T.E.E.D Q.D - /./.0.( _ z = l.V E.U [+v+> Y.Y , u w 6 5 /+<+.+(.> ++k > P.z 5+9 <+*+K.^ > 1+0 Y.I.Q V -+P t.h.F F F h.:.t.h.t.z C l._ 5.q.5.G.N P y.x Z.%+",
"k k s.s X.U @+; - ( /.Q.0.( D 0.0.$+6.$+$+- ' /.' ' ( - $+V h.q.} 0.c+r 7 `.q ' l w { 7+*+>+2+;+/+++U 3+U.- @+V.B ,+;+9 !+(.( = r.M.L.b Z.- ' r.F _ q.q.C 5.z F z q.q.C l.l.X X q.N #+d H @+Y.T ",
"w.} I.t X.U @+/ ' y /.M.0.0.' ^ / ' $+$+6.6.- , S.y $+> ( V 0.) S./+o+(+<+v+3+{ >+w P.4+m b+5 5 /+ +1+o `.P.E s 2+4 3 ]+8 v 0.k k /.l.O 5.{ Y.4+l.l.5.o.X _ _ t._ X k w.M.E.w.s.X z #+G.K L.Z.%+",
"w.M.Q.t {+n V.^ 0.0.0.- ' 6.; ! ~ ^ > ) ) , 6.]+<+/.$+y ( E ) > 5 ,+4 -+8 U.] 3+|+B { (.l+b+5 ;+]+)+N.1+c @+' {+b+3 3 2+]+Z ) ; / ~ 6.E.0.- /., / / > , / ) ) > ' 6.> > ! ! ) ) ! ( C 5.G c+Z.%+",
"E.D /.g _+~+L.) ' - y y , ' 6.) ) %+] ] > / y K.b+/ > x 4+) > g d+9+5 8 &+U { J.)+e+E 4 f+4 ]+2+<+!+n !+r %+] a+<+;+:+9 6 V./ 4+> %+] D ' , > ^ ^ ^ > > / ' ; , 6.^ ] { ] { ~ ] ) T.o.k h.3+*+@+",
"E././.B._+~+p+! ' 0.y y $+, / > ; > ; ] { ) ; ~ <+K.L.x ^ ) ~ |+(+|+6 &+8 Z x !+K.p `.r+i+3 4 2+5 U.=+)+L.! K.,+/+<+<+]+&+) / p+> c+4+$+> { ' ) ~ ~ ~ ~ ) > ! ^ ) ! ) ^ { { ] ] ; M.o.} :.B  +7+",
"Q./.T.c {+N.c+; ; y y 0.$+$+/ / $+, , ~ E > > ' 2 g+t E { ; 8+j+o+|+9 -+6 V.V.5+q w f+m d+,+;+|+<+X.`.Z.w c+3 0+/+]+&+8 U.' - B { x x 0.! ) ) ) ~ ~ { E ~ ) ] ! ~ ) ) ~ %+{ { { > E.l.V q.B  +u ",
"D D /.F.&+}+O.! ^ $+y ( y 6.> 6.0.> ^ ~ ~ ! ~ / 7+b |+2 ] s U.[+c+f 8+`.!+~ Z b+B {+m+n+m n+2+,+b+^+)+(.Z a |+,+|+ +.+3 U $+Q.w+] p+u+' ; ; ! ! ^ ! { E ! ^ ~ ) ] ) ) ] E { E { ! M.5.5.:.q+.+p ",
"I.D /.J.&+}+3+^ $+0./.- 0.y , > 0./ > %+P.] ) ; E o Z.4 U b w+V.) n *+++A.E U /+`.Y.}+S.X.9 X.)+F.8+^+q S.Z.e+.+'+> u+9 ~ D /.f { O.L.y ( /./ > %+) T { { ) { %+) ) P.{ { ~ ) P.' Q.l.} t.4+`.v ",
"L h q.v  +Y T T.w.k F 5.} ( ' $+$+' , > > $+$+D /.P.g ,+l &+,+Y t U. +/+Z c+ +/+N.q { 0.*+*+n r ) c+)+3+B $+q 0 1+> s 7 P.~ 3+[+P.] V.q.= q.l.C I.} Q.s.s.X V C _ Q.0.0.T.D ' ' w.V G.A H 4+=+P.",
"O l.h f ++2 / C h._ r.r.5.V C s.k o.k E.V o.s.V M./.B 0 0+f+t+4 t+a ^+)+w+S.b+5 _+(+c c+X. +6+S.V.f v+++1+K.!+/+3 O.6+v+q u+o 5+(+B.f , ! ~ %+6.y y y 6., , ] 3+$+$+) ; > ) / $+( s.l.M./.2 8 t ",
"s+k o.s X.'+E k 5.t.r.A = - / ' 0., - z O z z X Q.} y 3 n+x+k+k+y+m+n+>+;+o+(+9+.+n+9 9+]+!+{+v P.S.X.)+n &+++n 8 p O.O.b+9 ++*+U Z.o ; / / ; ' - T.> , / ) / ^ ~ T.' V.3+@+T > ) > M.V k c )+e+",
"9.V q.q a J.! M.( Q.D _ k d N A d e n.% K Q Q P q.( ! m g+k+z+A+g+t+t+t+t+h+r+9+X.m+5 )+`.b `.K.U.&+!+{+<+5 8+F. +8+-+Y.X.0 N.X.&+b Z Z x L./ E 4+! { T x ~ V.L.P.{ E ~ %+T { E ^ 6.M.Q.E.t _+S.",
"M.D k c _+8+c+^ Q.k E.V 1 1.+.[ #.@.-.H.D.y.G e r.t.k (.l+m+;+g+i+g+i+j+m 0+,+(+j+m n+o+o+9+a [+=+b a 5 :+<+/+:+6 .+a n J.++a 0 U Z s A.0.$+0./.- 0.5.} q.o.z w.5.~ 4+^ , E.w.6./ $+X y I.2 {+r ",
"q./.; B.&+6+4+- 6.D T.V.' { { ' ( } > s o 3+V.4+P.L.3+, %+L.) p+Y. +`.`.0 l <+<+]+`.U J.o P.> F.B 1+Y.S.2 X.8 -+X.K.7+w+7+Z /.Q u.C._ M.l.5.k k X Q.Q.$+0.' ) ; > 6.6.0.' /.( / 0.q.z ( s.q  +B ",
"y ( o.f )+S.x > y $+0.k $+> > ^ ] Z B f c+7+> E / 0.y ' ( ! E 4+] ' _ :.( / 4+p+V.%+~ ; z P M %+) ] { > ) ~ / y } Q.E %+; ! ~ 4+x 3+q w+V./ /.z 9.N z o.V X t.V M.6._ F = s.X = I.l.F = h.w )+u ",
"h.} _ 7+!+K.B > } o.( I.y E V.~ ] ~ $+, %+~ ~ ^ { x / 0.$+/.Q.y ; { { E / ) P.4+P.{ ^ ^ /.( , x 6.x u+u ~ T u+c+c+%+{ / ^ / $+> / T c+x u v w+c o A.2 B.v q+4+f (.Z.g Z L.o 3+q+u+w ~ , , N.6 [+",
"0., /.S.6 Y.V.! ' / $+) ] ( ( $+4+q+w+B c g q w+N.Z w+c+@+e+V.@+c+p+c+E E { T E P.P.L.E E T L.@+7+P.P.; ) ~ %+E %+E ^ ] ' / > P.w+7+x x L.e+v B O.J.Z B t s O.@+p x p+s A.v w @+Z J.F.B p+u 5 1+",
"k Q.Q.2 {+Y Z ~ T V.w+] L.B c+q s 7+! E ' ! ~ @+7+3+> ] E P.3+O.Z c+P.P.x f g 3+B v 4+! %+V.L.f x ) P.! { c+{ u+7+^ E T L.~ 4+T ' L.4+] ] { E w u @+P.%+B f 3+t '+(.B.3+L.V.^ , ' ! ; , Q.g .+2 ",
"l.5.o.s U.U Z 6.D ' 0.; / $+) / - ) { Z o f T c+{ T ~ L.; 4+w V.) ^ P.' ! ' M.T.; @+x ) ] , T @+7+P.T 4+p+P.L.L.L.{ E x 3+T $+y { ~ T Z o t R.t f q L.p+$+{ P.Z q+w B.Z %+w+O.7+@+- w.E.s.B  +q ",
"s.F O w+++c ! E.X w././ > ) 4+%+4+; ' ; 0.P.x 0.} ) / x @+' %+P.P.) ' 0.^ e+c+c+x B q q+E ; L.w { ^ - y ) T Z E ) > O.0.$+> ] - 0.x 4+, , - , , > ^ { ~ ' ) %+/ / ] 3+E 7+3+@+e+O.E p+q+P.U 6 J.",
"s+l.l.t .+A.L.E ) Q.6.0.E T p+{ /., I.$+k = C %+o g { $+4+s '+~+t @+; ~ x T , > / ~ $+, ~ L.S.Y 0.^ ! ' , ~ ' y E - ) ' - > x ) } y 6.~ u+7+O.p u 3+O.f Z L.) ! 3+3+q+Z 3+~ 4+L.! , E y ; (.8 (.",
"5._ t.f )+'+T ~ ^ $+/.E.0./.T.E.X E.M.Q.I.; ! { $+P.) y 7+R.] T 0.( D 6.' ( } k M.T.' 0./.~ ; 6.' ( , V.! 0./.' $+) E P.' 0.0.4+; { { O.V.w ~ / 3+^ ) 6.6.@+~ ~ / ( ( / ) / 0.' D :.w.T.h.q+++w ",
"s+h e ) ~+s D V F X C D /.E.y Q.^ E @+B r w M.- V., ' E 0./.D k I.X w.( } y D V } Q.M.! $+( ) - V l._ } k /.k } E ' V.w V.6.~ 3+x ! ) T P.^ { $+> y w.y ( /.; o.D > ' p+~ /.M.} J.c ' = O q {+o ",
"z z & } v V.} s+h z X M.X z z M./.] t N.(.V./.5.( 4+E E.:.I H C.H A w.> s.L I :.h.E.( - 0.M./.M.D G h.X A #+I Q O #+L A F l.Q v.d :.F l.F I.D T.E./.~ @+L.V.~ w.T./.} Q.} w.} I.q.h h s+h.t a r ",
"H L D.u.0.M.& % a.'.H.'.%.v.J v.k.A J & C.% P n.C.a.<.%.).N D /.:.L J q.y s.y X t.T.O.' K G.G.G.r.& D.p.J u.H.%.%.` S 1.@ B+1.` <.<.!.<.I G r.9.t.h.5.h.h s.I.:.M.7+E ] 4+{ M.} s+R C.t._ N.0 (.",
"T p+' G.C C e H.%.>.H 9.l.k T.M.A r.u. .*.K s+M.A r.k 4+/ X E.I.9.h I u.K e Q A t.O 5.9.h.t.I u.).a.` a.1.* . 3.7.z.^.a.).).'.a.H C.<.e M q.t.s+P O u.:.O G O C.D.a.n.& C.G J p.C.!.'.R /._+<+A.",
"$+P.p+Z w+@+o.r.#+N V V h.h.d s+h.o.C D T.! $+/ y - I./.$+T Q.5.s+G A 5.t.h.h h w.y Q.q.:.z h P ).>.{.=.1 %.[ [. .-.` ).` >.n.k.2.$.W.M v.P 9.l.:.G.P z e W.<.H : W.C.J P k.k.: J H.u.C 8+a+(+8 ",
"  O.Y  +]+9 .+Y.8+=+8+b 6+Z.b `.6+=+6+6+}+=+*+v+Z.Z.b Z.)+b b b n R.S.'+(.J.S.U U N.U 8+8+K.F.R.A.A.o v p e+e+e+v w w e+2 e+u g (.J.c 2 A.R.R.(.(.r B A..+~+8+7 9 5+9 U.*+N.R.K.Z.=+n 0 A+C+o+  ",
"    J.7 >+j+j+o+>+,+,+a+a+o+>+>+,+0+a+o+0+a+,+>+4 >+,+>+,+>+4 4 >+4 ,+a+,+4 (+b+b+b+b+b+4 4 b+(+(+4 b+9+|+(+b+(+b+3 3 3 :+5 5 :+2+5 2+|+3 ;+3 3 (+>+(+>+,+>+o+0+a+a+o+>+4 9+|+,+m j+h+g+l+b+    ",
"      u R.R.g 2 2 g A.J.F.A.U ~+~+n Y N.U S.Y U ~+U U K.U K.U ~+Y '+N.U U K.B.c J.o g c J.J.B.S.8+1+6+N.F.K.8+K.2 c Y Y K.n A.A.F.c s J.c (.o (.R.J.U Z.~+1+S.N.N.(.g ~+(.v+-+ +*+X.o+9 )+      "};
END

    close(ICON);
}




__END__

=head1 NAME

B<PerlJammer> - A music jukebox in Perl

=head1 VERSION

Version 1.13.0

=head1 SYNOPSIS

perljammer [options]

  Options:
    -autoplay
    -geometry
    -nonet
    -help, -usage, -?
    -man
    -debug
    -version

=head1 OPTIONS

=over 4

=item B<-autoplay>

Begin playing automatically after creating the playlist

=item B<-geometry>

Specify the window geometry on the command line (default: 800x600)

=item B<-nonet>

Do not open a remote-control network socket

=item B<-help, -usage, -?>

List command-line options and usage, then exit

=item B<-man>

Display full documentation and exit

=item B<-version>

Display version string and exit

=back

=head1 DEBUGGING OPTIONS

=over 4

=item B<-debug>

Produces debugging output tracking changes in the playing-selection index.  If set to 3
or higher, also reports artist/disc/title for tracks played, playlist items selected,
and drag-and-drop actions.

=item B<-log>

Direct debugging output to HOME/perljammer.log.

=back

All other options are set in the configuration file ($HOME/.pjam/config).

=head1 DESCRIPTION

B<PerlJammer> is a music jukebox written to replace ike Oliphant's abandoned MP3 jukebox
application B<DigitalDJ>.  It generates and plays random playlists from a DDJ-compatible
music database, and accepts remote control commands via its network socket.  If you do not
already have a pre-existing DDJ database, tools are provided to create and populate a
compatible SQL database containing only the tables and fields actually used by B<PerlJammer>.
A B<pjam-remote> companion program is provided for sending remote commands via the network.

B<DigitalDJ> could only play .mp3 files, but starting with version 1.11.0, B<PerlJammer>
also supports Ogg Vorbis files.

A new playlist is randomly generated each time B<PerlJammer> is started.  B<PerlJammer> can
dynamically modify the generated playlist while playing via insert, drop, and instant-replace
actions, or generate a new playlist on demand.  (Inserts and in-place replacements can only
be performed via the remote interface.  New playlists can only be generated from the GUI.)
Songs not currently playing can be rearranged via drag-and-drop in the GUI.  Additionally,
B<PerlJammer> can automatically replace any single track in the playlist with the entire disc
that it came from, in track order.

The command-line options listed above allow changing a few of B<PerlJammer>'s startup
parameters.  All of these options can be abbreviated to their first letter.  However,
the majority of B<PerlJammer>'s operating parameters are set in its configuration file
($HOME/.pjam/config).  From version 1.12.0 forward, .pjam/config is TOML compliant.
B<PerlJammer> SHOULD continue to correctly read 1.11 and earlier config files.

=head1 INITIALIZATION

On startup, B<PerlJammer> will first parse and act upon its command-line options.  If
none of the options -help, -usage, -?, -man or -version are used, it will next attempt
to open its configuration file.  If the file is not found, it will create (or attempt
to create) a default config file template, and then exit.  Assuming the file exists,
B<PerlJammer> will read the configuration file, merge the options from it with the
options supplied on the command line, and apply its own internal defaults for any
option not specified.  The following settings MUST either be correctly specified, or
the built-in defaults must be correct:

=over 4

=item B<mp3playercmd:>

Must point to a valid mp3 player executable and contain a placeholder for the mp3
filename

Default: /usr/bin/madplay -b 32 -Q -G %s

=item B<mp3playercmd:>

Must point to a valid Ogg Vorbis player executable and contain a placeholder for the .ogg
filename

Default: /usr/bin/ogg123 %s

=item B<sqlcfg:>

Must point to the SQL config file containing database login information.

Default: ~/.my.cnf

=item B<sqlgroup:>

Identifies the group name to use in the SQL config file.

Default: pjam

=item B<sqldb:>

Must contain the name of the SQL music DB.

Default: THIS ITEM HAS NO DEFAULT.  YOU MUST SET IT.

=back

Other options in the configuration file allow specifying the size of generated
playlists (default: 500 songs), whether to place the controls at the top or bottom
of the window (default: bottom), and others.  See the config file for details.

=head1 STARTUP

Assuming a valid SQL DB has been specified, B<PerlJammer> will now open its network
socket for remote control (on the port specified by the B<remoteport:> setting,
or the default port 16384); create its user interface; attempt to connect to the
SQL server to read the music database; and create a playlist.  At this time,
B<PerlJammer> does not have the capability to create the database for itself; it
relies on using an existing database created by DigitalDJ.  It expects one
additional field to exist that is not used by DigitalDJ: 'disable', a boolean.
The playlist will be randomly generated by retrieving all non-disabled music in the
database (tuples for which disable is set to 1 will be skipped), then randomly selecting
tracks until the number of songs set by the b<songlimit:> option (default: 500) is
reached.  B<PerlJammer> guarantees no tuple will be added to the playlist twice, but
does not guarantee no duplicate songs if the same song exists in multiple locations
on disk (and therefore has multiple tuples in the database).

Once the playlist has been created, B<PerlJammer> will wait for a user command to
begin playing, unless the 'autoplay' option is set in the config file or used on the
command line.  The playlist can be interactively modified once created; you can re-order
songs by drag-and-drop, delete songs from the playlist, add new songs (in the database
or not), or replace an individual song by the entire disc it comes from.  When adding
songs not in its database, B<PerlJammer> will attempt to read artist/title/disc
information from ID3 tags, but will not use them for mp3 files found in its database,
as ID3 tags are frequently incomplete or inaccurate.  (Which is a polite way of saying
that many MP3 files floating around the Internet appear to have been ID3 tagged by
drunken monkeys using a ouija board.)

=head1 CONTROLS

B<PerlJammer>'s main panel contains fourteen buttons in two groups.  From left to right,
the first group of buttons perform the following actions:  START, PREVIOUS, PLAY/PAUSE,
STOP, WAIT, NEXT, END.  Most of these buttons should be obvious.  The PLAY/PAUSE button
toggles between PLAY and PAUSE functions as appropriate, and turns red when a song is
paused.  In general, buttons which start or change things have blue icons which highlight
green when moused over; buttons which stop or delete things have black icons which
highlight red when moused over.

The WAIT button is a new function in B<PerlJammer>.  It finishes playing the currently
playing song, then advances to the next song and stops the playlist, ready to resume
play at any time.  The button turns red and changes its appearance while waiting for
the playing selection to end.

The second main group also contains seven buttons.  The first button in this group,
BREAKPOINT, inserts a breakpoint into the playlist at the position after the currently
selected song,  This does not change the selection or the song playing.  When B<PerlJammer>
reaches this breakpoint, it will pause playback and the selection will be set to the
first song after the playlist.  Multiple breakpoints are allowed in the playlist.

The second button, INSERT NEXT, identifies the disc containing the current selection,
which may be but need not necessarily be the current playing song, finds the next track
on the disc if the selected song is not the last track on the disc, and inserts that
song into the playlist after the current selection, then sets selection to the song just added.

The third button, PLAY DISC, also finds the disc containing the selected song, but
inserts all tracks from that disc, in track order, at the selected point in the
playlist.  If B<PerlJammer> detects that the selected song is playing B<and> is the
first track of the disc, this operation is performed seamlessly without stopping and
restarting play.  Likewise, if the selection is not the current playing song, the
operation is of itself seamless.  (Of course, if you insert a disc B<before> the playing
song, this will change the position in the playlist of the playing song.  See the
caveat below.)  If the selected song when the disc insert operation begins B<is> the
currently playing song, and B<is not> the first track of the disc, then B<PerlJammer>
will stop playback and restart from the first track of the disc after the insert.

The fourth button, LINK WITH NEXT, marks the currently selected song and the next song
in the playlist to always be kept together.  Once so marked, the songs will always be
loaded together when generating new playlists.  Linked songs will be shown in the playlist
with a pale goldenrod background.

The fifth button in the group, DROP, simply drops the currently selected song from
the playlist and, if playing, resumes play from the next song in the playlist.

The sixth button, DISABLE, not only drops the song from the current playlist, but
marks it to be skipped when building all future playlists.  B<THIS ACTION CANNOT BE
REVERSED FROM WITHIN PerlJammer AT THIS TIME>.  Accordingly, before this action is
taken, a dialog will pop up asking whether you are sure you want to disable the song,
and allowing you to either confirm the action or cancel it.

The seventh button and last button in this group, NEW, generates a complete new playlist
and resets the selection to the start of the new playlist.  If a song is playing when
this function is invoked, the currently playing song will not be interrupted, and will
become the first song of the new playlist.

The Quit button at far right, and the Help button at far left, should require no
explanation.

You can re-order songs in the playlist via drag-and-drop.  If a drag-and-drop action
(or a drop/disable action) changes the position in the list of the currently playing
selection, either directly or as a side-effect, PerlJammer will automatically adjust
for the new location of the selection.

=head1 PERLJAMMER HOT KEYS

From PerlJammer v1.7.0 on, the list of active hot heys has been revised.  PerlJammer's
UI now responds to the followng hot keys:

=over 4

=item B<Home:>

Go to the first song in the playlist.

=item B<Home:>

Go to the first song in the playlist.

=item B<Left arrow:>

Skip to the previous song in the playlist.

=item B<Right arrow:>

Skip to the next song in the playlist.

=item B<End:>

Go to the last song in the playlist.

=item B<Space bar:>

Start play at the current selection, or pause/resume playback if already playing.

=item B<Alt-S:>

Stop playback.

=item B<Delete:>

Drop the current selection from the playlist.

=item B<Up arrow:>

Increase volume one step, if volume control has been configured.  (See B<PERSISTENT
GAIN CONTROL> below.)

=item B<Down arrow:>

Decrease volume one step, if volume control has been configured.

=item B<Alt-N:>

Generate a new playlist.

=item B<Alt-W or Ctrl-Q:>

Quit PerlJammer.

=back

=head1 PERSISTENT GAIN CONTROL

If you enabled the gainctl option in your config file, which you should do ONLY AFTER
setting up a working volume change script for your sound system, there will be a yellow
gain control button (split into upper and lower halves) in between the two main control
groups.  Each click on the upper half will persistently INCREASE the playback gain for
that song by the amount you have defined as one 'step'.  Each click on the lower half
will persistently DECREASE the playback gain for the song.  PerlJammer will save this
gain adjustment in the database and remember it from session to session, and dynamically
adjust the volume from track to track to keep output volume at your preferred level.

=head1 B<PerlJammer> REMOTE CONTROL

The pjam-remote program can be used to send remote commands to B<PerlJammer> via a
network socket.  Available remote control commands and their synonyms are:  START,
PREVIOUS/PREV_SONG, PLAY/PLAY_SONG, PAUSE, UNPAUSE/RESUME, PLAY_OR_PAUSE, STOP, WAIT,
NEXT/NEXT_SONG, END, PLAY_DISC, DROP, INSERT, INSTANT, REPLACE, and STATUS.  All of
these commands are case insensitive.  Most of them are obvious, performing the same
functions as the buttons of the same name.  PLAY_OR_PAUSE is a toggle that starts
play if nothing is playing, and otherwise pauses or unpauses play according to whether
or not playback is currently paused.

The REPLACE command takes a full path to a single MP3 filename as an argument, and
replaces the currently playing selection with that file.  The INSERT and INSTANT
commands both take a list of pathnames, and insert those files into the playlist.
The difference between the two is that INSERT inserts the list after the current
selection, while INSTANT stops play and inserts the new songs before the previously
playing selection so that they are played instantly.  The STATUS command causes
PerlJammer to send back a reply consisting of the artist, disc title, song title, and play
time of the currently playing selection.  If nothing is playing, the reply is the
single word 'Inactive'.  An optional numeric argument can be sent with the STATUS
command; in this case, if sent the command B<STATUS n> for some integer value of
B<n>, PerlJammer will reply with the artist, disc, song title, and play time of the
next B<n> tracks in the playlist.

For obvious reasons, the DISABLE command cannot be sent remotely.  It is accepted
B<only> from the GUI.  A separate tool or function may be provided later to review
(and optionally revert) your disabled tracks.

=head1 pjam-remote

pjam-remote reads the same $HOME/.pjam/remote configuration file that B<PerlJammer>
uses, but it cares only about the B<remoteport:> and B<remotehost:> settings.  It
will attempt to connect to the port specified in B<remoteport:> when sending remote
commands.  The syntax for pjam-remote is as follows:

=over 4

pjam-remote host COMMAND [file [file...]]

=back

where COMMAND is one of the commands listed above.  The host can be omitted if the
target host is the host set in the B<remotehost:> setting.  It is possible to specify
a different port than that set in the B<remoteport:> setting, by using the following
syntax:

=over 4

pjam-remote host:port COMMAND [file [file...]]

=back

However, it is advised that you simply use the same B<remoteport:> setting for all
PerlJammer hosts on a given network.

=head1 PLAYBACK

B<PerlJammer> does not contain an internal MP3 decoder; it is a music jukebox, not
a music player.  It uses an external player to play back music files to enable you to
use your preferred player.  I personally B<prefer> and B<recommend> madplay for MP3
playback, for two reasons.  Firstly, madplay is a better decoder than mpg123, and
sounds better; and secondly, mpg123 stutters badly, or even falls over altogether,
under high system I/O loads because it does not buffer adequately.  (Additionally,
mpg123 does not support MP3 replay-gain adjustment.)

For the time being, only MP3 and Ogg Vorbis playback are supported (Ogg Vorbis support
is experimental in this version), although later versions of B<PerlJammer> may contain
support for declaring additional playercmd options for other music formats such as FLAC
and AAC.  FLAC support will probably come sooner than AAC.

=head1 KNOWN BUGS

=over 4

=item Columns in the playlist do not pack properly into the available window width
if geometry width is not 800 pixels.  This is because I have so far been unable to
get Tk::MListbox->pack() to work correctly.  I believe this is a Tk::MListbox bug.

=item Recent changes in the behavior of Tk::MListbox result in background tinting
in the playlist box looking like total ass.  I will fix this if and when I can.

=back

=head1 TODO

=over 4

=item Add a feature/command line option/tool to review and optionally re-enable
disabled tracks.

=item Add a user-definable way of setting volume increase/decrease commands.

=back

=head1 REPORTING BUGS

Please send all bug reports to the author.

=head1 LICENSE

B<PerlJammer> is copyright (C) 2003 Phil Stracchino.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=head1 OBTAINING PerlJammer

B<PerlJammer> can be downloaded from http://co.ordinate.org/perljammer/.

=head1 AUTHOR

B<PerlJammer> and its supporting tools are written and maintained by Phil Stracchino
(phil@co.ordinate.org).

=cut
