diff --git a/admin/mac/macdeploy.py b/admin/mac/macdeploy.py index 71bdefb77..6d7987982 100755 --- a/admin/mac/macdeploy.py +++ b/admin/mac/macdeploy.py @@ -199,10 +199,10 @@ QT_PLUGINS = [ ] TOMAHAWK_PLUGINS = [ - 'libtomahawk_account_xmpp.dylib', - 'libtomahawk_account_google.dylib', - 'libtomahawk_account_twitter.dylib', - 'libtomahawk_account_zeroconf.dylib', + 'libtomahawk_account_xmpp.so', + 'libtomahawk_account_google.so', + 'libtomahawk_account_twitter.so', + 'libtomahawk_account_zeroconf.so', ] QT_PLUGINS_SEARCH_PATH=[ @@ -495,11 +495,6 @@ for plugin in VLC_PLUGINS: for plugin in TOMAHAWK_PLUGINS: FixPlugin(plugin, '../MacOS') -try: - FixPlugin('spotify_tomahawkresolver', '../MacOS') -except: - print 'Failed to find spotify resolver' - try: FixPlugin('tomahawk_crash_reporter', '../MacOS') except: diff --git a/data/misc/tomahawk_pubkey.pem b/data/misc/tomahawk_pubkey.pem new file mode 100644 index 000000000..5e9606170 --- /dev/null +++ b/data/misc/tomahawk_pubkey.pem @@ -0,0 +1,20 @@ +-----BEGIN PUBLIC KEY----- +MIIDOzCCAi0GByqGSM44BAEwggIgAoIBAQDRltnNbKWFroVCsG1nTSdlTDmo7fjl +tgOuQ0YB2s0a1bcqgQ5YJRE59pFvF/z2pkHEHdyBA6USd9N7/T9lolwNcJoByJpO +MobUNs04elqZXliriaAdoSb2g6ZpxiedppbbyNP/BlK6o+zpyn0LVYXDI/OwJFzS +xjGXM+rBEWdUJnogZxV31gF9W3yD1Quz6icBulT9V/Soo6me9Mc60ooKSYj4Zgqd +3ln8tG90RFnWfbb0nbrITvR3ll6XXLfn081tjhymcXqHcgvaaqcmpKWL6ZWwX1mH +3t1pImnif/tSSZPG21KGE3FtuQ/+YFo19apQ6U6l8kaSFxqcDLAYzBy9AhUA/QfN +8WEIvzOEZ9uSWT7lYy64mUkCggEABsUmcs3kwjrmszIAAmPIowA0DBrxWZL03JBV +bDKT6tNHZaFFlCufVSjiL1EFZjRARC16OWYaDcElUsZYFMcsNIIa8LyDQaq6+SSm +quhMO5heeJiYPrutDiJzbJr0+HoY77Ll+Q4/cEkl0UAN4Ovp18WKwaq6GpHAvBnv +71LunLGAKsVb5joXBQ8In6zQkibJhgiBJwzLK90/j0OTiDaaOwM3PsAegORBVlVE +TAk4AQmawmF8nBGLzTyKXl83J571ku1Mm2JTl16jMYziKARKXYBmkcP1at0YddVK +WWpAwRKSxOucVJYfV58JqmjZqst8BBeH6esQKr5dklUvvDMaEwOCAQYAAoIBAQCw +5mo+8/R3S9cNYg9o8JNJGdSbMhSkurILHh9WNElsIC3RNtPcpijmAnWtXTVDhe6w +77wLj37tUuFGbsu2qPXtZoup35emf9DDshZ5w5UOclPaZ9HYjlC1H64c6d66Rllk +fY6FRDv9qVfjT84APbvMDrk6csJ5YHxFPDaqeQaFB0nxFiCMVwjEx+ZSvQNK1jJ2 +o2gtuOvSPVSphsMeJ72DDNxO+SRRVnOmWaxg9rlmFuGle6Z+UJ2FItfmPEvhSBMY +hzndUbC7Wi4sIpBzbm9O5MiPYMv0VmN+0t1156EiC9uR4f7AKH2S94dnQob/YeY0 +jMH+XxU/wzGUCmsOx1lx +-----END PUBLIC KEY----- diff --git a/resources.qrc b/resources.qrc index d275428d3..bea3686be 100644 --- a/resources.qrc +++ b/resources.qrc @@ -141,5 +141,6 @@ data/images/process-stop.png data/icons/tomahawk-icon-128x128-grayscale.png data/images/collection.png + data/misc/tomahawk_pubkey.pem diff --git a/src/AccountDelegate.cpp b/src/AccountDelegate.cpp index 95e5ce418..6f6185708 100644 --- a/src/AccountDelegate.cpp +++ b/src/AccountDelegate.cpp @@ -734,6 +734,15 @@ AccountDelegate::doneInstalling ( const QPersistentModelIndex& idx ) } +void +AccountDelegate::errorInstalling( const QPersistentModelIndex& idx ) +{ + // Just hide the loading spinner as we do after a successful install + qDebug() << "ERROR INSTALLING index:" << idx; + doneInstalling( idx ); +} + + void AccountDelegate::doUpdateIndex( const QPersistentModelIndex& idx ) { diff --git a/src/AccountDelegate.h b/src/AccountDelegate.h index 2028eb09b..82e59f4d2 100644 --- a/src/AccountDelegate.h +++ b/src/AccountDelegate.h @@ -43,6 +43,8 @@ public: public slots: void startInstalling( const QPersistentModelIndex& idx ); void doneInstalling ( const QPersistentModelIndex& idx ); + void errorInstalling ( const QPersistentModelIndex& idx ); + void doUpdateIndex( const QPersistentModelIndex& idx ); diff --git a/src/SettingsDialog.cpp b/src/SettingsDialog.cpp index 3999121de..c9cc2546c 100644 --- a/src/SettingsDialog.cpp +++ b/src/SettingsDialog.cpp @@ -52,6 +52,7 @@ #include #include "utils/Logger.h" #include "AccountFactoryWrapper.h" +#include "accounts/spotify/SpotifyAccount.h" #include "ui_ProxyDialog.h" #include "ui_StackedSettingsDialog.h" @@ -97,7 +98,7 @@ SettingsDialog::SettingsDialog( QWidget *parent ) ui->enableProxyCheckBox->setChecked( useProxy ); ui->proxyButton->setEnabled( useProxy ); - + createIcons(); #ifdef Q_WS_X11 ui->listWidget->setFrameShape( QFrame::StyledPanel ); @@ -116,6 +117,13 @@ SettingsDialog::SettingsDialog( QWidget *parent ) m_proxySettings.setSizeGripEnabled( true ); QSizeGrip* p = m_proxySettings.findChild< QSizeGrip* >(); p->setFixedSize( 0, 0 ); + + ui->groupBoxNetworkAdvanced->layout()->removeItem( ui->verticalSpacer ); + ui->groupBoxNetworkAdvanced->layout()->removeItem( ui->verticalSpacer_2 ); + ui->groupBoxNetworkAdvanced->layout()->removeItem( ui->verticalSpacer_4 ); + delete ui->verticalSpacer; + delete ui->verticalSpacer_2; + delete ui->verticalSpacer_4; #endif // Accounts @@ -135,6 +143,7 @@ SettingsDialog::SettingsDialog( QWidget *parent ) connect( m_accountProxy, SIGNAL( startInstalling( QPersistentModelIndex ) ), accountDelegate, SLOT( startInstalling(QPersistentModelIndex) ) ); connect( m_accountProxy, SIGNAL( doneInstalling( QPersistentModelIndex ) ), accountDelegate, SLOT( doneInstalling(QPersistentModelIndex) ) ); + connect( m_accountProxy, SIGNAL( errorInstalling( QPersistentModelIndex ) ), accountDelegate, SLOT( errorInstalling(QPersistentModelIndex) ) ); connect( m_accountProxy, SIGNAL( scrollTo( QModelIndex ) ), this, SLOT( scrollTo( QModelIndex ) ) ); ui->accountsView->setModel( m_accountProxy ); @@ -255,7 +264,7 @@ SettingsDialog::~SettingsDialog() } else qDebug() << "Settings dialog cancelled, NOT saving prefs."; - + delete ui; } @@ -450,15 +459,40 @@ SettingsDialog::installFromFile() if( !resolver.isEmpty() ) { + const QFileInfo resolverAbsoluteFilePath( resolver ); + TomahawkSettings::instance()->setScriptDefaultPath( resolverAbsoluteFilePath.absolutePath() ); + + if ( resolverAbsoluteFilePath.baseName() == "spotify_tomahawkresolver" ) + { + // HACK if this is a spotify resolver, we treat is specially. + // usually we expect the user to just download the spotify resolver from attica, + // however developers, those who build their own tomahawk, can't do that, or linux + // users can't do that. However, we have an already-existing SpotifyAccount that we + // know exists that we need to use this resolver path. + // + // Hence, we special-case the spotify resolver and directly set the path on it here. + SpotifyAccount* acct = 0; + foreach ( Account* account, AccountManager::instance()->accounts() ) + { + if ( SpotifyAccount* spotify = qobject_cast< SpotifyAccount* >( account ) ) + { + acct = spotify; + break; + } + } + + if ( acct ) + { + acct->setManualResolverPath( resolver ); + return; + } + } + Account* acct = AccountManager::instance()->accountFromPath( resolver ); AccountManager::instance()->addAccount( acct ); TomahawkSettings::instance()->addAccount( acct->accountId() ); AccountManager::instance()->enableAccount( acct ); - - - QFileInfo resolverAbsoluteFilePath( resolver ); - TomahawkSettings::instance()->setScriptDefaultPath( resolverAbsoluteFilePath.absolutePath() ); } } diff --git a/src/TomahawkApp.cpp b/src/TomahawkApp.cpp index 53b09bda9..5938e60c5 100644 --- a/src/TomahawkApp.cpp +++ b/src/TomahawkApp.cpp @@ -290,6 +290,16 @@ TomahawkApp::init() PlaylistUpdaterInterface::registerUpdaterFactory( new XspfUpdaterFactory ); PlaylistUpdaterInterface::registerUpdaterFactory( new SpotifyUpdaterFactory ); + // Following work-around/fix taken from Clementine rev. 13e13ccd9a95 and courtesy of David Sansome + // A bug in Qt means the wheel_scroll_lines setting gets ignored and replaced + // with the default value of 3 in QApplicationPrivate::initialize. + { + QSettings qt_settings(QSettings::UserScope, "Trolltech"); + qt_settings.beginGroup("Qt"); + QApplication::setWheelScrollLines( + qt_settings.value("wheelScrollLines", QApplication::wheelScrollLines()).toInt()); + } + #ifndef ENABLE_HEADLESS // Make sure to init GAM in the gui thread GlobalActionManager::instance(); diff --git a/src/accounts/spotify/SpotifyAccount.cpp b/src/accounts/spotify/SpotifyAccount.cpp index a15047d44..3a740f551 100644 --- a/src/accounts/spotify/SpotifyAccount.cpp +++ b/src/accounts/spotify/SpotifyAccount.cpp @@ -26,6 +26,9 @@ #include "resolvers/ScriptResolver.h" #include "utils/TomahawkUtils.h" #include "ActionCollection.h" +#include "Pipeline.h" +#include "accounts/AccountManager.h" +#include "utils/Closure.h" #ifndef ENABLE_HEADLESS #include "jobview/JobStatusView.h" @@ -37,6 +40,7 @@ #include #include #include +#include using namespace Tomahawk; using namespace Accounts; @@ -44,6 +48,14 @@ using namespace Accounts; static QPixmap* s_icon = 0; +#ifdef Q_OS_MAC +static QString s_resolverId = "spotify-osx"; +#elif defined(Q_OS_WIN) +static QString s_resolverId = "spotify-win"; +#else +static QString s_resolverId = "spotify-linux"; +#endif + Account* SpotifyAccountFactory::createAccount( const QString& accountId ) { @@ -51,21 +63,6 @@ SpotifyAccountFactory::createAccount( const QString& accountId ) } -bool -SpotifyAccountFactory::acceptsPath( const QString& path ) const -{ - QFileInfo info( path ); - return info.baseName().startsWith( "spotify_" ); -} - - -Account* -SpotifyAccountFactory::createFromPath( const QString& path ) -{ - return new SpotifyAccount( generateId( factoryId() ), path ); -} - - QPixmap SpotifyAccountFactory::icon() const { @@ -77,14 +74,8 @@ SpotifyAccountFactory::icon() const SpotifyAccount::SpotifyAccount( const QString& accountId ) - : ResolverAccount( accountId ) -{ - init(); -} - - -SpotifyAccount::SpotifyAccount( const QString& accountId, const QString& path ) - : ResolverAccount( accountId, path ) + : CustomAtticaAccount( accountId ) + , m_preventEnabling( false ) { init(); } @@ -99,10 +90,60 @@ SpotifyAccount::~SpotifyAccount() void SpotifyAccount::init() { + setAccountFriendlyName( "Spotify" ); + setAccountServiceName( "spotify" ); + + if ( !AtticaManager::instance()->resolversLoaded() ) + { + // If we're still waiting to load, wait for the attica resolvers to come down the pipe + connect( AtticaManager::instance(), SIGNAL( resolversLoaded( Attica::Content::List ) ), this, SLOT( init() ), Qt::UniqueConnection ); + return; + } + qRegisterMetaType< Tomahawk::Accounts::SpotifyPlaylistInfo* >( "Tomahawk::Accounts::SpotifyPlaylist*" ); - m_spotifyResolver = dynamic_cast< ScriptResolver* >( m_resolver.data() ); + AtticaManager::instance()->registerCustomAccount( s_resolverId, this ); + connect( AtticaManager::instance(), SIGNAL( resolverInstalled( QString ) ), this, SLOT( resolverInstalled( QString ) ) ); + + const Attica::Content res = AtticaManager::instance()->resolverForId( s_resolverId ); + const AtticaManager::ResolverState state = AtticaManager::instance()->resolverState( res ); + + const QString path = configuration().value( "path" ).toString(); // Manual path override + if ( !checkForResolver() && state != AtticaManager::Uninstalled ) + { + // If the user manually deleted the resolver, mark it as uninstalled, so we re-fetch for the user + AtticaManager::instance()->uninstallResolver( res ); + } + else if ( state == AtticaManager::Installed || !path.isEmpty() ) + { + hookupResolver(); + } +} + + +void +SpotifyAccount::hookupResolver() +{ + // initialize the resolver itself. this is called if the account actually has an installed spotify resolver, + // as it might not. + // If there is a spotify resolver from attica installed, create the corresponding ExternalResolver* and hook up to it + QString path = configuration().value( "path" ).toString(); + if ( path.isEmpty() ) + { + const Attica::Content res = AtticaManager::instance()->resolverForId( s_resolverId ); + const AtticaManager::ResolverState state = AtticaManager::instance()->resolverState( res ); + Q_ASSERT( state == AtticaManager::Installed ); + Q_UNUSED( state ); + + const AtticaManager::Resolver data = AtticaManager::instance()->resolverData( res.id() ); + path = data.scriptPath; + } + + qDebug() << "Starting spotify resolver with path:" << path; + m_spotifyResolver = QWeakPointer< ScriptResolver >( qobject_cast< ScriptResolver* >( Pipeline::instance()->addScriptResolver( path, enabled() ) ) ); + + connect( m_spotifyResolver.data(), SIGNAL( changed() ), this, SLOT( resolverChanged() ) ); connect( m_spotifyResolver.data(), SIGNAL( customMessage( QString,QVariantMap ) ), this, SLOT( resolverMessage( QString, QVariantMap ) ) ); const bool hasMigrated = configuration().value( "hasMigrated" ).toBool(); @@ -113,6 +154,151 @@ SpotifyAccount::init() msg[ "_msgtype" ] = "getCredentials"; m_spotifyResolver.data()->sendMessage( msg ); } + +} + + +bool SpotifyAccount::checkForResolver() +{ +#ifdef Q_OS_MAC + const QDir path = QCoreApplication::applicationDirPath(); + QFile file( path.absoluteFilePath( "spotify_tomahawkresolver" ) ); + return file.exists(); +#endif + + return false; +} + +void +SpotifyAccount::resolverChanged() +{ + emit connectionStateChanged( connectionState() ); +} + + +Attica::Content +SpotifyAccount::atticaContent() const +{ + return AtticaManager::instance()->resolverForId( s_resolverId ); +} + + +void +SpotifyAccount::authenticate() +{ + if ( !AtticaManager::instance()->resolversLoaded() ) + { + // If we're still waiting to load, wait for the attica resolvers to come down the pipe + connect( AtticaManager::instance(), SIGNAL( resolversLoaded( Attica::Content::List ) ), this, SLOT( atticaLoaded( Attica::Content::List ) ), Qt::UniqueConnection ); + return; + } + + const Attica::Content res = AtticaManager::instance()->resolverForId( s_resolverId ); + const AtticaManager::ResolverState state = AtticaManager::instance()->resolverState( res ); + + qDebug() << "Spotify account authenticating..."; + if ( m_spotifyResolver.isNull() && state == AtticaManager::Installed ) + { + // We don;t have the resolver but it has been installed via attica already, so lets just turn it on + hookupResolver(); + } + else if ( m_spotifyResolver.isNull() ) + { + qDebug() << "Got null resolver but asked to authenticate, so installing if we have one from attica:" << res.isValid() << res.id(); + if ( res.isValid() && !res.id().isEmpty() ) + AtticaManager::instance()->installResolver( res, false ); + else + { +#ifdef Q_OS_LINUX + m_preventEnabling = true; +#endif + } + } + else if ( !m_spotifyResolver.data()->running() ) + { + m_spotifyResolver.data()->start(); + } + + emit connectionStateChanged( connectionState() ); +} + + +void +SpotifyAccount::deauthenticate() +{ + if ( !m_spotifyResolver.isNull() && m_spotifyResolver.data()->running() ) + m_spotifyResolver.data()->stop(); + + emit connectionStateChanged( connectionState() ); +} + + +bool +SpotifyAccount::isAuthenticated() const +{ + return !m_spotifyResolver.isNull() && m_spotifyResolver.data()->running(); +} + + +Account::ConnectionState +SpotifyAccount::connectionState() const +{ + return (!m_spotifyResolver.isNull() && m_spotifyResolver.data()->running()) ? Account::Connected : Account::Disconnected; +} + + +void +SpotifyAccount::resolverInstalled(const QString& resolverId) +{ + if ( resolverId == s_resolverId ) + { + // We requested this install, so we want to launch it + hookupResolver(); + AccountManager::instance()->enableAccount( this ); + } +} + + +void +SpotifyAccount::atticaLoaded( Attica::Content::List ) +{ + disconnect( AtticaManager::instance(), SIGNAL( resolversLoaded( Attica::Content::List ) ), this, SLOT( atticaLoaded( Attica::Content::List ) ) ); + authenticate(); +} + + +void +SpotifyAccount::setManualResolverPath( const QString &resolverPath ) +{ + Q_ASSERT( !resolverPath.isEmpty() ); + + QVariantHash conf = configuration(); + conf[ "path" ] = resolverPath; + setConfiguration( conf ); + sync(); + + m_preventEnabling = false; + + if ( !m_spotifyResolver.isNull() ) + { + // replace + NewClosure( m_spotifyResolver.data(), SIGNAL( destroyed() ), this, SLOT( hookupAfterDeletion( bool ) ), true ); + m_spotifyResolver.data()->deleteLater(); + } + else + { + hookupResolver(); + AccountManager::instance()->enableAccount( this ); + } +} + + +void +SpotifyAccount::hookupAfterDeletion( bool autoEnable ) +{ + hookupResolver(); + if ( autoEnable ) + AccountManager::instance()->enableAccount( this ); } @@ -225,6 +411,7 @@ SpotifyAccount::resolverMessage( const QString &msgType, const QVariantMap &msg if ( msgType == "credentials" ) { QVariantHash creds = credentials(); + creds[ "username" ] = msg.value( "username" ); creds[ "password" ] = msg.value( "password" ); creds[ "highQuality" ] = msg.value( "highQuality" ); @@ -449,6 +636,9 @@ SpotifyAccount::icon() const QWidget* SpotifyAccount::configurationWidget() { + if ( m_spotifyResolver.isNull() ) + return 0; + if ( m_configWidget.isNull() ) { m_configWidget = QWeakPointer< SpotifyAccountConfig >( new SpotifyAccountConfig( this ) ); diff --git a/src/accounts/spotify/SpotifyAccount.h b/src/accounts/spotify/SpotifyAccount.h index c3ee789d8..af05d0be6 100644 --- a/src/accounts/spotify/SpotifyAccount.h +++ b/src/accounts/spotify/SpotifyAccount.h @@ -22,6 +22,7 @@ #include "accounts/ResolverAccount.h" #include "SourceList.h" +#include "AtticaManager.h" #include "Playlist.h" #include "utils/TomahawkUtils.h" #include "utils/SmartPointerList.h" @@ -61,9 +62,6 @@ public: virtual QString factoryId() const { return "spotifyaccount"; } virtual QString prettyName() const { return "Spotify"; } - virtual bool acceptsPath( const QString& path ) const; - virtual Account* createFromPath( const QString& path ); - virtual AccountTypes types() const { return AccountTypes( ResolverType ); } virtual bool allowUserCreation() const { return false; } virtual QPixmap icon() const; @@ -71,7 +69,7 @@ public: }; -class SpotifyAccount : public ResolverAccount +class SpotifyAccount : public CustomAtticaAccount { Q_OBJECT public: @@ -83,10 +81,16 @@ public: virtual QWidget* configurationWidget(); virtual QWidget* aboutWidget(); virtual void saveConfig(); + virtual Attica::Content atticaContent() const; + virtual void authenticate(); + virtual ConnectionState connectionState() const; + virtual bool isAuthenticated() const; + virtual void deauthenticate(); virtual QWidget* aclWidget() { return 0; } virtual Tomahawk::InfoSystem::InfoPluginPtr infoPlugin() { return Tomahawk::InfoSystem::InfoPluginPtr(); } virtual SipPlugin* sipPlugin() { return 0; } + virtual bool preventEnabling() const { return m_preventEnabling; } QString sendMessage( const QVariantMap& msg, QObject* receiver = 0, const QString& slot = QString() ); @@ -95,11 +99,17 @@ public: bool deleteOnUnsync() const; + void setManualResolverPath( const QString& resolverPath ); + public slots: void aboutToShow( QAction* action, const Tomahawk::playlist_ptr& playlist ); void syncActionTriggered( bool ); + void atticaLoaded(Attica::Content::List); private slots: + void resolverChanged(); + void resolverInstalled( const QString& resolverId ); + void resolverMessage( const QString& msgType, const QVariantMap& msg ); void login( const QString& username, const QString& password ); @@ -108,8 +118,13 @@ private slots: void startPlaylistSyncWithPlaylist( const QString& msgType, const QVariantMap& msg ); void playlistCreated( const QString& msgType, const QVariantMap& msg ); -private: void init(); + void hookupAfterDeletion( bool autoEnable ); + +private: + bool checkForResolver(); + void hookupResolver(); + void loadPlaylists(); void clearUser( bool permanentlyDelete = false ); @@ -134,6 +149,8 @@ private: QHash< QString, playlist_ptr > m_waitingForCreateReply; + bool m_preventEnabling; + SmartPointerList< QAction > m_customActions; friend class ::SpotifyPlaylistUpdater; }; diff --git a/src/libtomahawk/AtticaManager.cpp b/src/libtomahawk/AtticaManager.cpp index 0dd491e8f..4dc80d2fc 100644 --- a/src/libtomahawk/AtticaManager.cpp +++ b/src/libtomahawk/AtticaManager.cpp @@ -23,9 +23,8 @@ #include "Pipeline.h" #include -#include -#include +#include #include #include #include @@ -40,13 +39,78 @@ using namespace Attica; AtticaManager* AtticaManager::s_instance = 0; +class BinaryInstallerHelper : public QObject +{ + Q_OBJECT +public: + explicit BinaryInstallerHelper( const QString& resolverId, bool createAccount, AtticaManager* manager) + : QObject( manager ) + , m_manager( QWeakPointer< AtticaManager >( manager ) ) + , m_resolverId( resolverId ) + , m_createAccount( createAccount ) + { + Q_ASSERT( !m_resolverId.isEmpty() ); + Q_ASSERT( !m_manager.isNull() ); + + setProperty( "resolverid", m_resolverId ); + } + + virtual ~BinaryInstallerHelper() {} + +public slots: + void installSucceeded( const QString& path ) + { + qDebug() << Q_FUNC_INFO << "install of binary resolver succeeded, enabling: " << path; + + if ( m_manager.isNull() ) + return; + + if ( m_createAccount ) + { + Tomahawk::Accounts::Account* acct = Tomahawk::Accounts::AccountManager::instance()->accountFromPath( path ); + + Tomahawk::Accounts::AccountManager::instance()->addAccount( acct ); + TomahawkSettings::instance()->addAccount( acct->accountId() ); + Tomahawk::Accounts::AccountManager::instance()->enableAccount( acct ); + } + + m_manager.data()->m_resolverStates[ m_resolverId ].scriptPath = path; + m_manager.data()->m_resolverStates[ m_resolverId ].state = AtticaManager::Installed; + + TomahawkSettingsGui::instanceGui()->setAtticaResolverStates( m_manager.data()->m_resolverStates ); + emit m_manager.data()->resolverInstalled( m_resolverId ); + emit m_manager.data()->resolverStateChanged( m_resolverId ); + + deleteLater(); + } + void installFailed() + { + qDebug() << Q_FUNC_INFO << "install failed"; + + if ( m_manager.isNull() ) + return; + + m_manager.data()->resolverInstallationFailed( m_resolverId ); + + deleteLater(); + } + +private: + QString m_resolverId; + bool m_createAccount; + QWeakPointer m_manager; +}; + + AtticaManager::AtticaManager( QObject* parent ) : QObject( parent ) + , m_resolverJobsLoaded( 0 ) { connect( &m_manager, SIGNAL( providerAdded( Attica::Provider ) ), this, SLOT( providerAdded( Attica::Provider ) ) ); // resolvers - m_manager.addProviderFile( QUrl( "http://bakery.tomahawk-player.org/resolvers/providers.xml" ) ); + m_manager.addProviderFile( QUrl( "http://bakery.tomahawk-player.org/resolvers/providers.xml" ) ); +// m_manager.addProviderFile( QUrl( "http://lycophron/resolvers/providers.xml" ) ); qRegisterMetaType< Attica::Content >( "Attica::Content" ); } @@ -215,11 +279,8 @@ AtticaManager::userHasRated( const Content& c ) const bool AtticaManager::hasCustomAccountForAttica( const QString &id ) const { - // Only last.fm at the moment contains a custom account - if ( id == "lastfm" ) - return true; - - return false; + qDebug() << "Got custom account for?" << id << m_customAccounts.keys(); + return m_customAccounts.keys().contains( id ); } @@ -250,9 +311,32 @@ AtticaManager::providerAdded( const Provider& provider ) if ( provider.name() == "Tomahawk Resolvers" ) { m_resolverProvider = provider; + m_resolvers.clear(); + + m_resolverStates = TomahawkSettingsGui::instanceGui()->atticaResolverStates(); + + ListJob* job = m_resolverProvider.requestCategories(); + connect( job, SIGNAL( finished( Attica::BaseJob* ) ), this, SLOT( categoriesReturned( Attica::BaseJob* ) ) ); + job->start(); + } +} + + +void +AtticaManager::categoriesReturned( BaseJob* j ) +{ + ListJob< Category >* job = static_cast< ListJob< Category >* >( j ); + + Category::List categories = job->itemList(); + foreach ( const Category& category, categories ) + { + ListJob< Content >* job = m_resolverProvider.searchContents( Category::List() << category, QString(), Provider::Downloads, 0, 50 ); + + if ( category.name() == "Resolver" ) + connect( job, SIGNAL( finished( Attica::BaseJob* ) ), this, SLOT( resolversList( Attica::BaseJob* ) ) ); + else if ( category.name() == "BinaryResolver" ) + connect( job, SIGNAL( finished( Attica::BaseJob* ) ), this, SLOT( binaryResolversList( Attica::BaseJob* ) ) ); - ListJob< Content >* job = m_resolverProvider.searchContents( Category::List(), QString(), Provider::Downloads, 0, 30 ); - connect( job, SIGNAL( finished( Attica::BaseJob* ) ), this, SLOT( resolversList( Attica::BaseJob* ) ) ); job->start(); } } @@ -263,8 +347,7 @@ AtticaManager::resolversList( BaseJob* j ) { ListJob< Content >* job = static_cast< ListJob< Content >* >( j ); - m_resolvers = job->itemList(); - m_resolverStates = TomahawkSettingsGui::instanceGui()->atticaResolverStates(); + m_resolvers.append( job->itemList() ); // Sanity check. if any resolvers are installed that don't exist on the hd, remove them. foreach ( const QString& rId, m_resolverStates.keys() ) @@ -272,6 +355,9 @@ AtticaManager::resolversList( BaseJob* j ) if ( m_resolverStates[ rId ].state == Installed || m_resolverStates[ rId ].state == NeedsUpgrade ) { + if ( m_resolverStates[ rId ].binary ) + continue; + // Guess location on disk QDir dir( QString( "%1/atticaresolvers/%2" ).arg( TomahawkUtils::appDataDir().absolutePath() ).arg( rId ) ); if ( !dir.exists() ) @@ -303,7 +389,50 @@ AtticaManager::resolversList( BaseJob* j ) syncServerData(); - emit resolversLoaded( m_resolvers ); + if ( ++m_resolverJobsLoaded == 2 ) + emit resolversLoaded( m_resolvers ); +} + + +void +AtticaManager::binaryResolversList( BaseJob* j ) +{ + ListJob< Content >* job = static_cast< ListJob< Content >* >( j ); + + Content::List binaryResolvers = job->itemList(); + + // NOTE: No binary support for linux distros + QString platform; +#ifdef Q_OS_MAC + platform = "osx"; +#elif Q_OS_WIN + platform = "win"; +#endif + + foreach ( const Content& c, binaryResolvers ) + { + if ( !c.attribute( "typeid" ).isEmpty() && c.attribute( "typeid" ) == platform ) + { + // We have a binary resolver for this platform + qDebug() << "WE GOT A BINARY RESOLVER:" << c.id() << c.name() << c.attribute( "signature" ); + m_resolvers.append( c ); + if ( !m_resolverStates.contains( c.id() ) ) + { + Resolver r; + r.binary = true; + m_resolverStates.insert( c.id(), r ); + } + else if ( m_resolverStates[ c.id() ].binary != true ) + { // HACK workaround... why is this not set in the first place sometimes? Migration issue? + m_resolverStates[ c.id() ].binary = true; + } + + + } + } + + if ( ++m_resolverJobsLoaded == 2 ) + emit resolversLoaded( m_resolvers ); } @@ -382,6 +511,7 @@ AtticaManager::installResolver( const Content& resolver, bool autoCreateAccount connect( job, SIGNAL( finished( Attica::BaseJob* ) ), this, SLOT( resolverDownloadFinished( Attica::BaseJob* ) ) ); job->setProperty( "resolverId", resolver.id() ); job->setProperty( "createAccount", autoCreateAccount ); + job->setProperty( "binarySignature", resolver.attribute("signature")); job->start(); } @@ -418,6 +548,7 @@ AtticaManager::resolverDownloadFinished ( BaseJob* j ) connect( reply, SIGNAL( finished() ), this, SLOT( payloadFetched() ) ); reply->setProperty( "resolverId", job->property( "resolverId" ) ); reply->setProperty( "createAccount", job->property( "createAccount" ) ); + reply->setProperty( "binarySignature", job->property( "binarySignature" ) ); } else { @@ -432,6 +563,9 @@ AtticaManager::payloadFetched() QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() ); Q_ASSERT( reply ); + bool installedSuccessfully = false; + const QString resolverId = reply->property( "resolverId" ).toString(); + // we got a zip file, save it to a temporary file, then unzip it to our destination data dir if ( reply->error() == QNetworkReply::NoError ) { @@ -444,102 +578,64 @@ AtticaManager::payloadFetched() f.write( reply->readAll() ); f.close(); - QString resolverId = reply->property( "resolverId" ).toString(); - QDir dir( extractPayload( f.fileName(), resolverId ) ); - QString resolverPath = dir.absoluteFilePath( m_resolverStates[ resolverId ].scriptPath ); - - if ( !resolverPath.isEmpty() ) + if ( m_resolverStates[ resolverId ].binary ) { - // update with absolute, not relative, path - m_resolverStates[ resolverId ].scriptPath = resolverPath; - - if ( reply->property( "createAccount" ).toBool() ) + // First ensure the signature matches. If we can't verify it, abort! + const QString signature = reply->property( "binarySignature" ).toString(); + // Must have a signature for binary resolvers... + Q_ASSERT( !signature.isEmpty() ); + if ( signature.isEmpty() ) + return; + if ( !TomahawkUtils::verifyFile( f.fileName(), signature ) ) { - // Do the install / add to tomahawk - Tomahawk::Accounts::Account* resolver = Tomahawk::Accounts::ResolverAccountFactory::createFromPath( resolverPath, "resolveraccount", true ); - Tomahawk::Accounts::AccountManager::instance()->addAccount( resolver ); - TomahawkSettings::instance()->addAccount( resolver->accountId() ); + qWarning() << "FILE SIGNATURE FAILED FOR BINARY RESOLVER! WARNING! :" << f.fileName() << signature; } + else + { + TomahawkUtils::extractBinaryResolver( f.fileName(), new BinaryInstallerHelper( resolverId, reply->property( "createAccount" ).toBool(), this ) ); + // Don't emit failed yet + installedSuccessfully = true; + } + } + else + { + QDir dir( TomahawkUtils::extractScriptPayload( f.fileName(), resolverId ) ); + QString resolverPath = dir.absoluteFilePath( m_resolverStates[ resolverId ].scriptPath ); - m_resolverStates[ resolverId ].state = Installed; - TomahawkSettingsGui::instanceGui()->setAtticaResolverStates( m_resolverStates ); - emit resolverInstalled( resolverId ); - emit resolverStateChanged( resolverId ); + if ( !resolverPath.isEmpty() ) + { + // update with absolute, not relative, path + m_resolverStates[ resolverId ].scriptPath = resolverPath; + + if ( reply->property( "createAccount" ).toBool() ) + { + // Do the install / add to tomahawk + Tomahawk::Accounts::Account* resolver = Tomahawk::Accounts::ResolverAccountFactory::createFromPath( resolverPath, "resolveraccount", true ); + Tomahawk::Accounts::AccountManager::instance()->addAccount( resolver ); + TomahawkSettings::instance()->addAccount( resolver->accountId() ); + } + + installedSuccessfully = true; + } } } else { tLog() << "Failed to download attica payload...:" << reply->errorString(); } -} -QString -AtticaManager::extractPayload( const QString& filename, const QString& resolverId ) const -{ - // uses QuaZip to extract the temporary zip file to the user's tomahawk data/resolvers directory - QuaZip zipFile( filename ); - if ( !zipFile.open( QuaZip::mdUnzip ) ) + if ( installedSuccessfully ) { - tLog() << "Failed to QuaZip open:" << zipFile.getZipError(); - return QString(); + m_resolverStates[ resolverId ].state = Installed; + TomahawkSettingsGui::instanceGui()->setAtticaResolverStates( m_resolverStates ); + emit resolverInstalled( resolverId ); + emit resolverStateChanged( resolverId ); } - - if ( !zipFile.goToFirstFile() ) + else { - tLog() << "Failed to go to first file in zip archive: " << zipFile.getZipError(); - return QString(); + emit resolverInstallationFailed( resolverId ); } - - QDir resolverDir = TomahawkUtils::appDataDir(); - if ( !resolverDir.mkpath( QString( "atticaresolvers/%1" ).arg( resolverId ) ) ) - { - tLog() << "Failed to mkdir resolver save dir: " << TomahawkUtils::appDataDir().absoluteFilePath( QString( "atticaresolvers/%1" ).arg( resolverId ) ); - return QString(); - } - resolverDir.cd( QString( "atticaresolvers/%1" ).arg( resolverId ) ); - tDebug() << "Installing resolver to:" << resolverDir.absolutePath(); - - QuaZipFile fileInZip( &zipFile ); - do - { - QuaZipFileInfo info; - zipFile.getCurrentFileInfo( &info ); - - if ( !fileInZip.open( QIODevice::ReadOnly ) ) - { - tLog() << "Failed to open file inside zip archive:" << info.name << zipFile.getZipName() << "with error:" << zipFile.getZipError(); - continue; - } - - QFile out( resolverDir.absoluteFilePath( fileInZip.getActualFileName() ) ); - - QStringList parts = fileInZip.getActualFileName().split( "/" ); - if ( parts.size() > 1 ) - { - QStringList dirs = parts.mid( 0, parts.size() - 1 ); - QString dirPath = dirs.join( "/" ); // QDir translates / to \ internally if necessary - resolverDir.mkpath( dirPath ); - } - - // make dir if there is one needed - QDir d( fileInZip.getActualFileName() ); - - tDebug() << "Writing to output file..." << out.fileName(); - if ( !out.open( QIODevice::WriteOnly ) ) - { - tLog() << "Failed to open resolver extract file:" << out.errorString() << info.name; - continue; - } - - - out.write( fileInZip.readAll() ); - out.close(); - fileInZip.close(); - - } while ( zipFile.goToNextFile() ); - - return resolverDir.absolutePath(); } @@ -617,3 +713,5 @@ AtticaManager::doResolverRemove( const QString& id ) const TomahawkUtils::removeDirectory( resolverDir.absolutePath() ); } + +#include "AtticaManager.moc" diff --git a/src/libtomahawk/AtticaManager.h b/src/libtomahawk/AtticaManager.h index 27bcacda6..fecb2d27f 100644 --- a/src/libtomahawk/AtticaManager.h +++ b/src/libtomahawk/AtticaManager.h @@ -32,6 +32,7 @@ #include #include +class BinaryInstallerHelper; class DLLEXPORT AtticaManager : public QObject { @@ -51,13 +52,14 @@ public: int userRating; // 0-100 ResolverState state; QPixmap* pixmap; + bool binary; // internal bool pixmapDirty; - Resolver( const QString& v, const QString& path, int userR, ResolverState s ) - : version( v ), scriptPath( path ), userRating( userR ), state( s ), pixmap( 0 ), pixmapDirty( false ) {} - Resolver() : userRating( -1 ), state( Uninstalled ), pixmap( 0 ), pixmapDirty( false ) {} + Resolver( const QString& v, const QString& path, int userR, ResolverState s, bool resolver ) + : version( v ), scriptPath( path ), userRating( userR ), state( s ), pixmap( 0 ), binary( false ), pixmapDirty( false ) {} + Resolver() : userRating( -1 ), state( Uninstalled ), pixmap( 0 ), binary( false ), pixmapDirty( false ) {} }; typedef QHash< QString, AtticaManager::Resolver > StateHash; @@ -90,7 +92,7 @@ public: /** If the resolver coming from libattica has a native custom c++ account - as well. For example the last.fm account. + as well. For example the last.fm & spotify accounts. */ bool hasCustomAccountForAttica( const QString& id ) const; Tomahawk::Accounts::Account* customAccountForAttica( const QString& id ) const; @@ -108,10 +110,13 @@ signals: void resolverStateChanged( const QString& resolverId ); void resolverInstalled( const QString& resolverId ); void resolverUninstalled( const QString& resolverId ); + void resolverInstallationFailed( const QString& resolverId ); private slots: void providerAdded( const Attica::Provider& ); + void categoriesReturned( Attica::BaseJob* ); void resolversList( Attica::BaseJob* ); + void binaryResolversList( Attica::BaseJob* ); void resolverDownloadFinished( Attica::BaseJob* ); void payloadFetched(); @@ -131,9 +136,12 @@ private: Attica::Content::List m_resolvers; StateHash m_resolverStates; + int m_resolverJobsLoaded; QMap< QString, Tomahawk::Accounts::Account* > m_customAccounts; static AtticaManager* s_instance; + + friend class ::BinaryInstallerHelper; }; class DLLEXPORT CustomAtticaAccount : public Tomahawk::Accounts::Account diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt index bf6d9f624..501ef3188 100644 --- a/src/libtomahawk/CMakeLists.txt +++ b/src/libtomahawk/CMakeLists.txt @@ -370,6 +370,7 @@ IF( APPLE ) SET( libSources ${libSources} utils/TomahawkUtils_Mac.mm + mac/FileHelpers.mm thirdparty/Qocoa/qsearchfield_mac.mm ) SET_SOURCE_FILES_PROPERTIES(utils/TomahawkUtils_Mac.mm PROPERTIES COMPILE_FLAGS "-fvisibility=default") @@ -379,10 +380,11 @@ IF( APPLE ) # System ${COREAUDIO_LIBRARY} ${COREFOUNDATION_LIBRARY} - ${FOUNDATION_LIBRARY} + ${FOUNDATION_LIBRARY} ${SCRIPTINGBRIDGE_LIBRARY} /System/Library/Frameworks/AppKit.framework + /System/Library/Frameworks/Security.framework ) ELSE( APPLE ) SET( libGuiSources ${libGuiSources} thirdparty/Qocoa/qsearchfield.cpp ) diff --git a/src/libtomahawk/TomahawkSettings.cpp b/src/libtomahawk/TomahawkSettings.cpp index 3f68f5f99..cb563ec7a 100644 --- a/src/libtomahawk/TomahawkSettings.cpp +++ b/src/libtomahawk/TomahawkSettings.cpp @@ -127,6 +127,21 @@ TomahawkSettings::TomahawkSettings( QObject* parent ) // insert upgrade code here as required setValue( "configversion", TOMAHAWK_SETTINGS_VERSION ); } + + // Ensure last.fm and spotify accounts always exist + QString spotifyAcct, lastfmAcct; + foreach ( const QString& acct, value( "accounts/allaccounts" ).toStringList() ) + { + if ( acct.startsWith( "lastfmaccount_" ) ) + lastfmAcct = acct; + else if ( acct.startsWith( "spotifyaccount_" ) ) + spotifyAcct = acct; + } + + if ( spotifyAcct.isEmpty() ) + createSpotifyAccount(); + if ( lastfmAcct.isEmpty() ) + createLastFmAccount(); } @@ -142,6 +157,15 @@ TomahawkSettings::doInitialSetup() // by default we add a local network resolver addAccount( "sipzeroconf_autocreated" ); + + createLastFmAccount(); + createSpotifyAccount(); +} + + +void +TomahawkSettings::createLastFmAccount() +{ // Add a last.fm account for scrobbling and infosystem const QString accountKey = QString( "lastfmaccount_%1" ).arg( QUuid::createUuid().toString().mid( 1, 8 ) ); addAccount( accountKey ); @@ -151,6 +175,27 @@ TomahawkSettings::doInitialSetup() setValue( "autoconnect", true ); setValue( "types", QStringList() << "ResolverType" << "StatusPushType" ); endGroup(); + + QStringList allAccounts = value( "accounts/allaccounts" ).toStringList(); + allAccounts << accountKey; + setValue( "accounts/allaccounts", allAccounts ); +} + + +void +TomahawkSettings::createSpotifyAccount() +{ + const QString accountKey = QString( "spotifyaccount_%1" ).arg( QUuid::createUuid().toString().mid( 1, 8 ) ); + beginGroup( "accounts/" + accountKey ); + setValue( "enabled", false ); + setValue( "types", QStringList() << "ResolverType" ); + setValue( "credentials", QVariantHash() ); + setValue( "configuration", QVariantHash() ); + endGroup(); + + QStringList allAccounts = value( "accounts/allaccounts" ).toStringList(); + allAccounts << accountKey; + setValue( "accounts/allaccounts", allAccounts ); } @@ -516,6 +561,34 @@ TomahawkSettings::doUpgrade( int oldVersion, int newVersion ) remove( "playlistupdaters" ); } + else if ( oldVersion == 11 ) + { + // If the user doesn't have a spotify account, create one, since now it + // is like the last.fm account and always exists + QStringList allAccounts = value( "accounts/allaccounts" ).toStringList(); + QString acct; + foreach ( const QString& account, allAccounts ) + { + if ( account.startsWith( "spotifyaccount_" ) ) + { + acct = account; + break; + } + } + + if ( !acct.isEmpty() ) + { + beginGroup( "accounts/" + acct ); + QVariantHash conf = value( "configuration" ).toHash(); + foreach ( const QString& key, conf.keys() ) + qDebug() << key << conf[ key ].toString(); + endGroup(); + } + else + { + createSpotifyAccount(); + } + } } diff --git a/src/libtomahawk/TomahawkSettings.h b/src/libtomahawk/TomahawkSettings.h index e7c6156e7..717da384d 100644 --- a/src/libtomahawk/TomahawkSettings.h +++ b/src/libtomahawk/TomahawkSettings.h @@ -30,7 +30,7 @@ #include "DllMacro.h" -#define TOMAHAWK_SETTINGS_VERSION 10 +#define TOMAHAWK_SETTINGS_VERSION 12 /** * Convenience wrapper around QSettings for tomahawk-specific config @@ -214,6 +214,8 @@ private slots: private: void doInitialSetup(); + void createLastFmAccount(); + void createSpotifyAccount(); void doUpgrade( int oldVersion, int newVersion ); static TomahawkSettings* s_instance; diff --git a/src/libtomahawk/TomahawkSettingsGui.cpp b/src/libtomahawk/TomahawkSettingsGui.cpp index dea0c5236..8aa15b3c2 100644 --- a/src/libtomahawk/TomahawkSettingsGui.cpp +++ b/src/libtomahawk/TomahawkSettingsGui.cpp @@ -30,7 +30,7 @@ inline QDataStream& operator<<(QDataStream& out, const AtticaManager::StateHash& foreach( const QString& key, states.keys() ) { AtticaManager::Resolver resolver = states[ key ]; - out << key << resolver.version << resolver.scriptPath << (qint32)resolver.state << resolver.userRating; + out << key << resolver.version << resolver.scriptPath << (qint32)resolver.state << resolver.userRating << resolver.binary; } return out; } @@ -38,19 +38,25 @@ inline QDataStream& operator<<(QDataStream& out, const AtticaManager::StateHash& inline QDataStream& operator>>(QDataStream& in, AtticaManager::StateHash& states) { - quint32 count = 0, version = 0; - in >> version; + quint32 count = 0, configVersion = 0; + in >> configVersion; in >> count; for ( uint i = 0; i < count; i++ ) { QString key, version, scriptPath; qint32 state, userRating; + bool binary = false; in >> key; in >> version; in >> scriptPath; in >> state; in >> userRating; - states[ key ] = AtticaManager::Resolver( version, scriptPath, userRating, (AtticaManager::ResolverState)state ); + if ( configVersion > 10 ) + { + // V11 includes 'bool binary' flag + in >> binary; + } + states[ key ] = AtticaManager::Resolver( version, scriptPath, userRating, (AtticaManager::ResolverState)state, binary ); } return in; } diff --git a/src/libtomahawk/accounts/Account.h b/src/libtomahawk/accounts/Account.h index 98afb3750..48aecefa6 100644 --- a/src/libtomahawk/accounts/Account.h +++ b/src/libtomahawk/accounts/Account.h @@ -102,6 +102,10 @@ public: virtual Tomahawk::InfoSystem::InfoPluginPtr infoPlugin() = 0; virtual SipPlugin* sipPlugin() = 0; + // Some accounts cannot be enabled if authentication fails. Return true after failing to authenticate + // if this is the case, and the account will not be enabled + virtual bool preventEnabling() const { return false; } + AccountTypes types() const; void setAccountServiceName( const QString &serviceName ) { QMutexLocker locker( &m_mutex ); m_accountServiceName = serviceName; } diff --git a/src/libtomahawk/accounts/AccountManager.cpp b/src/libtomahawk/accounts/AccountManager.cpp index 0c9e3d190..1bacf9c30 100644 --- a/src/libtomahawk/accounts/AccountManager.cpp +++ b/src/libtomahawk/accounts/AccountManager.cpp @@ -202,6 +202,9 @@ AccountManager::enableAccount( Account* account ) account->authenticate(); + if ( account->preventEnabling() ) + return; + account->setEnabled( true ); m_enabledAccounts << account; diff --git a/src/libtomahawk/accounts/AccountModel.cpp b/src/libtomahawk/accounts/AccountModel.cpp index f58014435..677e9c3c6 100644 --- a/src/libtomahawk/accounts/AccountModel.cpp +++ b/src/libtomahawk/accounts/AccountModel.cpp @@ -24,6 +24,10 @@ #include "AtticaManager.h" #include "ResolverAccount.h" +#ifndef ENABLE_HEADLESS +#include +#endif + #include using namespace Tomahawk; @@ -35,6 +39,7 @@ AccountModel::AccountModel( QObject* parent ) : QAbstractListModel( parent ) { connect( AtticaManager::instance(), SIGNAL( resolversLoaded( Attica::Content::List ) ), this, SLOT( loadData() ) ); + connect( AtticaManager::instance(), SIGNAL( resolverInstallationFailed( QString ) ), this, SLOT( resolverInstallFailed( QString ) ) ); connect( AccountManager::instance(), SIGNAL( added( Tomahawk::Accounts::Account* ) ), this, SLOT( accountAdded( Tomahawk::Accounts::Account* ) ) ); connect( AccountManager::instance(), SIGNAL( removed( Tomahawk::Accounts::Account* ) ), this, SLOT( accountRemoved( Tomahawk::Accounts::Account* ) ) ); @@ -491,6 +496,21 @@ AccountModel::setData( const QModelIndex& index, const QVariant& value, int role else if( state == Qt::Unchecked ) AccountManager::instance()->disableAccount( acct ); +#if defined(Q_OS_LINUX) && !defined(ENABLE_HEADLESS) + if ( acct->preventEnabling() ) + { + // Can't install from attica yet on linux, so show a warning if the user tries to turn it on. + // TODO make a prettier display + QMessageBox box; + box.setWindowTitle( tr( "Manual Install Required" ) ); + box.setTextFormat( Qt::RichText ); + box.setIcon( QMessageBox::Information ); + box.setText( tr( "Unfortunately, automatic installation of this resolver is not yet available on Linux.

" + "Please use \"Install from file\" above, by fetching it from your distribution or compiling it yourself. Further instructions can be found here:

http://www.tomahawk-player.org/resolvers/%1" ).arg( acct->accountServiceName() ) ); + box.setStandardButtons( QMessageBox::Ok ); + box.exec(); + } +#endif emit dataChanged( index, index ); return true; @@ -615,7 +635,8 @@ AccountModel::accountStateChanged( Account* account , Account::ConnectionState ) // For each type that this node could be, check the corresponding data if ( ( n->type == AccountModelNode::UniqueFactoryType && n->accounts.size() && n->accounts.first() == account ) || ( n->type == AccountModelNode::AtticaType && n->atticaAccount && n->atticaAccount == account ) || - ( n->type == AccountModelNode::ManualResolverType && n->resolverAccount && n->resolverAccount == account ) ) + ( n->type == AccountModelNode::ManualResolverType && n->resolverAccount && n->resolverAccount == account ) || + ( n->type == AccountModelNode::CustomAccountType && n->customAccount && n->customAccount == account ) ) { const QModelIndex idx = index( i, 0, QModelIndex() ); emit dataChanged( idx, idx ); @@ -689,6 +710,22 @@ AccountModel::accountRemoved( Account* account ) } +void +AccountModel::resolverInstallFailed( const QString& resolverId ) +{ + for ( int i = 0; i < m_accounts.size(); i++ ) + { + if ( m_accounts[ i ]->type == AccountModelNode::AtticaType && m_accounts[ i ]->atticaContent.id() == resolverId ) + { + qDebug() << "Got failed attica install in account mode, emitting signal!"; + emit errorInstalling( index( i, 0, QModelIndex() ) ); + + return; + } + } +} + + int AccountModel::rowCount( const QModelIndex& ) const { diff --git a/src/libtomahawk/accounts/AccountModel.h b/src/libtomahawk/accounts/AccountModel.h index 68fcb89f6..e383b6ec6 100644 --- a/src/libtomahawk/accounts/AccountModel.h +++ b/src/libtomahawk/accounts/AccountModel.h @@ -96,6 +96,8 @@ signals: void startInstalling( const QPersistentModelIndex& idx ); void doneInstalling( const QPersistentModelIndex& idx ); + void errorInstalling( const QPersistentModelIndex& idx ); + private slots: void loadData(); @@ -103,6 +105,7 @@ private slots: void accountRemoved( Tomahawk::Accounts::Account* ); void accountStateChanged( Account*, Accounts::Account::ConnectionState ); + void resolverInstallFailed( const QString& resolverId ); private: QList< AccountModelNode* > m_accounts; QSet< QString > m_waitingForAtticaInstall; diff --git a/src/libtomahawk/accounts/AccountModelFilterProxy.cpp b/src/libtomahawk/accounts/AccountModelFilterProxy.cpp index 1a559faed..fee5516f3 100644 --- a/src/libtomahawk/accounts/AccountModelFilterProxy.cpp +++ b/src/libtomahawk/accounts/AccountModelFilterProxy.cpp @@ -38,6 +38,7 @@ AccountModelFilterProxy::setSourceModel( QAbstractItemModel* sourceModel ) connect( sourceModel, SIGNAL( scrollTo( QModelIndex ) ), this, SLOT( onScrollTo( QModelIndex ) ) ); connect( sourceModel, SIGNAL( startInstalling( QPersistentModelIndex ) ), this, SLOT( onStartInstalling( QPersistentModelIndex ) ) ); connect( sourceModel, SIGNAL( doneInstalling( QPersistentModelIndex ) ), this, SLOT( onDoneInstalling( QPersistentModelIndex ) ) ); + connect( sourceModel, SIGNAL( errorInstalling( QPersistentModelIndex ) ), this, SLOT( onErrorInstalling( QPersistentModelIndex ) ) ); QSortFilterProxyModel::setSourceModel( sourceModel ); } @@ -86,3 +87,11 @@ AccountModelFilterProxy::onStartInstalling( const QPersistentModelIndex& idx ) { emit startInstalling( mapFromSource( idx ) ); } + + +void +AccountModelFilterProxy::onErrorInstalling( const QPersistentModelIndex& idx ) +{ + emit errorInstalling( mapFromSource( idx ) ); +} + diff --git a/src/libtomahawk/accounts/AccountModelFilterProxy.h b/src/libtomahawk/accounts/AccountModelFilterProxy.h index 75021c6b1..2a8868fd8 100644 --- a/src/libtomahawk/accounts/AccountModelFilterProxy.h +++ b/src/libtomahawk/accounts/AccountModelFilterProxy.h @@ -42,6 +42,7 @@ signals: void startInstalling( const QPersistentModelIndex& idx ); void doneInstalling( const QPersistentModelIndex& idx ); + void errorInstalling( const QPersistentModelIndex& idx ); protected: virtual bool filterAcceptsRow ( int sourceRow, const QModelIndex& sourceParent ) const; @@ -51,6 +52,7 @@ private slots: void onStartInstalling( const QPersistentModelIndex& idx ); void onDoneInstalling( const QPersistentModelIndex& idx ); + void onErrorInstalling( const QPersistentModelIndex& idx ); private: Tomahawk::Accounts::AccountType m_filterType; diff --git a/src/libtomahawk/accounts/ResolverAccount.h b/src/libtomahawk/accounts/ResolverAccount.h index 03bd46fc5..bcf167640 100644 --- a/src/libtomahawk/accounts/ResolverAccount.h +++ b/src/libtomahawk/accounts/ResolverAccount.h @@ -99,7 +99,7 @@ protected: * Extends ResolverAccount with what attica additionally provides---e.g. icon * Assumes certain file layout on disk. */ -class AtticaResolverAccount : public ResolverAccount +class DLLEXPORT AtticaResolverAccount : public ResolverAccount { Q_OBJECT public: diff --git a/src/libtomahawk/database/DatabaseCollection.cpp b/src/libtomahawk/database/DatabaseCollection.cpp index cb864cc66..2dca1980d 100644 --- a/src/libtomahawk/database/DatabaseCollection.cpp +++ b/src/libtomahawk/database/DatabaseCollection.cpp @@ -1,6 +1,7 @@ /* === This file is part of Tomahawk Player - === * * Copyright 2010-2011, Christian Muehlhaeuser + * Copyright 2010-2011, Leo Franchi * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/libtomahawk/database/DatabaseCollection.h b/src/libtomahawk/database/DatabaseCollection.h index a9e9fae65..de94bbd6c 100644 --- a/src/libtomahawk/database/DatabaseCollection.h +++ b/src/libtomahawk/database/DatabaseCollection.h @@ -1,6 +1,7 @@ /* === This file is part of Tomahawk Player - === * * Copyright 2010-2011, Christian Muehlhaeuser + * Copyright 2010-2011, Leo Franchi * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/libtomahawk/mac/FileHelpers.h b/src/libtomahawk/mac/FileHelpers.h new file mode 100644 index 000000000..1a25cd97d --- /dev/null +++ b/src/libtomahawk/mac/FileHelpers.h @@ -0,0 +1,40 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi . + */ + +#ifndef MAC_FILE_HELPERS_H +#define MAC_FILE_HELPERS_H + +#import +#import + +// Implement this delegate protocol to get notified about the result of your copy attempt +@interface NSObject (SUInstallerDelegateInformalProtocol) +- (void)moveFinished; +- (void)moveFailedWithError:(NSError *)error; +@end + +@interface FileHelpers : NSObject +{} +// Move a file from point A to point B, asking for authentication if necessary +// Will be asynchronous: Implement the delegate protocol know about the completion ++ (void) moveFile:(NSString *)source to:(NSString*)dest withDelegate:delegate; + + +@end + +#endif diff --git a/src/libtomahawk/mac/FileHelpers.mm b/src/libtomahawk/mac/FileHelpers.mm new file mode 100644 index 000000000..97ee3c4ed --- /dev/null +++ b/src/libtomahawk/mac/FileHelpers.mm @@ -0,0 +1,226 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi . + */ + +#import "FileHelpers.h" + +#import +#import +#import +#import +#import +#import +#import + +#include + +static NSString * const TKCopySourceKey = @"TKInstallerSourcePath"; +static NSString * const TKCopyDestinationKey = @"TKInstallerDestinationPath"; +static NSString * const TKInstallerDelegateKey = @"TKInstallerDelegate"; +static NSString * const TKInstallerResultKey = @"TKInstallerResult"; +static NSString * const TKInstallerErrorKey = @"TKInstallerError"; + +class CAutoreleasePool +{ + NSAutoreleasePool *pool; + +public: + CAutoreleasePool() + { + pool = [[NSAutoreleasePool alloc] init]; + } + + ~CAutoreleasePool() + { + [pool drain]; + } +}; + +// Authorization code based on generous contribution from Allan Odgaard. Thanks, Allan! +static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authorization, const char* executablePath, AuthorizationFlags options, const char* const* arguments) +{ + // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! + + sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); + BOOL returnValue = YES; + + if (AuthorizationExecuteWithPrivileges(authorization, executablePath, options, (char* const*)arguments, NULL) == errAuthorizationSuccess) + { + int status; + pid_t pid = wait(&status); + if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) + returnValue = NO; + } + else + returnValue = NO; + + signal(SIGCHLD, oldSigChildHandler); + return returnValue; +} + +@implementation FileHelpers + ++ (void) moveFile:(NSString *)source to:(NSString*)dest withDelegate:delegate +{ + NSLog(@"FileHelpers moving file from %@ to %@", source, dest); + + NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:source, TKCopySourceKey, dest, TKCopyDestinationKey, delegate, TKInstallerDelegateKey, nil]; + [NSThread detachNewThreadSelector:@selector(performMoveWithInfo:) toTarget:self withObject:info]; +} + + ++ (void)performMoveWithInfo:(NSDictionary *)info +{ + // *** GETS CALLED ON NON-MAIN THREAD! + + CAutoreleasePool _p; + + NSString* fromPath = [info objectForKey: TKCopySourceKey]; + NSString* toPath = [info objectForKey: TKCopyDestinationKey]; + + AuthorizationRef auth = NULL; + OSStatus authStat = errAuthorizationDenied; + + NSLog(@"FileHelpers moving file from %@ to %@", fromPath, toPath); + BOOL haveOld = [[NSFileManager defaultManager] fileExistsAtPath: toPath]; + + if (haveOld == YES) { // delete the old file if it's there + if (0 != access([[toPath stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK) + || 0 != access([[[toPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK)) + { + const char* rmParams[] = { [toPath fileSystemRepresentation], NULL }; + // NSLog( @"WOULD DELETE: %@", [toPath fileSystemRepresentation] ); + + while( authStat == errAuthorizationDenied ) + { + authStat = AuthorizationCreate(NULL, + kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, + &auth); + } + if (authStat == errAuthorizationSuccess) + { + BOOL res = AuthorizationExecuteWithPrivilegesAndWait( auth, "/bin/rm", kAuthorizationFlagDefaults, rmParams ); + if (!res) + NSLog(@"Could not delete: %@", toPath); + } else { + qDebug() << "Failed to authenticate to delete file under target to move, aborting"; + return; + } + } else { + // We can delete it ourselves w/out authenticating + NSFileManager *manager = [[[NSFileManager alloc] init] autorelease]; + NSError* error; + BOOL success = [manager removeItemAtPath:toPath error:&error]; + + if (!success) { + NSLog(@"Failed to delete file (w/out perms) underneath copy!: %@", [[error userInfo] objectForKey: NSLocalizedDescriptionKey]); + return; + } + } + } + + FSRef dstRef, dstDirRef; + OSStatus err = FSPathMakeRefWithOptions((UInt8 *)[toPath fileSystemRepresentation], kFSPathMakeRefDoNotFollowLeafSymlink, &dstRef, NULL); + + if (err != noErr && err != fnfErr) { // If the file is not found that's fine, we're moving to there after all + qDebug() << "GOT AN ERROR DOING FSPathMakeRefWithOptions!!!!! aborting move"; + return; + } + + if (0 != access([[toPath stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK) + || 0 != access([[[toPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK)) + { + // Not writeable by user, so authenticate + if (!auth) { + while( authStat == errAuthorizationDenied ) + { + authStat = AuthorizationCreate(NULL, + kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, + &auth); + } + } + + if (authStat == errAuthorizationSuccess) + { + // Fix perms before moving so we have them correct when they arrive + struct stat dstSB; + stat([[toPath stringByDeletingLastPathComponent] fileSystemRepresentation], &dstSB); + char uidgid[42]; + snprintf(uidgid, sizeof(uidgid), "%d:%d", + dstSB.st_uid, dstSB.st_gid); + + const char* coParams[] = { "-R", uidgid, [fromPath fileSystemRepresentation], NULL }; + BOOL res = AuthorizationExecuteWithPrivilegesAndWait( auth, "/usr/sbin/chown", kAuthorizationFlagDefaults, coParams ); + if( !res ) + qDebug() << "Failed to set permissions before moving"; + + // Do the move + const char* mvParams[] = { "-f", [fromPath fileSystemRepresentation], [toPath fileSystemRepresentation], NULL }; + res = AuthorizationExecuteWithPrivilegesAndWait( auth, "/bin/mv", kAuthorizationFlagDefaults, mvParams ); + if( !res ) + NSLog(@"Failed to move source file from %@ to %@ with error %@", fromPath, toPath, res ); + + AuthorizationFree(auth, 0); + auth = NULL; + } + + return; + } + + if (auth) { + AuthorizationFree(auth, 0); + auth = NULL; + } + + err = FSPathMakeRef((UInt8 *)[[toPath stringByDeletingLastPathComponent] fileSystemRepresentation], &dstDirRef, NULL); + + if (err != noErr) { + qDebug() << "GOT AN ERROR DOING FSPathMakeRef to get dir to copy into!!!!! aborting move"; + return; + } + + NSFileManager *manager = [[[NSFileManager alloc] init] autorelease]; + NSError* error; + BOOL success = [manager moveItemAtPath:fromPath toPath:toPath error:&error]; + if (!success) { + NSLog( @"Failed to do non-authenticated move! Help! %@", [[error userInfo] objectForKey: NSLocalizedDescriptionKey] ); + } + [self notifyDelegate:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:success], TKInstallerResultKey, [info objectForKey:TKInstallerDelegateKey], TKInstallerDelegateKey, error, TKInstallerErrorKey, nil]]; +} + + ++ (void)notifyDelegate:(NSDictionary *)info +{ + // *** GETS CALLED ON NON-MAIN THREAD! + BOOL result = [[info objectForKey:TKInstallerResultKey] boolValue]; + if (result) + { + if ([[info objectForKey:TKInstallerDelegateKey] respondsToSelector:@selector(moveFinished)]) + [[info objectForKey:TKInstallerDelegateKey] performSelectorOnMainThread: @selector(moveFinished) withObject:nil waitUntilDone: NO]; + } + else + { + if ([[info objectForKey:TKInstallerDelegateKey] respondsToSelector:@selector(moveFailedWithError:)]) + { + [[info objectForKey:TKInstallerDelegateKey] performSelectorOnMainThread: @selector(moveFailedWithError) withObject:[NSDictionary dictionaryWithObjectsAndKeys:[info objectForKey:TKInstallerErrorKey], TKInstallerErrorKey, nil] waitUntilDone: NO]; + } + } +} + +@end diff --git a/src/libtomahawk/utils/Closure.h b/src/libtomahawk/utils/Closure.h index f071c0e28..591712358 100644 --- a/src/libtomahawk/utils/Closure.h +++ b/src/libtomahawk/utils/Closure.h @@ -1,5 +1,6 @@ -/* This file is part of Clementine. +/* This file is part of Tomabawk. Copyright 2011, David Sansome + Copyright 2012, Leo Franchi Clementine is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/libtomahawk/utils/TomahawkUtils.cpp b/src/libtomahawk/utils/TomahawkUtils.cpp index f61bf924d..3099cd170 100644 --- a/src/libtomahawk/utils/TomahawkUtils.cpp +++ b/src/libtomahawk/utils/TomahawkUtils.cpp @@ -39,7 +39,10 @@ #include #include -#ifdef Q_WS_WIN +#include +#include + +#ifdef Q_OS_WIN #include #include #endif @@ -49,6 +52,10 @@ #include #endif +#ifdef QCA2_FOUND +#include +#endif + namespace TomahawkUtils { static quint64 s_infosystemRequestId = 0; @@ -676,4 +683,220 @@ SharedTimeLine::disconnectNotify( const char* signal ) } +bool +verifyFile( const QString &filePath, const QString &signature ) +{ + QCA::Initializer init; + + if( !QCA::isSupported( "sha1" ) ) + { + qWarning() << "SHA1 not supported by QCA, aborting."; + return false; + } + + // The signature for the resolver.zip was created like so: + // openssl dgst -sha1 -binary < "#{tarball}" | openssl dgst -dss1 -sign "#{ARGV[2]}" | openssl enc -base64 + // which means we need to decode it with QCA's DSA public key signature verification tools + // The input data is: + // file -> SHA1 binary format -> DSS1/DSA signed -> base64 encoded. + + // Step 1: Load the public key + // Public key is in :/data/misc/tomahawk_pubkey.pem + QFile f( ":/data/misc/tomahawk_pubkey.pem" ); + if ( !f.open( QIODevice::ReadOnly ) ) + { + qWarning() << "Unable to read public key from resources!"; + return false; + } + + const QString pubkeyData = QString::fromUtf8( f.readAll() ); + QCA::ConvertResult conversionResult; + QCA::PublicKey publicKey = QCA::PublicKey::fromPEM( pubkeyData, &conversionResult ); + if ( QCA::ConvertGood != conversionResult) + { + qWarning() << "Public key reading/loading failed! Tried to load public key:" << pubkeyData; + return false; + } + + if ( !publicKey.canVerify() ) + { + qWarning() << "Loaded Tomahawk public key but cannot use it to verify! What is up...."; + return false; + } + + // Step 2: Get the SHA1 of the file contents + QFile toVerify( filePath ); + if ( !toVerify.exists() || !toVerify.open( QIODevice::ReadOnly ) ) + { + qWarning() << "Failed to open file we are trying to verify!" << filePath; + return false; + } + + const QByteArray fileHashData = QCA::Hash( "sha1" ).hash( toVerify.readAll() ).toByteArray(); + toVerify.close(); + + // Step 3: Base64 decode the signature + QCA::Base64 decoder( QCA::Decode ); + const QByteArray decodedSignature = decoder.decode( QCA::SecureArray( signature.trimmed().toUtf8() ) ).toByteArray(); + if ( decodedSignature.isEmpty() ) + { + qWarning() << "Got empty signature after we tried to decode it from Base64:" << signature.trimmed().toUtf8() << decodedSignature.toBase64(); + return false; + } + + // Step 4: Do the actual verifying! + const bool result = publicKey.verifyMessage( fileHashData, decodedSignature, QCA::EMSA1_SHA1, QCA::DERSequence ); + if ( !result ) + { + qWarning() << "File" << filePath << "FAILED VERIFICATION against our input signature!"; + return false; + } + + qDebug() << "Successfully verified signature of downloaded file:" << filePath; + + return true; +} + + +QString +extractScriptPayload( const QString& filename, const QString& resolverId ) +{ + // uses QuaZip to extract the temporary zip file to the user's tomahawk data/resolvers directory + QDir resolverDir = appDataDir(); + if ( !resolverDir.mkpath( QString( "atticaresolvers/%1" ).arg( resolverId ) ) ) + { + tLog() << "Failed to mkdir resolver save dir: " << TomahawkUtils::appDataDir().absoluteFilePath( QString( "atticaresolvers/%1" ).arg( resolverId ) ); + return QString(); + } + resolverDir.cd( QString( "atticaresolvers/%1" ).arg( resolverId ) ); + + + if ( !unzipFileInFolder( filename, resolverDir ) ) + { + qWarning() << "Failed to unzip resolver. Ooops."; + return QString(); + } + + return resolverDir.absolutePath(); +} + + +bool +unzipFileInFolder( const QString &zipFileName, const QDir &folder ) +{ + Q_ASSERT( !zipFileName.isEmpty() ); + Q_ASSERT( folder.exists() ); + + QuaZip zipFile( zipFileName ); + if ( !zipFile.open( QuaZip::mdUnzip ) ) + { + qWarning() << "Failed to QuaZip open:" << zipFile.getZipError(); + return false; + } + + if ( !zipFile.goToFirstFile() ) + { + tLog() << "Failed to go to first file in zip archive: " << zipFile.getZipError(); + return false; + } + + tDebug() << "Unzipping files to:" << folder.absolutePath(); + + QuaZipFile fileInZip( &zipFile ); + do + { + QuaZipFileInfo info; + zipFile.getCurrentFileInfo( &info ); + + if ( !fileInZip.open( QIODevice::ReadOnly ) ) + { + tLog() << "Failed to open file inside zip archive:" << info.name << zipFile.getZipName() << "with error:" << zipFile.getZipError(); + continue; + } + + QFile out( folder.absoluteFilePath( fileInZip.getActualFileName() ) ); + + // make dir if there is one needed + QStringList parts = fileInZip.getActualFileName().split( "/" ); + if ( parts.size() > 1 ) + { + QStringList dirs = parts.mid( 0, parts.size() - 1 ); + QString dirPath = dirs.join( "/" ); // QDir translates / to \ internally if necessary + folder.mkpath( dirPath ); + } + + tDebug() << "Writing to output file..." << out.fileName(); + if ( !out.open( QIODevice::WriteOnly ) ) + { + tLog() << "Failed to open zip extract file:" << out.errorString() << info.name; + continue; + } + + + out.write( fileInZip.readAll() ); + out.close(); + fileInZip.close(); + + } while ( zipFile.goToNextFile() ); + + return true; +} + + +void +extractBinaryResolver( const QString& zipFilename, QObject* receiver ) +{ +#if !defined(Q_OS_MAC) && !defined (Q_OS_WIN) + Q_ASSERT( false ); + qWarning() << "NO SUPPORT YET FOR LINUX BINARY RESOLVERS!"; + return; +#endif + +#ifdef Q_OS_MAC + // Platform-specific handling of resolver payload now. We know it's good + // Unzip the file. + QFileInfo info( zipFilename ); + QDir tmpDir = QDir::tempPath(); + if ( !tmpDir.mkdir( info.baseName() ) ) + { + qWarning() << "Failed to create temporary directory to unzip in:" << tmpDir.absolutePath(); + return; + } + tmpDir.cd( info.baseName() ); + TomahawkUtils::unzipFileInFolder( info.absoluteFilePath(), tmpDir ); + + // On OSX it just contains 1 file, the resolver executable itself. For now. We just copy it to + // the Tomahawk.app/Contents/MacOS/ folder alongside the Tomahawk executable. + const QString dest = QCoreApplication::applicationDirPath(); + // Find the filename + const QDir toList( tmpDir.absolutePath() ); + const QStringList files = toList.entryList( QStringList(), QDir::Files ); + Q_ASSERT( files.size() == 1 ); + + const QString src = toList.absoluteFilePath( files.first() ); + qDebug() << "OS X: Copying binary resolver from to:" << src << dest; + + copyWithAuthentication( src, dest, receiver ); +#elif defined(Q_OS_WIN) + // We unzip directly to the target location, just like normal attica resolvers + Q_ASSERT( receiver ); + if ( !receiver ) + return; + + const QString resolverId = receiver->property( "resolverid" ).toString(); + + Q_ASSERT( !resolverId.isEmpty() ); + if ( resolverId.isEmpty() ) + return; + + const QString resolverDir = extractScriptPayload( zipFilename, resolverId ); + QMetaObject::invokeMethod(receiver, "installSucceeded", Qt::DirectConnection, Q_ARG( QString, path ) ); + +#endif + + // No support for binary resolvers on linux! Shouldn't even have been allowed to see/install.. + Q_ASSERT( false ); +} + + } // ns diff --git a/src/libtomahawk/utils/TomahawkUtils.h b/src/libtomahawk/utils/TomahawkUtils.h index b2211a488..27350e3e1 100644 --- a/src/libtomahawk/utils/TomahawkUtils.h +++ b/src/libtomahawk/utils/TomahawkUtils.h @@ -115,7 +115,7 @@ namespace TomahawkUtils QStringList m_noProxyHosts; QNetworkProxy m_proxy; }; - + DLLEXPORT QString appFriendlyVersion(); @@ -138,7 +138,19 @@ namespace TomahawkUtils DLLEXPORT QString md5( const QByteArray& data ); DLLEXPORT bool removeDirectory( const QString& dir ); - + + DLLEXPORT bool verifyFile( const QString& filePath, const QString& signature ); + DLLEXPORT QString extractScriptPayload( const QString& filename, const QString& resolverId ); + DLLEXPORT bool unzipFileInFolder( const QString& zipFileName, const QDir& folder ); + + + // Extracting may be asynchronous, pass in a receiver object with the following slots: + // extractSucceeded( const QString& path ) and extractFailed() to be notified/ + DLLEXPORT void extractBinaryResolver( const QString& zipFilename, QObject* receiver ); + + // Used by the above, not exported + void copyWithAuthentication( const QString& srcFile, const QDir dest, QObject* receiver ); + /** * This helper is designed to help "update" an existing playlist with a newer revision of itself. * To avoid re-loading the whole playlist and re-resolving tracks that are the same in the old playlist, diff --git a/src/libtomahawk/utils/TomahawkUtils_Mac.h b/src/libtomahawk/utils/TomahawkUtils_Mac.h new file mode 100644 index 000000000..635db4117 --- /dev/null +++ b/src/libtomahawk/utils/TomahawkUtils_Mac.h @@ -0,0 +1,37 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi . + */ + +#ifndef TOMAHAWKUTILS_MAC_H +#define TOMAHAWKUTILS_MAC_H + +#include + +#import "mac/FileHelpers.h" + +@interface MoveDelegate : NSObject +{ + QObject* receiver; + QString path; +} +- (void)setReceiver:(QObject*)receiver; +- (void)setMoveTo:(QString)path; +- (void)moveFinished; +- (void)moveFailedWithError:(NSError *)error; +@end + +#endif // TOMAHAWKUTILS_MAC_H diff --git a/src/libtomahawk/utils/TomahawkUtils_Mac.mm b/src/libtomahawk/utils/TomahawkUtils_Mac.mm index aeac94866..fc5c3d08c 100644 --- a/src/libtomahawk/utils/TomahawkUtils_Mac.mm +++ b/src/libtomahawk/utils/TomahawkUtils_Mac.mm @@ -1,6 +1,75 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi . + */ + #include "TomahawkUtils.h" +#include "TomahawkUtils_Mac.h" +#include "mac/FileHelpers.h" + +#include +#include + #import +#import + +@implementation MoveDelegate + + +-(void) setReceiver:(QObject*) object +{ + receiver = object; +} + +-(void) setMoveTo:(QString) p +{ + path = p; +} + +- (void)moveFinished +{ + // HACK since I can't figure out how to get QuaZip to maintain executable permissions after unzip (nor find the info) + // we set the binary to executable here + + NSLog(@"Move succeeded!, handling result"); + + NSFileManager *manager = [[[NSFileManager alloc] init] autorelease]; + NSError* error; + NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:0755], NSFilePosixPermissions, nil]; + + NSString* target = [[NSString alloc] initWithBytes:path.toUtf8() length:path.length() encoding: NSUTF8StringEncoding]; + NSLog(@"Changing permissions to executable for: %@", target); + BOOL success = [manager setAttributes:attrs ofItemAtPath:target error:&error]; + if (!success) { + NSLog( @"Failed to do chmod +x of moved resolver! %@", [[error userInfo] objectForKey: NSLocalizedDescriptionKey] ); + } + + if ( receiver ) + QMetaObject::invokeMethod(receiver, "installSucceeded", Qt::DirectConnection, Q_ARG(QString, path)); + +} + +- (void)moveFailedWithError:(NSError *)error +{ + NSLog(@"Move failed, handling result"); + if ( receiver ) + QMetaObject::invokeMethod(receiver, "installFailed", Qt::DirectConnection); +} +@end namespace TomahawkUtils { @@ -10,4 +79,31 @@ bringToFront() { [NSApp activateIgnoringOtherApps:YES]; } +void +copyWithAuthentication( const QString& srcFile, const QDir dest, QObject* receiver ) +{ + /** + On OS X, we have to do the following: + 1) Authenticate to be able to have write access to the /Applications folder + 2) Copy file to dest + 5) Call result slots on receiver object + */ + + MoveDelegate* del = [[MoveDelegate alloc] init]; + [del setReceiver: receiver]; + + // Get the filename + path to save for later + QFileInfo srcInfo( srcFile ); + const QString resultingPath = dest.absoluteFilePath( srcInfo.fileName() ); + [del setMoveTo: resultingPath]; + + const QFileInfo info( srcFile ); + const QString destPath = dest.absoluteFilePath( info.fileName() ); + + NSString* src = [[NSString alloc] initWithBytes: srcFile.toUtf8() length: srcFile.length() encoding: NSUTF8StringEncoding]; + NSString* destStr = [[NSString alloc] initWithBytes: destPath.toUtf8() length: destPath.length() encoding: NSUTF8StringEncoding]; + [FileHelpers moveFile:src to:destStr withDelegate:del]; +} + + } diff --git a/src/main.cpp b/src/main.cpp index fe0332d1f..2c786bf55 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -121,7 +121,7 @@ int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, int main( int argc, char *argv[] ) { - #ifdef Q_WS_MAC +#ifdef Q_WS_MAC // Do Mac specific startup to get media keys working. // This must go before QApplication initialisation. Tomahawk::macMain();