File indexing completed on 2025-01-05 04:26:56
0001 /**************************************************************************************** 0002 * Copyright (c) 2009 Leo Franchi <lfranchi@kde.org> * 0003 * * 0004 * This program is free software; you can redistribute it and/or modify it under * 0005 * the terms of the GNU General Public License as published by the Free Software * 0006 * Foundation; either version 2 of the License, or (at your option) any later * 0007 * version. * 0008 * * 0009 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0010 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0011 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0012 * * 0013 * You should have received a copy of the GNU General Public License along with * 0014 * this program. If not, see <http://www.gnu.org/licenses/>. * 0015 ****************************************************************************************/ 0016 0017 #define DEBUG_PREFIX "LastFmBias" 0018 0019 #include "LastFmBias.h" 0020 0021 #include "core/meta/Meta.h" 0022 #include "core/support/Amarok.h" 0023 #include "core/support/Debug.h" 0024 #include "core-impl/collections/support/CollectionManager.h" 0025 0026 #include <KLocalizedString> 0027 #include <QStandardPaths> 0028 0029 #include <QDomDocument> 0030 #include <QDomNode> 0031 #include <QFile> 0032 #include <QLabel> 0033 #include <QPixmap> 0034 #include <QRadioButton> 0035 #include <QTimer> 0036 #include <QVBoxLayout> 0037 #include <QXmlStreamReader> 0038 #include <QXmlStreamWriter> 0039 0040 #include <Artist.h> 0041 #include <ws.h> 0042 0043 QString 0044 Dynamic::LastFmBiasFactory::i18nName() const 0045 { return i18nc("Name of the \"Last.fm\" similar bias", "Last.fm similar"); } 0046 0047 QString 0048 Dynamic::LastFmBiasFactory::name() const 0049 { return Dynamic::LastFmBias::sName(); } 0050 0051 QString 0052 Dynamic::LastFmBiasFactory::i18nDescription() const 0053 { return i18nc("Description of the \"Last.fm\" bias", 0054 "The \"Last.fm\" similar bias looks up tracks on Last.fm and only adds similar tracks."); } 0055 0056 Dynamic::BiasPtr 0057 Dynamic::LastFmBiasFactory::createBias() 0058 { return Dynamic::BiasPtr( new Dynamic::LastFmBias() ); } 0059 0060 0061 0062 // ----- LastFmBias -------- 0063 0064 Dynamic::LastFmBias::LastFmBias() 0065 : SimpleMatchBias() 0066 , m_match( SimilarArtist ) 0067 , m_mutex( QMutex::Recursive ) 0068 { 0069 loadDataFromFile(); 0070 } 0071 0072 Dynamic::LastFmBias::~LastFmBias() 0073 { 0074 // TODO: kill all running queries 0075 } 0076 0077 void 0078 Dynamic::LastFmBias::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::LastFmBias::toXml( QXmlStreamWriter *writer ) const 0103 { 0104 writer->writeTextElement( "match", nameForMatch( m_match ) ); 0105 } 0106 0107 QString 0108 Dynamic::LastFmBias::sName() 0109 { 0110 return "lastfm_similarartists"; 0111 } 0112 0113 QString 0114 Dynamic::LastFmBias::name() const 0115 { 0116 return Dynamic::LastFmBias::sName(); 0117 } 0118 0119 QString 0120 Dynamic::LastFmBias::toString() const 0121 { 0122 switch( m_match ) 0123 { 0124 case SimilarTrack: 0125 return i18nc("Last.fm bias representation", 0126 "Similar to the previous track (as reported by Last.fm)"); 0127 case SimilarArtist: 0128 return i18nc("Last.fm bias representation", 0129 "Similar to the previous artist (as reported by Last.fm)"); 0130 } 0131 return QString(); 0132 } 0133 0134 0135 QWidget* 0136 Dynamic::LastFmBias::widget( QWidget* parent ) 0137 { 0138 QWidget *widget = new QWidget( parent ); 0139 QVBoxLayout *layout = new QVBoxLayout( widget ); 0140 0141 QLabel *imageLabel = new QLabel(); 0142 imageLabel->setPixmap( QPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/lastfm.png" ) ) ); 0143 QLabel *label = new QLabel( i18n( "<a href=\"http://www.last.fm/\">Last.fm</a> thinks the track is similar to" ) ); 0144 0145 QRadioButton *rb1 = new QRadioButton( i18n( "the previous track's artist" ) ); 0146 QRadioButton *rb2 = new QRadioButton( i18n( "the previous track" ) ); 0147 0148 rb1->setChecked( m_match == SimilarArtist ); 0149 rb2->setChecked( m_match == SimilarTrack ); 0150 0151 connect( rb1, &QRadioButton::toggled, 0152 this, &LastFmBias::setMatchTypeArtist ); 0153 0154 layout->addWidget( imageLabel ); 0155 layout->addWidget( label ); 0156 layout->addWidget( rb1 ); 0157 layout->addWidget( rb2 ); 0158 0159 return widget; 0160 } 0161 0162 Dynamic::TrackSet 0163 Dynamic::LastFmBias::matchingTracks( const Meta::TrackList& playlist, 0164 int contextCount, int finalCount, 0165 Dynamic::TrackCollectionPtr universe ) const 0166 { 0167 Q_UNUSED( contextCount ); 0168 Q_UNUSED( finalCount ); 0169 0170 if( playlist.isEmpty() ) 0171 return Dynamic::TrackSet( universe, true ); 0172 0173 // determine the last track and artist 0174 Meta::TrackPtr lastTrack = playlist.last(); 0175 Meta::ArtistPtr lastArtist = lastTrack->artist(); 0176 0177 m_currentTrack = lastTrack->name(); 0178 m_currentArtist = lastArtist ? lastArtist->name() : QString(); 0179 0180 { 0181 QMutexLocker locker( &m_mutex ); 0182 0183 if( m_match == SimilarArtist ) 0184 { 0185 if( m_currentArtist.isEmpty() ) 0186 return Dynamic::TrackSet( universe, true ); 0187 if( m_tracksMap.contains( m_currentArtist ) ) 0188 return m_tracksMap.value( m_currentArtist ); 0189 } 0190 else if( m_match == SimilarTrack ) 0191 { 0192 if( m_currentTrack.isEmpty() ) 0193 return Dynamic::TrackSet( universe, true ); 0194 QString key = m_currentTrack + '|' + m_currentArtist; 0195 if( m_tracksMap.contains( key ) ) 0196 return m_tracksMap.value( key ); 0197 } 0198 } 0199 0200 m_tracks = Dynamic::TrackSet( universe, false ); 0201 QTimer::singleShot(0, 0202 const_cast<LastFmBias*>(this), 0203 &LastFmBias::newQuery); // create the new query from my parent thread 0204 0205 return Dynamic::TrackSet(); 0206 } 0207 0208 0209 bool 0210 Dynamic::LastFmBias::trackMatches( int position, 0211 const Meta::TrackList& playlist, 0212 int contextCount ) const 0213 { 0214 Q_UNUSED( contextCount ); 0215 0216 if( position <= 0 || position >= playlist.count()) 0217 return false; 0218 0219 // determine the last track and artist 0220 Meta::TrackPtr lastTrack = playlist[position-1]; 0221 Meta::ArtistPtr lastArtist = lastTrack->artist(); 0222 QString lastTrackName = lastTrack->name(); 0223 QString lastArtistName = lastArtist ? lastArtist->name() : QString(); 0224 0225 Meta::TrackPtr currentTrack = playlist[position]; 0226 Meta::ArtistPtr currentArtist = currentTrack->artist(); 0227 QString currentTrackName = currentTrack->name(); 0228 QString currentArtistName = currentArtist ? currentArtist->name() : QString(); 0229 0230 { 0231 QMutexLocker locker( &m_mutex ); 0232 0233 if( m_match == SimilarArtist ) 0234 { 0235 if( lastArtistName.isEmpty() ) 0236 return true; 0237 if( currentArtistName.isEmpty() ) 0238 return false; 0239 if( lastArtistName == currentArtistName ) 0240 return true; 0241 if( m_similarArtistMap.contains( lastArtistName ) ) 0242 return m_similarArtistMap.value( lastArtistName ).contains( currentArtistName ); 0243 } 0244 else if( m_match == SimilarTrack ) 0245 { 0246 if( lastTrackName.isEmpty() ) 0247 return true; 0248 if( currentTrackName.isEmpty() ) 0249 return false; 0250 if( lastTrackName == currentTrackName ) 0251 return true; 0252 TitleArtistPair lastKey( lastTrackName, lastArtistName ); 0253 TitleArtistPair currentKey( currentTrackName, currentArtistName ); 0254 if( m_similarTrackMap.contains( lastKey ) ) 0255 return m_similarTrackMap.value( lastKey ).contains( currentKey ); 0256 } 0257 } 0258 0259 debug() << "didn't have a cached suggestions for track:" << lastTrackName; 0260 return false; 0261 } 0262 0263 0264 void 0265 Dynamic::LastFmBias::invalidate() 0266 { 0267 SimpleMatchBias::invalidate(); 0268 m_tracksMap.clear(); 0269 } 0270 0271 void 0272 Dynamic::LastFmBias::newQuery() 0273 { 0274 DEBUG_BLOCK; 0275 0276 debug() << "similarArtists:"<<m_similarArtistMap.count() << "similarTracks:"<<m_similarTrackMap.count(); 0277 // - get the similar things 0278 QStringList similarArtists; 0279 QList<TitleArtistPair> similarTracks; 0280 { 0281 QMutexLocker locker( &m_mutex ); 0282 if( m_match == SimilarArtist ) 0283 { 0284 if( m_similarArtistMap.contains( m_currentArtist ) ) 0285 { 0286 similarArtists = m_similarArtistMap.value( m_currentArtist ); 0287 debug() << "for"<<m_currentArtist<<"got similar artists:" << similarArtists.join(", "); 0288 } 0289 else 0290 { 0291 locker.unlock(); 0292 newSimilarQuery(); 0293 return; // not yet ready to do construct a query maker 0294 } 0295 } 0296 else if( m_match == SimilarTrack ) 0297 { 0298 TitleArtistPair key( m_currentTrack, m_currentArtist ); 0299 if( m_similarTrackMap.contains( key ) ) 0300 { 0301 similarTracks = m_similarTrackMap.value( key ); 0302 debug() << "for"<<key<<"got similar tracks:" << similarTracks.count(); 0303 } 0304 else 0305 { 0306 locker.unlock(); 0307 newSimilarQuery(); 0308 return; // not yet ready to do construct a query maker 0309 } 0310 } 0311 } 0312 0313 // ok, I need a new query maker 0314 m_qm.reset( CollectionManager::instance()->queryMaker() ); 0315 0316 // - construct the query 0317 m_qm->beginOr(); 0318 if( m_match == SimilarArtist ) 0319 { 0320 foreach( const QString &name, similarArtists ) 0321 { 0322 m_qm->addFilter( Meta::valArtist, name, true, true ); 0323 } 0324 } 0325 else if( m_match == SimilarTrack ) 0326 { 0327 foreach( const TitleArtistPair &name, similarTracks ) 0328 { 0329 m_qm->beginAnd(); 0330 m_qm->addFilter( Meta::valTitle, name.first, true, true ); 0331 m_qm->addFilter( Meta::valArtist, name.second, true, true ); 0332 m_qm->endAndOr(); 0333 } 0334 } 0335 m_qm->endAndOr(); 0336 0337 m_qm->setQueryType( Collections::QueryMaker::Custom ); 0338 m_qm->addReturnValue( Meta::valUniqueId ); 0339 0340 connect( m_qm.data(), &Collections::QueryMaker::newResultReady, 0341 this, &LastFmBias::updateReady ); 0342 connect( m_qm.data(), &Collections::QueryMaker::queryDone, 0343 this, &LastFmBias::updateFinished ); 0344 0345 // - run the query 0346 m_qm->run(); 0347 } 0348 0349 0350 void Dynamic::LastFmBias::newSimilarQuery() 0351 { 0352 DEBUG_BLOCK 0353 0354 QMap< QString, QString > params; 0355 // params[ "limit" ] = "70"; 0356 if( m_match == SimilarArtist ) 0357 { 0358 params[ "method" ] = "artist.getSimilar"; 0359 params[ "artist" ] = m_currentArtist; 0360 QNetworkReply* request = lastfm::ws::get( params ); 0361 connect( request, &QNetworkReply::finished, 0362 this, &LastFmBias::similarArtistQueryDone ); 0363 } 0364 else if( m_match == SimilarTrack ) 0365 { 0366 // if( track->mb 0367 // TODO add mbid if the track has one 0368 params[ "method" ] = "track.getSimilar"; 0369 params[ "artist" ] = m_currentArtist; 0370 params[ "track" ] = m_currentTrack; 0371 QNetworkReply* request = lastfm::ws::get( params ); 0372 connect( request, &QNetworkReply::finished, 0373 this, &LastFmBias::similarTrackQueryDone ); 0374 } 0375 } 0376 0377 0378 void 0379 Dynamic::LastFmBias::similarArtistQueryDone() // slot 0380 { 0381 DEBUG_BLOCK 0382 0383 QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender()); 0384 0385 if( !reply ) 0386 { 0387 queryFailed( "job was deleted from under us...wtf! blame the gerbils." ); 0388 return; 0389 } 0390 reply->deleteLater(); 0391 0392 QByteArray data = reply->readAll(); 0393 // debug() << "artistQuery has data:" << data; 0394 QDomDocument d; 0395 if( !d.setContent( data ) ) 0396 { 0397 queryFailed( "Got invalid XML data from last.fm!" ); 0398 return; 0399 } 0400 0401 QDomNodeList nodes = d.elementsByTagName( "artist" ); 0402 QStringList similarArtists; 0403 for( int i =0; i < nodes.size(); ++i ) 0404 { 0405 QDomElement n = nodes.at( i ).toElement(); 0406 // n.firstChildElement( "match" ).text().toFloat() * 100, 0407 similarArtists.append( n.firstChildElement( "name" ).text() ); 0408 } 0409 0410 QMutexLocker locker( &m_mutex ); 0411 0412 m_similarArtistMap.insert( m_currentArtist, similarArtists ); 0413 0414 saveDataToFile(); 0415 0416 // -- try again to do the query 0417 newQuery(); 0418 } 0419 0420 void Dynamic::LastFmBias::similarTrackQueryDone() 0421 { 0422 DEBUG_BLOCK 0423 0424 QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender()); 0425 0426 if( !reply ) 0427 { 0428 queryFailed( "who send this...wtf! blame the gerbils." ); 0429 return; 0430 } 0431 reply->deleteLater(); 0432 0433 // double match value, qpair title - artist 0434 QMap< int, QPair<QString,QString> > similar; 0435 QByteArray data = reply->readAll(); 0436 // debug() << "trackQuery has data:" << data; 0437 QDomDocument d; 0438 if( !d.setContent( data ) ) 0439 { 0440 queryFailed( "Got invalid XML data from last.fm!" ); 0441 return; 0442 } 0443 0444 QDomNodeList nodes = d.elementsByTagName( "track" ); 0445 QList<TitleArtistPair> similarTracks; 0446 for( int i =0; i < nodes.size(); ++i ) 0447 { 0448 QDomElement n = nodes.at( i ).toElement(); 0449 // n.firstChildElement( "match" ).text().toFloat() * 100, 0450 TitleArtistPair pair( n.firstChildElement( "name" ).text(), 0451 n.firstChildElement( "artist" ).firstChildElement( "name" ).text() ); 0452 similarTracks.append( pair ); 0453 } 0454 0455 QMutexLocker locker( &m_mutex ); 0456 0457 TitleArtistPair key( m_currentTrack, m_currentArtist ); 0458 m_similarTrackMap.insert( key, similarTracks ); 0459 0460 saveDataToFile(); 0461 0462 // -- try again to do the query 0463 newQuery(); 0464 } 0465 0466 0467 void 0468 Dynamic::LastFmBias::queryFailed( const char *message ) 0469 { 0470 debug() << message; 0471 0472 m_tracks.reset( false ); 0473 emit resultReady( m_tracks ); 0474 return; 0475 } 0476 0477 0478 void 0479 Dynamic::LastFmBias::saveDataToFile() const 0480 { 0481 QFile file( Amarok::saveLocation() + "dynamic_lastfm_similar.xml" ); 0482 if( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) 0483 return; 0484 0485 QXmlStreamWriter writer( &file ); 0486 writer.setAutoFormatting( true ); 0487 0488 writer.writeStartDocument(); 0489 writer.writeStartElement( QLatin1String("lastfmSimilar") ); 0490 0491 // -- write the similar artists 0492 foreach( const QString& key, m_similarArtistMap.keys() ) 0493 { 0494 writer.writeStartElement( QLatin1String("similarArtist") ); 0495 writer.writeTextElement( QLatin1String("artist"), key ); 0496 foreach( const QString& name, m_similarArtistMap.value( key ) ) 0497 { 0498 writer.writeTextElement( QLatin1String("similar"), name ); 0499 } 0500 writer.writeEndElement(); 0501 } 0502 0503 // -- write the similar tracks 0504 foreach( const TitleArtistPair& key, m_similarTrackMap.keys() ) 0505 { 0506 writer.writeStartElement( QLatin1String("similarTrack") ); 0507 writer.writeStartElement( QLatin1String("track") ); 0508 writer.writeTextElement( QLatin1String("title"), key.first ); 0509 writer.writeTextElement( QLatin1String("artist"), key.second ); 0510 writer.writeEndElement(); 0511 0512 foreach( const TitleArtistPair& name, m_similarTrackMap.value( key ) ) 0513 { 0514 writer.writeStartElement( QLatin1String("similar") ); 0515 writer.writeTextElement( QLatin1String("title"), name.first ); 0516 writer.writeTextElement( QLatin1String("artist"), name.second ); 0517 writer.writeEndElement(); 0518 } 0519 writer.writeEndElement(); 0520 } 0521 0522 writer.writeEndElement(); 0523 writer.writeEndDocument(); 0524 } 0525 0526 void 0527 Dynamic::LastFmBias::readSimilarArtists( QXmlStreamReader *reader ) 0528 { 0529 QString key; 0530 QList<QString> artists; 0531 0532 while (!reader->atEnd()) { 0533 reader->readNext(); 0534 QStringRef name = reader->name(); 0535 0536 if( reader->isStartElement() ) 0537 { 0538 if( name == QLatin1String("artist") ) 0539 key = reader->readElementText(QXmlStreamReader::SkipChildElements); 0540 else if( name == QLatin1String("similar") ) 0541 artists.append( reader->readElementText(QXmlStreamReader::SkipChildElements) ); 0542 else 0543 reader->skipCurrentElement(); 0544 } 0545 else if( reader->isEndElement() ) 0546 { 0547 break; 0548 } 0549 } 0550 0551 m_similarArtistMap.insert( key, artists ); 0552 } 0553 0554 Dynamic::LastFmBias::TitleArtistPair 0555 Dynamic::LastFmBias::readTrack( QXmlStreamReader *reader ) 0556 { 0557 TitleArtistPair track; 0558 0559 while (!reader->atEnd()) { 0560 reader->readNext(); 0561 QStringRef name = reader->name(); 0562 0563 if( reader->isStartElement() ) 0564 { 0565 if( name == QLatin1String("title") ) 0566 track.first = reader->readElementText(QXmlStreamReader::SkipChildElements); 0567 else if( name == QLatin1String("artist") ) 0568 track.second = reader->readElementText(QXmlStreamReader::SkipChildElements); 0569 else 0570 reader->skipCurrentElement(); 0571 } 0572 else if( reader->isEndElement() ) 0573 { 0574 break; 0575 } 0576 } 0577 0578 return track; 0579 } 0580 0581 void 0582 Dynamic::LastFmBias::readSimilarTracks( QXmlStreamReader *reader ) 0583 { 0584 TitleArtistPair key; 0585 QList<TitleArtistPair> tracks; 0586 0587 while (!reader->atEnd()) { 0588 reader->readNext(); 0589 QStringRef name = reader->name(); 0590 0591 if( reader->isStartElement() ) 0592 { 0593 if( name == QLatin1String("track") ) 0594 key = readTrack( reader ); 0595 else if( name == QLatin1String("similar") ) 0596 tracks.append( readTrack( reader ) ); 0597 else 0598 reader->skipCurrentElement(); 0599 } 0600 else if( reader->isEndElement() ) 0601 { 0602 break; 0603 } 0604 } 0605 0606 m_similarTrackMap.insert( key, tracks ); 0607 } 0608 0609 void 0610 Dynamic::LastFmBias::loadDataFromFile() 0611 { 0612 m_similarArtistMap.clear(); 0613 m_similarTrackMap.clear(); 0614 0615 QFile file( Amarok::saveLocation() + "dynamic_lastfm_similar.xml" ); 0616 0617 if( !file.exists() || 0618 !file.open( QIODevice::ReadOnly ) ) 0619 return; 0620 0621 QXmlStreamReader reader( &file ); 0622 0623 while (!reader.atEnd()) { 0624 reader.readNext(); 0625 0626 QStringRef name = reader.name(); 0627 if( reader.isStartElement() ) 0628 { 0629 if( name == QLatin1String("lastfmSimilar") ) 0630 { 0631 ; // just recurse into the element 0632 } 0633 else if( name == QLatin1String("similarArtist") ) 0634 { 0635 readSimilarArtists( &reader ); 0636 } 0637 else if( name == QLatin1String("similarTrack") ) 0638 { 0639 readSimilarTracks( &reader ); 0640 } 0641 else 0642 { 0643 reader.skipCurrentElement(); 0644 } 0645 } 0646 else if( reader.isEndElement() ) 0647 { 0648 break; 0649 } 0650 } 0651 } 0652 0653 Dynamic::LastFmBias::MatchType 0654 Dynamic::LastFmBias::match() const 0655 { return m_match; } 0656 0657 void 0658 Dynamic::LastFmBias::setMatch( Dynamic::LastFmBias::MatchType value ) 0659 { 0660 m_match = value; 0661 invalidate(); 0662 emit changed( BiasPtr(this) ); 0663 } 0664 0665 void 0666 Dynamic::LastFmBias::setMatchTypeArtist( bool matchArtist ) 0667 { 0668 setMatch( matchArtist ? SimilarArtist : SimilarTrack ); 0669 } 0670 0671 QString 0672 Dynamic::LastFmBias::nameForMatch( Dynamic::LastFmBias::MatchType match ) 0673 { 0674 switch( match ) 0675 { 0676 case SimilarArtist: return "artist"; 0677 case SimilarTrack: return "track"; 0678 } 0679 return QString(); 0680 } 0681 0682 Dynamic::LastFmBias::MatchType 0683 Dynamic::LastFmBias::matchForName( const QString &name ) 0684 { 0685 if( name == "artist" ) return SimilarArtist; 0686 else if( name == "track" ) return SimilarTrack; 0687 else return SimilarArtist; 0688 } 0689 0690 0691 0692