2012-01-10 17:52:54 +01:00
|
|
|
#include "subsonicurlhandler.h"
|
2011-12-06 00:10:25 +01:00
|
|
|
#include "subsonicservice.h"
|
|
|
|
#include "internetmodel.h"
|
2011-12-07 19:06:11 +01:00
|
|
|
#include "core/logging.h"
|
2012-01-10 17:52:54 +01:00
|
|
|
#include "core/player.h"
|
2011-12-07 19:06:11 +01:00
|
|
|
|
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QNetworkCookieJar>
|
|
|
|
#include <QXmlStreamReader>
|
2011-12-06 00:10:25 +01:00
|
|
|
|
|
|
|
const char* SubsonicService::kServiceName = "Subsonic";
|
|
|
|
const char* SubsonicService::kSettingsGroup = "Subsonic";
|
2011-12-10 20:04:04 +01:00
|
|
|
const char* SubsonicService::kApiVersion = "1.7.0";
|
2011-12-06 00:10:25 +01:00
|
|
|
const char* SubsonicService::kApiClientName = "Clementine";
|
|
|
|
|
|
|
|
SubsonicService::SubsonicService(InternetModel *parent)
|
2011-12-07 19:06:11 +01:00
|
|
|
: InternetService(kServiceName, parent, parent),
|
|
|
|
network_(new QNetworkAccessManager(this)),
|
2012-01-10 17:52:54 +01:00
|
|
|
http_url_handler_(new SubsonicUrlHandler(this, this)),
|
|
|
|
https_url_handler_(new SubsonicHttpsUrlHandler(this, this)),
|
2011-12-10 20:04:04 +01:00
|
|
|
login_state_(LoginState_OtherError),
|
|
|
|
item_lookup_()
|
2011-12-06 00:10:25 +01:00
|
|
|
{
|
2012-01-10 17:52:54 +01:00
|
|
|
model()->player()->RegisterUrlHandler(http_url_handler_);
|
|
|
|
model()->player()->RegisterUrlHandler(https_url_handler_);
|
2011-12-06 00:10:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
{
|
2011-12-09 01:13:17 +01:00
|
|
|
switch (item->data(InternetModel::Role_Type).toInt())
|
|
|
|
{
|
|
|
|
case InternetModel::Type_Service:
|
2011-12-10 20:04:04 +01:00
|
|
|
GetIndexes();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type_Artist:
|
|
|
|
case Type_Album:
|
|
|
|
GetMusicDirectory(item->data(Role_Id).toString());
|
2011-12-09 01:13:17 +01:00
|
|
|
break;
|
2011-12-06 00:10:25 +01:00
|
|
|
|
2011-12-09 01:13:17 +01:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2012-01-10 22:23:40 +01:00
|
|
|
|
|
|
|
item->setRowCount(0);
|
|
|
|
QStandardItem* loading = new QStandardItem(tr("Loading..."));
|
|
|
|
item->appendRow(loading);
|
2011-12-06 00:10:25 +01:00
|
|
|
}
|
|
|
|
|
2011-12-07 19:06:11 +01:00
|
|
|
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_));
|
2011-12-08 21:00:50 +01:00
|
|
|
// Forget login state whilst waiting
|
|
|
|
login_state_ = LoginState_Unknown;
|
|
|
|
// Ping is enough to check credentials
|
2011-12-07 19:06:11 +01:00
|
|
|
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()
|
|
|
|
{
|
2011-12-09 01:13:17 +01:00
|
|
|
Send(BuildRequestUrl("ping"), SLOT(onPingFinished()));
|
|
|
|
}
|
|
|
|
|
2011-12-10 20:04:04 +01:00
|
|
|
void SubsonicService::GetIndexes()
|
2011-12-09 01:13:17 +01:00
|
|
|
{
|
2011-12-10 20:04:04 +01:00
|
|
|
Send(BuildRequestUrl("getIndexes"), SLOT(onGetIndexesFinished()));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SubsonicService::GetMusicDirectory(const QString &id)
|
|
|
|
{
|
|
|
|
QUrl url = BuildRequestUrl("getMusicDirectory");
|
|
|
|
url.addQueryItem("id", id);
|
|
|
|
Send(url, SLOT(onGetMusicDirectoryFinished()));
|
2011-12-07 19:06:11 +01:00
|
|
|
}
|
|
|
|
|
2011-12-06 00:10:25 +01:00
|
|
|
QModelIndex SubsonicService::GetCurrentIndex()
|
|
|
|
{
|
|
|
|
return context_item_;
|
|
|
|
}
|
|
|
|
|
2011-12-09 01:13:17 +01:00
|
|
|
QUrl SubsonicService::BuildRequestUrl(const QString &view)
|
2011-12-06 00:10:25 +01:00
|
|
|
{
|
2011-12-07 19:06:11 +01:00
|
|
|
QUrl url(server_ + "rest/" + view + ".view");
|
2011-12-06 00:10:25 +01:00
|
|
|
url.addQueryItem("v", kApiVersion);
|
|
|
|
url.addQueryItem("c", kApiClientName);
|
|
|
|
url.addQueryItem("u", username_);
|
|
|
|
url.addQueryItem("p", password_);
|
|
|
|
return url;
|
|
|
|
}
|
2011-12-07 19:06:11 +01:00
|
|
|
|
2011-12-09 01:13:17 +01:00
|
|
|
void SubsonicService::Send(const QUrl &url, const char *slot)
|
|
|
|
{
|
|
|
|
QNetworkReply *reply = network_->get(QNetworkRequest(url));
|
|
|
|
// It's very unlikely the Subsonic server will have a valid SSL certificate
|
|
|
|
reply->ignoreSslErrors();
|
|
|
|
connect(reply, SIGNAL(finished()), slot);
|
|
|
|
}
|
|
|
|
|
2011-12-10 20:04:04 +01:00
|
|
|
void SubsonicService::ReadIndex(QXmlStreamReader *reader, QStandardItem *parent)
|
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "index");
|
|
|
|
|
|
|
|
while (reader->readNextStartElement())
|
|
|
|
{
|
|
|
|
ReadArtist(reader, parent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SubsonicService::ReadArtist(QXmlStreamReader *reader, QStandardItem *parent)
|
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "artist");
|
|
|
|
QString id = reader->attributes().value("id").toString();
|
|
|
|
QStandardItem *item = new QStandardItem(reader->attributes().value("name").toString());
|
|
|
|
item->setData(Type_Artist, InternetModel::Role_Type);
|
|
|
|
item->setData(true, InternetModel::Role_CanLazyLoad);
|
|
|
|
item->setData(id, Role_Id);
|
|
|
|
parent->appendRow(item);
|
|
|
|
item_lookup_.insert(id, item);
|
|
|
|
reader->skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SubsonicService::ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent)
|
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "child");
|
|
|
|
QString id = reader->attributes().value("id").toString();
|
|
|
|
QStandardItem *item = new QStandardItem(reader->attributes().value("title").toString());
|
|
|
|
item->setData(Type_Album, InternetModel::Role_Type);
|
|
|
|
item->setData(true, InternetModel::Role_CanLazyLoad);
|
|
|
|
item->setData(id, Role_Id);
|
|
|
|
parent->appendRow(item);
|
|
|
|
item_lookup_.insert(id, item);
|
|
|
|
reader->skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent)
|
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "child");
|
2011-12-11 00:11:05 +01:00
|
|
|
|
|
|
|
Song song;
|
2011-12-10 20:04:04 +01:00
|
|
|
QString id = reader->attributes().value("id").toString();
|
2011-12-11 00:11:05 +01:00
|
|
|
song.set_title(reader->attributes().value("title").toString());
|
|
|
|
song.set_album(reader->attributes().value("album").toString());
|
2012-01-10 22:23:40 +01:00
|
|
|
song.set_track(reader->attributes().value("track").toString().toInt());
|
2011-12-11 00:11:05 +01:00
|
|
|
song.set_artist(reader->attributes().value("artist").toString());
|
|
|
|
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 = BuildRequestUrl("stream");
|
2012-01-10 17:52:54 +01:00
|
|
|
url.setScheme(url.scheme() == "https" ? "subsonics" : "subsonic");
|
2011-12-11 00:11:05 +01:00
|
|
|
url.addQueryItem("id", id);
|
|
|
|
song.set_url(url);
|
2012-01-10 22:23:40 +01:00
|
|
|
song.set_filesize(reader->attributes().value("size").toString().toInt());
|
2011-12-11 00:11:05 +01:00
|
|
|
|
2011-12-10 20:04:04 +01:00
|
|
|
QStandardItem *item = new QStandardItem(reader->attributes().value("title").toString());
|
|
|
|
item->setData(Type_Track, InternetModel::Role_Type);
|
|
|
|
item->setData(id, Role_Id);
|
2011-12-11 00:11:05 +01:00
|
|
|
item->setData(QVariant::fromValue(song), InternetModel::Role_SongMetadata);
|
|
|
|
item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour);
|
|
|
|
item->setData(song.url(), InternetModel::Role_Url);
|
2011-12-10 20:04:04 +01:00
|
|
|
parent->appendRow(item);
|
|
|
|
item_lookup_.insert(id, item);
|
|
|
|
reader->skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
2011-12-07 19:06:11 +01:00
|
|
|
void SubsonicService::onPingFinished()
|
|
|
|
{
|
|
|
|
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
|
|
{
|
|
|
|
login_state_ = LoginState_BadServer;
|
|
|
|
}
|
|
|
|
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();
|
|
|
|
switch (error)
|
|
|
|
{
|
2011-12-08 21:00:50 +01:00
|
|
|
// "Parameter missing" for "ping" is always blank username or password
|
|
|
|
case ApiError_ParameterMissing:
|
2011-12-07 19:06:11 +01:00
|
|
|
case ApiError_BadCredentials:
|
|
|
|
login_state_ = LoginState_BadCredentials;
|
|
|
|
break;
|
|
|
|
case ApiError_Unlicensed:
|
|
|
|
login_state_ = LoginState_Unlicensed;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
login_state_ = LoginState_OtherError;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
qLog(Debug) << "Login state changed: " << login_state_;
|
2011-12-08 21:00:50 +01:00
|
|
|
emit LoginStateChanged(login_state_);
|
2011-12-07 19:06:11 +01:00
|
|
|
}
|
2011-12-09 01:13:17 +01:00
|
|
|
|
2011-12-10 20:04:04 +01:00
|
|
|
void SubsonicService::onGetIndexesFinished()
|
|
|
|
{
|
|
|
|
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
|
|
|
|
Q_ASSERT(reply);
|
|
|
|
reply->deleteLater();
|
|
|
|
QXmlStreamReader reader(reply);
|
|
|
|
|
|
|
|
reader.readNextStartElement();
|
|
|
|
Q_ASSERT(reader.name() == "subsonic-response");
|
|
|
|
if (reader.attributes().value("status") != "ok")
|
|
|
|
{
|
|
|
|
// TODO: error handling
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
reader.readNextStartElement();
|
|
|
|
Q_ASSERT(reader.name() == "indexes");
|
2012-01-10 22:23:40 +01:00
|
|
|
root_->setRowCount(0);
|
2011-12-10 20:04:04 +01:00
|
|
|
while (reader.readNextStartElement())
|
|
|
|
{
|
|
|
|
if (reader.name() == "index")
|
|
|
|
{
|
|
|
|
ReadIndex(&reader, root_);
|
|
|
|
}
|
|
|
|
else if (reader.name() == "child" && reader.attributes().value("isVideo") == "false")
|
|
|
|
{
|
|
|
|
ReadTrack(&reader, root_);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SubsonicService::onGetMusicDirectoryFinished()
|
2011-12-09 01:13:17 +01:00
|
|
|
{
|
|
|
|
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
|
2011-12-10 20:04:04 +01:00
|
|
|
Q_ASSERT(reply);
|
2011-12-09 01:13:17 +01:00
|
|
|
reply->deleteLater();
|
|
|
|
QXmlStreamReader reader(reply);
|
|
|
|
|
|
|
|
reader.readNextStartElement();
|
2011-12-10 20:04:04 +01:00
|
|
|
Q_ASSERT(reader.name() == "subsonic-response");
|
2011-12-09 01:13:17 +01:00
|
|
|
if (reader.attributes().value("status") != "ok")
|
|
|
|
{
|
|
|
|
// TODO: error handling
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
reader.readNextStartElement();
|
2011-12-10 20:04:04 +01:00
|
|
|
Q_ASSERT(reader.name() == "directory");
|
|
|
|
QStandardItem *parent = item_lookup_.value(reader.attributes().value("id").toString());
|
2012-01-10 22:23:40 +01:00
|
|
|
parent->setRowCount(0);
|
2011-12-09 01:13:17 +01:00
|
|
|
while (reader.readNextStartElement())
|
|
|
|
{
|
2011-12-10 20:04:04 +01:00
|
|
|
if (reader.attributes().value("isDir") == "true")
|
|
|
|
{
|
|
|
|
ReadAlbum(&reader, parent);
|
|
|
|
}
|
|
|
|
else if (reader.attributes().value("isVideo") == "false")
|
|
|
|
{
|
|
|
|
ReadTrack(&reader, parent);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
2011-12-09 01:13:17 +01:00
|
|
|
}
|
|
|
|
}
|