1package Music::Audioscrobbler::MPD; 2our $VERSION = 0.13; 3require 5.006; 4 5# Copyright (c) 2007 Edward J. Allen III 6# Some code and inspiration from Audio::MPD Copyright (c) 2005 Tue Abrahamsen, Copyright (c) 2006 Nicholas J. Humfrey, Copyright (c) 2007 Jerome Quelin 7 8# 9# You may distribute under the terms of either the GNU General Public 10# License or the Artistic License, as specified in the README file. 11# 12 13 14# the GNU Public License. Both are distributed with Perl. 15 16=pod 17 18=for changes stop 19 20=head1 NAME 21 22Music::Audioscrobbler::MPD - Module providing routines to submit songs to last.fm from MPD. 23 24=for readme stop 25 26=head1 SYNOPSIS 27 28 use Music::Audioscrobbler::MPD 29 my $mpds = Music::Audioscrobbler::MPD->new(\%options); 30 $mpds->monitor_mpd(); 31 32=for readme continue 33 34=head1 DESCRIPTION 35 36Music::Audioscrobbler::MPD is a scrobbler for MPD. As of version .1, L<Music::Audioscrobbler::Submit> is used to submit information to last.fm. 37 38All internal code is subject to change. See L<musicmpdscrobble> for usage info. 39 40=begin readme 41 42=head1 INSTALLATION 43 44To install this module type the following: 45 46 perl Makefile.PL 47 make 48 make test 49 make install 50 51=head1 CONFIGURATION 52 53There is a sample config file under examples. A sample init file that I use for 54gentoo linux is there as well. 55 56=head1 USE 57 58Edit the sample config file and copy to /etc/musicmpdscrobble.conf or ~/.musicmpdscrobble.conf 59 60Test your configuration by issue the command 61 62 musicmpdscrobble --logfile=STDERR --monitor 63 64and playing some music. 65 66If it works, then the command 67 68 musicmpdscrobble --daemonize 69 70will run musicmpdscrobble as a daemon. Please see examples for a sample init script. If you make an init script 71for your distribution, please send it to me! 72 73=head1 DEPENDENCIES 74 75This module requires these other modules and libraries: 76 77 Music::Audioscrobbler::Submit 78 File::Spec 79 Digest::MD5 80 Encode 81 IO::Socket 82 IO::File 83 Config::Options 84 85I strongly encourage you to also install my module 86 87 Music::Tag 88 89This will allow you to read info from the file tag (such as the MusicBrainz ID). 90 91The version info in the Makefile is based on what I use. You can get 92away with older versions in many cases. 93 94=end readme 95 96=head1 MORE HELP 97 98Please see the documentation for L<musicmpdscrobble> which is available from 99 100 musicmpdscrobble --longhelp 101 102=for readme stop 103 104=cut 105 106use strict; 107use warnings; 108use Music::Audioscrobbler::Submit; 109use File::Spec; 110use Digest::MD5 qw(md5_hex); 111use Encode qw(encode); 112use IO::Socket; 113use IO::File; 114use Config::Options; 115use POSIX qw(WNOHANG); 116#use Storable; 117 118 119sub _default_options { 120 { lastfm_username => undef, 121 lastfm_password => undef, 122 mdb_opts => {}, 123 musictag => 0, 124 musictag_overwrite => 0, 125 verbose => 1, 126 monitor => 1, 127 daemonize => 0, 128 timeout => 15, # Set low to prevent missing a scrobble. Rather retry submit. 129 pidfile => "/var/run/musicmpdscrobble.pid", 130 logfile => undef, 131 default_cache_time => 86400, 132 mpd_password => undef, 133 allow_stream => 0, 134 mpd_server => $ENV{MPD_HOST} || 'localhost', 135 mpd_port => $ENV{MPD_PORT} || 6600, 136 music_directory => "/mnt/media/music/MP3s", 137 scrobble_queue => $ENV{HOME} . "/.musicaudioscrobbler_queue", 138 optionfile => [ "/etc/musicmpdscrobble.conf", $ENV{HOME} . "/.musicmpdscrobble.conf" ], 139 runonstart => [], 140 runonsubmit => [], 141 lastfm_client_id => "mam", 142 lastfm_client_version => "0.1", 143 music_tag_opts => { 144 quiet => 1, 145 verbose => 0, 146 ANSIColor => 0, 147 }, 148 }; 149} 150 151=head1 METHODS 152 153=over 4 154 155=item new() 156 157 my $mpds = Music::Audioscrobbler::MPD->new($options); 158 159=cut 160 161sub new { 162 my $class = shift; 163 my $options = shift || {}; 164 my $self = {}; 165 bless $self, $class; 166 $self->options( $self->_default_options ); 167 if ($options->{optionfile}) { 168 $self->options->options("optionfile", $options->{optionfile}); 169 } 170 $self->options->fromfile_perl( $self->options->{optionfile} ); 171 $self->options($options); 172 $self->{scrobble_ok} = 1; 173 $self->_convert_password(); 174 175 if ($self->options->{lastfm_client_id} eq "tst") { 176 $self->status(0, "WARNING: Using client id 'tst' is NO LONGER approved. Please use 'mam' or other assigned ID"); 177 } 178 if ($self->options("mpd_server") =~ /^(.*)@(.*)/) { 179 $self->options->{"mpd_server"} = $2; 180 $self->options->{"mpd_password"} = $1; 181 } 182 return $self; 183} 184 185sub _convert_password { 186 my $self = shift; 187 unless ( $self->options('lastfm_md5password') ) { 188 if ( $self->options('lastfm_password') ) { 189 $self->options->{lastfm_md5password} = 190 Digest::MD5::md5_hex( $self->options->{lastfm_password} ); 191 delete $self->options->{lastfm_password}; 192 } 193 } 194} 195 196 197=item monitor_mpd() 198 199Starts the main loop. 200 201=cut 202 203sub monitor_mpd { 204 my $self = shift; 205 $self->status( 1, "Starting Music::Audioscrobbler::MPD version $VERSION" ); 206 while (1) { 207 if ( $self->is_connected ) { 208 $self->update_info(); 209 sleep 1; 210 } 211 else { 212 $self->connect; 213 sleep 4; 214 } 215 unless ( $self->{scrobble_ok} ) { 216 if ( ( time - $self->{lastscrobbled} ) > 600 ) { 217 $self->{scrobble_ok} = $self->mas->process_scrobble_queue(); 218 $self->{lastscrobbled} = time; 219 } 220 } 221 $self->_reaper(); 222 } 223} 224 225=item options() 226 227Get or set options via hash. Here is a list of available options: 228 229=over 4 230 231=item optionfile 232 233Perl file used to get options from 234 235=item lastfm_username 236 237lastfm username 238 239=item lastfm_password 240 241lastfm password. Not needed if lastfm_md5password is set. 242 243=item lastfm_md5password 244 245MD5 hash of lastfm password. 246 247=item lastfm_client_id 248 249Client ID provided by last.fm. Defaults to "tst", which is valid for testing only. 250 251=item lastfm_client_version 252 253Set to the version of your program when setting a valid client_id. Defaults to "1.0" 254 255=item mpd_server 256 257hostname of mpd_server 258 259=item mpd_port 260 261port for mpd_server 262 263=item mpd_password 264 265mpd password 266 267=item verbose 268 269Set verbosity level (1 through 4) 270 271=item logfile 272 273File to output loginfo to 274 275=item scrobblequeue 276 277Path to file to queue info to 278 279=item music_directory 280 281Root to MP3 files 282 283=item get_mbid_from_mb 284 285Use the Music::Tag::MusicBrainz plugin to get missing "mbid" value. 286 287=item runonsubmit 288 289Array of commands to run after submit 290 291=item runonstart 292 293Array of commands to run on start of play 294 295=item monitor 296 297True if monitor should be turned on 298 299=item musictag 300 301True if you want to use Music::Tag to get info from file 302 303=item musictag_overwrite 304 305True if you want to Music::Tag info to override file info 306 307 308=item music_tag_opts 309 310Options for Music::Tag 311 312=item proxy_server 313 314Specify a procy server in the form http://proxy.server.tld:8080. Please note that environment is checked for HTTP_PROXY, so you may not need this option. 315 316=item allow_stream 317 318If set to true, will scrobble HTTP streams. 319 320=back 321 322=back 323 324=cut 325 326sub options { 327 my $self = shift; 328 if ( exists $self->{_options} ) { 329 return $self->{_options}->options(@_); 330 } 331 else { 332 $self->{_options} = Config::Options->new(); 333 return $self->{_options}->options(@_); 334 } 335} 336 337=head1 INTERNAL METHODS (for reference) 338 339=over 340 341=item mpdsock() 342 343returns open socket to mpd program. 344 345=cut 346 347sub mpdsock { 348 my $self = shift; 349 my $new = shift; 350 if ($new) { 351 $self->{mpdsock} = $new; 352 } 353 unless ( exists $self->{mpdsock} ) { 354 $self->{mpdsock} = undef; 355 } 356 return $self->{mpdsock}; 357} 358 359=item connect() 360 361Connect to MPD if necessary 362 363=cut 364 365sub connect { 366 my $self = shift; 367 if ( ( $self->mpdsock ) && ( $self->is_connected ) ) { 368 $self->status( 3, "Already connected just fine." ); 369 return 1; 370 } 371 372 $self->mpdsock( 373 IO::Socket::INET->new( PeerAddr => $self->options("mpd_server"), 374 PeerPort => $self->options("mpd_port"), 375 Proto => 'tcp', 376 ) 377 ); 378 379 unless ( ( $self->mpdsock ) && ( $self->mpdsock->connected ) ) { 380 $self->status( 1, "Could not create socket to mpd: $!" ); 381 return 0; 382 } 383 384 if ( $self->mpdsock->getline() =~ /^OK MPD (.+)$/ ) { 385 $self->{mpd_sever_version} = $1; 386 } 387 else { 388 $self->status( 1, "Bad response from mpd ($!)" ); 389 return 0; 390 } 391 $self->send_password if $self->options("mpd_password"); 392 return 1; 393} 394 395=item is_connected() 396 397Return true if connected to mpd. 398 399=cut 400 401sub is_connected { 402 my $self = shift; 403 if ( ( $self->mpdsock ) && ( $self->mpdsock->connected ) ) { 404 $self->mpdsock->print("ping\n"); 405 return ( $self->mpdsock->getline() =~ /^OK/ ); 406 } 407 return undef; 408} 409 410=item process_feedback 411 412Process response from mpd. 413 414=cut 415 416sub process_feedback { 417 my $self = shift; 418 my @output; 419 if ( ( $self->mpdsock ) && ( $self->mpdsock->connected ) ) { 420 while ( my $line = $self->mpdsock->getline() ) { 421 chomp($line); 422 423 # Did we cause an error? Save the data! 424 if ( $line =~ /^ACK \[(\d+)\@(\d+)\] {(.*)} (.+)$/ ) { 425 $self->{ack_error_id} = $1; 426 $self->{ack_error_command_id} = $2; 427 $self->{ack_error_command} = $3; 428 $self->{ack_error} = $4; 429 $self->status( 1, "Error sent to MPD: $line" ); 430 return undef; 431 } 432 last if ( $line =~ /^OK/ ); 433 push( @output, $line ); 434 } 435 } 436 437 # Let's return the output for post-processing 438 return @output; 439} 440 441=item send_command($command) 442 443send a command to mpd. 444 445=cut 446 447sub send_command { 448 my $self = shift; 449 if ( $self->is_connected ) { 450 $self->mpdsock->print( @_, "\n" ); 451 return $self->process_feedback; 452 } 453} 454 455=item send_password($command) 456 457send password to mpd. 458 459=cut 460 461sub send_password { 462 my $self = shift; 463 $self->send_command( "password ", $self->options("mpd_password")); 464} 465 466=item get_info($command) 467 468Send mpd a command and parse the output if output is a column seperated list. 469 470=cut 471 472sub get_info { 473 my $self = shift; 474 my $command = shift; 475 my $ret = {}; 476 foreach ( $self->send_command($command) ) { 477 if (/^(.[^:]+):\s(.+)$/) { 478 $ret->{$1} = $2; 479 } 480 } 481 return $ret; 482} 483 484=item get_status($command) 485 486 487get_status command. Returns hashref with: 488 489 * volume: (0-100) 490 * repeat: (0 or 1) 491 * random: (0 or 1) 492 * playlist: (31-bit unsigned integer, the playlist version number) 493 * playlistlength: (integer, the length of the playlist) 494 * playlistqueue: (integer, the temporary fifo playlist version number) 495 * xfade: <int seconds> (crossfade in seconds) 496 * state: ("play", "stop", or "pause") 497 * song: (current song stopped on or playing, playlist song number) 498 * songid: (current song stopped on or playing, playlist songid) 499 * time: <int elapsed>:<time total> (of current playing/paused song) 500 * bitrate: <int bitrate> (instantaneous bitrate in kbps) 501 * audio: <int sampleRate>:<int bits>:<int channels> 502 * updating_db: <int job id> 503 * error: if there is an error, returns message here 504 505=cut 506 507sub get_status { 508 my $self = shift; 509 $self->get_info("status"); 510} 511 512=item get_current_song_info($command) 513 514get_status command. Returns hashref with: 515 516 file: albums/bob_marley/songs_of_freedom/disc_four/12.bob_marley_-_could_you_be_loved_(12"_mix).flac 517 Time: 327 518 Album: Songs Of Freedom - Disc Four 519 Artist: Bob Marley 520 Title: Could You Be Loved (12" Mix) 521 Track: 12 522 Pos: 11 523 Id: 6601 524 525=cut 526 527sub get_current_song_info { 528 my $self = shift; 529 $self->get_info("currentsong"); 530} 531 532=item status($level, @message) 533 534Print to log. 535 536=cut 537 538sub status { 539 my $self = shift; 540 my $level = shift; 541 if ( $level <= $self->options->{verbose} ) { 542 my $out = $self->logfileout; 543 print $out scalar localtime(), " ", @_, "\n"; 544 } 545} 546 547=item logfileout 548 549returns filehandle to log. 550 551=cut 552 553sub logfileout { 554 my $self = shift; 555 my $fh = shift; 556 if ($fh) { 557 $self->{logfile} = $fh; 558 } 559 if ((not $self->options->{logfile}) or ($self->options->{logfile} eq "STDERR" )) { 560 return \*STDERR; 561 } 562 elsif ($self->options->{logfile} eq "STDOUT" ) { 563 return \*STDOUT; 564 } 565 unless ( ( exists $self->{logfile} ) && ( $self->{logfile} ) ) { 566 my $fh = IO::File->new( $self->options->{logfile}, ">>" ); 567 unless ($fh) { 568 print STDERR "Error opening log, using STDERR: $!"; 569 return \*STDERR; 570 } 571 $fh->autoflush(1); 572 $self->{logfile} = $fh; 573 } 574 return $self->{logfile}; 575} 576 577=item mas() 578 579Reference to underlying Music::Audioscrobbler::Submit object. If passed a Music::Audioscrobbler::Submit object, will 580use that one instead. 581 582=cut 583 584sub mas { 585 my $self = shift; 586 my $new = shift; 587 if ($new) { 588 $self->{mas} = $new; 589 } 590 unless ((exists $self->{mas}) && (ref $self->{mas})) { 591 $self->{mas} = Music::Audioscrobbler::Submit->new($self->options); 592 $self->{mas}->logfileout($self->logfileout); 593 } 594 return $self->{mas}; 595} 596 597=item new_info($cinfo) 598 599reset current song info. 600 601=cut 602 603sub new_info { 604 my $self = shift; 605 my $cinfo = shift; 606 $self->{current_song} = $cinfo->{file}; 607 if ( $self->{current_song} =~ /^http/i ) { 608 if ($self->options("allow_stream")) { 609 $self->{current_file} = 0; 610 } 611 else { 612 $self->{current_file} = undef; 613 } 614 } 615 elsif ( -e File::Spec->rel2abs( $self->{current_song}, $self->options->{music_directory} ) ) { 616 $self->{current_file} = 617 File::Spec->rel2abs( $self->{current_song}, $self->options->{music_directory} ); 618 } 619 else { 620 $self->status(1, "File not found: ", File::Spec->rel2abs( $self->{current_song}, $self->options->{music_directory} )); 621 $self->{current_file} = 0; 622 } 623 my $h = { album => $cinfo->{Album}, 624 artist => $cinfo->{Artist}, 625 title => $cinfo->{Title}, 626 secs => $cinfo->{Time}, 627 }; 628 if ($self->options->{musictag}) { 629 $h->{filename} = $self->{current_file}; 630 } 631 $self->{info} = $self->mas->info_to_hash( $h ); 632 633 #Prevent excessive calls to info_to_hash 634 delete $self->{info}->{filename}; 635 636 $self->{song_duration} = $cinfo->{Time}; 637 $self->{current_id} = $cinfo->{Id}; 638 $self->{running_time} = 0; 639 $self->{last_running_time} = undef; 640 $self->{state} = ""; 641 $self->{started_at} = time; 642 $self->status( 1, "New Song: ", $self->{current_id}, " - ", ($self->{current_file} ? $self->{current_file} : "Unknown File: $self->{current_song}") ); 643} 644 645=item song_change($cinfo) 646 647Run on song change 648 649=cut 650 651sub song_change { 652 my $self = shift; 653 my $cinfo = shift; 654 if ( ( defined $self->{current_file} ) 655 and ( ( $self->{running_time} >= 240 ) 656 or ( $self->{running_time} >= ( $self->{song_duration} / 2 ) ) ) 657 and ( ( $self->{song_duration} >= 30 ) or ( $self->{info}->{mbid} ) ) 658 ) { 659 $self->scrobble(); 660 $self->run_commands( $self->options->{runonsubmit} ); 661 } 662 else { 663 $self->status( 4, "Not scrobbling ", 664 $self->{current_file}, " with run time of ", 665 $self->{running_time} ); 666 } 667 my $state = $self->{state}; 668 $self->new_info($cinfo); 669 if ( ( defined $self->{current_file} ) && ( $cinfo->{Time} ) && ( $state eq "play" ) ) { 670 $self->status( 4, "Announcing start of play for: ", $self->{current_file} ); 671 $self->mas->now_playing( $self->{info} ); 672 $self->run_commands( $self->options->{runonstart} ); 673 } 674 else { 675 $self->status( 4, "Not announcing start of play for: ", $self->{current_file} ); 676 } 677 $self->status("4", "Storing debug info"); 678 #$Storable::forgive_me = 1; 679 #store($self, $self->options->{logfile}.".debug"); 680} 681 682=item update_info() 683 684Run on poll 685 686=cut 687 688sub update_info { 689 my $self = shift; 690 my $status = $self->get_status; 691 my $cinfo = $self->get_current_song_info(); 692 $self->{state} = $status->{state}; 693 my ( $so_far, $total ) = (0,0); 694 if ($status->{'time'}) { 695 ( $so_far, $total ) = split( /:/, $status->{'time'} ); 696 } 697 my $time = time; 698 if ( $self->{state} eq "play" ) { 699 unless ( $cinfo->{Id} eq $self->{current_id} ) { 700 $self->song_change($cinfo); 701 } 702 unless ( defined $self->{last_running_time} ) { 703 $self->{last_running_time} = $so_far; 704 } 705 unless ( defined $self->{last_update_time} ) { 706 $self->{last_update_time} = $time; 707 } 708 my $run_since_update = ( $so_far - $self->{last_running_time} ); 709 710 my $time_since_update = 711 ( $time - $self->{last_update_time} ) + 5; # Adding 5 seconds for rounding fudge 712 713 if ( ( $run_since_update > 0 ) && ( $run_since_update <= $time_since_update ) ) { 714 $self->{running_time} += $run_since_update; 715 } 716 elsif ( ( $run_since_update < -240 ) 717 or ( $run_since_update < ( -1 * ( $self->{song_duration} / 2 ) ) ) ) { 718 $self->status( 719 3, 720 "Long skip back detected ( $run_since_update ). You like this song. Scrobbling... " 721 ); 722 $self->song_change($cinfo); 723 } 724 elsif ($run_since_update) { 725 $self->status( 3, "Skip detected, ignoring time change." ); 726 } 727 $self->{last_running_time} = $so_far; 728 $self->{last_update_time} = $time; 729 } 730 elsif ( ( $self->{state} eq "stop" ) && ( $self->{running_time} ) ) { 731 $self->song_change($cinfo); 732 } 733 if ( $self->options->{monitor} ) { 734 $self->monitor(); 735 } 736} 737 738 739=item monitor() 740 741print current status to STDERR 742 743=cut 744 745sub monitor { 746 my $self = shift; 747 printf STDERR "%5s ID: %4s TIME: %5s \r", $self->{state} ? $self->{state} : "", $self->{current_id} ? $self->{current_id} : "", 748 $self->{running_time} ? $self->{running_time} : ""; 749} 750 751 752=item scrobble() 753 754Scrobble current song 755 756=cut 757 758sub scrobble { 759 my $self = shift; 760 if ( defined $self->{current_file} ) { 761 $self->status( 2, "Adding ", $self->{current_file}, " to scrobble queue" ); 762 $self->{scrobble_ok} = $self->mas->submit( [ $self->{info}, $self->{started_at} ] ); 763 $self->{lastscrobbled} = time; 764 } 765 else { 766 $self->status( 3, "Skipping stream: ", $self->{current_file} ); 767 } 768} 769 770 771=item run_commands() 772 773Fork and run list of commands. 774 775=cut 776 777sub run_commands { 778 my $self = shift; 779 my $commands = shift; 780 return unless ( ( ref $commands ) && ( scalar @{$commands} ) ); 781 my $pid = fork; 782 if ($pid) { 783 $self->_toreap($pid); 784 $self->status( 4, "Forked to run commands\n" ); 785 } 786 elsif ( defined $pid ) { 787 if ( $self->options->{logfile} ) { 788 my $out = $self->logfileout; 789 open STDOUT, ">&", $out; 790 select STDOUT; 791 $| = 1; 792 open STDERR, ">&", $out; 793 select STDERR; 794 $| = 1; 795 } 796 foreach my $c ( @{$commands} ) { 797 $c =~ s/\%f/$self->{current_file}/e; 798 $c =~ s/\%a/$self->{info}->{artist}/e; 799 $c =~ s/\%b/$self->{info}->{album}/e; 800 $c =~ s/\%t/$self->{info}->{title}/e; 801 $c =~ s/\%l/$self->{info}->{secs}/e; 802 $c =~ s/\%n/$self->{info}->{track}/e; 803 $c =~ s/\%m/$self->{info}->{mbid}/e; 804 my $s = system($c); 805 delete $self->{fh}; 806 807 if ($s) { 808 $self->status( 0, "Failed to run command: ${c}: $!" ); 809 } 810 else { 811 $self->status( 2, "Command ${c} successful" ); 812 } 813 } 814 exit; 815 } 816 else { 817 $self->status( 0, "Failed to fork for commands: $!" ); 818 } 819} 820 821sub _toreap { 822 my $self = shift; 823 my $pid = shift; 824 unless (exists $self->{reapme}) { 825 $self->{reapme} = []; 826 } 827 push @{$self->{reapme}}, $pid; 828} 829 830sub _reaper { 831 my $self = shift; 832 if (exists $self->{reapme}) { 833 my @newreap = (); 834 foreach (@{$self->{reapme}}) { 835 (waitpid $_, WNOHANG) or push @newreap, $_; 836 } 837 if (@newreap) { 838 $self->{reapme} = \@newreap; 839 } 840 else { 841 delete $self->{reapme}; 842 } 843 } 844} 845 846 847=back 848 849=head1 SEE ALSO 850 851L<musicmpdscrobble>, L<Music::Audioscrobbler::Submit>, L<Music::Tag> 852 853=for changes continue 854 855=head1 CHANGES 856 857=over 4 858 859=item Release Name: 0.13 860 861=over 4 862 863=item * 864 865Added option allow_stream, which will allow scrobbling of http streams if set to true (default false). Feature untested. 866 867=item * 868 869Fixed bug in password submition (thanks joeblow1102) 870 871=item * 872 873Added support for password@host value in MPD_HOST 874 875=item * 876 877Searched, without success, for memory leak. If anyone wants to help, uncomment the Storable lines and start looking into it... 878 879=item * 880 881Added (documented) support for Proxy server 882 883=back 884 885=back 886 887=over 4 888 889=item Release Name: 0.12 890 891=over 4 892 893=item * 894 895Fixed bug that sometimes prevented Music::Tag from working at all. Added some level 4 debug messages. 896 897=back 898 899=back 900 901 902=over 4 903 904=item Release Name: 0.11 905 906=over 4 907 908=item * 909 910Added musictag_overwrite option. This is false by default. It is a workaround for problems with Music::Tag and unicode. Setting this to 911true allows Music::Tag info to overwrite info from MPD. Do not set this to true until Music::Tag returns proper unicode consistantly. 912 913=back 914 915=back 916 917=over 4 918 919=item Release Name: 0.1 920 921=over 4 922 923=item * 924 925Split off all scrobbling code to Music::Audioscrobbler::Submit 926 927=item * 928 929Added an error message if file is not found. 930 931=item * 932 933Added use warnings for better debugging. 934 935=item * 936 937Started using Pod::Readme for README and CHANGES 938 939=back 940 941=begin changes 942 943=item Release Name: 0.09 944 945=over 4 946 947=item * 948 949Added waffelmanna's patch to fix the password submital to MPD. 950 951=back 952 953=item Release Name: 0.08 954 955=over 4 956 957=item * 958 959musicmpdscrobble daemonizes after creating Music::Audioscrobber::MPD object which allows pidfile to be set in options file (thanks K-os82) 960 961=item * 962 963Kwalitee changes such as pod fixes, license notes, etc. 964 965=item * 966 967Fixed bug which prevented working with a password to mpd. 968 969=item * 970 971Fixed bug causing reaper to block. 972 973=back 974 975=item Release Name: 0.07 976 977=over 4 978 979=item * 980 981Fixed Unicode issues with double encoding (thanks slothck) 982 983=item * 984 985Stoped using URI::Encode which did NOT solve locale issues. 986 987=back 988 989=item Release Name: 0.06 990 991=over 4 992 993=item * 994 995Configured get_mbid_from_mb to only grab if missing. 996 997=item * 998 999Changed to using URI::Encode 1000 1001=item * 1002 1003Fixed bug preventing log file from loading from command line. 1004 1005=back 1006 1007=item Release Name: 0.05 1008 1009=over 4 1010 1011=item * 1012 1013Fixed bug with log file handles (thanks T0dK0n) 1014 1015=item * 1016 1017Fixed bug caused when music_directory not set (thanks T0dK0n) 1018 1019=item * 1020 1021Revised Documentation Slightly 1022 1023=item * 1024 1025Fixed bug in kill function for musicmpdscrobble 1026 1027=item * 1028 1029Added option get_mbid_from_mb to get missing mbids using Music::Tag::MusicBrainz 1030 1031=back 1032 1033=item Release Name: 0.04 1034 1035=over 4 1036 1037=item * 1038 1039Have been assigned Client ID. If you set this in your configs, please remove. 1040 1041=back 1042 1043=item Release Name: 0.03 1044 1045=over 4 1046 1047=item * 1048 1049Name change for module. Is now Music::Audioscrobbler::MPD. Uninstall old version to facilitate change! 1050 1051=item * 1052 1053Repeating a song isn't a skip anymore (or rather skipping back a scrobblable distance is not a skip) 1054 1055=item * 1056 1057Only submits a song <30 seconds long if it has an mbid. 1058 1059=item * 1060 1061Very basic test script for sanity. 1062 1063=back 1064 1065=item Release Name: 0.02 1066 1067=over 4 1068 1069=item * 1070 1071Fixed bug caused my Music::Tag returning non-integer values for "secs" (thanks tunefish) 1072 1073=item * 1074 1075Along same lines, configure to not use Music::Tag secs values, but trust MPD 1076 1077=back 1078 1079=item Release Name: 0.01 1080 1081=over 4 1082 1083=item * 1084 1085Initial Release 1086 1087=item * 1088 1089Basic routines for scrobbling MPD. Code from Music::Audioscrobbler merged for now. 1090 1091=back 1092 1093=end changes 1094 1095=back 1096 1097=for changes stop 1098 1099=for readme continue 1100 1101=head1 AUTHOR 1102 1103Edward Allen, ealleniii _at_ cpan _dot_ org 1104 1105=head1 COPYRIGHT 1106 1107Copyright (c) 2007 Edward J. Allen III 1108 1109Some code and inspiration from L<Audio::MPD> 1110Copyright (c) 2005 Tue Abrahamsen, Copyright (c) 2006 Nicholas J. Humfrey, Copyright (c) 2007 Jerome Quelin 1111 1112=head1 LICENSE 1113 1114This program is free software; you can redistribute it and/or modify 1115it under the same terms as Perl itself, either: 1116 1117a) the GNU General Public License as published by the Free 1118Software Foundation; either version 1, or (at your option) any 1119later version, or 1120 1121b) the "Artistic License" which comes with Perl. 1122 1123This program is distributed in the hope that it will be useful, 1124but WITHOUT ANY WARRANTY; without even the implied warranty of 1125MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either 1126the GNU General Public License or the Artistic License for more details. 1127 1128You should have received a copy of the Artistic License with this 1129Kit, in the file named "Artistic". If not, I'll be glad to provide one. 1130 1131You should also have received a copy of the GNU General Public License 1132along with this program in the file named "Copying". If not, write to the 1133Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 1134Boston, MA 02110-1301, USA or visit their web page on the Internet at 1135http://www.gnu.org/copyleft/gpl.html. 1136 1137 1138=cut 1139 11401; 1141