Move some of the Google Drive bits out into a separate client class.

This commit is contained in:
David Sansome 2012-07-28 17:18:03 +01:00
parent 51631169fa
commit 165cec1e86
7 changed files with 381 additions and 84 deletions

View File

@ -999,9 +999,11 @@ optional_source(HAVE_MOODBAR
# Google Drive support
optional_source(HAVE_GOOGLE_DRIVE
SOURCES
internet/googledriveclient.cpp
internet/googledriveservice.cpp
internet/googledriveurlhandler.cpp
HEADERS
internet/googledriveclient.h
internet/googledriveservice.h
internet/googledriveurlhandler.h
)

View File

@ -0,0 +1,168 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.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 "googledriveclient.h"
#include "oauthenticator.h"
#include "core/closure.h"
#include "core/network.h"
#include <qjson/parser.h>
using namespace google_drive;
namespace {
static const char* kGoogleDriveFiles = "https://www.googleapis.com/drive/v2/files";
static const char* kGoogleDriveFile = "https://www.googleapis.com/drive/v2/files/%1";
}
ConnectResponse::ConnectResponse(QObject* parent)
: QObject(parent)
{
}
ListFilesResponse::ListFilesResponse(const QString& query, QObject* parent)
: QObject(parent),
query_(query)
{
}
GetFileResponse::GetFileResponse(const QString& file_id, QObject* parent)
: QObject(parent),
file_id_(file_id)
{
}
Client::Client(QObject* parent)
: QObject(parent),
network_(new NetworkAccessManager(this))
{
}
ConnectResponse* Client::Connect(const QString& refresh_token) {
ConnectResponse* ret = new ConnectResponse(this);
OAuthenticator* oauth = new OAuthenticator(this);
if (refresh_token.isEmpty()) {
oauth->StartAuthorisation();
} else {
oauth->RefreshAuthorisation(refresh_token);
}
NewClosure(oauth, SIGNAL(Finished()),
this, SLOT(ConnectFinished(ConnectResponse*,OAuthenticator*)),
ret, oauth);
return ret;
}
void Client::ConnectFinished(ConnectResponse* response, OAuthenticator* oauth) {
oauth->deleteLater();
access_token_ = oauth->access_token();
response->refresh_token_ = oauth->refresh_token();
emit response->Finished();
}
void Client::AddAuthorizationHeader(QNetworkRequest* request) const {
request->setRawHeader(
"Authorization", QString("Bearer %1").arg(access_token_).toUtf8());
}
ListFilesResponse* Client::ListFiles(const QString& query) {
ListFilesResponse* ret = new ListFilesResponse(query, this);
MakeListFilesRequest(ret);
return ret;
}
void Client::MakeListFilesRequest(ListFilesResponse* response, const QString& page_token) {
QUrl url = QUrl(kGoogleDriveFiles);
if (!response->query_.isEmpty()) {
url.addQueryItem("q", response->query_);
}
if (!page_token.isEmpty()) {
url.addQueryItem("pageToken", page_token);
}
QNetworkRequest request = QNetworkRequest(url);
AddAuthorizationHeader(&request);
QNetworkReply* reply = network_->get(request);
NewClosure(reply, SIGNAL(finished()),
this, SLOT(ListFilesFinished(ListFilesResponse*, QNetworkReply*)),
response, reply);
}
void Client::ListFilesFinished(ListFilesResponse* response, QNetworkReply* reply) {
reply->deleteLater();
// Parse the response
QJson::Parser parser;
bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap();
if (!ok) {
qLog(Error) << "Failed to request files from Google Drive";
emit response->Finished();
return;
}
// Emit the FilesFound signal for the files in the response.
FileList files;
foreach (const QVariant& v, result["items"].toList()) {
files << File(v.toMap());
}
emit response->FilesFound(files);
// Get the next page of results if there is one.
if (result.contains("nextPageToken")) {
MakeListFilesRequest(response, result["nextPageToken"].toString());
} else {
emit response->Finished();
}
}
GetFileResponse* Client::GetFile(const QString& file_id) {
GetFileResponse* ret = new GetFileResponse(file_id, this);
QString url = QString(kGoogleDriveFile).arg(file_id);
QNetworkRequest request = QNetworkRequest(url);
AddAuthorizationHeader(&request);
QNetworkReply* reply = network_->get(request);
NewClosure(reply, SIGNAL(finished()),
this, SLOT(GetFileFinished(GetFileResponse*,QNetworkReply*)),
ret, reply);
return ret;
}
void Client::GetFileFinished(GetFileResponse* response, QNetworkReply* reply) {
reply->deleteLater();
QJson::Parser parser;
bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap();
if (!ok) {
qLog(Error) << "Failed to fetch file with ID" << response->file_id_;
emit response->Finished();
return;
}
response->file_ = File(result);
emit response->Finished();
}

View File

@ -0,0 +1,145 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.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 GOOGLEDRIVECLIENT_H
#define GOOGLEDRIVECLIENT_H
#include <QDateTime>
#include <QList>
#include <QObject>
#include <QUrl>
#include <QVariantMap>
class OAuthenticator;
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkRequest;
namespace google_drive {
class Client;
// Holds the metadata for a file on Google Drive.
class File {
public:
File(const QVariantMap& data = QVariantMap()) : data_(data) {}
QString id() const { return data_["id"].toString(); }
QString etag() const { return data_["etag"].toString(); }
QString title() const { return data_["title"].toString(); }
long size() const { return data_["fileSize"].toUInt(); }
QUrl download_url() const { return data_["downloadUrl"].toUrl(); }
QDateTime modified_date() const {
return QDateTime::fromString(data_["modifiedDate"].toString(), Qt::ISODate);
}
QDateTime created_date() const {
return QDateTime::fromString(data_["createdDate"].toString(), Qt::ISODate);
}
private:
QVariantMap data_;
};
typedef QList<File> FileList;
class ConnectResponse : public QObject {
Q_OBJECT
friend class Client;
public:
const QString& refresh_token() const { return refresh_token_; }
signals:
void Finished();
private:
ConnectResponse(QObject* parent);
QString refresh_token_;
};
class ListFilesResponse : public QObject {
Q_OBJECT
friend class Client;
public:
const QString& query() const { return query_; }
signals:
void FilesFound(const QList<google_drive::File>& files);
void Finished();
private:
ListFilesResponse(const QString& query, QObject* parent);
QString query_;
};
class GetFileResponse : public QObject {
Q_OBJECT
friend class Client;
public:
const QString& file_id() const { return file_id_; }
const File& file() const { return file_; }
signals:
void Finished();
private:
GetFileResponse(const QString& file_id, QObject* parent);
QString file_id_;
File file_;
};
class Client : public QObject {
Q_OBJECT
public:
Client(QObject* parent = 0);
bool is_authenticated() const { return !access_token_.isEmpty(); }
const QString& access_token() const { return access_token_; }
ConnectResponse* Connect(const QString& refresh_token = QString());
ListFilesResponse* ListFiles(const QString& query);
GetFileResponse* GetFile(const QString& file_id);
private slots:
void ConnectFinished(ConnectResponse* response, OAuthenticator* oauth);
void ListFilesFinished(ListFilesResponse* response, QNetworkReply* reply);
void GetFileFinished(GetFileResponse* response, QNetworkReply* reply);
private:
void AddAuthorizationHeader(QNetworkRequest* request) const;
void MakeListFilesRequest(ListFilesResponse* response,
const QString& page_token = QString());
private:
QNetworkAccessManager* network_;
QString access_token_;
};
} // namespace
#endif // GOOGLEDRIVECLIENT_H

View File

@ -1,10 +1,9 @@
#include "googledriveservice.h"
#include <QEventLoop>
#include <QScopedPointer>
#include <QSortFilterProxyModel>
#include <qjson/parser.h>
#include <google/sparsetable>
#include <taglib/id3v2framefactory.h>
@ -22,14 +21,12 @@ using TagLib::ByteVector;
#include "globalsearch/librarysearchprovider.h"
#include "library/librarybackend.h"
#include "library/librarymodel.h"
#include "googledriveclient.h"
#include "googledriveurlhandler.h"
#include "internetmodel.h"
#include "oauthenticator.h"
namespace {
static const char* kGoogleDriveFiles = "https://www.googleapis.com/drive/v2/files";
static const char* kGoogleDriveFile = "https://www.googleapis.com/drive/v2/files/%1";
static const char* kSettingsGroup = "GoogleDrive";
static const char* kSongsTable = "google_drive_songs";
@ -188,11 +185,8 @@ class DriveStream : public TagLib::IOStream {
GoogleDriveService::GoogleDriveService(Application* app, InternetModel* parent)
: InternetService("Google Drive", app, parent, parent),
root_(NULL),
oauth_(new OAuthenticator(this)),
client_(new google_drive::Client(this)),
library_sort_model_(new QSortFilterProxyModel(this)) {
connect(oauth_, SIGNAL(AccessTokenAvailable(QString)), SLOT(AccessTokenAvailable(QString)));
connect(oauth_, SIGNAL(RefreshTokenAvailable(QString)), SLOT(RefreshTokenAvailable(QString)));
library_backend_ = new LibraryBackend;
library_backend_->moveToThread(app_->database()->thread());
library_backend_->Init(app_->database(), kSongsTable,
@ -236,56 +230,43 @@ void GoogleDriveService::Connect() {
QSettings s;
s.beginGroup(kSettingsGroup);
if (s.contains("refresh_token")) {
QString refresh_token = s.value("refresh_token").toString();
RefreshAuthorisation(refresh_token);
} else {
oauth_->StartAuthorisation();
}
google_drive::ConnectResponse* response =
client_->Connect(s.value("refresh_token").toString());
NewClosure(response, SIGNAL(Finished()),
this, SLOT(ConnectFinished(google_drive::ConnectResponse*)),
response);
}
void GoogleDriveService::RefreshAuthorisation(const QString& refresh_token) {
oauth_->RefreshAuthorisation(refresh_token);
}
void GoogleDriveService::ConnectFinished(google_drive::ConnectResponse* response) {
response->deleteLater();
void GoogleDriveService::AccessTokenAvailable(const QString& token) {
access_token_ = token;
QUrl url = QUrl(kGoogleDriveFiles);
url.addQueryItem("q", "mimeType = 'audio/mpeg'");
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader(
"Authorization", QString("Bearer %1").arg(token).toUtf8());
QNetworkReply* reply = network_.get(request);
NewClosure(reply, SIGNAL(finished()), this, SLOT(ListFilesFinished(QNetworkReply*)), reply);
}
void GoogleDriveService::RefreshTokenAvailable(const QString& token) {
// Save the refresh token
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("refresh_token", token);
s.setValue("refresh_token", response->refresh_token());
// Find any music files
google_drive::ListFilesResponse* list_response =
client_->ListFiles("mimeType = 'audio/mpeg'");
connect(list_response, SIGNAL(FilesFound(QList<google_drive::File>)),
this, SLOT(FilesFound(QList<google_drive::File>)));
NewClosure(list_response, SIGNAL(Finished()),
this, SLOT(ListFilesFinished(google_drive::ListFilesResponse*)));
}
void GoogleDriveService::ListFilesFinished(QNetworkReply* reply) {
reply->deleteLater();
QJson::Parser parser;
bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap();
if (!ok) {
qLog(Error) << "Failed to request files from Google Drive";
return;
}
QVariantList items = result["items"].toList();
foreach (const QVariant& v, items) {
QVariantMap file = v.toMap();
void GoogleDriveService::FilesFound(const QList<google_drive::File>& files) {
foreach (const google_drive::File& file, files) {
MaybeAddFileToDatabase(file);
}
}
void GoogleDriveService::MaybeAddFileToDatabase(const QVariantMap& file) {
QString url = QString("googledrive:%1").arg(file["id"].toString());
void GoogleDriveService::ListFilesFinished(google_drive::ListFilesResponse* response) {
response->deleteLater();
}
void GoogleDriveService::MaybeAddFileToDatabase(const google_drive::File& file) {
QString url = QString("googledrive:%1").arg(file.id());
Song song = library_backend_->GetSongByUrl(QUrl(url));
// Song already in index.
// TODO: Check etag and maybe update.
@ -295,10 +276,10 @@ void GoogleDriveService::MaybeAddFileToDatabase(const QVariantMap& file) {
// Song not in index; tag and add.
DriveStream* stream = new DriveStream(
file["downloadUrl"].toUrl(),
file["title"].toString(),
file["fileSize"].toUInt(),
access_token_,
file.download_url(),
file.title(),
file.size(),
client_->access_token(),
&network_);
TagLib::MPEG::File tag(
stream, // Takes ownership.
@ -311,14 +292,11 @@ void GoogleDriveService::MaybeAddFileToDatabase(const QVariantMap& file) {
song.set_album(tag.tag()->album().toCString(true));
song.set_url(url);
song.set_filesize(file["fileSize"].toInt());
song.set_etag(file["etag"].toString().remove('"'));
song.set_filesize(file.size());
song.set_etag(file.etag().remove('"'));
QString modified_date = file["modifiedDate"].toString();
QString created_date = file["createdDate"].toString();
song.set_mtime(QDateTime::fromString(modified_date, Qt::ISODate).toTime_t());
song.set_ctime(QDateTime::fromString(created_date, Qt::ISODate).toTime_t());
song.set_mtime(file.modified_date().toTime_t());
song.set_ctime(file.created_date().toTime_t());
song.set_filetype(Song::Type_Stream);
song.set_directory_id(0);
@ -337,18 +315,13 @@ void GoogleDriveService::MaybeAddFileToDatabase(const QVariantMap& file) {
}
QUrl GoogleDriveService::GetStreamingUrlFromSongId(const QString& id) {
QString url = QString(kGoogleDriveFile).arg(id);
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader(
"Authorization", QString("Bearer %1").arg(access_token_).toUtf8());
QNetworkReply* reply = network_.get(request);
QScopedPointer<google_drive::GetFileResponse> response(client_->GetFile(id));
QEventLoop loop;
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
connect(response.data(), SIGNAL(Finished()), &loop, SLOT(quit()));
loop.exec();
QJson::Parser parser;
bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap();
QString download_url = result["downloadUrl"].toString() + "#" + access_token_;
return QUrl(download_url);
QUrl url(response->file().download_url());
url.setFragment(client_->access_token());
return url;
}

View File

@ -9,9 +9,15 @@ class QStandardItem;
class LibraryBackend;
class LibraryModel;
class OAuthenticator;
class QSortFilterProxyModel;
namespace google_drive {
class Client;
class ConnectResponse;
class File;
class ListFilesResponse;
}
class GoogleDriveService : public InternetService {
Q_OBJECT
public:
@ -23,19 +29,18 @@ class GoogleDriveService : public InternetService {
QUrl GetStreamingUrlFromSongId(const QString& file_id);
private slots:
void AccessTokenAvailable(const QString& token);
void RefreshTokenAvailable(const QString& token);
void ListFilesFinished(QNetworkReply* reply);
void ConnectFinished(google_drive::ConnectResponse* response);
void FilesFound(const QList<google_drive::File>& files);
void ListFilesFinished(google_drive::ListFilesResponse* response);
private:
void Connect();
void RefreshAuthorisation(const QString& refresh_token);
void MaybeAddFileToDatabase(const QVariantMap& file);
void MaybeAddFileToDatabase(const google_drive::File& file);
QStandardItem* root_;
OAuthenticator* oauth_;
QString access_token_;
google_drive::Client* client_;
NetworkAccessManager network_;

View File

@ -158,11 +158,12 @@ void OAuthenticator::FetchAccessTokenFinished(QNetworkReply* reply) {
access_token_ = result["access_token"].toString();
refresh_token_ = result["refresh_token"].toString();
emit AccessTokenAvailable(access_token_);
emit RefreshTokenAvailable(refresh_token_);
emit Finished();
}
void OAuthenticator::RefreshAuthorisation(const QString& refresh_token) {
refresh_token_ = refresh_token;
QUrl url = QUrl(kGoogleOAuthTokenEndpoint);
typedef QPair<QString, QString> Param;
@ -192,6 +193,6 @@ void OAuthenticator::RefreshAccessTokenFinished(QNetworkReply* reply) {
bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap();
QString access_token = result["access_token"].toString();
emit AccessTokenAvailable(access_token);
access_token_ = result["access_token"].toString();
emit Finished();
}

View File

@ -15,11 +15,14 @@ class OAuthenticator : public QObject {
void StartAuthorisation();
void RefreshAuthorisation(const QString& refresh_token);
signals:
// Token to use now.
void AccessTokenAvailable(const QString& token);
const QString& access_token() const { return access_token_; }
// Token to use to get a new access token when it expires.
void RefreshTokenAvailable(const QString& token);
const QString& refresh_token() const { return refresh_token_; }
signals:
void Finished();
private slots:
void NewConnection();