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