#!/usr/bin/perl # jukebox.pl - GTK2 jukebox # Copyright (C) 2002-2005 Toby A Inkster $ver = '2.19b'; # 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 <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; } print "[Main] Starting player\n"; # 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 @_; print "[Player::Queue] Enqueuing '$song'\n"; push @queue, $song; $tQueue = ppQueue(@queue); } sub qPop { my $song = shift @queue; print "[Player::Queue] Dequeuing '$song'\n"; $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 { print "[np] Updating '$nppath'\n"; my $song = shift @_; open (NP, ">$nppath"); print NP "$song\n"; close NP; } ################### ## Player::Play ################### sub playSong { my $songfile = shift @_; print "[Player::Play] Play '$songfile'\n"; my $mixer = SDL::Mixer->new(-channels=>2); 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 { print "[Player::Play] Skip\n"; $mixer = SDL::Mixer->new(); if ($dofades) { $mixer->fade_out_music(1500); } else { $mixer->halt_music(); } } sub pauseSong { print "[Player::Play] Pause\n"; $mixer = SDL::Mixer->new(); $mixer->pause_music; } sub resumeSong { print "[Player::Play] Resume\n"; $mixer = SDL::Mixer->new(); $mixer->resume_music; } sub mute { print "[Player::Play] Toggling mute\n"; 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 { print "[Player::Songlist] Selecting random song\n"; my $r = int(rand($#allSongs + 1)); return $allSongs[$r]; } sub getSongList { print "[Player::Songlist] Reading song list from '$path'\n"; $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 { print "[GUI] Starting Graphical User Interface\n"; # 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 { print "[GUI] Button: enqueue\n"; my @indices = $flist->get_selected_indices(); foreach $i (@indices) { &qPush($allSongs[$i]); } } sub eventBtnEnd { print "[GUI] Button: delayed exit\n"; &qPush('*'); } sub eventBtnSkip { print "[GUI] Button: skip\n"; $button{'pause'}->set_active(FALSE); &killSong; } sub eventBtnPause { print "[GUI] Button: pause\n"; if ($button{'pause'}->get_active) { &pauseSong; } else { &resumeSong; } } sub eventBtnVolume { print "[GUI] Volume adjusted\n"; $vol = $adj->value; } sub eventExit { print "[GUI] Button: exit\n"; $instaQuit = TRUE; &killSong; Gtk2->main_quit; } ################### ## TCPUI::Start ################### sub tcpui { print "[TCP] Starting TCP Interface\n"; $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); printf "[TCP] Connect from %s\n", $client->peerhost; while ( <$client>) { print "[TCP] Incoming command: $_"; if (/quit|exit/i && !(/sexit/i)) { last; print $client "Bye!\n"; } elsif (/query/i) { print $client "$nowSong\n"; } elsif (/skip/i) { &killSong(); print $client "OK\n"; } 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(); } elsif (/list|ls/i) { $hsummarise = POSIX::floor($summarise / 3); $i = 0; 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); } print $client "[$i] $p\n"; $i++; } } elsif (/enqueue|play/i) { $i = 0; ($dummy,$i) = split(/ /); &qPush($allSongs[$i]); printf $client "QUEUED '%s'\n", $allSongs[$i]; } elsif (/queue/i) { $q = $tQueue; print $client "$q\n"; } elsif (/version/i) { print $client "jukebox.pl $ver\n"; } elsif (/sexitd/i) { &qPush('*'); printf $client "OK - Will end after queue is finished.\n"; } elsif (/sexit/i) { printf $client "OK - Will end now.\n"; $instaQuit = TRUE; &killSong; Gtk2->main_quit; exit; } else { print $client "HELP: \n"; print $client " query - query current song\n"; print $client " skip - skip current song\n"; print $client " list - show all songs\n"; print $client " play N - add song N to the queue\n"; print $client " queue - show current queue\n"; print $client " vol - query volume\n"; print $client " vup - raise volume\n"; print $client " vdown - lower volume\n"; print $client " mute - mute player\n"; print $client " sexit - tell server to exit\n"; print $client " sexitd - tell server to exit when finished queue\n"; print $client " version - what version is this?\n"; print $client " exit - close connection to jukebox\n"; } } printf "[TCP] Dropping connection to %s\n", $client->peerhost; close $client; } }