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"
|
2012-07-28 15:10:42 +02:00
|
|
|
#include "core/application.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"
|
2012-01-16 15:22:30 +01:00
|
|
|
#include "core/utilities.h"
|
2012-01-30 14:39:23 +01:00
|
|
|
#include "ui/iconloader.h"
|
2013-01-10 23:08:52 +01:00
|
|
|
#include "library/librarybackend.h"
|
|
|
|
#include "core/mergedproxymodel.h"
|
|
|
|
#include "core/database.h"
|
2011-12-07 19:06:11 +01:00
|
|
|
|
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QNetworkCookieJar>
|
2012-01-17 00:00:39 +01:00
|
|
|
#include <QSslConfiguration>
|
2011-12-07 19:06:11 +01:00
|
|
|
#include <QXmlStreamReader>
|
2013-01-10 23:08:52 +01:00
|
|
|
#include <QSortFilterProxyModel>
|
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";
|
|
|
|
|
2013-01-10 23:08:52 +01:00
|
|
|
const char* SubsonicService::kSongsTable = "subsonic_songs";
|
|
|
|
const char* SubsonicService::kFtsTable = "subsonic_songs_fts";
|
|
|
|
|
2012-07-28 15:10:42 +02:00
|
|
|
SubsonicService::SubsonicService(Application* app, InternetModel *parent)
|
|
|
|
: InternetService(kServiceName, app, parent, parent),
|
2011-12-07 19:06:11 +01:00
|
|
|
network_(new QNetworkAccessManager(this)),
|
2013-01-09 23:20:55 +01:00
|
|
|
url_handler_(new SubsonicUrlHandler(this, this)),
|
2013-01-10 23:08:52 +01:00
|
|
|
library_backend_(NULL),
|
|
|
|
library_model_(NULL),
|
|
|
|
library_sort_model_(new QSortFilterProxyModel(this)),
|
|
|
|
login_state_(LoginState_OtherError)
|
2011-12-06 00:10:25 +01:00
|
|
|
{
|
2013-01-09 23:20:55 +01:00
|
|
|
app_->player()->RegisterUrlHandler(url_handler_);
|
2013-01-10 23:08:52 +01:00
|
|
|
|
|
|
|
library_backend_ = new LibraryBackend;
|
|
|
|
library_backend_->moveToThread(app_->database()->thread());
|
|
|
|
library_backend_->Init(app_->database(),
|
|
|
|
kSongsTable,
|
|
|
|
QString::null,
|
|
|
|
QString::null,
|
|
|
|
kFtsTable);
|
|
|
|
|
|
|
|
library_model_ = new LibraryModel(library_backend_, app_, this);
|
|
|
|
library_model_->set_show_various_artists(false);
|
|
|
|
library_model_->set_show_smart_playlists(false);
|
|
|
|
|
|
|
|
library_sort_model_->setSourceModel(library_model_);
|
|
|
|
library_sort_model_->setSortRole(LibraryModel::Role_SortText);
|
|
|
|
library_sort_model_->setDynamicSortFilter(true);
|
|
|
|
library_sort_model_->sort(0);
|
|
|
|
|
2012-01-20 00:22:50 +01:00
|
|
|
connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)),
|
|
|
|
SLOT(onLoginStateChanged(SubsonicService::LoginState)));
|
2011-12-06 00:10:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
SubsonicService::~SubsonicService()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
QStandardItem* SubsonicService::CreateRootItem()
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
QStandardItem* item = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName);
|
|
|
|
item->setData(true, InternetModel::Role_CanLazyLoad);
|
|
|
|
return item;
|
2011-12-06 00:10:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void SubsonicService::LazyPopulate(QStandardItem *item)
|
|
|
|
{
|
2011-12-09 01:13:17 +01:00
|
|
|
switch (item->data(InternetModel::Role_Type).toInt())
|
|
|
|
{
|
|
|
|
case InternetModel::Type_Service:
|
2013-01-10 23:08:52 +01:00
|
|
|
// TODO: initiate library loading
|
|
|
|
library_backend_->DeleteAll();
|
2011-12-10 20:04:04 +01:00
|
|
|
GetIndexes();
|
2013-01-10 23:08:52 +01:00
|
|
|
model()->merged_model()->AddSubModel(item->index(), library_sort_model_);
|
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;
|
|
|
|
}
|
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-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
|
|
|
|
2013-01-09 23:20:55 +01:00
|
|
|
QModelIndex SubsonicService::GetCurrentIndex()
|
|
|
|
{
|
|
|
|
return context_item_;
|
|
|
|
}
|
|
|
|
|
2011-12-09 01:13:17 +01:00
|
|
|
void SubsonicService::Send(const QUrl &url, const char *slot)
|
|
|
|
{
|
2012-01-17 00:00:39 +01:00
|
|
|
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);
|
2011-12-09 01:13:17 +01:00
|
|
|
connect(reply, SIGNAL(finished()), slot);
|
|
|
|
}
|
|
|
|
|
2013-01-10 23:08:52 +01:00
|
|
|
void SubsonicService::ReadIndex(QXmlStreamReader *reader)
|
2011-12-10 20:04:04 +01:00
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "index");
|
|
|
|
|
|
|
|
while (reader->readNextStartElement())
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
ReadArtist(reader);
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-01-10 23:08:52 +01:00
|
|
|
void SubsonicService::ReadArtist(QXmlStreamReader *reader)
|
2011-12-10 20:04:04 +01:00
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "artist");
|
2013-01-10 23:08:52 +01:00
|
|
|
// TODO: recurse into directory
|
2011-12-10 20:04:04 +01:00
|
|
|
reader->skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
2013-01-10 23:08:52 +01:00
|
|
|
void SubsonicService::ReadAlbum(QXmlStreamReader *reader)
|
2011-12-10 20:04:04 +01:00
|
|
|
{
|
|
|
|
Q_ASSERT(reader->name() == "child");
|
2013-01-10 23:08:52 +01:00
|
|
|
// TODO: recurse into directory
|
2011-12-10 20:04:04 +01:00
|
|
|
reader->skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
2013-01-10 23:08:52 +01:00
|
|
|
Song SubsonicService::ReadTrack(QXmlStreamReader *reader)
|
2011-12-10 20:04:04 +01:00
|
|
|
{
|
|
|
|
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);
|
2013-01-09 23:20:55 +01:00
|
|
|
QUrl url = QUrl(QString("subsonic://%1").arg(id));
|
2011-12-11 00:11:05 +01:00
|
|
|
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
|
|
|
|
2013-01-10 23:08:52 +01:00
|
|
|
// We need to set these to satisfy the database constraints
|
|
|
|
song.set_directory_id(0);
|
|
|
|
song.set_mtime(0);
|
|
|
|
song.set_ctime(0);
|
|
|
|
|
2011-12-10 20:04:04 +01:00
|
|
|
reader->skipCurrentElement();
|
2013-01-10 23:08:52 +01:00
|
|
|
return song;
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
|
2012-01-20 00:22:50 +01:00
|
|
|
void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate)
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
// TODO: library refresh logic?
|
2012-01-20 00:22:50 +01:00
|
|
|
}
|
|
|
|
|
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;
|
2012-01-16 15:22:30 +01:00
|
|
|
qLog(Error) << "Failed to connect ("
|
|
|
|
<< Utilities::EnumToString(QNetworkReply::staticMetaObject, "NetworkError", reply->error())
|
|
|
|
<< "):" << reply->errorString();
|
2011-12-07 19:06:11 +01:00
|
|
|
}
|
|
|
|
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();
|
2012-01-16 15:31:45 +01:00
|
|
|
qLog(Error) << "Subsonic error ("
|
|
|
|
<< Utilities::EnumToString(SubsonicService::staticMetaObject, "ApiError", error)
|
|
|
|
<< "):" << reader.attributes().value("message").toString();
|
2011-12-07 19:06:11 +01:00
|
|
|
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;
|
2012-01-19 23:59:59 +01:00
|
|
|
case ApiError_OutdatedClient:
|
|
|
|
login_state_ = LoginState_OutdatedClient;
|
2011-12-07 19:06:11 +01:00
|
|
|
break;
|
2012-01-18 21:21:42 +01:00
|
|
|
case ApiError_OutdatedServer:
|
|
|
|
login_state_ = LoginState_OutdatedServer;
|
|
|
|
break;
|
2012-01-19 23:59:59 +01:00
|
|
|
case ApiError_Unlicensed:
|
|
|
|
login_state_ = LoginState_Unlicensed;
|
|
|
|
break;
|
2011-12-07 19:06:11 +01:00
|
|
|
default:
|
|
|
|
login_state_ = LoginState_OtherError;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2012-01-16 15:22:30 +01:00
|
|
|
qLog(Debug) << "Login state changed:"
|
|
|
|
<< Utilities::EnumToString(SubsonicService::staticMetaObject, "LoginState", 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");
|
2013-01-10 23:08:52 +01:00
|
|
|
// TODO: start loading library data
|
|
|
|
SongList songs;
|
2011-12-10 20:04:04 +01:00
|
|
|
while (reader.readNextStartElement())
|
|
|
|
{
|
|
|
|
if (reader.name() == "index")
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
ReadIndex(&reader);
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
else if (reader.name() == "child" && reader.attributes().value("isVideo") == "false")
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
songs << ReadTrack(&reader);
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
2013-01-10 23:08:52 +01:00
|
|
|
|
|
|
|
library_backend_->AddOrUpdateSongs(songs);
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
2013-01-10 23:08:52 +01:00
|
|
|
// TODO: add tracks, etc.
|
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")
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
ReadAlbum(&reader);
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
else if (reader.attributes().value("isVideo") == "false")
|
|
|
|
{
|
2013-01-10 23:08:52 +01:00
|
|
|
ReadTrack(&reader);
|
2011-12-10 20:04:04 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
2011-12-09 01:13:17 +01:00
|
|
|
}
|
|
|
|
}
|