# Copyright (C) 2005-2008 Quentin Sculo <squentin@free.fr>
#
# This file is part of Gmusicbrowser.
# Gmusicbrowser is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation

=gmbplugin FETCHCOVER
Picture finder
Picture finder plugin
Adds a menu entry to artist/album context menu, allowing to search the picture/cover in google and save it.
=cut

package GMB::Plugin::FETCHCOVER;
use strict;
use warnings;
require 'simple_http.pm';
use base 'Gtk2::Window';
use constant
{	OPT => 'PLUGIN_FETCHCOVER_',
	RES_LINES => 4,
	RES_PER_LINE => 5,
	PREVIEW_SIZE => 100,
};
use constant RES_PER_PAGE => RES_PER_LINE*RES_LINES;

my %Sites=
(artist =>
 {	googlei => ['google images',"http://images.google.com/images?q=%s&imgsz=medium|large", \&parse_googlei],
	lastfm => ['last.fm',"http://www.last.fm/music/%a/+images", \&parse_lastfm],
 },
 album =>
 {	googlei => ['google images',"http://images.google.com/images?q=%s&imgsz=medium|large", \&parse_googlei],
	slothradio => ['slothradio', "http://www.slothradio.com/covers/?artist=%a&album=%l", \&parse_sloth],
	#itunesgrabber => ['itunesgrabber',"http://www.thejosher.net/iTunes/index.php?artist=%a&album=%l", \&parse_itunesgrabber],
	freecovers => ['freecovers.net', "http://www.freecovers.net/api/search/%s", \&parse_freecovers], #could add /Music+CD but then we'd lose /Soundtrack
	#rateyourmusic=> ['rateyourmusic.com', "http://rateyourmusic.com/search?searchterm=%s&searchtype=l",\&parse_rateyourmusic], # urls results in "403 Forbidden"
 },
);

my %menuentry=
(	label => _"Search for a picture on internet",	#label of the menu entry
	code => sub {Fetch($_[0]{ID},'album')},		#when menu entry selected
	test => sub {$_[0]{col}==::SONG_ALBUM},		#the menu entry is displayed if returns true
);
my %menuentry2=
(	label => _"Search for a picture on internet",
	code => sub {Fetch($_[0]{ID},'artist',$_[0]{key})},
	test => sub {$_[0]{col}==::SONG_ARTIST},
);
my %fpaneentry=
(	label=> _"Search for a picture on internet",
	code => sub { my $aa=$_[0]{aa}; my $key=$_[0]{keylist}[0]; my $ID=($aa eq 'artist' ? \%::Artist : \%::Album)->{$key}[::AALIST][0]; $key=undef if $aa eq 'album'; Fetch($ID,$aa,$key) if defined $ID; },
	onlyone=> 'keylist',	#menu entry is hidden if more than one album/artist is selected
	istrue => 'aa',		#menu entry is hidden for non artist/album (aa) FPanes
);

::SetDefaultOptions(OPT, USEFILE => 1, COVERFILE => 'cover', PictureSite_artist => 'googlei', PictureSite_album => 'googlei');

sub Start
{	push @::cMenuAA,\%menuentry,\%menuentry2;
	push @FilterPane::cMenu, \%fpaneentry;
}
sub Stop
{	@::cMenuAA=  grep $_!=\%menuentry && $_!=\%menuentry2, @::SongCMenu;
	@FilterPane::cMenu= grep $_!=\%fpaneentry, @FilterPane::cMenu;
}

sub prefbox
{	my $check1=::NewPrefCheckButton(OPT.'ASK',_"Ask confirmation only if file already exists");
	my $check2=::NewPrefCheckButton(OPT.'UNIQUE',_"Find a unique filename if file already exists");
	my $entry1=::NewPrefEntry(OPT.'COVERPATH','');
	my $entry2=::NewPrefEntry(OPT.'COVERFILE','');
	my ($radio1a,$radio1b)=::NewPrefRadio(OPT.'USEPATH',undef,_"use song folder",0,_"use :",1);
	my ($radio2a,$radio2b)=::NewPrefRadio(OPT.'USEFILE',undef,_"use album name",0,_"use :",1);
	my $frame1=Gtk2::Frame->new(_"default folder");
	my $frame2=Gtk2::Frame->new(_"default filename");
	my $vbox1=::Vpack( $radio1a,[$radio1b,$entry1] );
	my $vbox2=::Vpack( $radio2a,[$radio2b,$entry2] );
	$frame1->add($vbox1);
	$frame2->add($vbox2);
	return ::Vpack( $frame1,$frame2,$check1,$check2 );
}

sub Fetch
{	my ($ID,$aa,$artist)=@_;
	my $self=bless Gtk2::Window->new;
	$self->set_border_width(4);
	my $Bsearch=::NewIconButton('gtk-find',_"Search");
	my $Bcur=Gtk2::Button->new($aa eq 'artist' ? _"Search for current artist" : _"Search for current album");
	::set_drag($Bcur, dest =>	[::DRAG_ID, sub { $_[0]->get_toplevel->SearchID($_[2]); }], );
	my $Bclose=Gtk2::Button->new_from_stock('gtk-close');
	my @entry;
	push @entry, $self->{"searchentry_$_"}=Gtk2::Entry->new for qw/s a l/;
	$::Tooltips->set_tip($self->{searchentry_s},_"Keywords");
	$::Tooltips->set_tip($self->{searchentry_a},_"Artist");
	$::Tooltips->set_tip($self->{searchentry_l},_"Album");
	my $source=::NewPrefCombo( OPT.'PictureSite_'.$aa, {map {$_=>$Sites{$aa}{$_}[0]} keys %{$Sites{$aa}}} , undef, \&combo_changed_cb);
	#$self->{Bnext}=	my $Bnext=::NewIconButton('gtk-go-forward',"More");
	$self->{Bnext}=		my $Bnext=Gtk2::Button->new(_"More results");
	$self->{Bstop}=		my $Bstop=Gtk2::Button->new_from_stock('gtk-stop');
	$self->{progress}=	my $pbar =Gtk2::ProgressBar->new;
	$self->{table}=		my $table=Gtk2::Table->new(RES_LINES,RES_PER_LINE,::TRUE);
	$self->add( ::Vpack
			(	[map( {('_',$_)} @entry), $Bsearch, $Bstop, $source],
				'_',$table,
				'-', ['_',$pbar , '-', $Bclose,$Bnext,$Bcur]
			) );
	for (@entry)
	{	$_->signal_connect(  activate => \&NewSearch );
		$_->show_all;
		$_->set_no_show_all(1);
	}
	$self->show_all;
	$Bsearch->signal_connect( clicked => \&NewSearch );
	$Bstop->signal_connect( clicked => sub {$_[0]->get_toplevel->stop });
	$Bclose->signal_connect(clicked => sub {$_[0]->get_toplevel->destroy});
	$Bnext->signal_connect( clicked => sub {$_[0]->get_toplevel->NextPage});
	$Bcur->signal_connect(clicked =>sub {$_[0]->get_toplevel->SearchID($::SongID)});
	$self->signal_connect( destroy => \&abort);
	$self->signal_connect( unrealize => sub {$::Options{OPT.'winsize'}=join ' ',$_[0]->get_size; });

	my $size= $::Options{OPT.'winsize'} || RES_PER_LINE*PREVIEW_SIZE.' '.RES_LINES*PREVIEW_SIZE;
	$self->resize(split ' ',$size,2);

	$self->{aa}=$aa;
	$self->{site}=$::Options{OPT.'PictureSite_'.$aa};
	$self->SearchID($ID,$aa,$artist);
	$self->UpdateSite;
}

sub combo_changed_cb
{	my $self=$_[0]->get_toplevel;
	$self->{site}=$::Options{OPT.'PictureSite_'.$self->{aa}};
	$self->UpdateSite;
	$self->NewSearch;
}
sub UpdateSite
{	my $self=$_[0];
	my $url=$Sites{$self->{aa}}{$self->{site}}[1];
	for my $l (qw/s a l/)
	{	my $entry=$self->{"searchentry_$l"};
		if ($url=~m/\%$l/)	{$entry->show}
		else			{$entry->hide}
	}
}

sub SearchID
{	my ($self,$ID,$aa,$artist)=@_;
	$self=::find_ancestor($_[0],__PACKAGE__);

	unless (defined $artist)
	{	$artist=$::Songs[$ID][::SONG_ARTIST];
		$artist= (split /$::re_artist/o,$artist)[0];
	}
	$artist='' if $artist eq '<Unknown>';
	$self->{dir}=$::Songs[$ID][::SONG_PATH];
	my $search='';
	my $album='';
	$aa||=$self->{aa}||'album';
	if ($aa eq 'artist')
	{	$self->{artist}=$artist;
		delete $self->{album};
		$search="\"$artist\"" unless $artist eq '';
	}
	else
	{	$album=$::Songs[$ID][::SONG_ALBUM];
		$album='' if $album=~m/^<Unknown>/;
		$self->{album}=$album;
		delete $self->{artist};
		$search="\"$album\"" if $album ne '';
		$search.=" \"$artist\"" unless $search eq '' || $artist eq '';
	}
	$self->set_title(_("Searching for a picture of : ").(defined $self->{album} ? $self->{album} : $self->{artist}));
	$self->{searchentry_s}->set_text($search);
	$self->{searchentry_a}->set_text($artist);
	$self->{searchentry_l}->set_text($album);
	$self->NewSearch;
}

sub NewSearch
{	my $self=::find_ancestor($_[0],__PACKAGE__);
	my $url=$Sites{$self->{aa}}{$self->{site}}[1];
	my %letter;
	for my $l (qw/s a l/)
	{	next unless $url=~m/\%$l/;
		my $s= $self->{"searchentry_$l"}->get_text;
		$s=~s/^\s+//; $s=~s/\s+$//;
	       	return if $s eq '';
		$letter{$l}=::url_escapeall($s);
	}
	$self->abort;
	$self->{results}=[];
	$self->{page}=0;
	$self->InitPage;
	$url=~s/%([sal])/$letter{$1}/g;
	warn "fetchcover : loading $url\n" if $::debug;
	$self->{waiting}=Simple_http::get_with_cb
	 (	cb => sub {$self->searchresults_cb(@_)},
		url => $url,	cache=>1
	 );
}

sub InitPage
{	my $self=$_[0];
	$self->abort;
	$self->{loaded}=0;
	$self->{Bnext}->set_sensitive(0);
	$self->{Bstop}->set_sensitive(1);
	$self->{progress}->set_fraction(0);
	$self->{progress}->show;
	my $table=$self->{table};
	$table->remove($_) for $table->get_children;
}

sub PrevPage
{	my $self=$_[0];
	return unless $self->{page};
	$self->{page}--;
	$self->InitPage;
	::IdleDo('8_FetchCovers'.$self,100,\&get_next,$self);
}

sub NextPage
{	my $self=$_[0];
	$self->{page}++;
	$self->InitPage;
	::IdleDo('8_FetchCovers'.$self,100,\&get_next,$self);
}

sub parse_rateyourmusic
{	my $result=$_[0];
	my @list;
	while ($result=~m#a title="\[Album(\d+)\]"#g)
	{	push @list,
		{	url=> "http://static.rateyourmusic.com/album_images/$1.jpg",
			previewurl => "http://static.rateyourmusic.com/album_images/s$1.jpg",
		};
	}
	return \@list;
}
sub parse_freecovers #FIXME could use a XML module	#can provide backcover and more too
{	my $result=$_[0];
	my @list;
	while ($result=~m#<title>(.+?)</title>#gs)
	{	my $res=$1;
		my %res;
		$res{desc}= ::decode_html(Encode::decode('cp1252',$1)) if $res=~m#<name>([^<]+)</name>#; #FIXME not sure of the encoding
		while ($res=~m#<cover>(.+?)</cover>#gs)
		{	my $res=$1;
			next unless $res=~m#<type>front</type>#;
			$res{url}=$1 if $res=~m#<preview>([^<]+)</preview>#;
			$res{previewurl}=$1 if $res=~m#<thumbnail>([^<]+)</thumbnail>#;
			last;
		}
		push @list, \%res if $res{url};
	}
	return \@list;
}
sub parse_lastfm
{	my $result=$_[0];
	my @list;
	while ($result=~m#<a href="/music/[^/]+/\+images/\d+"[^>]+?class="pic".+?<img [^>]+?src="([^"]+)"#gs)
	{	my $url=my $pre=$1;
		$url=~s#/\w+/(\d+.jpg)$#/_/$1#; ### /126b/123456.jpg -> /_/123456.jpg
		push @list, {url => $url, previewurl =>$pre,};
	}
	my $nexturl;
	$nexturl='http://www.lastfm.com'.$1 if $result=~m#<a href="([^"]+)" class="nextlink">#;
	return \@list,$nexturl;
}
sub parse_itunesgrabber
{	my $result=$_[0];
	my @list;
	push @list,{url=>$1} if $result=~m#Album art found.*?<a href="([^"]+)"#;
	return \@list;
}
sub parse_sloth
{	my $result=$_[0];
	my @list;
	while ($result=~m#<div class="album\d+"><img src="([^"]+)"#g)
	{	push @list, {url => $1};
	}
	my $nexturl;
	$nexturl='http://www.slothradio.com/covers/'.$1 if $result=~m#<div class="pages">[^<>]*(?:\s*<a href="[^"]+">\d+</a>)*\s*\d\s+<a href="([^"]+)">\d+</a>#;
	return \@list,$nexturl;
}
sub parse_googlei
{	my $result=$_[0];
	my @list;
	if ($result=~m#dyn.setResults\([^)](.*)\)#)	#parse google image results #assumes no unencoded ')' in the array of results
	{	my @matches=split /\],\["/,$1;	#not very reliable
		for my $m (@matches)
		{	my @fields=split /["\]],["\[]/,$m;
			my $url=$fields[3];
			my $desc=$fields[6]; $desc=~s#\\x([0-9a-f]{2})#chr(hex $1)#gie; $desc=~s#</?b>##g;
			$desc=Encode::decode('cp1252',$desc); #FIXME not sure of the encoding
			$desc=::decode_html($desc);
			my $preview='http://images.google.com/images?q=tbn:'.$fields[2].$url;
			push @list, {url => $url, previewurl =>$preview, desc => $desc };
		}
	}
	my $nexturl;
	if ($result=~m#<a href="(/images\?[^>"]*)"><img src="nav_next#)
	{	$nexturl='http://images.google.com'.$1;
		$nexturl=~s#&amp;#&#g;
	}
	return \@list,$nexturl;
}

sub searchresults_cb
{	my ($self,$result)=@_;
	$self->{waiting}=undef;
	unless (defined $result) { stop($self,_"connection failed."); return; }
	my $parse= $Sites{$self->{aa}}{$self->{site}}[2];
	my ($list,$nexturl)=$parse->($result);
	$self->{nexturl}=$nexturl;
	#$table->set_size_request(110*5,110*int(1+@list/5));
	push @{$self->{results}}, @$list;
	my $more= @{$self->{results}} - ($self->{page}+1) * RES_PER_PAGE;
	$self->{Bnext}->set_sensitive( $more>0 || $nexturl );
	unless (@{$self->{results}}) { stop($self,_"no matches found, you might want to remove some search terms."); return; }
	::IdleDo('8_FetchCovers'.$self,100,\&get_next,$self);
}

sub abort
{	my $self=$_[0];
	my $results=$self->{results};
	for my $r ($self,@$results)
	{	delete $r->{done};
		$r->{waiting}->abort if $r->{waiting};
		delete $r->{waiting};
	}
	delete $self->{waiting};
	delete $::ToDo{'8_FetchCovers'.$self};
}

sub stop
{	my ($self,$error)=@_;
	$self->abort;
	$self->{Bstop}->set_sensitive(0);
	#$self->{progress}->set_fraction(1);
	$self->{progress}->hide;
	if ($error)
	{	my $l=Gtk2::Label->new($error);
		$l->show;
		$self->{table}->attach($l,0,5,0,1,'fill','fill',1,1);
	}
}

sub get_next
{	my $self=shift;
	my $results=$self->{results};
	my $res_id;
	my $waiting;
	my $start= $self->{page} * RES_PER_PAGE;
	my $end= $start + RES_PER_PAGE -1;
	if ($#$results<$end && $self->{nexturl})
	{	#load next page
		$self->{waiting}=Simple_http::get_with_cb(cb => sub {$self->searchresults_cb(@_)}, url => delete $self->{nexturl}, cache=>1);
	}
	$end=$#$results if $#$results<$end;
	for my $id ($start .. $end)
	{	#warn "$id : waiting=".$results->[$id]{waiting}." done=".$results->[$id]{done};
		if ($results->[$id]{waiting}) {$waiting++; next};
		next if $results->[$id]{done};
		$res_id=$id;
		last;
	}
	unless (defined $res_id || $waiting || $self->{waiting})
	{	$self->stop;
		return;
	}
	return unless defined $res_id;
	return if $waiting && $waiting > 3; #no more than 4 pictures at once

	my $result=$self->{results}[$res_id];
	$result->{waiting}=Simple_http::get_with_cb(url => $result->{url}, cache=>1, cb =>
	sub
	{	my $pixdata=$_[0];
		$result->{waiting}=undef;
		my $loader;
		$loader=::LoadPixData($pixdata,PREVIEW_SIZE) if $pixdata;
		if ($loader)
		{	my $dim=$loader->{w}.' x '.$loader->{h};
			my $table=$self->{table};
			my $pixbuf=$loader->get_pixbuf;
			my $image=Gtk2::Image->new_from_pixbuf($pixbuf);
			my $button=Gtk2::Button->new;
			$button->{pixdata}=$pixdata;
			$button->{ext}=	($Gtk2::VERSION >= 1.092)?
					  $loader->get_format->{extensions}[0]
					: ( EntryCover::_identify_pictype($pixdata) )[0];
			$button->{ext}='jpg' if $button->{ext} eq 'jpeg';
			my $vbox=Gtk2::VBox->new(0,0);
			my $label=Gtk2::Label->new($dim);
			$vbox->add($image);
			$vbox->pack_end($label,0,0,0);
			$button->add($vbox);

			my $tip='';
			$tip=$result->{desc}."\n" if $result->{desc};
			$tip.=$dim."\n".$result->{url};
			$::Tooltips->set_tip($button,$tip);
			$button->signal_connect(clicked => \&set_cover);
			$button->signal_connect(button_press_event => \&::pixbox_button_press_cb,3); # 3 : mouse button 3
			$button->set_relief('none');
			$button->show_all;
			my $i= $res_id % RES_PER_PAGE;
			my $y=int( $i/RES_PER_LINE);
			my $x= $i % RES_PER_LINE;
			$table->attach($button,$x,$x+1,$y,$y+1,'fill','fill',1,1);
			$result->{done}=1;
			$self->{loaded}++;
		}
		elsif ($result->{previewurl})
		{	$result->{originurl}=$result->{url};
			$result->{url}=delete $result->{previewurl};
		}
		else { $result->{done}='error'; $self->{loaded}++; }
		$self->{progress}->set_fraction( $self->{loaded} / ( @{$self->{results}} - RES_PER_PAGE*$self->{page} ));
		::IdleDo('8_FetchCovers'.$self,100,\&get_next,$self);
	});
	::IdleDo('8_FetchCovers'.$self,1000,\&get_next,$self);
}

sub set_cover
{	my $button=$_[0];
	my $self=::find_ancestor($button,__PACKAGE__);
	my ($aa,$text,$href);
	if (defined $self->{album})
	{	$aa=$self->{album};
		$text=::__x(_"Use this picture as cover for album '{album}'", album => $aa);
		$href=$::Album{$aa};
	}
	else
	{	$aa=$self->{artist};
		$text=::__x(_"Use this picture for artist '{artist}'", artist => $aa);
		$href=$::Artist{$aa};
	}
	my $check=Gtk2::CheckButton->new( $text );
	$check->set_active(1);
	my $default_file=	$::Options{OPT.'USEFILE'} ?
			$::Options{OPT.'COVERFILE'} : $aa;
	$default_file=~s/\.(?:jpe?g|png|gif)$//;
	$default_file.='.'.$button->{ext};
	$default_file=~s/$::ILLEGALCHAR//g;
	$default_file=::filename_from_unicode($default_file);
	my $default_dir=$::Options{OPT.'COVERPATH'} || '';
	$default_dir=::filename_from_unicode($default_dir);
	$default_dir=~s/$::ILLEGALCHARDIR//g;
	$default_dir=$self->{dir} unless $::Options{OPT.'USEPATH'} && -d $default_dir;
	if ($::Options{OPT.'UNIQUE'})
	{	while (-e $default_dir.::SLASH.$default_file) #find a unique name
		{ last unless $default_file=~s/(?:_(\d+))?\.(\w+)$/'_'.($1? $1+1 : 1).".$2"/e ; }
	}
	my $file=$default_dir.::SLASH.$default_file;
	if (!$::Options{OPT.'ASK'} || -e $file)
	{$file=::ChooseSaveFile($self,_"Save picture as",
		$default_dir,$default_file,
		$check);
	}
	return unless $file;
	my $fh;
	until (open $fh,'>',$file)
	{	my $retry=::Retry_Dialog( ::__x( _"Error writing '{file}'", file => ::filename_to_utf8displayname($file) )." :\n$!." ,$self);
		return unless $retry eq 'yes';
	}
	print $fh $button->{pixdata};
	close $fh;
	return unless $check->get_active;
	$href->[::AAPIXLIST]=$file;
	::HasChanged('AAPicture',$aa);
}

1

__END__
xml.amazon.com/onca/xml3?t=webservices-20&dev-t=%l&KeywordSearch=%s&mode=music&type=heavy&locale=us&page=1&f=xml
