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