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