Add support for Box.

This commit is contained in:
John Maguire 2013-02-12 13:54:19 +01:00
parent 91233dd8d1
commit d21fa8cc67
19 changed files with 742 additions and 5 deletions

View File

@ -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
)

View File

@ -273,6 +273,7 @@
<file>providers/amazon.png</file>
<file>providers/aol.png</file>
<file>providers/bbc.png</file>
<file>providers/box.png</file>
<file>providers/cdbaby.png</file>
<file>providers/digitallyimported-32.png</file>
<file>providers/digitallyimported.png</file>
@ -341,6 +342,7 @@
<file>schema/schema-41.sql</file>
<file>schema/schema-42.sql</file>
<file>schema/schema-43.sql</file>
<file>schema/schema-44.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-6.sql</file>

BIN
data/providers/box.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

48
data/schema/schema-44.sql Normal file
View File

@ -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;

View File

@ -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

View File

@ -22,6 +22,7 @@
#cmakedefine ENABLE_VISUALISATIONS
#cmakedefine HAVE_AUDIOCD
#cmakedefine HAVE_BOX
#cmakedefine HAVE_BREAKPAD
#cmakedefine HAVE_DBUS
#cmakedefine HAVE_DEVICEKIT

View File

@ -37,7 +37,7 @@
#include <QVariant>
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;

312
src/internet/boxservice.cpp Normal file
View File

@ -0,0 +1,312 @@
#include "boxservice.h"
#include <qjson/parser.h>
#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;
}

55
src/internet/boxservice.h Normal file
View File

@ -0,0 +1,55 @@
#ifndef BOXSERVICE_H
#define BOXSERVICE_H
#include "cloudfileservice.h"
#include <QDateTime>
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

View File

@ -0,0 +1,89 @@
/* This file is part of Clementine.
Copyright 2013, John Maguire <john.maguire@gmail.com>
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 <http://www.gnu.org/licenses/>.
*/
#include "boxsettingspage.h"
#include <QSortFilterProxyModel>
#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<BoxService>())
{
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);
}

View File

@ -0,0 +1,53 @@
/* This file is part of Clementine.
Copyright 2013, John Maguire <john.maguire@gmail.com>
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 <http://www.gnu.org/licenses/>.
*/
#ifndef BOXSETTINGSPAGE_H
#define BOXSETTINGSPAGE_H
#include "ui/settingspage.h"
#include <QModelIndex>
#include <QWidget>
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

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BoxSettingsPage</class>
<widget class="QWidget" name="BoxSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>569</width>
<height>491</height>
</rect>
</property>
<property name="windowTitle">
<string>Box</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/providers/box.png</normaloff>:/providers/box.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Clementine can play music that you have uploaded to Box</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QWidget" name="login_container" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>28</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="login_button">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Clicking the Login button will open a web browser. You should return to Clementine after you have logged in.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>357</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -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);
}

View File

@ -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

View File

@ -3,8 +3,6 @@
#include "cloudfileservice.h"
#include "core/tagreaderclient.h"
namespace google_drive {
class Client;
class ConnectResponse;

View File

@ -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) {

View File

@ -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();
}

View File

@ -74,6 +74,10 @@
# include "internet/dropboxsettingspage.h"
#endif
#ifdef HAVE_BOX
# include "internet/boxsettingspage.h"
#endif
#include <QDesktopWidget>
#include <QPainter>
#include <QPushButton>
@ -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

View File

@ -82,6 +82,7 @@ public:
Page_UbuntuOne,
Page_Dropbox,
Page_Skydrive,
Page_Box,
};
enum Role {