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 2013-2014, David Sansome <me@davidsansome.com>
5    Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
6 
7    Clementine is free software: you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation, either version 3 of the License, or
10    (at your option) any later version.
11 
12    Clementine is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16 
17    You should have received a copy of the GNU General Public License
18    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #include "internet/core/cloudfileservice.h"
22 
23 #include <QMenu>
24 #include <QSortFilterProxyModel>
25 
26 #include "core/application.h"
27 #include "core/database.h"
28 #include "core/mergedproxymodel.h"
29 #include "core/network.h"
30 #include "core/player.h"
31 #include "core/taskmanager.h"
32 #include "globalsearch/globalsearch.h"
33 #include "internet/core/cloudfilesearchprovider.h"
34 #include "internet/core/internetmodel.h"
35 #include "library/librarybackend.h"
36 #include "library/librarymodel.h"
37 #include "playlist/playlist.h"
38 #include "ui/iconloader.h"
39 
CloudFileService(Application * app,InternetModel * parent,const QString & service_name,const QString & service_id,const QIcon & icon,SettingsDialog::Page settings_page)40 CloudFileService::CloudFileService(Application* app, InternetModel* parent,
41                                    const QString& service_name,
42                                    const QString& service_id, const QIcon& icon,
43                                    SettingsDialog::Page settings_page)
44     : InternetService(service_name, app, parent, parent),
45       root_(nullptr),
46       network_(new NetworkAccessManager(this)),
47       library_sort_model_(new QSortFilterProxyModel(this)),
48       playlist_manager_(app->playlist_manager()),
49       task_manager_(app->task_manager()),
50       icon_(icon),
51       settings_page_(settings_page),
52       indexing_task_id_(-1),
53       indexing_task_progress_(0),
54       indexing_task_max_(0) {
55   library_backend_ = new LibraryBackend;
56   library_backend_->moveToThread(app_->database()->thread());
57 
58   QString songs_table = service_id + "_songs";
59   QString songs_fts_table = service_id + "_songs_fts";
60 
61   library_backend_->Init(app->database(), songs_table, QString(), QString(),
62                          songs_fts_table);
63   library_model_ = new LibraryModel(library_backend_, app_, this);
64 
65   library_sort_model_->setSourceModel(library_model_);
66   library_sort_model_->setSortRole(LibraryModel::Role_SortText);
67   library_sort_model_->setDynamicSortFilter(true);
68   library_sort_model_->setSortLocaleAware(true);
69   library_sort_model_->sort(0);
70 
71   app->global_search()->AddProvider(
72       new CloudFileSearchProvider(library_backend_, service_id, icon_, this));
73 }
74 
CreateRootItem()75 QStandardItem* CloudFileService::CreateRootItem() {
76   root_ = new QStandardItem(icon_, name());
77   root_->setData(true, InternetModel::Role_CanLazyLoad);
78   return root_;
79 }
80 
LazyPopulate(QStandardItem * item)81 void CloudFileService::LazyPopulate(QStandardItem* item) {
82   switch (item->data(InternetModel::Role_Type).toInt()) {
83     case InternetModel::Type_Service:
84       if (!has_credentials()) {
85         ShowSettingsDialog();
86       } else {
87         Connect();
88       }
89       library_model_->Init();
90       model()->merged_model()->AddSubModel(item->index(), library_sort_model_);
91       break;
92 
93     default:
94       break;
95   }
96 }
97 
ShowContextMenu(const QPoint & global_pos)98 void CloudFileService::ShowContextMenu(const QPoint& global_pos) {
99   if (!context_menu_) {
100     context_menu_.reset(new QMenu);
101     context_menu_->addActions(GetPlaylistActions());
102     context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
103                              tr("Cover Manager"), this,
104                              SLOT(ShowCoverManager()));
105     context_menu_->addSeparator();
106     context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
107                              tr("Configure..."), this,
108                              SLOT(ShowSettingsDialog()));
109   }
110   context_menu_->popup(global_pos);
111 }
112 
ShowCoverManager()113 void CloudFileService::ShowCoverManager() {
114   if (!cover_manager_) {
115     cover_manager_.reset(new AlbumCoverManager(app_, library_backend_));
116     cover_manager_->Init();
117     connect(cover_manager_.get(), SIGNAL(AddToPlaylist(QMimeData*)),
118             SLOT(AddToPlaylist(QMimeData*)));
119   }
120   cover_manager_->show();
121 }
122 
AddToPlaylist(QMimeData * mime)123 void CloudFileService::AddToPlaylist(QMimeData* mime) {
124   playlist_manager_->current()->dropMimeData(mime, Qt::CopyAction, -1, 0,
125                                              QModelIndex());
126 }
127 
ShowSettingsDialog()128 void CloudFileService::ShowSettingsDialog() {
129   app_->OpenSettingsDialogAtPage(settings_page_);
130 }
131 
ShouldIndexFile(const QUrl & url,const QString & mime_type) const132 bool CloudFileService::ShouldIndexFile(const QUrl& url,
133                                        const QString& mime_type) const {
134   if (!IsSupportedMimeType(mime_type)) {
135     return false;
136   }
137   Song library_song = library_backend_->GetSongByUrl(url);
138   if (library_song.is_valid()) {
139     qLog(Debug) << "Already have:" << url;
140     return false;
141   }
142   return true;
143 }
144 
MaybeAddFileToDatabase(const Song & metadata,const QString & mime_type,const QUrl & download_url,const QString & authorisation)145 void CloudFileService::MaybeAddFileToDatabase(const Song& metadata,
146                                               const QString& mime_type,
147                                               const QUrl& download_url,
148                                               const QString& authorisation) {
149   if (!ShouldIndexFile(metadata.url(), mime_type)) {
150     return;
151   }
152 
153   if (indexing_task_id_ == -1) {
154     indexing_task_id_ = task_manager_->StartTask(tr("Indexing %1").arg(name()));
155     indexing_task_progress_ = 0;
156     indexing_task_max_ = 0;
157   }
158   indexing_task_max_++;
159   task_manager_->SetTaskProgress(indexing_task_id_, indexing_task_progress_,
160                                  indexing_task_max_);
161 
162   TagReaderClient::ReplyType* reply = app_->tag_reader_client()->ReadCloudFile(
163       download_url, metadata.title(), metadata.filesize(), mime_type,
164       authorisation);
165   pending_tagreader_replies_.append(reply);
166 
167   NewClosure(reply, SIGNAL(Finished(bool)), this,
168              SLOT(ReadTagsFinished(TagReaderClient::ReplyType*, Song)), reply,
169              metadata);
170 }
171 
ReadTagsFinished(TagReaderClient::ReplyType * reply,const Song & metadata)172 void CloudFileService::ReadTagsFinished(TagReaderClient::ReplyType* reply,
173                                         const Song& metadata) {
174   int index_reply;
175 
176   reply->deleteLater();
177 
178   if ((index_reply = pending_tagreader_replies_.indexOf(reply)) == -1) {
179     qLog(Debug) << "Ignore the reply";
180     return;
181   }
182 
183   pending_tagreader_replies_.removeAt(index_reply);
184 
185   indexing_task_progress_++;
186   if (indexing_task_progress_ == indexing_task_max_) {
187     task_manager_->SetTaskFinished(indexing_task_id_);
188     indexing_task_id_ = -1;
189     emit AllIndexingTasksFinished();
190   } else {
191     task_manager_->SetTaskProgress(indexing_task_id_, indexing_task_progress_,
192                                    indexing_task_max_);
193   }
194 
195   const pb::tagreader::ReadCloudFileResponse& message =
196       reply->message().read_cloud_file_response();
197   if (!message.has_metadata() || !message.metadata().filesize()) {
198     qLog(Debug) << "Failed to tag:" << metadata.url();
199     return;
200   }
201 
202   pb::tagreader::SongMetadata metadata_pb;
203   metadata.ToProtobuf(&metadata_pb);
204   metadata_pb.MergeFrom(message.metadata());
205 
206   Song song;
207   song.InitFromProtobuf(metadata_pb);
208   song.set_directory_id(0);
209 
210   qLog(Debug) << "Adding song to db:" << song.title();
211   library_backend_->AddOrUpdateSongs(SongList() << song);
212 }
213 
IsSupportedMimeType(const QString & mime_type) const214 bool CloudFileService::IsSupportedMimeType(const QString& mime_type) const {
215   return mime_type == "audio/ogg" || mime_type == "audio/mpeg" ||
216          mime_type == "audio/mp4" || mime_type == "audio/flac" ||
217          mime_type == "audio/x-flac" || mime_type == "application/ogg" ||
218          mime_type == "application/x-flac" || mime_type == "audio/x-ms-wma";
219 }
220 
GuessMimeTypeForFile(const QString & filename) const221 QString CloudFileService::GuessMimeTypeForFile(const QString& filename) const {
222   if (filename.endsWith(".mp3", Qt::CaseInsensitive)) {
223     return "audio/mpeg";
224   } else if (filename.endsWith(".m4a", Qt::CaseInsensitive) ||
225              filename.endsWith(".m4b", Qt::CaseInsensitive)) {
226     return "audio/mpeg";
227   } else if (filename.endsWith(".ogg", Qt::CaseInsensitive) ||
228              filename.endsWith(".opus", Qt::CaseInsensitive)) {
229     return "application/ogg";
230   } else if (filename.endsWith(".flac", Qt::CaseInsensitive)) {
231     return "application/x-flac";
232   } else if (filename.endsWith(".wma", Qt::CaseInsensitive)) {
233     return "audio/x-ms-wma";
234   }
235   return QString();
236 }
237 
AbortReadTagsReplies()238 void CloudFileService::AbortReadTagsReplies() {
239   qLog(Debug) << "Aborting the read tags replies";
240   pending_tagreader_replies_.clear();
241 
242   task_manager_->SetTaskFinished(indexing_task_id_);
243   indexing_task_id_ = -1;
244   emit AllIndexingTasksFinished();
245 }
246