Merge branch 'subsonic'

Conflicts:
	data/data.qrc
This commit is contained in:
John Maguire 2013-01-24 17:02:08 +01:00
commit fe2fb788d3
16 changed files with 937 additions and 2 deletions

View File

@ -1,6 +1,7 @@
<RCC>
<qresource prefix="/">
<file>blank.ttf</file>
<file>clementine_remote_qr.png</file>
<file>clementine-spotify-public.pem</file>
<file>currenttrack_bar_left.png</file>
<file>currenttrack_bar_mid.png</file>
@ -296,6 +297,8 @@
<file>providers/somafm.png</file>
<file>providers/songkick.png</file>
<file>providers/soundcloud.png</file>
<file>providers/subsonic-32.png</file>
<file>providers/subsonic.png</file>
<file>providers/ubuntuone.png</file>
<file>providers/wikipedia.png</file>
<file>sample.mood</file>
@ -337,6 +340,7 @@
<file>schema/schema-40.sql</file>
<file>schema/schema-41.sql</file>
<file>schema/schema-42.sql</file>
<file>schema/schema-43.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-6.sql</file>
@ -358,6 +362,5 @@
<file>volumeslider-handle_glow.png</file>
<file>volumeslider-handle.png</file>
<file>volumeslider-inset.png</file>
<file>clementine_remote_qr.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
data/providers/subsonic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

48
data/schema/schema-43.sql Normal file
View File

@ -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;

View File

@ -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

View File

@ -37,7 +37,7 @@
#include <QVariant>
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;

View File

@ -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

View File

@ -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 <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkCookieJar>
#include <QSslConfiguration>
#include <QXmlStreamReader>
#include <QSortFilterProxyModel>
#include <QMenu>
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);
}

View File

@ -0,0 +1,157 @@
#ifndef SUBSONICSERVICE_H
#define SUBSONICSERVICE_H
#include "internetmodel.h"
#include "internetservice.h"
#include <QQueue>
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<QString, QString> 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<QString> album_queue_;
QSet<QNetworkReply*> pending_requests_;
SongList songs_;
};
#endif // SUBSONICSERVICE_H

View File

@ -0,0 +1,109 @@
#include "subsonicsettingspage.h"
#include "ui_subsonicsettingspage.h"
#include "internetmodel.h"
#include <QSettings>
SubsonicSettingsPage::SubsonicSettingsPage(SettingsDialog *dialog)
: SettingsPage(dialog),
ui_(new Ui_SubsonicSettingsPage),
service_(InternetModel::Service<SubsonicService>())
{
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("");
}

View File

@ -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

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SubsonicSettingsPage</class>
<widget class="QWidget" name="SubsonicSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>505</width>
<height>219</height>
</rect>
</property>
<property name="windowTitle">
<string>Subsonic</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QGroupBox" name="server_group">
<property name="title">
<string>Server details</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="server_label">
<property name="text">
<string>Server</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="username_label">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="password_label">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="username"/>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="server"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>server</tabstop>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>login</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -76,6 +76,7 @@ public:
Page_Transcoding,
Page_Remote,
Page_Wiimotedev,
Page_Subsonic,
Page_Podcasts,
Page_GoogleDrive,
Page_UbuntuOne,