/thirdparty/liblastfm2/src/fingerprint/Fingerprint.cpp

http://github.com/tomahawk-player/tomahawk · C++ · 300 lines · 198 code · 60 blank · 42 comment · 29 complexity · 189e7a7af8cd7b7319424abf3097fea5 MD5 · raw file

  1. /*
  2. Copyright 2009 Last.fm Ltd.
  3. - Primarily authored by Max Howell, Jono Cole and Doug Mansell
  4. This file is part of liblastfm.
  5. liblastfm is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. liblastfm is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with liblastfm. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. #include "Fingerprint.h"
  17. #include "FingerprintableSource.h"
  18. #include "Collection.h"
  19. #include "Sha256.h"
  20. #include "fplib/FingerprintExtractor.h"
  21. #include "../ws/ws.h"
  22. #include <QFileInfo>
  23. #include <QNetworkAccessManager>
  24. #include <QNetworkRequest>
  25. #include <QNetworkReply>
  26. #include <QStringList>
  27. #include <fstream>
  28. using lastfm::Track;
  29. static const uint k_bufferSize = 1024 * 8;
  30. static const int k_minTrackDuration = 30;
  31. lastfm::Fingerprint::Fingerprint( const Track& t )
  32. : m_track( t )
  33. , m_id( -1 ), m_duration( 0 )
  34. , m_complete( false )
  35. {
  36. QString id = Collection::instance().getFingerprintId( t.url().toLocalFile() );
  37. if (id.size()) {
  38. bool b;
  39. m_id = id.toInt( &b );
  40. if (!b) m_id = -1;
  41. }
  42. }
  43. void
  44. lastfm::Fingerprint::generate( FingerprintableSource* ms ) throw( Error )
  45. {
  46. //TODO throw if we can't get required metadata from the track object
  47. //TODO if (!QFileInfo( path ).isReadable())
  48. //TODO throw ReadError;
  49. int sampleRate, bitrate, numChannels;
  50. if ( !ms )
  51. throw ReadError;
  52. try
  53. {
  54. ms->init( m_track.url().toLocalFile() );
  55. ms->getInfo( m_duration, sampleRate, bitrate, numChannels );
  56. }
  57. catch (std::exception& e)
  58. {
  59. qWarning() << e.what();
  60. throw HeadersError;
  61. }
  62. if (m_duration < k_minTrackDuration)
  63. throw TrackTooShortError;
  64. ms->skipSilence();
  65. bool fpDone = false;
  66. fingerprint::FingerprintExtractor* extractor;
  67. try
  68. {
  69. extractor = new fingerprint::FingerprintExtractor;
  70. if (m_complete)
  71. {
  72. extractor->initForFullSubmit( sampleRate, numChannels );
  73. }
  74. else
  75. {
  76. extractor->initForQuery( sampleRate, numChannels, m_duration );
  77. // Skippety skip for as long as the skipper sez (optimisation)
  78. ms->skip( extractor->getToSkipMs() );
  79. float secsToSkip = extractor->getToSkipMs() / 1000.0f;
  80. fpDone = extractor->process( 0,
  81. (size_t) sampleRate * numChannels * secsToSkip,
  82. false );
  83. }
  84. }
  85. catch (std::exception& e)
  86. {
  87. qWarning() << e.what();
  88. throw DecodeError;
  89. }
  90. const size_t PCMBufSize = 131072;
  91. short* pPCMBuffer = new short[PCMBufSize];
  92. while (!fpDone)
  93. {
  94. size_t readData = ms->updateBuffer( pPCMBuffer, PCMBufSize );
  95. if (readData == 0)
  96. break;
  97. try
  98. {
  99. fpDone = extractor->process( pPCMBuffer, readData, ms->eof() );
  100. }
  101. catch ( const std::exception& e )
  102. {
  103. qWarning() << e.what();
  104. delete ms;
  105. delete[] pPCMBuffer;
  106. throw InternalError;
  107. }
  108. }
  109. delete[] pPCMBuffer;
  110. if (!fpDone)
  111. throw InternalError;
  112. // We succeeded
  113. std::pair<const char*, size_t> fpData = extractor->getFingerprint();
  114. if (fpData.first == NULL || fpData.second == 0)
  115. throw InternalError;
  116. // Make a deep copy before extractor gets deleted
  117. m_data = QByteArray( fpData.first, fpData.second );
  118. delete extractor;
  119. }
  120. static QString sha256( const QString& path )
  121. {
  122. // no clue why this is static, there was no comment when I refactored it
  123. // initially --mxcl
  124. static uint8_t pBuffer[SHA_BUFFER_SIZE+7];
  125. unsigned char hash[SHA256_HASH_SIZE];
  126. {
  127. QByteArray path8 = QFile::encodeName( path );
  128. std::ifstream inFile( path8.data(), std::ios::binary);
  129. SHA256Context sha256;
  130. SHA256Init( &sha256 );
  131. uint8_t* pMovableBuffer = pBuffer;
  132. // Ensure it is on a 64-bit boundary.
  133. INTPTR offs;
  134. if ((offs = reinterpret_cast<INTPTR>(pBuffer) & 7L))
  135. pMovableBuffer += 8 - offs;
  136. unsigned int len;
  137. for (;;)
  138. {
  139. inFile.read( reinterpret_cast<char*>(pMovableBuffer), SHA_BUFFER_SIZE );
  140. len = inFile.gcount();
  141. if (len == 0)
  142. break;
  143. SHA256Update( &sha256, pMovableBuffer, len );
  144. }
  145. SHA256Final( &sha256, hash );
  146. }
  147. QString sha;
  148. for (int i = 0; i < SHA256_HASH_SIZE; ++i)
  149. {
  150. QString hex = QString("%1").arg(uchar(hash[i]), 2, 16,
  151. QChar('0'));
  152. sha.append(hex);
  153. }
  154. return sha;
  155. }
  156. static QByteArray number( uint n )
  157. {
  158. return n ? QByteArray::number( n ) : "";
  159. }
  160. QNetworkReply*
  161. lastfm::Fingerprint::submit() const
  162. {
  163. if (m_data.isEmpty())
  164. return 0;
  165. //Parameters understood by the server according to the MIR team:
  166. //{ "trackid", "recordingid", "artist", "album", "track", "duration",
  167. // "tracknum", "username", "sha256", "ip", "fpversion", "mbid",
  168. // "filename", "genre", "year", "samplerate", "noupdate", "fulldump" }
  169. Track const t = m_track;
  170. QString const path = t.url().toLocalFile();
  171. QFileInfo const fi( path );
  172. #define e( x ) QUrl::toPercentEncoding( x )
  173. QUrl url( "http://www.last.fm/fingerprint/query/" );
  174. url.addEncodedQueryItem( "artist", e(t.artist()) );
  175. url.addEncodedQueryItem( "album", e(t.album()) );
  176. url.addEncodedQueryItem( "track", e(t.title()) );
  177. url.addEncodedQueryItem( "duration", number( m_duration > 0 ? m_duration : t.duration() ) );
  178. url.addEncodedQueryItem( "mbid", e(t.mbid()) );
  179. url.addEncodedQueryItem( "filename", e(fi.completeBaseName()) );
  180. url.addEncodedQueryItem( "fileextension", e(fi.completeSuffix()) );
  181. url.addEncodedQueryItem( "tracknum", number( t.trackNumber() ) );
  182. url.addEncodedQueryItem( "sha256", sha256( path ).toAscii() );
  183. url.addEncodedQueryItem( "time", number(QDateTime::currentDateTime().toTime_t()) );
  184. url.addEncodedQueryItem( "fpversion", QByteArray::number((int)fingerprint::FingerprintExtractor::getVersion()) );
  185. url.addEncodedQueryItem( "fulldump", m_complete ? "true" : "false" );
  186. url.addEncodedQueryItem( "noupdate", "false" );
  187. #undef e
  188. //FIXME: talk to mir about submitting fplibversion
  189. QNetworkRequest request( url );
  190. request.setHeader( QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=----------------------------8e61d618ca16" );
  191. QByteArray bytes;
  192. bytes += "------------------------------8e61d618ca16\r\n";
  193. bytes += "Content-Disposition: ";
  194. bytes += "form-data; name=\"fpdata\"";
  195. bytes += "\r\n\r\n";
  196. bytes += m_data;
  197. bytes += "\r\n";
  198. bytes += "------------------------------8e61d618ca16--\r\n";
  199. qDebug() << url;
  200. qDebug() << "Fingerprint size:" << bytes.size() << "bytes";
  201. return lastfm::nam()->post( request, bytes );
  202. }
  203. void
  204. lastfm::Fingerprint::decode( QNetworkReply* reply, bool* complete_fingerprint_requested ) throw( Error )
  205. {
  206. // The response data will consist of a number and a string.
  207. // The number is the fpid and the string is either FOUND or NEW
  208. // (or NOT FOUND when noupdate was used). NEW means we should
  209. // schedule a full fingerprint.
  210. //
  211. // In the case of an error, there will be no initial number, just
  212. // an error string.
  213. QString const response( reply->readAll() );
  214. QStringList const list = response.split( ' ' );
  215. QString const fpid = list.value( 0 );
  216. QString const status = list.value( 1 );
  217. if (response.isEmpty() || list.count() < 2 || response == "No response to client error")
  218. goto bad_response;
  219. if (list.count() != 2)
  220. qWarning() << "Response looks bad but continuing anyway:" << response;
  221. {
  222. // so variables go out of scope before jump to label
  223. // otherwise compiler error on GCC 4.2
  224. bool b;
  225. uint fpid_as_uint = fpid.toUInt( &b );
  226. if (!b) goto bad_response;
  227. Collection::instance().setFingerprintId( m_track.url().toLocalFile(), fpid );
  228. if (complete_fingerprint_requested)
  229. *complete_fingerprint_requested = (status == "NEW");
  230. m_id = (int)fpid_as_uint;
  231. return;
  232. }
  233. bad_response:
  234. qWarning() << "Response is bad:" << response;
  235. throw BadResponseError;
  236. }