PageRenderTime 469ms CodeModel.GetById 141ms app.highlight 206ms RepoModel.GetById 116ms app.codeStats 0ms

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