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 "Fingerprint.h"
22 #include "FingerprintableSource.h"
23 #include "Collection.h"
24 #include "Sha256.h"
25 #include "fplib/FingerprintExtractor.h"
26 #include "ws.h"
27 #include "Url.h"
28 #include <QDebug>
29 #include <QFileInfo>
30 #include <QNetworkAccessManager>
31 #include <QNetworkRequest>
32 #include <QNetworkReply>
33 #include <QStringList>
34 #if QT_VERSION >= QT_VERSION_CHECK( 5, 0, 0 )
35 #include <QUrlQuery>
36 #endif
37 #include <fstream>
38
39 #include "User.h"
40
41 using lastfm::Track;
42
43 static const uint k_bufferSize = 1024 * 8;
44 static const int k_minTrackDuration = 30;
45
46 class lastfm::FingerprintPrivate
47 {
48 public:
FingerprintPrivate(const Track & t)49 FingerprintPrivate( const Track& t )
50 : m_track( t )
51 , m_id( -1 )
52 , m_duration( 0 )
53 , m_complete( false )
54 {
55 }
56 lastfm::Track m_track;
57 QByteArray m_data;
58 int m_id;
59 int m_duration;
60 bool m_complete;
61 };
62
63
Fingerprint(const Track & t)64 lastfm::Fingerprint::Fingerprint( const Track& t )
65 : d( new FingerprintPrivate( t ) )
66 {
67 QString id = Collection::instance().getFingerprintId( t.url().toLocalFile() );
68 if (id.size()) {
69 bool b;
70 d->m_id = id.toInt( &b );
71 if (!b) d->m_id = -1;
72 }
73 }
74
75
~Fingerprint()76 lastfm::Fingerprint::~Fingerprint()
77 {
78 delete d;
79 }
80
81
82 lastfm::FingerprintId
id() const83 lastfm::Fingerprint::id() const
84 {
85 return d->m_id;
86 }
87
88
89 QByteArray
data() const90 lastfm::Fingerprint::data() const
91 {
92 return d->m_data;
93 }
94
95
96 void
generate(FingerprintableSource * ms)97 lastfm::Fingerprint::generate( FingerprintableSource* ms ) throw( Error )
98 {
99 //TODO throw if we can't get required metadata from the track object
100
101 //TODO if (!QFileInfo( path ).isReadable())
102 //TODO throw ReadError;
103
104 int sampleRate, bitrate, numChannels;
105
106 if ( !ms )
107 throw ReadError;
108
109 try
110 {
111 ms->init( d->m_track.url().toLocalFile() );
112 ms->getInfo( d->m_duration, sampleRate, bitrate, numChannels );
113 }
114 catch (std::exception& e)
115 {
116 qWarning() << e.what();
117 throw HeadersError;
118 }
119
120
121 if (d->m_duration < k_minTrackDuration)
122 throw TrackTooShortError;
123
124 ms->skipSilence();
125
126 bool fpDone = false;
127 fingerprint::FingerprintExtractor* extractor;
128 try
129 {
130 extractor = new fingerprint::FingerprintExtractor;
131
132 if (d->m_complete)
133 {
134 extractor->initForFullSubmit( sampleRate, numChannels );
135 }
136 else
137 {
138 extractor->initForQuery( sampleRate, numChannels, d->m_duration );
139
140 // Skippety skip for as long as the skipper sez (optimisation)
141 ms->skip( extractor->getToSkipMs() );
142 float secsToSkip = extractor->getToSkipMs() / 1000.0f;
143 fpDone = extractor->process( 0,
144 (size_t) sampleRate * numChannels * secsToSkip,
145 false );
146 }
147 }
148 catch (std::exception& e)
149 {
150 qWarning() << e.what();
151 delete extractor;
152 throw DecodeError;
153 }
154
155 const size_t PCMBufSize = 131072;
156 short* pPCMBuffer = new short[PCMBufSize];
157
158 while (!fpDone)
159 {
160 size_t readData = ms->updateBuffer( pPCMBuffer, PCMBufSize );
161 if (readData == 0)
162 break;
163
164 try
165 {
166 fpDone = extractor->process( pPCMBuffer, readData, ms->eof() );
167 }
168 catch ( const std::exception& e )
169 {
170 qWarning() << e.what();
171 delete[] pPCMBuffer;
172 delete extractor;
173 throw InternalError;
174 }
175 }
176
177 delete[] pPCMBuffer;
178
179 if (!fpDone)
180 {
181 delete extractor;
182 throw InternalError;
183 }
184
185 // We succeeded
186 std::pair<const char*, size_t> fpData = extractor->getFingerprint();
187
188 if (fpData.first == NULL || fpData.second == 0)
189 {
190 delete extractor;
191 throw InternalError;
192 }
193
194 // Make a deep copy before extractor gets deleted
195 d->m_data = QByteArray( fpData.first, fpData.second );
196 delete extractor;
197 }
198
199
sha256(const QString & path)200 static QString sha256( const QString& path )
201 {
202 // no clue why this is static, there was no comment when I refactored it
203 // initially --mxcl
204 static uint8_t pBuffer[SHA_BUFFER_SIZE+7];
205
206 unsigned char hash[SHA256_HASH_SIZE];
207
208 {
209 QByteArray path8 = QFile::encodeName( path );
210 std::ifstream inFile( path8.data(), std::ios::binary);
211
212 SHA256Context sha256;
213 SHA256Init( &sha256 );
214
215 uint8_t* pMovableBuffer = pBuffer;
216
217 // Ensure it is on a 64-bit boundary.
218 INTPTR offs;
219 if ((offs = reinterpret_cast<INTPTR>(pBuffer) & 7L))
220 pMovableBuffer += 8 - offs;
221
222 unsigned int len;
223
224 for (;;)
225 {
226 inFile.read( reinterpret_cast<char*>(pMovableBuffer), SHA_BUFFER_SIZE );
227 len = inFile.gcount();
228
229 if (len == 0)
230 break;
231
232 SHA256Update( &sha256, pMovableBuffer, len );
233 }
234
235 SHA256Final( &sha256, hash );
236 }
237
238 QString sha;
239 for (int i = 0; i < SHA256_HASH_SIZE; ++i)
240 {
241 QString hex = QString("%1").arg(uchar(hash[i]), 2, 16,
242 QChar('0'));
243 sha.append(hex);
244 }
245
246 return sha;
247 }
248
249
number(uint n)250 static QByteArray number( uint n )
251 {
252 return n ? QByteArray::number( n ) : "";
253 }
254
255
256 QNetworkReply*
submit() const257 lastfm::Fingerprint::submit() const
258 {
259 if (d->m_data.isEmpty())
260 return 0;
261
262 //Parameters understood by the server according to the MIR team:
263 //{ "trackid", "recordingid", "artist", "album", "track", "duration",
264 // "tracknum", "username", "sha256", "ip", "fpversion", "mbid",
265 // "filename", "genre", "year", "samplerate", "noupdate", "fulldump" }
266
267 Track const t = d->m_track;
268 QString const path = t.url().toLocalFile();
269 QFileInfo const fi( path );
270
271 lastfm::Url url( QUrl( "http://ws.audioscrobbler.com/fingerprint/query/" ) );
272 url.addQueryItem( "username", lastfm::User().name() );
273 url.addQueryItem( "artist", t.artist() );
274 url.addQueryItem( "album", t.album() );
275 url.addQueryItem( "track", t.title() );
276 url.addQueryItem( "duration", number( d->m_duration > 0 ? d->m_duration : t.duration() ) );
277 url.addQueryItem( "mbid", t.mbid() );
278 url.addQueryItem( "filename", fi.completeBaseName() );
279 url.addQueryItem( "fileextension", fi.completeSuffix() );
280 url.addQueryItem( "tracknum", number( t.trackNumber() ) );
281 url.addQueryItem( "sha256", sha256( path ) );
282 url.addQueryItem( "time", number(QDateTime::currentDateTime().toTime_t()) );
283 url.addQueryItem( "fpversion", QByteArray::number((int)fingerprint::FingerprintExtractor::getVersion()) );
284 url.addQueryItem( "fulldump", d->m_complete ? "true" : "false" );
285 url.addQueryItem( "noupdate", "false" );
286
287 //FIXME: talk to mir about submitting fplibversion
288
289 QNetworkRequest request( url.url() );
290 request.setHeader( QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=----------------------------8e61d618ca16" );
291
292 QByteArray bytes;
293 bytes += "------------------------------8e61d618ca16\r\n";
294 bytes += "Content-Disposition: ";
295 bytes += "form-data; name=\"fpdata\"";
296 bytes += "\r\n\r\n";
297 bytes += d->m_data;
298 bytes += "\r\n";
299 bytes += "------------------------------8e61d618ca16--\r\n";
300
301 qDebug() << url.url();
302 qDebug() << "Fingerprint size:" << bytes.size() << "bytes";
303
304 return lastfm::nam()->post( request, bytes );
305 }
306
307
308 void
decode(QNetworkReply * reply,bool * complete_fingerprint_requested)309 lastfm::Fingerprint::decode( QNetworkReply* reply, bool* complete_fingerprint_requested ) throw( Error )
310 {
311 // The response data will consist of a number and a string.
312 // The number is the fpid and the string is either FOUND or NEW
313 // (or NOT FOUND when noupdate was used). NEW means we should
314 // schedule a full fingerprint.
315 //
316 // In the case of an error, there will be no initial number, just
317 // an error string.
318
319 reply->deleteLater();
320 QString const response( reply->readAll() );
321 QStringList const list = response.split( ' ' );
322
323 QString const fpid = list.value( 0 );
324 QString const status = list.value( 1 );
325
326 if (response.isEmpty() || list.count() < 2 || response == "No response to client error")
327 goto bad_response;
328 if (list.count() != 2)
329 qWarning() << "Response looks bad but continuing anyway:" << response;
330
331 {
332 // so variables go out of scope before jump to label
333 // otherwise compiler error on GCC 4.2
334 bool b;
335 uint fpid_as_uint = fpid.toUInt( &b );
336 if (!b) goto bad_response;
337
338 Collection::instance().setFingerprintId( d->m_track.url().toLocalFile(), fpid );
339
340 if (complete_fingerprint_requested)
341 *complete_fingerprint_requested = (status == "NEW");
342
343 d->m_id = (int)fpid_as_uint;
344 return;
345 }
346
347 bad_response:
348 qWarning() << "Response is bad:" << response;
349 throw BadResponseError;
350 }
351
352
CompleteFingerprint(const lastfm::Track & t)353 lastfm::CompleteFingerprint::CompleteFingerprint( const lastfm::Track& t ) : Fingerprint( t )
354 {
355 d->m_complete = true;
356 }
357
358
~CompleteFingerprint()359 lastfm::CompleteFingerprint::~CompleteFingerprint()
360 {
361 }
362
363
operator <<(QDebug d,lastfm::Fingerprint::Error e)364 QDebug operator<<( QDebug d, lastfm::Fingerprint::Error e )
365 {
366 #define CASE(x) case lastfm::Fingerprint::x: return d << #x;
367 switch (e)
368 {
369 CASE(ReadError)
370 CASE(HeadersError)
371 CASE(DecodeError)
372 CASE(TrackTooShortError)
373 CASE(BadResponseError)
374 CASE(InternalError)
375 }
376 #undef CASE
377
378 return d;
379 }
380