~ubuntu-branches/ubuntu/trusty/tomahawk/trusty-proposed

« back to all changes in this revision

Viewing changes to src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp

  • Committer: Package Import Robot
  • Author(s): Harald Sitter
  • Date: 2013-03-07 21:50:13 UTC
  • Revision ID: package-import@ubuntu.com-20130307215013-6gdjkdds7i9uenvs
Tags: upstream-0.6.0+dfsg
ImportĀ upstreamĀ versionĀ 0.6.0+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
 
2
 *
 
3
 *   Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
 
4
 *
 
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.
 
9
 *
 
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.
 
14
 *
 
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/>.
 
17
 */
 
18
 
 
19
#include "playlist/dynamic/echonest/EchonestGenerator.h"
 
20
#include "playlist/dynamic/echonest/EchonestControl.h"
 
21
#include "playlist/dynamic/echonest/EchonestSteerer.h"
 
22
#include "Query.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"
 
29
#include <QFile>
 
30
#include <QDir>
 
31
#include <EchonestCatalogSynchronizer.h>
 
32
 
 
33
using namespace Tomahawk;
 
34
 
 
35
 
 
36
QStringList EchonestGenerator::s_moods = QStringList();
 
37
QStringList EchonestGenerator::s_styles = QStringList();
 
38
QNetworkReply* EchonestGenerator::s_moodsJob = 0;
 
39
QNetworkReply* EchonestGenerator::s_stylesJob = 0;
 
40
 
 
41
CatalogManager* EchonestGenerator::s_catalogs = 0;
 
42
 
 
43
 
 
44
EchonestFactory::EchonestFactory()
 
45
{
 
46
}
 
47
 
 
48
 
 
49
GeneratorInterface*
 
50
EchonestFactory::create()
 
51
{
 
52
    return new EchonestGenerator();
 
53
}
 
54
 
 
55
 
 
56
dyncontrol_ptr
 
57
EchonestFactory::createControl( const QString& controlType )
 
58
{
 
59
    return dyncontrol_ptr( new EchonestControl( controlType, typeSelectors() ) );
 
60
}
 
61
 
 
62
 
 
63
QStringList
 
64
EchonestFactory::typeSelectors() const
 
65
{
 
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";
 
69
 
 
70
    return types;
 
71
}
 
72
 
 
73
CatalogManager::CatalogManager( QObject* parent )
 
74
    : QObject( parent )
 
75
{
 
76
    connect( SourceList::instance(), SIGNAL( ready() ), this, SLOT( init() ) );
 
77
}
 
78
 
 
79
void
 
80
CatalogManager::init()
 
81
{
 
82
    connect( EchonestCatalogSynchronizer::instance(), SIGNAL( knownCatalogsChanged() ), this, SLOT( doCatalogUpdate() ) );
 
83
    connect( SourceList::instance(), SIGNAL( ready() ), this, SLOT( doCatalogUpdate() ) );
 
84
 
 
85
    doCatalogUpdate();
 
86
}
 
87
 
 
88
void
 
89
CatalogManager::collectionAttributes( const PairList& data )
 
90
{
 
91
    QPair<QString, QString> part;
 
92
    m_catalogs.clear();
 
93
 
 
94
    foreach ( part, data )
 
95
    {
 
96
        if ( SourceList::instance()->get( part.first.toInt() ).isNull() )
 
97
            continue;
 
98
 
 
99
        const QString name = SourceList::instance()->get( part.first.toInt() )->friendlyName();
 
100
        m_catalogs.insert( name, part.second );
 
101
    }
 
102
 
 
103
    emit catalogsUpdated();
 
104
}
 
105
 
 
106
void
 
107
CatalogManager::doCatalogUpdate()
 
108
{
 
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 );
 
112
}
 
113
 
 
114
QHash< QString, QString >
 
115
CatalogManager::catalogs() const
 
116
{
 
117
    return m_catalogs;
 
118
}
 
119
 
 
120
 
 
121
EchonestGenerator::EchonestGenerator ( QObject* parent )
 
122
    : GeneratorInterface ( parent )
 
123
    , m_dynPlaylist( new Echonest::DynamicPlaylist() )
 
124
{
 
125
    m_type = "echonest";
 
126
    m_mode = OnDemand;
 
127
    m_logo.load( RESPATH "/images/echonest_logo.png" );
 
128
 
 
129
    loadStylesAndMoods();
 
130
 
 
131
    connect( s_catalogs, SIGNAL( catalogsUpdated() ), this, SLOT( knownCatalogsChanged() ) );
 
132
}
 
133
 
 
134
 
 
135
EchonestGenerator::~EchonestGenerator()
 
136
{
 
137
    if ( !m_dynPlaylist->sessionId().isNull() )
 
138
    {
 
139
        // Running session, delete it
 
140
        QNetworkReply* deleteReply = m_dynPlaylist->deleteSession();
 
141
        connect( deleteReply, SIGNAL( finished() ), deleteReply, SLOT( deleteLater() ) );
 
142
    }
 
143
 
 
144
    delete m_dynPlaylist;
 
145
}
 
146
 
 
147
void
 
148
EchonestGenerator::setupCatalogs()
 
149
{
 
150
    if ( s_catalogs == 0 )
 
151
        s_catalogs = new CatalogManager( 0 );
 
152
//    qDebug() << "ECHONEST:" << m_logo.size();
 
153
}
 
154
 
 
155
dyncontrol_ptr
 
156
EchonestGenerator::createControl( const QString& type )
 
157
{
 
158
    m_controls << dyncontrol_ptr( new EchonestControl( type, GeneratorFactory::typeSelectors( m_type ) ) );
 
159
    return m_controls.last();
 
160
}
 
161
 
 
162
 
 
163
QPixmap EchonestGenerator::logo()
 
164
{
 
165
    return m_logo;
 
166
}
 
167
 
 
168
void
 
169
EchonestGenerator::knownCatalogsChanged()
 
170
{
 
171
    // Refresh all contrls
 
172
    foreach( const dyncontrol_ptr& control, m_controls )
 
173
    {
 
174
        control.staticCast< EchonestControl >()->updateWidgetsFromData();
 
175
    }
 
176
}
 
177
 
 
178
 
 
179
void
 
180
EchonestGenerator::generate( int number )
 
181
{
 
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();
 
187
 
 
188
    setProperty( "number", number ); //HACK
 
189
 
 
190
    connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
 
191
 
 
192
    try {
 
193
        getParams();
 
194
    } catch( std::runtime_error& e ) {
 
195
        qWarning() << "Got invalid controls!" << e.what();
 
196
        emit error( "Filters are not valid", e.what() );
 
197
    }
 
198
}
 
199
 
 
200
 
 
201
void
 
202
EchonestGenerator::startOnDemand()
 
203
{
 
204
    if ( !m_dynPlaylist->sessionId().isNull() )
 
205
    {
 
206
        // Running session, delete it
 
207
        QNetworkReply* deleteReply = m_dynPlaylist->deleteSession();
 
208
        connect( deleteReply, SIGNAL( finished() ), deleteReply, SLOT( deleteLater() ) );
 
209
    }
 
210
 
 
211
    connect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
 
212
    try {
 
213
        getParams();
 
214
    } catch( std::runtime_error& e ) {
 
215
        qWarning() << "Got invalid controls!" << e.what();
 
216
        emit error( "Filters are not valid", e.what() );
 
217
    }
 
218
}
 
219
 
 
220
 
 
221
void
 
222
EchonestGenerator::doGenerate( const Echonest::DynamicPlaylist::PlaylistParams& paramsIn )
 
223
{
 
224
    disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doGenerate(Echonest::DynamicPlaylist::PlaylistParams ) ) );
 
225
 
 
226
    int number = property( "number" ).toInt();
 
227
    setProperty( "number", QVariant() );
 
228
 
 
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() ) );
 
234
}
 
235
 
 
236
 
 
237
void
 
238
EchonestGenerator::doStartOnDemand( const Echonest::DynamicPlaylist::PlaylistParams& params )
 
239
{
 
240
    disconnect( this, SIGNAL( paramsGenerated( Echonest::DynamicPlaylist::PlaylistParams ) ), this, SLOT( doStartOnDemand( Echonest::DynamicPlaylist::PlaylistParams ) ) );
 
241
 
 
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() ) );
 
245
}
 
246
 
 
247
 
 
248
void
 
249
EchonestGenerator::fetchNext( int rating )
 
250
{
 
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!";
 
254
        return;
 
255
    }
 
256
 
 
257
    if ( rating > -1 )
 
258
    {
 
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
 
263
    }
 
264
 
 
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() ) );
 
268
}
 
269
 
 
270
 
 
271
void
 
272
EchonestGenerator::staticFinished()
 
273
{
 
274
    Q_ASSERT( sender() );
 
275
    Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
 
276
 
 
277
    QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
 
278
 
 
279
    Echonest::SongList songs;
 
280
    try {
 
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();
 
284
 
 
285
        emit error( "The Echo Nest returned an error creating the playlist", e.what() );
 
286
        return;
 
287
    }
 
288
 
 
289
    QList< query_ptr > queries;
 
290
    foreach( const Echonest::Song& song, songs ) {
 
291
        qDebug() << "EchonestGenerator got song:" << song;
 
292
        queries << queryFromSong( song );
 
293
    }
 
294
 
 
295
    emit generated( queries );
 
296
}
 
297
 
 
298
 
 
299
void
 
300
EchonestGenerator::getParams() throw( std::runtime_error )
 
301
{
 
302
    Echonest::DynamicPlaylist::PlaylistParams params;
 
303
    foreach( const dyncontrol_ptr& control, m_controls ) {
 
304
        params.append( control.dynamicCast<EchonestControl>()->toENParam() );
 
305
    }
 
306
 
 
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 );
 
311
        m_waiting.clear();
 
312
 
 
313
        // one query per track
 
314
        for( int i = 0; i < params.count(); i++ ) {
 
315
            const Echonest::DynamicPlaylist::PlaylistParamData param = params.value( i );
 
316
 
 
317
            if( param.first == Echonest::DynamicPlaylist::SongId ) { // this is a song type enum
 
318
                QString text = param.second.toString();
 
319
 
 
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 );
 
325
 
 
326
                m_waiting.insert( r );
 
327
                connect( r, SIGNAL( finished() ), this, SLOT( songLookupFinished() ) );
 
328
            }
 
329
        }
 
330
 
 
331
        if( m_waiting.isEmpty() ) {
 
332
            m_storedParams.clear();
 
333
            emit paramsGenerated( params );
 
334
        }
 
335
 
 
336
    } else {
 
337
        emit paramsGenerated( params );
 
338
    }
 
339
}
 
340
 
 
341
 
 
342
void
 
343
EchonestGenerator::songLookupFinished()
 
344
{
 
345
    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
 
346
 
 
347
    if( !m_waiting.contains( r ) ) // another generate/start was begun meanwhile, we're out of date
 
348
        return;
 
349
 
 
350
    Q_ASSERT( r );
 
351
    m_waiting.remove( r );
 
352
 
 
353
    QString search = r->property( "search" ).toString();
 
354
    QByteArray id;
 
355
    try {
 
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;;
 
360
        } else {
 
361
            qDebug() << "Got no songs from our song id lookup.. :(. We looked for:" << search;
 
362
        }
 
363
    } catch( Echonest::ParseError& e ) {
 
364
        qWarning() << "Failed to parse song/search result:" << e.errorType() << e.what();
 
365
    }
 
366
    int idx = r->property( "index" ).toInt();
 
367
    Q_ASSERT( m_storedParams.count() >= idx );
 
368
 
 
369
    // replace the song text with the song id in-place
 
370
    m_storedParams[ idx ].second = id;
 
371
 
 
372
    if( m_waiting.isEmpty() ) { // we're done!
 
373
        emit paramsGenerated( m_storedParams );
 
374
    }
 
375
}
 
376
 
 
377
 
 
378
void
 
379
EchonestGenerator::dynamicStarted()
 
380
{
 
381
    Q_ASSERT( sender() );
 
382
    Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
 
383
    QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
 
384
 
 
385
    try
 
386
    {
 
387
        m_dynPlaylist->parseCreate( reply );
 
388
        fetchNext();
 
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() );
 
392
    }
 
393
}
 
394
 
 
395
 
 
396
void
 
397
EchonestGenerator::dynamicFetched()
 
398
{
 
399
    Q_ASSERT( sender() );
 
400
    Q_ASSERT( qobject_cast< QNetworkReply* >( sender() ) );
 
401
    QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
 
402
 
 
403
    try
 
404
    {
 
405
        Echonest::DynamicPlaylist::FetchPair fetched = m_dynPlaylist->parseNext( reply );
 
406
 
 
407
        if ( fetched.first.size() != 1 )
 
408
        {
 
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", "" );
 
411
            return;
 
412
        }
 
413
 
 
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() );
 
419
    }
 
420
}
 
421
 
 
422
 
 
423
QByteArray
 
424
EchonestGenerator::catalogId(const QString &collectionId)
 
425
{
 
426
    return s_catalogs->catalogs().value( collectionId ).toUtf8();
 
427
}
 
428
 
 
429
QStringList
 
430
EchonestGenerator::userCatalogs()
 
431
{
 
432
    return s_catalogs->catalogs().keys();
 
433
}
 
434
 
 
435
bool
 
436
EchonestGenerator::onlyThisArtistType( Echonest::DynamicPlaylist::ArtistTypeEnum type ) const throw( std::runtime_error )
 
437
{
 
438
    bool only = true;
 
439
    bool some = false;
 
440
 
 
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 ) {
 
443
            only = false;
 
444
        } else if( ( control->selectedType() == "Artist" || control->selectedType() == "Artist Description" || control->selectedType() == "Song" ) && static_cast<Echonest::DynamicPlaylist::ArtistTypeEnum>( control->match().toInt() ) == type ) {
 
445
            some = true;
 
446
        }
 
447
    }
 
448
    if( some && only ) {
 
449
        return true;
 
450
    } else if( some && !only ) {
 
451
        throw std::runtime_error( "All artist and song match types must be the same" );
 
452
    }
 
453
 
 
454
    return false;
 
455
}
 
456
 
 
457
 
 
458
Echonest::DynamicPlaylist::ArtistTypeEnum
 
459
EchonestGenerator::appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error )
 
460
{
 
461
    /**
 
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
 
464
     *
 
465
     */
 
466
 
 
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" )
 
475
            someCatalog = true;
 
476
    }
 
477
    if( someCatalog )
 
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 ) );
 
489
 
 
490
    return static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( params.last().second.toInt() );
 
491
}
 
492
 
 
493
 
 
494
query_ptr
 
495
EchonestGenerator::queryFromSong( const Echonest::Song& song )
 
496
{
 
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 );
 
499
}
 
500
 
 
501
 
 
502
QString
 
503
EchonestGenerator::sentenceSummary()
 
504
{
 
505
    /**
 
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.
 
507
     *
 
508
     * Rules:
 
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
 
515
     *
 
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.
 
518
     */
 
519
    QList< dyncontrol_ptr > allcontrols = m_controls;
 
520
    QString sentence = "Songs ";
 
521
 
 
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" )
 
528
            required << control;
 
529
        else if( control->selectedType() == "Sorting" )
 
530
            sorting = control;
 
531
    }
 
532
    if( !sorting.isNull() )
 
533
        allcontrols.removeAll( sorting );
 
534
 
 
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;
 
541
    }
 
542
    foreach( const dyncontrol_ptr& toremove, empty ) {
 
543
        required.removeAll( toremove );
 
544
        allcontrols.removeAll( toremove );
 
545
    }
 
546
 
 
547
    /// If there are no artists and no filters, show some help text
 
548
    if( required.isEmpty() && allcontrols.isEmpty() )
 
549
        sentence = "No configured filters!";
 
550
 
 
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
 
555
 
 
556
        /// Collapse artist lists
 
557
        QString center, suffix;
 
558
        QString summary = artist.dynamicCast< EchonestControl >()->summary();
 
559
 
 
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 )
 
563
                suffix = " and ";
 
564
            else if( required.size() > 2 ) // in a list with more after
 
565
                suffix = ", ";
 
566
            else if( allcontrols.isEmpty() && sorting.isNull() ) // the last one, and no more controls, so put a period
 
567
                suffix = ".";
 
568
            else
 
569
                suffix = " ";
 
570
        } else {
 
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() ) )
 
574
                    suffix = ", ";
 
575
                else
 
576
                    suffix = ".";
 
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
 
578
                suffix += ", ";
 
579
            else
 
580
                suffix += ", and ";
 
581
        }
 
582
        sentence += center + suffix;
 
583
    }
 
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 ) )
 
591
                prefix = "and ";
 
592
            suffix = ".";
 
593
        } else
 
594
            suffix = ", ";
 
595
        sentence += prefix + allcontrols.value( i ).dynamicCast< EchonestControl >()->summary() + suffix;
 
596
    }
 
597
 
 
598
    if( !sorting.isNull() ) {
 
599
        sentence += "and " + sorting.dynamicCast< EchonestControl >()->summary() + ".";
 
600
    }
 
601
 
 
602
    return sentence;
 
603
}
 
604
 
 
605
void
 
606
EchonestGenerator::loadStylesAndMoods()
 
607
{
 
608
    if( !s_styles.isEmpty() || !s_moods.isEmpty() )
 
609
        return;
 
610
 
 
611
    QFile dataFile( TomahawkUtils::appDataDir().absoluteFilePath( "echonest_stylesandmoods.dat" ) );
 
612
    if( !dataFile.exists() ) // load
 
613
    {
 
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() ) );
 
618
    } else
 
619
    {
 
620
        if( !dataFile.open( QIODevice::ReadOnly ) )
 
621
        {
 
622
            tLog() << "Failed to open for reading styles/moods db file:" << dataFile.fileName();
 
623
            return;
 
624
        }
 
625
 
 
626
        QString allData = QString::fromUtf8( dataFile.readAll() );
 
627
        QStringList parts = allData.split( "\n" );
 
628
        if( parts.size() != 2 )
 
629
        {
 
630
            tLog() << "Didn't get both moods and styles in file...:" << allData;
 
631
            return;
 
632
        }
 
633
        s_moods = parts[ 0 ].split( "|" );
 
634
        s_styles = parts[ 1 ].split( "|" );
 
635
    }
 
636
}
 
637
 
 
638
void
 
639
EchonestGenerator::saveStylesAndMoods()
 
640
{
 
641
    QFile dataFile( TomahawkUtils::appDataDir().absoluteFilePath( "echonest_stylesandmoods.dat" ) );
 
642
    if( !dataFile.open( QIODevice::WriteOnly ) )
 
643
    {
 
644
        tLog() << "Failed to open styles and moods data file for saving:" << dataFile.errorString() << dataFile.fileName();
 
645
        return;
 
646
    }
 
647
 
 
648
    QByteArray data = QString( "%1\n%2" ).arg( s_moods.join( "|" ) ).arg( s_styles.join( "|" ) ).toUtf8();
 
649
    dataFile.write( data );
 
650
}
 
651
 
 
652
 
 
653
 
 
654
QStringList
 
655
EchonestGenerator::moods()
 
656
{
 
657
    return s_moods;
 
658
}
 
659
 
 
660
 
 
661
void
 
662
EchonestGenerator::moodsReceived()
 
663
{
 
664
    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
 
665
    Q_ASSERT( r );
 
666
 
 
667
    try {
 
668
        s_moods = Echonest::Artist::parseTermList( r ).toList();
 
669
    } catch( Echonest::ParseError& e ) {
 
670
        qWarning() << "Echonest failed to parse moods list";
 
671
    }
 
672
    s_moodsJob = 0;
 
673
 
 
674
    if( !s_styles.isEmpty() )
 
675
        saveStylesAndMoods();
 
676
}
 
677
 
 
678
 
 
679
QStringList
 
680
EchonestGenerator::styles()
 
681
{
 
682
    return s_styles;
 
683
}
 
684
 
 
685
 
 
686
void
 
687
EchonestGenerator::stylesReceived()
 
688
{
 
689
    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
 
690
    Q_ASSERT( r );
 
691
 
 
692
    try {
 
693
        s_styles = Echonest::Artist::parseTermList( r ).toList();
 
694
    } catch( Echonest::ParseError& e ) {
 
695
        qWarning() << "Echonest failed to parse styles list";
 
696
    }
 
697
    s_stylesJob = 0;
 
698
 
 
699
    if( !s_moods.isEmpty() )
 
700
        saveStylesAndMoods();
 
701
}