PageRenderTime 77ms CodeModel.GetById 37ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/src/libtomahawk/utils/SpotifyParser.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 498 lines | 349 code | 92 blank | 57 comment | 74 complexity | bfc8ba157c6d4b53e242ffdcbdbcd4ad 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 *   Copyright 2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  6 *
  7 *   Tomahawk 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 *   Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 19 */
 20
 21#include "SpotifyParser.h"
 22
 23#include "jobview/JobStatusView.h"
 24#include "jobview/JobStatusModel.h"
 25#include "jobview/ErrorStatusMessage.h"
 26#include "utils/Json.h"
 27#include "utils/NetworkReply.h"
 28#include "utils/TomahawkUtils.h"
 29#include "utils/Logger.h"
 30#include "utils/NetworkAccessManager.h"
 31
 32#include "Query.h"
 33#include "SourceList.h"
 34#include "DropJob.h"
 35#include "DropJobNotifier.h"
 36#include "ViewManager.h"
 37
 38#include <QNetworkAccessManager>
 39
 40using namespace Tomahawk;
 41
 42QPixmap* SpotifyParser::s_pixmap = 0;
 43
 44
 45SpotifyParser::SpotifyParser( const QStringList& Urls, bool createNewPlaylist, QObject* parent )
 46    : QObject ( parent )
 47    , m_limit ( 40 )
 48    , m_single( false )
 49    , m_trackMode( true )
 50    , m_collaborative( false )
 51    , m_createNewPlaylist( createNewPlaylist )
 52    , m_browseJob( 0 )
 53    , m_subscribers( 0 )
 54{
 55    foreach ( const QString& url, Urls )
 56        lookupUrl( url );
 57}
 58
 59
 60SpotifyParser::SpotifyParser( const QString& Url, bool createNewPlaylist, QObject* parent )
 61    : QObject ( parent )
 62    , m_limit ( 40 )
 63    , m_single( true )
 64    , m_trackMode( true )
 65    , m_collaborative( false )
 66    , m_createNewPlaylist( createNewPlaylist )
 67    , m_browseJob( 0 )
 68    , m_subscribers( 0 )
 69{
 70    lookupUrl( Url );
 71}
 72
 73
 74SpotifyParser::~SpotifyParser()
 75{
 76}
 77
 78
 79void
 80SpotifyParser::lookupUrl( const QString& rawLink )
 81{
 82    tLog() << "Looking up Spotify rawURI:" << rawLink;
 83    QString link = rawLink;
 84
 85    QRegExp isHttp( "(?:((play|open)\\.spotify.com))(.*)" );
 86
 87    // Some spotify apps contain the link to the playlist as url-encoded in their link (e.g. ShareMyPlaylists)
 88    if ( link.contains( "%253A" ) )
 89    {
 90        link = QUrl::fromPercentEncoding( link.toUtf8() );
 91    }
 92
 93    if( link.contains( "%3A" ) )
 94    {
 95        link = QUrl::fromPercentEncoding( link.toUtf8() );
 96    }
 97
 98    if( isHttp.indexIn( link, 0 ) != -1 )
 99    {
100        link = "spotify"+isHttp.cap( 3 ).replace( "/", ":" );
101    }
102
103    // TODO: Ignoring search and user querys atm
104    // (spotify:(?:(?:artist|album|track|user:[^:]+:playlist):[a-zA-Z0-9]+|user:[^:]+|search:(?:[-\w$\.+!*'(),<>:\s]+|%[a-fA-F0-9\s]{2})+))
105    QRegExp rx( "(spotify:(?:(?:artist|album|track|user:[^:]+:playlist):[a-zA-Z0-9]+[^:\?]))" );
106    if ( rx.indexIn( link, 0 ) != -1 )
107    {
108        link = rx.cap( 1 );
109    }
110    else
111    {
112        tLog() << "Bad SpotifyURI!" << link;
113        return;
114    }
115
116    if ( link.contains( "track" ) )
117    {
118        m_trackMode = true;
119        lookupTrack( link );
120    }
121    else if ( link.contains( "playlist" ) ||  link.contains( "album" ) || link.contains( "artist" ) )
122    {
123        if( !m_createNewPlaylist )
124            m_trackMode = true;
125        else
126            m_trackMode = false;
127
128        lookupSpotifyBrowse( link );
129    }
130    else
131        return; // Not valid spotify item
132}
133
134
135void
136SpotifyParser::lookupSpotifyBrowse( const QString& link )
137{
138    tLog() << "Parsing Spotify Browse URI:" << link;
139
140    // Used in checkBrowseFinished as identifier
141    m_browseUri = link;
142
143    if ( m_browseUri.contains( "playlist" ) &&
144         Tomahawk::Accounts::SpotifyAccount::instance() != 0 &&
145         Tomahawk::Accounts::SpotifyAccount::instance()->loggedIn() )
146    {
147        // Do a playlist lookup locally
148        // Running resolver, so do the lookup through that
149        qDebug() << Q_FUNC_INFO << "Doing playlist lookup through spotify resolver:" << m_browseUri;
150        QVariantMap message;
151        message[ "_msgtype" ] = "playlistListing";
152        message[ "id" ] = m_browseUri;
153
154        QMetaObject::invokeMethod( Tomahawk::Accounts::SpotifyAccount::instance(), "sendMessage", Qt::QueuedConnection, Q_ARG( QVariantMap, message ),
155                                                                                                                        Q_ARG( QObject*, this ),
156                                                                                                                        Q_ARG( QString, "playlistListingResult" ) );
157
158        return;
159    }
160
161    DropJob::DropType type;
162
163    if ( m_browseUri.contains( "spotify:user" ) )
164        type = DropJob::Playlist;
165    else if ( m_browseUri.contains( "spotify:artist" ) )
166        type = DropJob::Artist;
167    else if ( m_browseUri.contains( "spotify:album" ) )
168        type = DropJob::Album;
169    else if ( m_browseUri.contains( "spotify:track" ) )
170        type = DropJob::Track;
171    else
172        return; // Type not supported.
173
174    QUrl url;
175
176    if ( type != DropJob::Artist )
177         url = QUrl( QString( SPOTIFY_PLAYLIST_API_URL "/browse/%1" ).arg( m_browseUri ) );
178    else
179         url = QUrl( QString( SPOTIFY_PLAYLIST_API_URL "/browse/%1/%2" ).arg( m_browseUri )
180                                                                        .arg ( m_limit ) );
181
182    NetworkReply* reply = new NetworkReply( Tomahawk::Utils::nam()->get( QNetworkRequest( url ) ) );
183    connect( reply, SIGNAL( finished() ), SLOT( spotifyBrowseFinished() ) );
184
185    m_browseJob = new DropJobNotifier( pixmap(), "Spotify", type, reply );
186    JobStatusView::instance()->model()->addJob( m_browseJob );
187
188    m_queries.insert( reply );
189}
190
191
192void
193SpotifyParser::lookupTrack( const QString& link )
194{
195    if ( !link.contains( "track" ) ) // we only support track links atm
196        return;
197
198    // we need Spotify URIs such as spotify:track:XXXXXX, so if we by chance get a http://open.spotify.com url, convert it
199    QString uri = link;
200    if ( link.contains( "open.spotify.com" ) || link.contains( "play.spotify.com" ) )
201    {
202        QString hash = link;
203        hash.replace( "http://open.spotify.com/track/", "" ).replace( "http://play.spotify.com/track/", "" );
204        uri = QString( "spotify:track:%1" ).arg( hash );
205    }
206
207    QUrl url = QUrl( QString( "http://ws.spotify.com/lookup/1/.json?uri=%1" ).arg( uri ) );
208
209    NetworkReply* reply = new NetworkReply( Tomahawk::Utils::nam()->get( QNetworkRequest( url ) ) );
210    connect( reply, SIGNAL( finished() ), SLOT( spotifyTrackLookupFinished() ) );
211
212    DropJobNotifier* j = new DropJobNotifier( pixmap(), QString( "Spotify" ), DropJob::Track, reply );
213    JobStatusView::instance()->model()->addJob( j );
214
215    m_queries.insert( reply );
216}
217
218
219void
220SpotifyParser::spotifyBrowseFinished()
221{
222    NetworkReply* r = qobject_cast< NetworkReply* >( sender() );
223    Q_ASSERT( r );
224    r->deleteLater();
225    m_queries.remove( r );
226
227    if ( r->reply()->error() == QNetworkReply::NoError )
228    {
229        bool ok;
230        QByteArray jsonData = r->reply()->readAll();
231        QVariantMap res = TomahawkUtils::parseJson( jsonData, &ok ).toMap();
232
233        if ( !ok )
234        {
235            tLog() << "Failed to parse json from Spotify browse item:" << jsonData;
236            checkTrackFinished();
237            return;
238        }
239
240        QVariantMap resultResponse = res.value( res.value( "type" ).toString() ).toMap();
241        if ( !resultResponse.isEmpty() )
242        {
243            m_title = resultResponse.value( "name" ).toString();
244            m_single = false;
245
246            if ( res.value( "type" ).toString() == "playlist" )
247                m_creator = resultResponse.value( "creator" ).toString();
248
249            // TODO for now only take the first artist
250            foreach ( QVariant result, resultResponse.value( "result" ).toList() )
251            {
252                QVariantMap trackResult = result.toMap();
253
254                QString title, artist, album;
255
256                title = trackResult.value( "title", QString() ).toString();
257                artist = trackResult.value( "artist", QString() ).toString();
258                album = trackResult.value( "album", QString() ).toString();
259
260                if ( title.isEmpty() && artist.isEmpty() ) // don't have enough...
261                {
262                    tLog() << "Didn't get an artist and track name from spotify, not enough to build a query on. Aborting" << title << artist << album;
263                    return;
264                }
265
266                Tomahawk::query_ptr q = Tomahawk::Query::get( artist, title, album, uuid(), m_trackMode );
267                if ( q.isNull() )
268                    continue;
269
270                tLog() << "Setting resulthint to " << trackResult.value( "trackuri" );
271                q->setResultHint( trackResult.value( "trackuri" ).toString() );
272                q->setProperty( "annotation", trackResult.value( "trackuri" ).toString() );
273
274                m_tracks << q;
275            }
276        }
277    }
278    else
279    {
280        JobStatusView::instance()->model()->addJob( new ErrorStatusMessage( tr( "Error fetching Spotify information from the network!" ) ) );
281        tLog() << "Error in network request to Spotify for track decoding:" << r->reply()->errorString();
282    }
283
284    if ( m_trackMode )
285        checkTrackFinished();
286    else
287        checkBrowseFinished();
288}
289
290
291void
292SpotifyParser::spotifyTrackLookupFinished()
293{
294    NetworkReply* r = qobject_cast< NetworkReply* >( sender() );
295    Q_ASSERT( r );
296    r->deleteLater();
297    m_queries.remove( r );
298
299    if ( r->reply()->error() == QNetworkReply::NoError )
300    {
301        bool ok;
302        QByteArray jsonData = r->reply()->readAll();
303        QVariantMap res = TomahawkUtils::parseJson( jsonData, &ok ).toMap();
304
305        if ( !ok )
306        {
307            tLog() << "Failed to parse json from Spotify track lookup:" << jsonData;
308            checkTrackFinished();
309            return;
310        }
311        else if ( !res.contains( "track" ) )
312        {
313            tLog() << "No 'track' item in the spotify track lookup result... not doing anything";
314            checkTrackFinished();
315            return;
316        }
317
318        // lets parse this baby
319        QVariantMap t = res.value( "track" ).toMap();
320        QString title, artist, album;
321
322        title = t.value( "name", QString() ).toString();
323        // TODO for now only take the first artist
324        if ( t.contains( "artists" ) && t[ "artists" ].canConvert< QVariantList >() && t[ "artists" ].toList().size() > 0 )
325            artist = t[ "artists" ].toList().first().toMap().value( "name", QString() ).toString();
326        if ( t.contains( "album" ) && t[ "album" ].canConvert< QVariantMap >() )
327            album = t[ "album" ].toMap().value( "name", QString() ).toString();
328
329        if ( title.isEmpty() && artist.isEmpty() ) // don't have enough...
330        {
331            tLog() << "Didn't get an artist and track name from spotify, not enough to build a query on. Aborting" << title << artist << album;
332            return;
333        }
334
335        Tomahawk::query_ptr q = Tomahawk::Query::get( artist, title, album, uuid(), m_trackMode );
336        if ( !q.isNull() )
337        {
338            q->setResultHint( t.value( "trackuri" ).toString() );
339
340            m_tracks << q;
341        }
342    }
343    else
344    {
345        tLog() << "Error in network request to Spotify for track decoding:" << r->reply()->errorString();
346    }
347
348    if ( m_trackMode )
349        checkTrackFinished();
350    else
351        checkBrowseFinished();
352}
353
354
355void
356SpotifyParser::playlistListingResult( const QString& msgType, const QVariantMap& msg, const QVariant& extraData )
357{
358    Q_UNUSED( extraData );
359
360    Q_ASSERT( msgType == "playlistListing" );
361
362    m_title = msg.value( "name" ).toString();
363    m_single = false;
364    m_creator = msg.value( "creator" ).toString();
365    m_collaborative = msg.value( "collaborative" ).toBool();
366    m_subscribers = msg.value( "subscribers" ).toInt();
367
368    const QVariantList tracks = msg.value( "tracks" ).toList();
369    foreach ( const QVariant& blob, tracks )
370    {
371        QVariantMap trackMap = blob.toMap();
372        const query_ptr q = Query::get( trackMap.value( "artist" ).toString(), trackMap.value( "track" ).toString(), trackMap.value( "album" ).toString(), uuid(), false );
373
374        if ( q.isNull() )
375            continue;
376
377        const QString id = trackMap.value( "id" ).toString();
378        if( !id.isEmpty() )
379        {
380            q->setResultHint( id );
381            q->setProperty( "annotation", id );
382        }
383
384        m_tracks << q;
385    }
386
387    checkBrowseFinished();
388}
389
390
391void
392SpotifyParser::checkBrowseFinished()
393{
394    tDebug() << "Checking for spotify batch playlist job finished" << m_queries.isEmpty() << m_createNewPlaylist;
395    if ( m_queries.isEmpty() ) // we're done
396    {
397        if ( m_browseJob )
398            m_browseJob->setFinished();
399
400        if ( m_createNewPlaylist && !m_tracks.isEmpty() )
401        {
402            QString spotifyUsername;
403            bool spotifyAccountLoggedIn = Accounts::SpotifyAccount::instance() && Accounts::SpotifyAccount::instance()->loggedIn();
404
405            if ( spotifyAccountLoggedIn )
406            {
407                QVariantMap creds = Accounts::SpotifyAccount::instance()->credentials();
408                spotifyUsername = creds.value( "username" ).toString();
409            }
410
411/*            if ( spotifyAccountLoggedIn &&  Accounts::SpotifyAccount::instance()->hasPlaylist( m_browseUri ) )
412            {
413                // The playlist is already registered with Tomahawk, so just open it instead of adding another instance.
414                m_playlist = Accounts::SpotifyAccount::instance()->playlistForURI( m_browseUri );
415                playlistCreated();
416            }
417            else*/
418            {
419                m_playlist = Playlist::create( SourceList::instance()->getLocal(),
420                                       uuid(),
421                                       m_title,
422                                       m_info,
423                                       spotifyUsername == m_creator ? QString() : m_creator,
424                                       false,
425                                       m_tracks );
426
427                connect( m_playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( playlistCreated() ) );
428
429/*                if ( spotifyAccountLoggedIn )
430                {
431                    SpotifyPlaylistUpdater* updater = new SpotifyPlaylistUpdater(
432                                                        Accounts::SpotifyAccount::instance(), m_playlist->currentrevision(), m_browseUri, m_playlist );
433
434
435                    // If the user isnt dropping a playlist the he owns, its subscribeable
436                    if ( !m_browseUri.contains( spotifyUsername ) )
437                        updater->setCanSubscribe( true );
438                    else
439                        updater->setOwner( true );
440
441                    updater->setCollaborative( m_collaborative );
442                    updater->setSubscribers( m_subscribers );
443                    // Just register the infos
444                    Accounts::SpotifyAccount::instance()->registerPlaylistInfo( m_title, m_browseUri, m_browseUri, false, false, updater->owner() );
445                    Accounts::SpotifyAccount::instance()->registerUpdaterForPlaylist( m_browseUri, updater );
446                    // On default, set the playlist as subscribed
447                    if( !updater->owner() )
448                        Accounts::SpotifyAccount::instance()->setSubscribedForPlaylist( m_playlist, true );
449                }*/
450            }
451            return;
452        }
453        else if ( m_single && !m_tracks.isEmpty() )
454            emit track( m_tracks.first() );
455        else if ( !m_single && !m_tracks.isEmpty() )
456            emit tracks( m_tracks );
457
458        deleteLater();
459    }
460}
461
462
463void
464SpotifyParser::checkTrackFinished()
465{
466    tDebug() << "Checking for spotify batch track job finished" << m_queries.isEmpty();
467    if ( m_queries.isEmpty() ) // we're done
468    {
469        if ( m_browseJob )
470            m_browseJob->setFinished();
471
472        if ( m_single && !m_tracks.isEmpty() )
473            emit track( m_tracks.first() );
474        else if ( !m_single && !m_tracks.isEmpty() )
475            emit tracks( m_tracks );
476
477        deleteLater();
478    }
479}
480
481
482void
483SpotifyParser::playlistCreated()
484{
485    ViewManager::instance()->show( m_playlist );
486
487    deleteLater();
488}
489
490
491QPixmap
492SpotifyParser::pixmap() const
493{
494    if ( !s_pixmap )
495        s_pixmap = new QPixmap( RESPATH "images/spotify-logo.png" );
496
497    return *s_pixmap;
498}