Merge branch 'drive'
This commit is contained in:
commit
b9278c6c8b
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 |
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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];
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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 "Copy to device".</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>
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -76,6 +76,7 @@ public:
|
|||
Page_Remote,
|
||||
Page_Wiimotedev,
|
||||
Page_Podcasts,
|
||||
Page_GoogleDrive,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
|
Loading…
Reference in New Issue