File indexing completed on 2024-05-19 04:49:40
0001 /**************************************************************************************** 0002 * Copyright (c) 2009 Leo Franchi <lfranchi@kde.org> * 0003 * Copyright (c) 2010, 2011, 2013 Ralf Engels <ralf-engels@gmx.de> * 0004 * * 0005 * This program is free software; you can redistribute it and/or modify it under * 0006 * the terms of the GNU General Public License as published by the Free Software * 0007 * Foundation; either version 2 of the License, or (at your option) any later * 0008 * version. * 0009 * * 0010 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0012 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0013 * * 0014 * You should have received a copy of the GNU General Public License along with * 0015 * this program. If not, see <http://www.gnu.org/licenses/>. * 0016 ****************************************************************************************/ 0017 0018 #define DEBUG_PREFIX "EchoNestBias" 0019 0020 #include "EchoNestBias.h" 0021 0022 #include "core/meta/Meta.h" 0023 #include "core/support/Amarok.h" 0024 #include "core/support/Debug.h" 0025 #include "core-impl/collections/support/CollectionManager.h" 0026 0027 #include <KIO/Job> 0028 #include <KLocalizedString> 0029 0030 #include <QDomDocument> 0031 #include <QDomNode> 0032 #include <QFile> 0033 #include <QLabel> 0034 #include <QPixmap> 0035 #include <QRadioButton> 0036 #include <QStandardPaths> 0037 #include <QTimer> 0038 #include <QUrlQuery> 0039 #include <QVBoxLayout> 0040 #include <QXmlStreamReader> 0041 #include <QXmlStreamWriter> 0042 0043 QString 0044 Dynamic::EchoNestBiasFactory::i18nName() const 0045 { return i18nc("Name of the \"EchoNest\" bias", "EchoNest similar artist"); } 0046 0047 QString 0048 Dynamic::EchoNestBiasFactory::name() const 0049 { return Dynamic::EchoNestBias::sName(); } 0050 0051 QString 0052 Dynamic::EchoNestBiasFactory::i18nDescription() const 0053 { return i18nc("Description of the \"EchoNest\" bias", 0054 "The \"EchoNest\" bias looks up tracks on echo nest and only adds similar tracks."); } 0055 0056 Dynamic::BiasPtr 0057 Dynamic::EchoNestBiasFactory::createBias() 0058 { return Dynamic::BiasPtr( new Dynamic::EchoNestBias() ); } 0059 0060 0061 // ----- EchoNestBias -------- 0062 0063 Dynamic::EchoNestBias::EchoNestBias() 0064 : SimpleMatchBias() 0065 , m_artistSuggestedQuery( nullptr ) 0066 , m_match( PreviousTrack ) 0067 , m_mutex( QMutex::Recursive ) 0068 { 0069 loadDataFromFile(); 0070 } 0071 0072 Dynamic::EchoNestBias::~EchoNestBias() 0073 { 0074 // TODO: kill all running queries 0075 } 0076 0077 void 0078 Dynamic::EchoNestBias::fromXml( QXmlStreamReader *reader ) 0079 { 0080 while (!reader->atEnd()) { 0081 reader->readNext(); 0082 0083 if( reader->isStartElement() ) 0084 { 0085 QStringRef name = reader->name(); 0086 if( name == "match" ) 0087 m_match = matchForName( reader->readElementText(QXmlStreamReader::SkipChildElements) ); 0088 else 0089 { 0090 debug()<<"Unexpected xml start element"<<reader->name()<<"in input"; 0091 reader->skipCurrentElement(); 0092 } 0093 } 0094 else if( reader->isEndElement() ) 0095 { 0096 break; 0097 } 0098 } 0099 } 0100 0101 void 0102 Dynamic::EchoNestBias::toXml( QXmlStreamWriter *writer ) const 0103 { 0104 writer->writeTextElement( QStringLiteral("match"), nameForMatch( m_match ) ); 0105 } 0106 0107 QString 0108 Dynamic::EchoNestBias::sName() 0109 { 0110 return QStringLiteral( "echoNestBias" ); 0111 } 0112 0113 QString 0114 Dynamic::EchoNestBias::name() const 0115 { 0116 return Dynamic::EchoNestBias::sName(); 0117 } 0118 0119 QString 0120 Dynamic::EchoNestBias::toString() const 0121 { 0122 switch( m_match ) 0123 { 0124 case PreviousTrack: 0125 return i18nc("EchoNest bias representation", 0126 "Similar to the previous artist (as reported by EchoNest)"); 0127 case Playlist: 0128 return i18nc("EchoNest bias representation", 0129 "Similar to any artist in the current playlist (as reported by EchoNest)"); 0130 } 0131 return QString(); 0132 } 0133 0134 QWidget* 0135 Dynamic::EchoNestBias::widget( QWidget* parent ) 0136 { 0137 QWidget *widget = new QWidget( parent ); 0138 QVBoxLayout *layout = new QVBoxLayout( widget ); 0139 0140 QLabel *imageLabel = new QLabel(); 0141 imageLabel->setPixmap( QPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/echonest.png") ) ) ); 0142 QLabel *label = new QLabel( i18n( "<a href=\"http://the.echonest.com/\">the echonest</a> thinks the artist is similar to" ) ); 0143 0144 QRadioButton *rb1 = new QRadioButton( i18n( "the previous track's artist" ) ); 0145 QRadioButton *rb2 = new QRadioButton( i18n( "one of the artist in the current playlist" ) ); 0146 0147 rb1->setChecked( m_match == PreviousTrack ); 0148 rb2->setChecked( m_match == Playlist ); 0149 0150 connect( rb2, &QRadioButton::toggled, 0151 this, &Dynamic::EchoNestBias::setMatchTypePlaylist ); 0152 0153 layout->addWidget( imageLabel ); 0154 layout->addWidget( label ); 0155 layout->addWidget( rb1 ); 0156 layout->addWidget( rb2 ); 0157 0158 return widget; 0159 } 0160 0161 Dynamic::TrackSet 0162 Dynamic::EchoNestBias::matchingTracks( const Meta::TrackList& playlist, 0163 int contextCount, int finalCount, 0164 const Dynamic::TrackCollectionPtr &universe ) const 0165 { 0166 Q_UNUSED( contextCount ); 0167 Q_UNUSED( finalCount ); 0168 0169 // collect the artist 0170 QStringList artists = currentArtists( playlist.count() - 1, playlist ); 0171 if( artists.isEmpty() ) 0172 return Dynamic::TrackSet( universe, true ); 0173 0174 { 0175 QMutexLocker locker( &m_mutex ); 0176 QString key = tracksMapKey( artists ); 0177 // debug() << "searching in cache for"<<key 0178 // <<"have tracks"<<m_tracksMap.contains( key ) 0179 // <<"have artists"<<m_similarArtistMap.contains( key ); 0180 if( m_tracksMap.contains( key ) ) 0181 return m_tracksMap.value( key ); 0182 } 0183 0184 m_tracks = Dynamic::TrackSet( universe, false ); 0185 m_currentArtists = artists; 0186 QTimer::singleShot(0, 0187 const_cast<EchoNestBias*>(this), 0188 &EchoNestBias::newQuery); // create the new query from my parent thread 0189 0190 return Dynamic::TrackSet(); 0191 } 0192 0193 0194 bool 0195 Dynamic::EchoNestBias::trackMatches( int position, 0196 const Meta::TrackList& playlist, 0197 int contextCount ) const 0198 { 0199 Q_UNUSED( contextCount ); 0200 0201 // collect the artist 0202 QStringList artists = currentArtists( position, playlist ); 0203 if( artists.isEmpty() ) 0204 return true; 0205 0206 // the artist of this track 0207 if( position < 0 || position >= playlist.count() ) 0208 return false; 0209 0210 Meta::TrackPtr track = playlist[position]; 0211 Meta::ArtistPtr artist = track->artist(); 0212 if( !artist || artist->name().isEmpty() ) 0213 return false; 0214 0215 { 0216 QMutexLocker locker( &m_mutex ); 0217 QString key = tracksMapKey( artists ); 0218 if( m_similarArtistMap.contains( key ) ) 0219 return m_similarArtistMap.value( key ).contains( artist->name() ); 0220 } 0221 debug() << "didn't have artist suggestions saved for this artist:" << artist->name(); 0222 return false; 0223 } 0224 0225 0226 void 0227 Dynamic::EchoNestBias::invalidate() 0228 { 0229 SimpleMatchBias::invalidate(); 0230 m_tracksMap.clear(); 0231 } 0232 0233 void 0234 Dynamic::EchoNestBias::newQuery() 0235 { 0236 // - get the similar artists 0237 QStringList similar; 0238 { 0239 QMutexLocker locker( &m_mutex ); 0240 QString key = tracksMapKey( m_currentArtists ); 0241 if( m_similarArtistMap.contains( key ) ) 0242 { 0243 similar = m_similarArtistMap.value( key ); 0244 debug() << "got similar artists:" << similar.join(QStringLiteral(", ")); 0245 } 0246 else 0247 { 0248 newSimilarArtistQuery(); 0249 return; // not yet ready to do construct a query maker 0250 } 0251 } 0252 0253 // ok, I need a new query maker 0254 m_qm.reset( CollectionManager::instance()->queryMaker() ); 0255 0256 // - construct the query 0257 m_qm->beginOr(); 0258 foreach( const QString &artistName, similar ) 0259 { 0260 m_qm->addFilter( Meta::valArtist, artistName, true, true ); 0261 0262 } 0263 m_qm->endAndOr(); 0264 0265 m_qm->setQueryType( Collections::QueryMaker::Custom ); 0266 m_qm->addReturnValue( Meta::valUniqueId ); 0267 0268 connect( m_qm.data(), &Collections::QueryMaker::newResultReady, 0269 this, &EchoNestBias::updateReady ); 0270 connect( m_qm.data(), &Collections::QueryMaker::queryDone, 0271 this, &EchoNestBias::updateFinished ); 0272 0273 // - run the query 0274 m_qm->run(); 0275 } 0276 0277 void 0278 Dynamic::EchoNestBias::newSimilarArtistQuery() 0279 { 0280 QMultiMap< QString, QString > params; 0281 0282 // -- start the query 0283 params.insert( QStringLiteral("results"), QStringLiteral("30") ); 0284 params.insert( QStringLiteral("name"), m_currentArtists.join(QStringLiteral(", ")) ); 0285 m_artistSuggestedQuery = KIO::storedGet( createUrl( QStringLiteral("artist/similar"), params ), KIO::NoReload, KIO::HideProgressInfo ); 0286 connect( m_artistSuggestedQuery, &KJob::result, 0287 this, &EchoNestBias::similarArtistQueryDone ); 0288 } 0289 0290 void 0291 Dynamic::EchoNestBias::similarArtistQueryDone( KJob* job ) // slot 0292 { 0293 job->deleteLater(); 0294 if( job != m_artistSuggestedQuery ) 0295 { 0296 debug() << "job was deleted from under us...wtf! blame the gerbils."; 0297 m_tracks.reset( false ); 0298 Q_EMIT resultReady( m_tracks ); 0299 return; 0300 } 0301 0302 QDomDocument doc; 0303 if( !doc.setContent( m_artistSuggestedQuery->data() ) ) 0304 { 0305 debug() << "got invalid XML from EchoNest::get_similar!"; 0306 m_tracks.reset( false ); 0307 Q_EMIT resultReady( m_tracks ); 0308 return; 0309 } 0310 0311 // -- decode the result 0312 QDomNodeList artists = doc.elementsByTagName( QStringLiteral("artist") ); 0313 if( artists.isEmpty() ) 0314 { 0315 debug() << "Got no similar artists! Bailing!"; 0316 m_tracks.reset( false ); 0317 Q_EMIT resultReady( m_tracks ); 0318 return; 0319 } 0320 0321 QStringList similarArtists; 0322 for( int i = 0; i < artists.count(); i++ ) 0323 { 0324 similarArtists.append( artists.at(i).firstChildElement( QStringLiteral("name") ).text() ); 0325 } 0326 0327 // -- commit the result 0328 { 0329 QMutexLocker locker( &m_mutex ); 0330 QString key = tracksMapKey( m_currentArtists ); 0331 m_similarArtistMap.insert( key, similarArtists ); 0332 saveDataToFile(); 0333 } 0334 0335 newQuery(); 0336 } 0337 0338 void 0339 Dynamic::EchoNestBias::updateFinished() 0340 { 0341 // -- store away the result for future reference 0342 QString key = tracksMapKey( m_currentArtists ); 0343 m_tracksMap.insert( key, m_tracks ); 0344 debug() << "saving found similar tracks to key:" << key; 0345 0346 SimpleMatchBias::updateFinished(); 0347 } 0348 0349 QStringList 0350 Dynamic::EchoNestBias::currentArtists( int position, const Meta::TrackList& playlist ) const 0351 { 0352 QStringList result; 0353 0354 if( m_match == PreviousTrack ) 0355 { 0356 if( position >= 0 && position < playlist.count() ) 0357 { 0358 Meta::ArtistPtr artist = playlist[ position ]->artist(); 0359 if( artist && !artist->name().isEmpty() ) 0360 result.append( artist->name() ); 0361 } 0362 } 0363 else if( m_match == Playlist ) 0364 { 0365 for( int i=0; i < position && i < playlist.count(); i++ ) 0366 { 0367 Meta::ArtistPtr artist = playlist[i]->artist(); 0368 if( artist && !artist->name().isEmpty() ) 0369 result.append( artist->name() ); 0370 } 0371 } 0372 0373 return result; 0374 } 0375 0376 0377 // this method shamelessly inspired by liblastfm/src/ws/ws.cpp 0378 QUrl Dynamic::EchoNestBias::createUrl( const QString &method, QMultiMap< QString, QString > params ) 0379 { 0380 params.insert( QStringLiteral("api_key"), QStringLiteral("DD9P0OV9OYFH1LCAE") ); 0381 params.insert( QStringLiteral("format"), QStringLiteral("xml") ); 0382 0383 QUrl url; 0384 QUrlQuery query; 0385 url.setScheme( QStringLiteral("http") ); 0386 url.setHost( QStringLiteral("developer.echonest.com") ); 0387 url.setPath( "/api/v4/" + method ); 0388 0389 // take care of the ID possibility manually 0390 // Qt setQueryItems doesn't encode a bunch of stuff, so we do it manually 0391 QMapIterator<QString, QString> i( params ); 0392 while ( i.hasNext() ) { 0393 i.next(); 0394 QByteArray const key = QUrl::toPercentEncoding( i.key() ); 0395 QByteArray const value = QUrl::toPercentEncoding( i.value() ); 0396 query.addQueryItem( key, value ); 0397 } 0398 url.setQuery( query ); 0399 0400 return url; 0401 } 0402 0403 void 0404 Dynamic::EchoNestBias::saveDataToFile() const 0405 { 0406 QFile file( Amarok::saveLocation() + "dynamic_echonest_similar.xml" ); 0407 if( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) 0408 return; 0409 0410 QXmlStreamWriter writer( &file ); 0411 writer.setAutoFormatting( true ); 0412 0413 writer.writeStartDocument(); 0414 writer.writeStartElement( QStringLiteral("echonestSimilar") ); 0415 0416 // -- write the similar artists 0417 foreach( const QString& key, m_similarArtistMap.keys() ) 0418 { 0419 writer.writeStartElement( QStringLiteral("similarArtist") ); 0420 writer.writeTextElement( QStringLiteral("artist"), key ); 0421 foreach( const QString& name, m_similarArtistMap.value( key ) ) 0422 { 0423 writer.writeTextElement( QStringLiteral("similar"), name ); 0424 } 0425 writer.writeEndElement(); 0426 } 0427 0428 writer.writeEndElement(); 0429 writer.writeEndDocument(); 0430 } 0431 0432 void 0433 Dynamic::EchoNestBias::readSimilarArtists( QXmlStreamReader *reader ) 0434 { 0435 QString key; 0436 QList<QString> artists; 0437 0438 while (!reader->atEnd()) { 0439 reader->readNext(); 0440 QStringRef name = reader->name(); 0441 0442 if( reader->isStartElement() ) 0443 { 0444 if( name == QLatin1String("artist") ) 0445 key = reader->readElementText(QXmlStreamReader::SkipChildElements); 0446 else if( name == QLatin1String("similar") ) 0447 artists.append( reader->readElementText(QXmlStreamReader::SkipChildElements) ); 0448 else 0449 reader->skipCurrentElement(); 0450 } 0451 else if( reader->isEndElement() ) 0452 { 0453 break; 0454 } 0455 } 0456 0457 m_similarArtistMap.insert( key, artists ); 0458 } 0459 0460 void 0461 Dynamic::EchoNestBias::loadDataFromFile() 0462 { 0463 m_similarArtistMap.clear(); 0464 0465 QFile file( Amarok::saveLocation() + "dynamic_echonest_similar.xml" ); 0466 0467 if( !file.exists() || 0468 !file.open( QIODevice::ReadOnly ) ) 0469 return; 0470 0471 QXmlStreamReader reader( &file ); 0472 0473 while (!reader.atEnd()) { 0474 reader.readNext(); 0475 0476 QStringRef name = reader.name(); 0477 if( reader.isStartElement() ) 0478 { 0479 if( name == QLatin1String("lastfmSimilar") ) 0480 { 0481 ; // just recurse into the element 0482 } 0483 else if( name == QLatin1String("similarArtist") ) 0484 { 0485 readSimilarArtists( &reader ); 0486 } 0487 else 0488 { 0489 reader.skipCurrentElement(); 0490 } 0491 } 0492 else if( reader.isEndElement() ) 0493 { 0494 break; 0495 } 0496 } 0497 } 0498 0499 Dynamic::EchoNestBias::MatchType 0500 Dynamic::EchoNestBias::match() const 0501 { return m_match; } 0502 0503 void 0504 Dynamic::EchoNestBias::setMatch( Dynamic::EchoNestBias::MatchType value ) 0505 { 0506 m_match = value; 0507 invalidate(); 0508 Q_EMIT changed( BiasPtr(this) ); 0509 } 0510 0511 0512 void 0513 Dynamic::EchoNestBias::setMatchTypePlaylist( bool playlist ) 0514 { 0515 setMatch( playlist ? Playlist : PreviousTrack ); 0516 } 0517 0518 0519 QString 0520 Dynamic::EchoNestBias::nameForMatch( Dynamic::EchoNestBias::MatchType match ) 0521 { 0522 switch( match ) 0523 { 0524 case Dynamic::EchoNestBias::PreviousTrack: return QStringLiteral("previous"); 0525 case Dynamic::EchoNestBias::Playlist: return QStringLiteral("playlist"); 0526 } 0527 return QString(); 0528 } 0529 0530 Dynamic::EchoNestBias::MatchType 0531 Dynamic::EchoNestBias::matchForName( const QString &name ) 0532 { 0533 if( name == QLatin1String("previous") ) return PreviousTrack; 0534 else if( name == QLatin1String("playlist") ) return Playlist; 0535 else return PreviousTrack; 0536 } 0537 0538 QString 0539 Dynamic::EchoNestBias::tracksMapKey( const QStringList &artists ) 0540 { 0541 return artists.join(QStringLiteral("|")); 0542 } 0543