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,