add mp3 and ogg torrent url info to JamendoAlbum
[amarok.git] / src / coverfetcher.cpp
blob3f75500cac94cd7bc2e1283d13c15b7857163ac8
1 // (C) 2004 Mark Kretschmann <markey@web.de>
2 // (C) 2004 Stefan Bogner <bochi@online.ms>
3 // (C) 2004 Max Howell
4 // See COPYING file for licensing information.
6 #include "coverfetcher.h"
8 #include "amarok.h"
9 #include "amarokconfig.h"
10 #include "querybuilder.h"
11 #include "covermanager.h"
12 #include "debug.h"
13 #include "statusbar.h"
15 #include <KApplication>
16 #include <KComboBox>
17 #include <KCursor> //waiting cursor
18 #include <KDialog>
19 #include <KHBox>
20 #include <KIconLoader>
21 #include <KFileDialog>
22 #include <KIO/Job>
23 #include <KLineEdit>
24 #include <KLocale>
25 #include <KMessageBox>
26 #include <KMenu>
27 #include <KPushButton>
28 #include <KUrl>
29 #include <KVBox>
30 #include <KWindowSystem>
32 #include <QDomDocument>
33 #include <QDomElement>
34 #include <QDomNode>
35 #include <q3popupmenu.h>
36 #include <QLabel>
37 #include <QLayout>
38 #include <QRegExp>
41 void
42 Amarok::coverContextMenu( QWidget *parent, QPoint point, const QString &artist, const QString &album, bool showCoverManager )
44 Q3PopupMenu menu;
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 );
56 menu.addSeparator();
58 menu.insertItem( KIcon( Amarok::icon( "remove" ) ), i18n( "&Unset Cover" ), ACTIONS_DELETE );
59 if ( showCoverManager ) {
60 menu.addSeparator();
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 ) )
70 case ACTIONS_SHOW:
71 CoverManager::viewCover( artist, album, parent );
72 break;
74 case ACTIONS_DELETE:
76 const int button = KMessageBox::warningContinueCancel( parent,
77 i18nc( "[only-singular]", "Are you sure you want to remove this cover from the Collection?" ),
78 QString(),
79 KStandardGuiItem::del() );
81 if ( button == KMessageBox::Continue )
82 CollectionDB::instance()->removeAlbumImage( artist, album );
83 break;
86 case ACTIONS_FETCH:
87 CollectionDB::instance()->fetchCover( parent, artist, album, false );
88 break;
90 case ACTIONS_CUSTOM:
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() ) {
98 KUrl url;
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 );
106 break;
109 case ACTIONS_MANAGER:
110 CoverManager::showOnce( album );
111 break;
117 CoverLabel::CoverLabel ( QWidget * parent, Qt::WindowFlags f )
118 : QLabel( parent, 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 )
131 : QObject( parent )
132 , m_artist( artist )
133 , m_album( album )
134 , m_size( 2 )
135 , m_success( true )
137 DEBUG_FUNC_INFO
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;
168 m_queries += album;
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()
189 DEBUG_FUNC_INFO
191 QApplication::restoreOverrideCursor();
194 void
195 CoverFetcher::startFetch()
197 DEBUG_FUNC_INFO
199 // Static license Key. Thanks muesli ;-)
200 const QString LICENSE( "D1URM11J3F2CEH" );
202 // reset all values
203 m_coverAmazonUrls.clear();
204 m_coverAsins.clear();
205 m_coverUrls.clear();
206 m_coverNames.clear();
207 m_xml.clear();
208 m_size = 2;
210 if ( m_queries.isEmpty() ) {
211 debug() << "m_queries is empty";
212 finishWithError( i18n("No cover found") );
213 return;
215 QString query = m_queries.front();
216 m_queries.pop_front();
218 // '&' breaks searching
219 query.remove('&');
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
225 QString tld = "com";
226 int mibenum = 4; // latin1
227 if( AmarokConfig::amazonLocale() == "jp" ) {
228 musicMode = "music-jp";
229 tld = "co.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";
237 QString url;
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()
244 + "&page=1&f=xml";
245 debug() << url;
247 KJob* job = KIO::storedGet( url, KIO::NoReload, KIO::HideProgressInfo );
248 connect( job, SIGNAL(result( KJob* )), SLOT(finishedXmlFetch( KJob* )) );
250 Amarok::StatusBar::instance()->newProgressOperation( job );
254 //////////////////////////////////////////////////////////////////////////////////////////
255 // PRIVATE SLOTS
256 //////////////////////////////////////////////////////////////////////////////////////////
258 void
259 CoverFetcher::finishedXmlFetch( KJob *job ) //SLOT
261 DEBUG_BLOCK
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 );
267 return;
269 if ( job ) {
270 KIO::StoredTransferJob* const storedJob = static_cast<KIO::StoredTransferJob*>( job );
271 m_xml = QString::fromUtf8( storedJob->data().data(), storedJob->data().size() );
274 QDomDocument doc;
275 if( !doc.setContent( m_xml ) ) {
276 m_errors += i18n("The XML obtained from Amazon is invalid.");
277 startFetch();
278 return;
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;
293 break;
296 it = it.nextSibling();
299 QString size = "ImageUrl";
300 switch( m_size ) {
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();
310 m_coverUrls.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
320 QString artist = "";
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();
339 void
340 CoverFetcher::finishedImageFetch( KJob *job ) //SLOT
342 if( job->error() ) {
343 debug() << "finishedImageFetch(): KIO::error(): " << job->error();
345 m_errors += i18n("The cover could not be retrieved.");
347 attemptAnotherFetch();
348 return;
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
363 showCover();
365 else
366 //image loaded successfully yay!
367 finish();
371 void
372 CoverFetcher::attemptAnotherFetch()
374 DEBUG_BLOCK
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()), KIO::NoReload, KIO::HideProgressInfo );
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.
399 m_size--;
401 finishedXmlFetch( 0 );
404 else if( !m_queries.isEmpty() ) {
405 // we have some queries left in the pot
406 startFetch();
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();
415 m_coverUrls.clear();
416 m_coverNames.clear();
418 else
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
427 public:
428 EditSearchDialog( QWidget* parent, const QString &text, const QString &keyword, CoverFetcher *fetcher )
429 : KDialog( parent )
431 setCaption( i18n( "Amazon Query Editor" ) );
433 // amazon combo box
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) ) );
444 else
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 );
474 adjustSize();
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(); }
484 QString
485 CoverFetcher::localeIDToString( int id )//static
487 switch ( id )
489 case International:
490 return "us";
491 case Canada:
492 return "ca";
493 case France:
494 return "fr";
495 case Germany:
496 return "de";
497 case Japan:
498 return "jp";
499 case UK:
500 return "uk";
503 return "us";
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;
516 return id;
519 void
520 CoverFetcher::changeLocale( int id )//SLOT
522 QString locale = localeIDToString( id );
523 AmarokConfig::setAmazonLocale( locale );
527 void
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() ),
535 explanation,
536 m_userQuery,
537 this );
539 switch( dialog.exec() )
541 case QDialog::Accepted:
542 m_userQuery = dialog.query();
543 m_queries.clear();
544 m_queries << m_userQuery;
545 startFetch();
546 break;
547 default:
548 finishWithError( i18n( "Aborted." ) );
549 break;
553 class CoverFoundDialog : public KDialog
555 public:
556 CoverFoundDialog( QWidget *parent, const QImage &cover, const QString &productname )
557 : KDialog( parent )
559 setButtons( None );
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 );
565 setMainWidget(box);
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 )
595 done( 1000 );
596 else if( qstrcmp( sender()->objectName().toAscii(), "NextCover" ) == 0 )
597 done( 1001 );
598 else
599 KDialog::accept();
604 void
605 CoverFetcher::showCover()
607 CoverFoundDialog dialog( static_cast<QWidget*>( parent() ), m_image, m_currentCoverName );
609 switch( dialog.exec() )
611 case KDialog::Accepted:
612 finish();
613 break;
614 case 1000: //showQueryEditor()
615 getUserQuery();
616 m_coverAmazonUrls.clear();
617 m_coverAsins.clear();
618 m_coverUrls.clear();
619 m_coverNames.clear();
620 break;
621 case 1001: //nextCover()
622 attemptAnotherFetch();
623 break;
624 default:
625 finishWithError( i18n( "Aborted." ) );
626 break;
631 void
632 CoverFetcher::finish()
634 emit result( this );
636 deleteLater();
639 void
640 CoverFetcher::finishWithError( const QString &message, KJob *job )
642 if( job )
643 warning() << message << " KIO::error(): " << job->errorText();
645 m_errors += message;
646 m_success = false;
648 emit result( this );
650 deleteLater();
653 #include "coverfetcher.moc"