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