From 165cec1e863add376572ea06c2f858ad85deab29 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Sat, 28 Jul 2012 17:18:03 +0100 Subject: [PATCH] Move some of the Google Drive bits out into a separate client class. --- src/CMakeLists.txt | 2 + src/internet/googledriveclient.cpp | 168 ++++++++++++++++++++++++++++ src/internet/googledriveclient.h | 145 ++++++++++++++++++++++++ src/internet/googledriveservice.cpp | 113 +++++++------------ src/internet/googledriveservice.h | 19 ++-- src/internet/oauthenticator.cpp | 9 +- src/internet/oauthenticator.h | 9 +- 7 files changed, 381 insertions(+), 84 deletions(-) create mode 100644 src/internet/googledriveclient.cpp create mode 100644 src/internet/googledriveclient.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2a35a1475..1b44f24b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 ) diff --git a/src/internet/googledriveclient.cpp b/src/internet/googledriveclient.cpp new file mode 100644 index 000000000..632f517f3 --- /dev/null +++ b/src/internet/googledriveclient.cpp @@ -0,0 +1,168 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + 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 "googledriveclient.h" +#include "oauthenticator.h" +#include "core/closure.h" +#include "core/network.h" + +#include + +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(); +} diff --git a/src/internet/googledriveclient.h b/src/internet/googledriveclient.h new file mode 100644 index 000000000..25dca2838 --- /dev/null +++ b/src/internet/googledriveclient.h @@ -0,0 +1,145 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + 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 GOOGLEDRIVECLIENT_H +#define GOOGLEDRIVECLIENT_H + +#include +#include +#include +#include +#include + +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 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& 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 diff --git a/src/internet/googledriveservice.cpp b/src/internet/googledriveservice.cpp index 284ca46bb..1dd7256f8 100644 --- a/src/internet/googledriveservice.cpp +++ b/src/internet/googledriveservice.cpp @@ -1,10 +1,9 @@ #include "googledriveservice.h" #include +#include #include -#include - #include #include @@ -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)), + this, SLOT(FilesFound(QList))); + + 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& 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 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; } diff --git a/src/internet/googledriveservice.h b/src/internet/googledriveservice.h index cd12e2c08..89a326e75 100644 --- a/src/internet/googledriveservice.h +++ b/src/internet/googledriveservice.h @@ -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& 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_; diff --git a/src/internet/oauthenticator.cpp b/src/internet/oauthenticator.cpp index 46e3e854c..b2cf31e75 100644 --- a/src/internet/oauthenticator.cpp +++ b/src/internet/oauthenticator.cpp @@ -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 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(); } diff --git a/src/internet/oauthenticator.h b/src/internet/oauthenticator.h index 141017cd1..a073fb186 100644 --- a/src/internet/oauthenticator.h +++ b/src/internet/oauthenticator.h @@ -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();