1 /****************************************************************************************
2  * Copyright (c) 2008 Casey Link <unnamedrambler@gmail.com>                             *
3  * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org>                                *
4  * Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org>                            *
5  *                                                                                      *
6  * This program is free software; you can redistribute it and/or modify it under        *
7  * the terms of the GNU General Public License as published by the Free Software        *
8  * Foundation; either version 2 of the License, or (at your option) any later           *
9  * version.                                                                             *
10  *                                                                                      *
11  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
12  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
13  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
14  *                                                                                      *
15  * You should have received a copy of the GNU General Public License along with         *
16  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
17  ****************************************************************************************/
18 
19 #define DEBUG_PREFIX "LastFmTreeModel"
20 #include "core/support/Debug.h"
21 
22 #include "LastFmTreeModel.h"
23 
24 #include "AvatarDownloader.h"
25 #include "core-impl/collections/support/CollectionManager.h"
26 #include "AmarokMimeData.h"
27 
28 #include <QIcon>
29 
30 #include <QPainter>
31 
32 #include <Tag.h>
33 #include <XmlQuery.h>
34 
35 using namespace LastFm;
36 
LastFmTreeModel(QObject * parent)37 LastFmTreeModel::LastFmTreeModel( QObject *parent )
38     : QAbstractItemModel( parent )
39 {
40     m_rootItem = new LastFmTreeItem( LastFm::Root, "Hello" );
41     setupModelData( m_rootItem );
42 
43     QNetworkReply *reply;
44 
45     reply = m_user.getFriends();
46     connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddFriends );
47 
48     reply = m_user.getTopTags();
49     connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddTags );
50 
51     reply = m_user.getTopArtists();
52     connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddTopArtists );
53 }
54 
~LastFmTreeModel()55 LastFmTreeModel::~LastFmTreeModel()
56 {
57     delete m_rootItem;
58 }
59 
60 void
slotAddFriends()61 LastFmTreeModel::slotAddFriends()
62 {
63     QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
64     if( !reply )
65     {
66         debug() << __PRETTY_FUNCTION__ << "null reply!";
67         return;
68     }
69     reply->deleteLater();
70 
71     lastfm::XmlQuery lfm;
72     if( lfm.parse( reply->readAll() ) )
73     {
74         QList<lastfm::XmlQuery> children = lfm[ "friends" ].children( "user" );
75         int start = m_myFriends->childCount();
76         QModelIndex parent = index( m_myFriends->row(), 0 );
77         beginInsertRows( parent, start, start + children.size() );
78 
79         foreach( const lastfm::XmlQuery &e, children )
80         {
81             const QString name = e[ "name" ].text();
82 
83             LastFmTreeItem* afriend = new LastFmTreeItem( mapTypeToUrl(LastFm::FriendsChild, name),
84                                                           LastFm::FriendsChild, name, m_myFriends );
85 
86             QUrl avatarUrl( e[ QLatin1String("image size=small") ].text() );
87             if( !avatarUrl.isEmpty() )
88                 afriend->setAvatarUrl( avatarUrl );
89 
90             m_myFriends->appendChild( afriend );
91             appendUserStations( afriend, name );
92         }
93 
94         endInsertRows();
95         emit dataChanged( parent, parent );
96     }
97     else
98     {
99         debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message();
100         return;
101     }
102 }
103 
104 void
slotAddTags()105 LastFmTreeModel::slotAddTags()
106 {
107     QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
108     if( !reply )
109     {
110         debug() << __PRETTY_FUNCTION__ << "null reply!";
111         return;
112     }
113     reply->deleteLater();
114 
115     QMap<int, QString> listWithWeights = lastfm::Tag::list( reply );
116     int start = m_myTags->childCount();
117     QModelIndex parent = index( m_myTags->row(), 0 );
118     beginInsertRows( parent, start, start + listWithWeights.size() );
119 
120     QMapIterator<int, QString> it( listWithWeights );
121     it.toBack();
122     while( it.hasPrevious() )
123     {
124         it.previous();
125         int count = it.key();
126         QString text = it.value();
127         QString prettyText = i18nc( "%1 is Last.fm tag name, %2 is its usage count",
128                                     "%1 (%2)", text, count );
129 
130         LastFmTreeItem *tag = new LastFmTreeItem( mapTypeToUrl( LastFm::MyTagsChild, text ),
131                                                   LastFm::MyTagsChild, prettyText, m_myTags );
132         m_myTags->appendChild( tag );
133     }
134 
135     endInsertRows();
136     emit dataChanged( parent, parent );
137 }
138 
139 void
slotAddTopArtists()140 LastFmTreeModel::slotAddTopArtists()
141 {
142     QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
143     if( !reply )
144     {
145         debug() << __PRETTY_FUNCTION__ << "null reply!";
146         return;
147     }
148     reply->deleteLater();
149 
150     QMultiMap<int, QString> playcountArtists;
151     lastfm::XmlQuery lfm;
152     if( lfm.parse( reply->readAll() ) )
153     {
154         foreach( const lastfm::XmlQuery &e, lfm[ "topartists" ].children( "artist" ) )
155         {
156             QString name = e[ "name" ].text();
157             int playcount = e[ "playcount" ].text().toInt();
158             playcountArtists.insert( playcount, name );
159         }
160     }
161     else
162     {
163         debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message();
164         return;
165     }
166 
167     int start = m_myTopArtists->childCount();
168     QModelIndex parent = index( m_myTopArtists->row(), 0 );
169     beginInsertRows( parent, start, start + playcountArtists.size() );
170 
171     QMapIterator<int, QString> it( playcountArtists );
172     it.toBack();
173     while( it.hasPrevious() )
174     {
175         it.previous();
176         int count = it.key();
177         QString text = it.value();
178         QString prettyText = i18ncp( "%2 is artist name, %1 is number of plays",
179                                      "%2 (%1 play)", "%2 (%1 plays)", count, text );
180 
181         LastFmTreeItem *artist = new LastFmTreeItem( mapTypeToUrl( LastFm::ArtistsChild, text ),
182                                                      LastFm::ArtistsChild, prettyText, m_myTopArtists );
183         m_myTopArtists->appendChild( artist );
184     }
185 
186     endInsertRows();
187     emit dataChanged( parent, parent );
188 }
189 
190 void
appendUserStations(LastFmTreeItem * item,const QString & user)191 LastFmTreeModel::appendUserStations( LastFmTreeItem* item, const QString &user )
192 {
193     // no need to call begin/endInsertRows() or dataChanged(), we're being called inside
194     // beginInsertRows().
195     LastFmTreeItem* personal = new LastFmTreeItem( mapTypeToUrl( LastFm::UserChildPersonal, user ),
196                                                    LastFm::UserChildPersonal, i18n( "Personal Radio" ), item );
197     item->appendChild( personal );
198 }
199 
200 void
prepareAvatar(QPixmap & avatar,int size)201 LastFmTreeModel::prepareAvatar( QPixmap &avatar, int size )
202 {
203     // This code is here to stop Qt from crashing on certain weirdly shaped avatars.
204     // We had a case were an avatar got a height of 1px after scaling and it would
205     // crash in the rendering code. This here just fills in the background with
206     // transparency first.
207     if( avatar.width() < size || avatar.height() < size )
208     {
209         QImage finalAvatar( size, size, QImage::Format_ARGB32 );
210         finalAvatar.fill( 0 );
211 
212         QPainter p( &finalAvatar );
213         QRect r;
214 
215         if( avatar.width() < size )
216             r = QRect( ( size - avatar.width() ) / 2, 0, avatar.width(), avatar.height() );
217         else
218             r = QRect( 0, ( size - avatar.height() ) / 2, avatar.width(), avatar.height() );
219 
220         p.drawPixmap( r, avatar );
221         p.end();
222 
223         avatar = QPixmap::fromImage( finalAvatar );
224     }
225 }
226 
227 void
onAvatarDownloaded(const QString & username,QPixmap avatar)228 LastFmTreeModel::onAvatarDownloaded( const QString &username, QPixmap avatar )
229 {
230     sender()->deleteLater();
231     if( avatar.isNull() || avatar.height() <= 0 || avatar.width() <= 0 )
232         return;
233     if( username == m_user.name() )
234         return;
235 
236     int m = avatarSize();
237     avatar = avatar.scaled( m, m, Qt::KeepAspectRatio, Qt::SmoothTransformation );
238     prepareAvatar( avatar, m );
239     m_avatars.insert( username, avatar );
240 
241     // these 2 categories have a chance to be updated:
242     QList<LastFmTreeItem *> categories;
243     categories << m_myFriends;
244 
245     // now go through all children of the categories and notify view as appropriate
246     foreach( LastFmTreeItem *category, categories )
247     {
248         QModelIndex parentIdx = index( category->row(), 0 );
249         for( int i = 0; i < category->childCount(); i++ )
250         {
251             LastFmTreeItem *item = category->child( i );
252             if( !item )
253                 continue;
254 
255             if( item->data() == username )
256             {
257                 QModelIndex idx = index( i, 0, parentIdx );
258                 emit dataChanged( idx, idx );
259                 break; // no user is twice in a single category
260             }
261         }
262     }
263 }
264 
265 QIcon
avatar(const QString & username,const QUrl & avatarUrl) const266 LastFmTreeModel::avatar( const QString &username, const QUrl &avatarUrl ) const
267 {
268     QIcon defaultIcon( "filename-artist-amarok" );
269     if( username.isEmpty() )
270         return defaultIcon;
271     if( m_avatars.contains(username) )
272         return m_avatars.value( username );
273     if( !avatarUrl.isValid() )
274         return defaultIcon;
275 
276      // insert placeholder so that we don't request the save avatar twice;
277     const_cast<LastFmTreeModel *>( this )->m_avatars.insert( username, defaultIcon );
278     AvatarDownloader* downloader = new AvatarDownloader();
279     downloader->downloadAvatar( username, avatarUrl );
280     connect( downloader, &AvatarDownloader::avatarDownloaded,
281              this, &LastFmTreeModel::onAvatarDownloaded );
282     return defaultIcon;
283 }
284 
285 int
columnCount(const QModelIndex & parent) const286 LastFmTreeModel::columnCount( const QModelIndex &parent ) const
287 {
288     Q_UNUSED( parent )
289     return 1;
290 }
291 
292 int
avatarSize()293 LastFmTreeModel::avatarSize()
294 {
295     return 32;
296 }
297 
298 QVariant
data(const QModelIndex & index,int role) const299 LastFmTreeModel::data( const QModelIndex &index, int role ) const
300 {
301     if( !index.isValid() )
302         return QVariant();
303 
304     LastFmTreeItem *i = static_cast<LastFmTreeItem*>( index.internalPointer() );
305     if( role == Qt::DisplayRole )
306         switch( i->type() )
307         {
308         case MyRecommendations:
309             return i18n( "My Recommendations" );
310         case PersonalRadio:
311             return i18n( "My Radio Station" );
312         case MixRadio:
313             return i18n( "My Mix Radio" );
314         case TopArtists:
315             return i18n( "My Top Artists" );
316         case MyTags:
317             return i18n( "My Tags" );
318         case Friends:
319             return i18n( "Friends" );
320         case FriendsChild:
321         case ArtistsChild:
322         case UserChildPersonal:
323         case MyTagsChild:
324             return i->data();
325         default:
326             break;
327         }
328 
329     if( role == Qt::DecorationRole )
330     {
331         switch( i->type() )
332         {
333         case MyRecommendations:
334             return QIcon::fromTheme( "lastfm-recommended-radio-amarok" );
335         case TopArtists:
336         case PersonalRadio:
337             return QIcon::fromTheme( "lastfm-personal-radio-amarok" );
338         case MixRadio:
339             return QIcon::fromTheme( "lastfm-mix-radio-amarok" );
340         case MyTags:
341             return QIcon::fromTheme( "lastfm-my-tags-amarok" );
342         case Friends:
343             return QIcon::fromTheme( "lastfm-my-friends-amarok" );
344 
345         case RecentlyPlayedTrack:
346             Q_FALLTHROUGH();
347         case RecentlyLovedTrack:
348             Q_FALLTHROUGH();
349         case RecentlyBannedTrack:
350             return QIcon::fromTheme( "icon_track" );
351         case MyTagsChild:
352             return QIcon::fromTheme( "lastfm-tag-amarok" );
353 
354         case FriendsChild:
355             return avatar( i->data().toString(), i->avatarUrl() );
356         case UserChildPersonal:
357             return QIcon::fromTheme( "lastfm-personal-radio-amarok" );
358 
359         case HistoryStation:
360             return QIcon::fromTheme( "icon_radio" );
361 
362         default:
363             break;
364         }
365     }
366 
367     if( role == LastFm::TrackRole )
368     {
369         switch( i->type() )
370         {
371             case LastFm::MyRecommendations:
372             case LastFm::PersonalRadio:
373             case LastFm::MixRadio:
374             case LastFm::FriendsChild:
375             case LastFm::MyTagsChild:
376             case LastFm::ArtistsChild:
377             case LastFm::UserChildPersonal:
378                 return QVariant::fromValue( i->track() );
379             default:
380                 break;
381         }
382     }
383     if( role == LastFm::TypeRole )
384         return i->type();
385 
386     return QVariant();
387 }
388 
389 Qt::ItemFlags
flags(const QModelIndex & index) const390 LastFmTreeModel::flags( const QModelIndex &index ) const
391 {
392     if( !index.isValid() )
393         return 0;
394 
395     Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsDropEnabled;
396     LastFmTreeItem *i = static_cast<LastFmTreeItem*>( index.internalPointer() );
397     switch( i->type() )
398     {
399     case MyRecommendations:
400     case PersonalRadio:
401     case MixRadio:
402     case RecentlyPlayedTrack:
403     case RecentlyLovedTrack:
404     case RecentlyBannedTrack:
405     case MyTagsChild:
406     case FriendsChild:
407     case ArtistsChild:
408     case HistoryStation:
409     case UserChildPersonal:
410         flags |= Qt::ItemIsSelectable;
411         break;
412 
413     default:
414         break;
415     }
416 
417     switch( i->type() )
418     {
419     case UserChildPersonal:
420     case MyTagsChild:
421     case ArtistsChild:
422     case MyRecommendations:
423     case PersonalRadio:
424     case MixRadio:
425         flags |= Qt::ItemIsDragEnabled;
426 
427     default:
428         break;
429     }
430 
431     return flags;
432 }
433 
434 QModelIndex
index(int row,int column,const QModelIndex & parent) const435 LastFmTreeModel::index( int row, int column, const QModelIndex &parent )
436 const
437 {
438     if( !hasIndex( row, column, parent ) )
439         return QModelIndex();
440 
441     LastFmTreeItem *parentItem;
442 
443     if( !parent.isValid() )
444         parentItem = m_rootItem;
445     else
446         parentItem = static_cast<LastFmTreeItem*>( parent.internalPointer() );
447 
448     LastFmTreeItem *childItem = parentItem->child( row );
449     if( childItem )
450         return createIndex( row, column, childItem );
451     else
452         return QModelIndex();
453 }
454 
455 QModelIndex
parent(const QModelIndex & index) const456 LastFmTreeModel::parent( const QModelIndex &index ) const
457 {
458     if( !index.isValid() )
459         return QModelIndex();
460 
461     LastFmTreeItem *childItem = static_cast<LastFmTreeItem*>( index.internalPointer() );
462     LastFmTreeItem *parentItem = childItem->parent();
463 
464     if( parentItem == m_rootItem )
465         return QModelIndex();
466 
467     return createIndex( parentItem->row(), 0, parentItem );
468 }
469 
470 int
rowCount(const QModelIndex & parent) const471 LastFmTreeModel::rowCount( const QModelIndex &parent ) const
472 {
473     LastFmTreeItem *parentItem;
474     if( parent.column() > 0 )
475         return 0;
476 
477     if( !parent.isValid() )
478         parentItem = m_rootItem;
479     else
480         parentItem = static_cast<LastFmTreeItem*>( parent.internalPointer() );
481 
482     return parentItem->childCount();
483 }
484 
485 void
setupModelData(LastFmTreeItem * parent)486 LastFmTreeModel::setupModelData( LastFmTreeItem *parent )
487 {
488     // no need to call beginInsertRows() here, this is only called from constructor
489     parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MyRecommendations ), LastFm::MyRecommendations, parent ) );
490     parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::PersonalRadio ), LastFm::PersonalRadio, parent ) );
491     parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MixRadio ), LastFm::MixRadio, parent ) );
492 
493     m_myTopArtists = new LastFmTreeItem( LastFm::TopArtists, parent );
494     parent->appendChild( m_myTopArtists );
495 
496     m_myTags = new LastFmTreeItem( LastFm::MyTags, parent );
497     parent->appendChild( m_myTags );
498 
499     m_myFriends = new LastFmTreeItem( LastFm::Friends, parent );
500     parent->appendChild( m_myFriends );
501 
502 }
503 
504 QString
mapTypeToUrl(LastFm::Type type,const QString & key)505 LastFmTreeModel::mapTypeToUrl( LastFm::Type type, const QString &key )
506 {
507     QString const encoded_username = QUrl::toPercentEncoding( m_user.name() );
508     switch( type )
509     {
510     case MyRecommendations:
511         return "lastfm://user/" + encoded_username + "/recommended";
512     case PersonalRadio:
513         return "lastfm://user/" + encoded_username + "/personal";
514     case MixRadio:
515         return "lastfm://user/" + encoded_username + "/mix";
516     case MyTagsChild:
517         return "lastfm://usertags/" + encoded_username + "/" + QUrl::toPercentEncoding( key );
518     case FriendsChild:
519         return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal";
520     case ArtistsChild:
521         return "lastfm://artist/" + QUrl::toPercentEncoding( key ) + "/similarartists";
522     case UserChildPersonal:
523         return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal";
524     default:
525         return "";
526     }
527 }
528 
LastFmTreeItem(const LastFm::Type & type,const QVariant & data,LastFmTreeItem * parent)529 LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent )
530         : mType( type ), parentItem( parent ), itemData( data )
531 {
532 }
533 
LastFmTreeItem(const LastFm::Type & type,LastFmTreeItem * parent)534 LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, LastFmTreeItem *parent )
535         : mType( type ), parentItem( parent )
536 {
537 
538 }
539 
LastFmTreeItem(const QString & url,const LastFm::Type & type,LastFmTreeItem * parent)540 LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, LastFmTreeItem *parent )
541         : mType( type ), parentItem( parent ), mUrl( url )
542 {
543 
544 }
545 
LastFmTreeItem(const QString & url,const LastFm::Type & type,const QVariant & data,LastFmTreeItem * parent)546 LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent )
547         : mType( type ), parentItem( parent ), itemData( data ), mUrl( url )
548 {
549 }
550 
~LastFmTreeItem()551 LastFmTreeItem::~LastFmTreeItem()
552 {
553     qDeleteAll( childItems );
554 }
555 
556 void
appendChild(LastFmTreeItem * item)557 LastFmTreeItem::appendChild( LastFmTreeItem *item )
558 {
559     childItems.append( item );
560 }
561 
562 LastFmTreeItem *
child(int row)563 LastFmTreeItem::child( int row )
564 {
565     return childItems.value( row );
566 }
567 
568 int
childCount() const569 LastFmTreeItem::childCount() const
570 {
571     return childItems.count();
572 }
573 
574 QVariant
data() const575 LastFmTreeItem::data() const
576 {
577     return itemData;
578 }
579 
580 Meta::TrackPtr
track() const581 LastFmTreeItem::track() const
582 {
583     Meta::TrackPtr track;
584     if( mUrl.isEmpty() )
585         return track;
586 
587     QUrl url( mUrl );
588     track = CollectionManager::instance()->trackForUrl( url );
589 
590     return track;
591 }
592 
parent()593 LastFmTreeItem *LastFmTreeItem::parent()
594 {
595     return parentItem;
596 }
597 
598 int
row() const599 LastFmTreeItem::row() const
600 {
601     if( parentItem )
602         return parentItem->childItems.indexOf( const_cast<LastFmTreeItem*>( this ) );
603 
604     return 0;
605 }
606 
607 QMimeData*
mimeData(const QModelIndexList & indices) const608 LastFmTreeModel::mimeData( const QModelIndexList &indices ) const
609 {
610     debug() << "LASTFM drag items : " << indices.size();
611     Meta::TrackList list;
612     foreach( const QModelIndex &item, indices )
613     {
614         Meta::TrackPtr track = data( item, LastFm::TrackRole ).value< Meta::TrackPtr >();
615         if( track )
616             list << track;
617     }
618     qStableSort( list.begin(), list.end(), Meta::Track::lessThan );
619 
620     AmarokMimeData *mimeData = new AmarokMimeData();
621     mimeData->setTracks( list );
622     return mimeData;
623 }
624