/src/libtomahawk/utils/rdioparser.cpp

http://github.com/tomahawk-player/tomahawk · C++ · 387 lines · 289 code · 78 blank · 20 comment · 40 complexity · cea584dbba94c229e257e9dd93afb426 MD5 · raw file

  1. /* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2. *
  3. * Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
  4. * Copyright 2010-2011, Hugo Lindstr??m <hugolm84@gmail.com>
  5. *
  6. * Tomahawk is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Tomahawk is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. #include "RdioParser.h"
  20. #include <QDateTime>
  21. #include <QtNetwork/QNetworkAccessManager>
  22. #include <QUrl>
  23. #include <QStringList>
  24. #include <QtCore/QCryptographicHash>
  25. #include <qjson/parser.h>
  26. #include "ShortenedLinkParser.h"
  27. #include "config.h"
  28. #include "DropJob.h"
  29. #include "DropJobNotifier.h"
  30. #include "ViewManager.h"
  31. #include "SourceList.h"
  32. #include "jobview/JobStatusView.h"
  33. #include "jobview/JobStatusModel.h"
  34. #include "jobview/ErrorStatusMessage.h"
  35. #include "utils/NetworkReply.h"
  36. #include "utils/TomahawkUtils.h"
  37. #include "utils/Logger.h"
  38. using namespace Tomahawk;
  39. QPixmap* RdioParser::s_pixmap = 0;
  40. #ifdef QCA2_FOUND
  41. QCA::Initializer RdioParser::m_qcaInit = QCA::Initializer();
  42. #endif
  43. RdioParser::RdioParser( QObject* parent )
  44. : QObject( parent )
  45. , m_count( 0 )
  46. , m_browseJob( 0 )
  47. , m_createPlaylist( false )
  48. {
  49. }
  50. RdioParser::~RdioParser()
  51. {
  52. }
  53. void
  54. RdioParser::parse( const QString& url )
  55. {
  56. m_multi = false;
  57. m_total = 1;
  58. parseUrl( url );
  59. }
  60. void
  61. RdioParser::parse( const QStringList& urls )
  62. {
  63. m_multi = true;
  64. m_total = urls.count();
  65. foreach ( const QString& url, urls )
  66. parseUrl( url );
  67. }
  68. void
  69. RdioParser::parseUrl( const QString& url )
  70. {
  71. if ( url.contains( "rd.io" ) ) // shortened
  72. {
  73. ShortenedLinkParser* p = new ShortenedLinkParser( QStringList() << url, this );
  74. connect( p, SIGNAL( urls( QStringList ) ), this, SLOT( expandedLinks( QStringList ) ) );
  75. return;
  76. }
  77. if ( url.contains( "artist" ) && url.contains( "album" ) && url.contains( "track" ) )
  78. parseTrack( url );
  79. else
  80. {
  81. DropJob::DropType type = DropJob::None;
  82. if ( url.contains( "artist" ) && url.contains( "album" ) )
  83. type = DropJob::Album;
  84. else if ( url.contains( "artist" ) )
  85. type = DropJob::Artist;
  86. else if ( url.contains( "people" ) && url.contains( "playlist" ) )
  87. type = DropJob::Playlist;
  88. else
  89. {
  90. tLog() << "Got Rdio URL I can't parse!" << url;
  91. return;
  92. }
  93. // artist, album, or playlist link requre fetching
  94. fetchObjectsFromUrl( url, type );
  95. }
  96. }
  97. void
  98. RdioParser::fetchObjectsFromUrl( const QString& url, DropJob::DropType type )
  99. {
  100. QList< QPair< QByteArray, QByteArray > > params;
  101. params.append( QPair<QByteArray, QByteArray>( "extras", "tracks" ) );
  102. QString cleanedUrl = url;
  103. cleanedUrl.replace("#/", "");
  104. QByteArray data;
  105. QNetworkRequest request = generateRequest( "getObjectFromUrl", cleanedUrl, params, &data );
  106. request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/x-www-form-urlencoded" ) );
  107. NetworkReply* reply = new NetworkReply( TomahawkUtils::nam()->post( request, data ) );
  108. connect( reply, SIGNAL( finished() ), SLOT( rdioReturned() ) );
  109. m_browseJob = new DropJobNotifier( pixmap(), QString( "Rdio" ), type, reply );
  110. JobStatusView::instance()->model()->addJob( m_browseJob );
  111. m_reqQueries.insert( reply );
  112. }
  113. void
  114. RdioParser::rdioReturned()
  115. {
  116. NetworkReply* r = qobject_cast< NetworkReply* >( sender() );
  117. Q_ASSERT( r );
  118. m_reqQueries.remove( r );
  119. m_count++;
  120. r->deleteLater();
  121. if ( r->reply()->error() == QNetworkReply::NoError )
  122. {
  123. QJson::Parser p;
  124. bool ok;
  125. QVariantMap res = p.parse( r->reply(), &ok ).toMap();
  126. QVariantMap result = res.value( "result" ).toMap();
  127. if ( !ok || result.isEmpty() )
  128. {
  129. tLog() << "Failed to parse json from Rdio browse item:" << p.errorString() << "On line" << p.errorLine() << "With data:" << res;
  130. return;
  131. }
  132. QVariantList tracks = result.value( "tracks" ).toList();
  133. if ( tracks.isEmpty() )
  134. {
  135. tLog() << "Got no tracks in result, ignoring!" << result;
  136. return;
  137. }
  138. // Playlists will have these
  139. m_title = result[ "name" ].toString();
  140. m_creator = result[ "owner" ].toString();
  141. foreach( QVariant track, tracks )
  142. {
  143. QVariantMap rdioResult = track.toMap();
  144. QString title, artist, album;
  145. title = rdioResult.value( "name", QString() ).toString();
  146. artist = rdioResult.value( "artist", QString() ).toString();
  147. album = rdioResult.value( "album", QString() ).toString();
  148. if ( title.isEmpty() && artist.isEmpty() ) // don't have enough...
  149. {
  150. tLog() << "Didn't get an artist and track name from Rdio, not enough to build a query on. Aborting" << title << artist << album;
  151. return;
  152. }
  153. Tomahawk::query_ptr q = Tomahawk::Query::get( artist, title, album, uuid(), !m_createPlaylist );
  154. if ( q.isNull() )
  155. continue;
  156. m_tracks << q;
  157. }
  158. }
  159. else
  160. {
  161. JobStatusView::instance()->model()->addJob( new ErrorStatusMessage( tr( "Error fetching Rdio information from the network!" ) ) );
  162. tLog() << "Error in network request to Rdio for track decoding:" << r->reply()->errorString();
  163. }
  164. checkFinished();
  165. }
  166. void
  167. RdioParser::parseTrack( const QString& origUrl )
  168. {
  169. QString url = origUrl;
  170. QString artist, trk, album, playlist;
  171. QString realUrl = url.replace( "_", " " );
  172. QString matchStr = "/%1/([^/]*)/";
  173. QString matchPlStr = "/%1/(?:[^/]*)/([^/]*)/";
  174. QRegExp r( QString( matchStr ).arg( "artist" ) );
  175. int loc = r.indexIn( realUrl );
  176. if ( loc >= 0 )
  177. artist = r.cap( 1 );
  178. r = QRegExp( QString( matchStr ).arg( "album" ) );
  179. loc = r.indexIn( realUrl );
  180. if ( loc >= 0 )
  181. album = r.cap( 1 );
  182. r = QRegExp( QString( matchStr ).arg( "track" ) );
  183. loc = r.indexIn( realUrl );
  184. if ( loc >= 0 )
  185. trk = r.cap( 1 );
  186. r = QRegExp( QString( matchPlStr ).arg( "playlists" ) );
  187. loc = r.indexIn( realUrl );
  188. if ( loc >= 0 )
  189. playlist = r.cap( 1 );
  190. if ( trk.isEmpty() || artist.isEmpty() )
  191. {
  192. tLog() << "Parsed Rdio track url but it's missing artist or track!" << url;
  193. return;
  194. }
  195. query_ptr q = Query::get( artist, trk, album, uuid(), !m_createPlaylist );
  196. m_count++;
  197. m_tracks << q;
  198. checkFinished();
  199. }
  200. QNetworkRequest
  201. RdioParser::generateRequest( const QString& method, const QString& url, const QList< QPair< QByteArray, QByteArray > >& extraParams, QByteArray* data )
  202. {
  203. QUrl fetchUrl( "http://api.rdio.com/1/" );
  204. QUrl toSignUrl = fetchUrl;
  205. QPair<QByteArray, QByteArray> param;
  206. foreach ( param, extraParams )
  207. {
  208. toSignUrl.addEncodedQueryItem( param.first, param.second );
  209. }
  210. toSignUrl.addQueryItem( "method", method );
  211. toSignUrl.addEncodedQueryItem("oauth_consumer_key", "gk8zmyzj5xztt8aj48csaart" );
  212. QString nonce;
  213. for ( int i = 0; i < 8; i++ )
  214. nonce += QString::number( qrand() % 10 );
  215. toSignUrl.addQueryItem("oauth_nonce", nonce );
  216. toSignUrl.addEncodedQueryItem("oauth_signature_method", "HMAC-SHA1");
  217. toSignUrl.addQueryItem("oauth_timestamp", QString::number(QDateTime::currentMSecsSinceEpoch() / 1000 ) );
  218. toSignUrl.addEncodedQueryItem("oauth_version", "1.0");
  219. toSignUrl.addEncodedQueryItem( "url", QUrl::toPercentEncoding( url ) );
  220. int size = toSignUrl.encodedQueryItems().size();
  221. for( int i = 0; i < size; i++ ) {
  222. const QPair< QByteArray, QByteArray > item = toSignUrl.encodedQueryItems().at( i );
  223. data->append( item.first + "=" + item.second + "&" );
  224. }
  225. data->truncate( data->size() - 1 ); // remove extra &
  226. QByteArray toSign = "POST&" + QUrl::toPercentEncoding( fetchUrl.toEncoded() ) + '&' + QUrl::toPercentEncoding( *data );
  227. qDebug() << "Rdio" << toSign;
  228. toSignUrl.addEncodedQueryItem( "oauth_signature", QUrl::toPercentEncoding( hmacSha1("yt35kakDyW&", toSign ) ) );
  229. data->clear();
  230. size = toSignUrl.encodedQueryItems().size();
  231. for( int i = 0; i < size; i++ ) {
  232. const QPair< QByteArray, QByteArray > item = toSignUrl.encodedQueryItems().at( i );
  233. data->append( item.first + "=" + item.second + "&" );
  234. }
  235. data->truncate( data->size() - 1 ); // remove extra &
  236. QNetworkRequest request = QNetworkRequest( fetchUrl );
  237. request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/x-www-form-urlencoded" ) );
  238. return request;
  239. }
  240. QByteArray
  241. RdioParser::hmacSha1(QByteArray key, QByteArray baseString)
  242. {
  243. #ifdef QCA2_FOUND
  244. QCA::MessageAuthenticationCode hmacsha1( "hmac(sha1)", QCA::SecureArray() );
  245. QCA::SymmetricKey keyObject( key );
  246. hmacsha1.setup( keyObject );
  247. hmacsha1.update( QCA::SecureArray( baseString ) );
  248. QCA::SecureArray resultArray = hmacsha1.final();
  249. QByteArray result = resultArray.toByteArray().toBase64();
  250. return result;
  251. #else
  252. tLog() << "Tomahawk compiled without QCA support, cannot generate HMAC signature";
  253. return QByteArray();
  254. #endif
  255. }
  256. void
  257. RdioParser::checkFinished()
  258. {
  259. tDebug() << "Checking for Rdio batch playlist job finished" << m_reqQueries.isEmpty();
  260. if ( m_reqQueries.isEmpty() ) // we're done
  261. {
  262. if ( m_browseJob )
  263. m_browseJob->setFinished();
  264. if ( m_tracks.isEmpty() )
  265. return;
  266. if ( m_createPlaylist )
  267. {
  268. m_playlist = Playlist::create( SourceList::instance()->getLocal(),
  269. uuid(),
  270. m_title,
  271. "",
  272. m_creator,
  273. false,
  274. m_tracks );
  275. connect( m_playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( playlistCreated() ) );
  276. return;
  277. }
  278. else
  279. {
  280. if ( !m_multi )
  281. emit track( m_tracks.first() );
  282. else if ( m_multi && m_count == m_total )
  283. emit tracks( m_tracks );
  284. m_tracks.clear();
  285. }
  286. deleteLater();
  287. }
  288. }
  289. void
  290. RdioParser::playlistCreated( Tomahawk::PlaylistRevision )
  291. {
  292. ViewManager::instance()->show( m_playlist );
  293. }
  294. void
  295. RdioParser::expandedLinks( const QStringList& urls )
  296. {
  297. foreach( const QString& url, urls )
  298. {
  299. if ( url.contains( "rdio.com" ) || url.contains( "rd.io" ) )
  300. parseUrl( url );
  301. }
  302. }
  303. QPixmap
  304. RdioParser::pixmap() const
  305. {
  306. if ( !s_pixmap )
  307. s_pixmap = new QPixmap( RESPATH "images/rdio.png" );
  308. return *s_pixmap;
  309. }