/* This file is part of Clementine. Copyright 2012-2014, John Maguire Copyright 2014, Krzysztof Sobiecki Copyright 2014, David Sansome 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 . */ #include "skydriveservice.h" #include #include #include #include #include #include "core/application.h" #include "core/player.h" #include "core/waitforsignal.h" #include "internet/core/oauthenticator.h" #include "internet/skydrive/skydriveurlhandler.h" #include "library/librarybackend.h" #include "ui/iconloader.h" namespace { // Keep legacy name since it needs to match the database table name. static const char* kServiceId = "skydrive"; static const char* kClientId = "905def38-34d2-4e32-8ba7-c37bcc329047"; static const char* kClientSecret = ""; // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow static const char* kOAuthEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; static const char* kOAuthTokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; static const char* kOAuthScope = "User.Read Files.Read Files.Read.All offline_access"; // MS Graph API // https://docs.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0 static const char* kGraphUserInfo = "https://graph.microsoft.com/v1.0/me"; static const char* kDriveBase = "https://graph.microsoft.com/v1.0/me/drive/"; } // namespace const char* SkydriveService::kServiceName = "OneDrive"; const char* SkydriveService::kSettingsGroup = "OneDrive"; SkydriveService::SkydriveService(Application* app, InternetModel* parent) : CloudFileService(app, parent, kServiceName, kServiceId, IconLoader::Load("skydrive", IconLoader::Provider), SettingsDialog::Page_Skydrive) { app->player()->RegisterUrlHandler(new SkydriveUrlHandler(this, this)); } bool SkydriveService::has_credentials() const { return !refresh_token().isEmpty(); } QString SkydriveService::refresh_token() const { QSettings s; s.beginGroup(kSettingsGroup); return s.value("refresh_token").toString(); } void SkydriveService::Connect() { OAuthenticator* oauth = new OAuthenticator( kClientId, kClientSecret, OAuthenticator::RedirectStyle::LOCALHOST, this); if (!refresh_token().isEmpty()) { oauth->RefreshAuthorisation(kOAuthTokenEndpoint, refresh_token()); } else { oauth->StartAuthorisation(kOAuthEndpoint, kOAuthTokenEndpoint, kOAuthScope); } NewClosure(oauth, SIGNAL(Finished()), this, SLOT(ConnectFinished(OAuthenticator*)), oauth); } void SkydriveService::ConnectFinished(OAuthenticator* oauth) { oauth->deleteLater(); QSettings s; s.beginGroup(kSettingsGroup); s.setValue("refresh_token", oauth->refresh_token()); access_token_ = oauth->access_token(); expiry_time_ = oauth->expiry_time(); FetchUserInfo(); } void SkydriveService::FetchUserInfo() { QUrl url(kGraphUserInfo); QNetworkRequest request(url); AddAuthorizationHeader(&request); QNetworkReply* reply = network_->get(request); NewClosure(reply, SIGNAL(finished()), this, SLOT(FetchUserInfoFinished(QNetworkReply*)), reply); } QByteArray SkydriveService::GetAuthHeader() const { return QString("Bearer %1").arg(access_token_).toUtf8(); } void SkydriveService::AddAuthorizationHeader(QNetworkRequest* request) { request->setRawHeader("Authorization", GetAuthHeader()); } void SkydriveService::FetchUserInfoFinished(QNetworkReply* reply) { reply->deleteLater(); QJsonDocument document = ParseJsonReply(reply); if (document.isNull()) return; QJsonObject json_response = document.object(); QString name = json_response["displayName"].toString(); if (!name.isEmpty()) { QSettings s; s.beginGroup(kSettingsGroup); s.setValue("name", name); } emit Connected(); ListFiles(); } void SkydriveService::ListFiles() { ListFiles("root"); } void SkydriveService::ListFiles(const QString& folder) { QUrl url(QString(kDriveBase) + folder + "/children"); QNetworkRequest request(url); AddAuthorizationHeader(&request); QNetworkReply* reply = network_->get(request); NewClosure(reply, SIGNAL(finished()), this, SLOT(ListFilesFinished(QNetworkReply*)), reply); } void SkydriveService::ListFilesFinished(QNetworkReply* reply) { reply->deleteLater(); QJsonDocument document = ParseJsonReply(reply); if (document.isNull()) return; QJsonObject json_response = document.object(); QJsonArray items = json_response["value"].toArray(); for (const QJsonValue& f : items) { QJsonObject item = f.toObject(); const QString id = item["id"].toString(); const QString name = item["name"].toString(); if (item.contains("folder")) { ListFiles(QString("items/%1").arg(id)); } else if (item.contains("file")) { // The response provides a mime type, but it doesn't know about some // types that we care about. QString mime_type = GuessMimeTypeForFile(name); QUrl url; url.setScheme(GetScheme()); url.setPath("/" + id); Song song; song.set_url(url); song.set_ctime( QDateTime::fromString(item["createdDateTime"].toString(), Qt::ISODate) .toTime_t()); song.set_mtime(QDateTime::fromString( item["lastModifiedDateTime"].toString(), Qt::ISODate) .toTime_t()); song.set_comment(item["description"].toString()); song.set_filesize(item["size"].toInt()); song.set_title(name); QUrl download_url = ItemUrl(id, "content"); MaybeAddFileToDatabase(song, mime_type, download_url, QString("Bearer %1").arg(access_token_)); } else { qLog(Debug) << "Unknown item type for" << name; } } } QUrl SkydriveService::ItemUrl(const QString& id, const QString& path) { return QUrl(QString(kDriveBase) + "items/" + id + "/" + path); } QUrl SkydriveService::GetStreamingUrlFromSongId(const QString& file_id) { EnsureConnected(); return ItemUrl(file_id, "content"); } void SkydriveService::PopulateContextMenu() { context_menu_->addActions(GetPlaylistActions()); full_rescan_action_ = context_menu_->addAction( IconLoader::Load("view-refresh", IconLoader::Base), tr("Do a full rescan..."), this, SLOT(FullRescanRequested())); context_menu_->addSeparator(); context_menu_->addAction(IconLoader::Load("download", IconLoader::Base), tr("Cover Manager"), this, SLOT(ShowCoverManager())); context_menu_->addSeparator(); context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base), tr("Configure..."), this, SLOT(ShowConfig())); } void SkydriveService::UpdateContextMenu() { full_rescan_action_->setEnabled(!is_indexing()); } void SkydriveService::EnsureConnected() { if (!access_token_.isEmpty()) { return; } Connect(); WaitForSignal(this, SIGNAL(Connected())); } void SkydriveService::ForgetCredentials() { QSettings s; s.beginGroup(kSettingsGroup); s.remove("refresh_token"); s.remove("name"); } void SkydriveService::DoFullRescan() { library_backend_->DeleteAll(); ListFiles(); }