1 /*
2 Copyright 2009 Last.fm Ltd.
3 - Primarily authored by Max Howell, Jono Cole and Doug Mansell
4
5 This file is part of liblastfm.
6
7 liblastfm is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 liblastfm is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with liblastfm. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "Track.h"
22 #include "User.h"
23 #include "UrlBuilder.h"
24 #include "XmlQuery.h"
25 #include "ws.h"
26
27 #include <QFileInfo>
28 #include <QStringList>
29 #include <QAbstractNetworkCache>
30 #include <QDebug>
31
32
33 class lastfm::TrackContextPrivate
34 {
35 public:
36 TrackContext::Type m_type;
37 QList<QString> m_values;
38 static TrackContext::Type getType( const QString& typeString );
39 };
40
41 lastfm::TrackContext::Type
getType(const QString & typeString)42 lastfm::TrackContextPrivate::getType( const QString& typeString )
43 {
44 lastfm::TrackContext::Type type = lastfm::TrackContext::UnknownType;
45
46 if ( typeString == "artist" )
47 type = lastfm::TrackContext::Artist;
48 else if ( typeString == "user" )
49 type = lastfm::TrackContext::User;
50 else if ( typeString == "neighbour" )
51 type = lastfm::TrackContext::Neighbour;
52 else if ( typeString == "friend" )
53 type = lastfm::TrackContext::Friend;
54
55 return type;
56 }
57
58
TrackContext()59 lastfm::TrackContext::TrackContext()
60 :d( new TrackContextPrivate )
61 {
62 d->m_type = UnknownType;
63 }
64
TrackContext(const QString & type,const QList<QString> & values)65 lastfm::TrackContext::TrackContext( const QString& type, const QList<QString>& values )
66 :d( new TrackContextPrivate )
67 {
68 d->m_values = values;
69 d->m_type = d->getType( type );
70 }
71
TrackContext(const TrackContext & that)72 lastfm::TrackContext::TrackContext( const TrackContext& that )
73 :d( new TrackContextPrivate( *that.d ) )
74 {
75 }
76
~TrackContext()77 lastfm::TrackContext::~TrackContext()
78 {
79 delete d;
80 }
81
82 lastfm::TrackContext::Type
type() const83 lastfm::TrackContext::type() const
84 {
85 return d->m_type;
86 }
87
88
89 QList<QString>
values() const90 lastfm::TrackContext::values() const
91 {
92 return d->m_values;
93 }
94
95 lastfm::TrackContext&
operator =(const TrackContext & that)96 lastfm::TrackContext::operator=( const TrackContext& that )
97 {
98 d->m_type = that.d->m_type;
99 d->m_values = that.d->m_values;
100 return *this;
101 }
102
103 class TrackObject : public QObject
104 {
105 Q_OBJECT
106 public:
TrackObject(lastfm::TrackData & data)107 TrackObject( lastfm::TrackData& data ) : m_data( data ) {;}
108
109 public:
110 void forceLoveToggled( bool love );
111 void forceScrobbleStatusChanged();
112 void forceCorrected( QString correction );
113
114 private slots:
115 void onLoveFinished();
116 void onUnloveFinished();
117 void onGotInfo();
118
119 signals:
120 void loveToggled( bool love );
121 void scrobbleStatusChanged( short scrobbleStatus );
122 void corrected( QString correction );
123
124 private:
125 lastfm::TrackData& m_data;
126 };
127
128 class lastfm::TrackData : public QSharedData
129 {
130 friend class TrackObject;
131
132 public:
133 TrackData();
134 ~TrackData();
135
136 public:
137 lastfm::Artist artist;
138 lastfm::Artist albumArtist;
139 lastfm::Album album;
140 QString title;
141 lastfm::Artist correctedArtist;
142 lastfm::Artist correctedAlbumArtist;
143 lastfm::Album correctedAlbum;
144 QString correctedTitle;
145 TrackContext context;
146 uint trackNumber;
147 uint duration;
148 short source;
149 short rating;
150 QString mbid; /// musicbrainz id
151 uint fpid;
152 QUrl url;
153 QDateTime time; /// the time the track was started at
154 lastfm::Track::LoveStatus loved;
155 QMap<AbstractType::ImageSize, QUrl> m_images;
156 short scrobbleStatus;
157 short scrobbleError;
158 QString scrobbleErrorText;
159
160 //FIXME I hate this, but is used for radio trackauth etc.
161 QMap<QString,QString> extras;
162
163 struct Observer
164 {
165 QNetworkReply* reply;
166 QPointer<QObject> receiver;
167 const char* method;
168 };
169
170 QList<Observer> observers;
171
172 bool null;
173
174 bool podcast;
175 bool video;
176
177 TrackObject* trackObject;
178 };
179
180
181
TrackData()182 lastfm::TrackData::TrackData()
183 : trackNumber( 0 ),
184 duration( 0 ),
185 source( Track::UnknownSource ),
186 rating( 0 ),
187 fpid( -1 ),
188 loved( Track::UnknownLoveStatus ),
189 scrobbleStatus( Track::Null ),
190 scrobbleError( Track::None ),
191 null( false ),
192 podcast( false ),
193 video( false )
194 {
195 trackObject = new TrackObject( *this );
196 }
197
~TrackData()198 lastfm::TrackData::~TrackData()
199 {
200 delete trackObject;
201 }
202
Track()203 lastfm::Track::Track()
204 :AbstractType()
205 {
206 d = new TrackData;
207 d->null = true;
208 }
209
Track(const Track & that)210 lastfm::Track::Track( const Track& that )
211 :AbstractType(), d( that.d )
212 {
213 }
214
Track(const QDomElement & e)215 lastfm::Track::Track( const QDomElement& e )
216 :AbstractType()
217 {
218 d = new TrackData;
219
220 if (e.isNull()) { d->null = true; return; }
221
222 //TODO: not sure of lastfm's xml changed, but <track> nodes have
223 // <artist><name>Artist Name</name><mbid>..<url></artist>
224 // as children isntead of <artist>Artist Name<artist>
225 // we detect both here.
226 QDomNode artistName = e.namedItem( "artist" ).namedItem( "name" );
227 if( artistName.isNull() ) {
228 d->artist = e.namedItem( "artist" ).toElement().text();
229 } else {
230 d->artist = artistName.toElement().text();
231
232 }
233
234 //TODO: not sure if lastfm xml's changed, or if chart.getTopTracks uses
235 //a different format, but the title is stored at
236 //<track><name>Title</name>...
237 //we detect both here.
238 QDomNode trackTitle = e.namedItem( "name" );
239 if( trackTitle.isNull() )
240 d->title = e.namedItem( "track" ).toElement().text();
241 else
242 d->title = trackTitle.toElement().text();
243
244 d->albumArtist = e.namedItem( "albumArtist" ).toElement().text();
245 d->album = Album( d->artist, e.namedItem( "album" ).toElement().text() );
246 d->correctedArtist = e.namedItem( "correctedArtist" ).toElement().text();
247 d->correctedAlbumArtist = e.namedItem( "correctedAlbumArtist" ).toElement().text();
248 d->correctedAlbum = Album( d->correctedArtist, e.namedItem( "correctedAlbum" ).toElement().text() );
249 d->correctedTitle = e.namedItem( "correctedTrack" ).toElement().text();
250 d->trackNumber = 0;
251 d->duration = e.namedItem( "duration" ).toElement().text().toInt();
252 d->url = e.namedItem( "url" ).toElement().text();
253 d->rating = e.namedItem( "rating" ).toElement().text().toUInt();
254 d->source = e.namedItem( "source" ).toElement().text().toInt(); //defaults to 0, or lastfm::Track::UnknownSource
255 d->time = QDateTime::fromTime_t( e.namedItem( "timestamp" ).toElement().text().toUInt() );
256 d->loved = static_cast<LoveStatus>(e.namedItem( "loved" ).toElement().text().toInt());
257 d->scrobbleStatus = e.namedItem( "scrobbleStatus" ).toElement().text().toInt();
258 d->scrobbleError = e.namedItem( "scrobbleError" ).toElement().text().toInt();
259 d->scrobbleErrorText = e.namedItem( "scrobbleErrorText" ).toElement().text();
260 d->podcast = e.namedItem( "podcast" ).toElement().text().toInt();
261 d->video = e.namedItem( "video" ).toElement().text().toInt();
262
263 for (QDomElement image = e.firstChildElement("image") ; !image.isNull() ; image = image.nextSiblingElement("image"))
264 d->m_images[static_cast<ImageSize>(image.attribute("size").toInt())] = image.text();
265
266 QDomNode artistNode = e.namedItem("artistImages");
267
268 for (QDomElement artistImage = artistNode.firstChildElement("image") ; !artistImage.isNull() ; artistImage = artistImage.nextSiblingElement("image"))
269 artist().setImageUrl( static_cast<ImageSize>(artistImage.attribute("size").toInt()), artistImage.text() );
270
271 QDomNode albumNode = e.namedItem("albumImages");
272
273 for (QDomElement albumImage = albumNode.firstChildElement("image") ; !albumImage.isNull() ; albumImage = albumImage.nextSiblingElement("image"))
274 album().setImageUrl( static_cast<ImageSize>(albumImage.attribute("size").toInt()), albumImage.text() );
275
276
277 QDomNodeList nodes = e.namedItem( "extras" ).childNodes();
278 for (int i = 0; i < nodes.count(); ++i)
279 {
280 QDomNode n = nodes.at(i);
281 QString key = n.nodeName();
282 d->extras[key] = n.toElement().text();
283 }
284 }
285
286 void
onLoveFinished()287 TrackObject::onLoveFinished()
288 {
289 lastfm::XmlQuery lfm;
290
291 if ( lfm.parse( static_cast<QNetworkReply*>(sender()) ) )
292 {
293 if ( lfm.attribute( "status" ) == "ok")
294 m_data.loved = lastfm::Track::Loved;
295
296 }
297
298 emit loveToggled( m_data.loved == lastfm::Track::Loved );
299 }
300
301
302 void
onUnloveFinished()303 TrackObject::onUnloveFinished()
304 {
305 lastfm::XmlQuery lfm;
306
307 if ( lfm.parse( static_cast<QNetworkReply*>(sender()) ) )
308 {
309 if ( lfm.attribute( "status" ) == "ok")
310 m_data.loved = lastfm::Track::Unloved;
311 }
312
313 emit loveToggled( m_data.loved == lastfm::Track::Loved );
314 }
315
316 void
onGotInfo()317 TrackObject::onGotInfo()
318 {
319 lastfm::TrackData::Observer observer;
320
321 for ( int i = 0 ; i < m_data.observers.count() ; ++i )
322 {
323 if ( m_data.observers.at( i ).reply == sender() )
324 {
325 observer = m_data.observers.takeAt( i );
326 break;
327 }
328 }
329
330 QNetworkReply* reply = static_cast<QNetworkReply*>(sender());
331 reply->deleteLater();
332 const QByteArray data = reply->readAll();
333
334 lastfm::XmlQuery lfm;
335
336 if ( lfm.parse( data ) )
337 {
338 QString imageUrl = lfm["track"]["image size=small"].text();
339 if ( !imageUrl.isEmpty() ) m_data.m_images[lastfm::AbstractType::SmallImage] = imageUrl;
340 imageUrl = lfm["track"]["image size=medium"].text();
341 if ( !imageUrl.isEmpty() ) m_data.m_images[lastfm::AbstractType::MediumImage] = imageUrl;
342 imageUrl = lfm["track"]["image size=large"].text();
343 if ( !imageUrl.isEmpty() ) m_data.m_images[lastfm::AbstractType::LargeImage] = imageUrl;
344 imageUrl = lfm["track"]["image size=extralarge"].text();
345 if ( !imageUrl.isEmpty() ) m_data.m_images[lastfm::AbstractType::ExtraLargeImage] = imageUrl;
346 imageUrl = lfm["track"]["image size=mega"].text();
347 if ( !imageUrl.isEmpty() ) m_data.m_images[lastfm::AbstractType::MegaImage] = imageUrl;
348
349 if ( lfm["track"]["userloved"].text().length() > 0 )
350 m_data.loved = lfm["track"]["userloved"].text() == "0" ? lastfm::Track::Unloved : lastfm::Track::Loved;
351
352 if ( observer.receiver )
353 if ( !QMetaObject::invokeMethod( observer.receiver, observer.method, Q_ARG(QByteArray, data) ) )
354 QMetaObject::invokeMethod( observer.receiver, observer.method );
355
356 emit loveToggled( m_data.loved == lastfm::Track::Loved );
357 }
358 else
359 {
360 if ( observer.receiver )
361 if ( !QMetaObject::invokeMethod( observer.receiver, observer.method, Q_ARG(QByteArray, data) ) )
362 QMetaObject::invokeMethod( observer.receiver, observer.method );
363 }
364 }
365
366 void
forceLoveToggled(bool love)367 TrackObject::forceLoveToggled( bool love )
368 {
369 emit loveToggled( love );
370 }
371
372 void
forceScrobbleStatusChanged()373 TrackObject::forceScrobbleStatusChanged()
374 {
375 emit scrobbleStatusChanged( m_data.scrobbleStatus );
376 }
377
378 void
forceCorrected(QString correction)379 TrackObject::forceCorrected( QString correction )
380 {
381 emit corrected( correction );
382 }
383
384
385 lastfm::Track&
operator =(const Track & that)386 lastfm::Track::operator=( const Track& that )
387 {
388 d = that.d;
389 return *this;
390 }
391
~Track()392 lastfm::Track::~Track()
393 {
394 }
395
396
397 lastfm::Track
clone() const398 lastfm::Track::clone() const
399 {
400 Track clone = *this;
401 clone.d.detach();
402 return clone;
403 }
404
405
406 QDomElement
toDomElement(QDomDocument & xml) const407 lastfm::Track::toDomElement( QDomDocument& xml ) const
408 {
409 QDomElement item = xml.createElement( "track" );
410
411 #define makeElement( tagname, getter ) { \
412 QString v = getter; \
413 if (!v.isEmpty()) \
414 { \
415 QDomElement e = xml.createElement( tagname ); \
416 e.appendChild( xml.createTextNode( v ) ); \
417 item.appendChild( e ); \
418 } \
419 }
420
421 makeElement( "artist", d->artist );
422 makeElement( "albumArtist", d->albumArtist );
423 makeElement( "album", d->album );
424 makeElement( "track", d->title );
425 makeElement( "correctedArtist", d->correctedArtist );
426 makeElement( "correctedAlbumArtist", d->correctedAlbumArtist );
427 makeElement( "correctedAlbum", d->correctedAlbum );
428 makeElement( "correctedTrack", d->correctedTitle );
429 makeElement( "duration", QString::number( d->duration ) );
430 makeElement( "timestamp", QString::number( d->time.toTime_t() ) );
431 makeElement( "url", d->url.toString() );
432 makeElement( "source", QString::number( d->source ) );
433 makeElement( "rating", QString::number(d->rating) );
434 makeElement( "fpId", QString::number(d->fpid) );
435 makeElement( "mbId", mbid() );
436 makeElement( "loved", QString::number( d->loved ) );
437 makeElement( "scrobbleStatus", QString::number( scrobbleStatus() ) );
438 makeElement( "scrobbleError", QString::number( scrobbleError() ) );
439 makeElement( "scrobbleErrorText", scrobbleErrorText() );
440 makeElement( "podcast", QString::number( isPodcast() ) );
441 makeElement( "video", QString::number( isVideo() ) );
442
443 // put the images urls in the dom
444 QMapIterator<ImageSize, QUrl> imageIter( d->m_images );
445 while (imageIter.hasNext()) {
446 QDomElement e = xml.createElement( "image" );
447 e.appendChild( xml.createTextNode( imageIter.next().value().toString() ) );
448 e.setAttribute( "size", imageIter.key() );
449 item.appendChild( e );
450 }
451
452 QDomElement artistElement = xml.createElement( "artistImages" );
453
454 for ( int size = SmallImage ; size <= MegaImage ; ++size )
455 {
456 QString imageUrl = d->artist.imageUrl( static_cast<ImageSize>(size) ).toString();
457
458 if ( !imageUrl.isEmpty() )
459 {
460 QDomElement e = xml.createElement( "image" );
461 e.appendChild( xml.createTextNode( d->artist.imageUrl( static_cast<ImageSize>(size) ).toString() ) );
462 e.setAttribute( "size", size );
463 artistElement.appendChild( e );
464 }
465 }
466
467 if ( artistElement.childNodes().count() != 0 )
468 item.appendChild( artistElement );
469
470 QDomElement albumElement = xml.createElement( "albumImages" );
471
472 for ( int size = SmallImage ; size <= MegaImage ; ++size )
473 {
474 QString imageUrl = d->album.imageUrl( static_cast<ImageSize>(size) ).toString();
475
476 if ( !imageUrl.isEmpty() )
477 {
478 QDomElement e = xml.createElement( "image" );
479 e.appendChild( xml.createTextNode( d->album.imageUrl( static_cast<ImageSize>(size) ).toString() ) );
480 e.setAttribute( "size", size );
481 albumElement.appendChild( e );
482 }
483 }
484
485 if ( albumElement.childNodes().count() != 0 )
486 item.appendChild( albumElement );
487
488 // add the extras to the dom
489 QDomElement extras = xml.createElement( "extras" );
490 QMapIterator<QString, QString> extrasIter( d->extras );
491 while (extrasIter.hasNext()) {
492 QDomElement e = xml.createElement( extrasIter.next().key() );
493 e.appendChild( xml.createTextNode( extrasIter.value() ) );
494 extras.appendChild( e );
495 }
496 item.appendChild( extras );
497
498 return item;
499 }
500
501
502 bool
corrected() const503 lastfm::Track::corrected() const
504 {
505 // If any of the corrected string have been set and they are different
506 // from the initial strings then this track has been corrected.
507 return ( (!d->correctedTitle.isEmpty() && (d->correctedTitle != d->title))
508 || (!d->correctedAlbum.toString().isEmpty() && (d->correctedAlbum.toString() != d->album.toString()))
509 || (!d->correctedArtist.isNull() && (d->correctedArtist.name() != d->artist.name()))
510 || (!d->correctedAlbumArtist.isNull() && (d->correctedAlbumArtist.name() != d->albumArtist.name())));
511 }
512
513 lastfm::Artist
artist(Corrections corrected) const514 lastfm::Track::artist( Corrections corrected ) const
515 {
516 if ( corrected == Corrected && !d->correctedArtist.name().isEmpty() )
517 return d->correctedArtist;
518
519 return d->artist;
520 }
521
522 lastfm::Artist
albumArtist(Corrections corrected) const523 lastfm::Track::albumArtist( Corrections corrected ) const
524 {
525 if ( corrected == Corrected && !d->correctedAlbumArtist.name().isEmpty() )
526 return d->correctedAlbumArtist;
527
528 return d->albumArtist;
529 }
530
531 lastfm::Album
album(Corrections corrected) const532 lastfm::Track::album( Corrections corrected ) const
533 {
534 if ( corrected == Corrected && !d->correctedAlbum.title().isEmpty() )
535 return d->correctedAlbum;
536
537 return d->album;
538 }
539
540 QString
title(Corrections corrected) const541 lastfm::Track::title( Corrections corrected ) const
542 {
543 /** if no title is set, return the musicbrainz unknown identifier
544 * in case some part of the GUI tries to display it anyway. Note isNull
545 * returns false still. So you should have queried this! */
546
547 if ( corrected == Corrected && !d->correctedTitle.isEmpty() )
548 return d->correctedTitle;
549
550 return d->title;
551 }
552
553
554 QUrl
imageUrl(ImageSize size,bool square) const555 lastfm::Track::imageUrl( ImageSize size, bool square ) const
556 {
557 if( !square ) return d->m_images.value( size );
558
559 QUrl url = d->m_images.value( size );
560 QRegExp re( "/serve/(\\d*)s?/" );
561 return QUrl( url.toString().replace( re, "/serve/\\1s/" ));
562 }
563
564
565 QString
toString(const QChar & separator,Corrections corrections) const566 lastfm::Track::toString( const QChar& separator, Corrections corrections ) const
567 {
568 if ( d->artist.name().isEmpty() )
569 {
570 if ( d->title.isEmpty() )
571 return QFileInfo( d->url.path() ).fileName();
572 else
573 return title( corrections );
574 }
575
576 if ( d->title.isEmpty() )
577 return artist( corrections );
578
579 return artist( corrections ) + ' ' + separator + ' ' + title( corrections );
580 }
581
582
583 QString //static
durationString(int const duration)584 lastfm::Track::durationString( int const duration )
585 {
586 QTime t = QTime().addSecs( duration );
587 if (duration < 60*60)
588 return t.toString( "m:ss" );
589 else
590 return t.toString( "hh:mm:ss" );
591 }
592
593
594 QNetworkReply*
share(const QStringList & recipients,const QString & message,bool isPublic) const595 lastfm::Track::share( const QStringList& recipients, const QString& message, bool isPublic ) const
596 {
597 QMap<QString, QString> map = params("share");
598 map["recipient"] = recipients.join(",");
599 map["public"] = isPublic ? "1" : "0";
600 if (message.size()) map["message"] = message;
601 return ws::post(map);
602 }
603
604 void
setFromLfm(const XmlQuery & lfm)605 lastfm::MutableTrack::setFromLfm( const XmlQuery& lfm )
606 {
607 QString imageUrl = lfm["track"]["image size=small"].text();
608 if ( !imageUrl.isEmpty() ) d->m_images[SmallImage] = imageUrl;
609 imageUrl = lfm["track"]["image size=medium"].text();
610 if ( !imageUrl.isEmpty() ) d->m_images[MediumImage] = imageUrl;
611 imageUrl = lfm["track"]["image size=large"].text();
612 if ( !imageUrl.isEmpty() ) d->m_images[LargeImage] = imageUrl;
613 imageUrl = lfm["track"]["image size=extralarge"].text();
614 if ( !imageUrl.isEmpty() ) d->m_images[ExtraLargeImage] = imageUrl;
615 imageUrl = lfm["track"]["image size=mega"].text();
616 if ( !imageUrl.isEmpty() ) d->m_images[MegaImage] = imageUrl;
617
618 if ( lfm["track"]["userloved"].text().length() > 0)
619 d->loved = lfm["track"]["userloved"].text() == "0" ? Unloved : Loved;
620
621 d->trackObject->forceLoveToggled( d->loved == Loved );
622 }
623
624 void
setImageUrl(ImageSize size,const QString & url)625 lastfm::MutableTrack::setImageUrl( ImageSize size, const QString& url )
626 {
627 if ( !url.isEmpty() )
628 d->m_images[size] = url;
629 }
630
631
632 void
love()633 lastfm::MutableTrack::love()
634 {
635 QNetworkReply* reply = ws::post(params("love"));
636 QObject::connect( reply, SIGNAL(finished()), signalProxy(), SLOT(onLoveFinished()));
637 }
638
639
640 void
unlove()641 lastfm::MutableTrack::unlove()
642 {
643 QNetworkReply* reply = ws::post(params("unlove"));
644 QObject::connect( reply, SIGNAL(finished()), signalProxy(), SLOT(onUnloveFinished()));
645 }
646
647 QNetworkReply*
ban()648 lastfm::MutableTrack::ban()
649 {
650 d->extras["rating"] = "B";
651 return ws::post(params("ban"));
652 }
653
654
655 QMap<QString, QString>
params(const QString & method,bool use_mbid) const656 lastfm::Track::params( const QString& method, bool use_mbid ) const
657 {
658 QMap<QString, QString> map;
659 map["method"] = "Track."+method;
660 if (d->mbid.size() && use_mbid)
661 map["mbid"] = d->mbid;
662 else {
663 map["artist"] = d->artist;
664 map["track"] = d->title;
665 }
666 return map;
667 }
668
669
670 QNetworkReply*
getSimilar(int limit) const671 lastfm::Track::getSimilar( int limit ) const
672 {
673 QMap<QString, QString> map = params("getSimilar");
674 if ( limit != -1 ) map["limit"] = QString::number( limit );
675 map["autocorrect"] = "1";
676 return ws::get( map );
677 }
678
679
680 QMap<int, QPair< QString, QString > > /* static */
getSimilar(QNetworkReply * r)681 lastfm::Track::getSimilar( QNetworkReply* r )
682 {
683 QMap<int, QPair< QString, QString > > tracks;
684 try
685 {
686 XmlQuery lfm;
687
688 if ( lfm.parse( r ) )
689 {
690 foreach (XmlQuery e, lfm.children( "track" ))
691 {
692 QPair< QString, QString > track;
693 track.first = e["name"].text();
694
695 XmlQuery artist = e.children( "artist" ).first();
696 track.second = artist["name"].text();
697
698 // convert floating percentage to int in range 0 to 10,000
699 int const match = e["match"].text().toFloat() * 100;
700 tracks.insertMulti( match, track );
701 }
702 }
703 }
704 catch (ws::ParseError& e)
705 {
706 qWarning() << e.message();
707 }
708
709 return tracks;
710 }
711
712
713 QNetworkReply*
getTopTags() const714 lastfm::Track::getTopTags() const
715 {
716 return ws::get( params("getTopTags", true) );
717 }
718
719
720 QNetworkReply*
getTopFans() const721 lastfm::Track::getTopFans() const
722 {
723 return ws::get( params("getTopFans", true) );
724 }
725
726
727 QNetworkReply*
getTags() const728 lastfm::Track::getTags() const
729 {
730 return ws::get( params("getTags", true) );
731 }
732
733 void
getInfo(QObject * receiver,const char * method,const QString & username) const734 lastfm::Track::getInfo( QObject *receiver, const char *method, const QString &username ) const
735 {
736 QMap<QString, QString> map = params("getInfo", true);
737 if (!username.isEmpty()) map["username"] = username;
738
739 // this is so the web services knows whether to use corrections or not
740 if (!lastfm::ws::SessionKey.isEmpty()) map["sk"] = lastfm::ws::SessionKey;
741
742 QNetworkReply* reply = ws::get( map );
743
744 TrackData::Observer observer;
745 observer.receiver = receiver;
746 observer.method = method;
747 observer.reply = reply;
748 d->observers << observer;
749
750 QObject::connect( reply, SIGNAL(finished()), d->trackObject, SLOT(onGotInfo()));
751 }
752
753
754 QNetworkReply*
getBuyLinks(const QString & country) const755 lastfm::Track::getBuyLinks( const QString& country ) const
756 {
757 QMap<QString, QString> map = params( "getBuyLinks", true );
758 map["country"] = country;
759 return ws::get( map );
760 }
761
762 QNetworkReply*
playlinks(const QList<Track> & tracks)763 lastfm::Track::playlinks( const QList<Track>& tracks )
764 {
765 QMap<QString, QString> map;
766
767 map["method"] = "Track.playlinks";
768
769 for ( int i = 0 ; i < tracks.count() ; ++i )
770 {
771 if ( tracks[i].d->mbid.size())
772 map["mbid[" + QString::number( i ) + "]"] = tracks[i].d->mbid;
773 else
774 {
775 map["artist[" + QString::number( i ) + "]"] = tracks[i].d->artist;
776 map["track[" + QString::number( i ) + "]"] = tracks[i].d->title;
777 }
778 }
779
780 return ws::get( map );
781 }
782
783
784 QNetworkReply*
addTags(const QStringList & tags) const785 lastfm::Track::addTags( const QStringList& tags ) const
786 {
787 if (tags.isEmpty())
788 return 0;
789 QMap<QString, QString> map = params("addTags");
790 map["tags"] = tags.join( QChar(',') );
791 return ws::post(map);
792 }
793
794
795 QNetworkReply*
removeTag(const QString & tag) const796 lastfm::Track::removeTag( const QString& tag ) const
797 {
798 if (tag.isEmpty())
799 return 0;
800 QMap<QString, QString> map = params( "removeTag" );
801 map["tag"] = tag;
802 return ws::post(map);
803 }
804
805
806 QNetworkReply*
updateNowPlaying() const807 lastfm::Track::updateNowPlaying() const
808 {
809 return updateNowPlaying(duration());
810 }
811
812 QNetworkReply*
updateNowPlaying(int duration) const813 lastfm::Track::updateNowPlaying( int duration ) const
814 {
815 QMap<QString, QString> map = params("updateNowPlaying");
816 map["duration"] = QString::number( duration );
817 map["albumArtist"] = d->albumArtist;
818 if ( !album().isNull() ) map["album"] = album();
819 map["context"] = extra("playerId");
820
821 return ws::post(map);
822 }
823
824 QNetworkReply*
removeNowPlaying() const825 lastfm::Track::removeNowPlaying() const
826 {
827 QMap<QString, QString> map;
828 map["method"] = "track.removeNowPlaying";
829
830 return ws::post(map);
831 }
832
833
834 QNetworkReply*
scrobble() const835 lastfm::Track::scrobble() const
836 {
837 QMap<QString, QString> map = params("scrobble");
838 map["duration"] = QString::number( d->duration );
839 map["timestamp"] = QString::number( d->time.toTime_t() );
840 map["context"] = extra("playerId");
841 map["albumArtist"] = d->albumArtist;
842 if ( !d->album.title().isEmpty() ) map["album"] = d->album.title();
843 map["chosenByUser"] = source() == Track::LastFmRadio ? "0" : "1";
844
845 return ws::post(map);
846 }
847
848 QNetworkReply*
scrobble(const QList<lastfm::Track> & tracks)849 lastfm::Track::scrobble(const QList<lastfm::Track>& tracks)
850 {
851 QMap<QString, QString> map;
852 map["method"] = "track.scrobble";
853
854 for ( int i(0) ; i < tracks.count() ; ++i )
855 {
856 map["duration[" + QString::number(i) + "]"] = QString::number( tracks[i].duration() );
857 map["timestamp[" + QString::number(i) + "]"] = QString::number( tracks[i].timestamp().toTime_t() );
858 map["track[" + QString::number(i) + "]"] = tracks[i].title();
859 map["context[" + QString::number(i) + "]"] = tracks[i].extra("playerId");
860 if ( !tracks[i].album().isNull() ) map["album[" + QString::number(i) + "]"] = tracks[i].album();
861 map["artist[" + QString::number(i) + "]"] = tracks[i].artist();
862 map["albumArtist[" + QString::number(i) + "]"] = tracks[i].albumArtist();
863 if ( !tracks[i].mbid().isNull() ) map["mbid[" + QString::number(i) + "]"] = tracks[i].mbid();
864 map["chosenByUser[" + QString::number(i) + "]"] = tracks[i].source() == Track::LastFmRadio ? "0" : "1";
865 }
866
867 return ws::post(map);
868 }
869
870
871 QUrl
www() const872 lastfm::Track::www() const
873 {
874 return UrlBuilder( "music" ).slash( artist( Corrected ) ).slash( album( Corrected ).isNull() ? QString("_") : album( Corrected )).slash( title( Corrected ) ).url();
875 }
876
877
878 bool
isMp3() const879 lastfm::Track::isMp3() const
880 {
881 //FIXME really we should check the file header?
882 return d->url.scheme() == "file" &&
883 d->url.path().endsWith( ".mp3", Qt::CaseInsensitive );
884 }
885
886 bool
sameObject(const Track & that)887 lastfm::Track::sameObject( const Track& that )
888 {
889 return (this->d == that.d);
890 }
891
892 bool
operator ==(const Track & that) const893 lastfm::Track::operator==( const Track& that ) const
894 {
895 return ( title( Corrected ) == that.title( Corrected )
896 // if either album is empty, assume they are the same album
897 && ( album( Corrected ).title().isEmpty() || that.album( Corrected ).title().isEmpty() || album( Corrected ) == that.album( Corrected ))
898 && artist( Corrected ) == that.artist( Corrected ));
899 }
900
901 bool
operator !=(const Track & that) const902 lastfm::Track::operator!=( const Track& that ) const
903 {
904 return !operator==( that );
905 }
906
907 const QObject*
signalProxy() const908 lastfm::Track::signalProxy() const
909 {
910 return d->trackObject;
911 }
912
913 bool
isNull() const914 lastfm::Track::isNull() const
915 {
916 return d->null;
917 }
918
919 uint
trackNumber() const920 lastfm::Track::trackNumber() const
921 { return d->trackNumber; }
922 uint
duration() const923 lastfm::Track::duration() const
924 {
925 // in seconds
926 return d->duration;
927 }
928
929 lastfm::Mbid
mbid() const930 lastfm::Track::mbid() const
931 {
932 return lastfm::Mbid(d->mbid); }
933 QUrl
url() const934 lastfm::Track::url() const
935 {
936 return d->url; }
937 QDateTime
timestamp() const938 lastfm::Track::timestamp() const
939 {
940 return d->time;
941 }
942
943 lastfm::Track::Source
source() const944 lastfm::Track::source() const
945 {
946 return static_cast<Source>(d->source);
947 }
948
949 uint
fingerprintId() const950 lastfm::Track::fingerprintId() const
951 {
952 return d->fpid;
953 }
954
955 bool
isLoved() const956 lastfm::Track::isLoved() const
957 {
958 return d->loved == Loved;
959 }
960
961 lastfm::Track::LoveStatus
loveStatus() const962 lastfm::Track::loveStatus() const
963 {
964 return d->loved;
965 }
966
967
968 QString
durationString() const969 lastfm::Track::durationString() const
970 {
971 return durationString( d->duration );
972 }
973
974
975 lastfm::Track::ScrobbleStatus
scrobbleStatus() const976 lastfm::Track::scrobbleStatus() const
977 {
978 return static_cast<ScrobbleStatus>(d->scrobbleStatus);
979 }
980
981 lastfm::Track::ScrobbleError
scrobbleError() const982 lastfm::Track::scrobbleError() const
983 {
984 return static_cast<ScrobbleError>(d->scrobbleError);
985 }
986 QString
scrobbleErrorText() const987 lastfm::Track::scrobbleErrorText() const
988 {
989 return d->scrobbleErrorText;
990 }
991
992 /** default separator is an en-dash */
993 QString
toString() const994 lastfm::Track::toString() const
995 {
996 return toString( Corrected );
997 }
998
999 QString
toString(Corrections corrections) const1000 lastfm::Track::toString( Corrections corrections ) const
1001 {
1002 return toString( QChar(8211), corrections );
1003 }
1004
1005 lastfm::TrackContext
context() const1006 lastfm::Track::context() const
1007 {
1008 return d->context;
1009 }
1010
1011 // iTunes tracks might be podcasts or videos
1012 bool
isPodcast() const1013 lastfm::Track::isPodcast() const
1014 {
1015 return d->podcast;
1016 }
1017
1018 bool
isVideo() const1019 lastfm::Track::isVideo() const
1020 {
1021 return d->video;
1022 }
1023
1024 QString
extra(const QString & key) const1025 lastfm::Track::extra( const QString& key ) const
1026 {
1027 return d->extras[ key ];
1028 }
1029
operator <(const Track & that) const1030 bool lastfm::Track::operator<( const Track &that ) const
1031 {
1032 return this->d->time < that.d->time;
1033 }
1034
operator QVariant() const1035 lastfm::Track::operator QVariant() const
1036 {
1037 return QVariant::fromValue( *this );
1038 }
1039
1040 void
setCorrections(QString title,QString album,QString artist,QString albumArtist)1041 lastfm::MutableTrack::setCorrections( QString title, QString album, QString artist, QString albumArtist )
1042 {
1043 d->correctedTitle = title;
1044 d->correctedArtist = artist;
1045 d->correctedAlbum = Album( artist, album );
1046 d->correctedAlbumArtist = albumArtist;
1047
1048 d->trackObject->forceCorrected( toString() );
1049 }
1050
MutableTrack()1051 lastfm::MutableTrack::MutableTrack()
1052 {
1053 d->null = false;
1054 }
1055
1056
MutableTrack(const Track & that)1057 lastfm::MutableTrack::MutableTrack( const Track& that )
1058 : Track( that )
1059 {
1060 d->null = false;
1061 }
1062
1063 void
setArtist(QString artist)1064 lastfm::MutableTrack::setArtist( QString artist )
1065 {
1066 d->artist.setName( artist.trimmed() );
1067 d->album.setArtist( artist.trimmed() );
1068 d->correctedAlbum.setArtist( artist.trimmed() );
1069 }
1070
1071 void
setAlbumArtist(QString albumArtist)1072 lastfm::MutableTrack::setAlbumArtist( QString albumArtist )
1073 {
1074 d->albumArtist.setName( albumArtist.trimmed() );
1075 }
1076
1077 void
setAlbum(QString album)1078 lastfm::MutableTrack::setAlbum( QString album )
1079 {
1080 d->album = Album( d->artist.name(), album.trimmed() );
1081 }
1082
1083 void
setTitle(QString title)1084 lastfm::MutableTrack::setTitle( QString title )
1085 {
1086 d->title = title.trimmed();
1087 }
1088
1089 void
setTrackNumber(uint n)1090 lastfm::MutableTrack::setTrackNumber( uint n )
1091 {
1092 d->trackNumber = n;
1093 }
1094
1095 void
setDuration(uint duration)1096 lastfm::MutableTrack::setDuration( uint duration )
1097 {
1098 d->duration = duration;
1099 }
1100
1101 void
setUrl(QUrl url)1102 lastfm::MutableTrack::setUrl( QUrl url )
1103 {
1104 d->url = url;
1105 }
1106
1107 void
setSource(Source s)1108 lastfm::MutableTrack::setSource( Source s )
1109 {
1110 d->source = s;
1111 }
1112
1113 void
setLoved(bool loved)1114 lastfm::MutableTrack::setLoved( bool loved )
1115 {
1116 d->loved = loved ? Loved : Unloved;
1117 }
1118
1119 void
setMbid(Mbid id)1120 lastfm::MutableTrack::setMbid( Mbid id )
1121 {
1122 d->mbid = id;
1123 }
1124
1125 void
setFingerprintId(uint id)1126 lastfm::MutableTrack::setFingerprintId( uint id )
1127 {
1128 d->fpid = id;
1129 }
1130
1131 void
setScrobbleStatus(ScrobbleStatus scrobbleStatus)1132 lastfm::MutableTrack::setScrobbleStatus( ScrobbleStatus scrobbleStatus )
1133 {
1134 if ( scrobbleStatus != d->scrobbleStatus )
1135 {
1136 d->scrobbleStatus = scrobbleStatus;
1137 d->trackObject->forceScrobbleStatusChanged();
1138 }
1139 }
1140
1141 void
setScrobbleError(ScrobbleError scrobbleError)1142 lastfm::MutableTrack::setScrobbleError( ScrobbleError scrobbleError )
1143 {
1144 d->scrobbleError = scrobbleError;
1145 }
1146
1147 void
setScrobbleErrorText(const QString & scrobbleErrorText)1148 lastfm::MutableTrack::setScrobbleErrorText( const QString& scrobbleErrorText )
1149 {
1150 d->scrobbleErrorText = scrobbleErrorText;
1151 }
1152
1153 void
stamp()1154 lastfm::MutableTrack::stamp()
1155 {
1156 d->time = QDateTime::currentDateTime();
1157 }
1158
1159 void
setExtra(const QString & key,const QString & value)1160 lastfm::MutableTrack::setExtra( const QString& key, const QString& value )
1161 {
1162 d->extras[key] = value;
1163 }
1164
1165 void
removeExtra(QString key)1166 lastfm::MutableTrack::removeExtra( QString key )
1167 {
1168 d->extras.remove( key );
1169 }
1170
1171 void
setTimeStamp(const QDateTime & dt)1172 lastfm::MutableTrack::setTimeStamp( const QDateTime& dt )
1173 {
1174 d->time = dt;
1175 }
1176
1177 void
setContext(TrackContext context)1178 lastfm::MutableTrack::setContext( TrackContext context )
1179 {
1180 d->context = context;
1181 }
1182
1183 // iTunes tracks might be podcasts or videos
1184 void
setPodcast(bool podcast)1185 lastfm::MutableTrack::setPodcast( bool podcast )
1186 {
1187 d->podcast = podcast;
1188 }
1189 void
setVideo(bool video)1190 lastfm::MutableTrack::setVideo( bool video )
1191 {
1192 d->video = video;
1193 }
1194
1195 QDebug
operator <<(QDebug d,const lastfm::Track & t)1196 operator<<( QDebug d, const lastfm::Track& t )
1197 {
1198 return !t.isNull()
1199 ? d << t.toString( '-' ) << t.url()
1200 : d << "Null Track object";
1201 }
1202
1203 #include "Track.moc"
1204