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