PageRenderTime 54ms CodeModel.GetById 2ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 0ms

/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
 20#include "RdioParser.h"
 21
 22#include <QDateTime>
 23#include <QtNetwork/QNetworkAccessManager>
 24#include <QUrl>
 25#include <QStringList>
 26
 27#include <QtCore/QCryptographicHash>
 28
 29#include <qjson/parser.h>
 30
 31#include "ShortenedLinkParser.h"
 32#include "config.h"
 33#include "DropJob.h"
 34#include "DropJobNotifier.h"
 35#include "ViewManager.h"
 36#include "SourceList.h"
 37#include "jobview/JobStatusView.h"
 38#include "jobview/JobStatusModel.h"
 39#include "jobview/ErrorStatusMessage.h"
 40#include "utils/NetworkReply.h"
 41#include "utils/TomahawkUtils.h"
 42#include "utils/Logger.h"
 43
 44using namespace Tomahawk;
 45
 46QPixmap* RdioParser::s_pixmap = 0;
 47
 48#ifdef QCA2_FOUND
 49QCA::Initializer RdioParser::m_qcaInit = QCA::Initializer();
 50#endif
 51
 52
 53RdioParser::RdioParser( QObject* parent )
 54    : QObject( parent )
 55    , m_count( 0 )
 56    , m_browseJob( 0 )
 57    , m_createPlaylist( false )
 58{
 59}
 60
 61
 62RdioParser::~RdioParser()
 63{
 64}
 65
 66
 67void
 68RdioParser::parse( const QString& url )
 69{
 70    m_multi = false;
 71    m_total = 1;
 72    parseUrl( url );
 73}
 74
 75
 76void
 77RdioParser::parse( const QStringList& urls )
 78{
 79    m_multi = true;
 80    m_total = urls.count();
 81
 82    foreach ( const QString& url, urls )
 83        parseUrl( url );
 84}
 85
 86
 87void
 88RdioParser::parseUrl( const QString& url )
 89{
 90    if ( url.contains( "rd.io" ) ) // shortened
 91    {
 92        ShortenedLinkParser* p = new ShortenedLinkParser( QStringList() << url, this );
 93        connect( p, SIGNAL( urls( QStringList ) ), this, SLOT( expandedLinks( QStringList ) ) );
 94        return;
 95    }
 96
 97    if ( url.contains( "artist" ) && url.contains( "album" ) && url.contains( "track" ) )
 98        parseTrack( url );
 99    else
100    {
101        DropJob::DropType type = DropJob::None;
102        if ( url.contains( "artist" ) && url.contains( "album" ) )
103            type = DropJob::Album;
104        else if ( url.contains( "artist" ) )
105            type = DropJob::Artist;
106        else if ( url.contains( "people" ) && url.contains( "playlist" ) )
107            type = DropJob::Playlist;
108        else
109        {
110            tLog() << "Got Rdio URL I can't parse!" << url;
111            return;
112        }
113
114        // artist, album, or playlist link requre fetching
115        fetchObjectsFromUrl( url, type );
116    }
117}
118
119
120void
121RdioParser::fetchObjectsFromUrl( const QString& url, DropJob::DropType type )
122{
123    QList< QPair< QByteArray, QByteArray > > params;
124    params.append( QPair<QByteArray, QByteArray>( "extras", "tracks" ) );
125
126    QString cleanedUrl = url;
127    cleanedUrl.replace("#/", "");
128
129    QByteArray data;
130    QNetworkRequest request = generateRequest( "getObjectFromUrl", cleanedUrl, params, &data );
131
132    request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/x-www-form-urlencoded" ) );
133    NetworkReply* reply = new NetworkReply( TomahawkUtils::nam()->post( request, data ) );
134    connect( reply, SIGNAL( finished() ), SLOT( rdioReturned() ) );
135
136    m_browseJob = new DropJobNotifier( pixmap(), QString( "Rdio" ), type, reply );
137    JobStatusView::instance()->model()->addJob( m_browseJob );
138
139    m_reqQueries.insert( reply );
140}
141
142
143void
144RdioParser::rdioReturned()
145{
146    NetworkReply* r = qobject_cast< NetworkReply* >( sender() );
147    Q_ASSERT( r );
148    m_reqQueries.remove( r );
149    m_count++;
150    r->deleteLater();
151
152    if ( r->reply()->error() == QNetworkReply::NoError )
153    {
154        QJson::Parser p;
155        bool ok;
156        QVariantMap res = p.parse( r->reply(), &ok ).toMap();
157        QVariantMap result = res.value( "result" ).toMap();
158
159        if ( !ok || result.isEmpty() )
160        {
161            tLog() << "Failed to parse json from Rdio browse item:" << p.errorString() << "On line" << p.errorLine() << "With data:" << res;
162
163            return;
164        }
165
166        QVariantList tracks = result.value( "tracks" ).toList();
167        if ( tracks.isEmpty() )
168        {
169            tLog() << "Got no tracks in result, ignoring!" << result;
170            return;
171        }
172
173        // Playlists will have these
174        m_title = result[ "name" ].toString();
175        m_creator = result[ "owner" ].toString();
176
177        foreach( QVariant track, tracks )
178        {
179            QVariantMap rdioResult = track.toMap();
180            QString title, artist, album;
181
182            title = rdioResult.value( "name", QString() ).toString();
183            artist = rdioResult.value( "artist", QString() ).toString();
184            album = rdioResult.value( "album", QString() ).toString();
185
186            if ( title.isEmpty() && artist.isEmpty() ) // don't have enough...
187            {
188                tLog() << "Didn't get an artist and track name from Rdio, not enough to build a query on. Aborting" << title << artist << album;
189                return;
190            }
191
192            Tomahawk::query_ptr q = Tomahawk::Query::get( artist, title, album, uuid(), !m_createPlaylist );
193            if ( q.isNull() )
194                continue;
195
196            m_tracks << q;
197        }
198    }
199    else
200    {
201        JobStatusView::instance()->model()->addJob( new ErrorStatusMessage( tr( "Error fetching Rdio information from the network!" ) ) );
202        tLog() << "Error in network request to Rdio for track decoding:" << r->reply()->errorString();
203    }
204
205    checkFinished();
206}
207
208
209void
210RdioParser::parseTrack( const QString& origUrl )
211{
212    QString url = origUrl;
213    QString artist, trk, album, playlist;
214    QString realUrl = url.replace( "_", " " );
215    QString matchStr = "/%1/([^/]*)/";
216    QString matchPlStr = "/%1/(?:[^/]*)/([^/]*)/";
217
218    QRegExp r( QString( matchStr ).arg( "artist" ) );
219
220    int loc = r.indexIn( realUrl );
221    if ( loc >= 0 )
222        artist = r.cap( 1 );
223
224    r = QRegExp( QString( matchStr ).arg( "album" ) );
225    loc = r.indexIn( realUrl );
226    if ( loc >= 0 )
227        album = r.cap( 1 );
228
229    r = QRegExp( QString( matchStr ).arg( "track" ) );
230    loc = r.indexIn( realUrl );
231    if ( loc >= 0 )
232        trk = r.cap( 1 );
233
234    r = QRegExp( QString( matchPlStr ).arg( "playlists" ) );
235    loc = r.indexIn( realUrl );
236    if ( loc >= 0 )
237        playlist = r.cap( 1 );
238
239    if ( trk.isEmpty() || artist.isEmpty() )
240    {
241        tLog() << "Parsed Rdio track url but it's missing artist or track!" << url;
242        return;
243    }
244
245    query_ptr q = Query::get( artist, trk, album, uuid(), !m_createPlaylist );
246    m_count++;
247    m_tracks << q;
248
249    checkFinished();
250}
251
252
253QNetworkRequest
254RdioParser::generateRequest( const QString& method, const QString& url, const QList< QPair< QByteArray, QByteArray > >& extraParams, QByteArray* data )
255{
256    QUrl fetchUrl( "http://api.rdio.com/1/" );
257    QUrl toSignUrl = fetchUrl;
258
259    QPair<QByteArray, QByteArray> param;
260    foreach ( param, extraParams )
261    {
262        toSignUrl.addEncodedQueryItem( param.first, param.second );
263    }
264    toSignUrl.addQueryItem( "method", method );
265    toSignUrl.addEncodedQueryItem("oauth_consumer_key", "gk8zmyzj5xztt8aj48csaart" );
266    QString nonce;
267    for ( int i = 0; i < 8; i++ )
268        nonce += QString::number( qrand() % 10 );
269    toSignUrl.addQueryItem("oauth_nonce", nonce );
270    toSignUrl.addEncodedQueryItem("oauth_signature_method", "HMAC-SHA1");
271    toSignUrl.addQueryItem("oauth_timestamp", QString::number(QDateTime::currentMSecsSinceEpoch() / 1000 ) );
272    toSignUrl.addEncodedQueryItem("oauth_version",  "1.0");
273    toSignUrl.addEncodedQueryItem( "url", QUrl::toPercentEncoding( url ) );
274    int size = toSignUrl.encodedQueryItems().size();
275    for( int i = 0; i < size; i++ ) {
276        const QPair< QByteArray, QByteArray > item = toSignUrl.encodedQueryItems().at( i );
277        data->append( item.first + "=" + item.second + "&" );
278    }
279    data->truncate( data->size() - 1 ); // remove extra &
280
281    QByteArray toSign = "POST&" + QUrl::toPercentEncoding( fetchUrl.toEncoded() ) + '&' + QUrl::toPercentEncoding( *data );
282    qDebug() << "Rdio" << toSign;
283
284    toSignUrl.addEncodedQueryItem( "oauth_signature", QUrl::toPercentEncoding( hmacSha1("yt35kakDyW&", toSign ) ) );
285
286    data->clear();
287    size = toSignUrl.encodedQueryItems().size();
288    for( int i = 0; i < size; i++ ) {
289        const QPair< QByteArray, QByteArray > item = toSignUrl.encodedQueryItems().at( i );
290        data->append( item.first + "=" + item.second + "&" );
291    }
292    data->truncate( data->size() - 1 ); // remove extra &
293
294    QNetworkRequest request = QNetworkRequest( fetchUrl );
295    request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/x-www-form-urlencoded" ) );
296
297    return request;
298}
299
300
301QByteArray
302RdioParser::hmacSha1(QByteArray key, QByteArray baseString)
303{
304#ifdef QCA2_FOUND
305    QCA::MessageAuthenticationCode hmacsha1( "hmac(sha1)", QCA::SecureArray() );
306    QCA::SymmetricKey keyObject( key );
307    hmacsha1.setup( keyObject );
308
309    hmacsha1.update( QCA::SecureArray( baseString ) );
310    QCA::SecureArray resultArray = hmacsha1.final();
311
312    QByteArray result = resultArray.toByteArray().toBase64();
313    return result;
314#else
315    tLog() << "Tomahawk compiled without QCA support, cannot generate HMAC signature";
316    return QByteArray();
317#endif
318}
319
320
321void
322RdioParser::checkFinished()
323{
324    tDebug() << "Checking for Rdio batch playlist job finished" << m_reqQueries.isEmpty();
325    if ( m_reqQueries.isEmpty() ) // we're done
326    {
327        if ( m_browseJob )
328            m_browseJob->setFinished();
329
330        if ( m_tracks.isEmpty() )
331            return;
332
333        if ( m_createPlaylist )
334        {
335            m_playlist = Playlist::create( SourceList::instance()->getLocal(),
336                                           uuid(),
337                                           m_title,
338                                           "",
339                                           m_creator,
340                                           false,
341                                           m_tracks );
342
343            connect( m_playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( playlistCreated() ) );
344
345            return;
346        }
347        else
348        {
349            if ( !m_multi )
350                emit track( m_tracks.first() );
351            else if ( m_multi && m_count == m_total )
352                emit tracks( m_tracks );
353
354            m_tracks.clear();
355        }
356
357        deleteLater();
358    }
359}
360
361
362void
363RdioParser::playlistCreated( Tomahawk::PlaylistRevision )
364{
365    ViewManager::instance()->show( m_playlist );
366}
367
368
369void
370RdioParser::expandedLinks( const QStringList& urls )
371{
372    foreach( const QString& url, urls )
373    {
374        if ( url.contains( "rdio.com" ) || url.contains( "rd.io" ) )
375            parseUrl( url );
376    }
377}
378
379
380QPixmap
381RdioParser::pixmap() const
382{
383    if ( !s_pixmap )
384        s_pixmap = new QPixmap( RESPATH "images/rdio.png" );
385
386    return *s_pixmap;
387}