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;