#!/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;

my $pjam_version	= '1.6.6';
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',
               'geometry|g=s',
               'help|usage|?|h|u',
               '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
    {
        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});

    exit(0);
}


sub jam
{
    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, @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 = "SELECT s.id, s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj
              FROM song as s, disc as d, artist as a
              WHERE s.artistid = a.id
              AND s.discid = d.id";

    if ($db_version >= 3)
    {
        $query = sprintf("%s AND s.disable & '%s' = 0",
                         $query,
                         $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] ];
    }
    $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})
        {
            $playlist[$i] = $songlist{$id};
            delete ($songlist{$id});
            $i++;
        }
    }
    
    for ($id = 0; $id < (scalar @playlist); $id++)
    {
        for ($field = 0; $field < 7; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert('end',
                                                $field == 4 ? sprintf("%02d", $playlist[$id][$field])
                                                            : $playlist[$id][$field]);
        }
    }

    $listbox->selectionSet(0);
}


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

    open(PREFS, $cfgfile);
    while (<PREFS>)
    {
        next if (/^#/);
        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.*\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/local/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->{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}            = '-*-CG Omega-medium-r-normal--*-120-*-*-p-*-iso8859' unless (defined $o->{font});
    (($o->{boldfont}      = $o->{font}) =~ s/medium/bold/) unless (defined $o->{boldfont});
    $o->{geometry}        = '800x600' unless (defined $o->{geometry});
    $o->{playercmd}       = '/usr/bin/madplay -Q -G %s' unless (defined $o->{playercmd});
    $o->{remoteport}      = 16384 unless (defined $o->{remoteport});
    $o->{sqlgroup}        = 'pjam' unless (defined $o->{sqlgroup});
    $o->{songlimit}       = 500 unless (defined $o->{songlimit});
    $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});

    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_default_charset='latin1';"
         . "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 => 0});
    $dbh->do("SET NAMES latin1");

    return ($dbh);
}


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

    $query = "SELECT s.filename, s.title, a.artist, d.disc, s.track_num, s.play_time, s.gain_adj
              FROM song as s, disc as d, artist as a
              WHERE s.artistid = a.id
              AND s.discid = d.id
              AND s.filename = '%s'
              LIMIT 1";

    $query = sprintf($query, $_[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]);
    }
    $sth->finish();
    $dbh->disconnect();

    return (@result);
}


sub get_mp3_data
{
    my $file = $_[0];
    my @data = get_sql_data($file);
    unless (scalar @data)
    {
        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);
        $mp3->close;
    }

    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})
            {
                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 => 'MidnightBlue');

                        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 => 'MidnightBlue');
                $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);

                if ($data[0] eq 'nowplaying' || $data[0] eq 'status')
                {
                    if ($data[1] > 0)
                    {
                        my $p = ($playing == -1)
                                ? $listbox->curselection()->[0]
                                : $playing + 1;

                        for (my $i = 0; $i < $data[1]; $i++)
                        {
                            last if ($p + $i > $listbox->size()-1);
                            my @row = $listbox->getRow($p + $i);
                            $data = sprintf("%s :: %s :: %s (%s)\n",
                                            $row[2],
                                            $row[3],
                                            $row[1],
                                            $row[5]);
                            print REMOTE $data;
                        }
                    }
                    elsif ($playing != -1)
                    {
                        my @row = $listbox->getRow($playing);
                        $data = sprintf("%s :: %s :: %s (%s)\n",
                                        $row[2],
                                        $row[3],
                                        $row[1],
                                        $row[5]);
                        print REMOTE $data;
                    }
                    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);

    clear_selections();
    $cmd = sprintf($opts{playercmd},
                      ($listbox->getRow($sel))[0]);
    ($player = $cmd) =~ s/^(\S+)\s.*$/$1/;

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

    $listbox->selectionSet($sel);
    $listbox->yview($sel < 5 ? 0 : $sel-5);
    $playing = $sel;
    $elapsed = 0;

    if ($opts{timebox_countdown})
    {
        my $time = ($listbox->getRow($sel))[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 = ($listbox->getRow($sel))[6];
        change_volume($vol) if ($vol != 0);
    }

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

    $player_pid = open($PLAYER, "|$cmd");
}


sub play_or_pause
{
    if ($player_pid > 0)
    {
        if ($paused)
        {
            kill ('CONT', $player_pid);
            $paused = 0;
            $playbutton->configure(-bitmap     => 'pausebutton',
                                   -foreground => 'MidnightBlue');
            $timebox->configure(-foreground => 'blue');
        }
        else
        {
            kill ('STOP', $player_pid);
            $paused = 1;
            $playbutton->configure(-bitmap     => 'playbutton',
                                   -foreground => 'red');
            $timebox->configure(-foreground => 'red');
        }
    }
    else
    {
        $active = 1;
        play_song($listbox->curselection()->[0]);
        $playbutton->configure(-bitmap     => 'pausebutton',
                               -foreground => 'MidnightBlue');
    }
}


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

    $playbutton->configure(-bitmap     => 'pausebutton',
                           -foreground => 'MidnightBlue');
}


sub stop
{
    if ($playing != -1)
    {
        resume_play() if ($paused);
        kill ('TERM', $player_pid);
        if ($opts{gainctl})
        {
            my $vol = ($listbox->getRow($playing))[6];
            change_volume(-$vol) if ($vol != 0);
        }
        close($PLAYER);
        $player_pid = 0;
        $active = 0;
        $playing = -1;
        $playbutton->configure(-bitmap     => 'playbutton',
                               -foreground => 'MidnightBlue');
        $waitbutton->configure(-bitmap     => 'waitbutton',
                               -foreground => 'black');
        $timebox->Contents('elapsed: 00:00:00');
    }
}


sub stop_after_current
{
    $active = 0;
    $waitbutton->configure(-bitmap	=> 'waitbutton2',
                           -foreground	=> 'red');
}


sub pause_play
{
    if ($playing != -1 && !$paused)
    {
        kill ('STOP', $player_pid);
        $paused = 1;
        $playbutton->configure(-bitmap     => 'playbutton',
                               -foreground => 'red');
        $timebox->configure(-foreground => 'red');
    }
}


sub resume_play
{
    if ($paused)
    {
        kill ('CONT', $player_pid);
        $paused = 0;
        $playbutton->configure(-bitmap => 'pausebutton',
                               -foreground => 'MidnightBlue');
        $timebox->configure(-foreground => 'blue');
    }
}


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 < 6 ? 0 : $p-6);
        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 < 4 ? 0 : $p-4);
        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 < 5 ? 0 : $p-5);
        play();
    }
}


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 < 5 ? 0 : $p-5);
        play();
    }
}


sub replace_song
{
    my $file = $_[0];
    my @data = get_mp3_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 < 7; $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 < 5 ? 0 : $p-5);
    play() if ($a);
}


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

    foreach $file (@_)
    {
        push (@rows, [get_mp3_data($file)]) if (-e $file);
    }

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

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

    for (my $i = 0; $i <scalar @rows; $i++)
    {
        for (my $field = 0; $field < 7; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($pp < $l ? $pp + $i: 'end',
                                                $field == 4 ? sprintf("%02d", $rows[$i][$field])
                                                            : $rows[$i][$field]);
        }
    }

    $listbox->selectionSet($p);
    $listbox->yview($p < 5 ? 0 : $p-5);
    play() if ($a);
}


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
                      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 < 7; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($p+1,
                                                $field == 4 ? sprintf("%02d", $$ref[$field])
                                                            : $$ref[$field]);
        }

        clear_selections();
        $listbox->selectionSet($p+1);
    }

    $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
                      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\"
                      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 < 7; $field++)
        {
            $listbox->columnGet($field)->Subwidget("listbox")
                                       ->insert($p + $i + $seamless,
                                                $field == 4 ? sprintf("%02d", $$ref[$field])
                                                            : $$ref[$field]);
        }
        $i++;
    }
    $sth->finish();
    $dbh->disconnect();

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


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;
    }
    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 = ifnull(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})));
}


###  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});
}


sub listbox_dclick_event
{
    my ($w, $infoHR) = @_;
    my @row = $listbox->getRow( $listbox->curselection()->[0]);
    printf("Playing %s\n",
           $infoHR->{-row},
           join(" : ",
                $row[2],
                $row[3],
                $row[1])) if ($opts{debug});
    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);
        $drag_start = $now;
    }
}


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

    $dragging = 0;
}


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

    $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});
    $listbox->delete($start, $start);
    $listbox->insert($end, [ @dragged ]);

    $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,
        @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(64,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',25];
        @list_top = ['%0',27];
        @list_bottom = ['%30',-2];
    }
    else
    {
        @bar_top = ['%30',-25];
        @bar_bottom = ['%30',-2];
        @list_top = ['%0',2];
        @list_bottom = ['%30',-27];
    }

    ($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	=> ['%64',-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);

    $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->columnHide(6,6);


# For some unknown reason, this column-packing code doesn't
# actually *work*.  It calculates reasonable-seeming column
# widths in pixels, but listbox->columnPack() doesn't actually
# honor them.  In particular, the last two columns will always
# be grossly overwide.

#    my $geom = $app->cget('-geometry');
#    print "Geometry: $geom\n" if ($opts{debug});
#    @widths = (0.34, 0.20, 0.34, 0.03, 0.03);
#    $width = $1 if ($opts{geometry} =~ /^(\d+)x\d+/);
#    $width -= 25;
#    for ($i = 0; $i < 5; $i++)
#    {
#        $sizes[$i] = sprintf("%d:%d",
#                             $i+1,
#                             int($width * $widths[$i])+1);
#        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);


    $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',
                                -command	=> sub { help(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'MidnightBlue',
                                -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		=> 1, 
                             -relief		=> 'groove',
                             -foreground	=> 'blue', 
                             -background	=> 'white', 
                             -selectforeground	=> 'blue', 
                             -selectbackground	=> 'white', 
                             -font		=> $opts{font},
                             -wrap		=> 'none')) -> form(-in		=> $app,
                                                                    -fill	=> 'both',
                                                                    -left	=> ['%6',1],
                                                                    -right	=> ['%16',-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	=> 'MidnightBlue',
                                 -activeforeground	=> 'green2',
                                 -anchor	=> 'center',
                                 -relief	=> 'raised',
                                 -width		=> 2,
                                 -height	=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%17',1],
                                                               -right	=> ['%20',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

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

    ($playbutton = $app->Button(-bitmap		=> 'playbutton',
                                -command	=> sub { play_or_pause(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'MidnightBlue',
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%23',1],
                                                               -right	=> ['%26',-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	=> ['%26',1],
                                                               -right	=> ['%29',-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	=> ['%29',1],
                                                               -right	=> ['%32',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

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

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


    if ($opts{gainctl})
    {
        ($upbutton = $app->Button(-bitmap		=> 'upbutton',
                                  -command		=> sub { change_gain(1); },
                                  -background		=> 'yellow',
                                  -foreground		=> 'MidnightBlue',
                                  -activebackground	=> 'black',
                                  -activeforeground	=> 'green2',
                                  -anchor		=> 'center',
                                  -relief		=> 'raised',
                                  -width		=> 2,
                                  -height		=> 1)) -> form(-in	=> $app,
                                  				       -fill	=> 'both',
                                    				       -left	=> ['%39',1],
                                    				       -right	=> ['%41',-2],
                                    				       -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 { change_gain(-1); },
                                    -background		=> 'yellow',
                                    -foreground		=> 'MidnightBlue',
                                    -activebackground	=> 'black',
                                    -activeforeground	=> 'green2',
                                    -anchor		=> 'center',
                                    -relief		=> 'raised',
                                    -width		=> 2,
                                    -height		=> 1)) -> form(-in	=> $app,
                                    				       -fill	=> 'both',
                                    				       -left	=> ['%39',1],
                                    				       -right	=> ['%41',-2],
                                    				       -top	=> $opts{controls} eq 'top'
										 ? ['%0',15]
										 : ['%30',-14],
                                    				       -bottom	=> $opts{controls} eq 'top'
 										 ? ['%0',25]
										 : ['%30',-2]);
    }


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


    ($discbutton = $app->Button(-bitmap		=> 'discbutton',
                                -command	=> sub { insert_disc(); },
                                -background	=> 'gainsboro',
                                -foreground	=> 'MidnightBlue',
                                -activeforeground	=> 'green2',
                                -anchor		=> 'center',
                                -relief		=> 'raised',
                                -width		=> 2,
                                -height		=> 1)) -> form(-in	=> $app,
                                                               -fill	=> 'both',
                                                               -left	=> ['%45',1],
                                                               -right	=> ['%48',-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	=> ['%48',1],
                                                               -right	=> ['%51',-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	=> ['%51',1],
                                                               -right	=> ['%54',-1],
                                                               -top	=> @bar_top,
                                                               -bottom	=> @bar_bottom);

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

    $app->bind('<Alt-h>' => \&help);
    $app->bind('<Alt-p>' => \&play);
    $app->bind('<Alt-s>' => \&stop);
    $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		=> 'MidnightBlue', 
                                   -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 twelve 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 contains five buttons.  The first button in this group, 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 second 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 itself"
." 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 third 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 fourth 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 fifth 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 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, 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"
."playercmd - 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:\nplayercmd: /usr/local/bin/madplay -o /dev/dsp6 -Q %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() to work properly.  I believe this is a Tk::MListbox bug.",
$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, December 29 2009, 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, $errorfont);
    $dialog = MainWindow->new();
    $dialog -> title("Confirm Disable Song");
    $dialog -> configure(-background => 'white');
    $dialog -> geometry("300x200");
    $dialog -> formGrid(10,1);
    $errorfont = '-misc-fixed-bold-r-normal--15-140-75-75-c-90-iso8859-1';

    ($errortext = $dialog->ROText(-width		=> 30,
                                  -height		=> 2,
                                  -padx			=> 5, 
                                  -pady			=> 5, 
                                  -relief		=> 'flat',
                                  -spacing3		=> 10,
                                  -foreground		=> 'black', 
                                  -background		=> 'gold', 
                                  -font			=> $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]);
#      $data = sprintf("%s :: %s :: %s (%s)\n",
#                                    $row[2],
#                                    $row[3],
#                                    $row[1],
#                                    $row[5]);


    $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, $errorfont);
    $dialog = MainWindow->new();
    $dialog -> title("Configuration failed!");
    $dialog -> configure(-background => 'white');
    $dialog -> geometry("400x300");
    $dialog -> formGrid(10,10);
    $errorfont = '-misc-fixed-bold-r-normal--15-140-75-75-c-90-iso8859-1';

    ($error = $dialog->Label(-text		=> '*** CONFIGURATION FAILURE! ***',
                             -font		=> $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			=> $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 $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 $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 command must be set to a valid command containing
# a %s placeholder for the filename.  It defaults to using
# madplay 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.)
#
# Examples:
# playercmd: /usr/bin/madplay -b 32 -Q -G %s
# playercmd: /usr/bin/mpg123 -q %s

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

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

# datadir: /usr/local/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

# 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.

gainctl: yes
volumecmd: /root/bin/chvol %d
stepsize:  5

# Geometry defaults to 800x600 if unspecified
geometry: 800x600

# Font defaults to CG Omega Medium 12pt
font: -*-CG Omega-medium-r-normal--*-120-*-*-p-*-iso8859

# Bold font defaults to bolded version of Font
boldfont: -*-CG Omega-bold-r-normal--*-120-*-*-p-*-iso8859

# Small font defaults to 10pt version of Font
smallfont: -*-CG Omega-medium-r-normal--*-100-*-*-p-*-iso8859

# Playlist size defaults to 500
songlimit: 500

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

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

# 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 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

# Autoplay defaults to off.  If set, PerlJammer will automatically
# start playing as soon as it has loaded its playlist.
autoplay: 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

remotehost: localhost

# RemoteJammer skins are 162x302 images in PNG, JPEG, GIF or XPM format.
# Default: /usr/local/share/perljammer/bluecurve.png

remoteskin: /usr/local/share/perljammer/bluecurve.png

# 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

# IMPORTANT:
# The following functionality is available ONLY on a database which
# has been either created, or updated to version 3, by pjam-dbtool.
# 
# 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.6.6

=head1 SYNOPSIS

perljammer [options]

  Options:
    -autoplay
    -geometry
    -nonet
    -help, -usage, -?
    -man
    -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

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 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.

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).

=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<playercmd:>

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<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 twelve 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 contains five buttons.  The first button in this group, 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 second 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 third 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 fourth 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 fifth 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.  Be advised that making
drag-and-drop changes that change the position in the playlist of a currently playing
song may have unanticipated side-effects such as causing a song to be played twice.

=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 MP3 files to enable you to
use your preferred player.  I personally B<prefer> and B<recommend> madplay, 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 playback is supported, although later versions of
B<PerlJammer> may contain support for declaring separate playercmd options for
other music formats such as Ogg Vorbis, and possibly FLAC and 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 properly.  I believe this is a Tk::MListbox bug.

=back

=head1 TODO

=over 4

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

=item Add gain adjustment controls and 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> and its supporting tools are free software.  You may redistribute
and/or modify them under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the License,
or (at your option) any later version.

=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
