Import Upstream version 1.0.3
Boyuan Yang
2 years ago
3 | 3 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) |
4 | 4 | |
5 | 5 | # general settings |
6 | set(LASTFM_VERSION 1.0.2) | |
6 | set(LASTFM_VERSION 1.0.3) | |
7 | 7 | set(LASTFM_SOVERSION 1) |
8 | 8 | |
9 | 9 | # options |
0 | Join us for chats on IRC! | |
1 | ||
2 | Server: irc.last.fm | |
3 | Channel: #last.desktop | |
4 | ||
0 | 5 | # liblastfm |
1 | 6 | |
2 | 7 | liblastfm is a collection of libraries to help you integrate Last.fm services |
3 | 8 | into your rich desktop software. It is officially supported software developed |
4 | 9 | by Last.fm staff. |
5 | 10 | |
6 | Michael Coffey http://twitter.com/eartle | |
11 | Michael Coffey http://twitter.com/eartle | |
7 | 12 | |
8 | 13 | Fork it: http://github.com/lastfm/liblastfm |
9 | 14 |
185 | 185 | |
186 | 186 | XmlQuery lfm; |
187 | 187 | |
188 | if ( lfm.parse( r->readAll() ) ) | |
188 | if ( lfm.parse( r ) ) | |
189 | 189 | { |
190 | 190 | foreach (XmlQuery e, lfm.children( "artist" )) |
191 | 191 | { |
209 | 209 | try |
210 | 210 | { |
211 | 211 | XmlQuery lfm; |
212 | lfm.parse( r->readAll() ); | |
212 | lfm.parse( r ); | |
213 | 213 | foreach (XmlQuery e, lfm.children( "track" )) |
214 | 214 | { |
215 | 215 | tracks << e["name"].text(); |
229 | 229 | QList<Artist> artists; |
230 | 230 | XmlQuery lfm; |
231 | 231 | |
232 | if ( lfm.parse( r->readAll() ) ) | |
232 | if ( lfm.parse( r ) ) | |
233 | 233 | { |
234 | 234 | foreach (XmlQuery xq, lfm.children( "artist" )) |
235 | 235 | { |
250 | 250 | { |
251 | 251 | XmlQuery lfm; |
252 | 252 | |
253 | if ( lfm.parse( r->readAll() ) ) | |
253 | if ( lfm.parse( r ) ) | |
254 | 254 | { |
255 | 255 | Artist artist = Artist( lfm["artist"] ); |
256 | 256 | return artist; |
92 | 92 | lastfm::Audioscrobbler::cacheBatch( const QList<lastfm::Track>& tracks, const QString& ) |
93 | 93 | { |
94 | 94 | d->m_cache.add( tracks ); |
95 | ||
96 | foreach ( const Track& track, d->m_cache.tracks() ) | |
97 | MutableTrack( track ).setScrobbleStatus( Track::Cached ); | |
98 | ||
99 | 95 | emit scrobblesCached( tracks ); |
100 | ||
101 | 96 | submit(); |
97 | } | |
98 | ||
99 | void | |
100 | lastfm::Audioscrobbler::cacheBatch( const QList<lastfm::Track>& tracks ) | |
101 | { | |
102 | cacheBatch( tracks, "" ); | |
102 | 103 | } |
103 | 104 | |
104 | 105 | |
156 | 157 | { |
157 | 158 | lastfm::XmlQuery lfm; |
158 | 159 | |
159 | if ( lfm.parse( static_cast<QNetworkReply*>(sender())->readAll() ) ) | |
160 | if ( lfm.parse( d->m_nowPlayingReply ) ) | |
160 | 161 | { |
161 | 162 | qDebug() << lfm; |
162 | 163 | |
164 | 165 | d->parseTrack( lfm["nowplaying"], d->m_nowPlayingTrack ); |
165 | 166 | else |
166 | 167 | emit nowPlayingError( lfm["error"].attribute("code").toInt(), lfm["error"].text() ); |
167 | ||
168 | d->m_nowPlayingTrack = Track(); | |
169 | d->m_nowPlayingReply = 0; | |
170 | 168 | } |
171 | 169 | else |
172 | 170 | { |
183 | 181 | { |
184 | 182 | lastfm::XmlQuery lfm; |
185 | 183 | |
186 | if ( lfm.parse( d->m_scrobbleReply->readAll() ) ) | |
184 | if ( lfm.parse( d->m_scrobbleReply ) ) | |
187 | 185 | { |
188 | 186 | qDebug() << lfm; |
189 | 187 |
56 | 56 | void nowPlaying( const Track& ); |
57 | 57 | /** will cache the track and call submit() */ |
58 | 58 | void cache( const Track& ); |
59 | void cacheBatch( const QList<lastfm::Track>&, const QString& id = "" ); | |
59 | void cacheBatch( const QList<lastfm::Track>&, const QString& id ); | |
60 | void cacheBatch( const QList<lastfm::Track>& ); | |
60 | 61 | |
61 | 62 | /** will submit the submission cache for this user */ |
62 | 63 | void submit(); |
81 | 81 | QMap<float, Track> tracks; |
82 | 82 | lastfm::XmlQuery lfm; |
83 | 83 | |
84 | if ( lfm.parse( reply->readAll() ) ) | |
84 | if ( lfm.parse( reply ) ) | |
85 | 85 | { |
86 | 86 | foreach ( const lastfm::XmlQuery& track, lfm["tracks"].children("track") ) |
87 | 87 | { |
307 | 307 | QList<lastfm::RadioStation> result; |
308 | 308 | XmlQuery lfm; |
309 | 309 | |
310 | if ( lfm.parse( r->readAll() ) ) | |
310 | if ( lfm.parse( r ) ) | |
311 | 311 | { |
312 | 312 | |
313 | 313 | foreach (XmlQuery xq, lfm.children("station")) |
184 | 184 | |
185 | 185 | XmlQuery lfm; |
186 | 186 | |
187 | if ( lfm.parse( qobject_cast<QNetworkReply*>(sender())->readAll() ) ) | |
187 | if ( lfm.parse( qobject_cast<QNetworkReply*>(sender()) ) ) | |
188 | 188 | { |
189 | 189 | qDebug() << lfm; |
190 | 190 | |
215 | 215 | |
216 | 216 | XmlQuery lfm; |
217 | 217 | |
218 | if ( lfm.parse( qobject_cast<QNetworkReply*>(sender())->readAll() ) ) | |
218 | if ( lfm.parse( qobject_cast<QNetworkReply*>(sender()) ) ) | |
219 | 219 | { |
220 | 220 | qDebug() << lfm; |
221 | 221 |
30 | 30 | |
31 | 31 | class lastfm::ScrobbleCachePrivate |
32 | 32 | { |
33 | public: | |
34 | enum Invalidity | |
35 | { | |
36 | TooShort, | |
37 | ArtistNameMissing, | |
38 | TrackNameMissing, | |
39 | ArtistInvalid, | |
40 | NoTimestamp, | |
41 | FromTheFuture, | |
42 | FromTheDistantPast | |
43 | }; | |
44 | ||
33 | public: | |
45 | 34 | QString m_username; |
46 | 35 | QString m_path; |
47 | 36 | QList<Track> m_tracks; |
48 | 37 | |
49 | bool isValid( const Track& track, Invalidity* = 0 ); | |
50 | 38 | void write(); /// writes m_tracks to m_path |
51 | 39 | void read( QDomDocument& xml ); /// reads from m_path into m_tracks |
52 | 40 | |
53 | 41 | }; |
54 | ||
55 | ||
56 | bool | |
57 | lastfm::ScrobbleCachePrivate::isValid( const lastfm::Track& track, Invalidity* v ) | |
58 | { | |
59 | #define TEST( test, x ) \ | |
60 | if (test) { \ | |
61 | if (v) *v = x; \ | |
62 | return false; \ | |
63 | } | |
64 | ||
65 | TEST( track.duration() < ScrobblePoint::scrobbleTimeMin(), TooShort ); | |
66 | ||
67 | TEST( !track.timestamp().isValid(), NoTimestamp ); | |
68 | ||
69 | // actual spam prevention is something like 12 hours, but we are only | |
70 | // trying to weed out obviously bad data, server side criteria for | |
71 | // "the future" may change, so we should let the server decide, not us | |
72 | TEST( track.timestamp() > QDateTime::currentDateTime().addMonths( 1 ), FromTheFuture ); | |
73 | ||
74 | TEST( track.timestamp() < QDateTime::fromString( "2003-01-01", Qt::ISODate ), FromTheDistantPast ); | |
75 | ||
76 | // Check if any required fields are empty | |
77 | TEST( track.artist().isNull(), ArtistNameMissing ); | |
78 | TEST( track.title().isEmpty(), TrackNameMissing ); | |
79 | ||
80 | TEST( (QStringList() << "unknown artist" | |
81 | << "unknown" | |
82 | << "[unknown]" | |
83 | << "[unknown artist]").contains( track.artist().name().toLower() ), | |
84 | ArtistInvalid ); | |
85 | ||
86 | return true; | |
87 | } | |
88 | 42 | |
89 | 43 | |
90 | 44 | ScrobbleCache::ScrobbleCache( const QString& username ) |
175 | 129 | { |
176 | 130 | foreach (const Track& track, tracks) |
177 | 131 | { |
178 | ScrobbleCachePrivate::Invalidity invalidity; | |
132 | Invalidity invalidity; | |
179 | 133 | |
180 | if ( !d->isValid( track, &invalidity ) ) | |
134 | if ( !isValid( track, &invalidity ) ) | |
181 | 135 | { |
182 | 136 | qWarning() << invalidity; |
137 | MutableTrack mt = MutableTrack( track ); | |
138 | mt.setScrobbleStatus( Track::Error ); | |
139 | mt.setScrobbleErrorText( QObject::tr( "Invalid" ) ); | |
183 | 140 | } |
184 | 141 | else if (track.isNull()) |
185 | 142 | qDebug() << "Will not cache an empty track"; |
186 | 143 | else |
187 | d->m_tracks += track; | |
144 | { | |
145 | bool ok; | |
146 | int plays = track.extra( "playCount" ).toInt( &ok ); | |
147 | if ( !ok ) plays = 1; | |
148 | ||
149 | for ( int i = 0 ; i < plays ; ++i ) | |
150 | d->m_tracks += track; | |
151 | ||
152 | MutableTrack( track ).setScrobbleStatus( Track::Cached ); | |
153 | } | |
188 | 154 | } |
189 | 155 | |
190 | 156 | d->write(); |
210 | 176 | } |
211 | 177 | |
212 | 178 | |
179 | bool | |
180 | ScrobbleCache::isValid( const lastfm::Track& track, Invalidity* v ) | |
181 | { | |
182 | #define TEST( test, x ) \ | |
183 | if (test) { \ | |
184 | if (v) *v = x; \ | |
185 | return false; \ | |
186 | } | |
187 | ||
188 | TEST( track.duration() < ScrobblePoint::scrobbleTimeMin(), ScrobbleCache::TooShort ); | |
189 | ||
190 | TEST( !track.timestamp().isValid(), ScrobbleCache::NoTimestamp ); | |
191 | ||
192 | // actual spam prevention is something like 12 hours, but we are only | |
193 | // trying to weed out obviously bad data, server side criteria for | |
194 | // "the future" may change, so we should let the server decide, not us | |
195 | TEST( track.timestamp() > QDateTime::currentDateTime().addMonths( 1 ), ScrobbleCache::FromTheFuture ); | |
196 | ||
197 | TEST( track.timestamp().daysTo( QDateTime::currentDateTime() ) > 14, ScrobbleCache::FromTheDistantPast ); | |
198 | ||
199 | // Check if any required fields are empty | |
200 | TEST( track.artist().isNull(), ScrobbleCache::ArtistNameMissing ); | |
201 | TEST( track.title().isEmpty(), ScrobbleCache::TrackNameMissing ); | |
202 | ||
203 | TEST( (QStringList() << "unknown artist" | |
204 | << "unknown" | |
205 | << "[unknown]" | |
206 | << "[unknown artist]").contains( track.artist().name().toLower() ), | |
207 | ScrobbleCache::ArtistInvalid ); | |
208 | ||
209 | return true; | |
210 | } | |
211 | ||
212 | ||
213 | 213 | QList<lastfm::Track> |
214 | 214 | ScrobbleCache::tracks() const |
215 | 215 | { |
28 | 28 | class LASTFM_DLLEXPORT ScrobbleCache |
29 | 29 | { |
30 | 30 | public: |
31 | enum Invalidity | |
32 | { | |
33 | TooShort, | |
34 | ArtistNameMissing, | |
35 | TrackNameMissing, | |
36 | ArtistInvalid, | |
37 | NoTimestamp, | |
38 | FromTheFuture, | |
39 | FromTheDistantPast | |
40 | }; | |
41 | ||
31 | 42 | explicit ScrobbleCache( const QString& username ); |
32 | 43 | ScrobbleCache( const ScrobbleCache& that ); |
33 | 44 | ~ScrobbleCache(); |
38 | 49 | |
39 | 50 | /** returns the number of tracks left in the queue */ |
40 | 51 | int remove( const QList<Track>& ); |
52 | ||
53 | static bool isValid( const lastfm::Track& track, Invalidity* v = 0 ); | |
41 | 54 | |
42 | 55 | ScrobbleCache& operator=( const ScrobbleCache& that ); |
43 | 56 |
119 | 119 | |
120 | 120 | XmlQuery lfm; |
121 | 121 | |
122 | if ( lfm.parse( r->readAll() ) ) | |
122 | if ( lfm.parse( r ) ) | |
123 | 123 | { |
124 | 124 | |
125 | 125 | foreach ( XmlQuery xq, lfm.children("tag") ) |
269 | 269 | { |
270 | 270 | XmlQuery lfm; |
271 | 271 | |
272 | if ( lfm.parse( static_cast<QNetworkReply*>(sender())->readAll() ) ) | |
272 | if ( lfm.parse( static_cast<QNetworkReply*>(sender()) ) ) | |
273 | 273 | { |
274 | 274 | if ( lfm.attribute( "status" ) == "ok") |
275 | 275 | loved = Track::Loved; |
285 | 285 | { |
286 | 286 | XmlQuery lfm; |
287 | 287 | |
288 | if ( lfm.parse( static_cast<QNetworkReply*>(sender())->readAll() ) ) | |
288 | if ( lfm.parse( static_cast<QNetworkReply*>(sender()) ) ) | |
289 | 289 | { |
290 | 290 | if ( lfm.attribute( "status" ) == "ok") |
291 | 291 | loved = Track::Unloved; |
307 | 307 | break; |
308 | 308 | } |
309 | 309 | } |
310 | ||
311 | const QByteArray data = static_cast<QNetworkReply*>(sender())->readAll(); | |
310 | ||
311 | QNetworkReply* reply = static_cast<QNetworkReply*>(sender()); | |
312 | reply->deleteLater(); | |
313 | const QByteArray data = reply->readAll(); | |
312 | 314 | |
313 | 315 | lastfm::XmlQuery lfm; |
314 | 316 | |
654 | 656 | QMap<int, QPair< QString, QString > > tracks; |
655 | 657 | try |
656 | 658 | { |
657 | QByteArray b = r->readAll(); | |
658 | 659 | XmlQuery lfm; |
659 | 660 | |
660 | if ( lfm.parse( b ) ) | |
661 | if ( lfm.parse( r ) ) | |
661 | 662 | { |
662 | 663 | foreach (XmlQuery e, lfm.children( "track" )) |
663 | 664 | { |
751 | 752 | if (tag.isEmpty()) |
752 | 753 | return 0; |
753 | 754 | QMap<QString, QString> map = params( "removeTag" ); |
754 | map["tags"] = tag; | |
755 | map["tag"] = tag; | |
755 | 756 | return ws::post(map); |
756 | 757 | } |
757 | 758 |
500 | 500 | |
501 | 501 | XmlQuery lfm; |
502 | 502 | |
503 | lfm.parse( r->readAll() ); | |
503 | lfm.parse( r ); | |
504 | 504 | return UserList( lfm ); |
505 | 505 | } |
506 | 506 |
88 | 88 | QDomElement error = d->e.firstChildElement( "error" ); |
89 | 89 | uint const n = d->e.childNodes().count(); |
90 | 90 | |
91 | // no elements beyond the lfm is perfectably acceptable <-- wtf? | |
91 | // no elements beyond the lfm is perfectably acceptable for example when | |
92 | // XmlQuery is used parse response of a POST request. | |
92 | 93 | // if (n == 0) // nothing useful in the response |
93 | 94 | if (status == "failed" || (n == 1 && !error.isNull()) ) |
94 | 95 | d->error = error.isNull() |
121 | 122 | return d->error.enumValue() == lastfm::ws::NoError; |
122 | 123 | } |
123 | 124 | |
124 | ||
125 | lastfm::ws::ParseError XmlQuery::parseError() const | |
125 | bool | |
126 | XmlQuery::parse( QNetworkReply* reply ) | |
127 | { | |
128 | reply->deleteLater(); | |
129 | return parse( reply->readAll() ); | |
130 | } | |
131 | ||
132 | ||
133 | lastfm::ws::ParseError | |
134 | XmlQuery::parseError() const | |
126 | 135 | { |
127 | 136 | return d->error; |
128 | 137 | } |
42 | 42 | XmlQuery(); |
43 | 43 | XmlQuery( const XmlQuery& that ); |
44 | 44 | ~XmlQuery(); |
45 | ||
46 | /** | |
47 | * Fills in the XmlQuery response by parsing raw reply @param data from the | |
48 | * webservice. | |
49 | * | |
50 | * @return true if successfully parsed and the response does not signify an error, | |
51 | * false otherwise. When false is returned, parseError() contains the error. | |
52 | */ | |
45 | 53 | bool parse( const QByteArray& data ); |
54 | ||
55 | /** | |
56 | * Convenience parse() overload that takes data from the @param reply and calls | |
57 | * deleteLater() on it. | |
58 | * | |
59 | * @return true if successfully parsed and the response does not signify an error, | |
60 | * false otherwise. When false is returned, parseError() contains the error. | |
61 | */ | |
62 | bool parse( QNetworkReply* reply ); | |
63 | ||
46 | 64 | ws::ParseError parseError() const; |
47 | ||
65 | ||
48 | 66 | XmlQuery( const QDomElement& e, const char* name = "" ); |
49 | 67 | |
50 | 68 | /** Selects a DIRECT child element, you can specify attributes like so: |
54 | 72 | XmlQuery operator[]( const QString& name ) const; |
55 | 73 | QString text() const; |
56 | 74 | QString attribute( const QString& name ) const; |
57 | ||
75 | ||
58 | 76 | /** selects all children with specified name, recursively */ |
59 | 77 | QList<XmlQuery> children( const QString& named ) const; |
60 | 78 |
305 | 305 | // In the case of an error, there will be no initial number, just |
306 | 306 | // an error string. |
307 | 307 | |
308 | reply->deleteLater(); | |
308 | 309 | QString const response( reply->readAll() ); |
309 | 310 | QStringList const list = response.split( ' ' ); |
310 | 311 | |
362 | 363 | CASE(InternalError) |
363 | 364 | } |
364 | 365 | #undef CASE |
365 | } | |
366 | ||
367 | return d; | |
368 | } |