You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
amarok/amarok/src/coverfetcher.cpp

679 lines
21 KiB

// (C) 2004 Mark Kretschmann <markey@web.de>
// (C) 2004 Stefan Bogner <bochi@online.ms>
// (C) 2004 Max Howell
// See COPYING file for licensing information.
#include "amarok.h"
#include "amarokconfig.h"
#include "collectiondb.h"
#include "config.h" //for AMAZON_SUPPORT
#include "covermanager.h"
#include "coverfetcher.h"
#include "debug.h"
#include "statusbar.h"
#include <tqdom.h>
#include <tqhbox.h>
#include <tqlabel.h>
#include <tqlayout.h>
#include <tqregexp.h>
#include <tdeapplication.h>
#include <kcombobox.h>
#include <kcursor.h> //waiting cursor
#include <kdialog.h>
#include <kiconloader.h>
#include <tdefiledialog.h>
#include <tdeio/job.h>
#include <tdeio/jobclasses.h>
#include <klineedit.h>
#include <tdelocale.h>
#include <tdemessagebox.h>
#include <tdepopupmenu.h>
#include <kpushbutton.h>
#include <twin.h>
void
Amarok::coverContextMenu( TQWidget *parent, TQPoint point, const TQString &artist, const TQString &album, bool showCoverManager )
{
TDEPopupMenu menu;
enum { SHOW, FETCH, CUSTOM, DELETE, MANAGER };
menu.insertTitle( i18n( "Cover Image" ) );
menu.insertItem( SmallIconSet( Amarok::icon( "zoom" ) ), i18n( "&Show Fullsize" ), SHOW );
menu.insertItem( SmallIconSet( Amarok::icon( "download" ) ), i18n( "&Fetch From amazon.%1" ).arg( CoverManager::amazonTld() ), FETCH );
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "Set &Custom Cover" ), CUSTOM );
bool disable = !album.isEmpty(); // disable setting covers for unknown albums
menu.setItemEnabled( FETCH, disable );
menu.setItemEnabled( CUSTOM, disable );
menu.insertSeparator();
menu.insertItem( SmallIconSet( Amarok::icon( "remove" ) ), i18n( "&Unset Cover" ), DELETE );
if ( showCoverManager ) {
menu.insertSeparator();
menu.insertItem( SmallIconSet( Amarok::icon( "covermanager" ) ), i18n( "Cover &Manager" ), MANAGER );
}
#ifndef AMAZON_SUPPORT
menu.setItemEnabled( FETCH, false );
#endif
disable = !CollectionDB::instance()->albumImage( artist, album, 0 ).contains( "nocover" );
menu.setItemEnabled( SHOW, disable );
menu.setItemEnabled( DELETE, disable );
switch( menu.exec( point ) )
{
case SHOW:
CoverManager::viewCover( artist, album, parent );
break;
case DELETE:
{
const int button = KMessageBox::warningContinueCancel( parent,
i18n( "Are you sure you want to remove this cover from the Collection?" ),
TQString(),
KStdGuiItem::del() );
if ( button == KMessageBox::Continue )
CollectionDB::instance()->removeAlbumImage( artist, album );
break;
}
case FETCH:
#ifdef AMAZON_SUPPORT
CollectionDB::instance()->fetchCover( parent, artist, album, false );
break;
#endif
case CUSTOM:
{
TQString artist_id; artist_id.setNum( CollectionDB::instance()->artistID( artist ) );
TQString album_id; album_id.setNum( CollectionDB::instance()->albumID( album ) );
TQStringList values = CollectionDB::instance()->albumTracks( artist_id, album_id );
TQString startPath = ":homedir";
if ( !values.isEmpty() ) {
KURL url;
url.setPath( values.first() );
startPath = url.directory();
}
KURL file = KFileDialog::getImageOpenURL( startPath, parent, i18n("Select Cover Image File") );
if ( !file.isEmpty() )
CollectionDB::instance()->setAlbumImage( artist, album, file );
break;
}
case MANAGER:
CoverManager::showOnce( album );
break;
}
}
CoverLabel::CoverLabel ( TQWidget * parent, const char * name, WFlags f )
: TQLabel( parent, name, f)
{}
void CoverLabel::mouseReleaseEvent(TQMouseEvent *pEvent) {
if (pEvent->button() == Qt::LeftButton || pEvent->button() == Qt::RightButton)
{
Amarok::coverContextMenu( this, pEvent->globalPos(), m_artist, m_album, false );
}
}
CoverFetcher::CoverFetcher( TQWidget *parent, const TQString &artist, TQString album )
: TQObject( parent, "CoverFetcher" )
, m_artist( artist )
, m_album( album )
, m_size( 2 )
, m_success( true )
{
DEBUG_FUNC_INFO
TQStringList extensions;
extensions << i18n("disc") << i18n("disk") << i18n("remaster") << i18n("cd") << i18n("single") << i18n("soundtrack") << i18n("part")
<< "disc" << "disk" << "remaster" << "cd" << "single" << "soundtrack" << "part" << "cds" /*cd single*/;
//we do several queries, one raw ie, without the following modifications
//the others have the above strings removed with the following regex, as this can increase hit-rate
const TQString template1 = " ?-? ?[(^{]* ?%1 ?\\d*[)^}\\]]* *$"; //eg album - [disk 1] -> album
foreach( extensions ) {
TQRegExp regexp( template1.arg( *it ) );
regexp.setCaseSensitive( false );
album.remove( regexp );
}
//TODO try queries that remove anything in album after a " - " eg Les Mis. - Excerpts
/**
* We search for artist - album, and just album, using the exact album text and the
* manipulated album text.
*/
//search on our modified term, then the original
if ( !m_artist.isEmpty() )
m_userQuery = m_artist + " - ";
m_userQuery += m_album;
m_queries += m_artist + " - " + album;
m_queries += m_userQuery;
m_queries += album;
m_queries += m_album;
//don't do the same searches twice in a row
if( m_album == album ) {
m_queries.pop_front();
m_queries.pop_back();
}
/**
* Finally we do a search for just the artist, just in case as this often
* turns up a cover, and it might just be the right one! Also it would be
* the only valid search if m_album.isEmpty()
*/
m_queries += m_artist;
TQApplication::setOverrideCursor( KCursor::workingCursor() );
}
CoverFetcher::~CoverFetcher()
{
DEBUG_FUNC_INFO
TQApplication::restoreOverrideCursor();
}
void
CoverFetcher::startFetch()
{
DEBUG_FUNC_INFO
// Static license Key. Thanks hydrogen ;-)
const TQString LICENSE( "11ZKJS8X1ETSTJ6MT802" );
// reset all values
m_coverAmazonUrls.clear();
m_coverAsins.clear();
m_coverUrls.clear();
m_coverNames.clear();
m_xml = TQString();
m_size = 2;
if ( m_queries.isEmpty() ) {
debug() << "m_queries is empty" << endl;
finishWithError( i18n("No cover found") );
return;
}
TQString query = m_queries.front();
m_queries.pop_front();
// '&' breaks searching
query.remove('&');
TQString locale = AmarokConfig::amazonLocale();
TQString tld;
if( locale == "us" )
tld = "com";
else if( locale =="uk" )
tld = "co.uk";
else
tld = locale;
int mibenum = 106; // utf-8
TQString url;
url = "http://ecs.amazonaws." + tld
+ "/onca/xml?Service=AWSECommerceService&Version=2007-10-29&Operation=ItemSearch&AssociateTag=webservices-20&AWSAccessKeyId=" + LICENSE
+ "&Keywords=" + KURL::encode_string_no_slash( query, mibenum )
+ "&SearchIndex=Music&ResponseGroup=Small,Images";
debug() << url << endl;
TDEIO::TransferJob* job = TDEIO::storedGet( url, false, false );
connect( job, TQT_SIGNAL(result( TDEIO::Job* )), TQT_SLOT(finishedXmlFetch( TDEIO::Job* )) );
Amarok::StatusBar::instance()->newProgressOperation( job );
}
//////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE SLOTS
//////////////////////////////////////////////////////////////////////////////////////////
void
CoverFetcher::finishedXmlFetch( TDEIO::Job *job ) //SLOT
{
DEBUG_BLOCK
// NOTE: job can become 0 when this method is called from attemptAnotherFetch()
if( job && job->error() ) {
finishWithError( i18n("There was an error communicating with Amazon."), job );
return;
}
if ( job ) {
TDEIO::StoredTransferJob* const storedJob = static_cast<TDEIO::StoredTransferJob*>( job );
m_xml = TQString::fromUtf8( storedJob->data().data(), storedJob->data().size() );
}
TQDomDocument doc;
if( !doc.setContent( m_xml ) ) {
m_errors += i18n("The XML obtained from Amazon is invalid.");
startFetch();
return;
}
m_coverAsins.clear();
m_coverAmazonUrls.clear();
m_coverUrls.clear();
m_coverNames.clear();
// the url for the Amazon product info page
const TQDomNodeList list = doc.documentElement().namedItem( "Items" ).childNodes();
for(int i = 0; i < list.count(); i++ )
{
TQDomNode n = list.item( i );
if( n.isElement() && n.nodeName() == "IsValid" )
{
if( n.toElement().text() == "False" )
{
warning() << "The XML Is Invalid!";
return;
}
}
else if( list.item( i ).nodeName() == "Item" )
{
const TQDomNode node = list.item( i );
parseItemNode( node );
}
}
attemptAnotherFetch();
}
void CoverFetcher::parseItemNode( const TQDomNode &node )
{
TQDomNode it = node.firstChild();
TQString size;
switch( m_size )
{
case 0: size = "Small"; break;
case 1: size = "Medium"; break;
default: size = "Large"; break;
}
size += "Image";
while ( !it.isNull() ) {
if ( it.isElement() ) {
TQDomElement e = it.toElement();
if(e.tagName()=="ASIN")
{
m_asin = e.text();
m_coverAsins += m_asin;
}
else if(e.tagName() == "DetailPageURL" )
{
m_amazonURL = e.text();
m_coverAmazonUrls += m_amazonURL;
}
else if( e.tagName() == size )
{
TQDomNode subIt = e.firstChild();
while( !subIt.isNull() )
{
if( subIt.isElement() )
{
TQDomElement subE = subIt.toElement();
if( subE.tagName() == "URL" )
{
const TQString coverUrl = subE.text();
m_coverUrls += coverUrl;
break;
}
}
subIt = subIt.nextSibling();
}
}
else if( e.tagName() == "ItemAttributes" )
{
TQDomNodeList nodes = e.childNodes();
TQDomNode iter;
TQString artist;
TQString album;
for( int i = 0; i < nodes.count(); i++ )
{
iter = nodes.item( i );
if( iter.isElement() )
{
if( iter.nodeName() == "Artist" )
{
artist = iter.toElement().text();
}
else if( iter.nodeName() == "Title" )
{
album = iter.toElement().text();
}
}
}
m_coverNames += TQString( artist + " - " + album );
}
}
it = it.nextSibling();
}
}
void
CoverFetcher::finishedImageFetch( TDEIO::Job *job ) //SLOT
{
if( job->error() ) {
debug() << "finishedImageFetch(): TDEIO::error(): " << job->error() << endl;
m_errors += i18n("The cover could not be retrieved.");
attemptAnotherFetch();
return;
}
m_image.loadFromData( static_cast<TDEIO::StoredTransferJob*>( job )->data() );
if( m_image.width() <= 1 ) {
//Amazon seems to offer images of size 1x1 sometimes
//Amazon has nothing to offer us for the requested image size
m_errors += i18n("The cover-data produced an invalid image.");
attemptAnotherFetch();
}
else if( m_userCanEditQuery )
//yay! image found :)
//lets see if the user wants it
showCover();
else
//image loaded successfully yay!
finish();
}
void
CoverFetcher::attemptAnotherFetch()
{
DEBUG_BLOCK
if( !m_coverUrls.isEmpty() ) {
// Amazon suggested some more cover URLs to try before we
// try a different query
TDEIO::TransferJob* job = TDEIO::storedGet( KURL(m_coverUrls.front()), false, false );
connect( job, TQT_SIGNAL(result( TDEIO::Job* )), TQT_SLOT(finishedImageFetch( TDEIO::Job* )) );
Amarok::StatusBar::instance()->newProgressOperation( job );
m_coverUrls.pop_front();
m_currentCoverName = m_coverNames.front();
m_coverNames.pop_front();
m_amazonURL = m_coverAmazonUrls.front();
m_coverAmazonUrls.pop_front();
m_asin = m_coverAsins.front();
m_coverAsins.pop_front();
}
else if( !m_xml.isEmpty() && m_size > 0 ) {
// we need to try smaller sizes, this often is
// fruitless, but does work out sometimes.
m_size--;
finishedXmlFetch( 0 );
}
else if( !m_queries.isEmpty() ) {
// we have some queries left in the pot
startFetch();
}
else if( m_userCanEditQuery ) {
// we have exhausted all the predetermined queries
// so lets let the user give it a try
getUserQuery( i18n("You have seen all the covers Amazon returned using the query below. Perhaps you can refine it:") );
m_coverAmazonUrls.clear();
m_coverAsins.clear();
m_coverUrls.clear();
m_coverNames.clear();
}
else
finishWithError( i18n("No cover found") );
}
// Moved outside the only function that uses it because
// gcc 2.95 doesn't like class declarations there.
class EditSearchDialog : public KDialog
{
public:
EditSearchDialog( TQWidget* parent, const TQString &text, const TQString &keyword, CoverFetcher *fetcher )
: KDialog( parent )
{
setCaption( i18n( "Amazon Query Editor" ) );
// amazon combo box
KComboBox* amazonLocale = new KComboBox( this );
amazonLocale->insertItem( i18n("International"), CoverFetcher::International );
amazonLocale->insertItem( i18n("Canada"), CoverFetcher::Canada );
amazonLocale->insertItem( i18n("France"), CoverFetcher::France );
amazonLocale->insertItem( i18n("Germany"), CoverFetcher::Germany );
amazonLocale->insertItem( i18n("Japan"), CoverFetcher::Japan);
amazonLocale->insertItem( i18n("United Kingdom"), CoverFetcher::UK );
if( CoverManager::instance() )
connect( amazonLocale, TQT_SIGNAL( activated(int) ),
CoverManager::instance(), TQT_SLOT( changeLocale(int) ) );
else
connect( amazonLocale, TQT_SIGNAL( activated(int) ),
fetcher, TQT_SLOT( changeLocale(int) ) );
TQHBoxLayout *hbox1 = new TQHBoxLayout( 8 );
hbox1->addWidget( new TQLabel( i18n( "Amazon Locale: " ), this ) );
hbox1->addWidget( amazonLocale );
int currentLocale = CoverFetcher::localeStringToID( AmarokConfig::amazonLocale() );
amazonLocale->setCurrentItem( currentLocale );
KPushButton* cancelButton = new KPushButton( KStdGuiItem::cancel(), this );
KPushButton* searchButton = new KPushButton( i18n("&Search"), this );
TQHBoxLayout *hbox2 = new TQHBoxLayout( 8 );
hbox2->addItem( new TQSpacerItem( 160, 8, TQSizePolicy::Expanding, TQSizePolicy::Minimum ) );
hbox2->addWidget( searchButton );
hbox2->addWidget( cancelButton );
TQVBoxLayout *vbox = new TQVBoxLayout( this, 8, 8 );
vbox->addLayout( hbox1 );
vbox->addWidget( new TQLabel( "<qt>" + text, this ) );
vbox->addWidget( new KLineEdit( keyword, this, "Query" ) );
vbox->addLayout( hbox2 );
searchButton->setDefault( true );
adjustSize();
setFixedHeight( height() );
connect( searchButton, TQT_SIGNAL(clicked()), TQT_SLOT(accept()) );
connect( cancelButton, TQT_SIGNAL(clicked()), TQT_SLOT(reject()) );
}
TQString query() { return static_cast<KLineEdit*>(TQT_TQWIDGET(child( "Query" )))->text(); }
};
TQString
CoverFetcher::localeIDToString( int id )//static
{
switch ( id )
{
case International:
return "us";
case Canada:
return "ca";
case France:
return "fr";
case Germany:
return "de";
case Japan:
return "jp";
case UK:
return "uk";
}
return "us";
}
int
CoverFetcher::localeStringToID( const TQString &s )
{
int id = International;
if( s == "fr" ) id = France;
else if( s == "de" ) id = Germany;
else if( s == "jp" ) id = Japan;
else if( s == "uk" ) id = UK;
else if( s == "ca" ) id = Canada;
return id;
}
void
CoverFetcher::changeLocale( int id )//SLOT
{
TQString locale = localeIDToString( id );
AmarokConfig::setAmazonLocale( locale );
}
void
CoverFetcher::getUserQuery( TQString explanation )
{
if( explanation.isEmpty() )
explanation = i18n("Ask Amazon for covers using this query:");
EditSearchDialog dialog(
TQT_TQWIDGET( parent() ),
explanation,
m_userQuery,
this );
switch( dialog.exec() )
{
case TQDialog::Accepted:
m_userQuery = dialog.query();
m_queries = m_userQuery;
startFetch();
break;
default:
finishWithError( i18n( "Aborted." ) );
break;
}
}
class CoverFoundDialog : public KDialog
{
public:
CoverFoundDialog( TQWidget *parent, const TQImage &cover, const TQString &productname )
: KDialog( parent )
{
// Gives the window a small title bar, and skips a taskbar entry
KWin::setType( winId(), NET::Utility );
KWin::setState( winId(), NET::SkipTaskbar );
(new TQVBoxLayout( this ))->setAutoAdd( true );
TQLabel *labelPix = new TQLabel( this );
TQLabel *labelName = new TQLabel( this );
TQHBox *buttons = new TQHBox( this );
KPushButton *save = new KPushButton( KStdGuiItem::save(), buttons );
KPushButton *newsearch = new KPushButton( i18n( "Ne&w Search..." ), buttons, "NewSearch" );
KPushButton *nextcover = new KPushButton( i18n( "&Next Cover" ), buttons, "NextCover" );
KPushButton *cancel = new KPushButton( KStdGuiItem::cancel(), buttons );
labelPix ->setAlignment( TQt::AlignHCenter );
labelName->setAlignment( TQt::AlignHCenter );
labelPix ->setPixmap( cover );
labelName->setText( productname );
save->setDefault( true );
this->setFixedSize( sizeHint() );
this->setCaption( i18n("Cover Found") );
connect( save, TQT_SIGNAL(clicked()), TQT_SLOT(accept()) );
connect( newsearch, TQT_SIGNAL(clicked()), TQT_SLOT(accept()) );
connect( nextcover, TQT_SIGNAL(clicked()), TQT_SLOT(accept()) );
connect( cancel, TQT_SIGNAL(clicked()), TQT_SLOT(reject()) );
}
virtual void accept()
{
if( tqstrcmp( TQT_TQOBJECT(const_cast<TQT_BASE_OBJECT_NAME*>(sender()))->name(), "NewSearch" ) == 0 )
done( 1000 );
else if( tqstrcmp( TQT_TQOBJECT(const_cast<TQT_BASE_OBJECT_NAME*>(sender()))->name(), "NextCover" ) == 0 )
done( 1001 );
else
KDialog::accept();
}
};
void
CoverFetcher::showCover()
{
CoverFoundDialog dialog( TQT_TQWIDGET( parent() ), m_image, m_currentCoverName );
switch( dialog.exec() )
{
case KDialog::Accepted:
finish();
break;
case 1000: //showQueryEditor()
getUserQuery();
m_coverAmazonUrls.clear();
m_coverAsins.clear();
m_coverUrls.clear();
m_coverNames.clear();
break;
case 1001: //nextCover()
attemptAnotherFetch();
break;
default:
finishWithError( i18n( "Aborted." ) );
break;
}
}
void
CoverFetcher::finish()
{
emit result( this );
deleteLater();
}
void
CoverFetcher::finishWithError( const TQString &message, TDEIO::Job *job )
{
if( job )
warning() << message << " TDEIO::error(): " << job->errorText() << endl;
m_errors += message;
m_success = false;
emit result( this );
deleteLater();
}
#include "coverfetcher.moc"