/src/libtomahawk/playlist/PlaylistModel.cpp

http://github.com/tomahawk-player/tomahawk · C++ · 656 lines · 487 code · 131 blank · 38 comment · 91 complexity · c63d7bfc04b784a016046a29e5432bc1 MD5 · raw file

  1. /* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2. *
  3. * Copyright 2010-2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  4. * Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
  5. * Copyright 2010-2012, Leo Franchi <lfranchi@kde.org>
  6. *
  7. * Tomahawk is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * Tomahawk is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. #include "PlaylistModel_p.h"
  21. #include <QMimeData>
  22. #include <QTreeView>
  23. #include "database/Database.h"
  24. #include "database/DatabaseCommand_PlaybackHistory.h"
  25. #include "playlist/dynamic/GeneratorInterface.h"
  26. #include "utils/Logger.h"
  27. #include "utils/TomahawkUtils.h"
  28. #include "Album.h"
  29. #include "Artist.h"
  30. #include "DropJob.h"
  31. #include "Pipeline.h"
  32. #include "PlayableItem.h"
  33. #include "PlaylistEntry.h"
  34. #include "Source.h"
  35. #include "SourceList.h"
  36. using namespace Tomahawk;
  37. void
  38. PlaylistModel::init()
  39. {
  40. Q_D( PlaylistModel );
  41. d->dropStorage.parent = QPersistentModelIndex();
  42. d->dropStorage.row = -10;
  43. setReadOnly( true );
  44. }
  45. PlaylistModel::PlaylistModel( QObject* parent )
  46. : PlayableModel( parent, new PlaylistModelPrivate( this ) )
  47. {
  48. init();
  49. }
  50. PlaylistModel::PlaylistModel( QObject* parent, PlaylistModelPrivate* d )
  51. : PlayableModel( parent, d )
  52. {
  53. init();
  54. }
  55. PlaylistModel::~PlaylistModel()
  56. {
  57. }
  58. QString
  59. PlaylistModel::guid() const
  60. {
  61. Q_D( const PlaylistModel );
  62. if ( d->playlist )
  63. {
  64. return QString( "playlistmodel/%1" ).arg( d->playlist->guid() );
  65. }
  66. else
  67. return QString();
  68. }
  69. void
  70. PlaylistModel::loadPlaylist( const Tomahawk::playlist_ptr& playlist, bool loadEntries )
  71. {
  72. Q_D( PlaylistModel );
  73. if ( d->playlist )
  74. {
  75. disconnect( d->playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( onRevisionLoaded( Tomahawk::PlaylistRevision ) ) );
  76. disconnect( d->playlist.data(), SIGNAL( deleted( Tomahawk::playlist_ptr ) ), this, SIGNAL( playlistDeleted() ) );
  77. disconnect( d->playlist.data(), SIGNAL( changed() ), this, SLOT( onPlaylistChanged() ) );
  78. }
  79. if ( loadEntries )
  80. {
  81. startLoading();
  82. clear();
  83. }
  84. d->playlist = playlist;
  85. connect( playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), SLOT( onRevisionLoaded( Tomahawk::PlaylistRevision ) ) );
  86. connect( playlist.data(), SIGNAL( deleted( Tomahawk::playlist_ptr ) ), SIGNAL( playlistDeleted() ) );
  87. connect( playlist.data(), SIGNAL( changed() ), SLOT( onPlaylistChanged() ) );
  88. setReadOnly( !d->playlist->author()->isLocal() );
  89. d->isTemporary = false;
  90. onPlaylistChanged();
  91. if ( !loadEntries )
  92. return;
  93. if ( playlist->loaded() )
  94. {
  95. QList<plentry_ptr> entries = playlist->entries();
  96. appendEntries( entries );
  97. // finishLoading() will be called by appendEntries, so it can keep showing it until all tracks are resolved
  98. // finishLoading();
  99. /* foreach ( const plentry_ptr& p, entries )
  100. qDebug() << p->guid() << p->query()->track() << p->query()->artist(); */
  101. }
  102. }
  103. void
  104. PlaylistModel::onPlaylistChanged()
  105. {
  106. Q_D( PlaylistModel );
  107. QString age = TomahawkUtils::ageToString( QDateTime::fromTime_t( d->playlist->createdOn() ), true );
  108. QString desc;
  109. // we check for "someone" to work-around an old bug
  110. if ( d->playlist->creator().isEmpty() || d->playlist->creator() == "someone" )
  111. {
  112. if ( d->playlist->author()->isLocal() )
  113. {
  114. desc = tr( "A playlist you created %1." )
  115. .arg( age );
  116. }
  117. else
  118. {
  119. desc = tr( "A playlist by %1, created %2." )
  120. .arg( d->playlist->author()->friendlyName() )
  121. .arg( age );
  122. }
  123. }
  124. else
  125. {
  126. desc = tr( "A playlist by %1, created %2." )
  127. .arg( d->playlist->creator() )
  128. .arg( age );
  129. }
  130. setTitle( d->playlist->title() );
  131. setDescription( desc );
  132. emit changed();
  133. }
  134. void
  135. PlaylistModel::clear()
  136. {
  137. Q_D( PlaylistModel );
  138. d->waitingForResolved.clear();
  139. PlayableModel::clear();
  140. }
  141. void
  142. PlaylistModel::appendEntries( const QList< plentry_ptr >& entries )
  143. {
  144. insertEntries( entries, rowCount( QModelIndex() ) );
  145. }
  146. void
  147. PlaylistModel::insertAlbums( const QList< Tomahawk::album_ptr >& albums, int row )
  148. {
  149. Q_D( PlaylistModel );
  150. // FIXME: This currently appends, not inserts!
  151. Q_UNUSED( row );
  152. foreach ( const album_ptr& album, albums )
  153. {
  154. if ( !album )
  155. return;
  156. connect( album.data(), SIGNAL( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ),
  157. SLOT( appendQueries( QList<Tomahawk::query_ptr> ) ) );
  158. appendQueries( album->playlistInterface( Mixed )->tracks() );
  159. }
  160. if ( rowCount( QModelIndex() ) == 0 && albums.count() == 1 )
  161. {
  162. setTitle( albums.first()->name() );
  163. setDescription( tr( "All tracks by %1 on album %2" ).arg( albums.first()->artist()->name() ).arg( albums.first()->name() ) );
  164. d->isTemporary = true;
  165. }
  166. }
  167. void
  168. PlaylistModel::insertArtists( const QList< Tomahawk::artist_ptr >& artists, int row )
  169. {
  170. Q_D( PlaylistModel );
  171. // FIXME: This currently appends, not inserts!
  172. Q_UNUSED( row );
  173. foreach ( const artist_ptr& artist, artists )
  174. {
  175. if ( !artist )
  176. return;
  177. connect( artist.data(), SIGNAL( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ),
  178. SLOT( appendQueries( QList<Tomahawk::query_ptr> ) ) );
  179. appendQueries( artist->playlistInterface( Mixed )->tracks() );
  180. }
  181. if ( rowCount( QModelIndex() ) == 0 && artists.count() == 1 )
  182. {
  183. setTitle( artists.first()->name() );
  184. setDescription( tr( "All tracks by %1" ).arg( artists.first()->name() ) );
  185. d->isTemporary = true;
  186. }
  187. }
  188. void
  189. PlaylistModel::insertQueries( const QList< Tomahawk::query_ptr >& queries, int row, const QList< Tomahawk::PlaybackLog >& logs, const QModelIndex& /* parent */ )
  190. {
  191. Q_D( PlaylistModel );
  192. QList< Tomahawk::plentry_ptr > entries;
  193. foreach ( const query_ptr& query, queries )
  194. {
  195. if ( d->acceptPlayableQueriesOnly && query && query->resolvingFinished() && !query->playable() )
  196. continue;
  197. plentry_ptr entry = plentry_ptr( new PlaylistEntry() );
  198. entry->setDuration( query->track()->duration() );
  199. entry->setLastmodified( 0 );
  200. QString annotation = "";
  201. if ( !query->property( "annotation" ).toString().isEmpty() )
  202. annotation = query->property( "annotation" ).toString();
  203. entry->setAnnotation( annotation );
  204. entry->setQuery( query );
  205. entry->setGuid( uuid() );
  206. entries << entry;
  207. }
  208. insertEntries( entries, row, QModelIndex(), logs );
  209. }
  210. void
  211. PlaylistModel::insertEntries( const QList< Tomahawk::plentry_ptr >& entries, int row, const QModelIndex& parent, const QList< Tomahawk::PlaybackLog >& logs )
  212. {
  213. Q_D( PlaylistModel );
  214. if ( entries.isEmpty() )
  215. {
  216. emit itemCountChanged( rowCount( QModelIndex() ) );
  217. finishLoading();
  218. return;
  219. }
  220. int c = row;
  221. QPair< int, int > crows;
  222. crows.first = c;
  223. crows.second = c + entries.count() - 1;
  224. if ( !d->isLoading )
  225. {
  226. d->savedInsertPos = row;
  227. d->savedInsertTracks = entries;
  228. }
  229. emit beginInsertRows( parent, crows.first, crows.second );
  230. QList< Tomahawk::query_ptr > queries;
  231. int i = 0;
  232. foreach( const plentry_ptr& entry, entries )
  233. {
  234. PlayableItem* pItem = itemFromIndex( parent );
  235. PlayableItem* plitem = new PlayableItem( entry, pItem, row + i );
  236. plitem->index = createIndex( row + i, 0, plitem );
  237. if ( logs.count() > i )
  238. plitem->setPlaybackLog( logs.at( i ) );
  239. i++;
  240. if ( entry->query()->id() == currentItemUuid() )
  241. setCurrentIndex( plitem->index );
  242. if ( !entry->query()->resolvingFinished() && !entry->query()->playable() )
  243. {
  244. queries << entry->query();
  245. d->waitingForResolved.append( entry->query().data() );
  246. connect( entry->query().data(), SIGNAL( playableStateChanged( bool ) ),
  247. SLOT( onQueryBecamePlayable( bool ) ),
  248. Qt::UniqueConnection );
  249. connect( entry->query().data(), SIGNAL( resolvingFinished( bool ) ),
  250. SLOT( trackResolved( bool ) ) );
  251. }
  252. connect( plitem, SIGNAL( dataChanged() ), SLOT( onDataChanged() ) );
  253. }
  254. if ( !d->waitingForResolved.isEmpty() )
  255. {
  256. startLoading();
  257. Pipeline::instance()->resolve( queries );
  258. }
  259. else
  260. {
  261. finishLoading();
  262. }
  263. emit endInsertRows();
  264. emit itemCountChanged( rowCount( QModelIndex() ) );
  265. emit selectRequest( index( 0, 0, parent ) );
  266. if ( parent.isValid() )
  267. emit expandRequest( parent );
  268. }
  269. void
  270. PlaylistModel::trackResolved( bool )
  271. {
  272. Q_D( PlaylistModel );
  273. Tomahawk::Query* q = qobject_cast< Query* >( sender() );
  274. if ( !q )
  275. {
  276. // Track has been removed from the playlist by now
  277. return;
  278. }
  279. if ( d->waitingForResolved.contains( q ) )
  280. {
  281. d->waitingForResolved.removeAll( q );
  282. disconnect( q, SIGNAL( resolvingFinished( bool ) ), this, SLOT( trackResolved( bool ) ) );
  283. }
  284. if ( d->waitingForResolved.isEmpty() )
  285. {
  286. finishLoading();
  287. }
  288. }
  289. void
  290. PlaylistModel::onRevisionLoaded( Tomahawk::PlaylistRevision revision )
  291. {
  292. Q_D( PlaylistModel );
  293. if ( !d->waitForRevision.contains( revision.revisionguid ) )
  294. {
  295. loadPlaylist( d->playlist );
  296. }
  297. else
  298. {
  299. d->waitForRevision.removeAll( revision.revisionguid );
  300. }
  301. }
  302. QMimeData*
  303. PlaylistModel::mimeData( const QModelIndexList& indexes ) const
  304. {
  305. Q_D( const PlaylistModel );
  306. // Add the playlist id to the mime data so that we can detect dropping on ourselves
  307. QMimeData* data = PlayableModel::mimeData( indexes );
  308. if ( d->playlist )
  309. data->setData( "application/tomahawk.playlist.id", d->playlist->guid().toLatin1() );
  310. return data;
  311. }
  312. bool
  313. PlaylistModel::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent )
  314. {
  315. Q_D( PlaylistModel );
  316. Q_UNUSED( column );
  317. if ( action == Qt::IgnoreAction || isReadOnly() )
  318. return true;
  319. if ( !DropJob::acceptsMimeData( data ) )
  320. return false;
  321. d->dropStorage.row = row;
  322. d->dropStorage.parent = QPersistentModelIndex( parent );
  323. d->dropStorage.action = action;
  324. DropJob* dj = new DropJob();
  325. if ( !DropJob::acceptsMimeData( data, DropJob::Track | DropJob::Playlist | DropJob::Album | DropJob::Artist ) )
  326. return false;
  327. dj->setDropTypes( DropJob::Track | DropJob::Playlist | DropJob::Artist | DropJob::Album );
  328. dj->setDropAction( DropJob::Append );
  329. /* if ( action & Qt::MoveAction )
  330. dj->setDropAction( DropJob::Move ); */
  331. #ifdef Q_OS_MAC
  332. // On mac, drags from outside the app are still Qt::MoveActions instead of Qt::CopyAction by default
  333. // so check if the drag originated in this playlist to determine whether or not to copy
  334. if ( !data->hasFormat( "application/tomahawk.playlist.id" ) ||
  335. ( d->playlist && data->data( "application/tomahawk.playlist.id" ) != d->playlist->guid() ) )
  336. {
  337. dj->setDropAction( DropJob::Append );
  338. }
  339. #endif
  340. connect( dj, SIGNAL( tracks( QList< Tomahawk::query_ptr > ) ), SLOT( parsedDroppedTracks( QList< Tomahawk::query_ptr > ) ) );
  341. dj->tracksFromMimeData( data );
  342. return true;
  343. }
  344. playlist_ptr
  345. PlaylistModel::playlist() const
  346. {
  347. Q_D( const PlaylistModel );
  348. return d->playlist;
  349. }
  350. void
  351. PlaylistModel::parsedDroppedTracks( QList< query_ptr > tracks )
  352. {
  353. Q_D( PlaylistModel );
  354. if ( d->dropStorage.row == -10 ) // nope
  355. return;
  356. int beginRow;
  357. if ( d->dropStorage.row != -1 )
  358. beginRow = d->dropStorage.row;
  359. else if ( d->dropStorage.parent.isValid() )
  360. beginRow = d->dropStorage.parent.row();
  361. else
  362. beginRow = rowCount( QModelIndex() );
  363. if ( !tracks.isEmpty() )
  364. {
  365. bool update = ( d->dropStorage.action & Qt::CopyAction || d->dropStorage.action & Qt::MoveAction );
  366. if ( update )
  367. beginPlaylistChanges();
  368. insertQueries( tracks, beginRow );
  369. if ( update && d->dropStorage.action & Qt::CopyAction )
  370. endPlaylistChanges();
  371. }
  372. d->dropStorage.parent = QPersistentModelIndex();
  373. d->dropStorage.row = -10;
  374. }
  375. void
  376. PlaylistModel::beginPlaylistChanges()
  377. {
  378. Q_D( PlaylistModel );
  379. if ( !d->playlist || !d->playlist->author()->isLocal() )
  380. return;
  381. Q_ASSERT( !d->changesOngoing );
  382. d->changesOngoing = true;
  383. }
  384. void
  385. PlaylistModel::endPlaylistChanges()
  386. {
  387. Q_D( PlaylistModel );
  388. if ( !d->playlist || !d->playlist->author()->isLocal() )
  389. {
  390. d->savedInsertPos = -1;
  391. d->savedInsertTracks.clear();
  392. d->savedRemoveTracks.clear();
  393. return;
  394. }
  395. if ( d->changesOngoing )
  396. {
  397. d->changesOngoing = false;
  398. }
  399. else
  400. {
  401. tDebug() << "Called" << Q_FUNC_INFO << "unexpectedly!";
  402. Q_ASSERT( false );
  403. }
  404. QList<plentry_ptr> l = playlistEntries();
  405. QString newrev = uuid();
  406. d->waitForRevision << newrev;
  407. if ( dynplaylist_ptr dynplaylist = d->playlist.dynamicCast<Tomahawk::DynamicPlaylist>() )
  408. {
  409. if ( dynplaylist->mode() == OnDemand )
  410. {
  411. dynplaylist->createNewRevision( newrev );
  412. }
  413. else if ( dynplaylist->mode() == Static )
  414. {
  415. dynplaylist->createNewRevision( newrev, dynplaylist->currentrevision(), dynplaylist->type(), dynplaylist->generator()->controls(), l );
  416. }
  417. }
  418. else
  419. {
  420. d->playlist->createNewRevision( newrev, d->playlist->currentrevision(), l );
  421. }
  422. if ( d->savedInsertPos >= 0 && !d->savedInsertTracks.isEmpty() &&
  423. !d->savedRemoveTracks.isEmpty() )
  424. {
  425. // If we have *both* an insert and remove, then it's a move action
  426. // However, since we got the insert before the remove (Qt...), the index we have as the saved
  427. // insert position is no longer valid. Find the proper one by finding the location of the first inserted
  428. // track
  429. for ( int i = 0; i < rowCount( QModelIndex() ); i++ )
  430. {
  431. const QModelIndex idx = index( i, 0, QModelIndex() );
  432. if ( !idx.isValid() )
  433. continue;
  434. const PlayableItem* item = itemFromIndex( idx );
  435. if ( !item || !item->entry() )
  436. continue;
  437. // qDebug() << "Checking for equality:" << (item->entry() == m_savedInsertTracks.first()) << m_savedInsertTracks.first()->query()->track() << m_savedInsertTracks.first()->query()->artist();
  438. if ( item->entry() == d->savedInsertTracks.first() )
  439. {
  440. // Found our index
  441. emit d->playlist->tracksMoved( d->savedInsertTracks, i );
  442. break;
  443. }
  444. }
  445. d->savedInsertPos = -1;
  446. d->savedInsertTracks.clear();
  447. d->savedRemoveTracks.clear();
  448. }
  449. else if ( d->savedInsertPos >= 0 )
  450. {
  451. emit d->playlist->tracksInserted( d->savedInsertTracks, d->savedInsertPos );
  452. d->savedInsertPos = -1;
  453. d->savedInsertTracks.clear();
  454. }
  455. else if ( !d->savedRemoveTracks.isEmpty() )
  456. {
  457. emit d->playlist->tracksRemoved( d->savedRemoveTracks );
  458. d->savedRemoveTracks.clear();
  459. }
  460. }
  461. QList<Tomahawk::plentry_ptr>
  462. PlaylistModel::playlistEntries() const
  463. {
  464. QList<plentry_ptr> l;
  465. for ( int i = 0; i < rowCount( QModelIndex() ); i++ )
  466. {
  467. QModelIndex idx = index( i, 0, QModelIndex() );
  468. if ( !idx.isValid() )
  469. continue;
  470. PlayableItem* item = itemFromIndex( idx );
  471. if ( item && item->entry() )
  472. l << item->entry();
  473. }
  474. return l;
  475. }
  476. void
  477. PlaylistModel::removeIndex( const QModelIndex& index, bool moreToCome )
  478. {
  479. Q_D( PlaylistModel );
  480. PlayableItem* item = itemFromIndex( index );
  481. if ( item && d->waitingForResolved.contains( item->query().data() ) )
  482. {
  483. disconnect( item->query().data(), SIGNAL( resolvingFinished( bool ) ), this, SLOT( trackResolved( bool ) ) );
  484. d->waitingForResolved.removeAll( item->query().data() );
  485. if ( d->waitingForResolved.isEmpty() )
  486. finishLoading();
  487. }
  488. if ( !d->changesOngoing )
  489. beginPlaylistChanges();
  490. if ( item && !d->isLoading )
  491. d->savedRemoveTracks << item->query();
  492. PlayableModel::removeIndex( index, moreToCome );
  493. if ( !moreToCome )
  494. endPlaylistChanges();
  495. }
  496. bool
  497. PlaylistModel::waitForRevision( const QString& revisionguid ) const
  498. {
  499. Q_D( const PlaylistModel );
  500. return d->waitForRevision.contains( revisionguid );
  501. }
  502. void
  503. PlaylistModel::removeFromWaitList( const QString& revisionguid )
  504. {
  505. Q_D( PlaylistModel );
  506. d->waitForRevision.removeAll( revisionguid );
  507. }
  508. bool
  509. PlaylistModel::isTemporary() const
  510. {
  511. Q_D( const PlaylistModel );
  512. return d->isTemporary;
  513. }
  514. bool
  515. PlaylistModel::acceptPlayableQueriesOnly() const
  516. {
  517. Q_D( const PlaylistModel );
  518. return d->acceptPlayableQueriesOnly;
  519. }
  520. void
  521. PlaylistModel::setAcceptPlayableQueriesOnly( bool b )
  522. {
  523. Q_D( PlaylistModel );
  524. d->acceptPlayableQueriesOnly = b;
  525. }