1 /* This file is part of Clementine.
2    Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
3    Copyright 2012, 2014, David Sansome <me@davidsansome.com>
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 "googledriveservice.h"
21 
22 #include <QDesktopServices>
23 #include <QEventLoop>
24 #include <QMenu>
25 #include <QMessageBox>
26 #include <QPushButton>
27 #include <QScopedPointer>
28 #include <QSortFilterProxyModel>
29 #include <QUrlQuery>
30 
31 #include "core/application.h"
32 #include "core/closure.h"
33 #include "core/database.h"
34 #include "core/mergedproxymodel.h"
35 #include "core/player.h"
36 #include "core/timeconstants.h"
37 #include "ui/albumcovermanager.h"
38 #include "globalsearch/globalsearch.h"
39 #include "globalsearch/librarysearchprovider.h"
40 #include "library/librarybackend.h"
41 #include "library/librarymodel.h"
42 #include "playlist/playlist.h"
43 #include "ui/iconloader.h"
44 #include "googledriveclient.h"
45 #include "googledriveurlhandler.h"
46 #include "internet/core/internetmodel.h"
47 
48 const char* GoogleDriveService::kServiceName = "Google Drive";
49 const char* GoogleDriveService::kSettingsGroup = "GoogleDrive";
50 
51 namespace {
52 
53 static const char* kDriveEditFileUrl = "https://docs.google.com/file/d/%1/edit";
54 static const char* kServiceId = "google_drive";
55 }
56 
GoogleDriveService(Application * app,InternetModel * parent)57 GoogleDriveService::GoogleDriveService(Application* app, InternetModel* parent)
58     : CloudFileService(app, parent, kServiceName, kServiceId,
59                        IconLoader::Load("googledrive", IconLoader::Provider),
60                        SettingsDialog::Page_GoogleDrive),
61       client_(new google_drive::Client(this)),
62       open_in_drive_action_(nullptr),
63       update_action_(nullptr),
64       full_rescan_action_(nullptr) {
65   app->player()->RegisterUrlHandler(new GoogleDriveUrlHandler(this, this));
66 }
67 
has_credentials() const68 bool GoogleDriveService::has_credentials() const {
69   return !refresh_token().isEmpty();
70 }
71 
refresh_token() const72 QString GoogleDriveService::refresh_token() const {
73   QSettings s;
74   s.beginGroup(kSettingsGroup);
75 
76   return s.value("refresh_token").toString();
77 }
78 
Connect()79 void GoogleDriveService::Connect() {
80   google_drive::ConnectResponse* response = client_->Connect(refresh_token());
81   NewClosure(response, SIGNAL(Finished()), this,
82              SLOT(ConnectFinished(google_drive::ConnectResponse*)), response);
83 }
84 
ForgetCredentials()85 void GoogleDriveService::ForgetCredentials() {
86   client_->ForgetCredentials();
87 
88   QSettings s;
89   s.beginGroup(kSettingsGroup);
90 
91   s.remove("refresh_token");
92   s.remove("user_email");
93 }
94 
ListChanges(const QString & cursor)95 void GoogleDriveService::ListChanges(const QString& cursor) {
96   google_drive::ListChangesResponse* changes_response =
97       client_->ListChanges(cursor);
98   connect(changes_response, SIGNAL(FilesFound(QList<google_drive::File>)),
99           SLOT(FilesFound(QList<google_drive::File>)));
100   connect(changes_response, SIGNAL(FilesDeleted(QList<QUrl>)),
101           SLOT(FilesDeleted(QList<QUrl>)));
102   NewClosure(changes_response, SIGNAL(Finished()), this,
103              SLOT(ListChangesFinished(google_drive::ListChangesResponse*)),
104              changes_response);
105 }
106 
ListChangesFinished(google_drive::ListChangesResponse * changes_response)107 void GoogleDriveService::ListChangesFinished(
108     google_drive::ListChangesResponse* changes_response) {
109   changes_response->deleteLater();
110 
111   const QString cursor = changes_response->next_cursor();
112   if (is_indexing()) {
113     // Only save the cursor after all the songs have been indexed - that way if
114     // Clementine is closed it'll resume next time.
115     NewClosure(this, SIGNAL(AllIndexingTasksFinished()), this,
116                SLOT(SaveCursor(QString)), cursor);
117   } else {
118     SaveCursor(cursor);
119   }
120 }
121 
SaveCursor(const QString & cursor)122 void GoogleDriveService::SaveCursor(const QString& cursor) {
123   QSettings s;
124   s.beginGroup(kSettingsGroup);
125   s.setValue("cursor", cursor);
126 }
127 
ConnectFinished(google_drive::ConnectResponse * response)128 void GoogleDriveService::ConnectFinished(
129     google_drive::ConnectResponse* response) {
130   response->deleteLater();
131 
132   // Save the refresh token
133   QSettings s;
134   s.beginGroup(kSettingsGroup);
135   s.setValue("refresh_token", response->refresh_token());
136 
137   if (!response->user_email().isEmpty()) {
138     // We only fetch the user's email address the first time we authenticate.
139     s.setValue("user_email", response->user_email());
140   }
141 
142   emit Connected();
143 
144   // Find all the changes since the last check.
145   CheckForUpdates();
146 }
147 
EnsureConnected()148 void GoogleDriveService::EnsureConnected() {
149   if (client_->is_authenticated()) {
150     return;
151   }
152 
153   QEventLoop loop;
154   connect(client_, SIGNAL(Authenticated()), &loop, SLOT(quit()));
155   Connect();
156   loop.exec();
157 }
158 
FilesFound(const QList<google_drive::File> & files)159 void GoogleDriveService::FilesFound(const QList<google_drive::File>& files) {
160   for (const google_drive::File& file : files) {
161     if (!IsSupportedMimeType(file.mime_type())) {
162       continue;
163     }
164 
165     QUrl url;
166     url.setScheme("googledrive");
167     url.setPath("/" + file.id());
168 
169     Song song;
170     // Add some extra tags from the Google Drive metadata.
171     song.set_etag(file.etag().remove('"'));
172     song.set_mtime(file.modified_date().toTime_t());
173     song.set_ctime(file.created_date().toTime_t());
174     song.set_comment(file.description());
175     song.set_directory_id(0);
176     song.set_url(QUrl(url));
177     song.set_filesize(file.size());
178 
179     // Use the Google Drive title if we couldn't read tags from the file.
180     if (song.title().isEmpty()) {
181       song.set_title(file.title());
182     }
183 
184     MaybeAddFileToDatabase(song, file.mime_type(), file.download_url(),
185                            QString("Bearer %1").arg(client_->access_token()));
186   }
187 }
188 
FilesDeleted(const QList<QUrl> & files)189 void GoogleDriveService::FilesDeleted(const QList<QUrl>& files) {
190   for (const QUrl& url : files) {
191     Song song = library_backend_->GetSongByUrl(url);
192     qLog(Debug) << "Deleting:" << url << song.title();
193     if (song.is_valid()) {
194       library_backend_->DeleteSongs(SongList() << song);
195     }
196   }
197 }
198 
GetStreamingUrlFromSongId(const QString & id)199 QUrl GoogleDriveService::GetStreamingUrlFromSongId(const QString& id) {
200   EnsureConnected();
201   QScopedPointer<google_drive::GetFileResponse> response(client_->GetFile(id));
202 
203   QEventLoop loop;
204   connect(response.data(), SIGNAL(Finished()), &loop, SLOT(quit()));
205   loop.exec();
206 
207   QUrl url(response->file().download_url());
208   QUrlQuery url_query(url);
209   url_query.addQueryItem("access_token", client_->access_token());
210   url.setQuery(url_query);
211   return url;
212 }
213 
ShowContextMenu(const QPoint & global_pos)214 void GoogleDriveService::ShowContextMenu(const QPoint& global_pos) {
215   if (!context_menu_) {
216     context_menu_.reset(new QMenu);
217     context_menu_->addActions(GetPlaylistActions());
218     open_in_drive_action_ = context_menu_->addAction(
219         IconLoader::Load("googledrive", IconLoader::Provider),
220         tr("Open in Google Drive"), this, SLOT(OpenWithDrive()));
221     context_menu_->addSeparator();
222     update_action_ = context_menu_->addAction(IconLoader::Load("view-refresh",
223                                               IconLoader::Base),
224                                               tr("Check for updates"), this,
225                                               SLOT(CheckForUpdates()));
226     full_rescan_action_ = context_menu_->addAction(
227         IconLoader::Load("view-refresh", IconLoader::Base),
228         tr("Do a full rescan..."), this, SLOT(ConfirmFullRescan()));
229     context_menu_->addSeparator();
230     context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
231                              tr("Cover Manager"), this,
232                              SLOT(ShowCoverManager()));
233     context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
234                              tr("Configure..."), this,
235                              SLOT(ShowSettingsDialog()));
236   }
237 
238   // Only show some actions if there are real songs selected
239   bool songs_selected = false;
240   for (const QModelIndex& index : model()->selected_indexes()) {
241     const int type = index.data(LibraryModel::Role_Type).toInt();
242     if (type == LibraryItem::Type_Song || type == LibraryItem::Type_Container) {
243       songs_selected = true;
244       break;
245     }
246   }
247 
248   open_in_drive_action_->setEnabled(songs_selected);
249   update_action_->setEnabled(!is_indexing());
250   full_rescan_action_->setEnabled(!is_indexing());
251 
252   context_menu_->popup(global_pos);
253 }
254 
OpenWithDrive()255 void GoogleDriveService::OpenWithDrive() {
256   // Map indexes to the actual library model.
257   QModelIndexList library_indexes;
258   for (const QModelIndex& index : model()->selected_indexes()) {
259     if (index.model() == library_sort_model_) {
260       library_indexes << library_sort_model_->mapToSource(index);
261     }
262   }
263 
264   // Ask the library for the songs for these indexes.
265   for (const Song& song : library_model_->GetChildSongs(library_indexes)) {
266     QDesktopServices::openUrl(
267         QUrl(QString(kDriveEditFileUrl).arg(song.url().path())));
268   }
269 }
270 
ConfirmFullRescan()271 void GoogleDriveService::ConfirmFullRescan() {
272   QMessageBox* message_box = new QMessageBox(
273       QMessageBox::Warning, tr("Do a full rescan"),
274       tr("Doing a full rescan will lose any metadata you've saved in "
275          "Clementine such as cover art, play counts and ratings.  Clementine "
276          "will rescan all your music in Google Drive which may take some "
277          "time."),
278       QMessageBox::NoButton);
279   QPushButton* button = message_box->addButton(tr("Do a full rescan"),
280                                                QMessageBox::DestructiveRole);
281   connect(button, SIGNAL(clicked()), SLOT(DoFullRescan()));
282 
283   message_box->addButton(QMessageBox::Cancel);
284   message_box->setAttribute(Qt::WA_DeleteOnClose);
285   message_box->show();
286 }
287 
DoFullRescan()288 void GoogleDriveService::DoFullRescan() {
289   QSettings s;
290   s.beginGroup(kSettingsGroup);
291   s.remove("cursor");
292 
293   library_backend_->DeleteAll();
294 
295   ListChanges(QString());
296 }
297 
CheckForUpdates()298 void GoogleDriveService::CheckForUpdates() {
299   QSettings s;
300   s.beginGroup(kSettingsGroup);
301   ListChanges(s.value("cursor").toString());
302 }
303