#!/usr/bin/perl
# jukebox.pl - GTK2 jukebox
# Copyright (C) 2002-2005  Toby A Inkster
 
$ver = '2.19a';
   
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or   
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# Load modules. These should all be on CPAN.
use threads;                            # threading
use threads::shared;
use Thread::Queue;
use Glib;                               # GUI
use Gtk2 '-init';
use Gtk2::SimpleList;
use constant TRUE  => 1;
use constant FALSE => 0;
use Ogg::Vorbis::Header;                # Audio
use SDL;
use SDL::Mixer;
use SDL::Music;
use File::Find;                        # files
use POSIX;                              # POSIX (for "floor" function)
use IO::Socket;                         # TCP/IP
use Getopt::Long qw(GetOptions);        # Options

# Shared variables for passing info around.
my @queue : shared = ();                # queue
my $tQueue : shared = '';               # pretty-printed queue
my @allSongs : shared = ();             # list of songs
my $nowSong : shared = '';              # current song file name
my $nowSongDetails : shared = '';       # pretty song pretty-printed details
my $instaQuit : shared = FALSE;         # used to pass back and forth quitting data
my $vol : shared = 100;                 # volume
my $mute : shared = 0;                  # used to store volume when jukebox is muted
my $dofades : shared = 1;               # should we use the fade effect?

# Sensible defaults
$path = $ENV{'JUKEBOX'} || '/usr/share/jukebox';
$tcpport = 5853;
$gui = 1;
$summarise = 60;
$nppath = $ENV{'NOWPLAYING'} || ($ENV{'HOME'} . '/.jukebox-np');

# Override defaults
GetOptions
(
        "usage|help|h"  => \$help,
        "version"       => \$version,
        "volume|v=i"    => \$vol,
        "fade!"         => \$dofades,
        "port|p=i"      => \$tcpport,
        "gui!"          => \$gui,
        "directory=s"   => \$path,
        "remote|r=s"    => \$remotecmd,
        "host=s"        => \$remotehost,
        "summarise=i"   => \$summarise,
        "nowplaying=s"  => \$nppath
);

# Show help stuff if asked for.
if ($version) {
        print "$ver\n";
        exit;
}
if ($help) {
        print <<EOF;

jukebox.pl [options]

    Help:
        --usage, --help, -h     Show this help text.
        --version               Show version.
       
    TCP Interface:
        --port=N, -p            Listen for commands on TCP port N. Set to 0 to
                                disable TCP interface. Default $tcpport.
        --remote=C, -r          Issue command C to already running instance.
                                Use --port as above to specify which port.
        --host=H                Use in conjunction with --remote.

    GUI Interface:
        --gui                   Show GUI. Shown by default.
        --nogui                 Hide GUI. Shown by default.
        --summarise=N           If song titles are longer than N, they will be
                                snipped. Default $summarise.

    Player:
        --volume=N, -v          Set volume to N between 0 to 100. Default $vol.
        --fade                  Use fade effect when skipping songs. Default.
        --nofade                Don't use fade effect.
        --directory=D           Pick songs from D. Default '$path'.
        --nowplaying=F          Output "now playing" data to file F. Default
                                is '$nppath'.

EOF
        exit;
}

# Do remote stuff if asked for.
if ($remotecmd) {
        $EOL = "\015\012";
        $remotehost = '127.0.0.1' unless ($remotehost);
        $remote = IO::Socket::INET->new( Proto     => "tcp",
                                         PeerAddr  => $remotehost,
                                         PeerPort  => $tcpport
                                        ) || die "Could not connect to $remotehost:$tcpport\n";
        $remote->autoflush(1);
        print $remote "$remotecmd$EOL";
        print $remote "quit$EOL";
        while(<$remote>) { print };
        close $remote;
        exit;
}

# Otherwise, start the main program.
@allSongs = &getSongList;
$guiThread = threads->create(\&drawGui) if ($gui);
$tcpThread = threads->create(\&tcpui) if ($tcpport>0);
while ($nowSong ne '*') {
        $nowSong = &qPop;
        if ($instaQuit == FALSE) {
                if (!defined $nowSong) {
                        $nowSong = &randomSong;
                }
                if ($nowSong ne '*') {
                        $nowSongDetails = &getDetails($nowSong);
                        &np($nowSong);
                        &playSong($nowSong);
                }
        } else {
                &np('');
                exit;
        }
}
exit;






###################################################################################################
############################################ FUNCTIONS ############################################
###################################################################################################






###################
## Player::Queue
###################

sub qPush {
        my $song = shift @_;
        push @queue, $song;
        $tQueue = ppQueue(@queue);
}

sub qPop {
        my $song = shift @queue;
        $tQueue = ppQueue(@queue);
        return $song;
}

sub ppQueue {
        my $r = '';
        while (my $s = shift) {
                if ($s eq '*') {
                        $r .= "[exit]\n"
                } else {
                        $r .= '.' . $s . "\n" ;
                }
        }
        chomp $r;
        return $r;
}

###################
## np
###################

sub np {
        my $song = shift @_;
        open (NP, ">$nppath");
        print NP "$song\n";
        close NP;
}

###################
## Player::Play
###################

sub playSong {
        my $songfile = shift @_;
       
        my $mixer = SDL::Mixer->new();
       
        my $music =  new SDL::Music("$path$songfile");
        $mixer->play_music($music,1);
       
        while ($mixer->playing_music() || $mixer->music_paused() || $mixer->fading_music() )
                { $mixer->music_volume($vol*1.28); sleep 1; }
}

sub killSong {
        $mixer = SDL::Mixer->new();
        if ($dofades) {
                $mixer->fade_out_music(1500);
        } else {
                $mixer->halt_music();
        }
}

sub pauseSong {
        $mixer = SDL::Mixer->new();
        $mixer->pause_music;
}

sub resumeSong {
        $mixer = SDL::Mixer->new();
        $mixer->resume_music;
}

sub mute {
        if ($mute==0) {
                $mute = $vol;
                $vol = 0;
        } else {
                $vol = $mute;
                $mute = 0;
        }
        $mixer = SDL::Mixer->new();
        $mixer->music_volume($vol*1.28);
}

sub getDetails {

        my $song = shift @_;
        my $r = '';
        my %details = ();

        my $ogg = Ogg::Vorbis::Header->new("$path$song");
        foreach my $com ($ogg->comment_tags) {
                $com =~ tr/[A-Z]/[a-z]/;
                @t = $ogg->comment($com);
                $details{$com} .= $t[0];
        }
        while (my ($k, $v) = each %{$ogg->info}) {
                $details{$k} = $v;
        }

        if ($details{'artist'}) {
                $r .= 'Artist: ' . $details{'artist'} . "\n";
        }
        if ($details{'title'}) {
                $r .= 'Title: ' . $details{'title'} . "\n";
        }
        if ($details{'album'}) {
                $r .= 'Album: ' . $details{'album'}. "\n";
        }
        if ($details{'date'}) {
                $r .= 'Date: ' . $details{'date'} . "\n";
        }
        my $min = POSIX::floor($details{'length'} / 60);
        my $sec = $details{'length'} % 60;
        $sec = "0$sec" unless ($sec > 9);
        my $bitrate = POSIX::floor($details{'bitrate_nominal'} / 1024);
        $r .= "File: .$song\n";
        $r .= "Length: $min:$sec (at $bitrate kbps)";

        return $r;
}

###################
## Player::Songlist
###################

sub randomSong {
        my $r = int(rand($#allSongs + 1));
        return $allSongs[$r];
}

sub getSongList {
        $l = length($path);
        @r = ();
        find(\&getSongListWanted,$path);
        return sort (@r);
}

sub getSongListWanted {
        if (/\.ogg$/) {
                $p = substr($File::Find::name, $l);
                push @r, $p;
        }
}

sub getSongListSort {
        my @files = grep { not /^\.\.?$/ } @_;
        return sort @files;
}

###################
## GUI::Draw
###################

sub drawGui {

        # create main window
        $window = Gtk2::Window->new;
        $window->set_title("Jukebox $ver");
        $window->signal_connect(destroy => \&eventExit);
        $window->set_border_width(3);
        $window->set_icon_from_file("$path/icon.png");
       
        # list of files
        $flist = Gtk2::SimpleList->new('Song' => 'text');
        $flist->set_rules_hint(TRUE);
        $flist->get_selection->set_mode ('multiple');
        $flist->get_selection->unselect_all;
        $flist->signal_connect (row_activated => \&eventBtnAdd);
        $hsummarise = POSIX::floor($summarise / 3);
        foreach my $s (@allSongs) {
                $p = substr($s,1);
                $p =~ s#\.ogg$##;
                $p =~ s#/# - #g;
                $p =~ s#_# #g;
                $p =~ s/\b(\w)/\U$1/g;
                if (length($p) > $summarise) {
                        $l = length($p);
                        $p = substr($p, 0, $hsummarise) . ' ... ' . substr($p, $l - $hsummarise, $hsummarise);
                }
                push @{$flist->{data}}, [ $p ];
        }
        $sw1 = Gtk2::ScrolledWindow->new(undef, undef);
        $sw1->set_shadow_type ('etched-in');
        $sw1->set_policy ('automatic', 'automatic');
        $sw1->add($flist);

        # status field
        $statusfield = Gtk2::Label->new;
        $statusfield->set_alignment(0,0);
        Glib::Timeout->add (500, sub {
                $x = $nowSongDetails;   # NOTE: These assignments seem useless, but
                $y = $tQueue;           # they seem to wake up threads::shared.
                $z = $vol;
                $statusfield->set_text($nowSongDetails);
                $queuefield->set_text($tQueue);
                $adj->set_value($vol);
                1;
        });

        # queue field
        $queuefield = Gtk2::Label->new('q');
        $queuefield->set_alignment(0,0);

        # boxes and frames for arranging things
        $vbox = Gtk2::VBox->new;
        $vbox->set_border_width(3);
        $frame = Gtk2::Frame->new('Controls');
        $frame->set_border_width(3);
        $frame->add($vbox);
        $hbox = Gtk2::HBox->new;
        $hbox->set_border_width(3);
        $hbox->pack_start($sw1, TRUE, TRUE, 0);
        $hbox->pack_start($frame, FALSE, FALSE, 0);
        $hr = Gtk2::HSeparator->new;
        $outerbox = Gtk2::VBox->new;
        $outerbox->pack_start($hbox, TRUE, TRUE, 0);
        $outerbox->pack_start($statusfield, FALSE, TRUE, 0);
        $outerbox->pack_start($hr, FALSE, FALSE, 0);
        $outerbox->pack_start($queuefield, FALSE, TRUE, 0);
        $window->add($outerbox);

        # buttons for controls
        $button{'add'} = Gtk2::Button->new('En_queue');
        $vbox->pack_start($button{'add'}, FALSE, FALSE, 0);
        $button{'add'}->signal_connect(clicked => \&eventBtnAdd);
       
        $button{'pause'} = Gtk2::ToggleButton->new('_Pause');
        $button{'pause'}->set_active(FALSE);
        $vbox->pack_start($button{'pause'}, FALSE, FALSE, 0);
        $button{'pause'}->signal_connect(clicked => \&eventBtnPause);

        $button{'skip'} = Gtk2::Button->new('_Skip');
        $vbox->pack_start($button{'skip'}, FALSE, FALSE, 0);
        $button{'skip'}->signal_connect(clicked => \&eventBtnSkip);
       
        $adj = Gtk2::Adjustment->new(100, 0, 101, 1, 10, 1);
        $adj->signal_connect(value_changed => \&eventBtnVolume);
        $button{'volume'} = Gtk2::HScale->new($adj);
        $button{'volume'}->set_value_pos('bottom');
        $button{'volume'}->set_digits(0);
        $vbox->pack_start($button{'volume'}, FALSE, FALSE, 0);
        $button{'volume'}->signal_connect('move-slider' => \&eventBtnVolume);

        $button{'quit'} = Gtk2::Button->new('E_xit');
        $button{'quit'}->signal_connect(clicked => \&eventExit);
        $vbox->pack_end($button{'quit'}, FALSE, FALSE, 0);

        $button{'end'} = Gtk2::Button->new('_Delayed Exit');
        $vbox->pack_end($button{'end'}, FALSE, FALSE, 0);
        $button{'end'}->signal_connect(clicked => \&eventBtnEnd);

        # show window and enter GTK+2 loop
        $window->set_default_size( 640, 480 );
        $window->show_all;
        Gtk2->main;
       
        $instaQuit = TRUE;

}

###################
## GUI::EventHandlers
###################

sub eventBtnAdd {
        my @indices = $flist->get_selected_indices();
        foreach $i (@indices) {
                &qPush($allSongs[$i]);
        }
}

sub eventBtnEnd {
        &qPush('*');
}

sub eventBtnSkip {
        $button{'pause'}->set_active(FALSE);
        &killSong;
}

sub eventBtnPause {
        if ($button{'pause'}->get_active) {
                &pauseSong;
        } else {
                &resumeSong;
        }
}

sub eventBtnVolume {
        $vol = $adj->value;
}

sub eventExit {
        $instaQuit = TRUE;
        &killSong;
        Gtk2->main_quit;       
}


###################
## TCPUI::Start
###################

sub tcpui {
        $server = IO::Socket::INET->new( Proto     => 'tcp',
                                        LocalPort => $tcpport,
                                        Listen    => SOMAXCONN,
                                        Reuse     => 1) || die "Unable to bind to port $tcpport\n";
        while ($client = $server->accept()) {
         $client->autoflush(1);
         print $client "Welcome to $0\n";
         printf "[Connect from %s]\n", $client->peerhost;
         while ( <$client>) {
           print "Command: $_";
           if    (/quit|exit/i)        { last; }
           elsif (/skip/i)             { print $client "SKIPPED\n"; &killSong(); }
           elsif (/vol/i)              {                                       print $client "VOL is $vol\n"; }
           elsif (/vup/i)              { $vol += 10; $vol=100 if ($vol>100);   print $client "VOL is $vol\n"; }
           elsif (/vdown/i)            { $vol -= 10; $vol=0 if ($vol<0);       print $client "VOL is $vol\n"; }
           elsif (/mute/i)             { &mute();                              print $client "VOL is $vol\n"; }
           else {
             print $client "Commands: skip vol vup vdown mute\n";
           }
         }
         printf "[Dropping connection to %s]\n", $client->peerhost;
         close $client;
        }
}