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