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