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