1 /* This file is part of Clementine.
2 Copyright 2012-2014, John Maguire <john.maguire@gmail.com>
3 Copyright 2013, Martin Brodbeck <martin@brodbeck-online.de>
4 Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
5
6 Clementine is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Clementine is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "dropboxservice.h"
21
22 #include <QFileInfo>
23 #include <QTimer>
24 #include <QJsonDocument>
25 #include <QJsonObject>
26 #include <QJsonArray>
27
28 #include "core/application.h"
29 #include "core/logging.h"
30 #include "core/network.h"
31 #include "core/player.h"
32 #include "core/utilities.h"
33 #include "core/waitforsignal.h"
34 #include "internet/core/oauthenticator.h"
35 #include "internet/dropbox/dropboxurlhandler.h"
36 #include "library/librarybackend.h"
37 #include "ui/iconloader.h"
38
39 using Utilities::ParseRFC822DateTime;
40
41 const char* DropboxService::kServiceName = "Dropbox";
42 const char* DropboxService::kSettingsGroup = "Dropbox";
43
44 namespace {
45
46 static const char* kServiceId = "dropbox";
47
48 static const char* kMediaEndpoint =
49 "https://api.dropboxapi.com/2/files/get_temporary_link";
50 static const char* kListFolderEndpoint =
51 "https://api.dropboxapi.com/2/files/list_folder";
52 static const char* kListFolderContinueEndpoint =
53 "https://api.dropboxapi.com/2/files/list_folder/continue";
54 static const char* kLongPollEndpoint =
55 "https://notify.dropboxapi.com/2/files/list_folder/longpoll";
56
57 } // namespace
58
DropboxService(Application * app,InternetModel * parent)59 DropboxService::DropboxService(Application* app, InternetModel* parent)
60 : CloudFileService(app, parent, kServiceName, kServiceId,
61 IconLoader::Load("dropbox", IconLoader::Provider),
62 SettingsDialog::Page_Dropbox),
63 network_(new NetworkAccessManager(this)) {
64 QSettings settings;
65 settings.beginGroup(kSettingsGroup);
66 // OAuth2 version of dropbox auth token.
67 access_token_ = settings.value("access_token2").toString();
68 app->player()->RegisterUrlHandler(new DropboxUrlHandler(this, this));
69 }
70
has_credentials() const71 bool DropboxService::has_credentials() const {
72 return !access_token_.isEmpty();
73 }
74
Connect()75 void DropboxService::Connect() {
76 if (has_credentials()) {
77 RequestFileList();
78 } else {
79 ShowSettingsDialog();
80 }
81 }
82
AuthenticationFinished(OAuthenticator * authenticator)83 void DropboxService::AuthenticationFinished(OAuthenticator* authenticator) {
84 authenticator->deleteLater();
85
86 access_token_ = authenticator->access_token();
87
88 QSettings settings;
89 settings.beginGroup(kSettingsGroup);
90 settings.setValue("access_token2", access_token_);
91
92 emit Connected();
93
94 RequestFileList();
95 }
96
GenerateAuthorisationHeader()97 QByteArray DropboxService::GenerateAuthorisationHeader() {
98 return QString("Bearer %1").arg(access_token_).toUtf8();
99 }
100
RequestFileList()101 void DropboxService::RequestFileList() {
102 QSettings s;
103 s.beginGroup(kSettingsGroup);
104
105 QString cursor = s.value("cursor", "").toString();
106
107 if (cursor.isEmpty()) {
108 QUrl url = QUrl(QString(kListFolderEndpoint));
109
110 QJsonObject json;
111 json.insert("path", "");
112 json.insert("recursive", true);
113 json.insert("include_deleted", true);
114
115 QNetworkRequest request(url);
116 request.setRawHeader("Authorization", GenerateAuthorisationHeader());
117 request.setRawHeader("Content-Type", "application/json; charset=utf-8");
118
119 QJsonDocument document(json);
120 QNetworkReply* reply = network_->post(request, document.toJson());
121 NewClosure(reply, SIGNAL(finished()), this,
122 SLOT(RequestFileListFinished(QNetworkReply*)), reply);
123 } else {
124 QUrl url = QUrl(kListFolderContinueEndpoint);
125 QJsonObject json;
126 json.insert("cursor", cursor);
127 QJsonDocument document(json);
128 QNetworkRequest request(url);
129 request.setRawHeader("Authorization", GenerateAuthorisationHeader());
130 request.setRawHeader("Content-Type", "application/json; charset=utf-8");
131 QNetworkReply* reply = network_->post(request, document.toJson());
132 NewClosure(reply, SIGNAL(finished()), this,
133 SLOT(RequestFileListFinished(QNetworkReply*)), reply);
134 }
135 }
136
RequestFileListFinished(QNetworkReply * reply)137 void DropboxService::RequestFileListFinished(QNetworkReply* reply) {
138 reply->deleteLater();
139
140 QJsonDocument document = QJsonDocument::fromBinaryData(reply->readAll());
141 QJsonObject json_response = document.object();
142
143 if (json_response.contains("reset") && json_response["reset"].toBool()) {
144 qLog(Debug) << "Resetting Dropbox DB";
145 library_backend_->DeleteAll();
146 }
147
148 QSettings settings;
149 settings.beginGroup(kSettingsGroup);
150 settings.setValue("cursor", json_response["cursor"].toString());
151
152 QJsonArray contents = json_response["entries"].toArray();
153 qLog(Debug) << "File list found:" << contents.size();
154 for (const QJsonValue& c : contents) {
155 QJsonObject item = c.toObject();
156 QString path = item["path_lower"].toString();
157
158 QUrl url;
159 url.setScheme("dropbox");
160 url.setPath(path);
161
162 if (item[".tag"].toString() == "deleted") {
163 qLog(Debug) << "Deleting:" << url;
164 Song song = library_backend_->GetSongByUrl(url);
165 if (song.is_valid()) {
166 library_backend_->DeleteSongs(SongList() << song);
167 }
168 continue;
169 }
170
171 if (item[".tag"].toString() == "folder") {
172 continue;
173 }
174
175 if (ShouldIndexFile(url, GuessMimeTypeForFile(url.toString()))) {
176 QNetworkReply* reply = FetchContentUrl(url);
177 NewClosure(reply, SIGNAL(finished()), this,
178 SLOT(FetchContentUrlFinished(QNetworkReply*, QVariantMap)),
179 reply, item);
180 }
181 }
182
183 if (json_response.contains("has_more") && json_response["has_more"].toBool()) {
184 QSettings s;
185 s.beginGroup(kSettingsGroup);
186 s.setValue("cursor", json_response["cursor"].toVariant());
187 RequestFileList();
188 } else {
189 // Long-poll wait for changes.
190 LongPollDelta();
191 }
192 }
193
LongPollDelta()194 void DropboxService::LongPollDelta() {
195 if (!has_credentials()) {
196 // Might have been signed out by the user.
197 return;
198 }
199 QSettings s;
200 s.beginGroup(kSettingsGroup);
201
202 QUrl request_url = QUrl(QString(kLongPollEndpoint));
203 QJsonObject json;
204 json.insert("cursor", s.value("cursor").toString());
205 json.insert("timeout", 30);
206 QNetworkRequest request(request_url);
207 request.setRawHeader("Content-Type", "application/json; charset=utf-8");
208 QJsonDocument document(json);
209 QNetworkReply* reply = network_->post(request, document.toJson());
210 NewClosure(reply, SIGNAL(finished()), this,
211 SLOT(LongPollFinished(QNetworkReply*)), reply);
212 }
213
LongPollFinished(QNetworkReply * reply)214 void DropboxService::LongPollFinished(QNetworkReply* reply) {
215 reply->deleteLater();
216 QJsonObject json_response = QJsonDocument::fromBinaryData(reply->readAll()).object();
217 if (json_response["changes"].toBool()) {
218 // New changes, we should request deltas again.
219 qLog(Debug) << "Detected new dropbox changes; fetching...";
220 RequestFileList();
221 } else {
222 bool ok = false;
223 int backoff_secs = json_response["backoff"].toString().toInt(&ok);
224 backoff_secs = ok ? backoff_secs : 0;
225
226 QTimer::singleShot(backoff_secs * 1000, this, SLOT(LongPollDelta()));
227 }
228 }
229
FetchContentUrl(const QUrl & url)230 QNetworkReply* DropboxService::FetchContentUrl(const QUrl& url) {
231 QUrl request_url(kMediaEndpoint);
232 QJsonObject json;
233 json.insert("path", url.path());
234 QJsonDocument document(json);
235 QNetworkRequest request(request_url);
236 request.setRawHeader("Authorization", GenerateAuthorisationHeader());
237 request.setRawHeader("Content-Type", "application/json; charset=utf-8");
238 return network_->post(request, document.toJson());
239 }
240
FetchContentUrlFinished(QNetworkReply * reply,const QVariantMap & data)241 void DropboxService::FetchContentUrlFinished(QNetworkReply* reply,
242 const QVariantMap& data) {
243 reply->deleteLater();
244 QJsonObject json_response = QJsonDocument::fromBinaryData(reply->readAll()).object();
245 QFileInfo info(data["path_lower"].toString());
246
247 QUrl url;
248 url.setScheme("dropbox");
249 url.setPath(data["path_lower"].toString());
250
251 Song song;
252 song.set_url(url);
253 song.set_etag(data["rev"].toString());
254 song.set_mtime(QDateTime::fromString(data["server_modified"].toString(),
255 Qt::ISODate).toTime_t());
256 song.set_title(info.fileName());
257 song.set_filesize(data["size"].toInt());
258 song.set_ctime(0);
259
260 MaybeAddFileToDatabase(
261 song, GuessMimeTypeForFile(url.toString()),
262 QUrl::fromEncoded(json_response["link"].toVariant().toByteArray()),
263 QString());
264 }
265
GetStreamingUrlFromSongId(const QUrl & url)266 QUrl DropboxService::GetStreamingUrlFromSongId(const QUrl& url) {
267 QNetworkReply* reply = FetchContentUrl(url);
268 WaitForSignal(reply, SIGNAL(finished()));
269
270 QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
271 return QUrl::fromEncoded(json_response["link"].toVariant().toByteArray());
272 }
273