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 }