diff --git a/data/data.qrc b/data/data.qrc index 56191eb88..50b68b300 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -1,6 +1,7 @@ blank.ttf + clementine_remote_qr.png clementine-spotify-public.pem currenttrack_bar_left.png currenttrack_bar_mid.png @@ -296,6 +297,8 @@ providers/somafm.png providers/songkick.png providers/soundcloud.png + providers/subsonic-32.png + providers/subsonic.png providers/ubuntuone.png providers/wikipedia.png sample.mood @@ -337,6 +340,7 @@ schema/schema-40.sql schema/schema-41.sql schema/schema-42.sql + schema/schema-43.sql schema/schema-4.sql schema/schema-5.sql schema/schema-6.sql @@ -358,6 +362,5 @@ volumeslider-handle_glow.png volumeslider-handle.png volumeslider-inset.png - clementine_remote_qr.png diff --git a/data/providers/subsonic-32.png b/data/providers/subsonic-32.png new file mode 100644 index 000000000..74221eb34 Binary files /dev/null and b/data/providers/subsonic-32.png differ diff --git a/data/providers/subsonic.png b/data/providers/subsonic.png new file mode 100644 index 000000000..efbfe3a29 Binary files /dev/null and b/data/providers/subsonic.png differ diff --git a/data/schema/schema-43.sql b/data/schema/schema-43.sql new file mode 100644 index 000000000..1ec71aec1 --- /dev/null +++ b/data/schema/schema-43.sql @@ -0,0 +1,48 @@ +CREATE TABLE subsonic_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 +); + +CREATE VIRTUAL TABLE subsonic_songs_fts USING fts3 ( + ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment, + tokenize=unicode +); + +UPDATE schema_version SET version=43; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 51ca21997..1dcdcee08 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,6 +194,9 @@ set(SOURCES internet/somafmservice.cpp internet/somafmurlhandler.cpp internet/soundcloudservice.cpp + internet/subsonicservice.cpp + internet/subsonicsettingspage.cpp + internet/subsonicurlhandler.cpp library/groupbydialog.cpp library/library.cpp @@ -478,6 +481,9 @@ set(HEADERS internet/somafmservice.h internet/somafmurlhandler.h internet/soundcloudservice.h + internet/subsonicservice.h + internet/subsonicsettingspage.h + internet/subsonicurlhandler.h library/groupbydialog.h library/library.h @@ -660,6 +666,7 @@ set(UI internet/magnatunesettingspage.ui internet/searchboxwidget.ui internet/spotifysettingspage.ui + internet/subsonicsettingspage.ui library/groupbydialog.ui library/libraryfilterwidget.ui diff --git a/src/core/database.cpp b/src/core/database.cpp index 0d3f605a6..9eef7c8b1 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -37,7 +37,7 @@ #include const char* Database::kDatabaseFilename = "clementine.db"; -const int Database::kSchemaVersion = 42; +const int Database::kSchemaVersion = 43; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index 34e9f2c76..f2b418a7b 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -29,7 +29,9 @@ #include "internetservice.h" #include "savedradio.h" #include "somafmservice.h" +#include "groovesharkservice.h" #include "soundcloudservice.h" +#include "subsonicservice.h" #include "core/closure.h" #include "core/logging.h" #include "core/mergedproxymodel.h" @@ -94,6 +96,7 @@ InternetModel::InternetModel(Application* app, QObject* parent) #ifdef HAVE_SPOTIFY AddService(new SpotifyService(app, this)); #endif + AddService(new SubsonicService(app, this)); #ifdef HAVE_UBUNTU_ONE AddService(new UbuntuOneService(app, this)); #endif diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp new file mode 100644 index 000000000..f188cebd5 --- /dev/null +++ b/src/internet/subsonicservice.cpp @@ -0,0 +1,433 @@ +#include "subsonicurlhandler.h" +#include "subsonicservice.h" +#include "internetmodel.h" +#include "core/application.h" +#include "core/logging.h" +#include "core/player.h" +#include "core/utilities.h" +#include "ui/iconloader.h" +#include "library/librarybackend.h" +#include "library/libraryfilterwidget.h" +#include "core/mergedproxymodel.h" +#include "core/database.h" +#include "core/closure.h" +#include "core/taskmanager.h" +#include "globalsearch/globalsearch.h" +#include "globalsearch/librarysearchprovider.h" + +#include +#include +#include +#include +#include +#include +#include + +const char* SubsonicService::kServiceName = "Subsonic"; +const char* SubsonicService::kSettingsGroup = "Subsonic"; +const char* SubsonicService::kApiVersion = "1.8.0"; +const char* SubsonicService::kApiClientName = "Clementine"; + +const char* SubsonicService::kSongsTable = "subsonic_songs"; +const char* SubsonicService::kFtsTable = "subsonic_songs_fts"; + +SubsonicService::SubsonicService(Application* app, InternetModel *parent) + : InternetService(kServiceName, app, parent, parent), + network_(new QNetworkAccessManager(this)), + url_handler_(new SubsonicUrlHandler(this, this)), + scanner_(new SubsonicLibraryScanner(this, this)), + load_database_task_id_(0), + context_menu_(NULL), + root_(NULL), + library_backend_(NULL), + library_model_(NULL), + library_filter_(NULL), + library_sort_model_(new QSortFilterProxyModel(this)), + total_song_count_(0), + login_state_(LoginState_OtherError) +{ + app_->player()->RegisterUrlHandler(url_handler_); + + connect(scanner_, SIGNAL(ScanFinished()), + SLOT(ReloadDatabaseFinished())); + + library_backend_ = new LibraryBackend; + library_backend_->moveToThread(app_->database()->thread()); + library_backend_->Init(app_->database(), + kSongsTable, + QString::null, + QString::null, + kFtsTable); + connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)), + SLOT(UpdateTotalSongCount(int))); + + library_model_ = new LibraryModel(library_backend_, app_, this); + library_model_->set_show_various_artists(false); + library_model_->set_show_smart_playlists(false); + + library_filter_ = new LibraryFilterWidget(0); + library_filter_->SetSettingsGroup(kSettingsGroup); + library_filter_->SetLibraryModel(library_model_); + library_filter_->SetFilterHint(tr("Search Subsonic")); + library_filter_->SetAgeFilterEnabled(false); + + library_sort_model_->setSourceModel(library_model_); + library_sort_model_->setSortRole(LibraryModel::Role_SortText); + library_sort_model_->setDynamicSortFilter(true); + library_sort_model_->sort(0); + + connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), + SLOT(onLoginStateChanged(SubsonicService::LoginState))); + + context_menu_ = new QMenu; + context_menu_->addActions(GetPlaylistActions()); + context_menu_->addSeparator(); + context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Refresh catalogue"), this, SLOT(ReloadDatabase())); + QAction* config_action = context_menu_->addAction(IconLoader::Load("configure"), + tr("Configure Subsonic..."), this, SLOT(ShowConfig())); + context_menu_->addSeparator(); + context_menu_->addMenu(library_filter_->menu()); + + library_filter_->AddMenuAction(config_action); + + app_->global_search()->AddProvider(new LibrarySearchProvider( + library_backend_, + tr("Subsonic"), + "subsonic", + QIcon(":/providers/subsonic.png"), + true, app_, this)); +} + +SubsonicService::~SubsonicService() +{ +} + +QStandardItem* SubsonicService::CreateRootItem() +{ + root_ = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName); + root_->setData(true, InternetModel::Role_CanLazyLoad); + return root_; +} + +void SubsonicService::LazyPopulate(QStandardItem *item) +{ + switch (item->data(InternetModel::Role_Type).toInt()) + { + case InternetModel::Type_Service: + library_model_->Init(); + if (login_state() != LoginState_Loggedin) { + ShowConfig(); + } else if (total_song_count_ == 0 && !load_database_task_id_) { + ReloadDatabase(); + } + model()->merged_model()->AddSubModel(item->index(), library_sort_model_); + break; + + default: + break; + } +} + +void SubsonicService::ShowContextMenu(const QPoint &global_pos) +{ + const bool is_valid = model()->current_index().model() == library_sort_model_; + + GetAppendToPlaylistAction()->setEnabled(is_valid); + GetReplacePlaylistAction()->setEnabled(is_valid); + GetOpenInNewPlaylistAction()->setEnabled(is_valid); + context_menu_->popup(global_pos); +} + +QWidget* SubsonicService::HeaderWidget() const +{ + return library_filter_; +} + +void SubsonicService::ReloadSettings() +{ + QSettings s; + s.beginGroup(kSettingsGroup); + + server_ = s.value("server").toString(); + username_ = s.value("username").toString(); + password_ = s.value("password").toString(); + + Login(); +} + +void SubsonicService::Login() +{ + // Forget session ID + network_->setCookieJar(new QNetworkCookieJar(network_)); + // Forget login state whilst waiting + login_state_ = LoginState_Unknown; + // Ping is enough to check credentials + Ping(); +} + +void SubsonicService::Login(const QString &server, const QString &username, const QString &password) +{ + server_ = QString(server); + username_ = QString(username); + password_ = QString(password); + Login(); +} + +void SubsonicService::Ping() +{ + QNetworkReply* reply = Send(BuildRequestUrl("ping")); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(onPingFinished(QNetworkReply*)), + reply); +} + +QUrl SubsonicService::BuildRequestUrl(const QString &view) +{ + QUrl url(server_ + "rest/" + view + ".view"); + url.addQueryItem("v", kApiVersion); + url.addQueryItem("c", kApiClientName); + url.addQueryItem("u", username_); + url.addQueryItem("p", password_); + return url; +} + +QNetworkReply* SubsonicService::Send(const QUrl &url) +{ + QNetworkRequest request(url); + // Don't try and check the authenticity of the SSL certificate - it'll almost + // certainly be self-signed. + QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration(); + sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone); + request.setSslConfiguration(sslconfig); + QNetworkReply *reply = network_->get(request); + return reply; +} + +void SubsonicService::UpdateTotalSongCount(int count) +{ + total_song_count_ = count; +} + +void SubsonicService::ReloadDatabase() +{ + if (!load_database_task_id_) + load_database_task_id_ = app_->task_manager()->StartTask(tr("Fetching Subsonic library")); + scanner_->Scan(); +} + +void SubsonicService::ReloadDatabaseFinished() +{ + app_->task_manager()->SetTaskFinished(load_database_task_id_); + load_database_task_id_ = 0; + + library_backend_->DeleteAll(); + library_backend_->AddOrUpdateSongs(scanner_->GetSongs()); + library_model_->Reset(); +} + +void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) +{ + // TODO: library refresh logic? +} + +void SubsonicService::onPingFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) + { + login_state_ = LoginState_BadServer; + qLog(Error) << "Failed to connect (" + << Utilities::EnumToString(QNetworkReply::staticMetaObject, "NetworkError", reply->error()) + << "):" << reply->errorString(); + } + else + { + QXmlStreamReader reader(reply); + reader.readNextStartElement(); + QStringRef status = reader.attributes().value("status"); + if (status == "ok") + { + login_state_ = LoginState_Loggedin; + } + else + { + reader.readNextStartElement(); + int error = reader.attributes().value("code").toString().toInt(); + qLog(Error) << "Subsonic error (" + << Utilities::EnumToString(SubsonicService::staticMetaObject, "ApiError", error) + << "):" << reader.attributes().value("message").toString(); + switch (error) + { + // "Parameter missing" for "ping" is always blank username or password + case ApiError_ParameterMissing: + case ApiError_BadCredentials: + login_state_ = LoginState_BadCredentials; + break; + case ApiError_OutdatedClient: + login_state_ = LoginState_OutdatedClient; + break; + case ApiError_OutdatedServer: + login_state_ = LoginState_OutdatedServer; + break; + case ApiError_Unlicensed: + login_state_ = LoginState_Unlicensed; + break; + default: + login_state_ = LoginState_OtherError; + break; + } + } + } + qLog(Debug) << "Login state changed:" + << Utilities::EnumToString(SubsonicService::staticMetaObject, "LoginState", login_state_); + emit LoginStateChanged(login_state_); +} + +void SubsonicService::ShowConfig() +{ + app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Subsonic); +} + + +const int SubsonicLibraryScanner::kAlbumChunkSize = 500; +const int SubsonicLibraryScanner::kConcurrentRequests = 8; + +SubsonicLibraryScanner::SubsonicLibraryScanner(SubsonicService* service, QObject* parent) + : QObject(parent), + service_(service), + scanning_(false) +{ +} + +SubsonicLibraryScanner::~SubsonicLibraryScanner() +{ +} + +void SubsonicLibraryScanner::Scan() +{ + if (scanning_) + return; + + album_queue_.clear(); + pending_requests_.clear(); + songs_.clear(); + scanning_ = true; + GetAlbumList(0); +} + +void SubsonicLibraryScanner::onGetAlbumListFinished(QNetworkReply *reply, int offset) +{ + reply->deleteLater(); + + QXmlStreamReader reader(reply); + reader.readNextStartElement(); + Q_ASSERT(reader.name() == "subsonic-response"); + if (reader.attributes().value("status") != "ok") { + // TODO: error handling + return; + } + + int albums_added = 0; + reader.readNextStartElement(); + Q_ASSERT(reader.name() == "albumList2"); + while (reader.readNextStartElement()) { + Q_ASSERT(reader.name() == "album"); + album_queue_ << reader.attributes().value("id").toString(); + albums_added++; + reader.skipCurrentElement(); + } + + if (albums_added > 0) { + // Non-empty reply means potentially more albums to fetch + GetAlbumList(offset + kAlbumChunkSize); + } else if (album_queue_.size() == 0) { + // Empty reply and no albums means an empty Subsonic server + scanning_ = false; + } else { + // Empty reply but we have some albums, time to start fetching songs + // Start up the maximum number of concurrent requests, finished requests get replaced with new ones + for (int i = 0; i < kConcurrentRequests && !album_queue_.empty(); ++i) { + GetAlbum(album_queue_.dequeue()); + } + } +} + +void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + pending_requests_.remove(reply); + + QXmlStreamReader reader(reply); + reader.readNextStartElement(); + Q_ASSERT(reader.name() == "subsonic-response"); + if (reader.attributes().value("status") != "ok") { + // TODO: error handling + return; + } + + // Read album information + reader.readNextStartElement(); + Q_ASSERT(reader.name() == "album"); + QString album_artist = reader.attributes().value("artist").toString(); + + // Read song information + while (reader.readNextStartElement()) { + Q_ASSERT(reader.name() == "song"); + Song song; + QString id = reader.attributes().value("id").toString(); + song.set_title(reader.attributes().value("title").toString()); + song.set_album(reader.attributes().value("album").toString()); + song.set_track(reader.attributes().value("track").toString().toInt()); + song.set_artist(reader.attributes().value("artist").toString()); + song.set_albumartist(album_artist); + song.set_bitrate(reader.attributes().value("bitRate").toString().toInt()); + song.set_year(reader.attributes().value("year").toString().toInt()); + song.set_genre(reader.attributes().value("genre").toString()); + qint64 length = reader.attributes().value("duration").toString().toInt(); + length *= 1000000000; + song.set_length_nanosec(length); + QUrl url = QUrl(QString("subsonic://%1").arg(id)); + song.set_url(url); + song.set_filesize(reader.attributes().value("size").toString().toInt()); + // We need to set these to satisfy the database constraints + song.set_directory_id(0); + song.set_mtime(0); + song.set_ctime(0); + songs_ << song; + reader.skipCurrentElement(); + } + + // Start the next request if albums remain + if (!album_queue_.empty()) + GetAlbum(album_queue_.dequeue()); + + // If this was the last response, we're done! + if (album_queue_.empty() && pending_requests_.empty()) { + scanning_ = false; + emit ScanFinished(); + } +} + +void SubsonicLibraryScanner::GetAlbumList(int offset) +{ + QUrl url = service_->BuildRequestUrl("getAlbumList2"); + url.addQueryItem("type", "alphabeticalByName"); + url.addQueryItem("size", QString::number(kAlbumChunkSize)); + url.addQueryItem("offset", QString::number(offset)); + QNetworkReply* reply = service_->Send(url); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(onGetAlbumListFinished(QNetworkReply*,int)), + reply, offset); +} + +void SubsonicLibraryScanner::GetAlbum(QString id) +{ + QUrl url = service_->BuildRequestUrl("getAlbum"); + url.addQueryItem("id", id); + QNetworkReply* reply = service_->Send(url); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(onGetAlbumFinished(QNetworkReply*)), + reply); + pending_requests_.insert(reply); +} diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h new file mode 100644 index 000000000..e6b7342ae --- /dev/null +++ b/src/internet/subsonicservice.h @@ -0,0 +1,157 @@ +#ifndef SUBSONICSERVICE_H +#define SUBSONICSERVICE_H + +#include "internetmodel.h" +#include "internetservice.h" + +#include + +class QNetworkAccessManager; +class QXmlStreamReader; +class QSortFilterProxyModel; +class QNetworkReply; + +class SubsonicUrlHandler; +class SubsonicLibraryScanner; + +class SubsonicService : public InternetService +{ + Q_OBJECT + Q_ENUMS(LoginState) + Q_ENUMS(ApiError) + + public: + SubsonicService(Application* app, InternetModel *parent); + ~SubsonicService(); + + enum LoginState { + LoginState_Loggedin, + LoginState_BadServer, + LoginState_OutdatedClient, + LoginState_OutdatedServer, + LoginState_BadCredentials, + LoginState_Unlicensed, + LoginState_OtherError, + LoginState_Unknown, + }; + + enum ApiError { + ApiError_Generic = 0, + ApiError_ParameterMissing = 10, + ApiError_OutdatedClient = 20, + ApiError_OutdatedServer = 30, + ApiError_BadCredentials = 40, + ApiError_Unauthorized = 50, + ApiError_Unlicensed = 60, + ApiError_NotFound = 70, + }; + + enum Type { + Type_Artist = InternetModel::TypeCount, + Type_Album, + Type_Track, + }; + + enum Role { + Role_Id = InternetModel::RoleCount, + }; + + typedef QMap RequestOptions; + + QStandardItem* CreateRootItem(); + void LazyPopulate(QStandardItem *item); + void ShowContextMenu(const QPoint &global_pos); + QWidget* HeaderWidget() const; + void ReloadSettings(); + + void Login(); + void Login(const QString &server, const QString &username, const QString &password); + LoginState login_state() const { return login_state_; } + + // Subsonic API methods + void Ping(); + + QUrl BuildRequestUrl(const QString &view); + // Convenience function to reduce QNetworkRequest/QSslConfiguration boilerplate + QNetworkReply* Send(const QUrl &url); + + static const char* kServiceName; + static const char* kSettingsGroup; + static const char* kApiVersion; + static const char* kApiClientName; + + static const char* kSongsTable; + static const char* kFtsTable; + + signals: + void LoginStateChanged(SubsonicService::LoginState newstate); + + private: + void EnsureMenuCreated(); + + QNetworkAccessManager* network_; + SubsonicUrlHandler* url_handler_; + + SubsonicLibraryScanner* scanner_; + int load_database_task_id_; + + QMenu* context_menu_; + QStandardItem* root_; + + LibraryBackend* library_backend_; + LibraryModel* library_model_; + LibraryFilterWidget* library_filter_; + QSortFilterProxyModel* library_sort_model_; + int total_song_count_; + + // Configuration + QString server_; + QString username_; + QString password_; + + LoginState login_state_; + + private slots: + void UpdateTotalSongCount(int count); + void ReloadDatabase(); + void ReloadDatabaseFinished(); + void onLoginStateChanged(SubsonicService::LoginState newstate); + void onPingFinished(QNetworkReply* reply); + + void ShowConfig(); +}; + +class SubsonicLibraryScanner : public QObject { + Q_OBJECT + + public: + SubsonicLibraryScanner(SubsonicService* service, QObject* parent=0); + ~SubsonicLibraryScanner(); + + void Scan(); + const SongList& GetSongs() const { return songs_; } + + static const int kAlbumChunkSize; + static const int kConcurrentRequests; + + signals: + void ScanFinished(); + + private slots: + // Step 1: use getAlbumList2 type=alphabeticalByName to list all albums + void onGetAlbumListFinished(QNetworkReply* reply, int offset); + // Step 2: use getAlbum id=? to list all songs for each album + void onGetAlbumFinished(QNetworkReply* reply); + + private: + void GetAlbumList(int offset); + void GetAlbum(QString id); + + SubsonicService* service_; + bool scanning_; + QQueue album_queue_; + QSet pending_requests_; + SongList songs_; +}; + +#endif // SUBSONICSERVICE_H diff --git a/src/internet/subsonicsettingspage.cpp b/src/internet/subsonicsettingspage.cpp new file mode 100644 index 000000000..7cef147e0 --- /dev/null +++ b/src/internet/subsonicsettingspage.cpp @@ -0,0 +1,109 @@ +#include "subsonicsettingspage.h" +#include "ui_subsonicsettingspage.h" +#include "internetmodel.h" + +#include + +SubsonicSettingsPage::SubsonicSettingsPage(SettingsDialog *dialog) + : SettingsPage(dialog), + ui_(new Ui_SubsonicSettingsPage), + service_(InternetModel::Service()) +{ + ui_->setupUi(this); + setWindowIcon(QIcon(":/providers/subsonic-32.png")); + + connect(ui_->login, SIGNAL(clicked()), SLOT(Login())); + connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout())); + connect(service_, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), + SLOT(LoginStateChanged(SubsonicService::LoginState))); + + ui_->login_state->AddCredentialField(ui_->server); + ui_->login_state->AddCredentialField(ui_->username); + ui_->login_state->AddCredentialField(ui_->password); + ui_->login_state->AddCredentialGroup(ui_->server_group); +} + +SubsonicSettingsPage::~SubsonicSettingsPage() +{ + delete ui_; +} + +void SubsonicSettingsPage::Load() +{ + QSettings s; + s.beginGroup(SubsonicService::kSettingsGroup); + + ui_->server->setText(s.value("server").toString()); + ui_->username->setText(s.value("username").toString()); + ui_->password->setText(s.value("password").toString()); + + // These are the same settings SubsonicService will have used already, so see if + // they were successful... + LoginStateChanged(service_->login_state()); +} + +void SubsonicSettingsPage::Save() +{ + QSettings s; + s.beginGroup(SubsonicService::kSettingsGroup); + + s.setValue("server", ui_->server->text()); + s.setValue("username", ui_->username->text()); + s.setValue("password", ui_->password->text()); +} + +void SubsonicSettingsPage::LoginStateChanged(SubsonicService::LoginState newstate) +{ + const bool logged_in = newstate == SubsonicService::LoginState_Loggedin; + + ui_->login_state->SetLoggedIn(logged_in ? LoginStateWidget::LoggedIn + : LoginStateWidget::LoggedOut, + QString("%1 (%2)") + .arg(ui_->username->text()) + .arg(ui_->server->text())); + ui_->login_state->SetAccountTypeVisible(!logged_in); + + switch (newstate) + { + case SubsonicService::LoginState_BadServer: + ui_->login_state->SetAccountTypeText(tr("Could not connect to Subsonic, check server URL. " + "Example: http://localhost:4040/")); + break; + + case SubsonicService::LoginState_BadCredentials: + ui_->login_state->SetAccountTypeText(tr("Wrong username or password.")); + break; + + case SubsonicService::LoginState_OutdatedClient: + ui_->login_state->SetAccountTypeText(tr("Incompatible Subsonic REST protocol version. Client must upgrade.")); + break; + + case SubsonicService::LoginState_OutdatedServer: + ui_->login_state->SetAccountTypeText(tr("Incompatible Subsonic REST protocol version. Server must upgrade.")); + break; + + case SubsonicService::LoginState_Unlicensed: + ui_->login_state->SetAccountTypeText(tr("The trial period for the Subsonic server is over. " + "Please donate to get a license key. Visit subsonic.org for details.")); + break; + + case SubsonicService::LoginState_OtherError: + ui_->login_state->SetAccountTypeText(tr("An unspecified error occurred.")); + break; + + default: + break; + } +} + +void SubsonicSettingsPage::Login() +{ + ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress); + service_->Login(ui_->server->text(), ui_->username->text(), ui_->password->text()); +} + +void SubsonicSettingsPage::Logout() +{ + ui_->username->setText(""); + ui_->password->setText(""); +} diff --git a/src/internet/subsonicsettingspage.h b/src/internet/subsonicsettingspage.h new file mode 100644 index 000000000..fda78c4dd --- /dev/null +++ b/src/internet/subsonicsettingspage.h @@ -0,0 +1,32 @@ +#ifndef SUBSONICSETTINGSPAGE_H +#define SUBSONICSETTINGSPAGE_H + +#include "ui/settingspage.h" +#include "subsonicservice.h" + +class Ui_SubsonicSettingsPage; + +class SubsonicSettingsPage : public SettingsPage +{ + Q_OBJECT + + public: + SubsonicSettingsPage(SettingsDialog *dialog); + ~SubsonicSettingsPage(); + + void Load(); + void Save(); + + public slots: + void LoginStateChanged(SubsonicService::LoginState newstate); + + private slots: + void Login(); + void Logout(); + + private: + Ui_SubsonicSettingsPage* ui_; + SubsonicService* service_; +}; + +#endif // SUBSONICSETTINGSPAGE_H diff --git a/src/internet/subsonicsettingspage.ui b/src/internet/subsonicsettingspage.ui new file mode 100644 index 000000000..da5706c73 --- /dev/null +++ b/src/internet/subsonicsettingspage.ui @@ -0,0 +1,101 @@ + + + SubsonicSettingsPage + + + + 0 + 0 + 505 + 219 + + + + Subsonic + + + + + + + + + Server details + + + + + + Server + + + + + + + Username + + + + + + + Password + + + + + + + QLineEdit::Password + + + + + + + + + + + + + Login + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + server + username + password + login + + + +
diff --git a/src/internet/subsonicurlhandler.cpp b/src/internet/subsonicurlhandler.cpp new file mode 100644 index 000000000..0e1e6fe5c --- /dev/null +++ b/src/internet/subsonicurlhandler.cpp @@ -0,0 +1,16 @@ +#include "subsonicservice.h" +#include "subsonicurlhandler.h" + +SubsonicUrlHandler::SubsonicUrlHandler(SubsonicService* service, QObject* parent) + : UrlHandler(parent), + service_(service) { +} + +UrlHandler::LoadResult SubsonicUrlHandler::StartLoading(const QUrl& url) { + if (service_->login_state() != SubsonicService::LoginState_Loggedin) + return LoadResult(); + + QUrl newurl = service_->BuildRequestUrl("stream"); + newurl.addQueryItem("id", url.host()); + return LoadResult(url, LoadResult::TrackAvailable, newurl); +} diff --git a/src/internet/subsonicurlhandler.h b/src/internet/subsonicurlhandler.h new file mode 100644 index 000000000..6d820f6fc --- /dev/null +++ b/src/internet/subsonicurlhandler.h @@ -0,0 +1,23 @@ +#ifndef SUBSONICURLHANDLER_H +#define SUBSONICURLHANDLER_H + +#include "core/urlhandler.h" + +class SubsonicService; + +// Subsonic URL handler: subsonic://id +class SubsonicUrlHandler : public UrlHandler { + Q_OBJECT + public: + SubsonicUrlHandler(SubsonicService* service, QObject* parent); + + QString scheme() const { return "subsonic"; } + QIcon icon() const { return QIcon(":providers/subsonic-32.png"); } + LoadResult StartLoading(const QUrl& url); + //LoadResult LoadNext(const QUrl& url); + + private: + SubsonicService* service_; +}; + +#endif // SUBSONICURLHANDLER_H diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index a7923be1b..5614e798d 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -38,6 +38,7 @@ #include "internet/digitallyimportedsettingspage.h" #include "internet/groovesharksettingspage.h" #include "internet/magnatunesettingspage.h" +#include "internet/subsonicsettingspage.h" #include "internet/ubuntuonesettingspage.h" #include "library/librarysettingspage.h" #include "playlist/playlistview.h" @@ -173,6 +174,7 @@ SettingsDialog::SettingsDialog(Application* app, BackgroundStreams* streams, QWi AddPage(Page_Magnatune, new MagnatuneSettingsPage(this), providers); AddPage(Page_DigitallyImported, new DigitallyImportedSettingsPage(this), providers); AddPage(Page_BackgroundStreams, new BackgroundStreamsSettingsPage(this), providers); + AddPage(Page_Subsonic, new SubsonicSettingsPage(this), providers); AddPage(Page_Podcasts, new PodcastSettingsPage(this), providers); // List box diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h index d5d63b59c..c34619e76 100644 --- a/src/ui/settingsdialog.h +++ b/src/ui/settingsdialog.h @@ -76,6 +76,7 @@ public: Page_Transcoding, Page_Remote, Page_Wiimotedev, + Page_Subsonic, Page_Podcasts, Page_GoogleDrive, Page_UbuntuOne,