diff --git a/CMakeLists.txt b/CMakeLists.txt
index 488bedac0..cf0c879d0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -84,6 +84,8 @@ find_library(LASTFM_LIBRARIES lastfm)
find_path(LASTFM_INCLUDE_DIRS lastfm/ws.h)
find_path(LASTFM1_INCLUDE_DIRS lastfm/Track.h)
+find_path(SPARSEHASH_INCLUDE_DIRS google/sparsetable)
+
if(LASTFM_INCLUDE_DIRS AND LASTFM1_INCLUDE_DIRS)
set(HAVE_LIBLASTFM1 ON)
endif()
@@ -200,6 +202,7 @@ option(ENABLE_BREAKPAD "Enable crash reporting" OFF)
option(ENABLE_SPOTIFY_BLOB "Build the spotify non-GPL binary" ON)
option(ENABLE_SPOTIFY "Enable spotify support" ON)
option(ENABLE_MOODBAR "Enable moodbar" ON)
+option(ENABLE_GOOGLE_DRIVE "Enable Google Drive support" ON)
if(WIN32)
option(ENABLE_WIN32_CONSOLE "Show the windows console even outside Debug mode" OFF)
@@ -266,6 +269,10 @@ if(ENABLE_MOODBAR AND FFTW3_FOUND)
set(HAVE_MOODBAR ON)
endif()
+if(ENABLE_GOOGLE_DRIVE AND SPARSEHASH_INCLUDE_DIRS AND TAGLIB_VERSION VERSION_GREATER 1.7.999)
+ set(HAVE_GOOGLE_DRIVE ON)
+endif()
+
if(ENABLE_VISUALISATIONS)
# When/if upstream accepts our patches then these options can be used to link
@@ -439,6 +446,7 @@ summary_add("Devices: iPod Touch, iPhone, iPad support" HAVE_IMOBILEDEVICE)
summary_add("Devices: MTP support" HAVE_LIBMTP)
summary_add("Devices: GIO backend" HAVE_GIO)
summary_add("Gnome sound menu integration" HAVE_LIBINDICATE)
+summary_add("Google Drive support" HAVE_GOOGLE_DRIVE)
summary_add("Last.fm support" HAVE_LIBLASTFM)
summary_add("Moodbar support" HAVE_MOODBAR)
summary_add("Spotify support: core code" HAVE_SPOTIFY)
diff --git a/data/data.qrc b/data/data.qrc
index a51757c54..71022f56b 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -275,6 +275,7 @@
providers/digitallyimported-32.png
providers/digitallyimported.png
providers/echonest.png
+ providers/googledrive.png
providers/grooveshark.png
providers/itunes.png
providers/jamendo.png
@@ -326,6 +327,7 @@
schema/schema-35.sql
schema/schema-36.sql
schema/schema-37.sql
+ schema/schema-38.sql
schema/schema-3.sql
schema/schema-4.sql
schema/schema-5.sql
@@ -348,5 +350,6 @@
volumeslider-handle_glow.png
volumeslider-handle.png
volumeslider-inset.png
+ oauthsuccess.html
diff --git a/data/oauthsuccess.html b/data/oauthsuccess.html
new file mode 100644
index 000000000..4aa0bda46
--- /dev/null
+++ b/data/oauthsuccess.html
@@ -0,0 +1,37 @@
+
+
+ tr("Return to Clementine")
+
+
+
+
+
+
+
tr("Success!")
+
![](data:image/png;base64,@IMAGE_DATA@)
+
tr("Please close your browser and return to Clementine.")
+
+
+
diff --git a/data/providers/googledrive.png b/data/providers/googledrive.png
new file mode 100644
index 000000000..590dde867
Binary files /dev/null and b/data/providers/googledrive.png differ
diff --git a/data/schema/schema-38.sql b/data/schema/schema-38.sql
new file mode 100644
index 000000000..bfa88048e
--- /dev/null
+++ b/data/schema/schema-38.sql
@@ -0,0 +1,50 @@
+ALTER TABLE %allsongstables ADD COLUMN etag TEXT;
+
+CREATE TABLE google_drive_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 google_drive_songs_fts USING fts3 (
+ ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment,
+ tokenize=unicode
+);
+
+UPDATE schema_version SET version=38;
diff --git a/ext/clementine-tagreader/CMakeLists.txt b/ext/clementine-tagreader/CMakeLists.txt
index 77b07ae4a..796334137 100644
--- a/ext/clementine-tagreader/CMakeLists.txt
+++ b/ext/clementine-tagreader/CMakeLists.txt
@@ -4,6 +4,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common)
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
include_directories(${CMAKE_SOURCE_DIR}/src)
+include_directories(${CMAKE_BINARY_DIR}/src)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
@@ -16,6 +17,12 @@ set(SOURCES
set(HEADERS
)
+optional_source(HAVE_GOOGLE_DRIVE
+ INCLUDE_DIRECTORIES ${SPARSEHASH_INCLUDE_DIRS}
+ SOURCES
+ googledrivestream.cpp
+)
+
qt4_wrap_cpp(MOC ${HEADERS})
add_executable(clementine-tagreader
diff --git a/ext/clementine-tagreader/googledrivestream.cpp b/ext/clementine-tagreader/googledrivestream.cpp
new file mode 100644
index 000000000..7489fc116
--- /dev/null
+++ b/ext/clementine-tagreader/googledrivestream.cpp
@@ -0,0 +1,185 @@
+/* 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 "googledrivestream.h"
+#include "core/logging.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+namespace {
+ static const int kTaglibPrefixCacheBytes = 64 * 1024; // Should be enough.
+ static const int kTaglibSuffixCacheBytes = 8 * 1024;
+}
+
+GoogleDriveStream::GoogleDriveStream(
+ const QUrl& url, const QString& filename, const long length,
+ const QString& auth, QNetworkAccessManager* network)
+ : url_(url),
+ filename_(filename),
+ encoded_filename_(filename_.toUtf8()),
+ length_(length),
+ auth_(auth),
+ cursor_(0),
+ network_(network),
+ cache_(length),
+ num_requests_(0) {
+}
+
+TagLib::FileName GoogleDriveStream::name() const {
+ return encoded_filename_.data();
+}
+
+bool GoogleDriveStream::CheckCache(int start, int end) {
+ for (int i = start; i <= end; ++i) {
+ if (!cache_.test(i)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void GoogleDriveStream::FillCache(int start, TagLib::ByteVector data) {
+ for (int i = 0; i < data.size(); ++i) {
+ cache_.set(start + i, data[i]);
+ }
+}
+
+TagLib::ByteVector GoogleDriveStream::GetCached(int start, int end) {
+ const uint size = end - start + 1;
+ TagLib::ByteVector ret(size);
+ for (int i = 0; i < size; ++i) {
+ ret[i] = cache_.get(start + i);
+ }
+ return ret;
+}
+
+void GoogleDriveStream::Precache() {
+ // For reading the tags of an MP3, TagLib tends to request:
+ // 1. The first 1024 bytes
+ // 2. Somewhere between the first 2KB and first 60KB
+ // 3. The last KB or two.
+ // 4. Somewhere in the first 64KB again
+ //
+ // OGG Vorbis may read the last 4KB.
+ //
+ // So, if we precache the first 64KB and the last 8KB we should be sorted :-)
+ // Ideally, we would use bytes=0-655364,-8096 but Google Drive does not seem
+ // to support multipart byte ranges yet so we have to make do with two
+ // requests.
+
+ seek(0, TagLib::IOStream::Beginning);
+ readBlock(kTaglibPrefixCacheBytes);
+ seek(kTaglibSuffixCacheBytes, TagLib::IOStream::End);
+ readBlock(kTaglibSuffixCacheBytes);
+ clear();
+}
+
+TagLib::ByteVector GoogleDriveStream::readBlock(ulong length) {
+ const uint start = cursor_;
+ const uint end = qMin(cursor_ + length - 1, length_ - 1);
+
+ if (end < start) {
+ return TagLib::ByteVector();
+ }
+
+ if (CheckCache(start, end)) {
+ TagLib::ByteVector cached = GetCached(start, end);
+ cursor_ += cached.size();
+ return cached;
+ }
+
+ QNetworkRequest request = QNetworkRequest(url_);
+ request.setRawHeader(
+ "Authorization", QString("Bearer %1").arg(auth_).toUtf8());
+ request.setRawHeader(
+ "Range", QString("bytes=%1-%2").arg(start).arg(end).toUtf8());
+
+ QNetworkReply* reply = network_->get(request);
+ ++num_requests_;
+
+ QEventLoop loop;
+ QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
+ loop.exec();
+ reply->deleteLater();
+
+ QByteArray data = reply->readAll();
+ TagLib::ByteVector bytes(data.data(), data.size());
+ cursor_ += data.size();
+
+ FillCache(start, bytes);
+ return bytes;
+}
+
+void GoogleDriveStream::writeBlock(const TagLib::ByteVector&) {
+ qLog(Debug) << Q_FUNC_INFO << "not implemented";
+}
+
+void GoogleDriveStream::insert(const TagLib::ByteVector&, ulong, ulong) {
+ qLog(Debug) << Q_FUNC_INFO << "not implemented";
+}
+
+void GoogleDriveStream::removeBlock(ulong, ulong) {
+ qLog(Debug) << Q_FUNC_INFO << "not implemented";
+}
+
+bool GoogleDriveStream::readOnly() const {
+ qLog(Debug) << Q_FUNC_INFO;
+ return true;
+}
+
+bool GoogleDriveStream::isOpen() const {
+ return true;
+}
+
+void GoogleDriveStream::seek(long offset, TagLib::IOStream::Position p) {
+ switch (p) {
+ case TagLib::IOStream::Beginning:
+ cursor_ = offset;
+ break;
+
+ case TagLib::IOStream::Current:
+ cursor_ = qMin(ulong(cursor_ + offset), length_);
+ break;
+
+ case TagLib::IOStream::End:
+ // This should really not have qAbs(), but OGG reading needs it.
+ cursor_ = qMax(0UL, length_ - qAbs(offset));
+ break;
+ }
+}
+
+void GoogleDriveStream::clear() {
+ cursor_ = 0;
+}
+
+long GoogleDriveStream::tell() const {
+ return cursor_;
+}
+
+long GoogleDriveStream::length() {
+ return length_;
+}
+
+void GoogleDriveStream::truncate(long) {
+ qLog(Debug) << Q_FUNC_INFO << "not implemented";
+}
diff --git a/ext/clementine-tagreader/googledrivestream.h b/ext/clementine-tagreader/googledrivestream.h
new file mode 100644
index 000000000..f11a34dc6
--- /dev/null
+++ b/ext/clementine-tagreader/googledrivestream.h
@@ -0,0 +1,80 @@
+/* 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 GOOGLEDRIVESTREAM_H
+#define GOOGLEDRIVESTREAM_H
+
+#include
+
+#include
+#include
+
+class QNetworkAccessManager;
+
+class GoogleDriveStream : public TagLib::IOStream {
+ public:
+ GoogleDriveStream(const QUrl& url,
+ const QString& filename,
+ const long length,
+ const QString& auth,
+ QNetworkAccessManager* network);
+
+ // Taglib::IOStream
+ virtual TagLib::FileName name() const;
+ virtual TagLib::ByteVector readBlock(ulong length);
+ virtual void writeBlock(const TagLib::ByteVector&);
+ virtual void insert(const TagLib::ByteVector&, ulong, ulong);
+ virtual void removeBlock(ulong, ulong);
+ virtual bool readOnly() const;
+ virtual bool isOpen() const;
+ virtual void seek(long offset, TagLib::IOStream::Position p);
+ virtual void clear();
+ virtual long tell() const;
+ virtual long length();
+ virtual void truncate(long);
+
+ google::sparsetable::size_type cached_bytes() const {
+ return cache_.num_nonempty();
+ }
+
+ int num_requests() const {
+ return num_requests_;
+ }
+
+ // Use educated guess to request the bytes that TagLib will probably want.
+ void Precache();
+
+ private:
+ bool CheckCache(int start, int end);
+ void FillCache(int start, TagLib::ByteVector data);
+ TagLib::ByteVector GetCached(int start, int end);
+
+ private:
+ const QUrl url_;
+ const QString filename_;
+ const QByteArray encoded_filename_;
+ const ulong length_;
+ const QString auth_;
+
+ int cursor_;
+ QNetworkAccessManager* network_;
+
+ google::sparsetable cache_;
+ int num_requests_;
+};
+
+#endif // GOOGLEDRIVESTREAM_H
diff --git a/ext/clementine-tagreader/tagreaderworker.cpp b/ext/clementine-tagreader/tagreaderworker.cpp
index b87723a4d..ab6b8a614 100644
--- a/ext/clementine-tagreader/tagreaderworker.cpp
+++ b/ext/clementine-tagreader/tagreaderworker.cpp
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
#include
#include
@@ -56,6 +57,10 @@
# define TAGLIB_HAS_FLAC_PICTURELIST
#endif
+#ifdef HAVE_GOOGLE_DRIVE
+# include "googledrivestream.h"
+#endif
+
using boost::scoped_ptr;
@@ -93,6 +98,7 @@ TagLib::String QStringToTaglibString(const QString& s) {
TagReaderWorker::TagReaderWorker(QIODevice* socket, QObject* parent)
: AbstractMessageHandler(socket, parent),
factory_(new TagLibFileRefFactory),
+ network_(new QNetworkAccessManager),
kEmbeddedCover("(embedded)")
{
}
@@ -123,6 +129,21 @@ void TagReaderWorker::MessageArrived(const pb::tagreader::Message& message) {
QStringFromStdString(message.load_embedded_art_request().filename()));
reply.mutable_load_embedded_art_response()->set_data(
data.constData(), data.size());
+ } else if (message.has_read_google_drive_request()) {
+#ifdef HAVE_GOOGLE_DRIVE
+ const pb::tagreader::ReadGoogleDriveRequest& req =
+ message.read_google_drive_request();
+ if (!ReadGoogleDrive(
+ QUrl::fromEncoded(QByteArray(req.download_url().data(),
+ req.download_url().size())),
+ QStringFromStdString(req.title()),
+ req.size(),
+ QStringFromStdString(req.mime_type()),
+ QStringFromStdString(req.access_token()),
+ reply.mutable_read_google_drive_response()->mutable_metadata())) {
+ reply.mutable_read_google_drive_response()->clear_metadata();
+ }
+#endif
}
SendReply(message, &reply);
@@ -588,3 +609,74 @@ void TagReaderWorker::DeviceClosed() {
qApp->exit();
}
+
+#ifdef HAVE_GOOGLE_DRIVE
+bool TagReaderWorker::ReadGoogleDrive(const QUrl& download_url,
+ const QString& title,
+ int size,
+ const QString& mime_type,
+ const QString& access_token,
+ pb::tagreader::SongMetadata* song) const {
+ qLog(Debug) << "Loading tags from" << title;
+
+ GoogleDriveStream* stream = new GoogleDriveStream(
+ download_url, title, size, access_token, network_);
+ stream->Precache();
+ scoped_ptr tag;
+ if (mime_type == "audio/mpeg" && title.endsWith(".mp3")) {
+ tag.reset(new TagLib::MPEG::File(
+ stream, // Takes ownership.
+ TagLib::ID3v2::FrameFactory::instance(),
+ TagLib::AudioProperties::Accurate));
+ } else if (mime_type == "audio/mpeg" && title.endsWith(".m4a")) {
+ tag.reset(new TagLib::MP4::File(
+ stream,
+ true,
+ TagLib::AudioProperties::Accurate));
+ } else if (mime_type == "application/ogg") {
+ tag.reset(new TagLib::Ogg::Vorbis::File(
+ stream,
+ true,
+ TagLib::AudioProperties::Accurate));
+ } else if (mime_type == "application/x-flac") {
+ tag.reset(new TagLib::FLAC::File(
+ stream,
+ TagLib::ID3v2::FrameFactory::instance(),
+ true,
+ TagLib::AudioProperties::Accurate));
+ } else {
+ qLog(Debug) << "Unknown mime type for tagging:" << mime_type;
+ return false;
+ }
+
+ if (stream->num_requests() > 2) {
+ // Warn if pre-caching failed.
+ qLog(Warning) << "Total requests for file:" << title
+ << stream->num_requests()
+ << stream->cached_bytes();
+ }
+
+ if (tag->tag()) {
+ song->set_title(tag->tag()->title().toCString(true));
+ song->set_artist(tag->tag()->artist().toCString(true));
+ song->set_album(tag->tag()->album().toCString(true));
+ song->set_filesize(size);
+
+ if (tag->tag()->track() != 0) {
+ song->set_track(tag->tag()->track());
+ }
+ if (tag->tag()->year() != 0) {
+ song->set_year(tag->tag()->year());
+ }
+
+ song->set_type(pb::tagreader::SongMetadata_Type_STREAM);
+
+ if (tag->audioProperties()) {
+ song->set_length_nanosec(tag->audioProperties()->length() * kNsecPerSec);
+ }
+ return true;
+ }
+
+ return false;
+}
+#endif // HAVE_GOOGLE_DRIVE
diff --git a/ext/clementine-tagreader/tagreaderworker.h b/ext/clementine-tagreader/tagreaderworker.h
index 021600bb0..997a8aaef 100644
--- a/ext/clementine-tagreader/tagreaderworker.h
+++ b/ext/clementine-tagreader/tagreaderworker.h
@@ -18,11 +18,16 @@
#ifndef TAGREADERWORKER_H
#define TAGREADERWORKER_H
+#include "config.h"
#include "tagreadermessages.pb.h"
#include "core/messagehandler.h"
#include
+#include
+
+class QNetworkAccessManager;
+
namespace TagLib {
class FileRef;
@@ -49,6 +54,15 @@ private:
bool IsMediaFile(const QString& filename) const;
QByteArray LoadEmbeddedArt(const QString& filename) const;
+ #ifdef HAVE_GOOGLE_DRIVE
+ bool ReadGoogleDrive(const QUrl& download_url,
+ const QString& title,
+ int size,
+ const QString& mime_type,
+ const QString& access_token,
+ pb::tagreader::SongMetadata* song) const;
+ #endif // HAVE_GOOGLE_DRIVE
+
static void Decode(const TagLib::String& tag, const QTextCodec* codec,
std::string* output);
static void Decode(const QString& tag, const QTextCodec* codec,
@@ -69,6 +83,7 @@ private:
private:
FileRefFactory* factory_;
+ QNetworkAccessManager* network_;
const std::string kEmbeddedCover;
};
diff --git a/ext/libclementine-tagreader/tagreadermessages.proto b/ext/libclementine-tagreader/tagreadermessages.proto
index 505e0eba2..4cc04c14a 100644
--- a/ext/libclementine-tagreader/tagreadermessages.proto
+++ b/ext/libclementine-tagreader/tagreadermessages.proto
@@ -85,6 +85,18 @@ message LoadEmbeddedArtResponse {
optional bytes data = 1;
}
+message ReadGoogleDriveRequest {
+ optional string download_url = 1;
+ optional string title = 2;
+ optional int32 size = 3;
+ optional string access_token = 4;
+ optional string mime_type = 5;
+}
+
+message ReadGoogleDriveResponse {
+ optional SongMetadata metadata = 1;
+}
+
message Message {
optional int32 id = 1;
@@ -99,4 +111,7 @@ message Message {
optional LoadEmbeddedArtRequest load_embedded_art_request = 8;
optional LoadEmbeddedArtResponse load_embedded_art_response = 9;
+
+ optional ReadGoogleDriveRequest read_google_drive_request = 10;
+ optional ReadGoogleDriveResponse read_google_drive_response = 11;
}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6be27725a..fc834f034 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -182,6 +182,7 @@ set(SOURCES
internet/magnatunesettingspage.cpp
internet/magnatuneservice.cpp
internet/magnatuneurlhandler.cpp
+ internet/oauthenticator.cpp
internet/savedradio.cpp
internet/searchboxwidget.cpp
internet/somafmservice.cpp
@@ -451,6 +452,7 @@ set(HEADERS
internet/magnatunedownloaddialog.h
internet/magnatunesettingspage.h
internet/magnatuneservice.h
+ internet/oauthenticator.h
internet/savedradio.h
internet/searchboxwidget.h
internet/somafmservice.h
@@ -997,6 +999,24 @@ optional_source(HAVE_MOODBAR
moodbar/moodbarproxystyle.h
)
+# Google Drive support
+optional_source(HAVE_GOOGLE_DRIVE
+ SOURCES
+ internet/googledriveclient.cpp
+ internet/googledrivefoldermodel.cpp
+ internet/googledriveservice.cpp
+ internet/googledrivesettingspage.cpp
+ internet/googledriveurlhandler.cpp
+ HEADERS
+ internet/googledriveclient.h
+ internet/googledrivefoldermodel.h
+ internet/googledriveservice.h
+ internet/googledrivesettingspage.h
+ internet/googledriveurlhandler.h
+ UI
+ internet/googledrivesettingspage.ui
+)
+
# Hack to add Clementine to the Unity system tray whitelist
optional_source(LINUX
SOURCES core/ubuntuunityhack.cpp
@@ -1017,6 +1037,7 @@ add_pot(POT
${CMAKE_CURRENT_SOURCE_DIR}/translations/header
${CMAKE_CURRENT_SOURCE_DIR}/translations/translations.pot
${SOURCES} ${MOC} ${UIC} ${OTHER_SOURCES}
+ ../data/oauthsuccess.html
)
add_po(PO clementine_
LANGUAGES ${LANGUAGES}
diff --git a/src/config.h.in b/src/config.h.in
index 9a6750420..cd0dc9fd4 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -26,6 +26,7 @@
#cmakedefine HAVE_DBUS
#cmakedefine HAVE_DEVICEKIT
#cmakedefine HAVE_GIO
+#cmakedefine HAVE_GOOGLE_DRIVE
#cmakedefine HAVE_IMOBILEDEVICE
#cmakedefine HAVE_LIBARCHIVE
#cmakedefine HAVE_LIBGPOD
diff --git a/src/core/database.cpp b/src/core/database.cpp
index 57191f94c..906e779a6 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 = 37;
+const int Database::kSchemaVersion = 38;
const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/song.cpp b/src/core/song.cpp
index 1387c2232..23c757042 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -79,7 +79,7 @@ const QStringList Song::kColumns = QStringList()
<< "art_manual" << "filetype" << "playcount" << "lastplayed" << "rating"
<< "forced_compilation_on" << "forced_compilation_off"
<< "effective_compilation" << "skipcount" << "score" << "beginning" << "length"
- << "cue_path" << "unavailable" << "effective_albumartist";
+ << "cue_path" << "unavailable" << "effective_albumartist" << "etag";
const QString Song::kColumnSpec = Song::kColumns.join(", ");
const QString Song::kBindSpec = Utilities::Prepend(":", Song::kColumns).join(", ");
@@ -167,6 +167,8 @@ struct Song::Private : public QSharedData {
// Whether the song does not exist on the file system anymore, but is still
// stored in the database so as to remember the user's metadata.
bool unavailable_;
+
+ QString etag_;
};
@@ -263,6 +265,7 @@ bool Song::is_stream() const { return d->filetype_ == Type_Stream; }
bool Song::is_cdda() const { return d->filetype_ == Type_Cdda; }
const QString& Song::art_automatic() const { return d->art_automatic_; }
const QString& Song::art_manual() const { return d->art_manual_; }
+const QString& Song::etag() const { return d->etag_; }
bool Song::has_manually_unset_cover() const { return d->art_manual_ == kManuallyUnsetCover; }
void Song::manually_unset_cover() { d->art_manual_ = kManuallyUnsetCover; }
bool Song::has_embedded_cover() const { return d->art_automatic_ == kEmbeddedCover; }
@@ -304,6 +307,7 @@ void Song::set_lastplayed(int v) { d->lastplayed_ = v; }
void Song::set_score(int v) { d->score_ = qBound(0, v, 100); }
void Song::set_cue_path(const QString& v) { d->cue_path_ = v; }
void Song::set_unavailable(bool v) { d->unavailable_ = v; }
+void Song::set_etag(const QString& etag) { d->etag_ = etag; }
void Song::set_url(const QUrl& v) { d->url_ = v; }
void Song::set_basefilename(const QString& v) { d->basefilename_ = v; }
void Song::set_directory_id(int v) { d->directory_id_ = v; }
@@ -1002,8 +1006,11 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":unavailable", d->unavailable_ ? 1 : 0);
query->bindValue(":effective_albumartist", this->effective_albumartist());
+ query->bindValue(":etag", strval(d->etag_));
+
#undef intval
#undef notnullintval
+ #undef strval
}
void Song::BindToFtsQuery(QSqlQuery *query) const {
diff --git a/src/core/song.h b/src/core/song.h
index 94f5a34ce..2a254ee54 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -187,6 +187,8 @@ class Song {
const QString& art_automatic() const;
const QString& art_manual() const;
+ const QString& etag() const;
+
// Returns true if this Song had it's cover manually unset by user.
bool has_manually_unset_cover() const;
// This method represents an explicit request to unset this song's
@@ -250,6 +252,7 @@ class Song {
void set_score(int v);
void set_cue_path(const QString& v);
void set_unavailable(bool v);
+ void set_etag(const QString& etag);
// Setters that should only be used by tests
void set_url(const QUrl& v);
diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp
index 7aec6aeb5..145d6dded 100644
--- a/src/core/tagreaderclient.cpp
+++ b/src/core/tagreaderclient.cpp
@@ -21,6 +21,7 @@
#include
#include
#include
+#include
const char* TagReaderClient::kWorkerExecutableName = "clementine-tagreader";
@@ -83,6 +84,25 @@ TagReaderReply* TagReaderClient::LoadEmbeddedArt(const QString& filename) {
return worker_pool_->SendMessageWithReply(&message);
}
+TagReaderReply* TagReaderClient::ReadGoogleDrive(const QUrl& download_url,
+ const QString& title,
+ int size,
+ const QString& mime_type,
+ const QString& access_token) {
+ pb::tagreader::Message message;
+ pb::tagreader::ReadGoogleDriveRequest* req =
+ message.mutable_read_google_drive_request();
+
+ const QString url_string = download_url.toEncoded();
+ req->set_download_url(DataCommaSizeFromQString(url_string));
+ req->set_title(DataCommaSizeFromQString(title));
+ req->set_size(size);
+ req->set_mime_type(DataCommaSizeFromQString(mime_type));
+ req->set_access_token(DataCommaSizeFromQString(access_token));
+
+ return worker_pool_->SendMessageWithReply(&message);
+}
+
void TagReaderClient::ReadFileBlocking(const QString& filename, Song* song) {
Q_ASSERT(QThread::currentThread() != thread());
diff --git a/src/core/tagreaderclient.h b/src/core/tagreaderclient.h
index 97d0b56cd..864d8ba8f 100644
--- a/src/core/tagreaderclient.h
+++ b/src/core/tagreaderclient.h
@@ -45,6 +45,11 @@ public:
ReplyType* SaveFile(const QString& filename, const Song& metadata);
ReplyType* IsMediaFile(const QString& filename);
ReplyType* LoadEmbeddedArt(const QString& filename);
+ ReplyType* ReadGoogleDrive(const QUrl& download_url,
+ const QString& title,
+ int size,
+ const QString& mime_type,
+ const QString& access_token);
// Convenience functions that call the above functions and wait for a
// response. These block the calling thread with a semaphore, and must NOT
diff --git a/src/core/taskmanager.h b/src/core/taskmanager.h
index 3e2f753c1..4a240c1b5 100644
--- a/src/core/taskmanager.h
+++ b/src/core/taskmanager.h
@@ -36,6 +36,24 @@ public:
bool blocks_library_scans;
};
+ class ScopedTask {
+ public:
+ ScopedTask(const int task_id, TaskManager* task_manager)
+ : task_id_(task_id),
+ task_manager_(task_manager) {
+ }
+
+ ~ScopedTask() {
+ task_manager_->SetTaskFinished(task_id_);
+ }
+
+ private:
+ const int task_id_;
+ TaskManager* task_manager_;
+
+ Q_DISABLE_COPY(ScopedTask);
+ };
+
// Everything here is thread safe
QList GetTasks();
@@ -56,6 +74,8 @@ private:
QMutex mutex_;
QMap tasks_;
int next_task_id_;
+
+ Q_DISABLE_COPY(TaskManager);
};
#endif // TASKMANAGER_H
diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp
index 6382eada0..d4679bdac 100644
--- a/src/engines/gstenginepipeline.cpp
+++ b/src/engines/gstenginepipeline.cpp
@@ -734,6 +734,21 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin, GParamSpec *ps
g_object_set(element, "extra-headers", headers, NULL);
gst_structure_free(headers);
}
+
+ if (element &&
+ g_object_class_find_property(G_OBJECT_GET_CLASS(element), "extra-headers") &&
+ instance->url().host().contains("googleusercontent.com") &&
+ instance->url().hasFragment()) {
+ QByteArray authorization = QString("Bearer %1").arg(
+ instance->url().fragment()).toAscii();
+ GstStructure* headers = gst_structure_new(
+ "extra-headers",
+ "Authorization", G_TYPE_STRING,
+ authorization.constData(),
+ NULL);
+ g_object_set(element, "extra-headers", headers, NULL);
+ gst_structure_free(headers);
+ }
}
void GstEnginePipeline::TransitionToNext() {
diff --git a/src/internet/googledriveclient.cpp b/src/internet/googledriveclient.cpp
new file mode 100644
index 000000000..b907a2b97
--- /dev/null
+++ b/src/internet/googledriveclient.cpp
@@ -0,0 +1,188 @@
+/* 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;
+
+const char* File::kFolderMimeType = "application/vnd.google-apps.folder";
+
+namespace {
+ static const char* kGoogleDriveFiles = "https://www.googleapis.com/drive/v2/files";
+ static const char* kGoogleDriveFile = "https://www.googleapis.com/drive/v2/files/%1";
+}
+
+QStringList File::parent_ids() const {
+ QStringList ret;
+
+ foreach (const QVariant& var, data_["parents"].toList()) {
+ QVariantMap map(var.toMap());
+
+ if (map["isRoot"].toBool()) {
+ ret << QString();
+ } else {
+ ret << map["id"].toString();
+ }
+ }
+
+ return ret;
+}
+
+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();
+
+ emit Authenticated();
+}
+
+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..6944a373a
--- /dev/null
+++ b/src/internet/googledriveclient.h
@@ -0,0 +1,166 @@
+/* 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
+#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) {}
+
+ static const char* kFolderMimeType;
+
+ QString id() const { return data_["id"].toString(); }
+ QString etag() const { return data_["etag"].toString(); }
+ QString title() const { return data_["title"].toString(); }
+ QString mime_type() const { return data_["mimeType"].toString(); }
+ QString description() const { return data_["description"].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);
+ }
+
+ bool is_folder() const { return mime_type() == kFolderMimeType; }
+ QStringList parent_ids() const;
+
+ bool has_label(const QString& name) const {
+ return data_["labels"].toMap()[name].toBool();
+ }
+
+ bool is_starred() const { return has_label("starred"); }
+ bool is_hidden() const { return has_label("hidden"); }
+ bool is_trashed() const { return has_label("trashed"); }
+ bool is_restricted() const { return has_label("restricted"); }
+ bool is_viewed() const { return has_label("viewed"); }
+
+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);
+
+signals:
+ void Authenticated();
+
+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/googledrivefoldermodel.cpp b/src/internet/googledrivefoldermodel.cpp
new file mode 100644
index 000000000..cbf0b51aa
--- /dev/null
+++ b/src/internet/googledrivefoldermodel.cpp
@@ -0,0 +1,112 @@
+/* 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 "googledrivefoldermodel.h"
+#include "core/closure.h"
+#include "ui/iconloader.h"
+
+using namespace google_drive;
+
+FolderModel::FolderModel(Client* client, QObject* parent)
+ : QStandardItemModel(parent),
+ client_(client)
+{
+ folder_icon_ = IconLoader::Load("folder");
+
+ root_ = new QStandardItem(tr("My Drive"));
+ item_by_id_[QString()] = root_;
+ invisibleRootItem()->appendRow(root_);
+
+ connect(client, SIGNAL(Authenticated()), SLOT(Refresh()));
+ if (client->is_authenticated()) {
+ Refresh();
+ }
+}
+
+void FolderModel::Refresh() {
+ ListFilesResponse* reply = client_->ListFiles(
+ QString("mimeType = '%1' and trashed = false and hidden = false")
+ .arg(File::kFolderMimeType));
+ connect(reply, SIGNAL(FilesFound(QList)),
+ this, SLOT(FilesFound(QList)));
+ NewClosure(reply, SIGNAL(Finished()),
+ this, SLOT(FindFilesFinished(ListFilesResponse*)),
+ reply);
+}
+
+void FolderModel::FindFilesFinished(ListFilesResponse* reply) {
+ reply->deleteLater();
+}
+
+void FolderModel::FilesFound(const QList& files) {
+ foreach (const File& file, files) {
+ const QString id(file.id());
+
+ // Does this file exist in the model already?
+ if (item_by_id_.contains(id)) {
+ // If it has the same etag ignore it, otherwise remove and recreate it.
+ QStandardItem* old_item = item_by_id_[id];
+ if (old_item->data(Role_Etag).toString() == file.etag()) {
+ continue;
+ } else {
+ item_by_id_.remove(id);
+ old_item->parent()->removeRow(old_item->row());
+ }
+ }
+
+ // Get the first parent's ID
+ const QStringList parent_ids = file.parent_ids();
+ if (parent_ids.isEmpty()) {
+ continue;
+ }
+ const QString parent_id = parent_ids.first();
+
+ // If the parent doesn't exist yet, remember this file for later.
+ if (!item_by_id_.contains(parent_id)) {
+ orphans_[parent_id] << file;
+ continue;
+ }
+
+ // Find the item for the parent
+ QStandardItem* parent = item_by_id_[parent_id];
+
+ // Create the item
+ QStandardItem* item = new QStandardItem(file.title());
+ item->setData(file.etag(), Role_Etag);
+ item->setData(id, Role_Id);
+ item_by_id_[id] = item;
+ parent->appendRow(item);
+
+ // Add any children for this item that we saw before.
+ if (orphans_.contains(id)) {
+ FilesFound(orphans_.take(id));
+ }
+ }
+}
+
+QVariant FolderModel::data(const QModelIndex& index, int role) const {
+ if (role == Qt::DecorationRole) {
+ return folder_icon_;
+ }
+
+ return QStandardItemModel::data(index, role);
+}
+
+QStandardItem* FolderModel::ItemById(const QString& id) const {
+ return item_by_id_[id];
+}
diff --git a/src/internet/googledrivefoldermodel.h b/src/internet/googledrivefoldermodel.h
new file mode 100644
index 000000000..899274d14
--- /dev/null
+++ b/src/internet/googledrivefoldermodel.h
@@ -0,0 +1,66 @@
+/* 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 GOOGLEDRIVEFOLDERMODEL_H
+#define GOOGLEDRIVEFOLDERMODEL_H
+
+#include "googledriveclient.h"
+
+#include
+
+namespace google_drive {
+
+class Client;
+
+class FolderModel : public QStandardItemModel {
+ Q_OBJECT
+
+public:
+ FolderModel(Client* client, QObject* parent = 0);
+
+ enum Role {
+ Role_Etag = Qt::UserRole,
+ Role_Id
+ };
+
+ QIcon folder_icon() const { return folder_icon_; }
+ void set_folder_icon(const QIcon& icon) { folder_icon_ = icon; }
+
+ QVariant data(const QModelIndex& index, int role) const;
+
+ QStandardItem* ItemById(const QString& id) const;
+
+public slots:
+ void Refresh();
+
+private slots:
+ void FilesFound(const QList& files);
+ void FindFilesFinished(ListFilesResponse* reply);
+
+private:
+ Client* client_;
+ QIcon folder_icon_;
+
+ QStandardItem* root_;
+
+ QMap item_by_id_;
+ QMap > orphans_;
+};
+
+} // namespace
+
+#endif // GOOGLEDRIVEFOLDERMODEL_H
diff --git a/src/internet/googledriveservice.cpp b/src/internet/googledriveservice.cpp
new file mode 100644
index 000000000..4c4086b60
--- /dev/null
+++ b/src/internet/googledriveservice.cpp
@@ -0,0 +1,209 @@
+#include "googledriveservice.h"
+
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/closure.h"
+#include "core/database.h"
+#include "core/mergedproxymodel.h"
+#include "core/player.h"
+#include "core/taskmanager.h"
+#include "core/timeconstants.h"
+#include "globalsearch/globalsearch.h"
+#include "globalsearch/librarysearchprovider.h"
+#include "library/librarybackend.h"
+#include "library/librarymodel.h"
+#include "googledriveclient.h"
+#include "googledriveurlhandler.h"
+#include "internetmodel.h"
+
+const char* GoogleDriveService::kServiceName = "Google Drive";
+const char* GoogleDriveService::kSettingsGroup = "GoogleDrive";
+
+namespace {
+
+static const char* kSongsTable = "google_drive_songs";
+static const char* kFtsTable = "google_drive_songs_fts";
+
+}
+
+
+GoogleDriveService::GoogleDriveService(Application* app, InternetModel* parent)
+ : InternetService(kServiceName, app, parent, parent),
+ root_(NULL),
+ client_(new google_drive::Client(this)),
+ task_manager_(app->task_manager()),
+ library_sort_model_(new QSortFilterProxyModel(this)) {
+ library_backend_ = new LibraryBackend;
+ library_backend_->moveToThread(app_->database()->thread());
+ library_backend_->Init(app_->database(), kSongsTable,
+ QString::null, QString::null, kFtsTable);
+ library_model_ = new LibraryModel(library_backend_, app_, this);
+
+ library_sort_model_->setSourceModel(library_model_);
+ library_sort_model_->setSortRole(LibraryModel::Role_SortText);
+ library_sort_model_->setDynamicSortFilter(true);
+ library_sort_model_->sort(0);
+
+ app->player()->RegisterUrlHandler(new GoogleDriveUrlHandler(this, this));
+ app->global_search()->AddProvider(new LibrarySearchProvider(
+ library_backend_,
+ tr("Google Drive"),
+ "google_drive",
+ QIcon(":/providers/googledrive.png"),
+ true, app_, this));
+}
+
+QStandardItem* GoogleDriveService::CreateRootItem() {
+ root_ = new QStandardItem(QIcon(":providers/googledrive.png"), "Google Drive");
+ root_->setData(true, InternetModel::Role_CanLazyLoad);
+ return root_;
+}
+
+void GoogleDriveService::LazyPopulate(QStandardItem* item) {
+ switch (item->data(InternetModel::Role_Type).toInt()) {
+ case InternetModel::Type_Service:
+ if (!client_->is_authenticated()) {
+ Connect();
+ }
+ library_model_->Init();
+ model()->merged_model()->AddSubModel(item->index(), library_sort_model_);
+ break;
+
+ default:
+ break;
+ }
+}
+
+void GoogleDriveService::Connect() {
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+
+ google_drive::ConnectResponse* response =
+ client_->Connect(s.value("refresh_token").toString());
+ NewClosure(response, SIGNAL(Finished()),
+ this, SLOT(ConnectFinished(google_drive::ConnectResponse*)),
+ response);
+}
+
+void GoogleDriveService::ListFilesForMimeType(const QString& mime_type) {
+ google_drive::ListFilesResponse* list_response = client_->ListFiles(
+ QString("mimeType = '%1' and trashed = false").arg(mime_type));
+ connect(list_response, SIGNAL(FilesFound(QList)),
+ this, SLOT(FilesFound(QList)));
+ NewClosure(list_response, SIGNAL(Finished()),
+ this, SLOT(ListFilesFinished(google_drive::ListFilesResponse*)),
+ list_response);
+}
+
+void GoogleDriveService::ConnectFinished(google_drive::ConnectResponse* response) {
+ response->deleteLater();
+
+ // Save the refresh token
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ s.setValue("refresh_token", response->refresh_token());
+
+ // Find any music files
+ ListFilesForMimeType("audio/mpeg"); // MP3/AAC
+ ListFilesForMimeType("application/ogg"); // OGG
+ ListFilesForMimeType("application/x-flac"); // FLAC
+}
+
+void GoogleDriveService::EnsureConnected() {
+ if (client_->is_authenticated()) {
+ return;
+ }
+
+ QEventLoop loop;
+ connect(client_, SIGNAL(Authenticated()), &loop, SLOT(quit()));
+ Connect();
+ loop.exec();
+}
+
+void GoogleDriveService::FilesFound(const QList& files) {
+ foreach (const google_drive::File& file, files) {
+ MaybeAddFileToDatabase(file);
+ }
+}
+
+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.
+ if (song.is_valid()) {
+ return;
+ }
+
+ const int task_id = task_manager_->StartTask(
+ tr("Indexing %1").arg(file.title()));
+
+ // Song not in index; tag and add.
+ TagReaderClient::ReplyType* reply = app_->tag_reader_client()->ReadGoogleDrive(
+ file.download_url(),
+ file.title(),
+ file.size(),
+ file.mime_type(),
+ client_->access_token());
+
+ NewClosure(reply, SIGNAL(Finished(bool)),
+ this, SLOT(ReadTagsFinished(TagReaderClient::ReplyType*,google_drive::File,QString,int)),
+ reply, file, url, task_id);
+}
+
+void GoogleDriveService::ReadTagsFinished(TagReaderClient::ReplyType* reply,
+ const google_drive::File& metadata,
+ const QString& url,
+ const int task_id) {
+ reply->deleteLater();
+
+ TaskManager::ScopedTask(task_id, task_manager_);
+
+ const pb::tagreader::ReadGoogleDriveResponse& msg =
+ reply->message().read_google_drive_response();
+ if (!msg.has_metadata() || !msg.metadata().filesize()) {
+ qLog(Debug) << "Failed to tag:" << metadata.title();
+ return;
+ }
+
+ // Read the Song metadata from the message.
+ Song song;
+ song.InitFromProtobuf(msg.metadata());
+
+ // Add some extra tags from the Google Drive metadata.
+ song.set_etag(metadata.etag().remove('"'));
+ song.set_mtime(metadata.modified_date().toTime_t());
+ song.set_ctime(metadata.created_date().toTime_t());
+ song.set_comment(metadata.description());
+ song.set_directory_id(0);
+ song.set_url(url);
+
+ // Use the Google Drive title if we couldn't read tags from the file.
+ if (song.title().isEmpty()) {
+ song.set_title(metadata.title());
+ }
+
+ // Add the song to the database
+ qLog(Debug) << "Adding song to db:" << song.title();
+ library_backend_->AddOrUpdateSongs(SongList() << song);
+}
+
+QUrl GoogleDriveService::GetStreamingUrlFromSongId(const QString& id) {
+ EnsureConnected();
+ QScopedPointer response(client_->GetFile(id));
+
+ QEventLoop loop;
+ connect(response.data(), SIGNAL(Finished()), &loop, SLOT(quit()));
+ loop.exec();
+
+ 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
new file mode 100644
index 000000000..22fa562b0
--- /dev/null
+++ b/src/internet/googledriveservice.h
@@ -0,0 +1,68 @@
+#ifndef GOOGLEDRIVESERVICE_H
+#define GOOGLEDRIVESERVICE_H
+
+#include "internetservice.h"
+
+#include "core/network.h"
+#include "core/tagreaderclient.h"
+
+class QStandardItem;
+
+class LibraryBackend;
+class LibraryModel;
+class TaskManager;
+class QSortFilterProxyModel;
+
+namespace google_drive {
+ class Client;
+ class ConnectResponse;
+ class File;
+ class ListFilesResponse;
+}
+
+class GoogleDriveService : public InternetService {
+ Q_OBJECT
+ public:
+ GoogleDriveService(Application* app, InternetModel* parent);
+
+ static const char* kServiceName;
+ static const char* kSettingsGroup;
+
+ google_drive::Client* client() const { return client_; }
+
+ QStandardItem* CreateRootItem();
+ void LazyPopulate(QStandardItem* item);
+
+ QUrl GetStreamingUrlFromSongId(const QString& file_id);
+
+ private slots:
+ void ConnectFinished(google_drive::ConnectResponse* response);
+ void FilesFound(const QList& files);
+ void ListFilesFinished(google_drive::ListFilesResponse* response);
+ void ReadTagsFinished(TagReaderClient::ReplyType* reply,
+ const google_drive::File& metadata,
+ const QString& url,
+ const int task_id);
+
+ private:
+ void Connect();
+ void EnsureConnected();
+ void RefreshAuthorisation(const QString& refresh_token);
+ void MaybeAddFileToDatabase(const google_drive::File& file);
+ void ListFilesForMimeType(const QString& mime_type);
+
+ QStandardItem* root_;
+
+ google_drive::Client* client_;
+
+ NetworkAccessManager network_;
+ TaskManager* task_manager_;
+
+ LibraryBackend* library_backend_;
+ LibraryModel* library_model_;
+ QSortFilterProxyModel* library_sort_model_;
+
+ int indexing_task_id_;
+};
+
+#endif
diff --git a/src/internet/googledrivesettingspage.cpp b/src/internet/googledrivesettingspage.cpp
new file mode 100644
index 000000000..cdffd0222
--- /dev/null
+++ b/src/internet/googledrivesettingspage.cpp
@@ -0,0 +1,90 @@
+/* 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 "googledrivefoldermodel.h"
+#include "googledriveservice.h"
+#include "googledrivesettingspage.h"
+#include "ui_googledrivesettingspage.h"
+#include "core/application.h"
+#include "internet/internetmodel.h"
+#include "ui/settingsdialog.h"
+
+#include
+
+GoogleDriveSettingsPage::GoogleDriveSettingsPage(SettingsDialog* parent)
+ : SettingsPage(parent),
+ ui_(new Ui::GoogleDriveSettingsPage),
+ model_(NULL),
+ proxy_model_(NULL),
+ item_needs_selecting_(false)
+{
+ ui_->setupUi(this);
+}
+
+GoogleDriveSettingsPage::~GoogleDriveSettingsPage() {
+ delete ui_;
+}
+
+void GoogleDriveSettingsPage::Load() {
+ QSettings s;
+ s.beginGroup(GoogleDriveService::kSettingsGroup);
+
+ destination_folder_id_ = s.value("destination_folder_id").toString();
+ item_needs_selecting_ = !destination_folder_id_.isEmpty();
+
+ if (!model_) {
+ GoogleDriveService* service =
+ dialog()->app()->internet_model()->Service();
+ google_drive::Client* client = service->client();
+
+ model_ = new google_drive::FolderModel(client, this);
+ proxy_model_ = new QSortFilterProxyModel(this);
+ proxy_model_->setSourceModel(model_);
+ proxy_model_->setDynamicSortFilter(true);
+ proxy_model_->setSortCaseSensitivity(Qt::CaseInsensitive);
+ proxy_model_->sort(0);
+
+ ui_->upload_destination->setModel(proxy_model_);
+
+ connect(model_, SIGNAL(rowsInserted(QModelIndex,int,int)),
+ this, SLOT(DirectoryRowsInserted(QModelIndex)));
+ }
+}
+
+void GoogleDriveSettingsPage::Save() {
+ QSettings s;
+ s.beginGroup(GoogleDriveService::kSettingsGroup);
+
+ s.setValue("destination_folder_id",
+ ui_->upload_destination->currentIndex().data(
+ google_drive::FolderModel::Role_Id).toString());
+}
+
+void GoogleDriveSettingsPage::DirectoryRowsInserted(const QModelIndex& parent) {
+ ui_->upload_destination->expand(proxy_model_->mapFromSource(parent));
+
+ if (item_needs_selecting_) {
+ QStandardItem* item = model_->ItemById(destination_folder_id_);
+ if (item) {
+ ui_->upload_destination->selectionModel()->select(
+ proxy_model_->mapFromSource(item->index()),
+ QItemSelectionModel::ClearAndSelect);
+ item_needs_selecting_ = false;
+ }
+ }
+}
diff --git a/src/internet/googledrivesettingspage.h b/src/internet/googledrivesettingspage.h
new file mode 100644
index 000000000..fb8ddf7b4
--- /dev/null
+++ b/src/internet/googledrivesettingspage.h
@@ -0,0 +1,57 @@
+/* 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 GOOGLEDRIVESETTINGSPAGE_H
+#define GOOGLEDRIVESETTINGSPAGE_H
+
+#include "ui/settingspage.h"
+
+#include
+#include
+
+class Ui_GoogleDriveSettingsPage;
+
+class QSortFilterProxyModel;
+
+namespace google_drive {
+ class FolderModel;
+}
+
+class GoogleDriveSettingsPage : public SettingsPage {
+ Q_OBJECT
+
+public:
+ GoogleDriveSettingsPage(SettingsDialog* parent = 0);
+ ~GoogleDriveSettingsPage();
+
+ void Load();
+ void Save();
+
+private slots:
+ void DirectoryRowsInserted(const QModelIndex& parent);
+
+private:
+ Ui_GoogleDriveSettingsPage* ui_;
+
+ google_drive::FolderModel* model_;
+ QSortFilterProxyModel* proxy_model_;
+
+ QString destination_folder_id_;
+ bool item_needs_selecting_;
+};
+
+#endif // GOOGLEDRIVESETTINGSPAGE_H
diff --git a/src/internet/googledrivesettingspage.ui b/src/internet/googledrivesettingspage.ui
new file mode 100644
index 000000000..57c5b5b00
--- /dev/null
+++ b/src/internet/googledrivesettingspage.ui
@@ -0,0 +1,72 @@
+
+
+ GoogleDriveSettingsPage
+
+
+
+ 0
+ 0
+ 569
+ 491
+
+
+
+ Google Drive
+
+
+
+ :/providers/googledrive.png:/providers/googledrive.png
+
+
+ -
+
+
+ Uploads
+
+
+
-
+
+
+ You can upload songs to Google Drive by right clicking and using "Copy to device".
+
+
+ true
+
+
+
+ -
+
+
+ Upload new songs to
+
+
+
+ -
+
+
+
+ 16777215
+ 150
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/internet/googledriveurlhandler.cpp b/src/internet/googledriveurlhandler.cpp
new file mode 100644
index 000000000..3c74e72dc
--- /dev/null
+++ b/src/internet/googledriveurlhandler.cpp
@@ -0,0 +1,16 @@
+#include "googledriveurlhandler.h"
+
+#include "googledriveservice.h"
+
+GoogleDriveUrlHandler::GoogleDriveUrlHandler(
+ GoogleDriveService* service,
+ QObject* parent)
+ : UrlHandler(parent),
+ service_(service) {
+}
+
+UrlHandler::LoadResult GoogleDriveUrlHandler::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/googledriveurlhandler.h b/src/internet/googledriveurlhandler.h
new file mode 100644
index 000000000..46b1bebea
--- /dev/null
+++ b/src/internet/googledriveurlhandler.h
@@ -0,0 +1,21 @@
+#ifndef GOOGLEDRIVEURLHANDLER_H
+#define GOOGLEDRIVEURLHANDLER_H
+
+#include "core/urlhandler.h"
+
+class GoogleDriveService;
+
+class GoogleDriveUrlHandler : public UrlHandler {
+ Q_OBJECT
+ public:
+ GoogleDriveUrlHandler(GoogleDriveService* service, QObject* parent = 0);
+
+ QString scheme() const { return "googledrive"; }
+ QIcon icon() const { return QIcon(":providers/googledrive.png"); }
+ LoadResult StartLoading(const QUrl& url);
+
+ private:
+ GoogleDriveService* service_;
+};
+
+#endif
diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp
index 0453bfe94..428be65ef 100644
--- a/src/internet/internetmodel.cpp
+++ b/src/internet/internetmodel.cpp
@@ -37,6 +37,9 @@
#ifdef HAVE_SPOTIFY
#include "spotifyservice.h"
#endif
+#ifdef HAVE_GOOGLE_DRIVE
+ #include "googledriveservice.h"
+#endif
#include
#include
@@ -64,6 +67,9 @@ InternetModel::InternetModel(Application* app, QObject* parent)
AddService(new JamendoService(app, this));
#ifdef HAVE_LIBLASTFM
AddService(new LastFMService(app, this));
+#endif
+#ifdef HAVE_GOOGLE_DRIVE
+ AddService(new GoogleDriveService(app, this));
#endif
AddService(new GroovesharkService(app, this));
AddService(new JazzRadioService(app, this));
diff --git a/src/internet/oauthenticator.cpp b/src/internet/oauthenticator.cpp
new file mode 100644
index 000000000..b2cf31e75
--- /dev/null
+++ b/src/internet/oauthenticator.cpp
@@ -0,0 +1,198 @@
+#include "oauthenticator.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "core/closure.h"
+
+namespace {
+
+const char* kGoogleOAuthEndpoint = "https://accounts.google.com/o/oauth2/auth";
+const char* kGoogleOAuthTokenEndpoint =
+ "https://accounts.google.com/o/oauth2/token";
+
+const char* kClientId = "679260893280.apps.googleusercontent.com";
+const char* kClientSecret = "l3cWb8efUZsrBI4wmY3uKl6i";
+
+} // namespace
+
+OAuthenticator::OAuthenticator(QObject* parent)
+ : QObject(parent) {
+}
+
+void OAuthenticator::StartAuthorisation() {
+ server_.listen(QHostAddress::LocalHost);
+ const quint16 port = server_.serverPort();
+
+ NewClosure(&server_, SIGNAL(newConnection()), this, SLOT(NewConnection()));
+
+ QUrl url = QUrl(kGoogleOAuthEndpoint);
+ url.addQueryItem("response_type", "code");
+ url.addQueryItem("client_id", kClientId);
+ url.addQueryItem("redirect_uri", QString("http://localhost:%1").arg(port));
+ url.addQueryItem("scope", "https://www.googleapis.com/auth/drive.readonly");
+
+ QDesktopServices::openUrl(url);
+}
+
+void OAuthenticator::NewConnection() {
+ QTcpSocket* socket = server_.nextPendingConnection();
+ server_.close();
+
+ QByteArray buffer;
+
+ NewClosure(socket, SIGNAL(readyRead()),
+ this, SLOT(RedirectArrived(QTcpSocket*, QByteArray)), socket, buffer);
+
+ // Everything is bon. Prepare and display the success page.
+ QFile page_file(":oauthsuccess.html");
+ page_file.open(QIODevice::ReadOnly);
+ QString page_data = QString::fromLatin1(page_file.readAll());
+
+ // Translate the strings inside
+ QRegExp tr_regexp("tr\\(\"([^\"]+)\"\\)");
+ int offset = 0;
+ forever {
+ offset = tr_regexp.indexIn(page_data, offset);
+ if (offset == -1) {
+ break;
+ }
+
+ page_data.replace(offset, tr_regexp.matchedLength(),
+ tr(tr_regexp.cap(1).toAscii()));
+ offset += tr_regexp.matchedLength();
+ }
+
+ // Add the tick image.
+ QBuffer image_buffer;
+ image_buffer.open(QIODevice::ReadWrite);
+ QApplication::style()->standardIcon(QStyle::SP_DialogOkButton)
+ .pixmap(16).toImage().save(&image_buffer, "PNG");
+
+ page_data.replace("@IMAGE_DATA@", image_buffer.data().toBase64());
+
+ socket->write("HTTP/1.0 200 OK\r\n");
+ socket->write("Content-type: text/html;charset=UTF-8\r\n");
+ socket->write("\r\n\r\n");
+ socket->write(page_data.toUtf8());
+ socket->flush();
+}
+
+void OAuthenticator::RedirectArrived(QTcpSocket* socket, QByteArray buffer) {
+ buffer.append(socket->readAll());
+
+ if (socket->atEnd() || buffer.endsWith("\r\n\r\n")) {
+ socket->deleteLater();
+ const QByteArray& code = ParseHttpRequest(buffer);
+ qLog(Debug) << "Code:" << code;
+ RequestAccessToken(code, socket->localPort());
+ } else {
+ NewClosure(socket, SIGNAL(readyReady()),
+ this, SLOT(RedirectArrived(QTcpSocket*, QByteArray)), socket, buffer);
+ }
+}
+
+QByteArray OAuthenticator::ParseHttpRequest(const QByteArray& request) const {
+ QList split = request.split('\r');
+ const QByteArray& request_line = split[0];
+ QByteArray path = request_line.split(' ')[1];
+ QByteArray code = path.split('=')[1];
+
+ return code;
+}
+
+void OAuthenticator::RequestAccessToken(const QByteArray& code, quint16 port) {
+ typedef QPair Param;
+ QList parameters;
+ parameters << Param("code", code)
+ << Param("client_id", kClientId)
+ << Param("client_secret", kClientSecret)
+ << Param("grant_type", "authorization_code")
+ // Even though we don't use this URI anymore, it must match the
+ // original one.
+ << Param("redirect_uri", QString("http://localhost:%1").arg(port));
+
+ QStringList params;
+ foreach (const Param& p, parameters) {
+ params.append(QString("%1=%2").arg(p.first, QString(QUrl::toPercentEncoding(p.second))));
+ }
+ QString post_data = params.join("&");
+ qLog(Debug) << post_data;
+
+ QNetworkRequest request = QNetworkRequest(QUrl(kGoogleOAuthTokenEndpoint));
+ request.setHeader(QNetworkRequest::ContentTypeHeader,
+ "application/x-www-form-urlencoded");
+
+ QNetworkReply* reply = network_.post(request, post_data.toUtf8());
+ NewClosure(reply, SIGNAL(finished()), this,
+ SLOT(FetchAccessTokenFinished(QNetworkReply*)), reply);
+}
+
+void OAuthenticator::FetchAccessTokenFinished(QNetworkReply* reply) {
+ reply->deleteLater();
+
+ if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 200) {
+ qLog(Error) << "Failed to get access token"
+ << reply->readAll();
+ return;
+ }
+
+ QJson::Parser parser;
+ bool ok = false;
+ QVariantMap result = parser.parse(reply, &ok).toMap();
+ if (!ok) {
+ qLog(Error) << "Failed to parse oauth reply";
+ return;
+ }
+
+ qLog(Debug) << result;
+
+ access_token_ = result["access_token"].toString();
+ refresh_token_ = result["refresh_token"].toString();
+
+ emit Finished();
+}
+
+void OAuthenticator::RefreshAuthorisation(const QString& refresh_token) {
+ refresh_token_ = refresh_token;
+
+ QUrl url = QUrl(kGoogleOAuthTokenEndpoint);
+
+ typedef QPair Param;
+ QList parameters;
+ parameters << Param("client_id", kClientId)
+ << Param("client_secret", kClientSecret)
+ << Param("grant_type", "refresh_token")
+ << Param("refresh_token", refresh_token);
+ QStringList params;
+ foreach (const Param& p, parameters) {
+ 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,
+ "application/x-www-form-urlencoded");
+ QNetworkReply* reply = network_.post(request, post_data.toUtf8());
+ NewClosure(reply, SIGNAL(finished()), this,
+ SLOT(RefreshAccessTokenFinished(QNetworkReply*)), reply);
+}
+
+void OAuthenticator::RefreshAccessTokenFinished(QNetworkReply* reply) {
+ reply->deleteLater();
+ QJson::Parser parser;
+ bool ok = false;
+
+ QVariantMap result = parser.parse(reply, &ok).toMap();
+ access_token_ = result["access_token"].toString();
+ emit Finished();
+}
diff --git a/src/internet/oauthenticator.h b/src/internet/oauthenticator.h
new file mode 100644
index 000000000..a073fb186
--- /dev/null
+++ b/src/internet/oauthenticator.h
@@ -0,0 +1,44 @@
+#ifndef OAUTHENTICATOR_H
+#define OAUTHENTICATOR_H
+
+#include
+#include
+
+#include "core/network.h"
+
+class QTcpSocket;
+
+class OAuthenticator : public QObject {
+ Q_OBJECT
+ public:
+ explicit OAuthenticator(QObject* parent = 0);
+ void StartAuthorisation();
+ void RefreshAuthorisation(const QString& refresh_token);
+
+ // Token to use now.
+ const QString& access_token() const { return access_token_; }
+
+ // Token to use to get a new access token when it expires.
+ const QString& refresh_token() const { return refresh_token_; }
+
+ signals:
+ void Finished();
+
+ private slots:
+ void NewConnection();
+ void RedirectArrived(QTcpSocket* socket, QByteArray buffer);
+ void FetchAccessTokenFinished(QNetworkReply* reply);
+ void RefreshAccessTokenFinished(QNetworkReply* reply);
+
+ private:
+ QByteArray ParseHttpRequest(const QByteArray& request) const;
+ void RequestAccessToken(const QByteArray& code, quint16 port);
+
+ QTcpServer server_;
+ NetworkAccessManager network_;
+
+ QString access_token_;
+ QString refresh_token_;
+};
+
+#endif
diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp
index fd7671376..868d06452 100644
--- a/src/ui/settingsdialog.cpp
+++ b/src/ui/settingsdialog.cpp
@@ -59,6 +59,10 @@
# include "internet/spotifysettingspage.h"
#endif
+#ifdef HAVE_GOOGLE_DRIVE
+# include "internet/googledrivesettingspage.h"
+#endif
+
#include
#include
#include
@@ -139,6 +143,10 @@ SettingsDialog::SettingsDialog(Application* app, BackgroundStreams* streams, QWi
AddPage(Page_Grooveshark, new GroovesharkSettingsPage(this), providers);
+#ifdef HAVE_GOOGLE_DRIVE
+ AddPage(Page_GoogleDrive, new GoogleDriveSettingsPage(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 b7db62d60..3d73b5691 100644
--- a/src/ui/settingsdialog.h
+++ b/src/ui/settingsdialog.h
@@ -76,6 +76,7 @@ public:
Page_Remote,
Page_Wiimotedev,
Page_Podcasts,
+ Page_GoogleDrive,
};
enum Role {