From f5db2856022075511e115952b830e11c508a3cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Lindstr=C3=B6m?= Date: Wed, 12 Sep 2012 14:38:28 +0200 Subject: [PATCH] Ex.fm parser --- src/libtomahawk/CMakeLists.txt | 1 + src/libtomahawk/DropJob.cpp | 42 +++- src/libtomahawk/DropJob.h | 1 + src/libtomahawk/utils/ExfmParser.cpp | 309 +++++++++++++++++++++++++++ src/libtomahawk/utils/ExfmParser.h | 89 ++++++++ 5 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 src/libtomahawk/utils/ExfmParser.cpp create mode 100644 src/libtomahawk/utils/ExfmParser.h diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt index 6ff801ce8..9bcc0a141 100644 --- a/src/libtomahawk/CMakeLists.txt +++ b/src/libtomahawk/CMakeLists.txt @@ -103,6 +103,7 @@ set( libGuiSources utils/RdioParser.cpp utils/ShortenedLinkParser.cpp utils/SoundcloudParser.cpp + utils/ExfmParser.cpp utils/StyleHelper.cpp utils/DropJobNotifier.cpp utils/ProxyStyle.cpp diff --git a/src/libtomahawk/DropJob.cpp b/src/libtomahawk/DropJob.cpp index f5de413b2..1fb27f5d7 100644 --- a/src/libtomahawk/DropJob.cpp +++ b/src/libtomahawk/DropJob.cpp @@ -31,6 +31,7 @@ #include "utils/M3uLoader.h" #include "utils/ShortenedLinkParser.h" #include "utils/SoundcloudParser.h" +#include "utils/ExfmParser.h" #include "utils/Logger.h" #include "utils/TomahawkUtils.h" #include "GlobalActionManager.h" @@ -132,6 +133,9 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType if( url.contains( "soundcloud" ) && url.contains( "sets" ) ) return true; + if( url.contains( "ex.fm" ) && !url.contains( "/song/" ) ) // We treat everything but song as playlist + return true; + if ( url.contains( "grooveshark.com" ) && url.contains( "playlist" ) ) return true; } @@ -150,6 +154,8 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType if ( url.contains( "spotify" ) && url.contains( "track" ) ) return true; + if ( url.contains( "ex.fm" ) && url.contains( "/song/" ) ) + return true; if( url.contains( "soundcloud" ) ) return true; @@ -167,6 +173,8 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType return true; if ( url.contains( "rdio.com" ) && ( url.contains( "artist" ) && url.contains( "album" ) && !url.contains( "track" ) ) ) return true; + if ( url.contains( "ex.fm" ) && url.contains( "site" ) && url.contains( "album" ) ) + return true; } if ( acceptedType.testFlag( Artist ) ) @@ -209,6 +217,9 @@ DropJob::isDropType( DropJob::DropType desired, const QMimeData* data ) if( url.contains( "soundcloud" ) && url.contains( "sets" ) ) return true; + if( url.contains( "ex.fm" ) && !url.contains( "/song/" ) ) // We treat all but song as playlist + return true; + if ( url.contains( "rdio.com" ) && url.contains( "people" ) && url.contains( "playlist" ) ) return true; #ifdef QCA2_FOUND @@ -597,6 +608,24 @@ DropJob::handleSoundcloudUrls( const QString& urlsRaw ) } + +void +DropJob::handleExfmUrls( const QString& urlsRaw ) +{ + QStringList urls = urlsRaw.split( QRegExp( "\\s+" ), QString::SkipEmptyParts ); + qDebug() << "Got Ex.fm urls!" << urls; + + + if ( dropAction() == Default ) + setDropAction( Create ); + + ExfmParser* exfm = new ExfmParser( urls, dropAction() == Create, this ); + connect( exfm, SIGNAL( tracks( QList ) ), this, SLOT( onTracksAdded( QList< Tomahawk::query_ptr > ) ) ); + + m_queryCount++; + +} + void DropJob::handleGroovesharkUrls ( const QString& urlsRaw ) { @@ -637,6 +666,8 @@ DropJob::handleAllUrls( const QString& urls ) handleRdioUrls( urls ); else if( urls.contains( "soundcloud" ) ) handleSoundcloudUrls( urls ); + else if( urls.contains( "ex.fm" ) ) + handleExfmUrls( urls ); #ifdef QCA2_FOUND else if ( urls.contains( "grooveshark.com" ) ) handleGroovesharkUrls( urls ); @@ -667,7 +698,7 @@ DropJob::handleTrackUrls( const QString& urls ) connect( spot, SIGNAL( tracks( QList ) ), this, SLOT( onTracksAdded( QList< Tomahawk::query_ptr > ) ) ); m_queryCount++; } - else if ( urls.contains( "soundcloud") ) + else if ( urls.contains( "soundcloud" ) ) { QStringList tracks = urls.split( QRegExp( "\\s+" ), QString::SkipEmptyParts ); @@ -676,6 +707,15 @@ DropJob::handleTrackUrls( const QString& urls ) connect( sc, SIGNAL( tracks( QList ) ), this, SLOT( onTracksAdded( QList< Tomahawk::query_ptr > ) ) ); m_queryCount++; } + else if ( urls.contains( "ex.fm" ) ) + { + QStringList tracks = urls.split( QRegExp( "\\s+" ), QString::SkipEmptyParts ); + + tDebug() << "Got a list of Exfm tracks!" << tracks; + ExfmParser* exfm = new ExfmParser( tracks, false, this ); + connect( exfm, SIGNAL( tracks( QList ) ), this, SLOT( onTracksAdded( QList< Tomahawk::query_ptr > ) ) ); + m_queryCount++; + } else if ( urls.contains( "rdio.com" ) ) { QStringList tracks = urls.split( QRegExp( "\\s+" ), QString::SkipEmptyParts ); diff --git a/src/libtomahawk/DropJob.h b/src/libtomahawk/DropJob.h index 7810fa2c9..6dd839b7c 100644 --- a/src/libtomahawk/DropJob.h +++ b/src/libtomahawk/DropJob.h @@ -105,6 +105,7 @@ public: void handleM3u( const QString& urls ); void handleSpotifyUrls( const QString& urls ); void handleRdioUrls( const QString& urls ); + void handleExfmUrls( const QString& urls ); void handleSoundcloudUrls( const QString& urls ); void handleGroovesharkUrls( const QString& urls ); diff --git a/src/libtomahawk/utils/ExfmParser.cpp b/src/libtomahawk/utils/ExfmParser.cpp new file mode 100644 index 000000000..7e6f05a9e --- /dev/null +++ b/src/libtomahawk/utils/ExfmParser.cpp @@ -0,0 +1,309 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Hugo Lindström + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +#include "ExfmParser.h" + +#include "utils/Logger.h" +#include "utils/TomahawkUtils.h" +#include "Query.h" +#include "SourceList.h" +#include "jobview/JobStatusView.h" +#include "jobview/JobStatusModel.h" +#include "jobview/ErrorStatusMessage.h" +#include "DropJobNotifier.h" +#include "ViewManager.h" + +#include + +#include +#include + +using namespace Tomahawk; + +QPixmap* ExfmParser::s_pixmap = 0; + + +ExfmParser::ExfmParser( const QStringList& Urls, bool createNewPlaylist, QObject* parent ) + : QObject ( parent ) + , m_single( false ) + , m_trackMode( true ) + , m_createNewPlaylist( createNewPlaylist ) + , m_browseJob( 0 ) + , m_type( DropJob::All ) + +{ + foreach ( const QString& url, Urls ) + lookupUrl( url ); +} + + +ExfmParser::ExfmParser( const QString& Url, bool createNewPlaylist, QObject* parent ) + : QObject ( parent ) + , m_single( true ) + , m_trackMode( true ) + , m_createNewPlaylist( createNewPlaylist ) + , m_browseJob( 0 ) + , m_type( DropJob::All ) +{ + lookupUrl( Url ); +} + + +ExfmParser::~ExfmParser() +{ +} + + +void +ExfmParser::lookupUrl( const QString& link ) +{ + + const QString apiBase = "http://ex.fm/api/v3"; + QString url(link); + QStringList paths; + foreach( const QString& path, QUrl( link ).path().split( "/" ) ) + { + if( !path.isEmpty() ) + paths << path; + } + + // User request + if( paths.count() == 1 ) + { + m_type = DropJob::Artist; + url = QString( apiBase + "/user/%1/trending" ).arg( paths.takeFirst() ); + } + else + { + if ( url.contains( "/explore/site-of-the-day" ) ) // We treat site as artist + { + m_type = DropJob::Artist; + url.replace( "/explore/site-of-the-day", "/sotd?results=1" ); + } + else if ( url.contains( "/song/" ) ) + { + m_type = DropJob::Track; + } + else if( ( url.contains( "site") && url.contains( "album" ) ) || url.contains( "mixtape-of-the-month" ) ) + { + m_type = DropJob::Album; + if( url.contains( "album-of-the-week") ) + url = QString( apiBase + "/%1?%2" ).arg( "aotw" ).arg( "results=1" ); // Only need the first album, eg. this week + if( url.contains( "mixtape-of-the-month" ) ) + url = QString( apiBase + "/%1?%2" ).arg( "motm" ).arg( "results=1" ); // Only need the first mixtape, eg. this week + } + else + { + m_type = DropJob::Playlist; + if( url.contains( "tastemakers" ) ) + url.replace( "trending", "explore" ); + } + if( m_type == DropJob::Playlist ) + { + url.replace( "/genre/", "/tag/" ); + url.replace( "/search/", "/song/search/" ); // We can only search for tracks, even though we want an artist or whatever + + } + + url.replace( "http://ex.fm", apiBase ); + } + + + tDebug() << "Looking up URL..." << url; + QNetworkReply* reply = TomahawkUtils::nam()->get( QNetworkRequest( QUrl( url ) ) ); + + if( m_createNewPlaylist ) + connect( reply, SIGNAL( finished() ), this, SLOT( exfmLookupFinished() ) ); + else + connect( reply, SIGNAL( finished() ), this, SLOT( exfmBrowseFinished() ) ); + + m_browseJob = new DropJobNotifier( pixmap(), "Exfm", m_type, reply ); + JobStatusView::instance()->model()->addJob( m_browseJob ); + + m_queries.insert( reply ); + + + +} + +void +ExfmParser::parseTrack( const QVariantMap& res ) +{ + + QString title, artist, album; + album = res.value( "album", QString() ).toString(); + title = res.value( "title", QString() ).toString(); + artist = res.value( "artist", QString() ).toString(); + + if ( title.isEmpty() && artist.isEmpty() ) // don't have enough... + { + tLog() << "Didn't get an artist and track name from Exfm, not enough to build a query on. Aborting" << title << artist; + return; + } + + Tomahawk::query_ptr q = Tomahawk::Query::get( artist, title, album, uuid(), m_trackMode ); + + if ( !q.isNull() ) + { + tLog() << "Setting resulthint to " << res.value( "url" ); + q->setResultHint( res.value( "url" ).toString() ); + q->setProperty( "annotation", res.value( "url" ).toString() ); + m_tracks << q; + } + +} + +void +ExfmParser::exfmLookupFinished() +{ + + QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() ); + Q_ASSERT( r ); + m_queries.remove( r ); + r->deleteLater(); + + if ( r->error() == QNetworkReply::NoError ) + { + QJson::Parser p; + bool ok; + QVariantMap res = p.parse( r, &ok ).toMap(); + + if ( !ok ) + { + tLog() << "Failed to parse json from Exfm browse item :" << p.errorString() << "On line" << p.errorLine(); + return; + } + + QStringList paths; + foreach( const QString& path, r->url().path().split( "/" ) ) + if( !path.isEmpty() ) + paths << path; + + QString title, artist, desc; + QVariantList tracks; + + if( m_type == DropJob::Album ) + { + QStringList meta = res.value( "site" ).toMap().value( "title" ).toString().split( ", by " ); + title = meta.takeFirst(); + artist = meta.takeLast(); + tracks = res.value( "site" ).toMap().value( "songs" ).toList(); + } + else + { + // Take endpoint as title + title = paths.takeLast(); + title[0] = title[0].toUpper(); + + if( paths.contains( "trending") ) + title = "Trending " + title; + else if( paths.contains( "explore" ) ) + title = "Explore " + title; + + if( paths.contains( "user" ) ) + { + int index = paths.indexOf( "user"); + if( index != -1 && paths.size() > index+1 ) + artist = paths.takeAt(paths.indexOf( "user") +1 ); + }else + artist = "Ex.fm"; + + tracks = res.value( "songs" ).toList(); + } + + // Make the title + title = title + " by " + artist; + + foreach( const QVariant& track, tracks ) + parseTrack( track.toMap() ); + + m_playlist = Playlist::create( SourceList::instance()->getLocal(), + uuid(), + title, + desc, + artist, + false, + m_tracks ); + + connect( m_playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( playlistCreated() ) ); + return; + } + +} + +void +ExfmParser::playlistCreated() +{ + ViewManager::instance()->show( m_playlist ); + deleteLater(); +} + +void +ExfmParser::exfmBrowseFinished() +{ + QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() ); + Q_ASSERT( r ); + + r->deleteLater(); + + if ( r->error() == QNetworkReply::NoError ) + { + + QJson::Parser p; + bool ok; + QVariantMap res = p.parse( r, &ok ).toMap(); + + if( !ok ) + { + qDebug() << "Failed to parse ex.fm json"; + return; + } + + if( m_type != DropJob::Track ) + { + QVariantList tracks; + if( m_type == DropJob::Album ) + tracks = res.value( "site" ).toMap().value( "songs" ).toList(); + else + tracks = res.value( "songs" ).toList(); + + foreach( const QVariant& track, tracks ) + parseTrack( track.toMap() ); + } + else + parseTrack( res.value( "song" ).toMap() ); + + qDebug() << "Got tracks:" << m_tracks; + if ( m_single && !m_tracks.isEmpty() ) + emit track( m_tracks.first() ); + else if ( !m_single && !m_tracks.isEmpty() ) + emit tracks( m_tracks ); + + deleteLater(); + } + +} + +QPixmap +ExfmParser::pixmap() const +{ + if ( !s_pixmap ) + s_pixmap = new QPixmap( RESPATH "images/exfm.png" ); + + return *s_pixmap; +} diff --git a/src/libtomahawk/utils/ExfmParser.h b/src/libtomahawk/utils/ExfmParser.h new file mode 100644 index 000000000..83a07485a --- /dev/null +++ b/src/libtomahawk/utils/ExfmParser.h @@ -0,0 +1,89 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2010-2011, Leo Franchi + * Copyright 2010-2011, Hugo Lindström + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +#ifndef Exfm_PARSER_H +#define Exfm_PARSER_H + +#include "DllMacro.h" +#include "Typedefs.h" +#include "Query.h" +#include "DropJob.h" +#include "jobview/JobStatusItem.h" +#include +#include + +/** + * Small class to parse Exfm links into query_ptrs + * + * Connect to the signals to get the results + */ + +class QNetworkReply; + +namespace Tomahawk +{ + +class DropJobNotifier; + +class DLLEXPORT ExfmParser : public QObject +{ + Q_OBJECT +public: + friend class ExfmJobNotifier; + explicit ExfmParser( const QString& trackUrl, bool createNewPlaylist = false, QObject* parent = 0 ); + explicit ExfmParser( const QStringList& trackUrls, bool createNewPlaylist = false, QObject* parent = 0 ); + virtual ~ExfmParser(); + + // if true, emits track(), if false, emits tracks(). + // only matters if you're using the QString constructor and explicityl dont' want + // the single track signal + void setSingleMode( bool single ) { m_single = single; } + +signals: + void track( const Tomahawk::query_ptr& track ); + void tracks( const QList< Tomahawk::query_ptr > tracks ); + void playlist( const Tomahawk::query_ptr& playlist ); + +private slots: + void exfmBrowseFinished(); + void exfmLookupFinished(); + void playlistCreated(); + +private: + QPixmap pixmap() const; + void lookupUrl( const QString& url ); + void checkBrowseFinished(); + void parseTrack( const QVariantMap& res ); + + bool m_single; + bool m_trackMode; + bool m_createNewPlaylist; + + int m_subscribers; + QList< query_ptr > m_tracks; + QSet< QNetworkReply* > m_queries; + Tomahawk::playlist_ptr m_playlist; + DropJobNotifier* m_browseJob; + DropJob::DropType m_type; + static QPixmap* s_pixmap; +}; + +} + +#endif