File indexing completed on 2025-01-05 04:26:56
0001 /**************************************************************************************** 0002 * Copyright (c) 2009 Leo Franchi <lfranchi@kde.org> * 0003 * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de> * 0004 * * 0005 * This program is free software; you can redistribute it and/or modify it under * 0006 * the terms of the GNU General Public License as published by the Free Software * 0007 * Foundation; either version 2 of the License, or (at your option) any later * 0008 * version. * 0009 * * 0010 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0012 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0013 * * 0014 * You should have received a copy of the GNU General Public License along with * 0015 * this program. If not, see <http://www.gnu.org/licenses/>. * 0016 ****************************************************************************************/ 0017 0018 #include "WeeklyTopBias.h" 0019 0020 #include "core/meta/Meta.h" 0021 #include "core/support/Amarok.h" 0022 #include "core/support/Debug.h" 0023 #include "core-impl/collections/support/CollectionManager.h" 0024 0025 #include <KLocalizedString> 0026 0027 #include <QDomDocument> 0028 #include <QDomElement> 0029 #include <QDomNode> 0030 #include <QLabel> 0031 #include <QNetworkReply> 0032 #include <QTimeEdit> 0033 #include <QVBoxLayout> 0034 #include <QXmlStreamReader> 0035 0036 #include <XmlQuery.h> 0037 0038 QString 0039 Dynamic::WeeklyTopBiasFactory::i18nName() const 0040 { return i18nc("Name of the \"WeeklyTop\" bias", "Last.fm weekly top artist"); } 0041 0042 QString 0043 Dynamic::WeeklyTopBiasFactory::name() const 0044 { return Dynamic::WeeklyTopBias::sName(); } 0045 0046 QString 0047 Dynamic::WeeklyTopBiasFactory::i18nDescription() const 0048 { return i18nc("Description of the \"WeeklyTop\" bias", 0049 "The \"WeeklyTop\" bias adds tracks that are in the weekly top chart of Last.fm."); } 0050 0051 Dynamic::BiasPtr 0052 Dynamic::WeeklyTopBiasFactory::createBias() 0053 { return Dynamic::BiasPtr( new Dynamic::WeeklyTopBias() ); } 0054 0055 0056 // ----- WeeklyTopBias -------- 0057 0058 0059 Dynamic::WeeklyTopBias::WeeklyTopBias() 0060 : SimpleMatchBias() 0061 , m_weeklyTimesJob( ) 0062 { 0063 m_range.from = QDateTime::currentDateTime(); 0064 m_range.to = QDateTime::currentDateTime(); 0065 loadFromFile(); 0066 } 0067 0068 Dynamic::WeeklyTopBias::~WeeklyTopBias() 0069 { } 0070 0071 0072 void 0073 Dynamic::WeeklyTopBias::fromXml( QXmlStreamReader *reader ) 0074 { 0075 loadFromFile(); 0076 0077 while (!reader->atEnd()) { 0078 reader->readNext(); 0079 0080 if( reader->isStartElement() ) 0081 { 0082 QStringRef name = reader->name(); 0083 if( name == "from" ) 0084 m_range.from = QDateTime::fromSecsSinceEpoch( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() ); 0085 else if( name == "to" ) 0086 m_range.to = QDateTime::fromSecsSinceEpoch( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() ); 0087 else 0088 { 0089 debug()<<"Unexpected xml start element"<<name<<"in input"; 0090 reader->skipCurrentElement(); 0091 } 0092 } 0093 else if( reader->isEndElement() ) 0094 { 0095 break; 0096 } 0097 } 0098 } 0099 0100 void 0101 Dynamic::WeeklyTopBias::toXml( QXmlStreamWriter *writer ) const 0102 { 0103 writer->writeTextElement( "from", QString::number( m_range.from.toSecsSinceEpoch() ) ); 0104 writer->writeTextElement( "to", QString::number( m_range.to.toSecsSinceEpoch() ) ); 0105 } 0106 0107 QString 0108 Dynamic::WeeklyTopBias::sName() 0109 { 0110 return "lastfm_weeklytop"; 0111 } 0112 0113 QString 0114 Dynamic::WeeklyTopBias::name() const 0115 { 0116 return Dynamic::WeeklyTopBias::sName(); 0117 } 0118 0119 QString 0120 Dynamic::WeeklyTopBias::toString() const 0121 { 0122 return i18nc("WeeklyTopBias bias representation", 0123 "Tracks from the Last.fm top lists from %1 to %2", m_range.from.toString(), m_range.to.toString() ); 0124 } 0125 0126 QWidget* 0127 Dynamic::WeeklyTopBias::widget( QWidget* parent ) 0128 { 0129 QWidget *widget = new QWidget( parent ); 0130 QVBoxLayout *layout = new QVBoxLayout( widget ); 0131 0132 QLabel *label = new QLabel( i18nc( "in WeeklyTopBias. Label for the date widget", "from:" ) ); 0133 QDateTimeEdit *fromEdit = new QDateTimeEdit( QDate::currentDate().addDays( -7 ) ); 0134 fromEdit->setMinimumDate( QDateTime::fromSecsSinceEpoch( 1111320001 ).date() ); // That's the first week in last fm 0135 fromEdit->setMaximumDate( QDate::currentDate() ); 0136 fromEdit->setCalendarPopup( true ); 0137 if( m_range.from.isValid() ) 0138 fromEdit->setDateTime( m_range.from ); 0139 0140 connect( fromEdit, &QDateTimeEdit::dateTimeChanged, this, &WeeklyTopBias::fromDateChanged ); 0141 label->setBuddy( fromEdit ); 0142 layout->addWidget( label ); 0143 layout->addWidget( fromEdit ); 0144 0145 label = new QLabel( i18nc( "in WeeklyTopBias. Label for the date widget", "to:" ) ); 0146 QDateTimeEdit *toEdit = new QDateTimeEdit( QDate::currentDate().addDays( -7 ) ); 0147 toEdit->setMinimumDate( QDateTime::fromSecsSinceEpoch( 1111320001 ).date() ); // That's the first week in last fm 0148 toEdit->setMaximumDate( QDate::currentDate() ); 0149 toEdit->setCalendarPopup( true ); 0150 if( m_range.to.isValid() ) 0151 toEdit->setDateTime( m_range.to ); 0152 0153 connect( toEdit, &QDateTimeEdit::dateTimeChanged, this, &WeeklyTopBias::toDateChanged ); 0154 label->setBuddy( toEdit ); 0155 layout->addWidget( label ); 0156 layout->addWidget( toEdit ); 0157 0158 return widget; 0159 } 0160 0161 0162 bool 0163 Dynamic::WeeklyTopBias::trackMatches( int position, 0164 const Meta::TrackList& playlist, 0165 int contextCount ) const 0166 { 0167 Q_UNUSED( contextCount ); 0168 0169 if( position < 0 || position >= playlist.count()) 0170 return false; 0171 0172 // - determine the current artist 0173 Meta::TrackPtr currentTrack = playlist[position-1]; 0174 Meta::ArtistPtr currentArtist = currentTrack->artist(); 0175 QString currentArtistName = currentArtist ? currentArtist->name() : QString(); 0176 0177 // - collect all the artists 0178 QStringList artists; 0179 bool weeksMissing = false; 0180 0181 uint fromTime = m_range.from.toSecsSinceEpoch(); 0182 uint toTime = m_range.to.toSecsSinceEpoch(); 0183 uint lastWeekTime = 0; 0184 foreach( uint weekTime, m_weeklyFromTimes ) 0185 { 0186 if( weekTime > fromTime && weekTime < toTime && lastWeekTime ) 0187 { 0188 if( m_weeklyArtistMap.contains( lastWeekTime ) ) 0189 { 0190 artists.append( m_weeklyArtistMap.value( lastWeekTime ) ); 0191 // debug() << "found already-saved data for week:" << lastWeekTime << m_weeklyArtistMap.value( lastWeekTime ); 0192 } 0193 else 0194 { 0195 weeksMissing = true; 0196 } 0197 } 0198 0199 lastWeekTime = weekTime; 0200 } 0201 0202 if( weeksMissing ) 0203 warning() << "didn't have a cached suggestions for weeks:" << m_range.from << "to" << m_range.to; 0204 0205 return artists.contains( currentArtistName ); 0206 } 0207 0208 void 0209 Dynamic::WeeklyTopBias::newQuery() 0210 { 0211 DEBUG_BLOCK; 0212 0213 // - check if we have week times 0214 if( m_weeklyFromTimes.isEmpty() ) 0215 { 0216 newWeeklyTimesQuery(); 0217 return; // not yet ready to do construct a query maker 0218 } 0219 0220 // - collect all the artists 0221 QStringList artists; 0222 bool weeksMissing = false; 0223 0224 uint fromTime = m_range.from.toSecsSinceEpoch(); 0225 uint toTime = m_range.to.toSecsSinceEpoch(); 0226 uint lastWeekTime = 0; 0227 foreach( uint weekTime, m_weeklyFromTimes ) 0228 { 0229 if( weekTime > fromTime && weekTime < toTime && lastWeekTime ) 0230 { 0231 if( m_weeklyArtistMap.contains( lastWeekTime ) ) 0232 { 0233 artists.append( m_weeklyArtistMap.value( lastWeekTime ) ); 0234 // debug() << "found already-saved data for week:" << lastWeekTime << m_weeklyArtistMap.value( lastWeekTime ); 0235 } 0236 else 0237 { 0238 weeksMissing = true; 0239 } 0240 } 0241 0242 lastWeekTime = weekTime; 0243 } 0244 0245 if( weeksMissing ) 0246 { 0247 newWeeklyArtistQuery(); 0248 return; // not yet ready to construct a query maker 0249 } 0250 0251 // ok, I need a new query maker 0252 m_qm.reset( CollectionManager::instance()->queryMaker() ); 0253 0254 // - construct the query 0255 m_qm->beginOr(); 0256 foreach( const QString &artist, artists ) 0257 { 0258 // debug() << "adding artist to query:" << artist; 0259 m_qm->addFilter( Meta::valArtist, artist, true, true ); 0260 } 0261 m_qm->endAndOr(); 0262 0263 m_qm->setQueryType( Collections::QueryMaker::Custom ); 0264 m_qm->addReturnValue( Meta::valUniqueId ); 0265 0266 connect( m_qm.data(), &Collections::QueryMaker::newResultReady, 0267 this, &WeeklyTopBias::updateReady ); 0268 connect( m_qm.data(), &Collections::QueryMaker::queryDone, 0269 this, &WeeklyTopBias::updateFinished ); 0270 0271 // - run the query 0272 m_qm->run(); 0273 } 0274 0275 void 0276 Dynamic::WeeklyTopBias::newWeeklyTimesQuery() 0277 { 0278 DEBUG_BLOCK 0279 0280 QMap< QString, QString > params; 0281 params[ "method" ] = "user.getWeeklyChartList" ; 0282 params[ "user" ] = lastfm::ws::Username; 0283 0284 m_weeklyTimesJob = lastfm::ws::get( params ); 0285 0286 connect( m_weeklyTimesJob, &QNetworkReply::finished, 0287 this, &WeeklyTopBias::weeklyTimesQueryFinished ); 0288 } 0289 0290 0291 void Dynamic::WeeklyTopBias::newWeeklyArtistQuery() 0292 { 0293 DEBUG_BLOCK 0294 debug() << "getting top artist info from" << m_range.from << "to" << m_range.to; 0295 0296 // - check if we have week times 0297 if( m_weeklyFromTimes.isEmpty() ) 0298 { 0299 newWeeklyTimesQuery(); 0300 return; // not yet ready to do construct a query maker 0301 } 0302 0303 // fetch 5 at a time, so as to conform to lastfm api requirements 0304 uint jobCount = m_weeklyArtistJobs.count(); 0305 if( jobCount >= 5 ) 0306 return; 0307 0308 uint fromTime = m_range.from.toSecsSinceEpoch(); 0309 uint toTime = m_range.to.toSecsSinceEpoch(); 0310 uint lastWeekTime = 0; 0311 foreach( uint weekTime, m_weeklyFromTimes ) 0312 { 0313 if( weekTime > fromTime && weekTime < toTime && lastWeekTime ) 0314 { 0315 if( m_weeklyArtistMap.contains( lastWeekTime ) ) 0316 { 0317 // we already have the data 0318 } 0319 else if( m_weeklyArtistJobs.contains( lastWeekTime ) ) 0320 { 0321 // we already fetch the data 0322 } 0323 else 0324 { 0325 QMap< QString, QString > params; 0326 params[ "method" ] = "user.getWeeklyArtistChart"; 0327 params[ "user" ] = lastfm::ws::Username; 0328 params[ "from" ] = QString::number( lastWeekTime ); 0329 params[ "to" ] = QString::number( m_weeklyToTimes[m_weeklyFromTimes.indexOf(lastWeekTime)] ); 0330 0331 QNetworkReply* reply = lastfm::ws::get( params ); 0332 connect( reply, &QNetworkReply::finished, 0333 this, &WeeklyTopBias::weeklyArtistQueryFinished ); 0334 0335 m_weeklyArtistJobs.insert( lastWeekTime, reply ); 0336 0337 jobCount++; 0338 if( jobCount >= 5 ) 0339 return; 0340 } 0341 } 0342 0343 lastWeekTime = weekTime; 0344 } 0345 } 0346 0347 0348 void 0349 Dynamic::WeeklyTopBias::weeklyArtistQueryFinished() 0350 { 0351 DEBUG_BLOCK 0352 QNetworkReply *reply = qobject_cast<QNetworkReply*>( sender() ); 0353 0354 if( !reply ) { 0355 warning() << "Failed to get qnetwork reply in finished slot."; 0356 return; 0357 } 0358 0359 0360 lastfm::XmlQuery lfm; 0361 if( lfm.parse( reply->readAll() ) ) 0362 { 0363 // debug() << "got response:" << lfm; 0364 QStringList artists; 0365 for( int i = 0; i < lfm[ "weeklyartistchart" ].children( "artist" ).size(); i++ ) 0366 { 0367 if( i == 12 ) // only up to 12 artist. 0368 break; 0369 lastfm::XmlQuery artist = lfm[ "weeklyartistchart" ].children( "artist" ).at( i ); 0370 artists.append( artist[ "name" ].text() ); 0371 } 0372 0373 uint week = QDomElement( lfm[ "weeklyartistchart" ] ).attribute( "from" ).toUInt(); 0374 m_weeklyArtistMap.insert( week, artists ); 0375 debug() << "got artists:" << artists << week; 0376 0377 if( m_weeklyArtistJobs.contains( week) ) 0378 { 0379 m_weeklyArtistJobs.remove( week ); 0380 } 0381 else 0382 { 0383 warning() << "Got a reply for a week"<<week<<"that was not requested."; 0384 return; 0385 } 0386 } 0387 else 0388 { 0389 debug() << "failed to parse weekly artist chart."; 0390 } 0391 0392 reply->deleteLater(); 0393 0394 saveDataToFile(); 0395 newQuery(); // try again to get the tracks 0396 } 0397 0398 void 0399 Dynamic::WeeklyTopBias::weeklyTimesQueryFinished() // SLOT 0400 { 0401 DEBUG_BLOCK 0402 if( !m_weeklyTimesJob ) 0403 return; // argh. where does this come from 0404 0405 QDomDocument doc; 0406 if( !doc.setContent( m_weeklyTimesJob->readAll() ) ) 0407 { 0408 debug() << "couldn't parse XML from rangeJob!"; 0409 return; 0410 } 0411 0412 QDomNodeList nodes = doc.elementsByTagName( "chart" ); 0413 if( nodes.count() == 0 ) 0414 { 0415 debug() << "USER has no history! can't do this!"; 0416 return; 0417 } 0418 0419 for( int i = 0; i < nodes.size(); i++ ) 0420 { 0421 QDomNode n = nodes.at( i ); 0422 m_weeklyFromTimes.append( n.attributes().namedItem( "from" ).nodeValue().toUInt() ); 0423 m_weeklyToTimes.append( n.attributes().namedItem( "to" ).nodeValue().toUInt() ); 0424 0425 // debug() << "weeklyTimesResult"<<i<<":"<<m_weeklyFromTimes.last()<<"to"<<m_weeklyToTimes.last(); 0426 m_weeklyFromTimes.append( n.attributes().namedItem( "from" ).nodeValue().toUInt() ); 0427 m_weeklyToTimes.append( n.attributes().namedItem( "to" ).nodeValue().toUInt() ); 0428 } 0429 0430 m_weeklyTimesJob->deleteLater(); 0431 0432 newQuery(); // try again to get the tracks 0433 } 0434 0435 0436 void 0437 Dynamic::WeeklyTopBias::fromDateChanged( const QDateTime& d ) // SLOT 0438 { 0439 if( d > m_range.to ) 0440 return; 0441 0442 m_range.from = d; 0443 invalidate(); 0444 emit changed( BiasPtr( this ) ); 0445 } 0446 0447 0448 void 0449 Dynamic::WeeklyTopBias::toDateChanged( const QDateTime& d ) // SLOT 0450 { 0451 if( d < m_range.from ) 0452 return; 0453 0454 m_range.to = d; 0455 invalidate(); 0456 emit changed( BiasPtr( this ) ); 0457 } 0458 0459 0460 void 0461 Dynamic::WeeklyTopBias::loadFromFile() 0462 { 0463 QFile file( Amarok::saveLocation() + "dynamic_lastfm_topweeklyartists.xml" ); 0464 file.open( QIODevice::ReadOnly | QIODevice::Text ); 0465 QTextStream in( &file ); 0466 while( !in.atEnd() ) 0467 { 0468 QString line = in.readLine(); 0469 m_weeklyArtistMap.insert( line.split( '#' )[ 0 ].toUInt(), line.split( '#' )[ 1 ].split( '^' ) ); 0470 } 0471 file.close(); 0472 } 0473 0474 0475 void 0476 Dynamic::WeeklyTopBias::saveDataToFile() const 0477 { 0478 QFile file( Amarok::saveLocation() + "dynamic_lastfm_topweeklyartists.xml" ); 0479 file.open( QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text ); 0480 QTextStream out( &file ); 0481 foreach( uint key, m_weeklyArtistMap.keys() ) 0482 { 0483 out << key << "#" << m_weeklyArtistMap[ key ].join( "^" ) << Qt::endl; 0484 } 0485 file.close(); 0486 0487 } 0488 0489