PageRenderTime 834ms CodeModel.GetById 341ms app.highlight 282ms RepoModel.GetById 91ms app.codeStats 0ms

/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 818 lines | 628 code | 129 blank | 61 comment | 113 complexity | 298f3ec6c9321878880af31e4543e6c0 MD5 | raw file
  1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2 *
  3 *   Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
  4 *   Copyright 2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  5 *
  6 *   Tomahawk is free software: you can redistribute it and/or modify
  7 *   it under the terms of the GNU General Public License as published by
  8 *   the Free Software Foundation, either version 3 of the License, or
  9 *   (at your option) any later version.
 10 *
 11 *   Tomahawk is distributed in the hope that it will be useful,
 12 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 13 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14 *   GNU General Public License for more details.
 15 *
 16 *   You should have received a copy of the GNU General Public License
 17 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 18 */
 19
 20#include "playlist/dynamic/echonest/EchonestGenerator.h"
 21#include "playlist/dynamic/echonest/EchonestControl.h"
 22#include "playlist/dynamic/echonest/EchonestSteerer.h"
 23#include "Query.h"
 24#include "utils/TomahawkUtils.h"
 25#include "utils/TomahawkCache.h"
 26#include "TomahawkSettings.h"
 27#include "database/DatabaseCommand_CollectionAttributes.h"
 28#include "database/Database.h"
 29#include "utils/Logger.h"
 30#include "SourceList.h"
 31
 32#include <QFile>
 33#include <QDir>
 34#include <QReadWriteLock>
 35#include <EchonestCatalogSynchronizer.h>
 36
 37using namespace Tomahawk;
 38
 39
 40QStringList EchonestGenerator::s_moods = QStringList();
 41QStringList EchonestGenerator::s_styles = QStringList();
 42QStringList EchonestGenerator::s_genres = QStringList();
 43QNetworkReply* EchonestGenerator::s_moodsJob = 0;
 44QNetworkReply* EchonestGenerator::s_stylesJob = 0;
 45QNetworkReply* EchonestGenerator::s_genresJob = 0;
 46
 47static QReadWriteLock s_moods_lock;
 48static QReadWriteLock s_styles_lock;
 49static QReadWriteLock s_genres_lock;
 50
 51CatalogManager* EchonestGenerator::s_catalogs = 0;
 52
 53
 54EchonestFactory::EchonestFactory()
 55{
 56}
 57
 58
 59GeneratorInterface*
 60EchonestFactory::create()
 61{
 62    return new EchonestGenerator();
 63}
 64
 65
 66dyncontrol_ptr
 67EchonestFactory::createControl( const QString& controlType )
 68{
 69    return dyncontrol_ptr( new EchonestControl( controlType, typeSelectors() ) );
 70}
 71
 72
 73QStringList
 74EchonestFactory::typeSelectors() const
 75{
 76    // Using QT_TRANSLATE_NOOP here because this function should return the untranslated types
 77    QStringList types =  QStringList() << QT_TRANSLATE_NOOP( "Type selector", "Artist" ) << QT_TRANSLATE_NOOP( "Type selector", "Artist Description" )
 78                          << QT_TRANSLATE_NOOP( "Type selector", "User Radio" ) << QT_TRANSLATE_NOOP( "Type selector", "Song" )
 79                          << QT_TRANSLATE_NOOP( "Type selector", "Genre" ) << QT_TRANSLATE_NOOP( "Type selector", "Mood" )
 80                          << QT_TRANSLATE_NOOP( "Type selector", "Style" ) << QT_TRANSLATE_NOOP( "Type selector", "Adventurousness" )
 81                          << QT_TRANSLATE_NOOP( "Type selector", "Variety" ) << QT_TRANSLATE_NOOP( "Type selector", "Tempo" )
 82                          << QT_TRANSLATE_NOOP( "Type selector", "Duration" ) << QT_TRANSLATE_NOOP( "Type selector", "Loudness" )
 83                          << QT_TRANSLATE_NOOP( "Type selector", "Danceability" ) << QT_TRANSLATE_NOOP( "Type selector", "Energy" )
 84                          << QT_TRANSLATE_NOOP( "Type selector", "Artist Familiarity" ) << QT_TRANSLATE_NOOP( "Type selector", "Artist Hotttnesss" )
 85                          << QT_TRANSLATE_NOOP( "Type selector", "Song Hotttnesss" ) << QT_TRANSLATE_NOOP( "Type selector", "Longitude" )
 86                          << QT_TRANSLATE_NOOP( "Type selector", "Latitude" ) << QT_TRANSLATE_NOOP( "Type selector", "Mode" )
 87                          << QT_TRANSLATE_NOOP( "Type selector", "Key" ) << QT_TRANSLATE_NOOP( "Type selector", "Sorting" )
 88                          << QT_TRANSLATE_NOOP( "Type selector", "Song Type" ) << QT_TRANSLATE_NOOP( "Type selector", "Distribution" )
 89                          << QT_TRANSLATE_NOOP( "Type selector", "Genre Preset" );
 90
 91    return types;
 92}
 93
 94CatalogManager::CatalogManager( QObject* parent )
 95    : QObject( parent )
 96{
 97    connect( SourceList::instance(), SIGNAL( ready() ), this, SLOT( init() ) );
 98}
 99
100void
101CatalogManager::init()
102{
103    connect( EchonestCatalogSynchronizer::instance(), SIGNAL( knownCatalogsChanged() ), this, SLOT( doCatalogUpdate() ) );
104    connect( SourceList::instance(), SIGNAL( ready() ), this, SLOT( doCatalogUpdate() ) );
105
106    doCatalogUpdate();
107}
108
109void
110CatalogManager::collectionAttributes( const PairList& data )
111{
112    QPair<QString, QString> part;
113    m_catalogs.clear();
114
115    foreach ( part, data )
116    {
117        if ( SourceList::instance()->get( part.first.toInt() ).isNull() )
118            continue;
119
120        const QString name = SourceList::instance()->get( part.first.toInt() )->friendlyName();
121        m_catalogs.insert( name, part.second );
122    }
123
124    emit catalogsUpdated();
125}
126
127void
128CatalogManager::doCatalogUpdate()
129{
130    Tomahawk::dbcmd_ptr cmd( new DatabaseCommand_CollectionAttributes( DatabaseCommand_SetCollectionAttributes::EchonestSongCatalog ) );
131    connect( cmd.data(), SIGNAL( collectionAttributes( PairList ) ), this, SLOT( collectionAttributes( PairList ) ) );
132    Database::instance()->enqueue( cmd );
133}
134
135QHash< QString, QString >
136CatalogManager::catalogs() const
137{
138    return m_catalogs;
139}
140
141
142EchonestGenerator::EchonestGenerator ( QObject* parent )
143    : GeneratorInterface ( parent )
144    , m_dynPlaylist( new Echonest::DynamicPlaylist() )
145{
146    m_type = "echonest";
147    m_mode = OnDemand;
148    m_logo.load( RESPATH "/images/echonest_logo.png" );
149
150    loadStylesMoodsAndGenres();
151
152    connect( s_catalogs, SIGNAL( catalogsUpdated() ), this, SLOT( knownCatalogsChanged() ) );
153}
154
155
156EchonestGenerator::~EchonestGenerator()
157{
158    if ( !m_dynPlaylist->sessionId().isNull() )
159    {
160        // Running session, delete it
161        QNetworkReply* deleteReply = m_dynPlaylist->deleteSession();
162        connect( deleteReply, SIGNAL( finished() ), deleteReply, SLOT( deleteLater() ) );
163    }
164
165    delete m_dynPlaylist;
166}
167
168void
169EchonestGenerator::setupCatalogs()
170{
171    if ( s_catalogs == 0 )
172        s_catalogs = new CatalogManager( 0 );
173//    qDebug() << "ECHONEST:" << m_logo.size();
174}
175
176dyncontrol_ptr
177EchonestGenerator::createControl( const QString& type )
178{
179    m_controls << dyncontrol_ptr( new EchonestControl( type, GeneratorFactory::typeSelectors( m_type ) ) );
180    return m_controls.last();
181}
182
183
184QPixmap EchonestGenerator::logo()
185{
186    return m_logo;
187}
188
189void
190EchonestGenerator::knownCatalogsChanged()
191{
192    // Refresh all contrls
193    foreach( const dyncontrol_ptr& control, m_controls )
194    {
195        control.staticCast< EchonestControl >()->updateWidgetsFromData();
196    }
197}
198
199
200void
201EchonestGenerator::generate( int number )
202{
203    // convert to an echonest query, and fire it off
204    qDebug() << Q_FUNC_INFO;
205    qDebug() << "Generating playlist with" << m_controls.size();
206    foreach( const dyncontrol_ptr& ctrl, m_controls )
207        qDebug() << ctrl->selectedType() << ctrl->match() << ctrl->input();
208
209    setProperty( "number", number ); //HACK
210
211    connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
212
213    try {
214        getParams();
215    } catch( std::runtime_error& e ) {
216        qWarning() << "Got invalid controls!" << e.what();
217        emit error( "Filters are not valid", e.what() );
218    }
219}
220
221
222void
223EchonestGenerator::startOnDemand()
224{
225    if ( !m_dynPlaylist->sessionId().isNull() )
226    {
227        // Running session, delete it
228        QNetworkReply* deleteReply = m_dynPlaylist->deleteSession();
229        connect( deleteReply, SIGNAL( finished() ), deleteReply, SLOT( deleteLater() ) );
230    }
231
232    connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
233    try {
234        getParams();
235    } catch( std::runtime_error& e ) {
236        qWarning() << "Got invalid controls!" << e.what();
237        emit error( "Filters are not valid", e.what() );
238    }
239}
240
241
242void
243EchonestGenerator::doGenerate( const Echonest::DynamicPlaylist::PlaylistParams& paramsIn )
244{
245    disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
246
247    int number = property( "number" ).toInt();
248    setProperty( "number", QVariant() );
249
250    Echonest::DynamicPlaylist::PlaylistParams params = paramsIn;
251    params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Results, number ) );
252    QNetworkReply* reply = Echonest::DynamicPlaylist::staticPlaylist( params );
253    qDebug() << "Generating a static playlist from echonest!" << reply->url().toString();
254    connect( reply, SIGNAL( finished() ), this, SLOT( staticFinished() ) );
255}
256
257
258void
259EchonestGenerator::doStartOnDemand( const Echonest::DynamicPlaylist::PlaylistParams& params )
260{
261    disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
262
263    QNetworkReply* reply = m_dynPlaylist->create( params );
264    qDebug() << "starting a dynamic playlist from echonest!" << reply->url().toString();
265    connect( reply, SIGNAL( finished() ), this, SLOT( dynamicStarted() ) );
266}
267
268
269void
270EchonestGenerator::fetchNext( int rating )
271{
272    if( m_dynPlaylist->sessionId().isEmpty() ) {
273        // we're not currently playing, oops!
274        qWarning() << Q_FUNC_INFO << "asked to fetch next dynamic song when we're not in the middle of a playlist!";
275        return;
276    }
277
278    if ( rating > -1 )
279    {
280        Echonest::DynamicPlaylist::DynamicFeedback feedback;
281        feedback.append( Echonest::DynamicPlaylist::DynamicFeedbackParamData( Echonest::DynamicPlaylist::RateSong, QString( "last^%1").arg( rating * 2 ).toUtf8() ) );
282        QNetworkReply* reply = m_dynPlaylist->feedback( feedback );
283        connect( reply, SIGNAL( finished() ), reply, SLOT( deleteLater() ) ); // we don't care about the result, just send it off
284    }
285
286    QNetworkReply* reply = m_dynPlaylist->next( 1, 0 );
287    qDebug() << "getting next song from echonest" << reply->url().toString();
288    connect( reply, SIGNAL( finished() ), this, SLOT( dynamicFetched() ) );
289}
290
291
292void
293EchonestGenerator::staticFinished()
294{
295    Q_ASSERT( sender() );
296    Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
297
298    QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
299    reply->deleteLater();
300
301    Echonest::SongList songs;
302    try {
303        songs = Echonest::DynamicPlaylist::parseStaticPlaylist( reply );
304    } catch( const Echonest::ParseError& e ) {
305        qWarning() << "libechonest threw an error trying to parse the static playlist code" << e.errorType() << "error desc:" << e.what();
306
307        emit error( "The Echo Nest returned an error creating the playlist", e.what() );
308        return;
309    }
310
311    QList< query_ptr > queries;
312    foreach( const Echonest::Song& song, songs ) {
313        qDebug() << "EchonestGenerator got song:" << song;
314        queries << queryFromSong( song );
315    }
316
317    emit generated( queries );
318}
319
320
321void
322EchonestGenerator::getParams() throw( std::runtime_error )
323{
324    Echonest::DynamicPlaylist::PlaylistParams params;
325    foreach( const dyncontrol_ptr& control, m_controls ) {
326        params.append( control.dynamicCast<EchonestControl>()->toENParam() );
327    }
328
329    if( appendRadioType( params ) == Echonest::DynamicPlaylist::SongRadioType ) {
330        // we need to do another pass, converting all song queries to song-ids.
331        m_storedParams = params;
332        qDeleteAll( m_waiting );
333        m_waiting.clear();
334
335        // one query per track
336        for( int i = 0; i < params.count(); i++ ) {
337            const Echonest::DynamicPlaylist::PlaylistParamData param = params.value( i );
338
339            if( param.first == Echonest::DynamicPlaylist::SongId ) { // this is a song type enum
340                QString text = param.second.toString();
341
342                Echonest::Song::SearchParams q;
343                q.append( Echonest::Song::SearchParamData( Echonest::Song::Combined, text ) ); // search with the free text "combined" parameter
344                QNetworkReply* r = Echonest::Song::search( q );
345                r->setProperty( "index", i );
346                r->setProperty( "search", text );
347
348                m_waiting.insert( r );
349                connect( r, SIGNAL( finished() ), this, SLOT( songLookupFinished() ) );
350            }
351        }
352
353        if( m_waiting.isEmpty() ) {
354            m_storedParams.clear();
355            emit paramsGenerated( params );
356        }
357
358    } else {
359        emit paramsGenerated( params );
360    }
361}
362
363
364void
365EchonestGenerator::songLookupFinished()
366{
367    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
368    r->deleteLater();
369
370    if( !m_waiting.contains( r ) ) // another generate/start was begun meanwhile, we're out of date
371        return;
372
373    Q_ASSERT( r );
374    m_waiting.remove( r );
375
376    QString search = r->property( "search" ).toString();
377    QByteArray id;
378    try {
379        Echonest::SongList songs = Echonest::Song::parseSearch( r );
380        if( songs.size() > 0 ) {
381            id = songs.first().id();
382            qDebug() << "Got ID for song:" << songs.first() << "from search:" << search;;
383        } else {
384            qDebug() << "Got no songs from our song id lookup.. :(. We looked for:" << search;
385        }
386    } catch( Echonest::ParseError& e ) {
387        qWarning() << "Failed to parse song/search result:" << e.errorType() << e.what();
388    }
389    int idx = r->property( "index" ).toInt();
390    Q_ASSERT( m_storedParams.count() >= idx );
391
392    // replace the song text with the song id in-place
393    m_storedParams[ idx ].second = id;
394
395    if( m_waiting.isEmpty() ) { // we're done!
396        emit paramsGenerated( m_storedParams );
397    }
398}
399
400
401void
402EchonestGenerator::dynamicStarted()
403{
404    Q_ASSERT( sender() );
405    Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
406    QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
407    reply->deleteLater();
408
409    try
410    {
411        m_dynPlaylist->parseCreate( reply );
412        fetchNext();
413    } catch( const Echonest::ParseError& e ) {
414        qWarning() << "libechonest threw an error parsing the start of the dynamic playlist:" << e.errorType() << e.what();
415        emit error( "The Echo Nest returned an error starting the station", e.what() );
416    }
417}
418
419
420void
421EchonestGenerator::dynamicFetched()
422{
423    Q_ASSERT( sender() );
424    Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
425    QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
426    reply->deleteLater();
427
428    try
429    {
430        Echonest::DynamicPlaylist::FetchPair fetched = m_dynPlaylist->parseNext( reply );
431
432        if ( fetched.first.size() != 1 )
433        {
434            qWarning() << "Did not get any track when looking up the next song from the echo nest!";
435            emit error( "No more songs from The Echo Nest available in the station", "" );
436            return;
437        }
438
439        query_ptr songQuery = queryFromSong( fetched.first.first() );
440        emit nextTrackGenerated( songQuery );
441    } catch( const Echonest::ParseError& e ) {
442        qWarning() << "libechonest threw an error parsing the next song of the dynamic playlist:" << e.errorType() << e.what();
443        emit error( "The Echo Nest returned an error getting the next song", e.what() );
444    }
445}
446
447
448QByteArray
449EchonestGenerator::catalogId(const QString &collectionId)
450{
451    return s_catalogs->catalogs().value( collectionId ).toUtf8();
452}
453
454QStringList
455EchonestGenerator::userCatalogs()
456{
457    return s_catalogs->catalogs().keys();
458}
459
460bool
461EchonestGenerator::onlyThisArtistType( Echonest::DynamicPlaylist::ArtistTypeEnum type ) const throw( std::runtime_error )
462{
463    bool only = true;
464    bool some = false;
465
466    foreach( const dyncontrol_ptr& control, m_controls ) {
467        if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) != type ) {
468            only = false;
469        } else if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) == type ) {
470            some = true;
471        }
472    }
473    if( some && only ) {
474        return true;
475    } else if( some && !only ) {
476        throw std::runtime_error( "All artist and song match types must be the same" );
477    }
478
479    return false;
480}
481
482
483Echonest::DynamicPlaylist::ArtistTypeEnum
484EchonestGenerator::appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error )
485{
486    /**
487     * So we try to match the best type of echonest playlist, based on the controls
488     * the types are artist, artist-radio, artist-description, catalog, catalog-radio, song-radio. we don't care about the catalog ones
489     *
490     */
491
492    /// 1. catalog-radio: If any the entries are catalog types.
493    /// 2. artist: If all the artist controls are Limit-To. If some were but not all, error out.
494    /// 3. artist-description: If all the artist entries are Description. If some were but not all, error out.
495    /// 4. artist-radio: If all the artist entries are Similar To. If some were but not all, error out.
496    /// 5. song-radio: If all the artist entries are Similar To. If some were but not all, error out.
497    bool someCatalog = false;
498    bool genreType = false;
499    foreach( const dyncontrol_ptr& control, m_controls ) {
500        if ( control->selectedType() == "User Radio" )
501            someCatalog = true;
502        else if ( control->selectedType() == "Genre" )
503            genreType = true;
504    }
505    if( someCatalog )
506        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::CatalogRadioType ) );
507    else if ( genreType )
508        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::GenreRadioType ) );
509    else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistType ) )
510        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistType ) );
511    else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistDescriptionType ) )
512        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistDescriptionType ) );
513    else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistRadioType ) )
514        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistRadioType ) );
515    else if( onlyThisArtistType( Echonest::DynamicPlaylist::SongRadioType ) )
516        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::SongRadioType ) );
517    else // no artist or song or description types. default to artist-description
518        params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistDescriptionType ) );
519
520    return static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( params.last().second.toInt() );
521}
522
523
524query_ptr
525EchonestGenerator::queryFromSong( const Echonest::Song& song )
526{
527    //         track[ "album" ] = song.release(); // TODO should we include it? can be quite specific
528    return Query::get( song.artistName(), song.title(), QString(), uuid(), false );
529}
530
531
532QString
533EchonestGenerator::sentenceSummary()
534{
535    /**
536     * 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.
537     *
538     * Rules:
539     *   - Sentence starts with "Songs "
540     *   - Artists always go first
541     *   - Separate phrases by comma, and before last phrase
542     *   - sorting always at end
543     *   - collapse artists. "Like X, like Y, like Z, ..." -> "Like X, Y, and Z"
544     *   - skip empty artist entries
545     *
546     *  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,
547     *        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.
548     */
549    QList< dyncontrol_ptr > allcontrols = m_controls;
550    QString sentence = QObject::tr( "Songs ", "Beginning of a sentence summary" );
551
552    /// 1. Collect all required filters
553    /// 2. Get the sorted by filter if it exists.
554    QList< dyncontrol_ptr > required;
555    dyncontrol_ptr sorting;
556    foreach( const dyncontrol_ptr& control, allcontrols ) {
557        if( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" )
558            required << control;
559        else if( control->selectedType() == "Sorting" )
560            sorting = control;
561    }
562    if( !sorting.isNull() )
563        allcontrols.removeAll( sorting );
564
565    /// Skip empty artists
566    QList< dyncontrol_ptr > empty;
567    foreach( const dyncontrol_ptr& artistOrTrack, required ) {
568        QString summary = artistOrTrack.dynamicCast< EchonestControl >()->summary();
569        if( summary.lastIndexOf( "~" ) == summary.length() - 1 )
570            empty << artistOrTrack;
571    }
572    foreach( const dyncontrol_ptr& toremove, empty ) {
573        required.removeAll( toremove );
574        allcontrols.removeAll( toremove );
575    }
576
577    /// If there are no artists and no filters, show some help text
578    if( required.isEmpty() && allcontrols.isEmpty() )
579        sentence = QObject::tr( "No configured filters!" );
580
581    /// Do the assembling. Start with the artists if there are any, then do all the rest.
582    for( int i = 0; i < required.size(); i++ ) {
583        dyncontrol_ptr artist = required.value( i );
584        allcontrols.removeAll( artist ); // remove from pool while we're here
585
586        /// Collapse artist lists
587        QString center, suffix;
588        QString summary = artist.dynamicCast< EchonestControl >()->summary();
589
590        if( i == 0 ) { // if it's the first.. special casez
591            center = summary.remove( "~" );
592            if( required.size() == 2 ) // special case for 2, no comma. ( X and Y )
593                suffix = QObject::tr( " and ", "Inserted between items in a list of two" );
594            else if( required.size() > 2 ) // in a list with more after
595                suffix = QObject::tr( ", ", "Inserted between items in a list" );
596            else if( allcontrols.isEmpty() && sorting.isNull() ) // the last one, and no more controls, so put a period
597                suffix = QObject::tr( ".", "Inserted when ending a sentence summary" );
598            else
599                suffix = " "; // shouldn't happen, but don't fail. it doesn't make sense to have this translatable
600        } else {
601            center = summary.mid( summary.indexOf( "~" ) + 1 );
602            if( i == required.size() - 1 ) { // if there are more, add an " and "
603                if( !( allcontrols.isEmpty() && sorting.isNull() ) )
604                    suffix = QObject::tr( ", ", "Inserted between items in a list" );
605                else
606                    suffix = QObject::tr( ".", "Inserted when ending a sentence summary" );
607            } 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
608                suffix += QObject::tr( ", ", "Inserted between items in a list" );
609            else
610                suffix += QObject::tr( ", and ", "Inserted between the last two items in a list of more than two" );
611        }
612        sentence += center + suffix;
613    }
614    /// Add each filter individually
615    for( int i = 0; i < allcontrols.size(); i++ ) {
616        /// end case: if this is the last AND there is not a sorting filter (so this is the real last one)
617        const bool last = ( i == allcontrols.size() - 1 && sorting.isNull() );
618        QString prefix, suffix;
619        if( last ) { // only if there is not just 1
620            if( !( required.isEmpty() && allcontrols.size() == 1 ) )
621                prefix = QObject::tr( "and ", "Inserted before the last item in a list" );
622            suffix = QObject::tr( ".", "Inserted when ending a sentence summary" );
623        } else
624            suffix = QObject::tr( ", ", "Inserted between items in a list" );
625        sentence += prefix + allcontrols.value( i ).dynamicCast< EchonestControl >()->summary() + suffix;
626    }
627
628    if( !sorting.isNull() ) {
629        sentence += QObject::tr( "and ", "Inserted before the sorting summary in a sentence summary" ) + sorting.dynamicCast< EchonestControl >()->summary() + QObject::tr( ".","Inserted when ending a sentence summary" );
630    }
631
632    return sentence;
633}
634
635void
636EchonestGenerator::loadStylesMoodsAndGenres()
637{
638    if( !s_styles.isEmpty() && !s_moods.isEmpty() && !s_genres.isEmpty() )
639        return;
640
641    loadStyles();
642    loadMoods();
643    loadGenres();
644}
645
646void
647EchonestGenerator::loadStyles()
648{
649    if ( s_styles.isEmpty() )
650    {
651        if ( s_styles_lock.tryLockForRead() )
652        {
653            QVariant styles = TomahawkUtils::Cache::instance()->getData( "EchonestGenerator", "styles" );
654            s_styles_lock.unlock();
655            if ( styles.isValid() && styles.canConvert< QStringList >() )
656            {
657                s_styles = styles.toStringList();
658            }
659            else
660            {
661                s_styles_lock.lockForWrite();
662                tLog() << "Styles not in cache or too old, refetching styles ...";
663                s_stylesJob = Echonest::Artist::listTerms( "style" );
664                connect( s_stylesJob, SIGNAL( finished() ), this, SLOT( stylesReceived() ) );
665            }
666        }
667        else
668        {
669            connect( this, SIGNAL( stylesSaved() ), this, SLOT( loadStyles() ) );
670        }
671    }
672}
673
674void
675EchonestGenerator::loadMoods()
676{
677    if ( s_moods.isEmpty() )
678    {
679        if ( s_moods_lock.tryLockForRead() )
680        {
681            QVariant moods = TomahawkUtils::Cache::instance()->getData( "EchonestGenerator", "moods" );
682            s_moods_lock.unlock();
683            if ( moods.isValid() && moods.canConvert< QStringList >() ) {
684                s_moods = moods.toStringList();
685            }
686            else
687            {
688                s_moods_lock.lockForWrite();
689                tLog() << "Moods not in cache or too old, refetching moods ...";
690                s_moodsJob = Echonest::Artist::listTerms( "mood" );
691                connect( s_moodsJob, SIGNAL( finished() ), this, SLOT( moodsReceived() ) );
692            }
693        }
694        else
695        {
696            connect( this, SIGNAL( moodsSaved() ), this, SLOT( loadMoods() ) );
697        }
698    }
699}
700
701void
702EchonestGenerator::loadGenres()
703{
704    if ( s_genres.isEmpty() )
705    {
706        if ( s_genres_lock.tryLockForRead() )
707        {
708            QVariant genres = TomahawkUtils::Cache::instance()->getData( "EchonestGenerator", "genres" );
709            s_genres_lock.unlock();
710            if ( genres.isValid() && genres.canConvert< QStringList >() )
711            {
712                s_genres = genres.toStringList();
713            }
714            else
715            {
716                s_genres_lock.lockForWrite();
717                tLog() << "Genres not in cache or too old, refetching genres ...";
718                s_genresJob = Echonest::Genre::fetchList( Echonest::GenreInformation(), 2000 );
719                connect( s_genresJob, SIGNAL( finished() ), this, SLOT( genresReceived() ) );
720            }
721        }
722        else
723        {
724            connect( this, SIGNAL( genresSaved() ), this, SLOT( loadGenres() ) );
725        }
726    }
727}
728
729QStringList
730EchonestGenerator::moods()
731{
732    return s_moods;
733}
734
735
736void
737EchonestGenerator::moodsReceived()
738{
739    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
740    Q_ASSERT( r );
741    r->deleteLater();
742
743    try
744    {
745        s_moods = Echonest::Artist::parseTermList( r ).toList();
746    }
747    catch( Echonest::ParseError& e )
748    {
749        qWarning() << "Echonest failed to parse moods list";
750    }
751    s_moodsJob = 0;
752
753    TomahawkUtils::Cache::instance()->putData( "EchonestGenerator", 1209600000 /* 2 weeks */, "moods", QVariant::fromValue< QStringList >( s_moods ) );
754    s_moods_lock.unlock();
755    emit moodsSaved();
756}
757
758
759QStringList
760EchonestGenerator::styles()
761{
762    return s_styles;
763}
764
765
766void
767EchonestGenerator::stylesReceived()
768{
769    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
770    Q_ASSERT( r );
771    r->deleteLater();
772
773    try
774    {
775        s_styles = Echonest::Artist::parseTermList( r ).toList();
776    }
777    catch( Echonest::ParseError& e )
778    {
779        qWarning() << "Echonest failed to parse styles list";
780    }
781    s_stylesJob = 0;
782
783    TomahawkUtils::Cache::instance()->putData( "EchonestGenerator", 1209600000 /* 2 weeks */, "styles", QVariant::fromValue< QStringList >( s_styles ) );
784    s_styles_lock.unlock();
785    emit stylesSaved();
786}
787
788QStringList
789EchonestGenerator::genres()
790{
791    return s_genres;
792}
793
794void
795EchonestGenerator::genresReceived()
796{
797    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
798    Q_ASSERT( r );
799    r->deleteLater();
800
801    try
802    {
803        Echonest::Genres genrelist = Echonest::Genre::parseList( r );
804        foreach( const Echonest::Genre& genre, genrelist )
805        {
806            s_genres << genre.name();
807        }
808    }
809    catch( Echonest::ParseError& e )
810    {
811        qWarning() << "Echonest failed to parse genres list";
812    }
813    s_genresJob = 0;
814
815    TomahawkUtils::Cache::instance()->putData( "EchonestGenerator", 1209600000 /* 2 weeks */, "genres", QVariant::fromValue< QStringList >( s_genres ) );
816    s_genres_lock.unlock();
817    emit genresSaved();
818}