001 #!/usr/bin/perl 002 ############################# 003 # rateplay - Rate & Play MP3s 004 # Mike Schilli, 2004 005 # (m@perlmeister.com) 006 ############################# 007 use strict; 008 use warnings; 009 010 our $DB_NAME ="/data/rp.dat"; 011 our $SONG_DIR= 012 "/ms1/SONGS/pods"; 013 our $FIND = "/usr/bin/find"; 014 015 use Gtk; 016 use POE; 017 use Class::DBI; 018 use POE::Component::Player::Musicus; 019 use Algorithm::Numerical::Shuffle qw(shuffle); 020 021 my (%GUI, %RATED, $TAG, 022 $SONG, @PLAYLIST, @MP3S); 023 my @PLAY_ENERG = 024 ( 0, 0, 0, 0, 0 ); 025 my @PLAY_SCHMO = 026 ( 0, 0, 0, 0, 0 ); 027 my $RATE_ENERG = 0; 028 my $RATE_SCHMO = 0; 029 my @RATE_ENERG_BUTTONS = (); 030 my @RATE_SCHMO_BUTTONS = (); 031 032 ############################# 033 package Rateplay::DBI; 034 ############################# 035 use base q(Class::DBI); 036 use 037 Class::DBI::AbstractSearch; 038 039 __PACKAGE__->set_db( 040 'Main', 041 "dbi:SQLite:$main::DB_NAME", 042 'root', ''); 043 044 if ( !-e "$main::DB_NAME" ) { 045 __PACKAGE__->set_sql( 046 create => q{ 047 CREATE TABLE rated_songs ( 048 path VARCHAR(256) 049 PRIMARY KEY NOT NULL, 050 energize INT, 051 schmoop INT 052 )}); 053 054 __PACKAGE__->sql_create() 055 ->execute(); 056 } 057 058 ############################# 059 package Rateplay::Song; 060 ############################# 061 use base q(Rateplay::DBI); 062 063 __PACKAGE__->table( 064 'rated_songs'); 065 __PACKAGE__->columns( All => 066 qw(path energize schmoop) 067 ); 068 069 ############################# 070 package main; 071 072 my $PLAYER = POE::Component::Player::Musicus->new(); 073 074 POE::Session->create( 075 package_states => [ 076 "main" => [ 077 qw(getpos getinfocurr 078 mp3_stdout song 079 scan_mp3s) ]], 080 081 inline_states => { 082 _start => \&my_gtk_init, 083 poll_player => sub { 084 $PLAYER->getpos(); 085 $poe_kernel->delay( 086 'poll_player', 1 ); 087 }}); 088 089 $poe_kernel->post( "main", 090 "poll_player" ); 091 $poe_kernel->post( "main", 092 "scan_mp3s" ); 093 $poe_kernel->run(); 094 095 ############################# 096 sub getpos { 097 ############################# 098 our $POS; 099 100 next_in_playlist() 101 if defined $POS 102 and $POS > 0 103 and $_[ARG0] < 0; 104 $POS = $_[ARG0]; 105 } 106 107 ############################# 108 sub getinfocurr { 109 ############################# 110 $TAG = $_[ARG0]; 111 $GUI{artist} 112 ->set( $TAG->{artist} ); 113 $GUI{title} 114 ->set( $TAG->{title} ); 115 } 116 117 ############################# 118 sub song { 119 ############################# 120 $SONG = $_[ARG0]; 121 $PLAYER->stop(); 122 $PLAYER->play($SONG); 123 $PLAYER->getinfocurr(); 124 update_rating($SONG); 125 } 126 127 ############################# 128 sub scan_mp3s { 129 ############################# 130 %RATED = 131 map { $_->path() => 1 } 132 Rateplay::Song 133 ->retrieve_all(); 134 135 my $comp = 136 POE::Component::Child 137 ->new( 138 events => { 139 'stdout' => 'mp3_stdout' 140 }); 141 142 $comp->run($FIND, 143 $SONG_DIR); 144 } 145 146 ############################# 147 sub add_label { 148 ############################# 149 my ($parent, $text, 150 @coords) = @_; 151 152 my $lbl= Gtk::Label->new(); 153 $lbl->set_alignment( 154 0.5, 0.5); 155 $lbl->set($text); 156 157 if (ref $parent eq 158 "Gtk::Table") { 159 $parent->attach_defaults( 160 $lbl, @coords); 161 } else { 162 $parent->pack_start( 163 $lbl, 0, 0, 0); 164 } 165 166 return $lbl; 167 } 168 169 ############################# 170 sub my_gtk_init { 171 ############################# 172 my @btns = ( 173 "Play Rated", "Play Next", 174 "Play Previous", 175 "Random Rate" 176 ); 177 178 $poe_kernel->alias_set( 179 'main'); 180 181 $GUI{mw} = 182 Gtk::Window->new(); 183 $GUI{mw}->set_default_size( 184 150, 200); 185 186 $GUI{vb} = 187 Gtk::VBox->new(0, 0); 188 189 $GUI{$_} = 190 Gtk::Button->new($_) 191 for @btns; 192 193 my $tbl = 194 Gtk::Table->new(2, 6); 195 $GUI{vb}->pack_start( 196 $tbl, 1, 1, 0); 197 198 add_label($tbl, 199 'Energize', 0, 1, 0, 1); 200 add_buttons( 201 $tbl, sub { 202 $PLAY_ENERG[$_[1]] ^= 1; 203 }, 0); 204 add_label( $tbl, 'Schmoop', 205 0, 1, 1, 2 ); 206 add_buttons( 207 $tbl, sub { 208 $PLAY_SCHMO[$_[1]] ^= 1; 209 }, 1); 210 211 # Status on top of buttons 212 $GUI{status} = 213 add_label($GUI{vb}, ""); 214 215 # Pack buttons 216 $GUI{vb}->pack_start( 217 $GUI{$_}, 0, 0, 0) 218 for @btns; 219 220 for (qw(artist title)) { 221 $GUI{$_} = add_label( 222 $GUI{vb}, ""); 223 } 224 225 $GUI{rate_table} = 226 Gtk::Table->new(2, 6); 227 $GUI{vb}->pack_start( 228 $GUI{rate_table},0,0,0); 229 230 add_label( 231 $GUI{rate_table}, 232 'Energize', 0, 1, 0, 1); 233 attach_radio_buttons( 234 $GUI{rate_table}, sub { 235 $RATE_ENERG = $_[1] +1; 236 }, 0, 237 \@RATE_ENERG_BUTTONS); 238 add_label( 239 $GUI{rate_table}, 240 'Schmoop', 0, 1, 1, 2); 241 attach_radio_buttons( 242 $GUI{rate_table}, sub { 243 $RATE_SCHMO = $_[1] +1; 244 }, 1, 245 \@RATE_SCHMO_BUTTONS); 246 247 my $rate = 248 Gtk::Button->new('Rate'); 249 $GUI{vb}->pack_start( 250 $rate, 0, 0, 0); 251 $GUI{mw}->add($GUI{vb}); 252 253 # Destroying window 254 $GUI{mw}->signal_connect( 255 'destroy', 256 sub { Gtk->exit(0) } ); 257 258 # Pressing Play Rated button 259 $GUI{ $btns[0] } 260 ->signal_connect( 261 'clicked', sub { 262 @PLAYLIST = 263 select_songs(); 264 $GUI{status}->set( 265 "Playlist has " 266 . scalar @PLAYLIST 267 . " songs." ); 268 next_in_playlist(); 269 }); 270 271 # Pressing Play Next button 272 $GUI{ $btns[1] } 273 ->signal_connect( 274 'clicked', sub { 275 next_in_playlist(); 276 }); 277 278 # Pressing "Play Previous" 279 $GUI{ $btns[2] } 280 ->signal_connect( 281 'clicked', sub { 282 next_in_playlist(1); 283 }); 284 285 # Pressing "Random Rate" 286 $GUI{ $btns[3] } 287 ->signal_connect( 288 'clicked', sub { 289 @PLAYLIST = 290 shuffle @MP3S; 291 $GUI{status}->set( 292 "Random Rating " 293 . scalar @PLAYLIST 294 . " songs." ); 295 next_in_playlist(); 296 }); 297 298 # Pressing "Rate" button 299 $rate->signal_connect( 300 'clicked', sub { 301 return 302 unless defined $TAG; 303 process_rating(); 304 next_in_playlist(); 305 } 306 ); 307 308 $GUI{mw}->show_all(); 309 } 310 311 ############################# 312 sub add_buttons { 313 ############################# 314 my($table, $sub, $row)= @_; 315 316 for (0 .. 4) { 317 my $b = 318 Gtk::CheckButton->new( 319 $_ + 1); 320 $b->signal_connect( 321 clicked => $sub, $_); 322 $table->attach_defaults( 323 $b, 1 + $_, 2 + $_, 324 0 + $row, 1 + $row ); 325 } 326 } 327 328 ############################# 329 sub attach_radio_buttons { 330 ############################# 331 my ($table, $sub, $row, 332 $buttons) = @_; 333 334 my $group; 335 336 for (0 .. 4) { 337 my $btn = 338 Gtk::RadioButton->new( 339 $_ + 1, 340 defined $group 341 ? $group : ()); 342 $group = $btn; 343 $btn->signal_connect( 344 clicked => $sub, $_); 345 push @$buttons, $btn; 346 $table->attach_defaults( 347 $btn, 1 + $_, 348 2 + $_, 0 + $row, 349 1 + $row 350 ); 351 } 352 } 353 354 ############################# 355 sub process_rating { 356 ############################# 357 my $rec = 358 Rateplay::Song 359 ->find_or_create( 360 { path => $SONG } ); 361 362 $rec->energize( 363 $RATE_ENERG); 364 $rec->schmoop($RATE_SCHMO); 365 $rec->update(); 366 } 367 368 ############################# 369 sub select_songs { 370 ############################# 371 my @energ = grep { 372 $PLAY_ENERG[ $_ - 1 ] 373 } ( 1 .. @PLAY_ENERG ); 374 375 my @schmo = grep { 376 $PLAY_SCHMO[ $_ - 1 ] 377 } ( 1 .. @PLAY_SCHMO ); 378 379 return sort { rand > 0.5 } 380 map { $_->path() } 381 Rateplay::Song 382 ->search_where({ 383 energize => 384 [ @energ, 0 ], 385 schmoop => 386 [ @schmo, 0 ]}); 387 } 388 389 ############################# 390 sub next_in_playlist { 391 ############################# 392 my ($backward) = @_; 393 394 return 395 unless scalar @PLAYLIST; 396 my $path; 397 398 { 399 if ($backward) { 400 $path = pop @PLAYLIST; 401 unshift @PLAYLIST, 402 $path; 403 } else { 404 $path = 405 shift @PLAYLIST; 406 push @PLAYLIST, $path; 407 } 408 redo 409 if defined $SONG 410 and $SONG eq $path 411 and @PLAYLIST > 1; 412 } 413 414 $PLAYER->stop(); 415 $poe_kernel->post('main', 416 'song', $path); 417 } 418 419 ############################# 420 sub update_rating { 421 ############################# 422 my ($path) = @_; 423 424 if(my ($song) = 425 Rateplay::Song->search( 426 path => $path)) { 427 428 my $e = $song->energize(); 429 my $s = $song->schmoop(); 430 431 $RATE_SCHMO_BUTTONS[$s-1] 432 ->activate(); 433 $RATE_ENERG_BUTTONS[$e-1] 434 ->activate(); 435 } else { 436 $RATE_SCHMO_BUTTONS[0] 437 ->activate(); 438 $RATE_ENERG_BUTTONS[0] 439 ->activate(); 440 } 441 } 442 443 ############################# 444 sub mp3_stdout { 445 ############################# 446 my ($self, $args) = 447 @_[ ARG0 .. $#_ ]; 448 449 return 450 if exists 451 $RATED{ $args->{out} }; 452 453 push @MP3S, $args->{out}; 454 455 $GUI{status}->set( 456 scalar @MP3S . " songs" . 457 " ready for rating."); 458 }