2 # $Id: adorno 35 2003-11-04 08:56:13Z $
4 # (c) 2002, 2003 Andrew McMillan - licensed under GPL version 2
10 use Time
::HiRes
qw( usleep );
11 use POSIX
qw(strftime);
14 use Ogg
::Vorbis
::Header
;
19 fifo
=> '/var/run/adorno/fifo',
20 pidfile
=> '/var/run/adorno/adorno.pid',
21 dumpfile
=> '/var/run/adorno/queue.txt',
22 config
=> '/etc/adorno/adorno.conf',
32 -d | --database <dbname> Use this database (adorno)
33 -u | --user <username> Log onto database as this user
34 --fifo <fifo_path> The path to the fifo ($opts{'fifo'})
35 --dumpfile <dump_path> The path to the file adorno dumps it's queue in ($opts{'dumpfile'})
36 --pid <pidfile_path> The path to the file adorno writes it's PID in ($opts{'pidfile'})
37 --config <config_path> The path to the configuration file ($opts{'config'})
38 --debug Debugging information
39 --help Request this help text
41 Note: The database options are bogus as yet - Adorno currently
42 does not connect to the database :-)
50 while( my $opt = shift ) {
52 if ( $opt =~ /^-(d|-database)$/ ) { $opts{'database'} = shift; }
53 elsif ( $opt =~ /^-(u|-user)$/ ) { $opts{'dbuser'} = shift; }
54 elsif ( $opt =~ /^--fifo$/ ) { $opts{'fifo'} = shift; }
55 elsif ( $opt =~ /^--dumpfile$/ ) { $opts{'dumpfile'} = shift; }
56 elsif ( $opt =~ /^--pidfile$/ ) { $opts{'pidfile'} = shift; }
57 elsif ( $opt =~ /^--config$/ ) { $opts{'config'} = shift; }
58 elsif ( $opt =~ /^--debug$/ ) { $opts{'debug'} = 1; }
59 elsif ( $opt =~ /^--help$/ ) { usage
(0); }
71 my $player_started = 0;
73 my $parent_process = 1; # set to 0 in the child process immediately after it starts
77 # Some things may be set in the config file, but here are
79 my @oggstream = ( "ogg123", "-q" );
80 my @mp3stream = ( "mpg321", "-q" );
81 my @oggfile = ( "ogg123", "-q" );
82 my @mp3file = ( "mpg321", "-q" );
84 # Used if we configure scrobbler
89 open( CONFIG
, "<", $opts{'config'} ) and do {
92 /^\s*oggstream\s*=\s*(\S.*)$/i && do { @oggstream = split /\s+/, $1 ; };
93 /^\s*oggfile\s*=\s*(\S.*)$/i && do { @oggfile = split /\s+/, $1 ; };
94 /^\s*mp3stream\s*=\s*(\S.*)$/i && do { @mp3stream = split /\s+/, $1 ; };
95 /^\s*mp3file\s*=\s*(\S.*)$/i && do { @mp3file = split /\s+/, $1 ; };
96 /^\s*scrobbler\[([^]]+)\]\s*=\s*(\S.*)$/i && do {
97 logf
( "info", "Scrobbler->{$1} = $2" ) if ( $opts{'debug'} );
98 $scrobbler->{$1} = $2 ;
103 logf
( 'warning', "Adorno Music Server starting (c) 2003-2006 Andrew McMillan" );
105 # Subroutine to ensure we don't zombie out.
107 my $waitedpid = wait;
108 $SIG{CHLD
} = \
&REAPER
;
112 # Create the fifo if necessary
113 my $fifo_perms = oct(10666);
114 unless (-p
$opts{'fifo'}) {
115 unlink $opts{'fifo'};
116 system('mknod', $opts{'fifo'}, 'p') or logf
( 'warning', "can't mknod $opts{'fifo'}: $!");
117 chmod $fifo_perms, $opts{'fifo'} or logf
( 'warning', "can't chmod %o $opts{'fifo'}: $!", $fifo_perms);
120 # Ensure anyone can write to that...
122 while ( $i++ < 10 && (stat($opts{'fifo'}))[2] != $fifo_perms ) {
124 logf
( 'notice', "Setting %o permissions on $opts{'fifo'} currently %o",
125 $fifo_perms, (stat($opts{'fifo'}))[2] );
126 chmod $fifo_perms, $opts{'fifo'} or logf
( 'warning', "can't chmod %o $opts{'fifo'}: $!", $fifo_perms );
128 logf
( 'crit', "Can't set %o permissions on $opts{'fifo'} - giving up completely", $fifo_perms);
135 unless ( $opts{'debug'} ) {
136 # Fork into background as a good daemon should when we're not debugging...
137 $SIG{CHLD
} = \
&REAPER
; $player_pid = fork;
139 open (PIDFILE
, "> $opts{'pidfile'}") || logf
( 'crit', "Parent can't save PID in $opts{'pidfile'} $!");
140 print PIDFILE
"$player_pid";
147 if ( defined($scrobbler->{'username'}) ) {
148 logf
( 'warning', "Using Audio::Scrobbler interface" );
149 use Audio
::Scrobbler
;
150 $scrob = new Audio
::Scrobbler
( cfg
=> $scrobbler );
151 $scrob->handshake() or do {
152 logf
( 'warning', "Scrobbler initialisation failed" );
158 ###########################################################
159 # Thinly veiled wrapping of syslog!
161 ###########################################################
163 openlog
('adorno', 'cons,pid', 'user');
166 if ( shift =~ /crit/ ) {
169 if ( $opts{'debug'} ) {
170 print scalar localtime, ": ";
176 ###########################################################
177 # Dump the queue out to a file for other software to
178 # peruse, and also to syslog if we are debugging...
180 ###########################################################
183 open( DUMP
, "> $opts{'dumpfile'}" ) || return;
184 logf
'debug', "Currently playing: $next_track" if ( $opts{'debug'} );
185 print DUMP
$next_track, "\n";
186 print DUMP
$player_started, "\n";
187 foreach my $i (0 .. $#queue ) {
188 logf
'debug', $queue[$i] if ( $opts{'debug'} );
189 print DUMP
$queue[$i], "\n";
194 ###########################################################
195 # Send a command to the fifo to tell the master process to
198 ###########################################################
199 sub send_command_to_fifo
{
202 open (CHFIFO
, "> $opts{'fifo'}") || logf
( 'err', "Child can't open $opts{'fifo'}: $!");
203 print CHFIFO
"$command\n";
205 logf
'info', ($parent_process ?
"PARNT" : "CHILD") . ": Commanded \"$command\"";
209 ###########################################################
210 # Start a player to play the track
212 ###########################################################
214 my $track = shift(@_);
218 # Ensure the on-disk list of tracks is up-to-date.
221 return if ( "$track" eq "" );
223 if ( $track =~ /^http:/i ) { # Streaming URLs
224 if ( $track =~ /\.ogg$/i ) { # Streaming OGGs
225 # push @cmdline, "ogg123", "-q", "-b", "512", "-p", "10", $track;
226 @cmdline = @oggstream;
228 else { # Assume streaming MP3
229 # push @cmdline, "mpg123", "-y", "-q", "-b", "384", $track ;
230 @cmdline = @mp3stream;
233 elsif ( $track =~ /\.mp3$/i ) { # File MP3
235 logf
( 'err', "PARNT: \"$track\" doesn't actually exist");
236 $next_track = shift @queue;
237 run_player
( $next_track );
240 # push @cmdline, "playmp3", $track;
243 elsif ( $track =~ /\.ogg$/i ) { # File OGG
245 logf
( 'err', "PARNT: \"$track\" doesn't actually exist");
246 $next_track = shift @queue;
247 run_player
( $next_track );
250 # push @cmdline, "ogg123", "-q", "-b", "512", "-p", "5", $track;
254 # Buggadifino! Skip this one and go to the next.
255 logf
'err', "PARNT: Unknown music format: \"$track\"";
256 $next_track = shift @queue;
257 run_player
( $next_track );
261 # Ensure the on-disk list of tracks is up-to-date, again...
262 # And this time note when we started on the new track
263 $player_started = strftime
"%Y-%m-%d %H:%M:%S", localtime;
266 push @cmdline, $track;
268 logf
'info', "PARNT: Forking to run '@cmdline'";
269 $SIG{CHLD
} = \
&REAPER
; $player_pid = fork;
270 if ( ! $player_pid ) {
271 ########################### CHILD PROCESS #######################
272 $parent_process = 0; # Since we're the child...
273 logf
'info', "CHILD: Running: @cmdline";
274 my $began_playing = time;
275 ($result = system(@cmdline)) == 0 or logf
( 'err', "CHILD: @cmdline failed: $? $!");
276 logf
'info', "CHILD: Result of player was $result, asking for next... - don't kill us!";
277 send_command_to_fifo
("next nokill");
279 if ( (time - $began_playing) > 20 ) {
280 if ( defined($scrob) ) {
281 logf
'info', "CHILD: Will try to submit to Scrobbler";
282 my $track_details = get_track_info
($track);
283 $scrob->submit( $track_details ) if ( $track_details->{'length'} > 30 );
286 logf
'info', "CHILD: Scrobbler does not appear to be configured.";
290 logf
'info', "CHILD: Track too short for scrobbling.";
293 usleep
(100000); # Is this really necessary?
294 logf
'info', "CHILD: exiting!";
295 exit($result); # Should we care enough to pass our resultcode back?
296 ####################### END CHILD PROCESS #######################
298 logf
'info', "PARNT: Started to play $track (PID $player_pid)";
303 #####################################################################
304 # Get Track Information
305 #####################################################################
307 my $filename = shift;
310 'artist' => 'Unknown',
311 'album' => 'Unknown',
312 'title' => 'Unknown',
316 return $info if ( $filename !~ /\.(ogg|mp3)$/ );
318 if ( $filename =~ /\.mp3$/ ) {
319 # Get the info out of the (hopefully) .mp3 file
320 my $mp3info = get_mp3info
($filename) or do {
321 logf
( "warning", "No MP3 info for $filename (the file is likely to be unplayable).");
324 my $mp3tag = get_mp3tag
($filename) or do {
325 logf
( "warning", "No ID3 tag info for $filename");
328 $info->{'artist'} = $mp3tag->{'ARTIST'};
329 $info->{'album'} = $mp3tag->{'ALBUM'};
330 $info->{'title'} = $mp3tag->{'TITLE'};
331 $info->{'length'} = ($mp3info->{'MM'} * 60) + $mp3info->{'SS'};
334 # Get the info out of the (presumed) .ogg file
335 my $ogghdr = Ogg
::Vorbis
::Header
->load($filename) or do {
336 logf
( "warning", "No Ogg Vorbis info for $filename");
339 $info->{'length'} = int( $ogghdr->info('length'));
341 foreach my $key ($ogghdr->comment_tags) {
342 foreach my $value ( $ogghdr->comment($key) ) {
343 if ( $key =~ /artist/i ) { $info->{'artist'} = $value; }
344 elsif ( $key =~ /album/i ) { $info->{'album'} = $value; }
345 elsif ( $key =~ /title/i ) { $info->{'title'} = $value; }
350 logf
( 'info', "PARNT: Scrobbler: Artist=%s, Album=%s, Title=%s, Length=%s", $info->{'artist'}, $info->{'album'}, $info->{'title'}, $info->{'length'} );
356 #####################################################################
357 # Get children of a process
358 # - this is somewhat architecture dependent. It works OK on Linux
359 # 2.5.73, but I am a wee mite worried that it ties itself to the
360 # /proc architecture. Of course the alternative is to tie it to
361 # something further up the food chain, but do we gain a lot?
363 #####################################################################
365 my $parent_pid = shift @_;
368 my ($pid, $j1, $j2, $ppid);
370 logf
'info', "PARNT: Looking for children of $parent_pid...";
372 opendir( PROC
, "/proc" );
373 while( $subpid = readdir( PROC
) ) {
374 if ( $subpid =~ /^[0-9]+$/ ) {
375 open( PROCSTAT
, "/proc/$subpid/stat" );
376 while( <PROCSTAT
> ) {
377 # This has some dependency on /proc format, but these fields are
379 ($pid, $j1, $j2, $ppid) = split;
380 if ( $ppid == $parent_pid ) {
381 logf
'debug', "PARNT: Found $pid child of $parent_pid";
382 push @children, $pid, get_child_pids
($pid);
393 ###########################################################
394 # Sometimes we just want to pause it...
396 ###########################################################
398 if ( ! $player_pid ) { return; }
399 my $parent = $player_pid;
400 my @children = get_child_pids
( $parent );
401 logf
'info', "PARNT: STOPping children @children (child of $player_pid)";
403 logf
'info', "PARNT: Done stopping processes...";
407 ###########################################################
410 ###########################################################
412 if ( ! $player_pid ) { return; }
413 my $parent = $player_pid;
414 my @children = get_child_pids
( $parent );
415 logf
'info', "PARNT: CONTing children @children (child of $player_pid)";
417 logf
'info', "PARNT: Done continuing processes...";
422 ###########################################################
423 # and sometimes it just deserves to die...
425 ###########################################################
427 if ( ! $player_pid ) { return; }
428 my $parent = $player_pid;
429 my @children = get_child_pids
( $parent );
430 if ( $#children == 0 ) {
431 logf
'info', "PARNT: No kids! (children of $player_pid)";
434 logf
'info', "PARNT: TERMing children @children (child of $player_pid)";
437 # Now wait until all of those children are actually dead.
439 @children = get_child_pids
( $parent );
440 while( $#children > 0 ) {
441 logf
'info', "PARNT: Waiting for $#children to die.";
443 @children = get_child_pids
( $parent );
446 logf
'info', "PARNT: Done terminating processes...";
451 ###########################################################
452 # Main loop. Forever just keep reopening and reading our
453 # fifo and acting on the commands we receive.
454 ###########################################################
457 logf
'info', "PARNT: Opening fifo";
458 if ( open (FIFO
, "< $opts{'fifo'}") ) {
460 logf
'info', "PARNT: Waiting for a command";
462 # next line blocks until there's a reader
464 logf
'info', "PARNT: FIFO Says: $_";
466 # Quit the application. Cron will probably restart it...
473 # Suspend the track being played
478 # Resume the suspended track
487 exec "shutdown -h now";
490 # Nuke everything in the current queue
496 # Put this track at the beginning of the queue
497 /^play (.*)$/i && do {
508 # Put this track at the end of the queue
509 /^queue (.*)$/i && do {
520 # Kill the currently playing track (which will start a new one)
521 /^next(.*)$/i && do {
522 if ( $1 =~ /nokill/ ) {
524 $next_track = shift @queue;
525 run_player
( $next_track );
528 # which will call us back with a `next nokill', or exit as true
529 if ( kill_player
() ) {
531 $next_track = shift @queue;
532 run_player
( $next_track );
541 logf
( 'err', "Couldn't open $opts{'fifo'}: $!");
547 logf
'info', "PARNT: Exiting";