From 8110cdf241107506a921b2be53996e32d971c7e2 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 5 Dec 2011 23:10:25 +0000 Subject: [PATCH 01/31] Minimal SubsonicService and SubsonicSettingsPage to show up in UI --- data/data.qrc | 1 + data/providers/subsonic.png | Bin 0 -> 734 bytes src/CMakeLists.txt | 5 ++ src/internet/internetmodel.cpp | 2 + src/internet/subsonicservice.cpp | 47 ++++++++++++ src/internet/subsonicservice.h | 39 ++++++++++ src/internet/subsonicsettingspage.cpp | 39 ++++++++++ src/internet/subsonicsettingspage.h | 25 +++++++ src/internet/subsonicsettingspage.ui | 101 ++++++++++++++++++++++++++ src/ui/settingsdialog.cpp | 2 + src/ui/settingsdialog.h | 1 + 11 files changed, 262 insertions(+) create mode 100644 data/providers/subsonic.png create mode 100644 src/internet/subsonicservice.cpp create mode 100644 src/internet/subsonicservice.h create mode 100644 src/internet/subsonicsettingspage.cpp create mode 100644 src/internet/subsonicsettingspage.h create mode 100644 src/internet/subsonicsettingspage.ui diff --git a/data/data.qrc b/data/data.qrc index 1fc89205b..f3a51c48b 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -337,5 +337,6 @@ schema/schema-35.sql schema/schema-36.sql grooveshark-valicert-ca.pem + providers/subsonic.png diff --git a/data/providers/subsonic.png b/data/providers/subsonic.png new file mode 100644 index 0000000000000000000000000000000000000000..d2d8647872fbfadcbb15d33abf7e61ec59b69b9c GIT binary patch literal 734 zcmV<40wMj0P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipt< z78)(9S|Sqw00Le~L_t(I%WacEYZE~jhM%3?gv3o_H`x@SJ(L8j=}A2FQVPBJ158_y zUX`Bw0UpgQ(33(B4T!hi>L2i?JxKK?N^&qoCS8_fDao>N&AQq3V73(559aXAyzlc3 z?=Z^p@-j(HCKD!;32_{gR*Isa>pHrwU+-x*1wsgbbzldm0f}+~`~Xgc5Z`Zlv`i** z-T1!0rR(}m6h-f%D0&qHfeVn$W{X0I`$C8V;53uTe7pvfN+kfUuC5k~#p0tVie6{4 z*`96NHwU>(OG_VuAm|Gro&#S?rP2sMGfguQ-2*O6)9e9MDiwOY9st#9m4$@`fS%(x z7r;H!G(Q1o<#IUz7=V@Z<#L(T)z!2tEx<^0nqe5p!4P<07zXF(=c%u-v9SSA27V?! z&9-fX5ae>XFDol6PfkuwkW!`s27>|n`}=qM{eF8e7!XAfYQ0`ZQIyr%?&X0y2q?4?{_ui0$wN-2-0 z(`h{jg3T}t5su^FIL>;j)jH~QI*djmfLsb7HyVxTbUL(Jts}>A)*Z+BmjSTY?Ork- zkFhKZpqK(ET9(D-)7}8x*=+Vcj$>N^)VbgmkOAVkDV(o({%f_lRssG1zkuI=0hm$t^>k8U Qg8%>k07*qoM6N<$f_Wr5e*gdg literal 0 HcmV?d00001 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 54a53f28e..a265e0bef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -169,6 +169,8 @@ set(SOURCES internet/savedradio.cpp internet/somafmservice.cpp internet/somafmurlhandler.cpp + internet/subsonicservice.cpp + internet/subsonicsettingspage.cpp library/groupbydialog.cpp library/library.cpp @@ -411,6 +413,8 @@ set(HEADERS internet/savedradio.h internet/somafmservice.h internet/somafmurlhandler.h + internet/subsonicservice.h + internet/subsonicsettingspage.h library/groupbydialog.h library/library.h @@ -559,6 +563,7 @@ set(UI internet/magnatunedownloaddialog.ui internet/magnatunesettingspage.ui internet/spotifysettingspage.ui + internet/subsonicsettingspage.ui library/groupbydialog.ui library/libraryfilterwidget.ui diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index f26ab7d97..8786d36f3 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -25,6 +25,7 @@ #include "savedradio.h" #include "somafmservice.h" #include "groovesharkservice.h" +#include "subsonicservice.h" #include "core/logging.h" #include "core/mergedproxymodel.h" #include "smartplaylists/generatormimedata.h" @@ -78,6 +79,7 @@ InternetModel::InternetModel(BackgroundThread* db_thread, #ifdef HAVE_SPOTIFY AddService(new SpotifyService(this)); #endif + AddService(new SubsonicService(this)); } void InternetModel::AddService(InternetService *service) { diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp new file mode 100644 index 000000000..bf8a6e5fe --- /dev/null +++ b/src/internet/subsonicservice.cpp @@ -0,0 +1,47 @@ +#include "subsonicservice.h" +#include "internetmodel.h" + +const char* SubsonicService::kServiceName = "Subsonic"; +const char* SubsonicService::kSettingsGroup = "Subsonic"; +const char* SubsonicService::kApiVersion = "1.6.0"; +const char* SubsonicService::kApiClientName = "Clementine"; + +SubsonicService::SubsonicService(InternetModel *parent) + : InternetService(kServiceName, parent, parent) +{ +} + +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) +{ + +} + +QModelIndex SubsonicService::GetCurrentIndex() +{ + return context_item_; +} + +QUrl SubsonicService::BuildRequestUrl(const QString &view, const RequestOptions &options) +{ + QUrl url(server_url_ + "rest/" + view + ".view"); + url.addQueryItem("v", kApiVersion); + url.addQueryItem("c", kApiClientName); + url.addQueryItem("u", username_); + url.addQueryItem("p", password_); + for (RequestOptions::const_iterator i = options.begin(); i != options.end(); ++i) + { + url.addQueryItem(i.key(), i.value()); + } + return url; +} diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h new file mode 100644 index 000000000..f574f7741 --- /dev/null +++ b/src/internet/subsonicservice.h @@ -0,0 +1,39 @@ +#ifndef SUBSONICSERVICE_H +#define SUBSONICSERVICE_H + +#include "internetservice.h" + +class SubsonicService : public InternetService +{ + Q_OBJECT + public: + SubsonicService(InternetModel *parent); + ~SubsonicService(); + + typedef QMap RequestOptions; + + QStandardItem* CreateRootItem(); + void LazyPopulate(QStandardItem *item); + + static const char* kServiceName; + static const char* kSettingsGroup; + static const char* kApiVersion; + static const char* kApiClientName; + + protected: + QModelIndex GetCurrentIndex(); + + private: + QUrl BuildRequestUrl(const QString &view, const RequestOptions &options); + + QModelIndex context_item_; + QStandardItem* root_; + + // Configuration + QString server_url_; + QString username_; + QString password_; + +}; + +#endif // SUBSONICSERVICE_H diff --git a/src/internet/subsonicsettingspage.cpp b/src/internet/subsonicsettingspage.cpp new file mode 100644 index 000000000..d669f0df2 --- /dev/null +++ b/src/internet/subsonicsettingspage.cpp @@ -0,0 +1,39 @@ +#include "subsonicsettingspage.h" +#include "ui_subsonicsettingspage.h" +#include "subsonicservice.h" + +#include + +SubsonicSettingsPage::SubsonicSettingsPage(SettingsDialog *dialog) + : SettingsPage(dialog), + ui_(new Ui_SubsonicSettingsPage) +{ + ui_->setupUi(this); + + setWindowIcon(QIcon(":/providers/subsonic.png")); +} + +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()); +} + +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()); +} diff --git a/src/internet/subsonicsettingspage.h b/src/internet/subsonicsettingspage.h new file mode 100644 index 000000000..acc4f021c --- /dev/null +++ b/src/internet/subsonicsettingspage.h @@ -0,0 +1,25 @@ +#ifndef SUBSONICSETTINGSPAGE_H +#define SUBSONICSETTINGSPAGE_H + +#include "ui/settingspage.h" + +class Ui_SubsonicSettingsPage; +class SubsonicService; + +class SubsonicSettingsPage : public SettingsPage +{ + Q_OBJECT + + public: + SubsonicSettingsPage(SettingsDialog *dialog); + ~SubsonicSettingsPage(); + + void Load(); + void Save(); + + 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/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index 0e01a16a5..e5026b1f3 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -35,6 +35,7 @@ #include "internet/digitallyimportedsettingspage.h" #include "internet/groovesharksettingspage.h" #include "internet/magnatunesettingspage.h" +#include "internet/subsonicsettingspage.h" #include "library/librarysettingspage.h" #include "playlist/playlistview.h" #include "songinfo/songinfosettingspage.h" @@ -149,6 +150,7 @@ SettingsDialog::SettingsDialog(BackgroundStreams* streams, QWidget* parent) 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); // List box connect(ui_->list, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h index 237c5cbb9..f2b1947d8 100644 --- a/src/ui/settingsdialog.h +++ b/src/ui/settingsdialog.h @@ -75,6 +75,7 @@ public: Page_Transcoding, Page_Remote, Page_Wiimotedev, + Page_Subsonic, }; enum Role { From e24c87861f8a34cc05f399bd0c37bc8e6b501526 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Wed, 7 Dec 2011 14:01:28 +0000 Subject: [PATCH 02/31] Corrected subsonic-related artwork --- data/data.qrc | 1 + data/providers/subsonic-32.png | Bin 0 -> 1634 bytes data/providers/subsonic.png | Bin 734 -> 665 bytes src/internet/subsonicsettingspage.cpp | 2 +- 4 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 data/providers/subsonic-32.png diff --git a/data/data.qrc b/data/data.qrc index f3a51c48b..a8a102398 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -338,5 +338,6 @@ schema/schema-36.sql grooveshark-valicert-ca.pem providers/subsonic.png + providers/subsonic-32.png diff --git a/data/providers/subsonic-32.png b/data/providers/subsonic-32.png new file mode 100644 index 0000000000000000000000000000000000000000..74221eb34f101bf474530565a04ffbe2fee4ae12 GIT binary patch literal 1634 zcmV-o2A%ndP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L06t9s06t9t%H1f-00007bV*G`2ipt> z4K_R6#d!7r00rVnL_t(o!|j$`jFndvhM#wT-}#?848z|rP~^8&jHD2&RhwvSo1n>@ zlQagjRjO%7t1;Gkfj35DY8s*!Hfpr7{v{ZT5$eo2DbQX?ttisirj|kmL~w)|hk==M zX3os{+k1I2$4Vl|jU-<1COg^L`^&f9{jQaFEx3uBxC&BFrrz3T_hoNrEM1etQ>hoV zP!`UHN19)2p1n~7;%l5Q(L&u9ZI1A-`40f+0J{+c+%?V@VW3DzTKiwVyI~4M7=#e4 ztB3UykMa>O)+#1Tn`uAI$&;^gc+7 z5=%RQg3O!+Oc<7sEGXzwz0H{!ckbxS#x>7y3ZS+Na#vBLR4P#@6#laY4t@Lz?S*Sp z$R*a~2>FD}A!==->74~Lf|s?tkV6X+j&P#-&58#lXziqtN#>l*{Ez2q6@U#m3Ok&^AfOB<%$(2H5*1iN`nb{p5#x zw8xj=@XgaC8zzW>B>X z$Hrk#$CbTd-XIvkg9XEUz?uQ-9y4HlK7QbKe%^WY8^FDTgM%qx%*+I=aHF56cM^rf=Um;)`rY#H@z*B?+lN$}Q=?~vn5w{3U~0=J zL(8o~7bdN?@l(_`Yz_5y?UasR1j(oesJ3CM2@?$%nGKwof$;{6*Ntg!@rE>^MKswY zCkZn%z?^gHDBvA1{?#Mw`^(vQEI0dbC5<{@Ja+uH zKlVLNbEVtk-fm_yg+gIC1`<0;yu3Hp z_;ho)Zo7YDe|~9%iI3iV#q$wYKAAg>$dZ z)*jez&mTF@FMv18LpS>9RPpq$kQ`a*84+`Id!R)%(#4t=g$`esbR_PCt8smx12{uK-h}QYl?16zZK$ z_zpAsU7=9uEtku0b;A^}s6%K9XaY$f4=e*#bkl6aP2(C+gVLRxWv7-2eap07*qoM6N<$g0B$*mjD0& literal 0 HcmV?d00001 diff --git a/data/providers/subsonic.png b/data/providers/subsonic.png index d2d8647872fbfadcbb15d33abf7e61ec59b69b9c..efbfe3a29f5013dc218f03c03ec84fd2cfb3518b 100644 GIT binary patch delta 602 zcmV-g0;T=l1(^kqN`F30002Hs0m|Jd%K!iX2XskIMF-mq2MsqD_XZ_Y0006RNkl_K!ilK ztVrH7AtDMaj21;AM2iBgT;%ACIv;P|$M3dCvo_(<1DAU__kW(Vx%UFw3dT+2T>L>i z#%69=qy1IKw4C06__}`HM1nfO_{u|IdUiYO5IIQR)@9zjpqEN%|Y9? zH?!n_llzHmeJm;Rwq4Xrhp6{CQB$p=o@QzvFQ5kiM0UY>@_0jh+;JS801%GgN~r?N zI+YnxmH`4HzJDU?A>fHnC&Pu>%*3>D;Oy;1v28mCAOO�npu4?3&e|9uf(K1s7cL zfn^WKnm|{Q(v_0Pv6kL}i;3^GQ8W-ZIygAE0AN6Khj22HNi;~x61Cb%27#zlC+P-& zr+|7W@Ip$_6Pl6MYBWCLMjH#ZZMOl~t6hGEwt6IsV1FgS#S@PBC5wgbm%>H}tASuG zAd9|W$t5ZQiWSvY_UV$eNQ&0igsE6Cr=sa^$i6;AeP;>j=>dunrw~MxBBn;Rn8Fnw zH(q$-O_6ejuVe) ong8Em5rDoP|G;saZS;V@04E3T?dtjVr~m)}07*qoM6N<$f{T3->i_@% delta 672 zcmV;R0$=@^1>OaaN`DIz000XU0RWnu7ytkO2XskIMF-mq1r{1Dt6Cxx0007BNkl1vn-GszVV>j6pp*@rYtm#QS^im4F_ybH^kzSRa`~e=#Ezpxf4-JU7 z-s&IlraegYCQ5QJMJ8RAWGTtAam~8f^lgWI%29!!A0IsgC7K_E=qbQ1AXS3O!ZQC~oxl2n+AA%t0 z3n88ZUrVLZ2!B8`O*0YQ11?O{>;Y6N6?(lM0M%-hg@pxxp5r(dz&+D6KLKdvaybDQ zfR*&+a+%fD)wC=vz({nOVHnB55O`o12IuGJsjslHu>nv9ekMN6wrzwEd`*-{OetR$&5JeGcyr%?&X0y2q?4?{_ui0$wN-2-0(`h{jg3T}t5su^F zIL>;j)jH~QI*djmfLsb7HyVxTbUL(Jts}>A)*Z+BmjSTY?Ork-kFhKZpqK(ET9(D- zlE!&~LV;SXN~5uT==*-x^E?zFt0>AHRaGAXYhf5Z1=_${zz3pvCeMHaP1D{0 z-PvsRK8|Bs0o1wR7LWnrxhb5lc>Zg(xmE%G02#l4-+uv^QTFw8QelGt0000setupUi(this); - setWindowIcon(QIcon(":/providers/subsonic.png")); + setWindowIcon(QIcon(":/providers/subsonic-32.png")); } SubsonicSettingsPage::~SubsonicSettingsPage() From ebc97e03c544f5b4b313af0a6b8d29372d3a5888 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Wed, 7 Dec 2011 18:06:11 +0000 Subject: [PATCH 03/31] SubsonicService now successfully pings a Subsonic server and interprets the response --- src/internet/subsonicservice.cpp | 99 ++++++++++++++++++++++++++++++-- src/internet/subsonicservice.h | 43 +++++++++++++- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index bf8a6e5fe..efd8ef224 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -1,5 +1,11 @@ #include "subsonicservice.h" #include "internetmodel.h" +#include "core/logging.h" + +#include +#include +#include +#include const char* SubsonicService::kServiceName = "Subsonic"; const char* SubsonicService::kSettingsGroup = "Subsonic"; @@ -7,7 +13,9 @@ const char* SubsonicService::kApiVersion = "1.6.0"; const char* SubsonicService::kApiClientName = "Clementine"; SubsonicService::SubsonicService(InternetModel *parent) - : InternetService(kServiceName, parent, parent) + : InternetService(kServiceName, parent, parent), + network_(new QNetworkAccessManager(this)), + login_state_(LoginState_OtherError) { } @@ -27,21 +35,102 @@ void SubsonicService::LazyPopulate(QStandardItem *item) } +void SubsonicService::ReloadSettings() +{ + QSettings s; + s.beginGroup(kSettingsGroup); + + server_ = s.value("server").toString(); + username_ = s.value("username").toString(); + password_ = s.value("password").toString(); + + // TODO: Move this? + Login(); +} + +void SubsonicService::Login() +{ + // Forget session ID + network_->setCookieJar(new QNetworkCookieJar(network_)); + // Ping is enough to authenticate + 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() +{ + QUrl request_url = BuildRequestUrl("ping"); + QNetworkReply *reply = network_->get(QNetworkRequest(request_url)); + reply->ignoreSslErrors(); + connect(reply, SIGNAL(finished()), this, SLOT(onPingFinished())); +} + QModelIndex SubsonicService::GetCurrentIndex() { return context_item_; } -QUrl SubsonicService::BuildRequestUrl(const QString &view, const RequestOptions &options) +QUrl SubsonicService::BuildRequestUrl(const QString &view, const RequestOptions *options) { - QUrl url(server_url_ + "rest/" + view + ".view"); + QUrl url(server_ + "rest/" + view + ".view"); url.addQueryItem("v", kApiVersion); url.addQueryItem("c", kApiClientName); + // TODO: only send username/password for login url.addQueryItem("u", username_); url.addQueryItem("p", password_); - for (RequestOptions::const_iterator i = options.begin(); i != options.end(); ++i) + if (options) { - url.addQueryItem(i.key(), i.value()); + for (RequestOptions::const_iterator i = options->begin(); i != options->end(); ++i) + { + url.addQueryItem(i.key(), i.value()); + } } return url; } + +void SubsonicService::onPingFinished() +{ + QNetworkReply *reply = qobject_cast(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) + { + 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_; + emit LoginStateChanged(); +} diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index f574f7741..1eff20614 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -3,37 +3,76 @@ #include "internetservice.h" +class QNetworkAccessManager; + class SubsonicService : public InternetService { Q_OBJECT + public: SubsonicService(InternetModel *parent); ~SubsonicService(); + enum LoginState { + LoginState_Loggedin, + LoginState_BadServer, + LoginState_BadCredentials, + LoginState_Unlicensed, + LoginState_OtherError, + }; + typedef QMap RequestOptions; QStandardItem* CreateRootItem(); void LazyPopulate(QStandardItem *item); + 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(); static const char* kServiceName; static const char* kSettingsGroup; static const char* kApiVersion; static const char* kApiClientName; + signals: + void LoginStateChanged(); + protected: QModelIndex GetCurrentIndex(); private: - QUrl BuildRequestUrl(const QString &view, const RequestOptions &options); + 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, + }; + + // TODO: Remove second argument, let caller call addQueryItem() + QUrl BuildRequestUrl(const QString &view, const RequestOptions *options = 0); QModelIndex context_item_; QStandardItem* root_; + QNetworkAccessManager* network_; // Configuration - QString server_url_; + QString server_; QString username_; QString password_; + LoginState login_state_; + + private slots: + void onPingFinished(); }; #endif // SUBSONICSERVICE_H From 76b0ffb1f2537b935907d4e46eaa0a0aecff3c84 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 8 Dec 2011 20:00:50 +0000 Subject: [PATCH 04/31] Hooked up LoginStateWidget for Subsonic settings --- src/internet/subsonicservice.cpp | 10 +++-- src/internet/subsonicservice.h | 3 +- src/internet/subsonicsettingspage.cpp | 65 +++++++++++++++++++++++++-- src/internet/subsonicsettingspage.h | 9 +++- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index efd8ef224..b2aaa8609 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -44,7 +44,6 @@ void SubsonicService::ReloadSettings() username_ = s.value("username").toString(); password_ = s.value("password").toString(); - // TODO: Move this? Login(); } @@ -52,7 +51,9 @@ void SubsonicService::Login() { // Forget session ID network_->setCookieJar(new QNetworkCookieJar(network_)); - // Ping is enough to authenticate + // Forget login state whilst waiting + login_state_ = LoginState_Unknown; + // Ping is enough to check credentials Ping(); } @@ -82,7 +83,6 @@ QUrl SubsonicService::BuildRequestUrl(const QString &view, const RequestOptions QUrl url(server_ + "rest/" + view + ".view"); url.addQueryItem("v", kApiVersion); url.addQueryItem("c", kApiClientName); - // TODO: only send username/password for login url.addQueryItem("u", username_); url.addQueryItem("p", password_); if (options) @@ -119,6 +119,8 @@ void SubsonicService::onPingFinished() int error = reader.attributes().value("code").toString().toInt(); switch (error) { + // "Parameter missing" for "ping" is always blank username or password + case ApiError_ParameterMissing: case ApiError_BadCredentials: login_state_ = LoginState_BadCredentials; break; @@ -132,5 +134,5 @@ void SubsonicService::onPingFinished() } } qLog(Debug) << "Login state changed: " << login_state_; - emit LoginStateChanged(); + emit LoginStateChanged(login_state_); } diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 1eff20614..418d1e5d6 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -19,6 +19,7 @@ class SubsonicService : public InternetService LoginState_BadCredentials, LoginState_Unlicensed, LoginState_OtherError, + LoginState_Unknown, }; typedef QMap RequestOptions; @@ -40,7 +41,7 @@ class SubsonicService : public InternetService static const char* kApiClientName; signals: - void LoginStateChanged(); + void LoginStateChanged(SubsonicService::LoginState newstate); protected: QModelIndex GetCurrentIndex(); diff --git a/src/internet/subsonicsettingspage.cpp b/src/internet/subsonicsettingspage.cpp index b8d6788a6..ec0768d6b 100644 --- a/src/internet/subsonicsettingspage.cpp +++ b/src/internet/subsonicsettingspage.cpp @@ -1,16 +1,26 @@ #include "subsonicsettingspage.h" #include "ui_subsonicsettingspage.h" -#include "subsonicservice.h" +#include "internetmodel.h" #include SubsonicSettingsPage::SubsonicSettingsPage(SettingsDialog *dialog) : SettingsPage(dialog), - ui_(new Ui_SubsonicSettingsPage) + 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() @@ -26,6 +36,9 @@ void SubsonicSettingsPage::Load() ui_->server->setText(s.value("server").toString()); ui_->username->setText(s.value("username").toString()); ui_->password->setText(s.value("password").toString()); + + // "Login" with the existing settings to see if they work + Login(); } void SubsonicSettingsPage::Save() @@ -37,3 +50,49 @@ void SubsonicSettingsPage::Save() 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("Unable to contact Subsonic server - check server URL.")); + break; + + case SubsonicService::LoginState_BadCredentials: + ui_->login_state->SetAccountTypeText(tr("Your username or password was incorrect.")); + break; + + case SubsonicService::LoginState_Unlicensed: + ui_->login_state->SetAccountTypeText(tr("The Subsonic API is only available on licensed servers.")); + 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 index acc4f021c..fda78c4dd 100644 --- a/src/internet/subsonicsettingspage.h +++ b/src/internet/subsonicsettingspage.h @@ -2,9 +2,9 @@ #define SUBSONICSETTINGSPAGE_H #include "ui/settingspage.h" +#include "subsonicservice.h" class Ui_SubsonicSettingsPage; -class SubsonicService; class SubsonicSettingsPage : public SettingsPage { @@ -17,6 +17,13 @@ class SubsonicSettingsPage : public SettingsPage void Load(); void Save(); + public slots: + void LoginStateChanged(SubsonicService::LoginState newstate); + + private slots: + void Login(); + void Logout(); + private: Ui_SubsonicSettingsPage* ui_; SubsonicService* service_; From 89ac5a41f43ff4294fcfe6114a0834370984e1e0 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Fri, 9 Dec 2011 00:13:17 +0000 Subject: [PATCH 05/31] Subsonic: lazy-load top-level "music folders" --- src/internet/subsonicservice.cpp | 59 +++++++++++++++++++++++++------- src/internet/subsonicservice.h | 19 ++++++++-- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index b2aaa8609..92736680d 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -32,7 +32,15 @@ QStandardItem* SubsonicService::CreateRootItem() void SubsonicService::LazyPopulate(QStandardItem *item) { + switch (item->data(InternetModel::Role_Type).toInt()) + { + case InternetModel::Type_Service: + GetMusicFolders(); + break; + default: + break; + } } void SubsonicService::ReloadSettings() @@ -67,10 +75,12 @@ void SubsonicService::Login(const QString &server, const QString &username, cons void SubsonicService::Ping() { - QUrl request_url = BuildRequestUrl("ping"); - QNetworkReply *reply = network_->get(QNetworkRequest(request_url)); - reply->ignoreSslErrors(); - connect(reply, SIGNAL(finished()), this, SLOT(onPingFinished())); + Send(BuildRequestUrl("ping"), SLOT(onPingFinished())); +} + +void SubsonicService::GetMusicFolders() +{ + Send(BuildRequestUrl("getMusicFolders"), SLOT(onGetMusicFoldersFinished())); } QModelIndex SubsonicService::GetCurrentIndex() @@ -78,23 +88,24 @@ QModelIndex SubsonicService::GetCurrentIndex() return context_item_; } -QUrl SubsonicService::BuildRequestUrl(const QString &view, const RequestOptions *options) +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_); - if (options) - { - for (RequestOptions::const_iterator i = options->begin(); i != options->end(); ++i) - { - url.addQueryItem(i.key(), i.value()); - } - } return url; } +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); +} + void SubsonicService::onPingFinished() { QNetworkReply *reply = qobject_cast(sender()); @@ -136,3 +147,27 @@ void SubsonicService::onPingFinished() qLog(Debug) << "Login state changed: " << login_state_; emit LoginStateChanged(login_state_); } + +void SubsonicService::onGetMusicFoldersFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + reply->deleteLater(); + QXmlStreamReader reader(reply); + + reader.readNextStartElement(); + if (reader.attributes().value("status") != "ok") + { + // TODO: error handling + return; + } + + reader.readNextStartElement(); + Q_ASSERT(reader.name() == "musicFolders"); + while (reader.readNextStartElement()) + { + QStandardItem *item = new QStandardItem(reader.attributes().value("name").toString()); + item->setData(Type_TopLevel, InternetModel::Role_Type); + root_->appendRow(item); + reader.skipCurrentElement(); + } +} diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 418d1e5d6..1f4129ba4 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -1,6 +1,7 @@ #ifndef SUBSONICSERVICE_H #define SUBSONICSERVICE_H +#include "internetmodel.h" #include "internetservice.h" class QNetworkAccessManager; @@ -22,6 +23,17 @@ class SubsonicService : public InternetService LoginState_Unknown, }; + enum Type { + Type_TopLevel = InternetModel::TypeCount, + Type_Artist, + Type_Album, + Type_Track, + }; + + enum Role { + Role_Id = InternetModel::RoleCount, + }; + typedef QMap RequestOptions; QStandardItem* CreateRootItem(); @@ -34,6 +46,7 @@ class SubsonicService : public InternetService // Subsonic API methods void Ping(); + void GetMusicFolders(); static const char* kServiceName; static const char* kSettingsGroup; @@ -58,8 +71,9 @@ class SubsonicService : public InternetService ApiError_NotFound = 70, }; - // TODO: Remove second argument, let caller call addQueryItem() - QUrl BuildRequestUrl(const QString &view, const RequestOptions *options = 0); + QUrl BuildRequestUrl(const QString &view); + // Convenience function to reduce QNetworkRequest/QNetworkReply/connect boilerplate + void Send(const QUrl &url, const char *slot); QModelIndex context_item_; QStandardItem* root_; @@ -74,6 +88,7 @@ class SubsonicService : public InternetService private slots: void onPingFinished(); + void onGetMusicFoldersFinished(); }; #endif // SUBSONICSERVICE_H From 96fc17bd46c96d7a1b7a023cb917cebe322a3163 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Sat, 10 Dec 2011 19:04:04 +0000 Subject: [PATCH 06/31] Subsonic: ability to explore full subsonic library via tree view Removed multiple top-level directory support Bumped client API version 1.6.0 -> 1.7.0 (depends on server version 4.6) --- src/internet/subsonicservice.cpp | 128 ++++++++++++++++++++++++++++--- src/internet/subsonicservice.h | 17 +++- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 92736680d..6e19f3fa8 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -9,13 +9,14 @@ const char* SubsonicService::kServiceName = "Subsonic"; const char* SubsonicService::kSettingsGroup = "Subsonic"; -const char* SubsonicService::kApiVersion = "1.6.0"; +const char* SubsonicService::kApiVersion = "1.7.0"; const char* SubsonicService::kApiClientName = "Clementine"; SubsonicService::SubsonicService(InternetModel *parent) : InternetService(kServiceName, parent, parent), network_(new QNetworkAccessManager(this)), - login_state_(LoginState_OtherError) + login_state_(LoginState_OtherError), + item_lookup_() { } @@ -35,7 +36,12 @@ void SubsonicService::LazyPopulate(QStandardItem *item) switch (item->data(InternetModel::Role_Type).toInt()) { case InternetModel::Type_Service: - GetMusicFolders(); + GetIndexes(); + break; + + case Type_Artist: + case Type_Album: + GetMusicDirectory(item->data(Role_Id).toString()); break; default: @@ -78,9 +84,16 @@ void SubsonicService::Ping() Send(BuildRequestUrl("ping"), SLOT(onPingFinished())); } -void SubsonicService::GetMusicFolders() +void SubsonicService::GetIndexes() { - Send(BuildRequestUrl("getMusicFolders"), SLOT(onGetMusicFoldersFinished())); + Send(BuildRequestUrl("getIndexes"), SLOT(onGetIndexesFinished())); +} + +void SubsonicService::GetMusicDirectory(const QString &id) +{ + QUrl url = BuildRequestUrl("getMusicDirectory"); + url.addQueryItem("id", id); + Send(url, SLOT(onGetMusicDirectoryFinished())); } QModelIndex SubsonicService::GetCurrentIndex() @@ -106,6 +119,54 @@ void SubsonicService::Send(const QUrl &url, const char *slot) connect(reply, SIGNAL(finished()), slot); } +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"); + QString id = reader->attributes().value("id").toString(); + QStandardItem *item = new QStandardItem(reader->attributes().value("title").toString()); + item->setData(Type_Track, InternetModel::Role_Type); + item->setData(id, Role_Id); + parent->appendRow(item); + item_lookup_.insert(id, item); + reader->skipCurrentElement(); +} + void SubsonicService::onPingFinished() { QNetworkReply *reply = qobject_cast(sender()); @@ -148,13 +209,15 @@ void SubsonicService::onPingFinished() emit LoginStateChanged(login_state_); } -void SubsonicService::onGetMusicFoldersFinished() +void SubsonicService::onGetIndexesFinished() { QNetworkReply *reply = qobject_cast(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 @@ -162,12 +225,55 @@ void SubsonicService::onGetMusicFoldersFinished() } reader.readNextStartElement(); - Q_ASSERT(reader.name() == "musicFolders"); + Q_ASSERT(reader.name() == "indexes"); while (reader.readNextStartElement()) { - QStandardItem *item = new QStandardItem(reader.attributes().value("name").toString()); - item->setData(Type_TopLevel, InternetModel::Role_Type); - root_->appendRow(item); - reader.skipCurrentElement(); + 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() +{ + QNetworkReply *reply = qobject_cast(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() == "directory"); + QStandardItem *parent = item_lookup_.value(reader.attributes().value("id").toString()); + while (reader.readNextStartElement()) + { + if (reader.attributes().value("isDir") == "true") + { + ReadAlbum(&reader, parent); + } + else if (reader.attributes().value("isVideo") == "false") + { + ReadTrack(&reader, parent); + } + else + { + reader.skipCurrentElement(); + } } } diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 1f4129ba4..6e3989456 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -5,6 +5,7 @@ #include "internetservice.h" class QNetworkAccessManager; +class QXmlStreamReader; class SubsonicService : public InternetService { @@ -24,8 +25,7 @@ class SubsonicService : public InternetService }; enum Type { - Type_TopLevel = InternetModel::TypeCount, - Type_Artist, + Type_Artist = InternetModel::TypeCount, Type_Album, Type_Track, }; @@ -46,7 +46,8 @@ class SubsonicService : public InternetService // Subsonic API methods void Ping(); - void GetMusicFolders(); + void GetIndexes(); + void GetMusicDirectory(const QString &id); static const char* kServiceName; static const char* kSettingsGroup; @@ -75,6 +76,11 @@ class SubsonicService : public InternetService // Convenience function to reduce QNetworkRequest/QNetworkReply/connect boilerplate void Send(const QUrl &url, const char *slot); + void ReadIndex(QXmlStreamReader *reader, QStandardItem *parent); + void ReadArtist(QXmlStreamReader *reader, QStandardItem *parent); + void ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent); + void ReadTrack(QXmlStreamReader *reader, QStandardItem *parent); + QModelIndex context_item_; QStandardItem* root_; QNetworkAccessManager* network_; @@ -86,9 +92,12 @@ class SubsonicService : public InternetService LoginState login_state_; + QMap item_lookup_; + private slots: void onPingFinished(); - void onGetMusicFoldersFinished(); + void onGetIndexesFinished(); + void onGetMusicDirectoryFinished(); }; #endif // SUBSONICSERVICE_H From eafb351126baf6a8440977f89d8c7faff1835e82 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Sat, 10 Dec 2011 23:11:05 +0000 Subject: [PATCH 07/31] Subsonic: enabled playing tracks from subsonic --- src/internet/subsonicservice.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 6e19f3fa8..e8b2cee8a 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -158,10 +158,28 @@ void SubsonicService::ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent) void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) { Q_ASSERT(reader->name() == "child"); + + 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_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"); + url.addQueryItem("id", id); + song.set_url(url); + QStandardItem *item = new QStandardItem(reader->attributes().value("title").toString()); item->setData(Type_Track, InternetModel::Role_Type); item->setData(id, Role_Id); + item->setData(QVariant::fromValue(song), InternetModel::Role_SongMetadata); + item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); + item->setData(song.url(), InternetModel::Role_Url); parent->appendRow(item); item_lookup_.insert(id, item); reader->skipCurrentElement(); From 577d13038a26d092c6589aeab2ebd4762816775b Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Tue, 10 Jan 2012 16:52:54 +0000 Subject: [PATCH 08/31] Created SubsonicUrlHandler --- src/CMakeLists.txt | 2 ++ src/internet/subsonicservice.cpp | 7 +++++ src/internet/subsonicservice.h | 5 ++++ src/internet/subsonicurlhandler.cpp | 16 +++++++++++ src/internet/subsonicurlhandler.h | 41 +++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 src/internet/subsonicurlhandler.cpp create mode 100644 src/internet/subsonicurlhandler.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a265e0bef..2a22d49fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -171,6 +171,7 @@ set(SOURCES internet/somafmurlhandler.cpp internet/subsonicservice.cpp internet/subsonicsettingspage.cpp + internet/subsonicurlhandler.cpp library/groupbydialog.cpp library/library.cpp @@ -415,6 +416,7 @@ set(HEADERS internet/somafmurlhandler.h internet/subsonicservice.h internet/subsonicsettingspage.h + internet/subsonicurlhandler.h library/groupbydialog.h library/library.h diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index e8b2cee8a..55348a569 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -1,6 +1,8 @@ +#include "subsonicurlhandler.h" #include "subsonicservice.h" #include "internetmodel.h" #include "core/logging.h" +#include "core/player.h" #include #include @@ -15,9 +17,13 @@ const char* SubsonicService::kApiClientName = "Clementine"; SubsonicService::SubsonicService(InternetModel *parent) : InternetService(kServiceName, parent, parent), network_(new QNetworkAccessManager(this)), + http_url_handler_(new SubsonicUrlHandler(this, this)), + https_url_handler_(new SubsonicHttpsUrlHandler(this, this)), login_state_(LoginState_OtherError), item_lookup_() { + model()->player()->RegisterUrlHandler(http_url_handler_); + model()->player()->RegisterUrlHandler(https_url_handler_); } SubsonicService::~SubsonicService() @@ -171,6 +177,7 @@ void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) length *= 1000000000; song.set_length_nanosec(length); QUrl url = BuildRequestUrl("stream"); + url.setScheme(url.scheme() == "https" ? "subsonics" : "subsonic"); url.addQueryItem("id", id); song.set_url(url); diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 6e3989456..d1ce254da 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -7,6 +7,9 @@ class QNetworkAccessManager; class QXmlStreamReader; +class SubsonicUrlHandler; +class SubsonicHttpsUrlHandler; + class SubsonicService : public InternetService { Q_OBJECT @@ -84,6 +87,8 @@ class SubsonicService : public InternetService QModelIndex context_item_; QStandardItem* root_; QNetworkAccessManager* network_; + SubsonicUrlHandler* http_url_handler_; + SubsonicHttpsUrlHandler* https_url_handler_; // Configuration QString server_; diff --git a/src/internet/subsonicurlhandler.cpp b/src/internet/subsonicurlhandler.cpp new file mode 100644 index 000000000..b229809f3 --- /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(url); + newurl.setScheme(realscheme()); + return LoadResult(url, LoadResult::TrackAvailable, newurl); +} diff --git a/src/internet/subsonicurlhandler.h b/src/internet/subsonicurlhandler.h new file mode 100644 index 000000000..48b56bf09 --- /dev/null +++ b/src/internet/subsonicurlhandler.h @@ -0,0 +1,41 @@ +#ifndef SUBSONICURLHANDLER_H +#define SUBSONICURLHANDLER_H + +#include "core/urlhandler.h" + +class SubsonicService; + +// Subsonic URL handler. +// For now, at least, Subsonic URLs are just HTTP URLs but with the scheme +// replaced with "subsonic" (or "subsonics" for HTTPS). This is a hook to +// allow magic to be implemented later. +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); + + protected: + virtual QString realscheme() const { return "http"; } + + private: + SubsonicService* service_; +}; + +class SubsonicHttpsUrlHandler : public SubsonicUrlHandler { + Q_OBJECT + public: + SubsonicHttpsUrlHandler(SubsonicService* service, QObject* parent) + : SubsonicUrlHandler(service, parent) {} + + QString scheme() const { return "subsonics"; } + + protected: + QString realscheme() const { return "https"; } +}; + +#endif // SUBSONICURLHANDLER_H From ea08c583c4225d4dc0347eab099df343629521dc Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Tue, 10 Jan 2012 21:23:40 +0000 Subject: [PATCH 09/31] Subsonic: Added more song metadata, and feedback when lazy-loading --- src/internet/subsonicservice.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 55348a569..0aa923a44 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -53,6 +53,10 @@ void SubsonicService::LazyPopulate(QStandardItem *item) default: break; } + + item->setRowCount(0); + QStandardItem* loading = new QStandardItem(tr("Loading...")); + item->appendRow(loading); } void SubsonicService::ReloadSettings() @@ -169,6 +173,7 @@ void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) 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_bitrate(reader->attributes().value("bitRate").toString().toInt()); song.set_year(reader->attributes().value("year").toString().toInt()); @@ -180,6 +185,7 @@ void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) url.setScheme(url.scheme() == "https" ? "subsonics" : "subsonic"); url.addQueryItem("id", id); song.set_url(url); + song.set_filesize(reader->attributes().value("size").toString().toInt()); QStandardItem *item = new QStandardItem(reader->attributes().value("title").toString()); item->setData(Type_Track, InternetModel::Role_Type); @@ -251,6 +257,7 @@ void SubsonicService::onGetIndexesFinished() reader.readNextStartElement(); Q_ASSERT(reader.name() == "indexes"); + root_->setRowCount(0); while (reader.readNextStartElement()) { if (reader.name() == "index") @@ -286,6 +293,7 @@ void SubsonicService::onGetMusicDirectoryFinished() reader.readNextStartElement(); Q_ASSERT(reader.name() == "directory"); QStandardItem *parent = item_lookup_.value(reader.attributes().value("id").toString()); + parent->setRowCount(0); while (reader.readNextStartElement()) { if (reader.attributes().value("isDir") == "true") From 024af25b00e0577c06f632ab56adebb25f6a9f69 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 16 Jan 2012 14:22:30 +0000 Subject: [PATCH 10/31] Subsonic: Added log output to diagnose "could not connect to server" problems --- src/internet/subsonicservice.cpp | 7 ++++++- src/internet/subsonicservice.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 0aa923a44..eaa4787fa 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -3,6 +3,7 @@ #include "internetmodel.h" #include "core/logging.h" #include "core/player.h" +#include "core/utilities.h" #include #include @@ -206,6 +207,9 @@ void SubsonicService::onPingFinished() if (reply->error() != QNetworkReply::NoError) { login_state_ = LoginState_BadServer; + qLog(Error) << "Failed to connect (" + << Utilities::EnumToString(QNetworkReply::staticMetaObject, "NetworkError", reply->error()) + << "):" << reply->errorString(); } else { @@ -236,7 +240,8 @@ void SubsonicService::onPingFinished() } } } - qLog(Debug) << "Login state changed: " << login_state_; + qLog(Debug) << "Login state changed:" + << Utilities::EnumToString(SubsonicService::staticMetaObject, "LoginState", login_state_); emit LoginStateChanged(login_state_); } diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index d1ce254da..d0931ad2a 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -13,6 +13,7 @@ class SubsonicHttpsUrlHandler; class SubsonicService : public InternetService { Q_OBJECT + Q_ENUMS(LoginState) public: SubsonicService(InternetModel *parent); From 705a547ff23b849e13778759b3ad4228e6b69494 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 16 Jan 2012 14:31:45 +0000 Subject: [PATCH 11/31] Subsonic: Even more error log output --- src/internet/subsonicservice.cpp | 3 +++ src/internet/subsonicservice.h | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index eaa4787fa..68c84a29c 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -224,6 +224,9 @@ void SubsonicService::onPingFinished() { 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 diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index d0931ad2a..574f3706e 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -14,6 +14,7 @@ class SubsonicService : public InternetService { Q_OBJECT Q_ENUMS(LoginState) + Q_ENUMS(ApiError) public: SubsonicService(InternetModel *parent); @@ -28,6 +29,17 @@ class SubsonicService : public InternetService 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, @@ -65,17 +77,6 @@ class SubsonicService : public InternetService QModelIndex GetCurrentIndex(); private: - 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, - }; - QUrl BuildRequestUrl(const QString &view); // Convenience function to reduce QNetworkRequest/QNetworkReply/connect boilerplate void Send(const QUrl &url, const char *slot); From 6eb1e853a33fbaad3fc7fc24765442c1ac867d18 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 16 Jan 2012 23:00:39 +0000 Subject: [PATCH 12/31] Subsonic: using a "more correct" way of handling self-signed SSL certificates --- src/internet/subsonicservice.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 68c84a29c..1ec7def11 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include const char* SubsonicService::kServiceName = "Subsonic"; @@ -124,9 +125,13 @@ QUrl SubsonicService::BuildRequestUrl(const QString &view) 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(); + 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); connect(reply, SIGNAL(finished()), slot); } From 551551b451914f9e3266810399ad569e73ecf928 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 18 Jan 2012 21:21:42 +0100 Subject: [PATCH 13/31] When API version doesn't match, show error message (fixes #2) --- src/internet/subsonicservice.cpp | 3 +++ src/internet/subsonicservice.h | 1 + src/internet/subsonicsettingspage.cpp | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 1ec7def11..28f08287b 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -242,6 +242,9 @@ void SubsonicService::onPingFinished() case ApiError_Unlicensed: login_state_ = LoginState_Unlicensed; break; + case ApiError_OutdatedServer: + login_state_ = LoginState_OutdatedServer; + break; default: login_state_ = LoginState_OtherError; break; diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 574f3706e..be4621d8e 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -25,6 +25,7 @@ class SubsonicService : public InternetService LoginState_BadServer, LoginState_BadCredentials, LoginState_Unlicensed, + LoginState_OutdatedServer, LoginState_OtherError, LoginState_Unknown, }; diff --git a/src/internet/subsonicsettingspage.cpp b/src/internet/subsonicsettingspage.cpp index ec0768d6b..827296350 100644 --- a/src/internet/subsonicsettingspage.cpp +++ b/src/internet/subsonicsettingspage.cpp @@ -76,6 +76,10 @@ void SubsonicSettingsPage::LoginStateChanged(SubsonicService::LoginState newstat ui_->login_state->SetAccountTypeText(tr("The Subsonic API is only available on licensed servers.")); break; + case SubsonicService::LoginState_OutdatedServer: + ui_->login_state->SetAccountTypeText(tr("Incompatible Subsonic REST protocol version. Server must upgrade.")); + break; + case SubsonicService::LoginState_OtherError: ui_->login_state->SetAccountTypeText(tr("An unspecified error occurred.")); break; From 33a9adffa61146f24284202603ea8863b4cbceaa Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 19 Jan 2012 22:59:59 +0000 Subject: [PATCH 14/31] Handle "outdated client API" and "unlicensed" errors, improved error messages --- src/internet/subsonicservice.cpp | 7 +++++-- src/internet/subsonicservice.h | 3 ++- src/internet/subsonicsettingspage.cpp | 14 ++++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 28f08287b..e8ffe8760 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -239,12 +239,15 @@ void SubsonicService::onPingFinished() case ApiError_BadCredentials: login_state_ = LoginState_BadCredentials; break; - case ApiError_Unlicensed: - login_state_ = LoginState_Unlicensed; + 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; diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index be4621d8e..02939afce 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -23,9 +23,10 @@ class SubsonicService : public InternetService enum LoginState { LoginState_Loggedin, LoginState_BadServer, + LoginState_OutdatedClient, + LoginState_OutdatedServer, LoginState_BadCredentials, LoginState_Unlicensed, - LoginState_OutdatedServer, LoginState_OtherError, LoginState_Unknown, }; diff --git a/src/internet/subsonicsettingspage.cpp b/src/internet/subsonicsettingspage.cpp index 827296350..88ec1b9bd 100644 --- a/src/internet/subsonicsettingspage.cpp +++ b/src/internet/subsonicsettingspage.cpp @@ -65,21 +65,27 @@ void SubsonicSettingsPage::LoginStateChanged(SubsonicService::LoginState newstat switch (newstate) { case SubsonicService::LoginState_BadServer: - ui_->login_state->SetAccountTypeText(tr("Unable to contact Subsonic server - check server URL.")); + 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("Your username or password was incorrect.")); + ui_->login_state->SetAccountTypeText(tr("Wrong username or password.")); break; - case SubsonicService::LoginState_Unlicensed: - ui_->login_state->SetAccountTypeText(tr("The Subsonic API is only available on licensed servers.")); + 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; From 81b7b9e8987c5c2957d18d5c41c1c935e39a47ae Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 19 Jan 2012 23:22:50 +0000 Subject: [PATCH 15/31] Invalidate Subsonic browser tree when login state changes Also, opening preferences no longer causes an implicit login state change. --- src/internet/subsonicservice.cpp | 8 ++++++++ src/internet/subsonicservice.h | 1 + src/internet/subsonicsettingspage.cpp | 5 +++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index e8ffe8760..415ffee44 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -26,6 +26,8 @@ SubsonicService::SubsonicService(InternetModel *parent) { model()->player()->RegisterUrlHandler(http_url_handler_); model()->player()->RegisterUrlHandler(https_url_handler_); + connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), + SLOT(onLoginStateChanged(SubsonicService::LoginState))); } SubsonicService::~SubsonicService() @@ -204,6 +206,12 @@ void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) reader->skipCurrentElement(); } +void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) +{ + root_->setRowCount(0); + root_->setData(true, InternetModel::Role_CanLazyLoad); +} + void SubsonicService::onPingFinished() { QNetworkReply *reply = qobject_cast(sender()); diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 02939afce..a345b478b 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -104,6 +104,7 @@ class SubsonicService : public InternetService QMap item_lookup_; private slots: + void onLoginStateChanged(SubsonicService::LoginState newstate); void onPingFinished(); void onGetIndexesFinished(); void onGetMusicDirectoryFinished(); diff --git a/src/internet/subsonicsettingspage.cpp b/src/internet/subsonicsettingspage.cpp index 88ec1b9bd..7cef147e0 100644 --- a/src/internet/subsonicsettingspage.cpp +++ b/src/internet/subsonicsettingspage.cpp @@ -37,8 +37,9 @@ void SubsonicSettingsPage::Load() ui_->username->setText(s.value("username").toString()); ui_->password->setText(s.value("password").toString()); - // "Login" with the existing settings to see if they work - Login(); + // These are the same settings SubsonicService will have used already, so see if + // they were successful... + LoginStateChanged(service_->login_state()); } void SubsonicSettingsPage::Save() From 532185c15ed9c077fc6ac45cbf911c70a640177d Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 30 Jan 2012 13:39:23 +0000 Subject: [PATCH 16/31] Subsonic: Add folder icons for non-track tree items --- src/internet/subsonicservice.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 415ffee44..b0d681eea 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -4,6 +4,7 @@ #include "core/logging.h" #include "core/player.h" #include "core/utilities.h" +#include "ui/iconloader.h" #include #include @@ -151,7 +152,8 @@ 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()); + QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), + reader->attributes().value("name").toString()); item->setData(Type_Artist, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); item->setData(id, Role_Id); @@ -164,7 +166,8 @@ 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()); + QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), + reader->attributes().value("title").toString()); item->setData(Type_Album, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); item->setData(id, Role_Id); From 6c8d1b25b73f4f2576f05fbdc356dd30e56031cc Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Sat, 28 Jul 2012 13:08:35 +0100 Subject: [PATCH 17/31] Accept self-signed certificate in https:// stream --- src/engines/gstenginepipeline.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index 76daf747f..f2ecefcc7 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -726,6 +726,12 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin, GParamSpec *ps g_object_set(element, "extra-headers", headers, NULL); gst_structure_free(headers); } + if (element && + g_object_class_find_property(G_OBJECT_GET_CLASS(element), "accept-self-signed")) { + // Can't always rely on HTTPS streams to have a valid 3rd-party-signed + // certificate, so tell neonhttpsrc to accept self-signed certificates. + g_object_set(element, "accept-self-signed", true, NULL); + } } void GstEnginePipeline::TransitionToNext() { From a62b7752adc58dee888924ba1d7bf808aac56b8c Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 30 Jan 2012 18:56:34 +0000 Subject: [PATCH 18/31] Recursive UserPlaylist adding, subsonic uses UserPlaylist for folders --- src/internet/internetmodel.cpp | 20 ++++++++++++++++++-- src/internet/subsonicservice.cpp | 18 ++++++++++++++---- src/internet/subsonicservice.h | 1 + 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index 8786d36f3..25bd740b8 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -231,8 +231,24 @@ QMimeData* InternetModel::mimeData(const QModelIndexList& indexes) const { int column = 0; QModelIndex child = index.child(row, column); while (child.isValid()) { - new_indexes << child; - urls << child.data(Role_Url).toUrl(); + // If the playlist contains another playlist, expand it + if (child.data(Role_Type).toInt() == Type_UserPlaylist) { + // "List" of indexes to recurse on + QModelIndexList templist; + templist.append(child); + // We know this is going to be an InternetMimeData because we're calling + // ourselves with something that we always return InternetMimeData for! + InternetMimeData* recurse = qobject_cast(mimeData(templist)); + // Add children if there were any + if (recurse) { + new_indexes.append(recurse->indexes); + urls.append(recurse->urls()); + delete recurse; + } + } else { + new_indexes << child; + urls << child.data(Role_Url).toUrl(); + } child = index.child(++row, column); } } else { diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index b0d681eea..efebc282a 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -46,12 +46,14 @@ void SubsonicService::LazyPopulate(QStandardItem *item) { switch (item->data(InternetModel::Role_Type).toInt()) { + // The "root" item case InternetModel::Type_Service: GetIndexes(); break; - case Type_Artist: - case Type_Album: + // Any folder item + case InternetModel::Type_UserPlaylist: + qLog(Debug) << "Lazy loading" << item->data(Role_Id).toString(); GetMusicDirectory(item->data(Role_Id).toString()); break; @@ -64,6 +66,12 @@ void SubsonicService::LazyPopulate(QStandardItem *item) item->appendRow(loading); } +smart_playlists::GeneratorPtr SubsonicService::CreateGenerator(QStandardItem* item) +{ + qLog(Debug) << "Attempting to smart load" << item->data(Role_Id).toString(); + return smart_playlists::GeneratorPtr(); +} + void SubsonicService::ReloadSettings() { QSettings s; @@ -154,8 +162,9 @@ void SubsonicService::ReadArtist(QXmlStreamReader *reader, QStandardItem *parent QString id = reader->attributes().value("id").toString(); QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), reader->attributes().value("name").toString()); - item->setData(Type_Artist, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); + item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type); + item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); item->setData(id, Role_Id); parent->appendRow(item); item_lookup_.insert(id, item); @@ -168,8 +177,9 @@ void SubsonicService::ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent) QString id = reader->attributes().value("id").toString(); QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), reader->attributes().value("title").toString()); - item->setData(Type_Album, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); + item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type); + item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); item->setData(id, Role_Id); parent->appendRow(item); item_lookup_.insert(id, item); diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index a345b478b..9dec97f4a 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -56,6 +56,7 @@ class SubsonicService : public InternetService QStandardItem* CreateRootItem(); void LazyPopulate(QStandardItem *item); + smart_playlists::GeneratorPtr CreateGenerator(QStandardItem* item); void ReloadSettings(); void Login(); From 6a049d7e50758466f075174a492ab13bb167b4ce Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Sun, 29 Jul 2012 03:30:01 +0100 Subject: [PATCH 19/31] Revert "Accept self-signed certificate in https:// stream" This reverts commit 6c8d1b25b73f4f2576f05fbdc356dd30e56031cc. The fix only applies when neonhttpsrc is being used, which only happens if gstreamer-bad-plugins is available without gstreamer-good-plugins. --- src/engines/gstenginepipeline.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index 4baa70656..6382eada0 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -734,12 +734,6 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin, GParamSpec *ps g_object_set(element, "extra-headers", headers, NULL); gst_structure_free(headers); } - if (element && - g_object_class_find_property(G_OBJECT_GET_CLASS(element), "accept-self-signed")) { - // Can't always rely on HTTPS streams to have a valid 3rd-party-signed - // certificate, so tell neonhttpsrc to accept self-signed certificates. - g_object_set(element, "accept-self-signed", true, NULL); - } } void GstEnginePipeline::TransitionToNext() { From 38ce86529ea5f95f9a03484c1f7dee20b54187a4 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Wed, 9 Jan 2013 21:47:51 +0000 Subject: [PATCH 20/31] Revert "Recursive UserPlaylist adding, subsonic uses UserPlaylist for folders" This reverts commit a62b7752adc58dee888924ba1d7bf808aac56b8c. Using this mechanism for recursively loading trees doesn't work any more, wasn't that great to start with, and the tree view will soon be replaced with a library view. --- src/internet/internetmodel.cpp | 20 ++------------------ src/internet/subsonicservice.cpp | 18 ++++-------------- src/internet/subsonicservice.h | 1 - 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index 1564ea952..f2b418a7b 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -258,24 +258,8 @@ QMimeData* InternetModel::mimeData(const QModelIndexList& indexes) const { int column = 0; QModelIndex child = index.child(row, column); while (child.isValid()) { - // If the playlist contains another playlist, expand it - if (child.data(Role_Type).toInt() == Type_UserPlaylist) { - // "List" of indexes to recurse on - QModelIndexList templist; - templist.append(child); - // We know this is going to be an InternetMimeData because we're calling - // ourselves with something that we always return InternetMimeData for! - InternetMimeData* recurse = qobject_cast(mimeData(templist)); - // Add children if there were any - if (recurse) { - new_indexes.append(recurse->indexes); - urls.append(recurse->urls()); - delete recurse; - } - } else { - new_indexes << child; - urls << child.data(Role_Url).toUrl(); - } + new_indexes << child; + urls << child.data(Role_Url).toUrl(); child = index.child(++row, column); } } else { diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index e2c01c537..5597b1d04 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -47,14 +47,12 @@ void SubsonicService::LazyPopulate(QStandardItem *item) { switch (item->data(InternetModel::Role_Type).toInt()) { - // The "root" item case InternetModel::Type_Service: GetIndexes(); break; - // Any folder item - case InternetModel::Type_UserPlaylist: - qLog(Debug) << "Lazy loading" << item->data(Role_Id).toString(); + case Type_Artist: + case Type_Album: GetMusicDirectory(item->data(Role_Id).toString()); break; @@ -67,12 +65,6 @@ void SubsonicService::LazyPopulate(QStandardItem *item) item->appendRow(loading); } -smart_playlists::GeneratorPtr SubsonicService::CreateGenerator(QStandardItem* item) -{ - qLog(Debug) << "Attempting to smart load" << item->data(Role_Id).toString(); - return smart_playlists::GeneratorPtr(); -} - void SubsonicService::ReloadSettings() { QSettings s; @@ -163,9 +155,8 @@ void SubsonicService::ReadArtist(QXmlStreamReader *reader, QStandardItem *parent QString id = reader->attributes().value("id").toString(); QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), reader->attributes().value("name").toString()); + item->setData(Type_Artist, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); - item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type); - item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); item->setData(id, Role_Id); parent->appendRow(item); item_lookup_.insert(id, item); @@ -178,9 +169,8 @@ void SubsonicService::ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent) QString id = reader->attributes().value("id").toString(); QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), reader->attributes().value("title").toString()); + item->setData(Type_Album, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); - item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type); - item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); item->setData(id, Role_Id); parent->appendRow(item); item_lookup_.insert(id, item); diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 024f2f047..a002fd564 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -56,7 +56,6 @@ class SubsonicService : public InternetService QStandardItem* CreateRootItem(); void LazyPopulate(QStandardItem *item); - smart_playlists::GeneratorPtr CreateGenerator(QStandardItem* item); void ReloadSettings(); void Login(); From d05202265e4aeb670eb00759407c7e71d097b6d9 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Wed, 9 Jan 2013 22:20:55 +0000 Subject: [PATCH 21/31] Simplify Subsonic URL handling --- src/internet/subsonicservice.cpp | 20 ++++++++------------ src/internet/subsonicservice.h | 6 +++--- src/internet/subsonicurlhandler.cpp | 4 ++-- src/internet/subsonicurlhandler.h | 20 +------------------- 4 files changed, 14 insertions(+), 36 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 5597b1d04..3742750b4 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -21,13 +21,11 @@ const char* SubsonicService::kApiClientName = "Clementine"; SubsonicService::SubsonicService(Application* app, InternetModel *parent) : InternetService(kServiceName, app, parent, parent), network_(new QNetworkAccessManager(this)), - http_url_handler_(new SubsonicUrlHandler(this, this)), - https_url_handler_(new SubsonicHttpsUrlHandler(this, this)), + url_handler_(new SubsonicUrlHandler(this, this)), login_state_(LoginState_OtherError), item_lookup_() { - app_->player()->RegisterUrlHandler(http_url_handler_); - app_->player()->RegisterUrlHandler(https_url_handler_); + app_->player()->RegisterUrlHandler(url_handler_); connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), SLOT(onLoginStateChanged(SubsonicService::LoginState))); } @@ -112,11 +110,6 @@ void SubsonicService::GetMusicDirectory(const QString &id) Send(url, SLOT(onGetMusicDirectoryFinished())); } -QModelIndex SubsonicService::GetCurrentIndex() -{ - return context_item_; -} - QUrl SubsonicService::BuildRequestUrl(const QString &view) { QUrl url(server_ + "rest/" + view + ".view"); @@ -127,6 +120,11 @@ QUrl SubsonicService::BuildRequestUrl(const QString &view) return url; } +QModelIndex SubsonicService::GetCurrentIndex() +{ + return context_item_; +} + void SubsonicService::Send(const QUrl &url, const char *slot) { QNetworkRequest request(url); @@ -193,9 +191,7 @@ void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) qint64 length = reader->attributes().value("duration").toString().toInt(); length *= 1000000000; song.set_length_nanosec(length); - QUrl url = BuildRequestUrl("stream"); - url.setScheme(url.scheme() == "https" ? "subsonics" : "subsonic"); - url.addQueryItem("id", id); + QUrl url = QUrl(QString("subsonic://%1").arg(id)); song.set_url(url); song.set_filesize(reader->attributes().value("size").toString().toInt()); diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index a002fd564..37cbd318b 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -67,6 +67,8 @@ class SubsonicService : public InternetService void GetIndexes(); void GetMusicDirectory(const QString &id); + QUrl BuildRequestUrl(const QString &view); + static const char* kServiceName; static const char* kSettingsGroup; static const char* kApiVersion; @@ -79,7 +81,6 @@ class SubsonicService : public InternetService QModelIndex GetCurrentIndex(); private: - QUrl BuildRequestUrl(const QString &view); // Convenience function to reduce QNetworkRequest/QNetworkReply/connect boilerplate void Send(const QUrl &url, const char *slot); @@ -91,8 +92,7 @@ class SubsonicService : public InternetService QModelIndex context_item_; QStandardItem* root_; QNetworkAccessManager* network_; - SubsonicUrlHandler* http_url_handler_; - SubsonicHttpsUrlHandler* https_url_handler_; + SubsonicUrlHandler* url_handler_; // Configuration QString server_; diff --git a/src/internet/subsonicurlhandler.cpp b/src/internet/subsonicurlhandler.cpp index b229809f3..0e1e6fe5c 100644 --- a/src/internet/subsonicurlhandler.cpp +++ b/src/internet/subsonicurlhandler.cpp @@ -10,7 +10,7 @@ UrlHandler::LoadResult SubsonicUrlHandler::StartLoading(const QUrl& url) { if (service_->login_state() != SubsonicService::LoginState_Loggedin) return LoadResult(); - QUrl newurl(url); - newurl.setScheme(realscheme()); + 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 index 48b56bf09..6d820f6fc 100644 --- a/src/internet/subsonicurlhandler.h +++ b/src/internet/subsonicurlhandler.h @@ -5,10 +5,7 @@ class SubsonicService; -// Subsonic URL handler. -// For now, at least, Subsonic URLs are just HTTP URLs but with the scheme -// replaced with "subsonic" (or "subsonics" for HTTPS). This is a hook to -// allow magic to be implemented later. +// Subsonic URL handler: subsonic://id class SubsonicUrlHandler : public UrlHandler { Q_OBJECT public: @@ -19,23 +16,8 @@ class SubsonicUrlHandler : public UrlHandler { LoadResult StartLoading(const QUrl& url); //LoadResult LoadNext(const QUrl& url); - protected: - virtual QString realscheme() const { return "http"; } - private: SubsonicService* service_; }; -class SubsonicHttpsUrlHandler : public SubsonicUrlHandler { - Q_OBJECT - public: - SubsonicHttpsUrlHandler(SubsonicService* service, QObject* parent) - : SubsonicUrlHandler(service, parent) {} - - QString scheme() const { return "subsonics"; } - - protected: - QString realscheme() const { return "https"; } -}; - #endif // SUBSONICURLHANDLER_H From 349231793fc106fbec3e34f8332a86c7cc665eea Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 10 Jan 2013 22:08:52 +0000 Subject: [PATCH 22/31] Hacky basics of starting to load Subsonic library --- data/data.qrc | 1 + data/schema/schema-43.sql | 48 ++++++++++++++ src/core/database.cpp | 2 +- src/internet/subsonicservice.cpp | 110 ++++++++++++++++--------------- src/internet/subsonicservice.h | 20 +++--- 5 files changed, 120 insertions(+), 61 deletions(-) create mode 100644 data/schema/schema-43.sql diff --git a/data/data.qrc b/data/data.qrc index a590b703d..96ab888e4 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -337,6 +337,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 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/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/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 3742750b4..f23ee1004 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -6,26 +6,53 @@ #include "core/player.h" #include "core/utilities.h" #include "ui/iconloader.h" +#include "library/librarybackend.h" +#include "core/mergedproxymodel.h" +#include "core/database.h" #include #include #include #include #include +#include const char* SubsonicService::kServiceName = "Subsonic"; const char* SubsonicService::kSettingsGroup = "Subsonic"; const char* SubsonicService::kApiVersion = "1.7.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)), - login_state_(LoginState_OtherError), - item_lookup_() + library_backend_(NULL), + library_model_(NULL), + library_sort_model_(new QSortFilterProxyModel(this)), + login_state_(LoginState_OtherError) { app_->player()->RegisterUrlHandler(url_handler_); + + 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); + connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), SLOT(onLoginStateChanged(SubsonicService::LoginState))); } @@ -36,9 +63,9 @@ SubsonicService::~SubsonicService() QStandardItem* SubsonicService::CreateRootItem() { - root_ = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName); - root_->setData(true, InternetModel::Role_CanLazyLoad); - return root_; + QStandardItem* item = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName); + item->setData(true, InternetModel::Role_CanLazyLoad); + return item; } void SubsonicService::LazyPopulate(QStandardItem *item) @@ -46,21 +73,15 @@ void SubsonicService::LazyPopulate(QStandardItem *item) switch (item->data(InternetModel::Role_Type).toInt()) { case InternetModel::Type_Service: + // TODO: initiate library loading + library_backend_->DeleteAll(); GetIndexes(); - break; - - case Type_Artist: - case Type_Album: - GetMusicDirectory(item->data(Role_Id).toString()); + model()->merged_model()->AddSubModel(item->index(), library_sort_model_); break; default: break; } - - item->setRowCount(0); - QStandardItem* loading = new QStandardItem(tr("Loading...")); - item->appendRow(loading); } void SubsonicService::ReloadSettings() @@ -137,45 +158,31 @@ void SubsonicService::Send(const QUrl &url, const char *slot) connect(reply, SIGNAL(finished()), slot); } -void SubsonicService::ReadIndex(QXmlStreamReader *reader, QStandardItem *parent) +void SubsonicService::ReadIndex(QXmlStreamReader *reader) { Q_ASSERT(reader->name() == "index"); while (reader->readNextStartElement()) { - ReadArtist(reader, parent); + ReadArtist(reader); } } -void SubsonicService::ReadArtist(QXmlStreamReader *reader, QStandardItem *parent) +void SubsonicService::ReadArtist(QXmlStreamReader *reader) { Q_ASSERT(reader->name() == "artist"); - QString id = reader->attributes().value("id").toString(); - QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), - 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); + // TODO: recurse into directory reader->skipCurrentElement(); } -void SubsonicService::ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent) +void SubsonicService::ReadAlbum(QXmlStreamReader *reader) { Q_ASSERT(reader->name() == "child"); - QString id = reader->attributes().value("id").toString(); - QStandardItem *item = new QStandardItem(IconLoader::Load("document-open-folder"), - 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); + // TODO: recurse into directory reader->skipCurrentElement(); } -void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) +Song SubsonicService::ReadTrack(QXmlStreamReader *reader) { Q_ASSERT(reader->name() == "child"); @@ -195,21 +202,18 @@ void SubsonicService::ReadTrack(QXmlStreamReader *reader, QStandardItem *parent) song.set_url(url); song.set_filesize(reader->attributes().value("size").toString().toInt()); - QStandardItem *item = new QStandardItem(reader->attributes().value("title").toString()); - item->setData(Type_Track, InternetModel::Role_Type); - item->setData(id, Role_Id); - item->setData(QVariant::fromValue(song), InternetModel::Role_SongMetadata); - item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); - item->setData(song.url(), InternetModel::Role_Url); - parent->appendRow(item); - item_lookup_.insert(id, item); + // We need to set these to satisfy the database constraints + song.set_directory_id(0); + song.set_mtime(0); + song.set_ctime(0); + reader->skipCurrentElement(); + return song; } void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) { - root_->setRowCount(0); - root_->setData(true, InternetModel::Role_CanLazyLoad); + // TODO: library refresh logic? } void SubsonicService::onPingFinished() @@ -284,22 +288,25 @@ void SubsonicService::onGetIndexesFinished() reader.readNextStartElement(); Q_ASSERT(reader.name() == "indexes"); - root_->setRowCount(0); + // TODO: start loading library data + SongList songs; while (reader.readNextStartElement()) { if (reader.name() == "index") { - ReadIndex(&reader, root_); + ReadIndex(&reader); } else if (reader.name() == "child" && reader.attributes().value("isVideo") == "false") { - ReadTrack(&reader, root_); + songs << ReadTrack(&reader); } else { reader.skipCurrentElement(); } } + + library_backend_->AddOrUpdateSongs(songs); } void SubsonicService::onGetMusicDirectoryFinished() @@ -319,17 +326,16 @@ void SubsonicService::onGetMusicDirectoryFinished() reader.readNextStartElement(); Q_ASSERT(reader.name() == "directory"); - QStandardItem *parent = item_lookup_.value(reader.attributes().value("id").toString()); - parent->setRowCount(0); + // TODO: add tracks, etc. while (reader.readNextStartElement()) { if (reader.attributes().value("isDir") == "true") { - ReadAlbum(&reader, parent); + ReadAlbum(&reader); } else if (reader.attributes().value("isVideo") == "false") { - ReadTrack(&reader, parent); + ReadTrack(&reader); } else { diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 37cbd318b..b85c8979f 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -6,9 +6,9 @@ class QNetworkAccessManager; class QXmlStreamReader; +class QSortFilterProxyModel; class SubsonicUrlHandler; -class SubsonicHttpsUrlHandler; class SubsonicService : public InternetService { @@ -74,6 +74,9 @@ class SubsonicService : public InternetService static const char* kApiVersion; static const char* kApiClientName; + static const char* kSongsTable; + static const char* kFtsTable; + signals: void LoginStateChanged(SubsonicService::LoginState newstate); @@ -84,16 +87,19 @@ class SubsonicService : public InternetService // Convenience function to reduce QNetworkRequest/QNetworkReply/connect boilerplate void Send(const QUrl &url, const char *slot); - void ReadIndex(QXmlStreamReader *reader, QStandardItem *parent); - void ReadArtist(QXmlStreamReader *reader, QStandardItem *parent); - void ReadAlbum(QXmlStreamReader *reader, QStandardItem *parent); - void ReadTrack(QXmlStreamReader *reader, QStandardItem *parent); + void ReadIndex(QXmlStreamReader *reader); + void ReadArtist(QXmlStreamReader *reader); + void ReadAlbum(QXmlStreamReader *reader); + Song ReadTrack(QXmlStreamReader *reader); QModelIndex context_item_; - QStandardItem* root_; QNetworkAccessManager* network_; SubsonicUrlHandler* url_handler_; + LibraryBackend* library_backend_; + LibraryModel* library_model_; + QSortFilterProxyModel* library_sort_model_; + // Configuration QString server_; QString username_; @@ -101,8 +107,6 @@ class SubsonicService : public InternetService LoginState login_state_; - QMap item_lookup_; - private slots: void onLoginStateChanged(SubsonicService::LoginState newstate); void onPingFinished(); From 49d27b0bfedbd063cbcf30ea8916bf7131ce4264 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Fri, 11 Jan 2013 23:14:15 +0000 Subject: [PATCH 23/31] Load subsonic library recursively --- src/internet/subsonicservice.cpp | 30 ++++++++++++++++++++++++------ src/internet/subsonicservice.h | 6 ++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index f23ee1004..0ecb935ae 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -25,6 +25,8 @@ const char* SubsonicService::kApiClientName = "Clementine"; const char* SubsonicService::kSongsTable = "subsonic_songs"; const char* SubsonicService::kFtsTable = "subsonic_songs_fts"; +const int SubsonicService::kChunkSize = 1000; + SubsonicService::SubsonicService(Application* app, InternetModel *parent) : InternetService(kServiceName, app, parent, parent), network_(new QNetworkAccessManager(this)), @@ -126,6 +128,7 @@ void SubsonicService::GetIndexes() void SubsonicService::GetMusicDirectory(const QString &id) { + ++directory_count_; QUrl url = BuildRequestUrl("getMusicDirectory"); url.addQueryItem("id", id); Send(url, SLOT(onGetMusicDirectoryFinished())); @@ -171,14 +174,16 @@ void SubsonicService::ReadIndex(QXmlStreamReader *reader) void SubsonicService::ReadArtist(QXmlStreamReader *reader) { Q_ASSERT(reader->name() == "artist"); - // TODO: recurse into directory + QString id = reader->attributes().value("id").toString(); + GetMusicDirectory(id); reader->skipCurrentElement(); } void SubsonicService::ReadAlbum(QXmlStreamReader *reader) { Q_ASSERT(reader->name() == "child"); - // TODO: recurse into directory + QString id = reader->attributes().value("id").toString(); + GetMusicDirectory(id); reader->skipCurrentElement(); } @@ -286,10 +291,13 @@ void SubsonicService::onGetIndexesFinished() return; } + directory_count_ = 0; + processed_directory_count_ = 0; + new_songs_ = SongList(); + reader.readNextStartElement(); Q_ASSERT(reader.name() == "indexes"); // TODO: start loading library data - SongList songs; while (reader.readNextStartElement()) { if (reader.name() == "index") @@ -298,7 +306,7 @@ void SubsonicService::onGetIndexesFinished() } else if (reader.name() == "child" && reader.attributes().value("isVideo") == "false") { - songs << ReadTrack(&reader); + new_songs_ << ReadTrack(&reader); } else { @@ -306,7 +314,10 @@ void SubsonicService::onGetIndexesFinished() } } - library_backend_->AddOrUpdateSongs(songs); + if (new_songs_.size() >= kChunkSize || directory_count_ == processed_directory_count_) { + library_backend_->AddOrUpdateSongs(new_songs_); + new_songs_ = SongList(); + } } void SubsonicService::onGetMusicDirectoryFinished() @@ -316,6 +327,8 @@ void SubsonicService::onGetMusicDirectoryFinished() reply->deleteLater(); QXmlStreamReader reader(reply); + ++processed_directory_count_; + reader.readNextStartElement(); Q_ASSERT(reader.name() == "subsonic-response"); if (reader.attributes().value("status") != "ok") @@ -335,11 +348,16 @@ void SubsonicService::onGetMusicDirectoryFinished() } else if (reader.attributes().value("isVideo") == "false") { - ReadTrack(&reader); + new_songs_ << ReadTrack(&reader); } else { reader.skipCurrentElement(); } } + + if (new_songs_.size() >= kChunkSize || directory_count_ == processed_directory_count_) { + library_backend_->AddOrUpdateSongs(new_songs_); + new_songs_ = SongList(); + } } diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index b85c8979f..7e02fe717 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -77,6 +77,8 @@ class SubsonicService : public InternetService static const char* kSongsTable; static const char* kFtsTable; + static const int kChunkSize; + signals: void LoginStateChanged(SubsonicService::LoginState newstate); @@ -107,6 +109,10 @@ class SubsonicService : public InternetService LoginState login_state_; + int directory_count_; + int processed_directory_count_; + SongList new_songs_; + private slots: void onLoginStateChanged(SubsonicService::LoginState newstate); void onPingFinished(); From f88e73737c98ec09fece62defef4d4d183eb7021 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 14 Jan 2013 23:36:23 +0000 Subject: [PATCH 24/31] Re-implement subsonic library scanner Use the ID3-tag-oriented album listings in Subsonic >= 4.7 to fetch all library data. --- src/internet/subsonicservice.cpp | 256 +++++++++++++++---------------- src/internet/subsonicservice.h | 57 +++++-- 2 files changed, 161 insertions(+), 152 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 0ecb935ae..eb7fd4cbb 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -9,6 +9,7 @@ #include "library/librarybackend.h" #include "core/mergedproxymodel.h" #include "core/database.h" +#include "core/closure.h" #include #include @@ -25,12 +26,11 @@ const char* SubsonicService::kApiClientName = "Clementine"; const char* SubsonicService::kSongsTable = "subsonic_songs"; const char* SubsonicService::kFtsTable = "subsonic_songs_fts"; -const int SubsonicService::kChunkSize = 1000; - 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)), library_backend_(NULL), library_model_(NULL), library_sort_model_(new QSortFilterProxyModel(this)), @@ -38,6 +38,9 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) { app_->player()->RegisterUrlHandler(url_handler_); + connect(scanner_, SIGNAL(SongsDiscovered(SongList)), + SLOT(onSongsDiscovered(SongList))); + library_backend_ = new LibraryBackend; library_backend_->moveToThread(app_->database()->thread()); library_backend_->Init(app_->database(), @@ -77,7 +80,7 @@ void SubsonicService::LazyPopulate(QStandardItem *item) case InternetModel::Type_Service: // TODO: initiate library loading library_backend_->DeleteAll(); - GetIndexes(); + scanner_->Scan(); model()->merged_model()->AddSubModel(item->index(), library_sort_model_); break; @@ -118,20 +121,10 @@ void SubsonicService::Login(const QString &server, const QString &username, cons void SubsonicService::Ping() { - Send(BuildRequestUrl("ping"), SLOT(onPingFinished())); -} - -void SubsonicService::GetIndexes() -{ - Send(BuildRequestUrl("getIndexes"), SLOT(onGetIndexesFinished())); -} - -void SubsonicService::GetMusicDirectory(const QString &id) -{ - ++directory_count_; - QUrl url = BuildRequestUrl("getMusicDirectory"); - url.addQueryItem("id", id); - Send(url, SLOT(onGetMusicDirectoryFinished())); + QNetworkReply* reply = Send(BuildRequestUrl("ping")); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(onPingFinished(QNetworkReply*)), + reply); } QUrl SubsonicService::BuildRequestUrl(const QString &view) @@ -144,12 +137,7 @@ QUrl SubsonicService::BuildRequestUrl(const QString &view) return url; } -QModelIndex SubsonicService::GetCurrentIndex() -{ - return context_item_; -} - -void SubsonicService::Send(const QUrl &url, const char *slot) +QNetworkReply* SubsonicService::Send(const QUrl &url) { QNetworkRequest request(url); // Don't try and check the authenticity of the SSL certificate - it'll almost @@ -158,62 +146,12 @@ void SubsonicService::Send(const QUrl &url, const char *slot) sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(sslconfig); QNetworkReply *reply = network_->get(request); - connect(reply, SIGNAL(finished()), slot); + return reply; } -void SubsonicService::ReadIndex(QXmlStreamReader *reader) +QModelIndex SubsonicService::GetCurrentIndex() { - Q_ASSERT(reader->name() == "index"); - - while (reader->readNextStartElement()) - { - ReadArtist(reader); - } -} - -void SubsonicService::ReadArtist(QXmlStreamReader *reader) -{ - Q_ASSERT(reader->name() == "artist"); - QString id = reader->attributes().value("id").toString(); - GetMusicDirectory(id); - reader->skipCurrentElement(); -} - -void SubsonicService::ReadAlbum(QXmlStreamReader *reader) -{ - Q_ASSERT(reader->name() == "child"); - QString id = reader->attributes().value("id").toString(); - GetMusicDirectory(id); - reader->skipCurrentElement(); -} - -Song SubsonicService::ReadTrack(QXmlStreamReader *reader) -{ - Q_ASSERT(reader->name() == "child"); - - 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_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); - - reader->skipCurrentElement(); - return song; + return context_item_; } void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) @@ -221,9 +159,8 @@ void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) // TODO: library refresh logic? } -void SubsonicService::onPingFinished() +void SubsonicService::onPingFinished(QNetworkReply *reply) { - QNetworkReply *reply = qobject_cast(sender()); reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) @@ -276,88 +213,137 @@ void SubsonicService::onPingFinished() emit LoginStateChanged(login_state_); } -void SubsonicService::onGetIndexesFinished() +void SubsonicService::onSongsDiscovered(SongList songs) { - QNetworkReply *reply = qobject_cast(sender()); - Q_ASSERT(reply); - reply->deleteLater(); - QXmlStreamReader reader(reply); + library_backend_->AddOrUpdateSongs(songs); +} + +const int SubsonicLibraryScanner::kAlbumChunkSize = 500; +const int SubsonicLibraryScanner::kSongListMinChunkSize = 500; +const int SubsonicLibraryScanner::kConcurrentRequests = 8; + +SubsonicLibraryScanner::SubsonicLibraryScanner(SubsonicService* service, QObject* parent) + : QObject(parent), + service_(service) +{ +} + +SubsonicLibraryScanner::~SubsonicLibraryScanner() +{ +} + +void SubsonicLibraryScanner::Scan() +{ + album_queue_.clear(); + songlist_buffer_.clear(); + 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") - { + if (reader.attributes().value("status") != "ok") { // TODO: error handling return; } - directory_count_ = 0; - processed_directory_count_ = 0; - new_songs_ = SongList(); - + int albums_added = 0; reader.readNextStartElement(); - Q_ASSERT(reader.name() == "indexes"); - // TODO: start loading library data - while (reader.readNextStartElement()) - { - if (reader.name() == "index") - { - ReadIndex(&reader); - } - else if (reader.name() == "child" && reader.attributes().value("isVideo") == "false") - { - new_songs_ << ReadTrack(&reader); - } - else - { - reader.skipCurrentElement(); - } + 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 (new_songs_.size() >= kChunkSize || directory_count_ == processed_directory_count_) { - library_backend_->AddOrUpdateSongs(new_songs_); - new_songs_ = SongList(); + // If this reply was non-empty, get the next chunk, otherwise start fetching songs + if (albums_added > 0) { + GetAlbumList(offset + kAlbumChunkSize); + } else { + // Start up the maximum number of concurrent requests + for (int i = 0; i < kConcurrentRequests && !album_queue_.empty(); ++i) { + GetAlbum(album_queue_.dequeue()); + } } } -void SubsonicService::onGetMusicDirectoryFinished() +void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply) { - QNetworkReply *reply = qobject_cast(sender()); - Q_ASSERT(reply); reply->deleteLater(); + QXmlStreamReader reader(reply); - - ++processed_directory_count_; - reader.readNextStartElement(); Q_ASSERT(reader.name() == "subsonic-response"); - if (reader.attributes().value("status") != "ok") - { + if (reader.attributes().value("status") != "ok") { // TODO: error handling return; } reader.readNextStartElement(); - Q_ASSERT(reader.name() == "directory"); - // TODO: add tracks, etc. - while (reader.readNextStartElement()) - { - if (reader.attributes().value("isDir") == "true") - { - ReadAlbum(&reader); - } - else if (reader.attributes().value("isVideo") == "false") - { - new_songs_ << ReadTrack(&reader); - } - else - { - reader.skipCurrentElement(); - } + Q_ASSERT(reader.name() == "album"); + 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_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); + songlist_buffer_ << song; + reader.skipCurrentElement(); } - if (new_songs_.size() >= kChunkSize || directory_count_ == processed_directory_count_) { - library_backend_->AddOrUpdateSongs(new_songs_); - new_songs_ = SongList(); + // If the songlist buffer is big enough, or we're (nearly) done, emit the songlist (see below) + bool should_emit = album_queue_.empty() || songlist_buffer_.size() >= kSongListMinChunkSize; + + // Start the next request + if (!album_queue_.empty()) { + GetAlbum(album_queue_.dequeue()); + } + + if (should_emit) { + emit SongsDiscovered(songlist_buffer_); + songlist_buffer_.clear(); } } + +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); +} diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 7e02fe717..cde2b9c72 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -4,11 +4,15 @@ #include "internetmodel.h" #include "internetservice.h" +#include + class QNetworkAccessManager; class QXmlStreamReader; class QSortFilterProxyModel; +class QNetworkReply; class SubsonicUrlHandler; +class SubsonicLibraryScanner; class SubsonicService : public InternetService { @@ -68,6 +72,8 @@ class SubsonicService : public InternetService void GetMusicDirectory(const QString &id); 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; @@ -77,8 +83,6 @@ class SubsonicService : public InternetService static const char* kSongsTable; static const char* kFtsTable; - static const int kChunkSize; - signals: void LoginStateChanged(SubsonicService::LoginState newstate); @@ -86,17 +90,10 @@ class SubsonicService : public InternetService QModelIndex GetCurrentIndex(); private: - // Convenience function to reduce QNetworkRequest/QNetworkReply/connect boilerplate - void Send(const QUrl &url, const char *slot); - - void ReadIndex(QXmlStreamReader *reader); - void ReadArtist(QXmlStreamReader *reader); - void ReadAlbum(QXmlStreamReader *reader); - Song ReadTrack(QXmlStreamReader *reader); - QModelIndex context_item_; QNetworkAccessManager* network_; SubsonicUrlHandler* url_handler_; + SubsonicLibraryScanner* scanner_; LibraryBackend* library_backend_; LibraryModel* library_model_; @@ -109,15 +106,41 @@ class SubsonicService : public InternetService LoginState login_state_; - int directory_count_; - int processed_directory_count_; - SongList new_songs_; - private slots: void onLoginStateChanged(SubsonicService::LoginState newstate); - void onPingFinished(); - void onGetIndexesFinished(); - void onGetMusicDirectoryFinished(); + void onPingFinished(QNetworkReply* reply); + void onSongsDiscovered(SongList songs); +}; + +class SubsonicLibraryScanner : public QObject { + Q_OBJECT + + public: + SubsonicLibraryScanner(SubsonicService* service, QObject* parent=0); + ~SubsonicLibraryScanner(); + + void Scan(); + + static const int kAlbumChunkSize; + static const int kSongListMinChunkSize; + static const int kConcurrentRequests; + + signals: + void SongsDiscovered(SongList); + + 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_; + QQueue album_queue_; + SongList songlist_buffer_; }; #endif // SUBSONICSERVICE_H From 34553d8238eeaf9d5d14ea89072d114779d1f893 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 17 Jan 2013 20:18:26 +0000 Subject: [PATCH 25/31] Read subsonic albumartist data --- src/internet/subsonicservice.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index eb7fd4cbb..0e9ddde04 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -285,8 +285,12 @@ void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply) 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; @@ -295,6 +299,7 @@ void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply) 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()); From e8ab6ed40aa54331747a4055ccb2912c722e15d7 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 17 Jan 2013 21:01:54 +0000 Subject: [PATCH 26/31] Subsonic: add LibraryFilterWidget and basic context menu --- src/internet/subsonicservice.cpp | 42 ++++++++++++++++++++++++++------ src/internet/subsonicservice.h | 12 ++++++--- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 0e9ddde04..f6813253a 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -7,6 +7,7 @@ #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" @@ -17,6 +18,7 @@ #include #include #include +#include const char* SubsonicService::kServiceName = "Subsonic"; const char* SubsonicService::kSettingsGroup = "Subsonic"; @@ -31,8 +33,11 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) network_(new QNetworkAccessManager(this)), url_handler_(new SubsonicUrlHandler(this, this)), scanner_(new SubsonicLibraryScanner(this, this)), + context_menu_(NULL), + root_(NULL), library_backend_(NULL), library_model_(NULL), + library_filter_(NULL), library_sort_model_(new QSortFilterProxyModel(this)), login_state_(LoginState_OtherError) { @@ -53,6 +58,12 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) 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); @@ -60,6 +71,11 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), SLOT(onLoginStateChanged(SubsonicService::LoginState))); + + context_menu_ = new QMenu; + context_menu_->addActions(GetPlaylistActions()); + context_menu_->addSeparator(); + context_menu_->addMenu(library_filter_->menu()); } SubsonicService::~SubsonicService() @@ -68,9 +84,9 @@ SubsonicService::~SubsonicService() QStandardItem* SubsonicService::CreateRootItem() { - QStandardItem* item = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName); - item->setData(true, InternetModel::Role_CanLazyLoad); - return item; + root_ = new QStandardItem(QIcon(":providers/subsonic.png"), kServiceName); + root_->setData(true, InternetModel::Role_CanLazyLoad); + return root_; } void SubsonicService::LazyPopulate(QStandardItem *item) @@ -89,6 +105,21 @@ void SubsonicService::LazyPopulate(QStandardItem *item) } } +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; @@ -149,11 +180,6 @@ QNetworkReply* SubsonicService::Send(const QUrl &url) return reply; } -QModelIndex SubsonicService::GetCurrentIndex() -{ - return context_item_; -} - void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) { // TODO: library refresh logic? diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index cde2b9c72..9aae89704 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -60,6 +60,8 @@ class SubsonicService : public InternetService QStandardItem* CreateRootItem(); void LazyPopulate(QStandardItem *item); + void ShowContextMenu(const QPoint &global_pos); + QWidget* HeaderWidget() const; void ReloadSettings(); void Login(); @@ -86,17 +88,19 @@ class SubsonicService : public InternetService signals: void LoginStateChanged(SubsonicService::LoginState newstate); - protected: - QModelIndex GetCurrentIndex(); - private: - QModelIndex context_item_; + void EnsureMenuCreated(); + QNetworkAccessManager* network_; SubsonicUrlHandler* url_handler_; SubsonicLibraryScanner* scanner_; + QMenu* context_menu_; + QStandardItem* root_; + LibraryBackend* library_backend_; LibraryModel* library_model_; + LibraryFilterWidget* library_filter_; QSortFilterProxyModel* library_sort_model_; // Configuration From 38f271528af016982e073c0ba725b54d563bd6e2 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Thu, 17 Jan 2013 22:13:57 +0000 Subject: [PATCH 27/31] Improve subsonic library fetching * Use task notification * Update library in one chunk * Use stored library data, add ability to manually reload --- src/internet/subsonicservice.cpp | 80 ++++++++++++++++++++++---------- src/internet/subsonicservice.h | 16 ++++--- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index f6813253a..d7d9ff282 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -11,6 +11,7 @@ #include "core/mergedproxymodel.h" #include "core/database.h" #include "core/closure.h" +#include "core/taskmanager.h" #include #include @@ -33,6 +34,7 @@ SubsonicService::SubsonicService(Application* app, InternetModel *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), @@ -43,8 +45,8 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) { app_->player()->RegisterUrlHandler(url_handler_); - connect(scanner_, SIGNAL(SongsDiscovered(SongList)), - SLOT(onSongsDiscovered(SongList))); + connect(scanner_, SIGNAL(ScanFinished()), + SLOT(ReloadDatabaseFinished())); library_backend_ = new LibraryBackend; library_backend_->moveToThread(app_->database()->thread()); @@ -69,12 +71,17 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) library_sort_model_->setDynamicSortFilter(true); library_sort_model_->sort(0); + connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)), + SLOT(UpdateTotalSongCount(int))); + 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())); + context_menu_->addSeparator(); context_menu_->addMenu(library_filter_->menu()); } @@ -94,9 +101,7 @@ void SubsonicService::LazyPopulate(QStandardItem *item) switch (item->data(InternetModel::Role_Type).toInt()) { case InternetModel::Type_Service: - // TODO: initiate library loading - library_backend_->DeleteAll(); - scanner_->Scan(); + library_model_->Init(); model()->merged_model()->AddSubModel(item->index(), library_sort_model_); break; @@ -180,6 +185,28 @@ QNetworkReply* SubsonicService::Send(const QUrl &url) return reply; } +void SubsonicService::UpdateTotalSongCount(int count) +{ + if (count == 0 && !load_database_task_id_) + ReloadDatabase(); +} + +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()); +} + void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) { // TODO: library refresh logic? @@ -239,19 +266,14 @@ void SubsonicService::onPingFinished(QNetworkReply *reply) emit LoginStateChanged(login_state_); } -void SubsonicService::onSongsDiscovered(SongList songs) -{ - library_backend_->AddOrUpdateSongs(songs); -} - const int SubsonicLibraryScanner::kAlbumChunkSize = 500; -const int SubsonicLibraryScanner::kSongListMinChunkSize = 500; const int SubsonicLibraryScanner::kConcurrentRequests = 8; SubsonicLibraryScanner::SubsonicLibraryScanner(SubsonicService* service, QObject* parent) : QObject(parent), - service_(service) + service_(service), + scanning_(false) { } @@ -261,8 +283,13 @@ SubsonicLibraryScanner::~SubsonicLibraryScanner() void SubsonicLibraryScanner::Scan() { + if (scanning_) + return; + album_queue_.clear(); - songlist_buffer_.clear(); + pending_requests_.clear(); + songs_.clear(); + scanning_ = true; GetAlbumList(0); } @@ -288,11 +315,15 @@ void SubsonicLibraryScanner::onGetAlbumListFinished(QNetworkReply *reply, int of reader.skipCurrentElement(); } - // If this reply was non-empty, get the next chunk, otherwise start fetching songs 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 { - // Start up the maximum number of concurrent requests + // 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()); } @@ -302,6 +333,7 @@ void SubsonicLibraryScanner::onGetAlbumListFinished(QNetworkReply *reply, int of void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply) { reply->deleteLater(); + pending_requests_.remove(reply); QXmlStreamReader reader(reply); reader.readNextStartElement(); @@ -339,21 +371,18 @@ void SubsonicLibraryScanner::onGetAlbumFinished(QNetworkReply *reply) song.set_directory_id(0); song.set_mtime(0); song.set_ctime(0); - songlist_buffer_ << song; + songs_ << song; reader.skipCurrentElement(); } - // If the songlist buffer is big enough, or we're (nearly) done, emit the songlist (see below) - bool should_emit = album_queue_.empty() || songlist_buffer_.size() >= kSongListMinChunkSize; - - // Start the next request - if (!album_queue_.empty()) { + // Start the next request if albums remain + if (!album_queue_.empty()) GetAlbum(album_queue_.dequeue()); - } - if (should_emit) { - emit SongsDiscovered(songlist_buffer_); - songlist_buffer_.clear(); + // If this was the last response, we're done! + if (album_queue_.empty() && pending_requests_.empty()) { + scanning_ = false; + emit ScanFinished(); } } @@ -377,4 +406,5 @@ void SubsonicLibraryScanner::GetAlbum(QString id) 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 index 9aae89704..72517cd2e 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -70,8 +70,6 @@ class SubsonicService : public InternetService // Subsonic API methods void Ping(); - void GetIndexes(); - void GetMusicDirectory(const QString &id); QUrl BuildRequestUrl(const QString &view); // Convenience function to reduce QNetworkRequest/QSslConfiguration boilerplate @@ -93,7 +91,9 @@ class SubsonicService : public InternetService QNetworkAccessManager* network_; SubsonicUrlHandler* url_handler_; + SubsonicLibraryScanner* scanner_; + int load_database_task_id_; QMenu* context_menu_; QStandardItem* root_; @@ -111,9 +111,11 @@ class SubsonicService : public InternetService LoginState login_state_; private slots: + void UpdateTotalSongCount(int count); + void ReloadDatabase(); + void ReloadDatabaseFinished(); void onLoginStateChanged(SubsonicService::LoginState newstate); void onPingFinished(QNetworkReply* reply); - void onSongsDiscovered(SongList songs); }; class SubsonicLibraryScanner : public QObject { @@ -124,13 +126,13 @@ class SubsonicLibraryScanner : public QObject { ~SubsonicLibraryScanner(); void Scan(); + const SongList& GetSongs() const { return songs_; } static const int kAlbumChunkSize; - static const int kSongListMinChunkSize; static const int kConcurrentRequests; signals: - void SongsDiscovered(SongList); + void ScanFinished(); private slots: // Step 1: use getAlbumList2 type=alphabeticalByName to list all albums @@ -143,8 +145,10 @@ class SubsonicLibraryScanner : public QObject { void GetAlbum(QString id); SubsonicService* service_; + bool scanning_; QQueue album_queue_; - SongList songlist_buffer_; + QSet pending_requests_; + SongList songs_; }; #endif // SUBSONICSERVICE_H From 2d68315c22e78f7f64c622e2ba49d2803f0ee568 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Fri, 18 Jan 2013 10:35:47 +0000 Subject: [PATCH 28/31] Fix Subsonic library not being sorted after refresh --- src/internet/subsonicservice.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index d7d9ff282..df0dc7b46 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -205,6 +205,7 @@ void SubsonicService::ReloadDatabaseFinished() library_backend_->DeleteAll(); library_backend_->AddOrUpdateSongs(scanner_->GetSongs()); + library_model_->Reset(); } void SubsonicService::onLoginStateChanged(SubsonicService::LoginState newstate) From a7fe1b693bcc3add92b3292341b2bb1d3a1c3912 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Sun, 20 Jan 2013 23:24:14 +0000 Subject: [PATCH 29/31] Eliminate race condition for subsonic library TotalSongCountUpdated --- src/internet/subsonicservice.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index df0dc7b46..7cac1c066 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -55,6 +55,8 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) 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); @@ -71,9 +73,6 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) library_sort_model_->setDynamicSortFilter(true); library_sort_model_->sort(0); - connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)), - SLOT(UpdateTotalSongCount(int))); - connect(this, SIGNAL(LoginStateChanged(SubsonicService::LoginState)), SLOT(onLoginStateChanged(SubsonicService::LoginState))); From d424ed93a49990e47553237e339fd84cb574e1c0 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Sun, 20 Jan 2013 23:25:54 +0000 Subject: [PATCH 30/31] Don't fetch subsonic library until service is expanded --- src/internet/subsonicservice.cpp | 7 +++++-- src/internet/subsonicservice.h | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 7cac1c066..992d45fe4 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -41,6 +41,7 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) 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_); @@ -101,6 +102,9 @@ void SubsonicService::LazyPopulate(QStandardItem *item) { case InternetModel::Type_Service: library_model_->Init(); + if (total_song_count_ == 0 && !load_database_task_id_) { + ReloadDatabase(); + } model()->merged_model()->AddSubModel(item->index(), library_sort_model_); break; @@ -186,8 +190,7 @@ QNetworkReply* SubsonicService::Send(const QUrl &url) void SubsonicService::UpdateTotalSongCount(int count) { - if (count == 0 && !load_database_task_id_) - ReloadDatabase(); + total_song_count_ = count; } void SubsonicService::ReloadDatabase() diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 72517cd2e..389da6904 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -102,6 +102,7 @@ class SubsonicService : public InternetService LibraryModel* library_model_; LibraryFilterWidget* library_filter_; QSortFilterProxyModel* library_sort_model_; + int total_song_count_; // Configuration QString server_; From a5f6356be4fc7765cecf6ef6a1013a65c8573b03 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Mon, 21 Jan 2013 22:13:48 +0000 Subject: [PATCH 31/31] Subsonic global search provider and some extra UI polish --- src/internet/subsonicservice.cpp | 24 ++++++++++++++++++++++-- src/internet/subsonicservice.h | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/internet/subsonicservice.cpp b/src/internet/subsonicservice.cpp index 992d45fe4..f188cebd5 100644 --- a/src/internet/subsonicservice.cpp +++ b/src/internet/subsonicservice.cpp @@ -12,6 +12,8 @@ #include "core/database.h" #include "core/closure.h" #include "core/taskmanager.h" +#include "globalsearch/globalsearch.h" +#include "globalsearch/librarysearchprovider.h" #include #include @@ -23,7 +25,7 @@ const char* SubsonicService::kServiceName = "Subsonic"; const char* SubsonicService::kSettingsGroup = "Subsonic"; -const char* SubsonicService::kApiVersion = "1.7.0"; +const char* SubsonicService::kApiVersion = "1.8.0"; const char* SubsonicService::kApiClientName = "Clementine"; const char* SubsonicService::kSongsTable = "subsonic_songs"; @@ -81,8 +83,19 @@ SubsonicService::SubsonicService(Application* app, InternetModel *parent) 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() @@ -102,7 +115,9 @@ void SubsonicService::LazyPopulate(QStandardItem *item) { case InternetModel::Type_Service: library_model_->Init(); - if (total_song_count_ == 0 && !load_database_task_id_) { + 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_); @@ -269,6 +284,11 @@ void SubsonicService::onPingFinished(QNetworkReply *reply) emit LoginStateChanged(login_state_); } +void SubsonicService::ShowConfig() +{ + app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Subsonic); +} + const int SubsonicLibraryScanner::kAlbumChunkSize = 500; const int SubsonicLibraryScanner::kConcurrentRequests = 8; diff --git a/src/internet/subsonicservice.h b/src/internet/subsonicservice.h index 389da6904..e6b7342ae 100644 --- a/src/internet/subsonicservice.h +++ b/src/internet/subsonicservice.h @@ -117,6 +117,8 @@ class SubsonicService : public InternetService void ReloadDatabaseFinished(); void onLoginStateChanged(SubsonicService::LoginState newstate); void onPingFinished(QNetworkReply* reply); + + void ShowConfig(); }; class SubsonicLibraryScanner : public QObject {