#!/usr/bin/perl use strict; use POSIX ":sys_wait_h"; use IPC::Open3; use Socket; use IO::Handle; # Not standard with perl5.6 use Proc::Killfam; use Curses::UI; # <3 Curses:UI # Should be here... use FileBrowser; # Constants {{{ use constant STATE_PLAYING => 1; use constant STATE_STOPPED => 2; use constant STATE_PAUSE => 3; #}}} open(STDERR, ">> log"); my $VERSION = ".1"; # init {{{ # ipc stuffs {{{ my $childpid = 0; my $WAS_CHILD_KILLED = 0; my $state = {}; $SIG{CHLD} = \&death; # to-child communication pipe(IPCREAD, IPCWRITE); # <3 pipe() open(STD_FAKEOUT, "> /dev/null"); IPCWRITE->autoflush(1); IPCREAD->autoflush(1); #}}} # Create the UI {{{ my $cui = new Curses::UI; # Shitty documentation, great code. We need this set so key queries don't block $cui->{-read_timeout} = 1; # Setting this to 0 sends cpu usage up up up :( my $mainwindow = $cui->add( 'mainwindow', 'Window', -padbottom => 1); my $status = $cui->add( 'statuswindow', 'Window', -y => -1); my $statuslabel = $status->add('testlab', 'Label', -y => -1 ); $statuslabel->text("PiMP v$VERSION - Pretty music player :("); my $jumpwindow = $cui->add('jumpto', 'Window', -border => 1, -title => "Jump to...", -pad => 3); $jumpwindow->add('jumpto_query', 'TextEntry', -padtop => 1, -sbborder => 1, -onchange => \&jump_to_edit ); $jumpwindow->add('jumpto_list', 'Listbox', -padtop => 2, -border => 1, -padbottom => 2, -onchange => \&jump_to); $jumpwindow->add('jumpto_label', "Label", -y => -1, -x => 1, -text => "Perl regular expressions are valid here =)"); $jumpwindow->loose_focus(); my $playlist = $mainwindow->add( 'playlist', 'Listbox', -border => 1, -onchange => \&list_changed, ); #}}} # Bind default keys {{{ # Global $cui->set_binding(\&quit, "\cC"); $cui->set_binding(\&redraw, "\cL"); # Main window $mainwindow->set_binding(\&quit, "q", "Q", "\cQ"); $mainwindow->set_binding(\&jump_to, "J", "ê"); $mainwindow->set_binding(\&audio_send_command_skip5, $cui->KEY_RIGHT, "l"); $mainwindow->set_binding(\&audio_send_command_back5, $cui->KEY_LEFT, "h"); $mainwindow->set_binding(\&audio_send_command_pause, "p"); $mainwindow->set_binding(\&audio_send_command_startover, "H"); $mainwindow->set_binding(\&queue_file_browse, "o"); # Jump window $jumpwindow->set_binding(\&jump_to_cancel, "\c["); #}}} my @playlist = split("\n", `find /home/psionic/mp3 -type f`); $playlist->values(\@playlist); #}}} # Main loop {{{ $status = "PiMP v$VERSION - Pretty media player"; $cui->focus(undef, 1); $cui->draw(); update_status_label(); my ($time, $otime) = (0,0); $time = time(); while (1) { $cui->do_one_event; if ($time > $otime) { update_status_label(); $otime = $time; } $time = time(); } #}}} # Misc functions {{{ # Jump window functions {{{ sub jump_to { #{{{ my $jump = shift; if (ref($jump) eq 'Curses::UI::Listbox') { my $sel = $jump->get(); my $a = 0; foreach (@playlist) { if ($_ eq $sel) { $playlist->{-selected} = $a; last; } $a++; } $jump->{-selected} = -1; $playlist->focus(); $playlist->draw(); } else { jump_to_edit($jumpwindow->getobj('jumpto_query')); $jumpwindow->focus(); $jumpwindow->getobj('jumpto_query')->focus(); $jumpwindow->intellidraw(); $jumpwindow->intellidraw(); } } #}}} sub jump_to_edit { #{{{ my $jtext = shift; $jtext = $jtext->get(); #my $jumpwindow = $cui->getobj("jumpto"); #print STDERR ">> $jump_to\n"; my @jlist = @playlist; #@{$jump_to->getobj('jumpto_list')->{-values}}; $state->{'jumpwindow_waitregex'} = [ ] unless ($state->{'jumpwindow_waitregex'}); #$jtext =~ s,/,\\/,g; $jtext =~ s/ *$//; # Chop up the string into words. $jtext =~ s/((\S+|(\\ ))+[^\\ ]?)( +| *$)?/(?=.*$1)/g; # I was trying to write some fancy validater for $jtext as a regexp, but... eval { m/$jtext/ }; # This works just dandy! unless ($@) { @jlist = grep(/$jtext/i,@jlist); $jumpwindow->getobj('jumpto_list')->values(\@jlist); $jumpwindow->getobj('jumpto_list')->draw(); } } #}}} sub jump_to_cancel { jump_to_edit($jumpwindow->getobj('jumpto_query')); $jumpwindow->loose_focus(); $playlist->focus(); $playlist->draw(); $jumpwindow->getobj('jumpto_query')->text(""); } #}}} sub list_changed { #{{{ my $listbox = shift; print STDERR $listbox->get() . "\n"; audio_play($listbox->get()); $playlist->focus(); } #}}} sub redraw { #{{{2 my $self = shift; $self->{-canvasscr}->clear(); $self->draw(); } #}}} sub quit {#{{{ my ($confirm) = @_; $confirm = $cui->dialog( -message => "Quit?", -buttons => [ 'cancel', 'ok' ], -values => [1,0], ); if ($confirm) { $SIG{CHLD} = 'IGNORE'; kill_child(1); exit; } } #}}} sub focus_top_window() { #{{{ $cui->getfocusobj()->focus(); } #}}} sub update_status_label { #{{{ my $text; if ($state->{'state'} == STATE_PLAYING) { $text = "Playing! :)"; if ($state->{"lasttime"} < time()) { $state->{"progress"} += time() - $state->{"progress"}; $state->{"lasttime"} = time(); } } elsif ($state->{'state'} == STATE_STOPPED) { $text = "Stopped :("; } elsif ($state->{'state'} == STATE_PAUSE) { $state->{"lasttime"} = time(); } $text .= " - Seconds: " . $state->{"progress"}; # And finally... $statuslabel->text($text); $statuslabel->draw(); } #}}} sub queue_file_browse { #{{{ my $file; #my $dialog = $cui->add('filebrowser', 'Dialog::FileBrowser'); #$dialog->focus(); #$file = $dialog->get(); #$cui->delete('filebrowser'); $file = $cui->tempdialog("Dialog::FileBrowser"); print STDERR "File: $file\n"; } #}}} #}}} # Process Management and IPC {{{1 sub kill_child { #{{{2 return unless ($childpid); my $nohurtmeplz = shift; print STDERR "Killing child and children, $childpid\n"; $WAS_CHILD_KILLED = 1; print IPCWRITE "quit\n"; #system("pkill -P $childpid"); killfam("TERM",$childpid); $childpid = 0; close(IPCWRITE); pipe(IPCREAD,IPCWRITE); my @pids = split("\n",`ps -o 'ppid pid command'`); if (0) { print STDERR "-" x 80 . "\n"; foreach (@pids) { next if (m/ps -o 'ppid pid command'$/); next unless (m/^\s*$childpid\s+([0-9]+)\s+(.*)/); my ($pid,$cmd) = ($1,$2); print STDERR $_ . "\n"; if (kill(0,$pid)) { print STDERR "--> Checking children\n"; #foreach (@pids) { #next if (m/ps -o ppid pid command$/); #next unless (m/^\s*$pid\s+([0-9]+)\s+(.*)/); #my ($pid,$cmd) = ($1,$2); #print STDERR $_ . "\n"; #my $pid = $1; #system("pkill -P $pid"); #} } kill("TERM",$pid); kill("TERM",$pid); } # kill("TERM",$childpid) unless ($nohurtmeplz); #print STDERR "-" x 80 . "\n"; } } #}}} sub death { #{{{2 my $child; $child = waitpid(-1,WNOHANG); return if ($child == -1 || !defined($childpid)); if ($child == $childpid) { print STDERR "Child (Parent; $$) died: $child (Need $childpid)\n"; $state->{'state'} = STATE_STOPPED; print STDERR "#### CLOSING PIPE [Death: ".($? >> 8)." | $!]\n"; killfam("TERM",$childpid) unless ($childpid == $$); close(IPCWRITE); pipe(IPCREAD,IPCWRITE); audio_end_of_song() if ($WAS_CHILD_KILLED); # Reset this to 1, so if mplayer dies/finishes, move to next song. $WAS_CHILD_KILLED = 1; } #if ($child == $childpid) { # Our fork died, what now? #print STDERR "Stopped!\n"; #return if ($WAS_CHILD_KILLED); #$WAS_CHILD_KILLED = 0; #} $SIG{CHLD} = \&death; } #}}} # }}} # Audio related functions {{{ sub audio_end_of_song { #{{{ print STDERR "End of song reached\n"; $playlist->{-selected} = $playlist->{-selected} + 1; $playlist->draw(); list_changed($playlist); } #}}} sub audio_play { #{{{ my $file = shift; #kill_child(); if ($childpid) { print STDERR "CHILD PID: $childpid\n"; #system("pkill -P $childpid"); $WAS_CHILD_KILLED = 0; killfam("TERM",$childpid); #foreach (split("\n",`ps -o 'ppid pid'`)) { #next unless (m/$childpid/); #print STDERR "-> $_\n"; #s/^.*\s+//; ##kill("TERM",$_); #} close(IPCWRITE); pipe(IPCREAD,IPCWRITE); } $childpid = open3("<&IPCREAD", ">&STD_FAKEOUT", ">&STDERR", "mplayer -slave -ao esd -really-quiet \"$file\"") or die("$!\n"); print STDERR "NEW MPLAYER PID $childpid\n"; $state->{"state"} = STATE_PLAYING; $state->{"progress"} = 0; } #}}} # I <3 mplayer sub audio_send_command { my $cmd = shift; $state->{'lastcommand'} = $cmd; print STDERR "--> $cmd\n"; print IPCWRITE $cmd . "\n"; } sub audio_send_command_skip5 { audio_send_command("seek +5"); } sub audio_send_command_back5 { audio_send_command("seek -5"); } sub audio_send_command_pause { audio_send_command("pause"); audio_toggle_pause(); } sub audio_send_command_startover { audio_send_command("seek 0 2"); } sub audio_toggle_pause { $state->{"state"} = ( ($state->{"state"} == STATE_PAUSE) ? STATE_PLAYING : STATE_PAUSE); $state->{"lasttime"} = time() if ($state->{"state"} == STATE_PLAYING); } #}}}