From 8d3fd00956df75ecfe0a252879fc31c42a6e4455 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Fri, 27 Mar 2015 14:56:08 +0100 Subject: [PATCH] Amazon Cloud Drive support. Squashed commit of the following: commit 451a327fabb5f9aba077d93a33d75d8a6a288f5f Author: John Maguire Date: Fri Mar 27 14:55:36 2015 +0100 Revert debug console changes. commit 52f643c3dc524a837f56268b6da4881187204165 Author: John Maguire Date: Fri Mar 27 14:49:28 2015 +0100 Revert extra logging commit 23645f9fea4caa65d93c2a0a5ad5e2a164c3b535 Author: John Maguire Date: Fri Mar 27 14:47:55 2015 +0100 How did you get there commit 8153388f19db17caf4286618922516b495a3f1d3 Author: John Maguire Date: Fri Mar 27 14:45:12 2015 +0100 Update copyright headers. commit fa9e279259604a16564287291180b69cbb22d74f Author: John Maguire Date: Fri Mar 27 14:43:27 2015 +0100 Remove logging commit 47a405543c8f6924adb60fbc34ec7360c608a9ec Author: John Maguire Date: Fri Mar 27 14:42:05 2015 +0100 Show login state correctly for Amazon. commit 748d88d993fb56ecd97e14b8e7c7b6c49f11c410 Author: John Maguire Date: Fri Mar 27 14:28:55 2015 +0100 Ensure Amazon is connected before serving URLs. commit 25ec9c65f4b0be4fc2df13cf941cf236f7cf6b46 Author: John Maguire Date: Fri Mar 27 14:22:28 2015 +0100 Refresh Amazon authorisation & follow changes. commit 27c1a37173a76e04341b87abe2ada8438d6ee59f Author: John Maguire Date: Thu Mar 26 18:27:27 2015 +0100 Revert unneeded OAuthenticator change. commit 3594af5be12d979762719010535db8f5aaec0905 Author: John Maguire Date: Thu Mar 26 16:52:19 2015 +0100 Initial support for Amazon Cloud Drive. --- CMakeLists.txt | 5 + data/data.qrc | 2 + data/providers/amazonclouddrive.png | Bin 0 -> 3102 bytes data/schema/schema-48.sql | 50 +++++ src/CMakeLists.txt | 14 ++ src/config.h.in | 1 + src/core/database.cpp | 2 +- src/engines/gstenginepipeline.cpp | 9 + src/internet/amazon/amazonclouddrive.cpp | 246 +++++++++++++++++++++ src/internet/amazon/amazonclouddrive.h | 71 ++++++ src/internet/amazon/amazonsettingspage.cpp | 80 +++++++ src/internet/amazon/amazonsettingspage.h | 53 +++++ src/internet/amazon/amazonsettingspage.ui | 110 +++++++++ src/internet/amazon/amazonurlhandler.cpp | 28 +++ src/internet/amazon/amazonurlhandler.h | 39 ++++ src/internet/core/internetmodel.cpp | 6 + src/ui/settingsdialog.cpp | 8 + src/ui/settingsdialog.h | 3 +- 18 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 data/providers/amazonclouddrive.png create mode 100644 data/schema/schema-48.sql create mode 100644 src/internet/amazon/amazonclouddrive.cpp create mode 100644 src/internet/amazon/amazonclouddrive.h create mode 100644 src/internet/amazon/amazonsettingspage.cpp create mode 100644 src/internet/amazon/amazonsettingspage.h create mode 100644 src/internet/amazon/amazonsettingspage.ui create mode 100644 src/internet/amazon/amazonurlhandler.cpp create mode 100644 src/internet/amazon/amazonurlhandler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 97d2b9b3a..94f2bf979 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,6 +215,11 @@ optional_component(SEAFILE ON "Seafile support" DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999" ) +optional_component(AMAZON_CLOUD_DRIVE ON "Amazon Cloud Drive support" + DEPENDS "Google sparsehash" SPARSEHASH_INCLUDE_DIRS + DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999" +) + optional_component(AUDIOCD ON "Devices: Audio CD support" DEPENDS "libcdio" CDIO_FOUND ) diff --git a/data/data.qrc b/data/data.qrc index 162c2f7ff..afd87697b 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -310,6 +310,7 @@ playstore/uk_generic_rgb_wo_45.png playstore/vi_generic_rgb_wo_45.png providers/amazon.png + providers/amazonclouddrive.png providers/aol.png providers/bbc.png providers/box.png @@ -385,6 +386,7 @@ schema/schema-45.sql schema/schema-46.sql schema/schema-47.sql + schema/schema-48.sql schema/schema-4.sql schema/schema-5.sql schema/schema-6.sql diff --git a/data/providers/amazonclouddrive.png b/data/providers/amazonclouddrive.png new file mode 100644 index 0000000000000000000000000000000000000000..d3707287ab61e17e4adc60a6e1e96f5cafe9ce32 GIT binary patch literal 3102 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 literal 0 HcmV?d00001 diff --git a/data/schema/schema-48.sql b/data/schema/schema-48.sql new file mode 100644 index 000000000..bdff64858 --- /dev/null +++ b/data/schema/schema-48.sql @@ -0,0 +1,50 @@ +CREATE TABLE amazon_cloud_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, + performer TEXT, + grouping TEXT +); + +CREATE VIRTUAL TABLE amazon_cloud_drive_songs_fts USING fts3 ( + ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, + tokenize=unicode +); + +UPDATE schema_version SET version=48; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a80c5994..355b605db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1178,6 +1178,20 @@ optional_source(HAVE_SEAFILE internet/seafile/seafilesettingspage.ui ) +# Amazon Cloud Drive support +optional_source(HAVE_AMAZON_CLOUD_DRIVE + SOURCES + internet/amazon/amazonclouddrive.cpp + internet/amazon/amazonsettingspage.cpp + internet/amazon/amazonurlhandler.cpp + HEADERS + internet/amazon/amazonclouddrive.h + internet/amazon/amazonsettingspage.h + internet/amazon/amazonurlhandler.h + UI + internet/amazon/amazonsettingspage.ui +) + # Pulse audio integration optional_source(HAVE_LIBPULSE diff --git a/src/config.h.in b/src/config.h.in index f9279072f..342919fbc 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -21,6 +21,7 @@ #define CMAKE_EXECUTABLE_SUFFIX "${CMAKE_EXECUTABLE_SUFFIX}" #cmakedefine ENABLE_VISUALISATIONS +#cmakedefine HAVE_AMAZON_CLOUD_DRIVE #cmakedefine HAVE_AUDIOCD #cmakedefine HAVE_BOX #cmakedefine HAVE_BREAKPAD diff --git a/src/core/database.cpp b/src/core/database.cpp index e351aa32e..e3bbee56a 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -47,7 +47,7 @@ #include const char* Database::kDatabaseFilename = "clementine.db"; -const int Database::kSchemaVersion = 47; +const int Database::kSchemaVersion = 48; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index 7f48f1fb2..5887a6dbd 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -903,6 +903,15 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin, g_object_set(element, "extra-headers", headers, nullptr); gst_structure_free(headers); } + if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), + "extra-headers") && + instance->url().host().contains("amazonaws.com")) { + GstStructure* headers = gst_structure_new( + "extra-headers", "Authorization", G_TYPE_STRING, + instance->url().fragment().toAscii().data(), nullptr); + g_object_set(element, "extra-headers", headers, nullptr); + gst_structure_free(headers); + } if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "user-agent")) { QString user_agent = diff --git a/src/internet/amazon/amazonclouddrive.cpp b/src/internet/amazon/amazonclouddrive.cpp new file mode 100644 index 000000000..7b2dd1be0 --- /dev/null +++ b/src/internet/amazon/amazonclouddrive.cpp @@ -0,0 +1,246 @@ +/* This file is part of Clementine. + Copyright 2015, John Maguire + + 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 "internet/amazon/amazonclouddrive.h" + +#include + +#include +#include + +#include "core/application.h" +#include "core/closure.h" +#include "core/logging.h" +#include "core/network.h" +#include "core/player.h" +#include "core/waitforsignal.h" +#include "internet/core/oauthenticator.h" +#include "internet/amazon/amazonurlhandler.h" +#include "library/librarybackend.h" +#include "ui/settingsdialog.h" + +const char* AmazonCloudDrive::kServiceName = "Cloud Drive"; +const char* AmazonCloudDrive::kSettingsGroup = "AmazonCloudDrive"; + +namespace { +static const char* kServiceId = "amazon_cloud_drive"; +static const char* kClientId = + "amzn1.application-oa2-client.2b1157a7dadc45c3888567882b3a9f05"; +static const char* kClientSecret = + "acfbf95340cc4c381dd43fb75b5e111882d7fd1b02a02f3013ab124baf8d1655"; +static const char* kOAuthScope = "clouddrive:read"; +static const char* kOAuthEndpoint = "https://www.amazon.com/ap/oa"; +static const char* kOAuthTokenEndpoint = "https://api.amazon.com/auth/o2/token"; + +static const char* kEndpointEndpoint = + "https://drive.amazonaws.com/drive/v1/account/endpoint"; +static const char* kChangesEndpoint = "%1/changes"; +static const char* kDownloadEndpoint = "%1/nodes/%2/content"; +} // namespace + +AmazonCloudDrive::AmazonCloudDrive(Application* app, InternetModel* parent) + : CloudFileService(app, parent, kServiceName, kServiceId, + QIcon(":/providers/amazonclouddrive.png"), + SettingsDialog::Page_AmazonCloudDrive), + network_(new NetworkAccessManager) { + app->player()->RegisterUrlHandler(new AmazonUrlHandler(this, this)); +} + +bool AmazonCloudDrive::has_credentials() const { + QSettings s; + s.beginGroup(kSettingsGroup); + return !s.value("refresh_token").toString().isEmpty(); +} + +QUrl AmazonCloudDrive::GetStreamingUrlFromSongId(const QUrl& url) { + EnsureConnected(); // Access token must be up to date. + QUrl download_url( + QString(kDownloadEndpoint).arg(content_url_).arg(url.path())); + download_url.setFragment(QString("Bearer %1").arg(access_token_)); + return download_url; +} + +void AmazonCloudDrive::Connect() { + OAuthenticator* oauth = new OAuthenticator( + kClientId, kClientSecret, + // Amazon forbids arbitrary query parameters so REMOTE_WITH_STATE is + // required. + OAuthenticator::RedirectStyle::REMOTE_WITH_STATE, this); + + QSettings s; + s.beginGroup(kSettingsGroup); + QString refresh_token = s.value("refresh_token").toString(); + if (refresh_token.isEmpty()) { + oauth->StartAuthorisation(kOAuthEndpoint, kOAuthTokenEndpoint, kOAuthScope); + } else { + oauth->RefreshAuthorisation(kOAuthTokenEndpoint, refresh_token); + } + + NewClosure(oauth, SIGNAL(Finished()), this, + SLOT(ConnectFinished(OAuthenticator*)), oauth); +} + +void AmazonCloudDrive::EnsureConnected() { + if (access_token_.isEmpty() || + QDateTime::currentDateTime().secsTo(expiry_time_) < 60) { + Connect(); + WaitForSignal(this, SIGNAL(Connected())); + } +} + +void AmazonCloudDrive::ForgetCredentials() { + QSettings s; + s.beginGroup(kSettingsGroup); + s.remove(""); + access_token_ = QString(); + expiry_time_ = QDateTime(); +} + +void AmazonCloudDrive::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(); + + FetchEndpoint(); +} + +void AmazonCloudDrive::FetchEndpoint() { + QUrl url(kEndpointEndpoint); + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->get(request); + NewClosure(reply, SIGNAL(finished()), this, + SLOT(FetchEndpointFinished(QNetworkReply*)), reply); +} + +void AmazonCloudDrive::FetchEndpointFinished(QNetworkReply* reply) { + reply->deleteLater(); + QJson::Parser parser; + QVariantMap response = parser.parse(reply).toMap(); + content_url_ = response["contentUrl"].toString(); + metadata_url_ = response["metadataUrl"].toString(); + QSettings s; + s.beginGroup(kSettingsGroup); + QString checkpoint = s.value("checkpoint", "").toString(); + RequestChanges(checkpoint); + + // We wait until we know the endpoint URLs before emitting Connected(); + emit Connected(); +} + +void AmazonCloudDrive::RequestChanges(const QString& checkpoint) { + EnsureConnected(); + QUrl url(QString(kChangesEndpoint).arg(metadata_url_)); + + QVariantMap data; + data["includePurged"] = "true"; + if (!checkpoint.isEmpty()) { + data["checkpoint"] = checkpoint; + } + QJson::Serializer serializer; + QByteArray json = serializer.serialize(data); + + QNetworkRequest request(url); + AddAuthorizationHeader(&request); + QNetworkReply* reply = network_->post(request, json); + NewClosure(reply, SIGNAL(finished()), this, + SLOT(RequestChangesFinished(QNetworkReply*)), reply); +} + +void AmazonCloudDrive::RequestChangesFinished(QNetworkReply* reply) { + reply->deleteLater(); + + QByteArray data = reply->readAll(); + QBuffer buffer(&data); + buffer.open(QIODevice::ReadOnly); + + QJson::Parser parser; + QVariantMap response = parser.parse(&buffer).toMap(); + + QString checkpoint = response["checkpoint"].toString(); + QSettings settings; + settings.beginGroup(kSettingsGroup); + settings.setValue("checkpoint", checkpoint); + + QVariantList nodes = response["nodes"].toList(); + for (const QVariant& n : nodes) { + QVariantMap node = n.toMap(); + if (node["kind"].toString() == "FOLDER") { + // Skip directories. + continue; + } + QUrl url; + url.setScheme("amazonclouddrive"); + url.setPath(node["id"].toString()); + + QString status = node["status"].toString(); + if (status == "PURGED") { + // Remove no longer available files. + Song song = library_backend_->GetSongByUrl(url); + if (song.is_valid()) { + library_backend_->DeleteSongs(SongList() << song); + } + continue; + } + if (status != "AVAILABLE") { + // Ignore any other statuses. + continue; + } + + QVariantMap content_properties = node["contentProperties"].toMap(); + QString mime_type = content_properties["contentType"].toString(); + + if (ShouldIndexFile(url, mime_type)) { + QString node_id = node["id"].toString(); + QUrl content_url( + QString(kDownloadEndpoint).arg(content_url_).arg(node_id)); + QString md5 = content_properties["md5"].toString(); + + Song song; + song.set_url(url); + song.set_etag(md5); + song.set_mtime(node["modifiedDate"].toDateTime().toTime_t()); + song.set_ctime(node["createdDate"].toDateTime().toTime_t()); + song.set_title(node["name"].toString()); + song.set_filesize(content_properties["size"].toInt()); + + MaybeAddFileToDatabase(song, mime_type, content_url, QString("Bearer %1").arg(access_token_)); + } + } + + // The API potentially returns a second JSON dictionary appended with a + // newline at the end of the response with {"end": true} indicating that our + // client is up to date with the latest changes. + const int last_newline_index = data.lastIndexOf('\n'); + QByteArray last_line = data.mid(last_newline_index); + QVariantMap end_json = parser.parse(last_line).toMap(); + if (end_json.contains("end") && end_json["end"].toBool()) { + return; + } else { + RequestChanges(checkpoint); + } +} + +void AmazonCloudDrive::AddAuthorizationHeader(QNetworkRequest* request) { + request->setRawHeader("Authorization", + QString("Bearer %1").arg(access_token_).toUtf8()); +} diff --git a/src/internet/amazon/amazonclouddrive.h b/src/internet/amazon/amazonclouddrive.h new file mode 100644 index 000000000..49350dee1 --- /dev/null +++ b/src/internet/amazon/amazonclouddrive.h @@ -0,0 +1,71 @@ +/* This file is part of Clementine. + Copyright 2015, John Maguire + + 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 . +*/ + +#ifndef INTERNET_AMAZON_AMAZON_CLOUD_DRIVE_H_ +#define INTERNET_AMAZON_AMAZON_CLOUD_DRIVE_H_ + +#include "internet/core/cloudfileservice.h" + +#include +#include +#include + +class NetworkAccessManager; +class OAuthenticator; +class QNetworkReply; +class QNetworkRequest; + +class AmazonCloudDrive : public CloudFileService { + Q_OBJECT + public: + AmazonCloudDrive(Application* app, InternetModel* parent); + + static const char* kServiceName; + static const char* kSettingsGroup; + + virtual bool has_credentials() const; + + QUrl GetStreamingUrlFromSongId(const QUrl& url); + + void ForgetCredentials(); + + signals: + void Connected(); + + public slots: + void Connect(); + + private: + void FetchEndpoint(); + void RequestChanges(const QString& checkpoint); + void AddAuthorizationHeader(QNetworkRequest* request); + void EnsureConnected(); + + private slots: + void ConnectFinished(OAuthenticator*); + void FetchEndpointFinished(QNetworkReply*); + void RequestChangesFinished(QNetworkReply*); + + private: + NetworkAccessManager* network_; + QString access_token_; + QDateTime expiry_time_; + QString content_url_; + QString metadata_url_; +}; + +#endif // INTERNET_AMAZON_AMAZON_CLOUD_DRIVE_H_ diff --git a/src/internet/amazon/amazonsettingspage.cpp b/src/internet/amazon/amazonsettingspage.cpp new file mode 100644 index 000000000..0fd41fcce --- /dev/null +++ b/src/internet/amazon/amazonsettingspage.cpp @@ -0,0 +1,80 @@ +/* This file is part of Clementine. + Copyright 2015, John Maguire + + 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 "amazonsettingspage.h" +#include "ui_amazonsettingspage.h" + +#include "core/application.h" +#include "internet/amazon/amazonclouddrive.h" +#include "internet/core/internetmodel.h" +#include "ui/settingsdialog.h" + +AmazonSettingsPage::AmazonSettingsPage(SettingsDialog* parent) + : SettingsPage(parent), + ui_(new Ui::AmazonSettingsPage), + service_(dialog()->app()->internet_model()->Service()) { + ui_->setupUi(this); + ui_->login_state->AddCredentialGroup(ui_->login_container); + + connect(ui_->login_button, SIGNAL(clicked()), SLOT(LoginClicked())); + connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); + connect(service_, SIGNAL(Connected()), SLOT(Connected())); + + dialog()->installEventFilter(this); +} + +AmazonSettingsPage::~AmazonSettingsPage() { delete ui_; } + +void AmazonSettingsPage::Load() { + QSettings s; + s.beginGroup(AmazonCloudDrive::kSettingsGroup); + + const QString token = s.value("refresh_token").toString(); + + if (!token.isEmpty()) { + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); + } +} + +void AmazonSettingsPage::Save() { + QSettings s; + s.beginGroup(AmazonCloudDrive::kSettingsGroup); +} + +void AmazonSettingsPage::LoginClicked() { + service_->Connect(); + ui_->login_button->setEnabled(false); + ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress); +} + +bool AmazonSettingsPage::eventFilter(QObject* object, QEvent* event) { + if (object == dialog() && event->type() == QEvent::Enter) { + ui_->login_button->setEnabled(true); + return false; + } + + return SettingsPage::eventFilter(object, event); +} + +void AmazonSettingsPage::LogoutClicked() { + service_->ForgetCredentials(); + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); +} + +void AmazonSettingsPage::Connected() { + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); +} diff --git a/src/internet/amazon/amazonsettingspage.h b/src/internet/amazon/amazonsettingspage.h new file mode 100644 index 000000000..8ff6b303e --- /dev/null +++ b/src/internet/amazon/amazonsettingspage.h @@ -0,0 +1,53 @@ +/* This file is part of Clementine. + Copyright 2015, John Maguire + + 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 . +*/ + +#ifndef INTERNET_AMAZON_AMAZONSETTINGSPAGE_H_ +#define INTERNET_AMAZON_AMAZONSETTINGSPAGE_H_ + +#include "ui/settingspage.h" + +#include +#include + +class AmazonCloudDrive; +class Ui_AmazonSettingsPage; + +class AmazonSettingsPage : public SettingsPage { + Q_OBJECT + + public: + explicit AmazonSettingsPage(SettingsDialog* parent = nullptr); + ~AmazonSettingsPage(); + + void Load(); + void Save(); + + // QObject + bool eventFilter(QObject* object, QEvent* event); + + private slots: + void LoginClicked(); + void LogoutClicked(); + void Connected(); + + private: + Ui_AmazonSettingsPage* ui_; + + AmazonCloudDrive* service_; +}; + +#endif // INTERNET_AMAZON_AMAZONSETTINGSPAGE_H_ diff --git a/src/internet/amazon/amazonsettingspage.ui b/src/internet/amazon/amazonsettingspage.ui new file mode 100644 index 000000000..d496f3424 --- /dev/null +++ b/src/internet/amazon/amazonsettingspage.ui @@ -0,0 +1,110 @@ + + + AmazonSettingsPage + + + + 0 + 0 + 569 + 491 + + + + Amazon + + + + :/providers/amazonclouddrive.png:/providers/amazonclouddrive.png + + + + + + Clementine can play music that you have uploaded to Amazon Cloud Drive + + + true + + + + + + + + + + + 28 + + + 0 + + + 0 + + + + + + + Login + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Clicking the Login button will open a web browser. You should return to Clementine after you have logged in. + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 357 + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + + + +
diff --git a/src/internet/amazon/amazonurlhandler.cpp b/src/internet/amazon/amazonurlhandler.cpp new file mode 100644 index 000000000..42433a3f1 --- /dev/null +++ b/src/internet/amazon/amazonurlhandler.cpp @@ -0,0 +1,28 @@ +/* This file is part of Clementine. + Copyright 2015, John Maguire + + 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 "internet/amazon/amazonurlhandler.h" + +#include "internet/amazon/amazonclouddrive.h" + +AmazonUrlHandler::AmazonUrlHandler(AmazonCloudDrive* service, QObject* parent) + : UrlHandler(parent), service_(service) {} + +UrlHandler::LoadResult AmazonUrlHandler::StartLoading(const QUrl& url) { + return LoadResult(url, LoadResult::TrackAvailable, + service_->GetStreamingUrlFromSongId(url)); +} diff --git a/src/internet/amazon/amazonurlhandler.h b/src/internet/amazon/amazonurlhandler.h new file mode 100644 index 000000000..807f24732 --- /dev/null +++ b/src/internet/amazon/amazonurlhandler.h @@ -0,0 +1,39 @@ +/* This file is part of Clementine. + Copyright 2015, John Maguire + + 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 . +*/ + +#ifndef INTERNET_AMAZON_AMAZONURLHANDLER_H_ +#define INTERNET_AMAZON_AMAZONURLHANDLER_H_ + +#include "core/urlhandler.h" + +class AmazonCloudDrive; + +class AmazonUrlHandler : public UrlHandler { + Q_OBJECT + public: + explicit AmazonUrlHandler( + AmazonCloudDrive* service, QObject* parent = nullptr); + + QString scheme() const { return "amazonclouddrive"; } + QIcon icon() const { return QIcon(":providers/amazonclouddrive.png"); } + LoadResult StartLoading(const QUrl& url); + + private: + AmazonCloudDrive* service_; +}; + +#endif // INTERNET_AMAZON_AMAZONURLHANDLER_H_ diff --git a/src/internet/core/internetmodel.cpp b/src/internet/core/internetmodel.cpp index 4783466ee..09754cd5e 100644 --- a/src/internet/core/internetmodel.cpp +++ b/src/internet/core/internetmodel.cpp @@ -64,6 +64,9 @@ #ifdef HAVE_SEAFILE #include "internet/seafile/seafileservice.h" #endif +#ifdef HAVE_AMAZON_CLOUD_DRIVE +#include "internet/amazon/amazonclouddrive.h" +#endif using smart_playlists::Generator; using smart_playlists::GeneratorMimeData; @@ -117,6 +120,9 @@ InternetModel::InternetModel(Application* app, QObject* parent) #ifdef HAVE_VK AddService(new VkService(app, this)); #endif +#ifdef HAVE_AMAZON_CLOUD_DRIVE + AddService(new AmazonCloudDrive(app, this)); +#endif invisibleRootItem()->sortChildren(0, Qt::AscendingOrder); UpdateServices(); diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index b82e00a27..5cfb1bda8 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -84,6 +84,10 @@ #include "internet/seafile/seafilesettingspage.h" #endif +#ifdef HAVE_AMAZON_CLOUD_DRIVE +#include "internet/amazon/amazonsettingspage.h" +#endif + #include #include #include @@ -193,6 +197,10 @@ SettingsDialog::SettingsDialog(Application* app, BackgroundStreams* streams, AddPage(Page_Seafile, new SeafileSettingsPage(this), providers); #endif +#ifdef HAVE_AMAZON_CLOUD_DRIVE + AddPage(Page_AmazonCloudDrive, new AmazonSettingsPage(this), providers); +#endif + AddPage(Page_Magnatune, new MagnatuneSettingsPage(this), providers); AddPage(Page_DigitallyImported, new DigitallyImportedSettingsPage(this), providers); diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h index 2c4a29db6..607ecc0e8 100644 --- a/src/ui/settingsdialog.h +++ b/src/ui/settingsdialog.h @@ -86,7 +86,8 @@ class SettingsDialog : public QDialog { Page_Box, Page_Vk, Page_Seafile, - Page_InternetShow + Page_InternetShow, + Page_AmazonCloudDrive, }; enum Role { Role_IsSeparator = Qt::UserRole };