1 // (C) 2004 Mark Kretschmann <markey@web.de>
2 // (C) 2004 Stefan Bogner <bochi@online.ms>
4 // See COPYING file for licensing information.
6 #include "coverfetcher.h"
9 #include "amarokconfig.h"
10 #include "querybuilder.h"
11 #include "covermanager.h"
13 #include "statusbar.h"
15 #include <KApplication>
17 #include <KCursor> //waiting cursor
20 #include <KIconLoader>
21 #include <KFileDialog>
25 #include <KMessageBox>
27 #include <KPushButton>
30 #include <KWindowSystem>
32 #include <QDomDocument>
33 #include <QDomElement>
35 #include <q3popupmenu.h>
42 Amarok::coverContextMenu( QWidget
*parent
, QPoint point
, const QString
&artist
, const QString
&album
, bool showCoverManager
)
45 enum { ACTIONS_SHOW
, ACTIONS_FETCH
,
46 ACTIONS_CUSTOM
, ACTIONS_DELETE
, ACTIONS_MANAGER
};
48 menu
.setTitle( i18n( "Cover Image" ) );
50 menu
.insertItem( KIcon( Amarok::icon( "zoom" ) ), i18n( "&Show Fullsize" ), ACTIONS_SHOW
);
51 menu
.insertItem( KIcon( Amarok::icon( "download" ) ), i18n( "&Fetch From amazon.%1", CoverManager::amazonTld() ), ACTIONS_FETCH
);
52 menu
.insertItem( KIcon( Amarok::icon( "files" ) ), i18n( "Set &Custom Cover" ), ACTIONS_CUSTOM
);
53 bool disable
= !album
.isEmpty(); // disable setting covers for unknown albums
54 menu
.setItemEnabled( ACTIONS_FETCH
, disable
);
55 menu
.setItemEnabled( ACTIONS_CUSTOM
, disable
);
58 menu
.insertItem( KIcon( Amarok::icon( "remove" ) ), i18n( "&Unset Cover" ), ACTIONS_DELETE
);
59 if ( showCoverManager
) {
61 menu
.insertItem( KIcon( Amarok::icon( "covermanager" ) ), i18n( "Cover &Manager" ), ACTIONS_MANAGER
);
64 disable
= !CollectionDB::instance()->albumImage( artist
, album
, 0 ).contains( "nocover" );
65 menu
.setItemEnabled( ACTIONS_SHOW
, disable
);
66 menu
.setItemEnabled( ACTIONS_DELETE
, disable
);
68 switch( menu
.exec( point
) )
71 CoverManager::viewCover( artist
, album
, parent
);
76 const int button
= KMessageBox::warningContinueCancel( parent
,
77 i18nc( "[only-singular]", "Are you sure you want to remove this cover from the Collection?" ),
79 KStandardGuiItem::del() );
81 if ( button
== KMessageBox::Continue
)
82 CollectionDB::instance()->removeAlbumImage( artist
, album
);
87 CollectionDB::instance()->fetchCover( parent
, artist
, album
, false );
92 QString artist_id
; artist_id
.setNum( CollectionDB::instance()->artistID( artist
) );
93 QString album_id
; album_id
.setNum( CollectionDB::instance()->albumID( album
) );
94 QStringList values
= CollectionDB::instance()->albumTracks( artist_id
, album_id
);
95 QString startPath
= ":homedir";
97 if ( !values
.isEmpty() ) {
99 url
.setPath( values
.first() );
100 startPath
= url
.directory();
103 KUrl file
= KFileDialog::getImageOpenUrl( startPath
, parent
, i18n("Select Cover Image File") );
104 if ( !file
.isEmpty() )
105 CollectionDB::instance()->setAlbumImage( artist
, album
, file
);
109 case ACTIONS_MANAGER
:
110 CoverManager::showOnce( album
);
117 CoverLabel::CoverLabel ( QWidget
* parent
, Qt::WindowFlags f
)
122 void CoverLabel::mouseReleaseEvent(QMouseEvent
*pEvent
) {
123 if (pEvent
->button() == Qt::LeftButton
|| pEvent
->button() == Qt::RightButton
)
125 Amarok::coverContextMenu( this, pEvent
->globalPos(), m_artist
, m_album
, false );
130 CoverFetcher::CoverFetcher( QWidget
*parent
, const QString
&artist
, QString album
)
139 setObjectName( "CoverFetcher" );
141 QStringList extensions
;
142 extensions
<< i18n("disc") << i18n("disk") << i18n("remaster") << i18n("cd") << i18n("single") << i18n("soundtrack") << i18n("part")
143 << "disc" << "disk" << "remaster" << "cd" << "single" << "soundtrack" << "part" << "cds" /*cd single*/;
145 //we do several queries, one raw ie, without the following modifications
146 //the others have the above strings removed with the following regex, as this can increase hit-rate
147 const QString template1
= " ?-? ?[(^{]* ?%1 ?\\d*[)^}\\]]* *$"; //eg album - [disk 1] -> album
148 oldForeach( extensions
) {
149 QRegExp
regexp( template1
.arg( *it
) );
150 regexp
.setCaseSensitivity( Qt::CaseInsensitive
);
151 album
.remove( regexp
);
154 //TODO try queries that remove anything in album after a " - " eg Les Mis. - Excerpts
157 * We search for artist - album, and just album, using the exact album text and the
158 * manipulated album text.
161 //search on our modified term, then the original
162 if ( !m_artist
.isEmpty() )
163 m_userQuery
= m_artist
+ " - ";
164 m_userQuery
+= m_album
;
166 m_queries
+= m_artist
+ " - " + album
;
167 m_queries
+= m_userQuery
;
169 m_queries
+= m_album
;
171 //don't do the same searches twice in a row
172 if( m_album
== album
) {
173 m_queries
.pop_front();
174 m_queries
.pop_back();
178 * Finally we do a search for just the artist, just in case as this often
179 * turns up a cover, and it might just be the right one! Also it would be
180 * the only valid search if m_album.isEmpty()
182 m_queries
+= m_artist
;
184 QApplication::setOverrideCursor( Qt::BusyCursor
);
187 CoverFetcher::~CoverFetcher()
191 QApplication::restoreOverrideCursor();
195 CoverFetcher::startFetch()
199 // Static license Key. Thanks muesli ;-)
200 const QString
LICENSE( "D1URM11J3F2CEH" );
203 m_coverAmazonUrls
.clear();
204 m_coverAsins
.clear();
206 m_coverNames
.clear();
210 if ( m_queries
.isEmpty() ) {
211 debug() << "m_queries is empty";
212 finishWithError( i18n("No cover found") );
215 QString query
= m_queries
.front();
216 m_queries
.pop_front();
218 // '&' breaks searching
221 // Bug 97901: Import cover from amazon france doesn't work properly
222 // (we have to set "mode=music-fr" instead of "mode=music")
223 QString musicMode
= "music";
224 //Amazon Japan isn't on xml.amazon.com
226 int mibenum
= 4; // latin1
227 if( AmarokConfig::amazonLocale() == "jp" ) {
228 musicMode
= "music-jp";
230 mibenum
= 106; // utf-8
232 else if( AmarokConfig::amazonLocale() == "ca" )
233 musicMode
= "music-ca";
234 else if( AmarokConfig::amazonLocale() == "fr" )
235 musicMode
= "music-fr";
238 // changed to type=lite because it makes less traffic
239 url
= "http://xml.amazon." + tld
240 + "/onca/xml3?t=webservices-20&dev-t=" + LICENSE
241 + "&KeywordSearch=" + KUrl::toPercentEncoding( query
, "/" ) // FIXME: we will have to find something else
242 + "&mode=" + musicMode
243 + "&type=lite&locale=" + AmarokConfig::amazonLocale()
247 KJob
* job
= KIO::storedGet( url
, false, false );
248 connect( job
, SIGNAL(result( KJob
* )), SLOT(finishedXmlFetch( KJob
* )) );
250 Amarok::StatusBar::instance()->newProgressOperation( job
);
254 //////////////////////////////////////////////////////////////////////////////////////////
256 //////////////////////////////////////////////////////////////////////////////////////////
259 CoverFetcher::finishedXmlFetch( KJob
*job
) //SLOT
263 // NOTE: job can become 0 when this method is called from attemptAnotherFetch()
265 if( job
&& job
->error() ) {
266 finishWithError( i18n("There was an error communicating with Amazon."), job
);
270 KIO::StoredTransferJob
* const storedJob
= static_cast<KIO::StoredTransferJob
*>( job
);
271 m_xml
= QString::fromUtf8( storedJob
->data().data(), storedJob
->data().size() );
275 if( !doc
.setContent( m_xml
) ) {
276 m_errors
+= i18n("The XML obtained from Amazon is invalid.");
281 const QDomNode details
= doc
.documentElement().namedItem( "Details" );
283 // the url for the Amazon product info page
284 m_amazonURL
= details
.attributes().namedItem( "url" ).toAttr().value();
285 QDomNode it
= details
.firstChild();
286 while ( !it
.isNull() ) {
287 if ( it
.isElement() ) {
288 QDomElement e
= it
.toElement();
289 if(e
.tagName()=="Asin")
291 m_asin
= e
.firstChild().toText().data();
292 debug() << "setting the ASIN as" << m_asin
;
296 it
= it
.nextSibling();
299 QString size
= "ImageUrl";
301 case 0: size
+= "Small"; break;
302 case 1: size
+= "Medium"; break;
303 default: size
+= "Large"; break;
306 debug() << "Fetching size: " << size
;
308 m_coverAsins
.clear();
309 m_coverAmazonUrls
.clear();
311 m_coverNames
.clear();
312 for( QDomNode node
= details
; !node
.isNull(); node
= node
.nextSibling() ) {
313 QString amazonUrl
= node
.attributes().namedItem( "url" ).toAttr().value();
314 QString coverUrl
= node
.namedItem( size
).firstChild().toText().nodeValue();
315 QString asin
= node
.namedItem( "Asin" ).firstChild().toText().nodeValue();
316 QString name
= node
.namedItem( "ProductName" ).firstChild().toText().nodeValue();
318 const QDomNode artists
= node
.namedItem("Artists");
319 // in most cases, Amazon only sends one Artist in Artists
321 if (!artists
.isNull())
322 artist
= artists
.namedItem( "Artist" ).firstChild().toText().nodeValue();
324 debug() << "name:" << name
<< " artist:" << artist
<< " url:" << coverUrl
;
326 if( !coverUrl
.isEmpty() )
328 m_coverAmazonUrls
+= amazonUrl
;
329 m_coverAsins
+= asin
;
330 m_coverUrls
+= coverUrl
;
331 m_coverNames
+= artist
+ " - " + name
;
335 attemptAnotherFetch();
340 CoverFetcher::finishedImageFetch( KJob
*job
) //SLOT
343 debug() << "finishedImageFetch(): KIO::error(): " << job
->error();
345 m_errors
+= i18n("The cover could not be retrieved.");
347 attemptAnotherFetch();
351 m_image
.loadFromData( static_cast<KIO::StoredTransferJob
*>( job
)->data() );
353 if( m_image
.width() <= 1 ) {
354 //Amazon seems to offer images of size 1x1 sometimes
355 //Amazon has nothing to offer us for the requested image size
356 m_errors
+= i18n("The cover-data produced an invalid image.");
357 attemptAnotherFetch();
360 else if( m_userCanEditQuery
)
361 //yay! image found :)
362 //lets see if the user wants it
366 //image loaded successfully yay!
372 CoverFetcher::attemptAnotherFetch()
376 if( !m_coverUrls
.isEmpty() ) {
377 // Amazon suggested some more cover URLs to try before we
378 // try a different query
379 KJob
* job
= KIO::storedGet( KUrl(m_coverUrls
.front()), false, false );
380 connect( job
, SIGNAL(result( KJob
* )), SLOT(finishedImageFetch( KJob
* )) );
382 Amarok::StatusBar::instance()->newProgressOperation( job
);
384 m_coverUrls
.pop_front();
386 m_currentCoverName
= m_coverNames
.front();
387 m_coverNames
.pop_front();
389 m_amazonURL
= m_coverAmazonUrls
.front();
390 m_coverAmazonUrls
.pop_front();
392 m_asin
= m_coverAsins
.front();
393 m_coverAsins
.pop_front();
396 else if( !m_xml
.isEmpty() && m_size
> 0 ) {
397 // we need to try smaller sizes, this often is
398 // fruitless, but does work out sometimes.
401 finishedXmlFetch( 0 );
404 else if( !m_queries
.isEmpty() ) {
405 // we have some queries left in the pot
409 else if( m_userCanEditQuery
) {
410 // we have exhausted all the predetermined queries
411 // so lets let the user give it a try
412 getUserQuery( i18n("You have seen all the covers Amazon returned using the query below. Perhaps you can refine it:") );
413 m_coverAmazonUrls
.clear();
414 m_coverAsins
.clear();
416 m_coverNames
.clear();
419 finishWithError( i18n("No cover found") );
423 // Moved outside the only function that uses it because
424 // gcc 2.95 doesn't like class declarations there.
425 class EditSearchDialog
: public KDialog
428 EditSearchDialog( QWidget
* parent
, const QString
&text
, const QString
&keyword
, CoverFetcher
*fetcher
)
431 setCaption( i18n( "Amazon Query Editor" ) );
434 KComboBox
* amazonLocale
= new KComboBox( this );
435 amazonLocale
->addItem( i18n("International"), CoverFetcher::International
);
436 amazonLocale
->addItem( i18n("Canada"), CoverFetcher::Canada
);
437 amazonLocale
->addItem( i18n("France"), CoverFetcher::France
);
438 amazonLocale
->addItem( i18n("Germany"), CoverFetcher::Germany
);
439 amazonLocale
->addItem( i18n("Japan"), CoverFetcher::Japan
);
440 amazonLocale
->addItem( i18n("United Kingdom"), CoverFetcher::UK
);
441 if( CoverManager::instance() )
442 connect( amazonLocale
, SIGNAL( activated(int) ),
443 CoverManager::instance(), SLOT( changeLocale(int) ) );
445 connect( amazonLocale
, SIGNAL( activated(int) ),
446 fetcher
, SLOT( changeLocale(int) ) );
447 QHBoxLayout
*hbox1
= new QHBoxLayout();
448 hbox1
->setSpacing( 8 );
449 hbox1
->addWidget( new QLabel( i18n( "Amazon Locale: " ), this ) );
450 hbox1
->addWidget( amazonLocale
);
452 int currentLocale
= CoverFetcher::localeStringToID( AmarokConfig::amazonLocale() );
453 amazonLocale
->setCurrentIndex( currentLocale
);
455 KPushButton
* cancelButton
= new KPushButton( KStandardGuiItem::cancel(), this );
456 KPushButton
* searchButton
= new KPushButton( i18n("&Search"), this );
458 QHBoxLayout
*hbox2
= new QHBoxLayout();
459 hbox2
->setSpacing( 8 );
460 hbox2
->addItem( new QSpacerItem( 160, 8, QSizePolicy::Expanding
, QSizePolicy::Minimum
) );
461 hbox2
->addWidget( searchButton
);
462 hbox2
->addWidget( cancelButton
);
464 QVBoxLayout
*vbox
= new QVBoxLayout();
465 vbox
->setMargin( 8 );
466 vbox
->setSpacing( 8 );
467 vbox
->addLayout( hbox1
);
468 vbox
->addWidget( new QLabel( "<qt>" + text
, this ) );
469 vbox
->addWidget( new KLineEdit( keyword
, this ) );
470 vbox
->addLayout( hbox2
);
472 searchButton
->setDefault( true );
475 setFixedHeight( height() );
477 connect( searchButton
, SIGNAL(clicked()), SLOT(accept()) );
478 connect( cancelButton
, SIGNAL(clicked()), SLOT(reject()) );
481 QString
query() { return findChild
<KLineEdit
*>( "Query" )->text(); }
485 CoverFetcher::localeIDToString( int id
)//static
507 CoverFetcher::localeStringToID( const QString
&s
)
509 int id
= International
;
510 if( s
== "fr" ) id
= France
;
511 else if( s
== "de" ) id
= Germany
;
512 else if( s
== "jp" ) id
= Japan
;
513 else if( s
== "uk" ) id
= UK
;
514 else if( s
== "ca" ) id
= Canada
;
520 CoverFetcher::changeLocale( int id
)//SLOT
522 QString locale
= localeIDToString( id
);
523 AmarokConfig::setAmazonLocale( locale
);
528 CoverFetcher::getUserQuery( QString explanation
)
530 if( explanation
.isEmpty() )
531 explanation
= i18n("Ask Amazon for covers using this query:");
533 EditSearchDialog
dialog(
534 static_cast<QWidget
*>( parent() ),
539 switch( dialog
.exec() )
541 case QDialog::Accepted
:
542 m_userQuery
= dialog
.query();
544 m_queries
<< m_userQuery
;
548 finishWithError( i18n( "Aborted." ) );
553 class CoverFoundDialog
: public KDialog
556 CoverFoundDialog( QWidget
*parent
, const QImage
&cover
, const QString
&productname
)
560 showButtonSeparator( false );
561 // Gives the window a small title bar, and skips a taskbar entry
562 //KWindowSystem::setType( winId(), NET::Utility );
563 //KWindowSystem::setState( winId(), NET::SkipTaskbar );
564 KVBox
*box
= new KVBox( this );
567 QLabel
*labelPix
= new QLabel( box
);
568 QLabel
*labelName
= new QLabel( box
);
569 KHBox
*buttons
= new KHBox( box
);
570 KPushButton
*save
= new KPushButton( KStandardGuiItem::save(), buttons
);
571 KPushButton
*newsearch
= new KPushButton( i18n( "Ne&w Search..." ), buttons
);
572 newsearch
->setObjectName( "NewSearch" );
573 KPushButton
*nextcover
= new KPushButton( i18n( "&Next Cover" ), buttons
);
574 nextcover
->setObjectName( "NextCover" );
575 KPushButton
*cancel
= new KPushButton( KStandardGuiItem::cancel(), buttons
);
577 labelPix
->setAlignment( Qt::AlignHCenter
);
578 labelName
->setAlignment( Qt::AlignHCenter
);
579 labelPix
->setPixmap( QPixmap::fromImage( cover
) );
580 labelName
->setText( productname
);
582 save
->setDefault( true );
583 this->setFixedSize( sizeHint() );
584 this->setCaption( i18n("Cover Found") );
586 connect( save
, SIGNAL(clicked()), SLOT(accept()) );
587 connect( newsearch
, SIGNAL(clicked()), SLOT(accept()) );
588 connect( nextcover
, SIGNAL(clicked()), SLOT(accept()) );
589 connect( cancel
, SIGNAL(clicked()), SLOT(reject()) );
592 virtual void accept()
594 if( qstrcmp( sender()->objectName().toAscii(), "NewSearch" ) == 0 )
596 else if( qstrcmp( sender()->objectName().toAscii(), "NextCover" ) == 0 )
605 CoverFetcher::showCover()
607 CoverFoundDialog
dialog( static_cast<QWidget
*>( parent() ), m_image
, m_currentCoverName
);
609 switch( dialog
.exec() )
611 case KDialog::Accepted
:
614 case 1000: //showQueryEditor()
616 m_coverAmazonUrls
.clear();
617 m_coverAsins
.clear();
619 m_coverNames
.clear();
621 case 1001: //nextCover()
622 attemptAnotherFetch();
625 finishWithError( i18n( "Aborted." ) );
632 CoverFetcher::finish()
640 CoverFetcher::finishWithError( const QString
&message
, KJob
*job
)
643 warning() << message
<< " KIO::error(): " << job
->errorText() << endl
;
653 #include "coverfetcher.moc"