From d21fa8cc67c3d34fbf68c179b227ec435ceadcb5 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 12 Feb 2013 13:54:19 +0100 Subject: [PATCH] Add support for Box. --- CMakeLists.txt | 5 + data/data.qrc | 2 + data/providers/box.png | Bin 0 -> 4562 bytes data/schema/schema-44.sql | 48 +++++ src/CMakeLists.txt | 14 ++ src/config.h.in | 1 + src/core/database.cpp | 2 +- src/internet/boxservice.cpp | 312 ++++++++++++++++++++++++++++++ src/internet/boxservice.h | 55 ++++++ src/internet/boxsettingspage.cpp | 89 +++++++++ src/internet/boxsettingspage.h | 53 +++++ src/internet/boxsettingspage.ui | 110 +++++++++++ src/internet/boxurlhandler.cpp | 14 ++ src/internet/boxurlhandler.h | 21 ++ src/internet/googledriveservice.h | 2 - src/internet/internetmodel.cpp | 6 + src/internet/oauthenticator.cpp | 4 +- src/ui/settingsdialog.cpp | 8 + src/ui/settingsdialog.h | 1 + 19 files changed, 742 insertions(+), 5 deletions(-) create mode 100644 data/providers/box.png create mode 100644 data/schema/schema-44.sql create mode 100644 src/internet/boxservice.cpp create mode 100644 src/internet/boxservice.h create mode 100644 src/internet/boxsettingspage.cpp create mode 100644 src/internet/boxsettingspage.h create mode 100644 src/internet/boxsettingspage.ui create mode 100644 src/internet/boxurlhandler.cpp create mode 100644 src/internet/boxurlhandler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 97e6c5f21..70f9e6102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,6 +214,11 @@ optional_component(SKYDRIVE ON "Skydrive support" DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999" ) +optional_component(BOX ON "Box support" + DEPENDS "Google sparsehash" SPARSEHASH_INCLUDE_DIRS + DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999" +) + optional_component(AUDIOCD ON "Devices: Audio CD support" DEPENDS "libcdio" CDIO_FOUND ) diff --git a/data/data.qrc b/data/data.qrc index 50b68b300..0269dd706 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -273,6 +273,7 @@ providers/amazon.png providers/aol.png providers/bbc.png + providers/box.png providers/cdbaby.png providers/digitallyimported-32.png providers/digitallyimported.png @@ -341,6 +342,7 @@ schema/schema-41.sql schema/schema-42.sql schema/schema-43.sql + schema/schema-44.sql schema/schema-4.sql schema/schema-5.sql schema/schema-6.sql diff --git a/data/providers/box.png b/data/providers/box.png new file mode 100644 index 0000000000000000000000000000000000000000..bff20dc3022065cabfee12aadb1aee62f779360d GIT binary patch literal 4562 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 literal 0 HcmV?d00001 diff --git a/data/schema/schema-44.sql b/data/schema/schema-44.sql new file mode 100644 index 000000000..89a92b8ca --- /dev/null +++ b/data/schema/schema-44.sql @@ -0,0 +1,48 @@ +CREATE TABLE box_songs( + title TEXT, + album TEXT, + artist TEXT, + albumartist TEXT, + composer TEXT, + track INTEGER, + disc INTEGER, + bpm REAL, + year INTEGER, + genre TEXT, + comment TEXT, + compilation INTEGER, + + length INTEGER, + bitrate INTEGER, + samplerate INTEGER, + + directory INTEGER NOT NULL, + filename TEXT NOT NULL, + mtime INTEGER NOT NULL, + ctime INTEGER NOT NULL, + filesize INTEGER NOT NULL, + sampler INTEGER NOT NULL DEFAULT 0, + art_automatic TEXT, + art_manual TEXT, + filetype INTEGER NOT NULL DEFAULT 0, + playcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER, + rating INTEGER, + forced_compilation_on INTEGER NOT NULL DEFAULT 0, + forced_compilation_off INTEGER NOT NULL DEFAULT 0, + effective_compilation NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + score INTEGER NOT NULL DEFAULT 0, + beginning INTEGER NOT NULL DEFAULT 0, + cue_path TEXT, + unavailable INTEGER DEFAULT 0, + effective_albumartist TEXT, + etag TEXT +); + +CREATE VIRTUAL TABLE box_songs_fts USING fts3 ( + ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment, + tokenize=unicode +); + +UPDATE schema_version SET version=44; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 72dc7e2a8..f1dca0ace 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1146,6 +1146,20 @@ optional_source(HAVE_SKYDRIVE internet/skydriveurlhandler.h ) +# Box support +optional_source(HAVE_BOX + SOURCES + internet/boxservice.cpp + internet/boxsettingspage.cpp + internet/boxurlhandler.cpp + HEADERS + internet/boxservice.h + internet/boxsettingspage.h + internet/boxurlhandler.h + UI + internet/boxsettingspage.ui +) + # Hack to add Clementine to the Unity system tray whitelist optional_source(LINUX SOURCES core/ubuntuunityhack.cpp diff --git a/src/config.h.in b/src/config.h.in index f84526c67..8088eec90 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -22,6 +22,7 @@ #cmakedefine ENABLE_VISUALISATIONS #cmakedefine HAVE_AUDIOCD +#cmakedefine HAVE_BOX #cmakedefine HAVE_BREAKPAD #cmakedefine HAVE_DBUS #cmakedefine HAVE_DEVICEKIT diff --git a/src/core/database.cpp b/src/core/database.cpp index 9eef7c8b1..ccbabe394 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -37,7 +37,7 @@ #include const char* Database::kDatabaseFilename = "clementine.db"; -const int Database::kSchemaVersion = 43; +const int Database::kSchemaVersion = 44; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/internet/boxservice.cpp b/src/internet/boxservice.cpp new file mode 100644 index 000000000..f4ec9af9a --- /dev/null +++ b/src/internet/boxservice.cpp @@ -0,0 +1,312 @@ +#include "boxservice.h" + +#include + +#include "core/application.h" +#include "core/player.h" +#include "core/waitforsignal.h" +#include "internet/boxurlhandler.h" +#include "internet/oauthenticator.h" +#include "library/librarybackend.h" + +const char* BoxService::kServiceName = "Box"; +const char* BoxService::kSettingsGroup = "Box"; + +namespace { + +static const char* kClientId = "gbswb9wp7gjyldc3qrw68h2rk68jaf4h"; +static const char* kClientSecret = "pZ6cUCQz5X0xaWoPVbCDg6GpmfTtz73s"; + +static const char* kOAuthEndpoint = + "https://api.box.com/oauth2/authorize"; +static const char* kOAuthTokenEndpoint = + "https://api.box.com/oauth2/token"; + +static const char* kUserInfo = + "https://api.box.com/2.0/users/me"; +static const char* kFolderItems = + "https://api.box.com/2.0/folders/%1/items"; +static const int kRootFolderId = 0; + +static const char* kFileContent = + "https://api.box.com/2.0/files/%1/content"; + +static const char* kEvents = + "https://api.box.com/2.0/events"; + +} + +BoxService::BoxService(Application* app, InternetModel* parent) + : CloudFileService( + app, parent, + kServiceName, kSettingsGroup, + QIcon(":/providers/box.png"), + SettingsDialog::Page_Box) { + app->player()->RegisterUrlHandler(new BoxUrlHandler(this, this)); +} + +bool BoxService::has_credentials() const { + return !refresh_token().isEmpty(); +} + +QString BoxService::refresh_token() const { + QSettings s; + s.beginGroup(kSettingsGroup); + + return s.value("refresh_token").toString(); +} + +bool BoxService::is_authenticated() const { + return !access_token_.isEmpty() && + QDateTime::currentDateTime().secsTo(expiry_time_) > 0; +} + +void BoxService::EnsureConnected() { + if (is_authenticated()) { + return; + } + + Connect(); + WaitForSignal(this, SIGNAL(Connected())); +} + +void BoxService::Connect() { + OAuthenticator* oauth = new OAuthenticator( + kClientId, kClientSecret, OAuthenticator::RedirectStyle::LOCALHOST, this); + if (!refresh_token().isEmpty()) { + oauth->RefreshAuthorisation( + kOAuthTokenEndpoint, refresh_token()); + } else { + oauth->StartAuthorisation( + kOAuthEndpoint, + kOAuthTokenEndpoint, + QString::null); + } + + NewClosure(oauth, SIGNAL(Finished()), + this, SLOT(ConnectFinished(OAuthenticator*)), oauth); +} + +void BoxService::ConnectFinished(OAuthenticator* oauth) { + oauth->deleteLater(); + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("refresh_token", oauth->refresh_token()); + + access_token_ = oauth->access_token(); + expiry_time_ = oauth->expiry_time(); + + if (s.value("name").toString().isEmpty()) { + QUrl url(kUserInfo); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + + QNetworkReply* reply = network_->get(request); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(FetchUserInfoFinished(QNetworkReply*)), reply); + } else { + emit Connected(); + } + UpdateFiles(); +} + +void BoxService::AddAuthorizationHeader(QNetworkRequest* request) const { + request->setRawHeader( + "Authorization", QString("Bearer %1").arg(access_token_).toUtf8()); +} + +void BoxService::FetchUserInfoFinished(QNetworkReply* reply) { + reply->deleteLater(); + + QJson::Parser parser; + QVariantMap response = parser.parse(reply).toMap(); + + QString name = response["name"].toString(); + if (!name.isEmpty()) { + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("name", name); + } + + emit Connected(); +} + +void BoxService::ForgetCredentials() { + QSettings s; + s.beginGroup(kSettingsGroup); + + s.remove("refresh_token"); + s.remove("name"); +} + +void BoxService::UpdateFiles() { + QSettings s; + s.beginGroup(kSettingsGroup); + + if (!s.value("cursor").toString().isEmpty()) { + // Use events API to fetch changes. + UpdateFilesFromCursor(s.value("cursor").toString()); + return; + } + + // First run we scan as events may not cover everything. + FetchRecursiveFolderItems(kRootFolderId); + InitialiseEventsCursor(); +} + +void BoxService::InitialiseEventsCursor() { + QUrl url(kEvents); + url.addQueryItem("stream_position", "now"); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(InitialiseEventsFinished(QNetworkReply*)), reply); +} + +void BoxService::InitialiseEventsFinished(QNetworkReply* reply) { + reply->deleteLater(); + QJson::Parser parser; + QVariantMap response = parser.parse(reply).toMap(); + if (response.contains("next_stream_position")) { + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("cursor", response["next_stream_position"]); + } +} + +void BoxService::FetchRecursiveFolderItems(const int folder_id) { + // TODO: Page through large folders. + QUrl url(QString(kFolderItems).arg(folder_id)); + QStringList fields; + fields << "etag" + << "size" + << "created_at" + << "modified_at" + << "name"; + QString fields_list = fields.join(","); + url.addQueryItem("fields", fields_list); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(FetchFolderItemsFinished(QNetworkReply*)), reply); +} + +void BoxService::FetchFolderItemsFinished(QNetworkReply* reply) { + reply->deleteLater(); + + QByteArray data = reply->readAll(); + + QJson::Parser parser; + QVariantMap response = parser.parse(data).toMap(); + + QVariantList entries = response["entries"].toList(); + foreach (const QVariant& e, entries) { + QVariantMap entry = e.toMap(); + if (entry["type"].toString() == "folder") { + FetchRecursiveFolderItems(entry["id"].toInt()); + } else { + MaybeAddFileEntry(entry); + } + } +} + +void BoxService::MaybeAddFileEntry(const QVariantMap& entry) { + QString mime_type = GuessMimeTypeForFile(entry["name"].toString()); + QUrl url; + url.setScheme("box"); + url.setPath(entry["id"].toString()); + + Song song; + song.set_url(url); + song.set_ctime(entry["created_at"].toDateTime().toTime_t()); + song.set_mtime(entry["modified_at"].toDateTime().toTime_t()); + song.set_filesize(entry["size"].toInt()); + song.set_title(entry["name"].toString()); + + // This is actually a redirect. Follow it now. + QNetworkReply* reply = FetchContentUrlForFile(entry["id"].toString()); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(RedirectFollowed(QNetworkReply*, Song, QString)), + reply, song, mime_type); +} + +QNetworkReply* BoxService::FetchContentUrlForFile(const QString& file_id) { + QUrl content_url(QString(kFileContent).arg(file_id)); + QNetworkRequest request(content_url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + return reply; +} + +void BoxService::RedirectFollowed( + QNetworkReply* reply, const Song& song, const QString& mime_type) { + reply->deleteLater(); + QVariant redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + if (!redirect.isValid()) { + return; + } + + QUrl real_url = redirect.toUrl(); + MaybeAddFileToDatabase( + song, + mime_type, + real_url, + QString("Bearer %1").arg(access_token_)); +} + +void BoxService::UpdateFilesFromCursor(const QString& cursor) { + QUrl url(kEvents); + url.addQueryItem("stream_position", cursor); + url.addQueryItem("limit", "5000"); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(FetchEventsFinished(QNetworkReply*)), reply); +} + +void BoxService::FetchEventsFinished(QNetworkReply* reply) { + // TODO: Page through events. + reply->deleteLater(); + QJson::Parser parser; + QVariantMap response = parser.parse(reply).toMap(); + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("cursor", response["next_stream_position"]); + + QVariantList entries = response["entries"].toList(); + foreach (const QVariant& e, entries) { + QVariantMap event = e.toMap(); + QString type = event["event_type"].toString(); + QVariantMap source = event["source"].toMap(); + if (source["type"] == "file") { + if (type == "ITEM_UPLOAD") { + // Add file. + MaybeAddFileEntry(source); + } else if (type == "ITEM_TRASH") { + // Delete file. + QUrl url; + url.setScheme("box"); + url.setPath(source["id"].toString()); + Song song = library_backend_->GetSongByUrl(url); + if (song.is_valid()) { + library_backend_->DeleteSongs(SongList() << song); + } + } + } + } +} + +QUrl BoxService::GetStreamingUrlFromSongId(const QString& id) { + EnsureConnected(); + QNetworkReply* reply = FetchContentUrlForFile(id); + WaitForSignal(reply, SIGNAL(finished())); + reply->deleteLater(); + QUrl real_url = + reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + return real_url; +} diff --git a/src/internet/boxservice.h b/src/internet/boxservice.h new file mode 100644 index 000000000..97583ac30 --- /dev/null +++ b/src/internet/boxservice.h @@ -0,0 +1,55 @@ +#ifndef BOXSERVICE_H +#define BOXSERVICE_H + +#include "cloudfileservice.h" + +#include + +class OAuthenticator; +class QNetworkReply; +class QNetworkRequest; + +class BoxService : public CloudFileService { + Q_OBJECT + public: + BoxService(Application* app, InternetModel* parent); + + static const char* kServiceName; + static const char* kSettingsGroup; + + virtual bool has_credentials() const; + QUrl GetStreamingUrlFromSongId(const QString& id); + + public slots: + void Connect(); + void ForgetCredentials(); + + signals: + void Connected(); + + private slots: + void ConnectFinished(OAuthenticator* oauth); + void FetchUserInfoFinished(QNetworkReply* reply); + void FetchFolderItemsFinished(QNetworkReply* reply); + void RedirectFollowed( + QNetworkReply* reply, const Song& song, const QString& mime_type); + void InitialiseEventsFinished(QNetworkReply* reply); + void FetchEventsFinished(QNetworkReply* reply); + + private: + QString refresh_token() const; + bool is_authenticated() const; + void AddAuthorizationHeader(QNetworkRequest* request) const; + void UpdateFiles(); + void FetchRecursiveFolderItems(const int folder_id); + void UpdateFilesFromCursor(const QString& cursor); + QNetworkReply* FetchContentUrlForFile(const QString& file_id); + void InitialiseEventsCursor(); + void MaybeAddFileEntry(const QVariantMap& entry); + void EnsureConnected(); + + QString access_token_; + QDateTime expiry_time_; +}; + +#endif // BOXSERVICE_H diff --git a/src/internet/boxsettingspage.cpp b/src/internet/boxsettingspage.cpp new file mode 100644 index 000000000..3874829a5 --- /dev/null +++ b/src/internet/boxsettingspage.cpp @@ -0,0 +1,89 @@ +/* This file is part of Clementine. + Copyright 2013, John Maguire + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#include "boxsettingspage.h" + +#include + +#include "ui_boxsettingspage.h" +#include "core/application.h" +#include "internet/boxservice.h" +#include "internet/internetmodel.h" +#include "ui/settingsdialog.h" + +BoxSettingsPage::BoxSettingsPage(SettingsDialog* parent) + : SettingsPage(parent), + ui_(new Ui::BoxSettingsPage), + service_(dialog()->app()->internet_model()->Service()) +{ + ui_->setupUi(this); + ui_->login_state->AddCredentialGroup(ui_->login_container); + + connect(ui_->login_button, SIGNAL(clicked()), SLOT(LoginClicked())); + connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); + connect(service_, SIGNAL(Connected()), SLOT(Connected())); + + dialog()->installEventFilter(this); +} + +BoxSettingsPage::~BoxSettingsPage() { + delete ui_; +} + +void BoxSettingsPage::Load() { + QSettings s; + s.beginGroup(BoxService::kSettingsGroup); + + const QString name = s.value("name").toString(); + + if (!name.isEmpty()) { + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn, name); + } +} + +void BoxSettingsPage::Save() { + QSettings s; + s.beginGroup(BoxService::kSettingsGroup); +} + +void BoxSettingsPage::LoginClicked() { + service_->Connect(); + ui_->login_button->setEnabled(false); +} + +bool BoxSettingsPage::eventFilter(QObject* object, QEvent* event) { + if (object == dialog() && event->type() == QEvent::Enter) { + ui_->login_button->setEnabled(true); + return false; + } + + return SettingsPage::eventFilter(object, event); +} + +void BoxSettingsPage::LogoutClicked() { + service_->ForgetCredentials(); + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); +} + +void BoxSettingsPage::Connected() { + QSettings s; + s.beginGroup(BoxService::kSettingsGroup); + + const QString name = s.value("name").toString(); + + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn, name); +} diff --git a/src/internet/boxsettingspage.h b/src/internet/boxsettingspage.h new file mode 100644 index 000000000..13b46c34f --- /dev/null +++ b/src/internet/boxsettingspage.h @@ -0,0 +1,53 @@ +/* This file is part of Clementine. + Copyright 2013, John Maguire + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#ifndef BOXSETTINGSPAGE_H +#define BOXSETTINGSPAGE_H + +#include "ui/settingspage.h" + +#include +#include + +class BoxService; +class Ui_BoxSettingsPage; + +class BoxSettingsPage : public SettingsPage { + Q_OBJECT + +public: + BoxSettingsPage(SettingsDialog* parent = 0); + ~BoxSettingsPage(); + + void Load(); + void Save(); + + // QObject + bool eventFilter(QObject* object, QEvent* event); + +private slots: + void LoginClicked(); + void LogoutClicked(); + void Connected(); + +private: + Ui_BoxSettingsPage* ui_; + + BoxService* service_; +}; + +#endif // BOXSETTINGSPAGE_H diff --git a/src/internet/boxsettingspage.ui b/src/internet/boxsettingspage.ui new file mode 100644 index 000000000..45352db8c --- /dev/null +++ b/src/internet/boxsettingspage.ui @@ -0,0 +1,110 @@ + + + BoxSettingsPage + + + + 0 + 0 + 569 + 491 + + + + Box + + + + :/providers/box.png:/providers/box.png + + + + + + Clementine can play music that you have uploaded to Box + + + true + + + + + + + + + + + 28 + + + 0 + + + 0 + + + + + + + Login + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Clicking the Login button will open a web browser. You should return to Clementine after you have logged in. + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 357 + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + + + +
diff --git a/src/internet/boxurlhandler.cpp b/src/internet/boxurlhandler.cpp new file mode 100644 index 000000000..d296e6ba6 --- /dev/null +++ b/src/internet/boxurlhandler.cpp @@ -0,0 +1,14 @@ +#include "boxurlhandler.h" + +#include "boxservice.h" + +BoxUrlHandler::BoxUrlHandler(BoxService* service, QObject* parent) + : UrlHandler(parent), + service_(service) { +} + +UrlHandler::LoadResult BoxUrlHandler::StartLoading(const QUrl& url) { + QString file_id = url.path(); + QUrl real_url = service_->GetStreamingUrlFromSongId(file_id); + return LoadResult(url, LoadResult::TrackAvailable, real_url); +} diff --git a/src/internet/boxurlhandler.h b/src/internet/boxurlhandler.h new file mode 100644 index 000000000..c004bbbca --- /dev/null +++ b/src/internet/boxurlhandler.h @@ -0,0 +1,21 @@ +#ifndef BOXURLHANDLER_H +#define BOXURLHANDLER_H + +#include "core/urlhandler.h" + +class BoxService; + +class BoxUrlHandler : public UrlHandler { + Q_OBJECT + public: + BoxUrlHandler(BoxService* service, QObject* parent = 0); + + QString scheme() const { return "box"; } + QIcon icon() const { return QIcon(":/providers/box.png"); } + LoadResult StartLoading(const QUrl& url); + + private: + BoxService* service_; +}; + +#endif // BOXURLHANDLER_H diff --git a/src/internet/googledriveservice.h b/src/internet/googledriveservice.h index 5c6ef6d2a..a5760902b 100644 --- a/src/internet/googledriveservice.h +++ b/src/internet/googledriveservice.h @@ -3,8 +3,6 @@ #include "cloudfileservice.h" -#include "core/tagreaderclient.h" - namespace google_drive { class Client; class ConnectResponse; diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index f2b418a7b..a4e10dc2d 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -56,6 +56,9 @@ #ifdef HAVE_SKYDRIVE #include "skydriveservice.h" #endif +#ifdef HAVE_BOX + #include "boxservice.h" +#endif using smart_playlists::Generator; using smart_playlists::GeneratorMimeData; @@ -106,6 +109,9 @@ InternetModel::InternetModel(Application* app, QObject* parent) #ifdef HAVE_SKYDRIVE AddService(new SkydriveService(app, this)); #endif +#ifdef HAVE_BOX + AddService(new BoxService(app, this)); +#endif } void InternetModel::AddService(InternetService *service) { diff --git a/src/internet/oauthenticator.cpp b/src/internet/oauthenticator.cpp index 42b161de6..b8b8129d8 100644 --- a/src/internet/oauthenticator.cpp +++ b/src/internet/oauthenticator.cpp @@ -139,7 +139,6 @@ void OAuthenticator::RefreshAuthorisation( params.append(QString("%1=%2").arg(p.first, QString(QUrl::toPercentEncoding(p.second)))); } QString post_data = params.join("&"); - qLog(Debug) << "Refresh post data:" << post_data; QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -152,7 +151,7 @@ void OAuthenticator::RefreshAuthorisation( void OAuthenticator::SetExpiryTime(int expires_in_seconds) { // Set the expiry time with two minutes' grace. expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in_seconds - 120); - qLog(Debug) << "Current Google Drive token expires at:" << expiry_time_; + qLog(Debug) << "Current oauth access token expires at:" << expiry_time_; } void OAuthenticator::RefreshAccessTokenFinished(QNetworkReply* reply) { @@ -162,6 +161,7 @@ void OAuthenticator::RefreshAccessTokenFinished(QNetworkReply* reply) { QVariantMap result = parser.parse(reply, &ok).toMap(); access_token_ = result["access_token"].toString(); + refresh_token_ = result["refresh_token"].toString(); SetExpiryTime(result["expires_in"].toInt()); emit Finished(); } diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index 5614e798d..cbfa9c8ed 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -74,6 +74,10 @@ # include "internet/dropboxsettingspage.h" #endif +#ifdef HAVE_BOX +# include "internet/boxsettingspage.h" +#endif + #include #include #include @@ -167,6 +171,10 @@ SettingsDialog::SettingsDialog(Application* app, BackgroundStreams* streams, QWi AddPage(Page_Dropbox, new DropboxSettingsPage(this), providers); #endif +#ifdef HAVE_BOX + AddPage(Page_Box, new BoxSettingsPage(this), providers); +#endif + #ifdef HAVE_SPOTIFY AddPage(Page_Spotify, new SpotifySettingsPage(this), providers); #endif diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h index c34619e76..a21e26546 100644 --- a/src/ui/settingsdialog.h +++ b/src/ui/settingsdialog.h @@ -82,6 +82,7 @@ public: Page_UbuntuOne, Page_Dropbox, Page_Skydrive, + Page_Box, }; enum Role {