File indexing completed on 2024-05-19 04:50:17

0001 /****************************************************************************************
0002  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "SynchronizationAdapter.h"
0018 
0019 #include "MetaValues.h"
0020 #include "core/support/Debug.h"
0021 #include "core/support/SemaphoreReleaser.h"
0022 #include "services/lastfm/SynchronizationTrack.h"
0023 
0024 #include <KLocalizedString>
0025 
0026 #include <QNetworkReply>
0027 
0028 #include <Library.h>
0029 #include <XmlQuery.h>
0030 
0031 const int SynchronizationAdapter::s_entriesPerQuery( 200 );
0032 
0033 SynchronizationAdapter::SynchronizationAdapter( const LastFmServiceConfigPtr &config )
0034     : m_config( config )
0035 {
0036     connect( this, &SynchronizationAdapter::startArtistSearch, this, &SynchronizationAdapter::slotStartArtistSearch );
0037     connect( this, &SynchronizationAdapter::startTrackSearch, this, &SynchronizationAdapter::slotStartTrackSearch );
0038     connect( this, &SynchronizationAdapter::startTagSearch, this, &SynchronizationAdapter::slotStartTagSearch );
0039 }
0040 
0041 SynchronizationAdapter::~SynchronizationAdapter()
0042 {
0043 }
0044 
0045 QString
0046 SynchronizationAdapter::id() const
0047 {
0048     return "lastfm";
0049 }
0050 
0051 QString
0052 SynchronizationAdapter::prettyName() const
0053 {
0054     return i18n( "Last.fm" );
0055 }
0056 
0057 QString
0058 SynchronizationAdapter::description() const
0059 {
0060     return i18nc( "description of the Last.fm statistics synchronization provider",
0061                   "slows down track matching" );
0062 }
0063 
0064 QIcon
0065 SynchronizationAdapter::icon() const
0066 {
0067     return QIcon::fromTheme( "view-services-lastfm-amarok" );
0068 }
0069 
0070 qint64
0071 SynchronizationAdapter::reliableTrackMetaData() const
0072 {
0073     return Meta::valArtist | Meta::valAlbum | Meta::valTitle;
0074 }
0075 
0076 qint64
0077 SynchronizationAdapter::writableTrackStatsData() const
0078 {
0079     bool useRating = m_config->useFancyRatingTags();
0080     return ( useRating ? Meta::valRating : 0 ) | Meta::valLabel;
0081 }
0082 
0083 StatSyncing::Provider::Preference
0084 SynchronizationAdapter::defaultPreference()
0085 {
0086     return StatSyncing::Provider::Never; // don't overload Last.fm servers
0087 }
0088 
0089 QSet<QString>
0090 SynchronizationAdapter::artists()
0091 {
0092     DEBUG_BLOCK
0093     Q_ASSERT( m_semaphore.available() == 0 );
0094     emit startArtistSearch( 1 ); // Last.fm indexes from 1
0095 
0096     m_semaphore.acquire();
0097     QSet<QString> ret = m_artists;
0098     m_artists.clear(); // save memory
0099     debug() << __PRETTY_FUNCTION__ << ret.count() << "artists total";
0100     return ret;
0101 }
0102 
0103 StatSyncing::TrackList
0104 SynchronizationAdapter::artistTracks( const QString &artistName )
0105 {
0106     /* This method should match track artists case-sensitively, but we don't do it.
0107      * Last.fm webservice returns only the preferred capitalisation in artists(), so no
0108      * duplicates threat us. */
0109     Q_ASSERT( m_semaphore.available() == 0 );
0110     emit startTrackSearch( artistName, 1 ); // Last.fm indexes from 1
0111 
0112     m_semaphore.acquire();
0113     debug() << __PRETTY_FUNCTION__ << m_tracks.count() << "tracks from" << artistName
0114             << m_tagQueue.count() << "of them have tags";
0115 
0116     // fetch tags
0117     QMutableListIterator<StatSyncing::TrackPtr> it( m_tagQueue );
0118     while( it.hasNext() )
0119     {
0120         StatSyncing::TrackPtr track = it.next();
0121         emit startTagSearch( track->artist(), track->name() );
0122         m_semaphore.acquire();
0123         it.remove();
0124     }
0125 
0126     StatSyncing::TrackList ret = m_tracks;
0127     m_tracks.clear(); // save memory
0128     m_tagQueue.clear(); // paranoia
0129     return ret;
0130 }
0131 
0132 void
0133 SynchronizationAdapter::slotStartArtistSearch( int page )
0134 {
0135     QString user = m_config->username();
0136     QNetworkReply *reply = lastfm::Library::getArtists( user, s_entriesPerQuery, page );
0137     connect( reply, &QNetworkReply::finished, this, &SynchronizationAdapter::slotArtistsReceived );
0138 }
0139 
0140 void
0141 SynchronizationAdapter::slotStartTrackSearch( QString artistName, int page )
0142 {
0143     lastfm::Artist artist( artistName );
0144     QString user = m_config->username();
0145     QNetworkReply *reply = lastfm::Library::getTracks( user, artist, s_entriesPerQuery, page );
0146     connect( reply, &QNetworkReply::finished, this, &SynchronizationAdapter::slotTracksReceived );
0147 }
0148 
0149 void
0150 SynchronizationAdapter::slotStartTagSearch( QString artistName, QString trackName )
0151 {
0152     lastfm::MutableTrack track;
0153     track.setArtist( artistName );
0154     track.setTitle( trackName );
0155     QNetworkReply *reply = track.getTags();
0156     connect( reply, &QNetworkReply::finished, this, &SynchronizationAdapter::slotTagsReceived );
0157 }
0158 
0159 void
0160 SynchronizationAdapter::slotArtistsReceived()
0161 {
0162     SemaphoreReleaser releaser( &m_semaphore );
0163     QNetworkReply *reply =  qobject_cast<QNetworkReply *>( sender() );
0164     if( !reply )
0165     {
0166         warning() << __PRETTY_FUNCTION__ << "cannot cast sender to QNetworkReply. (?)";
0167         return;
0168     }
0169     reply->deleteLater();
0170 
0171     lastfm::XmlQuery lfm;
0172     if( !lfm.parse( reply->readAll() ) )
0173     {
0174         warning() << __PRETTY_FUNCTION__ << "Error parsing Last.fm reply:" << lfm.parseError().message();
0175         return;
0176     }
0177     lastfm::XmlQuery artists = lfm[ "artists" ];
0178     bool ok = false;
0179     int page = artists.attribute( "page" ).toInt( &ok );
0180     if( !ok )
0181     {
0182         warning() << __PRETTY_FUNCTION__ << "cannot read page number";
0183         return;
0184     }
0185     int totalPages = artists.attribute( "totalPages" ).toInt( &ok );
0186     if( !ok )
0187     {
0188         warning() << __PRETTY_FUNCTION__ << "cannot read total number or pages";
0189         return;
0190     }
0191     debug() << __PRETTY_FUNCTION__ << "page" << page << "of" << totalPages;
0192 
0193     // following is based on lastfm::Artist::list():
0194     foreach( const lastfm::XmlQuery &xq, lfm.children( "artist" ) )
0195     {
0196         lastfm::Artist artist( xq );
0197         m_artists.insert( artist.name() );
0198     }
0199 
0200     // Last.fm indexes from 1!
0201     if( page < totalPages )
0202     {
0203         releaser.dontRelease(); // don't release the semaphore yet
0204         emit startArtistSearch( page + 1 );
0205     }
0206 }
0207 
0208 void
0209 SynchronizationAdapter::slotTracksReceived()
0210 {
0211     SemaphoreReleaser releaser( &m_semaphore );
0212     QNetworkReply *reply =  qobject_cast<QNetworkReply *>( sender() );
0213     if( !reply )
0214     {
0215         warning() << __PRETTY_FUNCTION__ << "cannot cast sender to QNetworkReply. (?)";
0216         return;
0217     }
0218     reply->deleteLater();
0219 
0220     lastfm::XmlQuery lfm;
0221     if( !lfm.parse( reply->readAll() ) )
0222     {
0223         warning() << __PRETTY_FUNCTION__ << "Error parsing Last.fm reply:" << lfm.parseError().message();
0224         return;
0225     }
0226     lastfm::XmlQuery tracks = lfm[ "tracks" ];
0227     bool ok = false;
0228     int page = tracks.attribute( "page" ).toInt( &ok );
0229     if( !ok )
0230     {
0231         warning() << __PRETTY_FUNCTION__ << "cannot read page number";
0232         return;
0233     }
0234     int totalPages = tracks.attribute( "totalPages" ).toInt( &ok );
0235     if( !ok )
0236     {
0237         warning() << __PRETTY_FUNCTION__ << "cannot read total number or pages";
0238         return;
0239     }
0240     QString searchedArtist = tracks.attribute( "artist" );
0241     if( searchedArtist.isEmpty() )
0242     {
0243         warning() << __PRETTY_FUNCTION__ << "searchedArtist in Last.fm reply is empty";
0244         return;
0245     }
0246 
0247     // following is based on lastfm::Track::list():
0248     foreach( const lastfm::XmlQuery &xq, lfm.children( "track" ) )
0249     {
0250         QString name = xq[ "name" ].text();
0251         int playCount = xq[ "playcount" ].text().toInt();
0252         int tagCount = xq[ "tagcount" ].text().toInt();
0253         QString artist = xq[ "artist" ][ "name" ].text();
0254         QString album = xq[ "album" ][ "name" ].text();
0255 
0256         bool useRatings = m_config->useFancyRatingTags();
0257         StatSyncing::TrackPtr track( new SynchronizationTrack( artist, album, name,
0258                                                                playCount, useRatings ) );
0259         m_tracks.append( track );
0260         if( tagCount > 0 )
0261             m_tagQueue.append( track );
0262     }
0263 
0264     // Last.fm indexes from 1!
0265     if( page < totalPages )
0266     {
0267         releaser.dontRelease(); // don't release the semaphore yet
0268         emit startTrackSearch( searchedArtist, page + 1 );
0269     }
0270 }
0271 
0272 void
0273 SynchronizationAdapter::slotTagsReceived()
0274 {
0275     SemaphoreReleaser releaser( &m_semaphore );
0276     QNetworkReply *reply =  qobject_cast<QNetworkReply *>( sender() );
0277     if( !reply )
0278     {
0279         warning() << __PRETTY_FUNCTION__ << "cannot cast sender to QNetworkReply. (?)";
0280         return;
0281     }
0282     reply->deleteLater();
0283 
0284     lastfm::XmlQuery lfm;
0285     if( !lfm.parse( reply->readAll() ) )
0286     {
0287         warning() << __PRETTY_FUNCTION__ << "Error parsing Last.fm reply:" << lfm.parseError().message();
0288         return;
0289     }
0290     QSet<QString> tags;
0291     foreach( const lastfm::XmlQuery &xq, lfm.children( "tag" ) )
0292     {
0293         tags.insert( xq[ "name" ].text() );
0294     }
0295     Q_ASSERT( !m_tagQueue.isEmpty() );
0296     SynchronizationTrack *track = dynamic_cast<SynchronizationTrack *>( m_tagQueue.first().data() );
0297     Q_ASSERT( track );
0298     track->parseAndSaveLastFmTags( tags );
0299 }