1
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
3
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
5
* Tomahawk is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 3 of the License, or
8
* (at your option) any later version.
10
* Tomahawk is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
19
#include "playlist/dynamic/echonest/EchonestGenerator.h"
20
#include "playlist/dynamic/echonest/EchonestControl.h"
21
#include "playlist/dynamic/echonest/EchonestSteerer.h"
23
#include "utils/TomahawkUtils.h"
24
#include "TomahawkSettings.h"
25
#include "database/DatabaseCommand_CollectionAttributes.h"
26
#include "database/Database.h"
27
#include "utils/Logger.h"
28
#include "SourceList.h"
31
#include <EchonestCatalogSynchronizer.h>
33
using namespace Tomahawk;
36
QStringList EchonestGenerator::s_moods = QStringList();
37
QStringList EchonestGenerator::s_styles = QStringList();
38
QNetworkReply* EchonestGenerator::s_moodsJob = 0;
39
QNetworkReply* EchonestGenerator::s_stylesJob = 0;
41
CatalogManager* EchonestGenerator::s_catalogs = 0;
44
EchonestFactory::EchonestFactory()
50
EchonestFactory::create()
52
return new EchonestGenerator();
57
EchonestFactory::createControl( const QString& controlType )
59
return dyncontrol_ptr( new EchonestControl( controlType, typeSelectors() ) );
64
EchonestFactory::typeSelectors() const
66
QStringList types = QStringList() << "Artist" << "Artist Description" << "User Radio" << "Song" << "Mood" << "Style" << "Adventurousness" << "Variety" << "Tempo" << "Duration" << "Loudness"
67
<< "Danceability" << "Energy" << "Artist Familiarity" << "Artist Hotttnesss" << "Song Hotttnesss"
68
<< "Longitude" << "Latitude" << "Mode" << "Key" << "Sorting";
73
CatalogManager::CatalogManager( QObject* parent )
76
connect( SourceList::instance(), SIGNAL( ready() ), this, SLOT( init() ) );
80
CatalogManager::init()
82
connect( EchonestCatalogSynchronizer::instance(), SIGNAL( knownCatalogsChanged() ), this, SLOT( doCatalogUpdate() ) );
83
connect( SourceList::instance(), SIGNAL( ready() ), this, SLOT( doCatalogUpdate() ) );
89
CatalogManager::collectionAttributes( const PairList& data )
91
QPair<QString, QString> part;
94
foreach ( part, data )
96
if ( SourceList::instance()->get( part.first.toInt() ).isNull() )
99
const QString name = SourceList::instance()->get( part.first.toInt() )->friendlyName();
100
m_catalogs.insert( name, part.second );
103
emit catalogsUpdated();
107
CatalogManager::doCatalogUpdate()
109
QSharedPointer< DatabaseCommand > cmd( new DatabaseCommand_CollectionAttributes( DatabaseCommand_SetCollectionAttributes::EchonestSongCatalog ) );
110
connect( cmd.data(), SIGNAL( collectionAttributes( PairList ) ), this, SLOT( collectionAttributes( PairList ) ) );
111
Database::instance()->enqueue( cmd );
114
QHash< QString, QString >
115
CatalogManager::catalogs() const
121
EchonestGenerator::EchonestGenerator ( QObject* parent )
122
: GeneratorInterface ( parent )
123
, m_dynPlaylist( new Echonest::DynamicPlaylist() )
127
m_logo.load( RESPATH "/images/echonest_logo.png" );
129
loadStylesAndMoods();
131
connect( s_catalogs, SIGNAL( catalogsUpdated() ), this, SLOT( knownCatalogsChanged() ) );
135
EchonestGenerator::~EchonestGenerator()
137
if ( !m_dynPlaylist->sessionId().isNull() )
139
// Running session, delete it
140
QNetworkReply* deleteReply = m_dynPlaylist->deleteSession();
141
connect( deleteReply, SIGNAL( finished() ), deleteReply, SLOT( deleteLater() ) );
144
delete m_dynPlaylist;
148
EchonestGenerator::setupCatalogs()
150
if ( s_catalogs == 0 )
151
s_catalogs = new CatalogManager( 0 );
152
// qDebug() << "ECHONEST:" << m_logo.size();
156
EchonestGenerator::createControl( const QString& type )
158
m_controls << dyncontrol_ptr( new EchonestControl( type, GeneratorFactory::typeSelectors( m_type ) ) );
159
return m_controls.last();
163
QPixmap EchonestGenerator::logo()
169
EchonestGenerator::knownCatalogsChanged()
171
// Refresh all contrls
172
foreach( const dyncontrol_ptr& control, m_controls )
174
control.staticCast< EchonestControl >()->updateWidgetsFromData();
180
EchonestGenerator::generate( int number )
182
// convert to an echonest query, and fire it off
183
qDebug() << Q_FUNC_INFO;
184
qDebug() << "Generating playlist with" << m_controls.size();
185
foreach( const dyncontrol_ptr& ctrl, m_controls )
186
qDebug() << ctrl->selectedType() << ctrl->match() << ctrl->input();
188
setProperty( "number", number ); //HACK
190
connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
194
} catch( std::runtime_error& e ) {
195
qWarning() << "Got invalid controls!" << e.what();
196
emit error( "Filters are not valid", e.what() );
202
EchonestGenerator::startOnDemand()
204
if ( !m_dynPlaylist->sessionId().isNull() )
206
// Running session, delete it
207
QNetworkReply* deleteReply = m_dynPlaylist->deleteSession();
208
connect( deleteReply, SIGNAL( finished() ), deleteReply, SLOT( deleteLater() ) );
211
connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
214
} catch( std::runtime_error& e ) {
215
qWarning() << "Got invalid controls!" << e.what();
216
emit error( "Filters are not valid", e.what() );
222
EchonestGenerator::doGenerate( const Echonest::DynamicPlaylist::PlaylistParams& paramsIn )
224
disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
226
int number = property( "number" ).toInt();
227
setProperty( "number", QVariant() );
229
Echonest::DynamicPlaylist::PlaylistParams params = paramsIn;
230
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Results, number ) );
231
QNetworkReply* reply = Echonest::DynamicPlaylist::staticPlaylist( params );
232
qDebug() << "Generating a static playlist from echonest!" << reply->url().toString();
233
connect( reply, SIGNAL( finished() ), this, SLOT( staticFinished() ) );
238
EchonestGenerator::doStartOnDemand( const Echonest::DynamicPlaylist::PlaylistParams& params )
240
disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
242
QNetworkReply* reply = m_dynPlaylist->create( params );
243
qDebug() << "starting a dynamic playlist from echonest!" << reply->url().toString();
244
connect( reply, SIGNAL( finished() ), this, SLOT( dynamicStarted() ) );
249
EchonestGenerator::fetchNext( int rating )
251
if( m_dynPlaylist->sessionId().isEmpty() ) {
252
// we're not currently playing, oops!
253
qWarning() << Q_FUNC_INFO << "asked to fetch next dynamic song when we're not in the middle of a playlist!";
259
Echonest::DynamicPlaylist::DynamicFeedback feedback;
260
feedback.append( Echonest::DynamicPlaylist::DynamicFeedbackParamData( Echonest::DynamicPlaylist::RateSong, QString( "last^%1").arg( rating * 2 ).toUtf8() ) );
261
QNetworkReply* reply = m_dynPlaylist->feedback( feedback );
262
connect( reply, SIGNAL( finished() ), reply, SLOT( deleteLater() ) ); // we don't care about the result, just send it off
265
QNetworkReply* reply = m_dynPlaylist->next( 1, 0 );
266
qDebug() << "getting next song from echonest" << reply->url().toString();
267
connect( reply, SIGNAL( finished() ), this, SLOT( dynamicFetched() ) );
272
EchonestGenerator::staticFinished()
274
Q_ASSERT( sender() );
275
Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
277
QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
279
Echonest::SongList songs;
281
songs = Echonest::DynamicPlaylist::parseStaticPlaylist( reply );
282
} catch( const Echonest::ParseError& e ) {
283
qWarning() << "libechonest threw an error trying to parse the static playlist code" << e.errorType() << "error desc:" << e.what();
285
emit error( "The Echo Nest returned an error creating the playlist", e.what() );
289
QList< query_ptr > queries;
290
foreach( const Echonest::Song& song, songs ) {
291
qDebug() << "EchonestGenerator got song:" << song;
292
queries << queryFromSong( song );
295
emit generated( queries );
300
EchonestGenerator::getParams() throw( std::runtime_error )
302
Echonest::DynamicPlaylist::PlaylistParams params;
303
foreach( const dyncontrol_ptr& control, m_controls ) {
304
params.append( control.dynamicCast<EchonestControl>()->toENParam() );
307
if( appendRadioType( params ) == Echonest::DynamicPlaylist::SongRadioType ) {
308
// we need to do another pass, converting all song queries to song-ids.
309
m_storedParams = params;
310
qDeleteAll( m_waiting );
313
// one query per track
314
for( int i = 0; i < params.count(); i++ ) {
315
const Echonest::DynamicPlaylist::PlaylistParamData param = params.value( i );
317
if( param.first == Echonest::DynamicPlaylist::SongId ) { // this is a song type enum
318
QString text = param.second.toString();
320
Echonest::Song::SearchParams q;
321
q.append( Echonest::Song::SearchParamData( Echonest::Song::Combined, text ) ); // search with the free text "combined" parameter
322
QNetworkReply* r = Echonest::Song::search( q );
323
r->setProperty( "index", i );
324
r->setProperty( "search", text );
326
m_waiting.insert( r );
327
connect( r, SIGNAL( finished() ), this, SLOT( songLookupFinished() ) );
331
if( m_waiting.isEmpty() ) {
332
m_storedParams.clear();
333
emit paramsGenerated( params );
337
emit paramsGenerated( params );
343
EchonestGenerator::songLookupFinished()
345
QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
347
if( !m_waiting.contains( r ) ) // another generate/start was begun meanwhile, we're out of date
351
m_waiting.remove( r );
353
QString search = r->property( "search" ).toString();
356
Echonest::SongList songs = Echonest::Song::parseSearch( r );
357
if( songs.size() > 0 ) {
358
id = songs.first().id();
359
qDebug() << "Got ID for song:" << songs.first() << "from search:" << search;;
361
qDebug() << "Got no songs from our song id lookup.. :(. We looked for:" << search;
363
} catch( Echonest::ParseError& e ) {
364
qWarning() << "Failed to parse song/search result:" << e.errorType() << e.what();
366
int idx = r->property( "index" ).toInt();
367
Q_ASSERT( m_storedParams.count() >= idx );
369
// replace the song text with the song id in-place
370
m_storedParams[ idx ].second = id;
372
if( m_waiting.isEmpty() ) { // we're done!
373
emit paramsGenerated( m_storedParams );
379
EchonestGenerator::dynamicStarted()
381
Q_ASSERT( sender() );
382
Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
383
QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
387
m_dynPlaylist->parseCreate( reply );
389
} catch( const Echonest::ParseError& e ) {
390
qWarning() << "libechonest threw an error parsing the start of the dynamic playlist:" << e.errorType() << e.what();
391
emit error( "The Echo Nest returned an error starting the station", e.what() );
397
EchonestGenerator::dynamicFetched()
399
Q_ASSERT( sender() );
400
Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
401
QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
405
Echonest::DynamicPlaylist::FetchPair fetched = m_dynPlaylist->parseNext( reply );
407
if ( fetched.first.size() != 1 )
409
qWarning() << "Did not get any track when looking up the next song from the echo nest!";
410
emit error( "No more songs from The Echo Nest available in the station", "" );
414
query_ptr songQuery = queryFromSong( fetched.first.first() );
415
emit nextTrackGenerated( songQuery );
416
} catch( const Echonest::ParseError& e ) {
417
qWarning() << "libechonest threw an error parsing the next song of the dynamic playlist:" << e.errorType() << e.what();
418
emit error( "The Echo Nest returned an error getting the next song", e.what() );
424
EchonestGenerator::catalogId(const QString &collectionId)
426
return s_catalogs->catalogs().value( collectionId ).toUtf8();
430
EchonestGenerator::userCatalogs()
432
return s_catalogs->catalogs().keys();
436
EchonestGenerator::onlyThisArtistType( Echonest::DynamicPlaylist::ArtistTypeEnum type ) const throw( std::runtime_error )
441
foreach( const dyncontrol_ptr& control, m_controls ) {
442
if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) != type ) {
444
} else if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) == type ) {
450
} else if( some && !only ) {
451
throw std::runtime_error( "All artist and song match types must be the same" );
458
Echonest::DynamicPlaylist::ArtistTypeEnum
459
EchonestGenerator::appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error )
462
* So we try to match the best type of echonest playlist, based on the controls
463
* the types are artist, artist-radio, artist-description, catalog, catalog-radio, song-radio. we don't care about the catalog ones
467
/// 1. catalog-radio: If any the entries are catalog types.
468
/// 2. artist: If all the artist controls are Limit-To. If some were but not all, error out.
469
/// 3. artist-description: If all the artist entries are Description. If some were but not all, error out.
470
/// 4. artist-radio: If all the artist entries are Similar To. If some were but not all, error out.
471
/// 5. song-radio: If all the artist entries are Similar To. If some were but not all, error out.
472
bool someCatalog = false;
473
foreach( const dyncontrol_ptr& control, m_controls ) {
474
if ( control->selectedType() == "User Radio" )
478
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::CatalogRadioType ) );
479
else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistType ) )
480
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistType ) );
481
else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistDescriptionType ) )
482
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistDescriptionType ) );
483
else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistRadioType ) )
484
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistRadioType ) );
485
else if( onlyThisArtistType( Echonest::DynamicPlaylist::SongRadioType ) )
486
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::SongRadioType ) );
487
else // no artist or song or description types. default to artist-description
488
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistDescriptionType ) );
490
return static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( params.last().second.toInt() );
495
EchonestGenerator::queryFromSong( const Echonest::Song& song )
497
// track[ "album" ] = song.release(); // TODO should we include it? can be quite specific
498
return Query::get( song.artistName(), song.title(), QString(), uuid(), false );
503
EchonestGenerator::sentenceSummary()
506
* The idea is we generate an english sentence from the individual phrases of the controls. We have to follow a few rules, but othewise it's quite straightforward.
509
* - Sentence starts with "Songs "
510
* - Artists always go first
511
* - Separate phrases by comma, and before last phrase
512
* - sorting always at end
513
* - collapse artists. "Like X, like Y, like Z, ..." -> "Like X, Y, and Z"
514
* - skip empty artist entries
516
* NOTE / TODO: In order for the sentence to be grammatically correct, we must follow the EN API rules. That means we can't have multiple of some types of filters,
517
* and all Artist types must be the same. The filters aren't checked at the moment until Generate / Play is pressed. Consider doing a check on hide as well.
519
QList< dyncontrol_ptr > allcontrols = m_controls;
520
QString sentence = "Songs ";
522
/// 1. Collect all required filters
523
/// 2. Get the sorted by filter if it exists.
524
QList< dyncontrol_ptr > required;
525
dyncontrol_ptr sorting;
526
foreach( const dyncontrol_ptr& control, allcontrols ) {
527
if( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" )
529
else if( control->selectedType() == "Sorting" )
532
if( !sorting.isNull() )
533
allcontrols.removeAll( sorting );
535
/// Skip empty artists
536
QList< dyncontrol_ptr > empty;
537
foreach( const dyncontrol_ptr& artistOrTrack, required ) {
538
QString summary = artistOrTrack.dynamicCast< EchonestControl >()->summary();
539
if( summary.lastIndexOf( "~" ) == summary.length() - 1 )
540
empty << artistOrTrack;
542
foreach( const dyncontrol_ptr& toremove, empty ) {
543
required.removeAll( toremove );
544
allcontrols.removeAll( toremove );
547
/// If there are no artists and no filters, show some help text
548
if( required.isEmpty() && allcontrols.isEmpty() )
549
sentence = "No configured filters!";
551
/// Do the assembling. Start with the artists if there are any, then do all the rest.
552
for( int i = 0; i < required.size(); i++ ) {
553
dyncontrol_ptr artist = required.value( i );
554
allcontrols.removeAll( artist ); // remove from pool while we're here
556
/// Collapse artist lists
557
QString center, suffix;
558
QString summary = artist.dynamicCast< EchonestControl >()->summary();
560
if( i == 0 ) { // if it's the first.. special casez
561
center = summary.remove( "~" );
562
if( required.size() == 2 ) // special case for 2, no comma. ( X and Y )
564
else if( required.size() > 2 ) // in a list with more after
566
else if( allcontrols.isEmpty() && sorting.isNull() ) // the last one, and no more controls, so put a period
571
center = summary.mid( summary.indexOf( "~" ) + 1 );
572
if( i == required.size() - 1 ) { // if there are more, add an " and "
573
if( !( allcontrols.isEmpty() && sorting.isNull() ) )
577
} else if ( i < required.size() - 2 ) // An item in the list that is before the second to last one, don't use ", and", we only want that for the last item
582
sentence += center + suffix;
584
/// Add each filter individually
585
for( int i = 0; i < allcontrols.size(); i++ ) {
586
/// end case: if this is the last AND there is not a sorting filter (so this is the real last one)
587
const bool last = ( i == allcontrols.size() - 1 && sorting.isNull() );
588
QString prefix, suffix;
589
if( last ) { // only if there is not just 1
590
if( !( required.isEmpty() && allcontrols.size() == 1 ) )
595
sentence += prefix + allcontrols.value( i ).dynamicCast< EchonestControl >()->summary() + suffix;
598
if( !sorting.isNull() ) {
599
sentence += "and " + sorting.dynamicCast< EchonestControl >()->summary() + ".";
606
EchonestGenerator::loadStylesAndMoods()
608
if( !s_styles.isEmpty() || !s_moods.isEmpty() )
611
QFile dataFile( TomahawkUtils::appDataDir().absoluteFilePath( "echonest_stylesandmoods.dat" ) );
612
if( !dataFile.exists() ) // load
614
s_stylesJob = Echonest::Artist::listTerms( "style" );
615
connect( s_stylesJob, SIGNAL( finished() ), this, SLOT( stylesReceived() ) );
616
s_moodsJob = Echonest::Artist::listTerms( "mood" );
617
connect( s_moodsJob, SIGNAL( finished() ), this, SLOT( moodsReceived() ) );
620
if( !dataFile.open( QIODevice::ReadOnly ) )
622
tLog() << "Failed to open for reading styles/moods db file:" << dataFile.fileName();
626
QString allData = QString::fromUtf8( dataFile.readAll() );
627
QStringList parts = allData.split( "\n" );
628
if( parts.size() != 2 )
630
tLog() << "Didn't get both moods and styles in file...:" << allData;
633
s_moods = parts[ 0 ].split( "|" );
634
s_styles = parts[ 1 ].split( "|" );
639
EchonestGenerator::saveStylesAndMoods()
641
QFile dataFile( TomahawkUtils::appDataDir().absoluteFilePath( "echonest_stylesandmoods.dat" ) );
642
if( !dataFile.open( QIODevice::WriteOnly ) )
644
tLog() << "Failed to open styles and moods data file for saving:" << dataFile.errorString() << dataFile.fileName();
648
QByteArray data = QString( "%1\n%2" ).arg( s_moods.join( "|" ) ).arg( s_styles.join( "|" ) ).toUtf8();
649
dataFile.write( data );
655
EchonestGenerator::moods()
662
EchonestGenerator::moodsReceived()
664
QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
668
s_moods = Echonest::Artist::parseTermList( r ).toList();
669
} catch( Echonest::ParseError& e ) {
670
qWarning() << "Echonest failed to parse moods list";
674
if( !s_styles.isEmpty() )
675
saveStylesAndMoods();
680
EchonestGenerator::styles()
687
EchonestGenerator::stylesReceived()
689
QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
693
s_styles = Echonest::Artist::parseTermList( r ).toList();
694
} catch( Echonest::ParseError& e ) {
695
qWarning() << "Echonest failed to parse styles list";
699
if( !s_moods.isEmpty() )
700
saveStylesAndMoods();