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