1# Copyright (C) 2005-2009 Quentin Sculo <squentin@free.fr> 2# 3# This file is part of Gmusicbrowser. 4# Gmusicbrowser is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License version 3, as 6# published by the Free Software Foundation 7 8=for gmbplugin AUDIOSCROBBLER 9name last.fm 10title last.fm plugin 11desc Submit played songs to last.fm 12=cut 13 14 15package GMB::Plugin::AUDIOSCROBBLER; 16use strict; 17use warnings; 18use constant 19{ CLIENTID => 'gmb', VERSION => '0.1', 20 OPT => 'PLUGIN_AUDIOSCROBBLER_',#used to identify the plugin's options 21 SAVEFILE => 'audioscrobbler.queue', #file used to save unsent data 22}; 23use Digest::MD5 'md5_hex'; 24require $::HTTP_module; 25 26our $ignore_current_song; 27 28my $self=bless {},__PACKAGE__; 29my @ToSubmit; my $NowPlaying; my $NowPlayingID; my $unsent_saved=0; 30my $interval=5; my ($timeout,$waiting); 31my ($HandshakeOK,$submiturl,$nowplayingurl,$sessionid); 32my ($Serrors,$Stop); 33my $Log=Gtk2::ListStore->new('Glib::String'); 34Load(); 35 36sub Start 37{ ::Watch($self,PlayingSong=> \&SongChanged); 38 ::Watch($self,Played => \&Played); 39 ::Watch($self,Save => \&Save); 40 $self->{on}=1; 41 Sleep(); 42 SongChanged() if $::TogPlay; 43 $Serrors=$Stop=undef; 44} 45sub Stop 46{ $waiting->abort if $waiting; 47 $waiting=undef; 48 ::UnWatch($self,$_) for qw/PlayingSong Played Save/; 49 $self->{on}=undef; 50 $interval=5; 51 #@ToSubmit=(); 52} 53 54sub prefbox 55{ my $vbox=Gtk2::VBox->new(::FALSE, 2); 56 my $sg1=Gtk2::SizeGroup->new('horizontal'); 57 my $sg2=Gtk2::SizeGroup->new('horizontal'); 58 my $entry1=::NewPrefEntry(OPT.'USER',_"username :", cb => \&userpass_changed, sizeg1 => $sg1,sizeg2=>$sg2); 59 my $entry2=::NewPrefEntry(OPT.'PASS',_"password :", cb => \&userpass_changed, sizeg1 => $sg1,sizeg2=>$sg2, hide => 1); 60 my $label2=Gtk2::Button->new(_"(see http://www.last.fm)"); 61 $label2->set_relief('none'); 62 $label2->signal_connect(clicked => sub 63 { my $url='http://www.last.fm'; 64 my $user=$::Options{OPT.'USER'}; 65 $url.="/user/$user/" if defined $user && $user ne ''; 66 ::openurl($url); 67 }); 68 my $ignore=Gtk2::CheckButton->new(_"Don't submit current song"); 69 $ignore->signal_connect(toggled=>sub { return if $_[0]->{busy}; $ignore_current_song= $_[0]->get_active ? $::SongID : undef; ::HasChanged('Lastfm_ignore_current'); }); 70 ::Watch($ignore,Lastfm_ignore_current => sub { $_[0]->{busy}=1; $_[0]->set_active(defined $ignore_current_song); delete $_[0]->{busy}; } ); 71 $vbox->pack_start($_,::FALSE,::FALSE,0) for $label2,$entry1,$entry2,$ignore; 72 $vbox->add( ::LogView($Log) ); 73 return $vbox; 74} 75sub userpass_changed 76{ $HandshakeOK=$Serrors=undef; 77 $Stop=undef if $Stop && $Stop eq 'BadAuth'; 78} 79 80sub SongChanged 81{ if (defined $ignore_current_song) 82 { return if defined $::SongID && $::SongID == $ignore_current_song; 83 $ignore_current_song=undef; ::HasChanged('Lastfm_ignore_current'); 84 } 85 $NowPlaying=undef; 86 my ($title,$artist,$album,$track,$length)= Songs::Get($::SongID,qw/title artist album track length/); 87 return if $title eq '' || $artist eq ''; 88 $NowPlaying= [ $artist, $title, $album, $length, $track, '' ]; 89 $NowPlayingID=$::SongID; 90 Sleep(); 91} 92 93sub Played 94{ my (undef,$ID,undef,$start_time,$seconds,$coverage)=@_; 95 return if $ignore_current_song; 96 return unless $seconds>10; 97 my $length= Songs::Get($ID,'length'); 98 if ($length>=30 && ($seconds >= 240 || $coverage >= .5) ) 99 { my ($title,$artist,$album,$track)= Songs::Get($ID,qw/title artist album track/); 100 return if $title eq '' || $artist eq ''; 101 ::IdleDo("9_".__PACKAGE__,10000,\&Save) if @ToSubmit>$unsent_saved; 102 push @ToSubmit,[ $artist,$title,$album,'',$length,$start_time,$track,'P' ]; 103 Sleep(); 104 } 105} 106 107sub Handshake 108{ $HandshakeOK=0; 109 my $user=$::Options{OPT.'USER'}; 110 return 0 unless defined $user && $user ne ''; 111 my $pass=$::Options{OPT.'PASS'}; 112 my $time=time; 113 my $auth=md5_hex(md5_hex($pass).$time); 114 Send(\&response_cb,'http://post.audioscrobbler.com/?hs=true&p=1.2&c='.CLIENTID.'&v='.VERSION."&u=$user&t=$time&a=$auth"); 115} 116 117sub response_cb 118{ my ($response,@lines)=@_; 119 my $error; 120 if (!defined $response) {$error=_"connection failed";} 121 elsif ($response eq 'OK') { } 122 elsif ($response=~m/^FAILED (.*)$/) {$error=$1} 123 elsif ($response eq 'BADAUTH') {$error=_("User authentification error"); $Stop='BadAuth';} 124 elsif ($response eq 'BANNED') {$error=_("Client banned, contact gmusicbrowser's developer"); $Stop='Banned';} 125 elsif ($response eq 'BADTIME') {$error=_("System clock is not close enough to the current time"); $Stop='BadTime';} 126 else {$error=_"unknown error";} 127 128 if (defined $error) 129 { unless ($Stop) 130 { $interval*=2; 131 $interval=30*60 if $interval>30*60; 132 $interval=60 if $interval<60; 133 $error.= ::__x( ' (' . _("retry in {seconds} s") . ')', seconds => $interval); 134 } 135 Log(_("Handshake failed : ").$error); 136 } 137 else 138 { ($sessionid,$nowplayingurl,$submiturl)=@lines; 139 $interval=5; 140 $HandshakeOK=1; 141 $Serrors=0; 142 Log(_"Handshake OK"); 143 } 144} 145 146sub Submit 147{ my $post="s=$sessionid"; 148 my $i=0; 149 my $url; 150 if (@ToSubmit) 151 { while (my $aref=$ToSubmit[$i]) 152 { my @data= map { defined $_ ? ::url_escapeall($_) : "" } @$aref; 153 $post.=sprintf "&a[$i]=%s&t[$i]=%s&b[$i]=%s&m[$i]=%s&l[$i]=%s&i[$i]=%s&n[$i]=%s&o[$i]=%s&r[$i]=", @data; 154 $i++; 155 last if $i==50; #don't submit more than 50 songs at a time 156 } 157 $url=$submiturl; 158 return unless $i; 159 } 160 elsif ($NowPlaying) 161 { if (!defined $::PlayingID || $::PlayingID!=$NowPlayingID) { $NowPlaying=undef; return } 162 my @data= map { defined $_ ? ::url_escapeall($_) : "" } @$NowPlaying; 163 $post.= sprintf "&a=%s&t=%s&b=%s&l=%s&n=%s&m=%s", @data; 164 $url=$nowplayingurl; 165 } 166 else {return} 167 my $response_cb=sub 168 { my ($response,@lines)=@_; 169 my $error; 170 if (!defined $response) {$error=_"connection failed"; $Serrors++} 171 elsif ($response eq 'OK') 172 { $Serrors=0; 173 if ($i) 174 { Log( _("Submit OK") . ' ('. 175 ($i>1 ? ::__n("%d song","%d songs",$i) 176 : ::__x( _"{song} by {artist}", song=> $ToSubmit[0][1], artist => $ToSubmit[0][0]) ) . ')' ); 177 splice @ToSubmit,0,$i; 178 ::IdleDo("9_".__PACKAGE__,10000,\&Save) if $unsent_saved; 179 } 180 elsif ($NowPlaying) 181 { Log( _("Submit Now-Playing OK") . ' ('. 182 ::__x( _"{song} by {artist}", song=> $NowPlaying->[1], artist => $NowPlaying->[0]) . ')' ); 183 $NowPlaying=undef; 184 } 185 } 186 elsif ($response eq 'BADSESSION') 187 { $error=_"Bad session"; 188 $HandshakeOK=0; 189 } 190 elsif ($response=~m/^FAILED (.*)$/) 191 { $error=$1; 192 $Serrors++; 193 } 194 else {$error=_"unknown error"; $Serrors++} 195 196 $HandshakeOK=0 if $Serrors && $Serrors>2; 197 198 if (defined $error) 199 { Log(_("Submit failed : ").$error); 200 } 201 }; 202 203 warn "submitting: $post\n" if $::debug; 204 Send($response_cb,$url,$post); 205} 206 207sub Sleep 208{ #warn "Sleep\n"; 209 return unless $self->{on}; 210 return if $Stop || $waiting || $timeout; 211 $timeout=Glib::Timeout->add(1000*$interval,\&Awake) if @ToSubmit || $NowPlaying; 212 #warn "Sleeping $interval seconds\n" if $timeout; 213} 214sub Awake 215{ #warn "Awoke\n"; 216 $timeout=undef; 217 return 0 unless $self->{on}; 218 if ($HandshakeOK) { Submit(); } 219 else { Handshake(); } 220 Sleep(); 221 return 0; 222} 223sub Send 224{ my ($response_cb,$url,$post)=@_; 225 my $cb=sub 226 { my @response=(defined $_[0])? split "\012",$_[0] : (); 227 $waiting=undef; 228 &$response_cb(@response); 229 Sleep(); 230 }; 231 $waiting=Simple_http::get_with_cb(cb => $cb,url => $url,post => $post); 232} 233 234sub Log 235{ my $text=$_[0]; 236 $Log->set( $Log->prepend,0, localtime().' '.$text ); 237 warn "$text\n" if $::debug; 238 if (my $iter=$Log->iter_nth_child(undef,50)) { $Log->remove($iter); } 239} 240 241sub Load #read unsent data 242{ return unless -r $::HomeDir.SAVEFILE; 243 return unless open my$fh,'<:utf8',$::HomeDir.SAVEFILE; 244 while (my $line=<$fh>) 245 { chomp $line; 246 my @data=split "\x1D",$line; 247 push @ToSubmit,\@data if @data==8; 248 } 249 close $fh; 250 Log(::__("Loaded %d unsent song from previous session","Loaded %d unsent songs from previous session", scalar @ToSubmit)); 251} 252sub Save #save unsent data to a file 253{ $unsent_saved=@ToSubmit; 254 unless (@ToSubmit) 255 { unlink $::HomeDir.SAVEFILE; return } 256 my $fh; 257 unless (open $fh,'>:utf8',$::HomeDir.SAVEFILE) 258 { warn "Error creating '$::HomeDir".SAVEFILE."' : $!\nUnsent last.fm data will be lost.\n"; return; } 259 print $fh join("\x1D",@$_)."\n" for @ToSubmit; 260 close $fh; 261} 262 2631; 264