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