From 2f5dd43e5bb6d09b7ac87b9d1a4fc09f4fb2b7d7 Mon Sep 17 00:00:00 2001
From: Leo Franchi <lfranchi@kde.org>
Date: Mon, 23 May 2011 20:41:19 -0400
Subject: [PATCH] add a Song search type to dynamic playlists.

---
 .../dynamic/echonest/EchonestControl.cpp      |  24 ++-
 .../dynamic/echonest/EchonestGenerator.cpp    | 164 +++++++++++++-----
 .../dynamic/echonest/EchonestGenerator.h      |  18 +-
 3 files changed, 163 insertions(+), 43 deletions(-)

diff --git a/src/libtomahawk/playlist/dynamic/echonest/EchonestControl.cpp b/src/libtomahawk/playlist/dynamic/echonest/EchonestControl.cpp
index aea71a78e..c4889fab1 100644
--- a/src/libtomahawk/playlist/dynamic/echonest/EchonestControl.cpp
+++ b/src/libtomahawk/playlist/dynamic/echonest/EchonestControl.cpp
@@ -172,6 +172,24 @@ Tomahawk::EchonestControl::updateWidgets()
         connect( input, SIGNAL( textEdited( QString ) ), &m_editingTimer, SLOT( stop() ) );
         connect( input, SIGNAL( textEdited( QString ) ), &m_delayedEditTimer, SLOT( start() ) );
 
+        match->hide();
+        input->hide();
+        m_match = QWeakPointer< QWidget >( match );
+        m_input = QWeakPointer< QWidget >( input );
+    } else if( selectedType() == "Song" ) {
+        m_currentType = Echonest::DynamicPlaylist::SongId;
+
+        QLabel* match = new QLabel( tr( "similar to" ) );
+        QLineEdit* input =  new QLineEdit();
+
+        m_matchString = QString();
+        m_matchData = QString::number( (int)Echonest::DynamicPlaylist::SongRadioType );
+
+        connect( input, SIGNAL( textChanged(QString) ), this, SLOT( updateData() ) );
+        connect( input, SIGNAL( editingFinished() ), this, SLOT( editingFinished() ) );
+        connect( input, SIGNAL( textEdited( QString ) ), &m_editingTimer, SLOT( stop() ) );
+        connect( input, SIGNAL( textEdited( QString ) ), &m_delayedEditTimer, SLOT( start() ) );
+
         match->hide();
         input->hide();
         m_match = QWeakPointer< QWidget >( match );
@@ -394,7 +412,7 @@ Tomahawk::EchonestControl::updateData()
             m_data.first = m_currentType;
             m_data.second = edit->text();
         }
-    } else if( selectedType() == "Artist Description" ) {
+    } else if( selectedType() == "Artist Description" || selectedType() == "Song" ) {
         QLineEdit* edit = qobject_cast<QLineEdit*>( m_input.data() );
         if( edit && !edit->text().isEmpty() ) {
             m_data.first = m_currentType;
@@ -467,7 +485,7 @@ Tomahawk::EchonestControl::updateWidgetsFromData()
         QLineEdit* edit = qobject_cast<QLineEdit*>( m_input.data() );
         if( edit )
             edit->setText( m_data.second.toString() );
-    } else if( selectedType() == "Artist Description" ) {
+    } else if( selectedType() == "Artist Description" || selectedType() == "Song" ) {
         QLineEdit* edit = qobject_cast<QLineEdit*>( m_input.data() );
         if( edit )
             edit->setText( m_data.second.toString() );
@@ -547,6 +565,8 @@ Tomahawk::EchonestControl::calculateSummary()
             summary = QString( "similar to ~%1" ).arg( m_data.second.toString() );
     } else if( selectedType() == "Artist Description" ) {
         summary = QString( "with genre ~%1" ).arg( m_data.second.toString() );
+    } else if( selectedType() == "Artist Description" ) {
+        summary = QString( "similar to ~%1" ).arg( m_data.second.toString() );
     } else if( selectedType() == "Variety" || selectedType() == "Danceability" || selectedType() == "Artist Hotttnesss" || selectedType() == "Energy" || selectedType() == "Artist Familiarity" || selectedType() == "Song Hotttnesss" ) {
         QString modifier;
         qreal sliderVal = m_data.second.toReal();
diff --git a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp
index 428364f15..86ceb8d25 100644
--- a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp
+++ b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp
@@ -90,19 +90,18 @@ QPixmap EchonestGenerator::logo()
 void
 EchonestGenerator::generate( int number )
 {
-   // convert to an echonest query, and fire it off
-   qDebug() << Q_FUNC_INFO;
-   qDebug() << "Generating playlist with" << m_controls.size();
-   foreach( const dyncontrol_ptr& ctrl, m_controls )
-       qDebug() << ctrl->selectedType() << ctrl->match() << ctrl->input();
+    // convert to an echonest query, and fire it off
+    qDebug() << Q_FUNC_INFO;
+    qDebug() << "Generating playlist with" << m_controls.size();
+    foreach( const dyncontrol_ptr& ctrl, m_controls )
+        qDebug() << ctrl->selectedType() << ctrl->match() << ctrl->input();
+
+    setProperty( "number", number ); //HACK
+
+    connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
 
     try {
-        Echonest::DynamicPlaylist::PlaylistParams params = getParams();
-
-        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Results, number ) );
-        QNetworkReply* reply = Echonest::DynamicPlaylist::staticPlaylist( params );
-        qDebug() << "Generating a static playlist from echonest!" << reply->url().toString();
-        connect( reply, SIGNAL( finished() ), this, SLOT( staticFinished() ) );
+        getParams();
     } catch( std::runtime_error& e ) {
         qWarning() << "Got invalid controls!" << e.what();
         emit error( "Filters are not valid", e.what() );
@@ -112,18 +111,41 @@ EchonestGenerator::generate( int number )
 void
 EchonestGenerator::startOnDemand()
 {
+    connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
     try {
-        Echonest::DynamicPlaylist::PlaylistParams params = getParams();
-
-        QNetworkReply* reply = m_dynPlaylist->start( params );
-        qDebug() << "starting a dynamic playlist from echonest!" << reply->url().toString();
-        connect( reply, SIGNAL( finished() ), this, SLOT( dynamicStarted() ) );
+        getParams();
     } catch( std::runtime_error& e ) {
         qWarning() << "Got invalid controls!" << e.what();
         emit error( "Filters are not valid", e.what() );
     }
 }
 
+void
+EchonestGenerator::doGenerate( const Echonest::DynamicPlaylist::PlaylistParams& paramsIn )
+{
+    disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
+
+    int number = property( "number" ).toInt();
+    setProperty( "number", QVariant() );
+
+    Echonest::DynamicPlaylist::PlaylistParams params = paramsIn;
+    params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Results, number ) );
+    QNetworkReply* reply = Echonest::DynamicPlaylist::staticPlaylist( params );
+    qDebug() << "Generating a static playlist from echonest!" << reply->url().toString();
+    connect( reply, SIGNAL( finished() ), this, SLOT( staticFinished() ) );
+}
+
+void
+EchonestGenerator::doStartOnDemand( const Echonest::DynamicPlaylist::PlaylistParams& params )
+{
+    disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
+
+    QNetworkReply* reply = m_dynPlaylist->start( params );
+    qDebug() << "starting a dynamic playlist from echonest!" << reply->url().toString();
+    connect( reply, SIGNAL( finished() ), this, SLOT( dynamicStarted() ) );
+}
+
+
 void
 EchonestGenerator::fetchNext( int rating )
 {
@@ -173,17 +195,78 @@ EchonestGenerator::staticFinished()
     emit generated( queries );
 }
 
-Echonest::DynamicPlaylist::PlaylistParams
-EchonestGenerator::getParams() const throw( std::runtime_error )
+void
+EchonestGenerator::getParams() throw( std::runtime_error )
 {
     Echonest::DynamicPlaylist::PlaylistParams params;
     foreach( const dyncontrol_ptr& control, m_controls ) {
         params.append( control.dynamicCast<EchonestControl>()->toENParam() );
     }
-    appendRadioType( params );
-    return params;
+
+    if( appendRadioType( params ) == Echonest::DynamicPlaylist::SongRadioType ) {
+        // we need to do another pass, converting all song queries to song-ids.
+        m_storedParams = params;
+        qDeleteAll( m_waiting );
+        m_waiting.clear();
+
+        // one query per track
+        for( int i = 0; i < params.count(); i++ ) {
+            const Echonest::DynamicPlaylist::PlaylistParamData param = params.value( i );
+
+            if( param.first == Echonest::DynamicPlaylist::SongId ) { // this is a song type enum
+                QString text = param.second.toString();
+
+                Echonest::Song::SearchParams q;
+                q.append( Echonest::Song::SearchParamData( Echonest::Song::Combined, text ) ); // search with the free text "combined" parameter
+                QNetworkReply* r = Echonest::Song::search( q );
+                r->setProperty( "index", i );
+                r->setProperty( "search", text );
+
+                m_waiting.insert( r );
+                connect( r, SIGNAL( finished() ), this, SLOT( songLookupFinished() ) );
+            }
+        }
+    } else {
+        emit paramsGenerated( params );
+    }
 }
 
+void
+EchonestGenerator::songLookupFinished()
+{
+    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
+
+    if( !m_waiting.contains( r ) ) // another generate/start was begun meanwhile, we're out of date
+        return;
+
+    Q_ASSERT( r );
+    m_waiting.remove( r );
+
+    QString search = r->property( "search" ).toString();
+    QByteArray id;
+    try {
+        Echonest::SongList songs = Echonest::Song::parseSearch( r );
+        if( songs.size() > 0 ) {
+            id = songs.first().id();
+            qDebug() << "Got ID for song:" << songs.first() << "from search:" << search;;
+        } else {
+            qDebug() << "Got no songs from our song id lookup.. :(. We looked for:" << search;
+        }
+    } catch( Echonest::ParseError& e ) {
+        qWarning() << "Failed to parse song/search result:" << e.errorType() << e.what();
+    }
+    int idx = r->property( "index" ).toInt();
+    Q_ASSERT( m_storedParams.count() >= idx );
+
+    // replace the song text with the song id in-place
+    m_storedParams[ idx ].second = id;
+
+    if( m_waiting.isEmpty() ) { // we're done!
+        emit paramsGenerated( m_storedParams );
+    }
+}
+
+
 void
 EchonestGenerator::dynamicStarted()
 {
@@ -257,9 +340,9 @@ EchonestGenerator::onlyThisArtistType( Echonest::DynamicPlaylist::ArtistTypeEnum
     bool some = false;
 
     foreach( const dyncontrol_ptr& control, m_controls ) {
-        if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) != type ) {
+        if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) != type ) {
             only = false;
-        } else if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) == type ) {
+        } else if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) == type ) {
             some = true;
         }
     }
@@ -272,26 +355,29 @@ EchonestGenerator::onlyThisArtistType( Echonest::DynamicPlaylist::ArtistTypeEnum
     return false;
 }
 
-void
+Echonest::DynamicPlaylist::ArtistTypeEnum
 EchonestGenerator::appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error )
 {
     /**
      * So we try to match the best type of echonest playlist, based on the controls
-     * the types are artist, artist-radio, artist-description, catalog, catalog-radio, song-radio. we don't care about the catalog ones, and
-     * we can't use the song ones since for the moment EN only accepts Song IDs, not names, and we don't want to insert an extra song.search
-     * call first.
+     * the types are artist, artist-radio, artist-description, catalog, catalog-radio, song-radio. we don't care about the catalog ones
      *
      */
 
     /// 1. artist: If all the artist controls are Limit-To. If some were but not all, error out.
     /// 2. artist-description: If all the artist entries are Description. If some were but not all, error out.
     /// 3. artist-radio: If all the artist entries are Similar To. If some were but not all, error out.
+    /// 4. song-radio: If all the artist entries are Similar To. If some were but not all, error out.
     if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistType ) )
         params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistType ) );
     else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistDescriptionType ) )
         params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistDescriptionType ) );
     else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistRadioType ) )
         params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistRadioType ) );
+    else if( onlyThisArtistType( Echonest::DynamicPlaylist::SongRadioType ) )
+        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::SongRadioType ) );
+
+    return static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( params.last().second.toInt() );
 }
 
 query_ptr
@@ -336,13 +422,13 @@ EchonestGenerator::sentenceSummary()
     QList< dyncontrol_ptr > allcontrols = m_controls;
     QString sentence = "Songs ";
 
-    /// 1. Collect all artist filters
+    /// 1. Collect all required filters
     /// 2. Get the sorted by filter if it exists.
-    QList< dyncontrol_ptr > artists;
+    QList< dyncontrol_ptr > required;
     dyncontrol_ptr sorting;
     foreach( const dyncontrol_ptr& control, allcontrols ) {
-        if( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" )
-            artists << control;
+        if( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" )
+            required << control;
         else if( control->selectedType() == "Sorting" )
             sorting = control;
     }
@@ -351,23 +437,23 @@ EchonestGenerator::sentenceSummary()
 
     /// Skip empty artists
     QList< dyncontrol_ptr > empty;
-    foreach( const dyncontrol_ptr& artist, artists ) {
+    foreach( const dyncontrol_ptr& artist, required ) {
         QString summary = artist.dynamicCast< EchonestControl >()->summary();
         if( summary.lastIndexOf( "~" ) == summary.length() - 1 )
             empty << artist;
     }
     foreach( const dyncontrol_ptr& toremove, empty ) {
-        artists.removeAll( toremove );
+        required.removeAll( toremove );
         allcontrols.removeAll( toremove );
     }
 
     /// If there are no artists and no filters, show some help text
-    if( artists.isEmpty() && allcontrols.isEmpty() )
+    if( required.isEmpty() && allcontrols.isEmpty() )
         sentence = "No configured filters!";
 
     /// Do the assembling. Start with the artists if there are any, then do all the rest.
-    for( int i = 0; i < artists.size(); i++ ) {
-        dyncontrol_ptr artist = artists.value( i );
+    for( int i = 0; i < required.size(); i++ ) {
+        dyncontrol_ptr artist = required.value( i );
         allcontrols.removeAll( artist ); // remove from pool while we're here
 
         /// Collapse artist lists
@@ -376,9 +462,9 @@ EchonestGenerator::sentenceSummary()
 
         if( i == 0 ) { // if it's the first.. special casez
             center = summary.remove( "~" );
-            if( artists.size() == 2 ) // special case for 2, no comma. ( X and Y )
+            if( required.size() == 2 ) // special case for 2, no comma. ( X and Y )
                 suffix = " and ";
-            else if( artists.size() > 2 ) // in a list with more after
+            else if( required.size() > 2 ) // in a list with more after
                 suffix = ", ";
             else if( allcontrols.isEmpty() && sorting.isNull() ) // the last one, and no more controls, so put a period
                 suffix = ".";
@@ -386,7 +472,7 @@ EchonestGenerator::sentenceSummary()
                 suffix = " ";
         } else {
             center = summary.mid( summary.indexOf( "~" ) + 1 );
-            if( i == artists.size() - 1 ) { // if there are more, add an " and "
+            if( i == required.size() - 1 ) { // if there are more, add an " and "
                 if( !( allcontrols.isEmpty() && sorting.isNull() ) )
                     suffix = ", ";
                 else
@@ -402,7 +488,7 @@ EchonestGenerator::sentenceSummary()
         const bool last = ( i == allcontrols.size() - 1 && sorting.isNull() );
         QString prefix, suffix;
         if( last ) { // only if there is not just 1
-            if( !( artists.isEmpty() && allcontrols.size() == 1 ) )
+            if( !( required.isEmpty() && allcontrols.size() == 1 ) )
                 prefix = "and ";
             suffix = ".";
         } else
diff --git a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h
index 2fb1d0ba4..1af9fc12c 100644
--- a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h
+++ b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h
@@ -61,6 +61,10 @@ public:
 
     static QVector< QString > styles();
     static QVector< QString > moods();
+
+signals:
+    void paramsGenerated( const Echonest::DynamicPlaylist::PlaylistParams& );
+
 private slots:
     void staticFinished();
     void dynamicStarted();
@@ -71,13 +75,19 @@ private slots:
     void steerDescription( const QString& desc );
     void resetSteering();
 
+    void doGenerate( const Echonest::DynamicPlaylist::PlaylistParams& params );
+    void doStartOnDemand( const Echonest::DynamicPlaylist::PlaylistParams& params );
+
     void stylesReceived();
     void moodsReceived();
 
+    void songLookupFinished();
 private:
-    Echonest::DynamicPlaylist::PlaylistParams getParams() const throw( std::runtime_error );
+    // get result from signal paramsGenerated
+    void getParams() throw( std::runtime_error );
+
     query_ptr queryFromSong( const Echonest::Song& song );
-    void appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error );
+    Echonest::DynamicPlaylist::ArtistTypeEnum appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error );
     bool onlyThisArtistType( Echonest::DynamicPlaylist::ArtistTypeEnum type ) const throw( std::runtime_error );
 
     Echonest::DynamicPlaylist* m_dynPlaylist;
@@ -86,6 +96,10 @@ private:
     static QVector< QString > s_styles;
     static QVector< QString > s_moods;
 
+    // used for the intermediary song id lookup
+    QSet< QNetworkReply* > m_waiting;
+    Echonest::DynamicPlaylist::PlaylistParams m_storedParams;
+
     QWeakPointer<EchonestSteerer> m_steerer;
     bool m_steeredSinceLastTrack;
     Echonest::DynamicPlaylist::DynamicControl m_steerData;