1 /****************************************************************************************
2  * Copyright (c) 2009 Leo Franchi <lfranchi@kde.org>                                    *
3  * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de>                                     *
4  *                                                                                      *
5  * This program is free software; you can redistribute it and/or modify it under        *
6  * the terms of the GNU General Public License as published by the Free Software        *
7  * Foundation; either version 2 of the License, or (at your option) any later           *
8  * version.                                                                             *
9  *                                                                                      *
10  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
11  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
12  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
13  *                                                                                      *
14  * You should have received a copy of the GNU General Public License along with         *
15  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
16  ****************************************************************************************/
17 
18 #include "WeeklyTopBias.h"
19 
20 #include "core/meta/Meta.h"
21 #include "core/support/Amarok.h"
22 #include "core/support/Debug.h"
23 #include "core-impl/collections/support/CollectionManager.h"
24 
25 #include <KLocalizedString>
26 
27 #include <QDomDocument>
28 #include <QDomElement>
29 #include <QDomNode>
30 #include <QLabel>
31 #include <QNetworkReply>
32 #include <QTimeEdit>
33 #include <QVBoxLayout>
34 #include <QXmlStreamReader>
35 
36 #include <XmlQuery.h>
37 
38 QString
i18nName() const39 Dynamic::WeeklyTopBiasFactory::i18nName() const
40 { return i18nc("Name of the \"WeeklyTop\" bias", "Last.fm weekly top artist"); }
41 
42 QString
name() const43 Dynamic::WeeklyTopBiasFactory::name() const
44 { return Dynamic::WeeklyTopBias::sName(); }
45 
46 QString
i18nDescription() const47 Dynamic::WeeklyTopBiasFactory::i18nDescription() const
48 { return i18nc("Description of the \"WeeklyTop\" bias",
49                    "The \"WeeklyTop\" bias adds tracks that are in the weekly top chart of Last.fm."); }
50 
51 Dynamic::BiasPtr
createBias()52 Dynamic::WeeklyTopBiasFactory::createBias()
53 { return Dynamic::BiasPtr( new Dynamic::WeeklyTopBias() ); }
54 
55 
56 // ----- WeeklyTopBias --------
57 
58 
WeeklyTopBias()59 Dynamic::WeeklyTopBias::WeeklyTopBias()
60     : SimpleMatchBias()
61     , m_weeklyTimesJob( )
62 {
63     m_range.from = QDateTime::currentDateTime();
64     m_range.to = QDateTime::currentDateTime();
65     loadFromFile();
66 }
67 
~WeeklyTopBias()68 Dynamic::WeeklyTopBias::~WeeklyTopBias()
69 { }
70 
71 
72 void
fromXml(QXmlStreamReader * reader)73 Dynamic::WeeklyTopBias::fromXml( QXmlStreamReader *reader )
74 {
75     loadFromFile();
76 
77     while (!reader->atEnd()) {
78         reader->readNext();
79 
80         if( reader->isStartElement() )
81         {
82             QStringRef name = reader->name();
83             if( name == "from" )
84                 m_range.from = QDateTime::fromSecsSinceEpoch( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() );
85             else if( name == "to" )
86                 m_range.to = QDateTime::fromSecsSinceEpoch( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() );
87             else
88             {
89                 debug()<<"Unexpected xml start element"<<name<<"in input";
90                 reader->skipCurrentElement();
91             }
92         }
93         else if( reader->isEndElement() )
94         {
95             break;
96         }
97     }
98 }
99 
100 void
toXml(QXmlStreamWriter * writer) const101 Dynamic::WeeklyTopBias::toXml( QXmlStreamWriter *writer ) const
102 {
103     writer->writeTextElement( "from", QString::number( m_range.from.toSecsSinceEpoch() ) );
104     writer->writeTextElement( "to",   QString::number( m_range.to.toSecsSinceEpoch() ) );
105 }
106 
107 QString
sName()108 Dynamic::WeeklyTopBias::sName()
109 {
110     return "lastfm_weeklytop";
111 }
112 
113 QString
name() const114 Dynamic::WeeklyTopBias::name() const
115 {
116     return Dynamic::WeeklyTopBias::sName();
117 }
118 
119 QString
toString() const120 Dynamic::WeeklyTopBias::toString() const
121 {
122     return i18nc("WeeklyTopBias bias representation",
123                  "Tracks from the Last.fm top lists from %1 to %2", m_range.from.toString(), m_range.to.toString() );
124 }
125 
126 QWidget*
widget(QWidget * parent)127 Dynamic::WeeklyTopBias::widget( QWidget* parent )
128 {
129     QWidget *widget = new QWidget( parent );
130     QVBoxLayout *layout = new QVBoxLayout( widget );
131 
132     QLabel *label = new QLabel( i18nc( "in WeeklyTopBias. Label for the date widget", "from:" ) );
133     QDateTimeEdit *fromEdit = new QDateTimeEdit( QDate::currentDate().addDays( -7 ) );
134     fromEdit->setMinimumDate( QDateTime::fromSecsSinceEpoch( 1111320001 ).date() ); // That's the first week in last fm
135     fromEdit->setMaximumDate( QDate::currentDate() );
136     fromEdit->setCalendarPopup( true );
137     if( m_range.from.isValid() )
138         fromEdit->setDateTime( m_range.from );
139 
140     connect( fromEdit, &QDateTimeEdit::dateTimeChanged, this, &WeeklyTopBias::fromDateChanged );
141     label->setBuddy( fromEdit );
142     layout->addWidget( label );
143     layout->addWidget( fromEdit );
144 
145     label = new QLabel( i18nc( "in WeeklyTopBias. Label for the date widget", "to:" ) );
146     QDateTimeEdit *toEdit = new QDateTimeEdit( QDate::currentDate().addDays( -7 ) );
147     toEdit->setMinimumDate( QDateTime::fromSecsSinceEpoch( 1111320001 ).date() ); // That's the first week in last fm
148     toEdit->setMaximumDate( QDate::currentDate() );
149     toEdit->setCalendarPopup( true );
150     if( m_range.to.isValid() )
151         toEdit->setDateTime( m_range.to );
152 
153     connect( toEdit, &QDateTimeEdit::dateTimeChanged, this, &WeeklyTopBias::toDateChanged );
154     label->setBuddy( toEdit );
155     layout->addWidget( label );
156     layout->addWidget( toEdit );
157 
158     return widget;
159 }
160 
161 
162 bool
trackMatches(int position,const Meta::TrackList & playlist,int contextCount) const163 Dynamic::WeeklyTopBias::trackMatches( int position,
164                                    const Meta::TrackList& playlist,
165                                    int contextCount ) const
166 {
167     Q_UNUSED( contextCount );
168 
169     if( position < 0 || position >= playlist.count())
170         return false;
171 
172     // - determine the current artist
173     Meta::TrackPtr currentTrack = playlist[position-1];
174     Meta::ArtistPtr currentArtist = currentTrack->artist();
175     QString currentArtistName = currentArtist ? currentArtist->name() : QString();
176 
177     // - collect all the artists
178     QStringList artists;
179     bool weeksMissing = false;
180 
181     uint fromTime = m_range.from.toSecsSinceEpoch();
182     uint toTime   = m_range.to.toSecsSinceEpoch();
183     uint lastWeekTime = 0;
184     foreach( uint weekTime, m_weeklyFromTimes )
185     {
186         if( weekTime > fromTime && weekTime < toTime && lastWeekTime )
187         {
188             if( m_weeklyArtistMap.contains( lastWeekTime ) )
189             {
190                 artists.append( m_weeklyArtistMap.value( lastWeekTime ) );
191                 // debug() << "found already-saved data for week:" << lastWeekTime << m_weeklyArtistMap.value( lastWeekTime );
192             }
193             else
194             {
195                 weeksMissing = true;
196             }
197         }
198 
199        lastWeekTime = weekTime;
200     }
201 
202     if( weeksMissing )
203         warning() << "didn't have a cached suggestions for weeks:" << m_range.from << "to" << m_range.to;
204 
205     return artists.contains( currentArtistName );
206 }
207 
208 void
newQuery()209 Dynamic::WeeklyTopBias::newQuery()
210 {
211     DEBUG_BLOCK;
212 
213     // - check if we have week times
214     if( m_weeklyFromTimes.isEmpty() )
215     {
216         newWeeklyTimesQuery();
217         return; // not yet ready to do construct a query maker
218     }
219 
220     // - collect all the artists
221     QStringList artists;
222     bool weeksMissing = false;
223 
224     uint fromTime = m_range.from.toSecsSinceEpoch();
225     uint toTime   = m_range.to.toSecsSinceEpoch();
226     uint lastWeekTime = 0;
227     foreach( uint weekTime, m_weeklyFromTimes )
228     {
229         if( weekTime > fromTime && weekTime < toTime && lastWeekTime )
230         {
231             if( m_weeklyArtistMap.contains( lastWeekTime ) )
232             {
233                 artists.append( m_weeklyArtistMap.value( lastWeekTime ) );
234                 // debug() << "found already-saved data for week:" << lastWeekTime << m_weeklyArtistMap.value( lastWeekTime );
235             }
236             else
237             {
238                 weeksMissing = true;
239             }
240         }
241 
242        lastWeekTime = weekTime;
243     }
244 
245     if( weeksMissing )
246     {
247         newWeeklyArtistQuery();
248         return; // not yet ready to construct a query maker
249     }
250 
251     // ok, I need a new query maker
252     m_qm.reset( CollectionManager::instance()->queryMaker() );
253 
254     // - construct the query
255     m_qm->beginOr();
256     foreach( const QString &artist, artists )
257     {
258         // debug() << "adding artist to query:" << artist;
259         m_qm->addFilter( Meta::valArtist, artist, true, true );
260     }
261     m_qm->endAndOr();
262 
263     m_qm->setQueryType( Collections::QueryMaker::Custom );
264     m_qm->addReturnValue( Meta::valUniqueId );
265 
266     connect( m_qm.data(), &Collections::QueryMaker::newResultReady,
267              this, &WeeklyTopBias::updateReady );
268     connect( m_qm.data(), &Collections::QueryMaker::queryDone,
269              this, &WeeklyTopBias::updateFinished );
270 
271     // - run the query
272     m_qm->run();
273 }
274 
275 void
newWeeklyTimesQuery()276 Dynamic::WeeklyTopBias::newWeeklyTimesQuery()
277 {
278     DEBUG_BLOCK
279 
280     QMap< QString, QString > params;
281     params[ "method" ] = "user.getWeeklyChartList" ;
282     params[ "user" ] = lastfm::ws::Username;
283 
284     m_weeklyTimesJob = lastfm::ws::get( params );
285 
286     connect( m_weeklyTimesJob, &QNetworkReply::finished,
287              this, &WeeklyTopBias::weeklyTimesQueryFinished );
288 }
289 
290 
newWeeklyArtistQuery()291 void Dynamic::WeeklyTopBias::newWeeklyArtistQuery()
292 {
293     DEBUG_BLOCK
294     debug() << "getting top artist info from" << m_range.from << "to" << m_range.to;
295 
296     // - check if we have week times
297     if( m_weeklyFromTimes.isEmpty() )
298     {
299         newWeeklyTimesQuery();
300         return; // not yet ready to do construct a query maker
301     }
302 
303     // fetch 5 at a time, so as to conform to lastfm api requirements
304     uint jobCount = m_weeklyArtistJobs.count();
305     if( jobCount >= 5 )
306         return;
307 
308     uint fromTime = m_range.from.toSecsSinceEpoch();
309     uint toTime   = m_range.to.toSecsSinceEpoch();
310     uint lastWeekTime = 0;
311     foreach( uint weekTime, m_weeklyFromTimes )
312     {
313         if( weekTime > fromTime && weekTime < toTime && lastWeekTime )
314         {
315             if( m_weeklyArtistMap.contains( lastWeekTime ) )
316             {
317                 // we already have the data
318             }
319             else if( m_weeklyArtistJobs.contains( lastWeekTime ) )
320             {
321                 // we already fetch the data
322             }
323             else
324             {
325                 QMap< QString, QString > params;
326                 params[ "method" ] = "user.getWeeklyArtistChart";
327                 params[ "user" ] = lastfm::ws::Username;
328                 params[ "from" ] = QString::number( lastWeekTime );
329                 params[ "to" ] = QString::number( m_weeklyToTimes[m_weeklyFromTimes.indexOf(lastWeekTime)] );
330 
331                 QNetworkReply* reply = lastfm::ws::get( params );
332                 connect( reply, &QNetworkReply::finished,
333                          this, &WeeklyTopBias::weeklyArtistQueryFinished );
334 
335                 m_weeklyArtistJobs.insert( lastWeekTime, reply );
336 
337                 jobCount++;
338                 if( jobCount >= 5 )
339                     return;
340             }
341         }
342 
343        lastWeekTime = weekTime;
344     }
345 }
346 
347 
348 void
weeklyArtistQueryFinished()349 Dynamic::WeeklyTopBias::weeklyArtistQueryFinished()
350 {
351     DEBUG_BLOCK
352     QNetworkReply *reply = qobject_cast<QNetworkReply*>( sender() );
353 
354     if( !reply ) {
355         warning() << "Failed to get qnetwork reply in finished slot.";
356         return;
357     }
358 
359 
360     lastfm::XmlQuery lfm;
361     if( lfm.parse( reply->readAll() ) )
362     {
363         // debug() << "got response:" << lfm;
364         QStringList artists;
365         for( int i = 0; i < lfm[ "weeklyartistchart" ].children( "artist" ).size(); i++ )
366         {
367             if( i == 12 ) // only up to 12 artist.
368                 break;
369             lastfm::XmlQuery artist = lfm[ "weeklyartistchart" ].children( "artist" ).at( i );
370             artists.append( artist[ "name" ].text() );
371         }
372 
373         uint week = QDomElement( lfm[ "weeklyartistchart" ] ).attribute( "from" ).toUInt();
374         m_weeklyArtistMap.insert( week, artists );
375         debug() << "got artists:" << artists << week;
376 
377         if( m_weeklyArtistJobs.contains( week) )
378         {
379             m_weeklyArtistJobs.remove( week );
380         }
381         else
382         {
383             warning() << "Got a reply for a week"<<week<<"that was not requested.";
384             return;
385         }
386     }
387     else
388     {
389         debug() << "failed to parse weekly artist chart.";
390     }
391 
392     reply->deleteLater();
393 
394     saveDataToFile();
395     newQuery(); // try again to get the tracks
396 }
397 
398 void
weeklyTimesQueryFinished()399 Dynamic::WeeklyTopBias::weeklyTimesQueryFinished() // SLOT
400 {
401     DEBUG_BLOCK
402     if( !m_weeklyTimesJob )
403         return; // argh. where does this come from
404 
405     QDomDocument doc;
406     if( !doc.setContent( m_weeklyTimesJob->readAll() ) )
407     {
408         debug() << "couldn't parse XML from rangeJob!";
409         return;
410     }
411 
412     QDomNodeList nodes = doc.elementsByTagName( "chart" );
413     if( nodes.count() == 0 )
414     {
415         debug() << "USER has no history! can't do this!";
416         return;
417     }
418 
419     for( int i = 0; i < nodes.size(); i++ )
420     {
421         QDomNode n = nodes.at( i );
422         m_weeklyFromTimes.append( n.attributes().namedItem( "from" ).nodeValue().toUInt() );
423         m_weeklyToTimes.append( n.attributes().namedItem( "to" ).nodeValue().toUInt() );
424 
425         // debug() << "weeklyTimesResult"<<i<<":"<<m_weeklyFromTimes.last()<<"to"<<m_weeklyToTimes.last();
426         m_weeklyFromTimes.append( n.attributes().namedItem( "from" ).nodeValue().toUInt() );
427         m_weeklyToTimes.append( n.attributes().namedItem( "to" ).nodeValue().toUInt() );
428     }
429 
430     m_weeklyTimesJob->deleteLater();
431 
432     newQuery(); // try again to get the tracks
433 }
434 
435 
436 void
fromDateChanged(const QDateTime & d)437 Dynamic::WeeklyTopBias::fromDateChanged( const QDateTime& d ) // SLOT
438 {
439     if( d > m_range.to )
440         return;
441 
442     m_range.from = d;
443     invalidate();
444     emit changed( BiasPtr( this ) );
445 }
446 
447 
448 void
toDateChanged(const QDateTime & d)449 Dynamic::WeeklyTopBias::toDateChanged( const QDateTime& d ) // SLOT
450 {
451     if( d < m_range.from )
452         return;
453 
454     m_range.to = d;
455     invalidate();
456     emit changed( BiasPtr( this ) );
457 }
458 
459 
460 void
loadFromFile()461 Dynamic::WeeklyTopBias::loadFromFile()
462 {
463     QFile file( Amarok::saveLocation() + "dynamic_lastfm_topweeklyartists.xml" );
464     file.open( QIODevice::ReadOnly | QIODevice::Text );
465     QTextStream in( &file );
466     while( !in.atEnd() )
467     {
468         QString line = in.readLine();
469         m_weeklyArtistMap.insert( line.split( '#' )[ 0 ].toUInt(), line.split( '#' )[ 1 ].split( '^' )  );
470     }
471     file.close();
472 }
473 
474 
475 void
saveDataToFile() const476 Dynamic::WeeklyTopBias::saveDataToFile() const
477 {
478     QFile file( Amarok::saveLocation() + "dynamic_lastfm_topweeklyartists.xml" );
479     file.open( QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text );
480     QTextStream out( &file );
481     foreach( uint key, m_weeklyArtistMap.keys() )
482     {
483         out << key << "#" << m_weeklyArtistMap[ key ].join( "^" ) << endl;
484     }
485     file.close();
486 
487 }
488 
489 
490