File indexing completed on 2024-05-19 04:50:16
0001 /**************************************************************************************** 0002 * Copyright (c) 2008 Casey Link <unnamedrambler@gmail.com> * 0003 * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org> * 0004 * Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org> * 0005 * * 0006 * This program is free software; you can redistribute it and/or modify it under * 0007 * the terms of the GNU General Public License as published by the Free Software * 0008 * Foundation; either version 2 of the License, or (at your option) any later * 0009 * version. * 0010 * * 0011 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0012 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0013 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0014 * * 0015 * You should have received a copy of the GNU General Public License along with * 0016 * this program. If not, see <http://www.gnu.org/licenses/>. * 0017 ****************************************************************************************/ 0018 0019 #define DEBUG_PREFIX "LastFmTreeModel" 0020 #include "core/support/Debug.h" 0021 0022 #include "LastFmTreeModel.h" 0023 0024 #include "AvatarDownloader.h" 0025 #include "core-impl/collections/support/CollectionManager.h" 0026 #include "AmarokMimeData.h" 0027 0028 #include <QIcon> 0029 #include <QPainter> 0030 0031 #include <Tag.h> 0032 #include <XmlQuery.h> 0033 0034 #include <algorithm> 0035 0036 using namespace LastFm; 0037 0038 LastFmTreeModel::LastFmTreeModel( QObject *parent ) 0039 : QAbstractItemModel( parent ) 0040 { 0041 m_rootItem = new LastFmTreeItem( LastFm::Root, "Hello" ); 0042 setupModelData( m_rootItem ); 0043 0044 QNetworkReply *reply; 0045 0046 reply = m_user.getFriends(); 0047 connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddFriends ); 0048 0049 reply = m_user.getTopTags(); 0050 connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddTags ); 0051 0052 reply = m_user.getTopArtists(); 0053 connect( reply, &QNetworkReply::finished, this, &LastFmTreeModel::slotAddTopArtists ); 0054 } 0055 0056 LastFmTreeModel::~LastFmTreeModel() 0057 { 0058 delete m_rootItem; 0059 } 0060 0061 void 0062 LastFmTreeModel::slotAddFriends() 0063 { 0064 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() ); 0065 if( !reply ) 0066 { 0067 debug() << __PRETTY_FUNCTION__ << "null reply!"; 0068 return; 0069 } 0070 reply->deleteLater(); 0071 0072 lastfm::XmlQuery lfm; 0073 if( lfm.parse( reply->readAll() ) ) 0074 { 0075 QList<lastfm::XmlQuery> children = lfm[ "friends" ].children( "user" ); 0076 int start = m_myFriends->childCount(); 0077 QModelIndex parent = index( m_myFriends->row(), 0 ); 0078 beginInsertRows( parent, start, start + children.size() ); 0079 0080 foreach( const lastfm::XmlQuery &e, children ) 0081 { 0082 const QString name = e[ "name" ].text(); 0083 0084 LastFmTreeItem* afriend = new LastFmTreeItem( mapTypeToUrl(LastFm::FriendsChild, name), 0085 LastFm::FriendsChild, name, m_myFriends ); 0086 0087 QUrl avatarUrl( e[ QLatin1String("image size=small") ].text() ); 0088 if( !avatarUrl.isEmpty() ) 0089 afriend->setAvatarUrl( avatarUrl ); 0090 0091 m_myFriends->appendChild( afriend ); 0092 appendUserStations( afriend, name ); 0093 } 0094 0095 endInsertRows(); 0096 emit dataChanged( parent, parent ); 0097 } 0098 else 0099 { 0100 debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); 0101 return; 0102 } 0103 } 0104 0105 void 0106 LastFmTreeModel::slotAddTags() 0107 { 0108 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() ); 0109 if( !reply ) 0110 { 0111 debug() << __PRETTY_FUNCTION__ << "null reply!"; 0112 return; 0113 } 0114 reply->deleteLater(); 0115 0116 QMap<int, QString> listWithWeights = lastfm::Tag::list( reply ); 0117 int start = m_myTags->childCount(); 0118 QModelIndex parent = index( m_myTags->row(), 0 ); 0119 beginInsertRows( parent, start, start + listWithWeights.size() ); 0120 0121 QMapIterator<int, QString> it( listWithWeights ); 0122 it.toBack(); 0123 while( it.hasPrevious() ) 0124 { 0125 it.previous(); 0126 int count = it.key(); 0127 QString text = it.value(); 0128 QString prettyText = i18nc( "%1 is Last.fm tag name, %2 is its usage count", 0129 "%1 (%2)", text, count ); 0130 0131 LastFmTreeItem *tag = new LastFmTreeItem( mapTypeToUrl( LastFm::MyTagsChild, text ), 0132 LastFm::MyTagsChild, prettyText, m_myTags ); 0133 m_myTags->appendChild( tag ); 0134 } 0135 0136 endInsertRows(); 0137 emit dataChanged( parent, parent ); 0138 } 0139 0140 void 0141 LastFmTreeModel::slotAddTopArtists() 0142 { 0143 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() ); 0144 if( !reply ) 0145 { 0146 debug() << __PRETTY_FUNCTION__ << "null reply!"; 0147 return; 0148 } 0149 reply->deleteLater(); 0150 0151 QMultiMap<int, QString> playcountArtists; 0152 lastfm::XmlQuery lfm; 0153 if( lfm.parse( reply->readAll() ) ) 0154 { 0155 foreach( const lastfm::XmlQuery &e, lfm[ "topartists" ].children( "artist" ) ) 0156 { 0157 QString name = e[ "name" ].text(); 0158 int playcount = e[ "playcount" ].text().toInt(); 0159 playcountArtists.insert( playcount, name ); 0160 } 0161 } 0162 else 0163 { 0164 debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); 0165 return; 0166 } 0167 0168 int start = m_myTopArtists->childCount(); 0169 QModelIndex parent = index( m_myTopArtists->row(), 0 ); 0170 beginInsertRows( parent, start, start + playcountArtists.size() ); 0171 0172 QMapIterator<int, QString> it( playcountArtists ); 0173 it.toBack(); 0174 while( it.hasPrevious() ) 0175 { 0176 it.previous(); 0177 int count = it.key(); 0178 QString text = it.value(); 0179 QString prettyText = i18ncp( "%2 is artist name, %1 is number of plays", 0180 "%2 (%1 play)", "%2 (%1 plays)", count, text ); 0181 0182 LastFmTreeItem *artist = new LastFmTreeItem( mapTypeToUrl( LastFm::ArtistsChild, text ), 0183 LastFm::ArtistsChild, prettyText, m_myTopArtists ); 0184 m_myTopArtists->appendChild( artist ); 0185 } 0186 0187 endInsertRows(); 0188 emit dataChanged( parent, parent ); 0189 } 0190 0191 void 0192 LastFmTreeModel::appendUserStations( LastFmTreeItem* item, const QString &user ) 0193 { 0194 // no need to call begin/endInsertRows() or dataChanged(), we're being called inside 0195 // beginInsertRows(). 0196 LastFmTreeItem* personal = new LastFmTreeItem( mapTypeToUrl( LastFm::UserChildPersonal, user ), 0197 LastFm::UserChildPersonal, i18n( "Personal Radio" ), item ); 0198 item->appendChild( personal ); 0199 } 0200 0201 void 0202 LastFmTreeModel::prepareAvatar( QPixmap &avatar, int size ) 0203 { 0204 // This code is here to stop Qt from crashing on certain weirdly shaped avatars. 0205 // We had a case were an avatar got a height of 1px after scaling and it would 0206 // crash in the rendering code. This here just fills in the background with 0207 // transparency first. 0208 if( avatar.width() < size || avatar.height() < size ) 0209 { 0210 QImage finalAvatar( size, size, QImage::Format_ARGB32 ); 0211 finalAvatar.fill( 0 ); 0212 0213 QPainter p( &finalAvatar ); 0214 QRect r; 0215 0216 if( avatar.width() < size ) 0217 r = QRect( ( size - avatar.width() ) / 2, 0, avatar.width(), avatar.height() ); 0218 else 0219 r = QRect( 0, ( size - avatar.height() ) / 2, avatar.width(), avatar.height() ); 0220 0221 p.drawPixmap( r, avatar ); 0222 p.end(); 0223 0224 avatar = QPixmap::fromImage( finalAvatar ); 0225 } 0226 } 0227 0228 void 0229 LastFmTreeModel::onAvatarDownloaded( const QString &username, QPixmap avatar ) 0230 { 0231 sender()->deleteLater(); 0232 if( avatar.isNull() || avatar.height() <= 0 || avatar.width() <= 0 ) 0233 return; 0234 if( username == m_user.name() ) 0235 return; 0236 0237 int m = avatarSize(); 0238 avatar = avatar.scaled( m, m, Qt::KeepAspectRatio, Qt::SmoothTransformation ); 0239 prepareAvatar( avatar, m ); 0240 m_avatars.insert( username, avatar ); 0241 0242 // these 2 categories have a chance to be updated: 0243 QList<LastFmTreeItem *> categories; 0244 categories << m_myFriends; 0245 0246 // now go through all children of the categories and notify view as appropriate 0247 foreach( LastFmTreeItem *category, categories ) 0248 { 0249 QModelIndex parentIdx = index( category->row(), 0 ); 0250 for( int i = 0; i < category->childCount(); i++ ) 0251 { 0252 LastFmTreeItem *item = category->child( i ); 0253 if( !item ) 0254 continue; 0255 0256 if( item->data() == username ) 0257 { 0258 QModelIndex idx = index( i, 0, parentIdx ); 0259 emit dataChanged( idx, idx ); 0260 break; // no user is twice in a single category 0261 } 0262 } 0263 } 0264 } 0265 0266 QIcon 0267 LastFmTreeModel::avatar( const QString &username, const QUrl &avatarUrl ) const 0268 { 0269 QIcon defaultIcon( "filename-artist-amarok" ); 0270 if( username.isEmpty() ) 0271 return defaultIcon; 0272 if( m_avatars.contains(username) ) 0273 return m_avatars.value( username ); 0274 if( !avatarUrl.isValid() ) 0275 return defaultIcon; 0276 0277 // insert placeholder so that we don't request the save avatar twice; 0278 const_cast<LastFmTreeModel *>( this )->m_avatars.insert( username, defaultIcon ); 0279 AvatarDownloader* downloader = new AvatarDownloader(); 0280 downloader->downloadAvatar( username, avatarUrl ); 0281 connect( downloader, &AvatarDownloader::avatarDownloaded, 0282 this, &LastFmTreeModel::onAvatarDownloaded ); 0283 return defaultIcon; 0284 } 0285 0286 int 0287 LastFmTreeModel::columnCount( const QModelIndex &parent ) const 0288 { 0289 Q_UNUSED( parent ) 0290 return 1; 0291 } 0292 0293 int 0294 LastFmTreeModel::avatarSize() 0295 { 0296 return 32; 0297 } 0298 0299 QVariant 0300 LastFmTreeModel::data( const QModelIndex &index, int role ) const 0301 { 0302 if( !index.isValid() ) 0303 return QVariant(); 0304 0305 LastFmTreeItem *i = static_cast<LastFmTreeItem*>( index.internalPointer() ); 0306 if( role == Qt::DisplayRole ) 0307 switch( i->type() ) 0308 { 0309 case MyRecommendations: 0310 return i18n( "My Recommendations" ); 0311 case PersonalRadio: 0312 return i18n( "My Radio Station" ); 0313 case MixRadio: 0314 return i18n( "My Mix Radio" ); 0315 case TopArtists: 0316 return i18n( "My Top Artists" ); 0317 case MyTags: 0318 return i18n( "My Tags" ); 0319 case Friends: 0320 return i18n( "Friends" ); 0321 case FriendsChild: 0322 case ArtistsChild: 0323 case UserChildPersonal: 0324 case MyTagsChild: 0325 return i->data(); 0326 default: 0327 break; 0328 } 0329 0330 if( role == Qt::DecorationRole ) 0331 { 0332 switch( i->type() ) 0333 { 0334 case MyRecommendations: 0335 return QIcon::fromTheme( "lastfm-recommended-radio-amarok" ); 0336 case TopArtists: 0337 case PersonalRadio: 0338 return QIcon::fromTheme( "lastfm-personal-radio-amarok" ); 0339 case MixRadio: 0340 return QIcon::fromTheme( "lastfm-mix-radio-amarok" ); 0341 case MyTags: 0342 return QIcon::fromTheme( "lastfm-my-tags-amarok" ); 0343 case Friends: 0344 return QIcon::fromTheme( "lastfm-my-friends-amarok" ); 0345 0346 case RecentlyPlayedTrack: 0347 Q_FALLTHROUGH(); 0348 case RecentlyLovedTrack: 0349 Q_FALLTHROUGH(); 0350 case RecentlyBannedTrack: 0351 return QIcon::fromTheme( "icon_track" ); 0352 case MyTagsChild: 0353 return QIcon::fromTheme( "lastfm-tag-amarok" ); 0354 0355 case FriendsChild: 0356 return avatar( i->data().toString(), i->avatarUrl() ); 0357 case UserChildPersonal: 0358 return QIcon::fromTheme( "lastfm-personal-radio-amarok" ); 0359 0360 case HistoryStation: 0361 return QIcon::fromTheme( "icon_radio" ); 0362 0363 default: 0364 break; 0365 } 0366 } 0367 0368 if( role == LastFm::TrackRole ) 0369 { 0370 switch( i->type() ) 0371 { 0372 case LastFm::MyRecommendations: 0373 case LastFm::PersonalRadio: 0374 case LastFm::MixRadio: 0375 case LastFm::FriendsChild: 0376 case LastFm::MyTagsChild: 0377 case LastFm::ArtistsChild: 0378 case LastFm::UserChildPersonal: 0379 return QVariant::fromValue( i->track() ); 0380 default: 0381 break; 0382 } 0383 } 0384 if( role == LastFm::TypeRole ) 0385 return i->type(); 0386 0387 return QVariant(); 0388 } 0389 0390 Qt::ItemFlags 0391 LastFmTreeModel::flags( const QModelIndex &index ) const 0392 { 0393 if( !index.isValid() ) 0394 return {}; 0395 0396 Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsDropEnabled; 0397 LastFmTreeItem *i = static_cast<LastFmTreeItem*>( index.internalPointer() ); 0398 switch( i->type() ) 0399 { 0400 case MyRecommendations: 0401 case PersonalRadio: 0402 case MixRadio: 0403 case RecentlyPlayedTrack: 0404 case RecentlyLovedTrack: 0405 case RecentlyBannedTrack: 0406 case MyTagsChild: 0407 case FriendsChild: 0408 case ArtistsChild: 0409 case HistoryStation: 0410 case UserChildPersonal: 0411 flags |= Qt::ItemIsSelectable; 0412 break; 0413 0414 default: 0415 break; 0416 } 0417 0418 switch( i->type() ) 0419 { 0420 case UserChildPersonal: 0421 case MyTagsChild: 0422 case ArtistsChild: 0423 case MyRecommendations: 0424 case PersonalRadio: 0425 case MixRadio: 0426 flags |= Qt::ItemIsDragEnabled; 0427 0428 default: 0429 break; 0430 } 0431 0432 return flags; 0433 } 0434 0435 QModelIndex 0436 LastFmTreeModel::index( int row, int column, const QModelIndex &parent ) 0437 const 0438 { 0439 if( !hasIndex( row, column, parent ) ) 0440 return QModelIndex(); 0441 0442 LastFmTreeItem *parentItem; 0443 0444 if( !parent.isValid() ) 0445 parentItem = m_rootItem; 0446 else 0447 parentItem = static_cast<LastFmTreeItem*>( parent.internalPointer() ); 0448 0449 LastFmTreeItem *childItem = parentItem->child( row ); 0450 if( childItem ) 0451 return createIndex( row, column, childItem ); 0452 else 0453 return QModelIndex(); 0454 } 0455 0456 QModelIndex 0457 LastFmTreeModel::parent( const QModelIndex &index ) const 0458 { 0459 if( !index.isValid() ) 0460 return QModelIndex(); 0461 0462 LastFmTreeItem *childItem = static_cast<LastFmTreeItem*>( index.internalPointer() ); 0463 LastFmTreeItem *parentItem = childItem->parent(); 0464 0465 if( parentItem == m_rootItem ) 0466 return QModelIndex(); 0467 0468 return createIndex( parentItem->row(), 0, parentItem ); 0469 } 0470 0471 int 0472 LastFmTreeModel::rowCount( const QModelIndex &parent ) const 0473 { 0474 LastFmTreeItem *parentItem; 0475 if( parent.column() > 0 ) 0476 return 0; 0477 0478 if( !parent.isValid() ) 0479 parentItem = m_rootItem; 0480 else 0481 parentItem = static_cast<LastFmTreeItem*>( parent.internalPointer() ); 0482 0483 return parentItem->childCount(); 0484 } 0485 0486 void 0487 LastFmTreeModel::setupModelData( LastFmTreeItem *parent ) 0488 { 0489 // no need to call beginInsertRows() here, this is only called from constructor 0490 parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MyRecommendations ), LastFm::MyRecommendations, parent ) ); 0491 parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::PersonalRadio ), LastFm::PersonalRadio, parent ) ); 0492 parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MixRadio ), LastFm::MixRadio, parent ) ); 0493 0494 m_myTopArtists = new LastFmTreeItem( LastFm::TopArtists, parent ); 0495 parent->appendChild( m_myTopArtists ); 0496 0497 m_myTags = new LastFmTreeItem( LastFm::MyTags, parent ); 0498 parent->appendChild( m_myTags ); 0499 0500 m_myFriends = new LastFmTreeItem( LastFm::Friends, parent ); 0501 parent->appendChild( m_myFriends ); 0502 0503 } 0504 0505 QString 0506 LastFmTreeModel::mapTypeToUrl( LastFm::Type type, const QString &key ) 0507 { 0508 QString const encoded_username = QUrl::toPercentEncoding( m_user.name() ); 0509 switch( type ) 0510 { 0511 case MyRecommendations: 0512 return "lastfm://user/" + encoded_username + "/recommended"; 0513 case PersonalRadio: 0514 return "lastfm://user/" + encoded_username + "/personal"; 0515 case MixRadio: 0516 return "lastfm://user/" + encoded_username + "/mix"; 0517 case MyTagsChild: 0518 return "lastfm://usertags/" + encoded_username + "/" + QUrl::toPercentEncoding( key ); 0519 case FriendsChild: 0520 return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal"; 0521 case ArtistsChild: 0522 return "lastfm://artist/" + QUrl::toPercentEncoding( key ) + "/similarartists"; 0523 case UserChildPersonal: 0524 return "lastfm://user/" + QUrl::toPercentEncoding( key ) + "/personal"; 0525 default: 0526 return ""; 0527 } 0528 } 0529 0530 LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent ) 0531 : mType( type ), parentItem( parent ), itemData( data ) 0532 { 0533 } 0534 0535 LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, LastFmTreeItem *parent ) 0536 : mType( type ), parentItem( parent ) 0537 { 0538 0539 } 0540 0541 LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, LastFmTreeItem *parent ) 0542 : mType( type ), parentItem( parent ), mUrl( url ) 0543 { 0544 0545 } 0546 0547 LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent ) 0548 : mType( type ), parentItem( parent ), itemData( data ), mUrl( url ) 0549 { 0550 } 0551 0552 LastFmTreeItem::~LastFmTreeItem() 0553 { 0554 qDeleteAll( childItems ); 0555 } 0556 0557 void 0558 LastFmTreeItem::appendChild( LastFmTreeItem *item ) 0559 { 0560 childItems.append( item ); 0561 } 0562 0563 LastFmTreeItem * 0564 LastFmTreeItem::child( int row ) 0565 { 0566 return childItems.value( row ); 0567 } 0568 0569 int 0570 LastFmTreeItem::childCount() const 0571 { 0572 return childItems.count(); 0573 } 0574 0575 QVariant 0576 LastFmTreeItem::data() const 0577 { 0578 return itemData; 0579 } 0580 0581 Meta::TrackPtr 0582 LastFmTreeItem::track() const 0583 { 0584 Meta::TrackPtr track; 0585 if( mUrl.isEmpty() ) 0586 return track; 0587 0588 QUrl url( mUrl ); 0589 track = CollectionManager::instance()->trackForUrl( url ); 0590 0591 return track; 0592 } 0593 0594 LastFmTreeItem *LastFmTreeItem::parent() 0595 { 0596 return parentItem; 0597 } 0598 0599 int 0600 LastFmTreeItem::row() const 0601 { 0602 if( parentItem ) 0603 return parentItem->childItems.indexOf( const_cast<LastFmTreeItem*>( this ) ); 0604 0605 return 0; 0606 } 0607 0608 QMimeData* 0609 LastFmTreeModel::mimeData( const QModelIndexList &indices ) const 0610 { 0611 debug() << "LASTFM drag items : " << indices.size(); 0612 Meta::TrackList list; 0613 foreach( const QModelIndex &item, indices ) 0614 { 0615 Meta::TrackPtr track = data( item, LastFm::TrackRole ).value< Meta::TrackPtr >(); 0616 if( track ) 0617 list << track; 0618 } 0619 std::stable_sort( list.begin(), list.end(), Meta::Track::lessThan ); 0620 0621 AmarokMimeData *mimeData = new AmarokMimeData(); 0622 mimeData->setTracks( list ); 0623 return mimeData; 0624 }