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