Merge branch 'drive'

This commit is contained in:
John Maguire 2012-08-13 16:04:38 -07:00
commit b9278c6c8b
36 changed files with 1908 additions and 2 deletions

View File

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

View File

@ -275,6 +275,7 @@
<file>providers/digitallyimported-32.png</file>
<file>providers/digitallyimported.png</file>
<file>providers/echonest.png</file>
<file>providers/googledrive.png</file>
<file>providers/grooveshark.png</file>
<file>providers/itunes.png</file>
<file>providers/jamendo.png</file>
@ -326,6 +327,7 @@
<file>schema/schema-35.sql</file>
<file>schema/schema-36.sql</file>
<file>schema/schema-37.sql</file>
<file>schema/schema-38.sql</file>
<file>schema/schema-3.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
@ -348,5 +350,6 @@
<file>volumeslider-handle_glow.png</file>
<file>volumeslider-handle.png</file>
<file>volumeslider-inset.png</file>
<file>oauthsuccess.html</file>
</qresource>
</RCC>

37
data/oauthsuccess.html Normal file
View File

@ -0,0 +1,37 @@
<html>
<head>
<title>tr("Return to Clementine")</title>
<style>
#container {
margin: 6em auto 0px auto;
max-width: 800px;
font-family: sans;
}
#container img {
width: 16px;
height: 16px;
float: left;
margin-right: 0.5em;
}
#container h1 {
margin: 0px 0px 0.75em 0px;
font-size: 15pt;
}
#container p {
margin-top: 0px;
}
</style>
</head>
<body>
<div id="container">
<h1>tr("Success!")</h1>
<img src="data:image/png;base64,@IMAGE_DATA@"/>
<p>tr("Please close your browser and return to Clementine.")</p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

50
data/schema/schema-38.sql Normal file
View File

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

View File

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

View File

@ -0,0 +1,185 @@
/* 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 "googledrivestream.h"
#include "core/logging.h"
#include <QEventLoop>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <taglib/id3v2framefactory.h>
#include <taglib/mpegfile.h>
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";
}

View File

@ -0,0 +1,80 @@
/* 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 GOOGLEDRIVESTREAM_H
#define GOOGLEDRIVESTREAM_H
#include <QUrl>
#include <google/sparsetable>
#include <taglib/tiostream.h>
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<char>::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<char> cache_;
int num_requests_;
};
#endif // GOOGLEDRIVESTREAM_H

View File

@ -24,6 +24,7 @@
#include <QCoreApplication>
#include <QDateTime>
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <QTextCodec>
#include <QUrl>
@ -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<pb::tagreader::Message>(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<TagLib::File> 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

View File

@ -18,11 +18,16 @@
#ifndef TAGREADERWORKER_H
#define TAGREADERWORKER_H
#include "config.h"
#include "tagreadermessages.pb.h"
#include "core/messagehandler.h"
#include <taglib/xiphcomment.h>
#include <QUrl>
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;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@
#include <QFile>
#include <QProcess>
#include <QTcpServer>
#include <QUrl>
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());

View File

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

View File

@ -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<Task> GetTasks();
@ -56,6 +74,8 @@ private:
QMutex mutex_;
QMap<int, Task> tasks_;
int next_task_id_;
Q_DISABLE_COPY(TaskManager);
};
#endif // TASKMANAGER_H

View File

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

View File

@ -0,0 +1,188 @@
/* 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;
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();
}

View File

@ -0,0 +1,166 @@
/* 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 <QStringList>
#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) {}
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<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);
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

View File

@ -0,0 +1,112 @@
/* 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 "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<google_drive::File>)),
this, SLOT(FilesFound(QList<google_drive::File>)));
NewClosure(reply, SIGNAL(Finished()),
this, SLOT(FindFilesFinished(ListFilesResponse*)),
reply);
}
void FolderModel::FindFilesFinished(ListFilesResponse* reply) {
reply->deleteLater();
}
void FolderModel::FilesFound(const QList<google_drive::File>& 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];
}

View File

@ -0,0 +1,66 @@
/* 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 GOOGLEDRIVEFOLDERMODEL_H
#define GOOGLEDRIVEFOLDERMODEL_H
#include "googledriveclient.h"
#include <QStandardItemModel>
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<google_drive::File>& files);
void FindFilesFinished(ListFilesResponse* reply);
private:
Client* client_;
QIcon folder_icon_;
QStandardItem* root_;
QMap<QString, QStandardItem*> item_by_id_;
QMap<QString, QList<google_drive::File> > orphans_;
};
} // namespace
#endif // GOOGLEDRIVEFOLDERMODEL_H

View File

@ -0,0 +1,209 @@
#include "googledriveservice.h"
#include <QEventLoop>
#include <QScopedPointer>
#include <QSortFilterProxyModel>
#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<google_drive::File>)),
this, SLOT(FilesFound(QList<google_drive::File>)));
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<google_drive::File>& 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<google_drive::GetFileResponse> 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;
}

View File

@ -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<google_drive::File>& 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

View File

@ -0,0 +1,90 @@
/* 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 "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 <QSortFilterProxyModel>
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<GoogleDriveService>();
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;
}
}
}

View File

@ -0,0 +1,57 @@
/* 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 GOOGLEDRIVESETTINGSPAGE_H
#define GOOGLEDRIVESETTINGSPAGE_H
#include "ui/settingspage.h"
#include <QModelIndex>
#include <QWidget>
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

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GoogleDriveSettingsPage</class>
<widget class="QWidget" name="GoogleDriveSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>569</width>
<height>491</height>
</rect>
</property>
<property name="windowTitle">
<string>Google Drive</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/providers/googledrive.png</normaloff>:/providers/googledrive.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Uploads</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>You can upload songs to Google Drive by right clicking and using &quot;Copy to device&quot;.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Upload new songs to</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QTreeView" name="upload_destination">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections/>
</ui>

View File

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

View File

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

View File

@ -37,6 +37,9 @@
#ifdef HAVE_SPOTIFY
#include "spotifyservice.h"
#endif
#ifdef HAVE_GOOGLE_DRIVE
#include "googledriveservice.h"
#endif
#include <QMimeData>
#include <QtDebug>
@ -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));

View File

@ -0,0 +1,198 @@
#include "oauthenticator.h"
#include <QApplication>
#include <QBuffer>
#include <QDesktopServices>
#include <QFile>
#include <QStringList>
#include <QStyle>
#include <QTcpSocket>
#include <QUrl>
#include <qjson/parser.h>
#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<QByteArray> 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<QString, QString> Param;
QList<Param> 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<QString, QString> Param;
QList<Param> 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();
}

View File

@ -0,0 +1,44 @@
#ifndef OAUTHENTICATOR_H
#define OAUTHENTICATOR_H
#include <QObject>
#include <QTcpServer>
#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

View File

@ -59,6 +59,10 @@
# include "internet/spotifysettingspage.h"
#endif
#ifdef HAVE_GOOGLE_DRIVE
# include "internet/googledrivesettingspage.h"
#endif
#include <QDesktopWidget>
#include <QPainter>
#include <QPushButton>
@ -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

View File

@ -76,6 +76,7 @@ public:
Page_Remote,
Page_Wiimotedev,
Page_Podcasts,
Page_GoogleDrive,
};
enum Role {