diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f1046911b..0cb379451 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -197,6 +197,7 @@ set(SOURCES internet/subsonic/subsonicservice.cpp internet/subsonic/subsonicsettingspage.cpp internet/subsonic/subsonicurlhandler.cpp + internet/subsonic/subsonicdynamicplaylist.cpp library/groupbydialog.cpp library/library.cpp @@ -499,6 +500,7 @@ set(HEADERS internet/subsonic/subsonicservice.h internet/subsonic/subsonicsettingspage.h internet/subsonic/subsonicurlhandler.h + internet/subsonic/subsonicdynamicplaylist.h library/groupbydialog.h library/library.h diff --git a/src/internet/subsonic/subsonicdynamicplaylist.cpp b/src/internet/subsonic/subsonicdynamicplaylist.cpp new file mode 100644 index 000000000..e5691acb3 --- /dev/null +++ b/src/internet/subsonic/subsonicdynamicplaylist.cpp @@ -0,0 +1,259 @@ +/* This file is part of Clementine. + Copyright 2015, Nick Lanham + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "subsonicdynamicplaylist.h" + +#include +#include +#include +#include + +#include "core/application.h" +#include "core/logging.h" +#include "core/network.h" +#include "core/taskmanager.h" +#include "core/timeconstants.h" +#include "internet/core/internetplaylistitem.h" + +SubsonicDynamicPlaylist::SubsonicDynamicPlaylist() + : stat_(QueryStat_Newest), + offset_(0) {} + +SubsonicDynamicPlaylist::SubsonicDynamicPlaylist(const QString& name, + QueryStat stat) + : stat_(stat), + offset_(0) { + set_name(name); +} + +void SubsonicDynamicPlaylist::Load(const QByteArray& data) { + QDataStream s(data); + s >> *this; +} + +void SubsonicDynamicPlaylist::Load(QueryStat stat) { + stat_ = stat; +} + +QByteArray SubsonicDynamicPlaylist::Save() const { + QByteArray ret; + QDataStream s(&ret, QIODevice::WriteOnly); + s << *this; + return ret; +} + +// copied from SubsonicService +QNetworkReply* SubsonicDynamicPlaylist::Send(QNetworkAccessManager& network, const QUrl& url, const bool usesslv3) { + 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); + if (usesslv3) { + sslconfig.setProtocol(QSsl::SslV3); + } + request.setSslConfiguration(sslconfig); + QNetworkReply* reply = network.get(request); + return reply; +} + +PlaylistItemList SubsonicDynamicPlaylist::Generate() { return GenerateMore(10); } + +PlaylistItemList SubsonicDynamicPlaylist::GenerateMore(int count) { + SubsonicService* service = InternetModel::Service(); + int task_id = service->app_->task_manager()->StartTask(tr("Fetching Playlist Items")); + QUrl url = service->BuildRequestUrl("GetAlbumList"); + QNetworkAccessManager network; + + switch (stat_) { + case QueryStat::QueryStat_Newest: + url.addQueryItem("type","newest"); + break; + case QueryStat::QueryStat_Highest: + url.addQueryItem("type","highest"); + break; + case QueryStat::QueryStat_Frequent: + url.addQueryItem("type","frequent"); + break; + case QueryStat::QueryStat_Recent: + url.addQueryItem("type","recent"); + break; + case QueryStat::QueryStat_Starred: + url.addQueryItem("type","starred"); + break; + case QueryStat::QueryStat_Random: + url.addQueryItem("type","random"); + break; + } + + if (count > 500) count = 500; // 500 limit per subsonic api + if (count != 10) { // 10 is default + url.addQueryItem("size",QString::number(count)); + } + + if (offset_ != 0) { // 0 is default + url.addQueryItem("offset",QString::number(offset_)); + } + + PlaylistItemList items; + + QNetworkReply* reply = Send(network,url,service->usesslv3_); + + // wait for reply + { + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + } + + if (reply->error() != QNetworkReply::NoError) { + qLog(Warning) << "HTTP error returned from Subsonic:" << reply->errorString() + << ", url:" << url.toString(); + service->app_->task_manager()->SetTaskFinished(task_id); + return items; // empty + } + + reply->deleteLater(); + QXmlStreamReader reader(reply); + reader.readNextStartElement(); + if (reader.name() != "subsonic-response") { + qLog(Warning) << "Not a subsonic-response, aboring playlist fetch"; + service->app_->task_manager()->SetTaskFinished(task_id); + return items; + } + + if (reader.attributes().value("status") != "ok") { + reader.readNextStartElement(); + int error = reader.attributes().value("code").toString().toInt(); + qLog(Warning) << "An error occured fetching data. Code: "<app_->task_manager()->SetTaskFinished(task_id); + return items; + } + + while (reader.readNextStartElement()) { + if (reader.name() != "album") { + qLog(Warning) << "album tag expected. Aboring playlist fetch"; + service->app_->task_manager()->SetTaskFinished(task_id); + return items; + } + + qLog(Debug) << "Getting album: "<usesslv3_); + + reader.skipCurrentElement(); + } + offset_+=count; + service->app_->task_manager()->SetTaskFinished(task_id); + return items; +} + +void SubsonicDynamicPlaylist::GetAlbum(SubsonicService* service, PlaylistItemList& list, QString id, QNetworkAccessManager& network, const bool usesslv3) { + QUrl url = service->BuildRequestUrl("getAlbum"); + url.addQueryItem("id", id); + QNetworkReply* reply = Send(network, url, usesslv3); + + { // wait for reply + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + } + + if (reply->error() != QNetworkReply::NoError) { + qLog(Warning) << "HTTP error returned from Subsonic:" << reply->errorString() + << ", url:" << url.toString(); + return; + } + + QXmlStreamReader reader(reply); + reader.readNextStartElement(); + + if (reader.name() != "subsonic-response") { + qLog(Warning) << "Not a subsonic-response. Aborting playlist fetch."; + return; + } + + if (reader.attributes().value("status") != "ok") { + qLog(Warning) << "Status not okay. Aborting playlist fetch."; + return; + } + + // Read album information + reader.readNextStartElement(); + if (reader.name() != "album") { + qLog(Warning) << "album tag expected. Aborting playlist fetch."; + return; + } + + QString album_artist = reader.attributes().value("artist").toString(); + + // Read song information + while (reader.readNextStartElement()) { + if (reader.name() != "song") { + qLog(Warning) << "song tag expected. Aborting playlist fetch."; + return; + } + + 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_disc(reader.attributes().value("discNumber").toString().toInt()); + song.set_artist(reader.attributes().value("artist").toString()); + song.set_albumartist(album_artist); + song.set_bitrate(reader.attributes().value("bitRate").toString().toInt()); + song.set_year(reader.attributes().value("year").toString().toInt()); + song.set_genre(reader.attributes().value("genre").toString()); + qint64 length = reader.attributes().value("duration").toString().toInt(); + length *= kNsecPerSec; + 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()); + QFileInfo fi(reader.attributes().value("path").toString()); + song.set_basefilename(fi.fileName()); + // We need to set these to satisfy the database constraints + song.set_directory_id(0); + song.set_mtime(0); + song.set_ctime(0); + + + list << std::shared_ptr( + new InternetPlaylistItem(service,song)); + + reader.skipCurrentElement(); + } +} + +QDataStream& operator<<(QDataStream& s, const SubsonicDynamicPlaylist& p) { + s << quint8(p.stat_); + return s; +} + +QDataStream& operator>>(QDataStream& s, SubsonicDynamicPlaylist& p) { + quint8 stat; + s >> stat; + p.stat_ = SubsonicDynamicPlaylist::QueryStat(stat); + return s; +} + + diff --git a/src/internet/subsonic/subsonicdynamicplaylist.h b/src/internet/subsonic/subsonicdynamicplaylist.h new file mode 100644 index 000000000..bb7409076 --- /dev/null +++ b/src/internet/subsonic/subsonicdynamicplaylist.h @@ -0,0 +1,75 @@ +/* This file is part of Clementine. + Copyright 2015, Nick Lanham + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef INTERNET_SUBSONIC_SUBSONICDYNAMICPLAYLIST_H_ +#define INTERNET_SUBSONIC_SUBSONICDYNAMICPLAYLIST_H_ + +#include "smartplaylists/generator.h" + +#include + +class SubsonicService; + +class SubsonicDynamicPlaylist : public smart_playlists::Generator { + Q_OBJECT + friend QDataStream& operator<<(QDataStream& s, + const SubsonicDynamicPlaylist& p); + friend QDataStream& operator>>(QDataStream& s, SubsonicDynamicPlaylist& p); + + public: + + // things that subsonic can return to us, persisted so only add at end + enum QueryStat { + QueryStat_Newest = 0, + QueryStat_Highest = 1, + QueryStat_Frequent = 2, + QueryStat_Recent = 3, + QueryStat_Starred = 4, + QueryStat_Random =5, + }; + + SubsonicDynamicPlaylist(); + SubsonicDynamicPlaylist(const QString& name, QueryStat stat); + + QString type() const { return "Subsonic"; } + + void Load(const QByteArray& data); + void Load(QueryStat stat); + QByteArray Save() const; + + PlaylistItemList Generate(); + + bool is_dynamic() const { return true; } + PlaylistItemList GenerateMore(int count); + + private: + void GetAlbum(SubsonicService* service, PlaylistItemList& list, QString id, QNetworkAccessManager& network, const bool usesslv3); + // need our own one since we run in a different thread from service + QNetworkReply* Send(QNetworkAccessManager& network, const QUrl& url, const bool usesslv3); + + private: + QueryStat stat_; + int offset_; + SubsonicService* service_; +}; + +#include "subsonicservice.h" + +QDataStream& operator<<(QDataStream& s, const SubsonicDynamicPlaylist& p); +QDataStream& operator>>(QDataStream& s, SubsonicDynamicPlaylist& p); + +#endif // INTERNET_SUBSONIC_SUBSONICDYNAMICPLAYLIST_H_ diff --git a/src/internet/subsonic/subsonicservice.cpp b/src/internet/subsonic/subsonicservice.cpp index 4d1b73fe1..2cd454f12 100644 --- a/src/internet/subsonic/subsonicservice.cpp +++ b/src/internet/subsonic/subsonicservice.cpp @@ -43,8 +43,11 @@ #include "globalsearch/librarysearchprovider.h" #include "internet/core/internetmodel.h" #include "internet/subsonic/subsonicurlhandler.h" +#include "internet/subsonic/subsonicdynamicplaylist.h" #include "library/librarybackend.h" #include "library/libraryfilterwidget.h" +#include "smartplaylists/generator.h" +#include "smartplaylists/querygenerator.h" #include "ui/iconloader.h" const char* SubsonicService::kServiceName = "Subsonic"; @@ -83,9 +86,34 @@ SubsonicService::SubsonicService(Application* app, InternetModel* parent) connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(UpdateTotalSongCount(int))); + using smart_playlists::Generator; + using smart_playlists::GeneratorPtr; + library_model_ = new LibraryModel(library_backend_, app_, this); library_model_->set_show_various_artists(false); - library_model_->set_show_smart_playlists(false); + library_model_->set_show_smart_playlists(true); + library_model_->set_default_smart_playlists( + LibraryModel::DefaultGenerators() + << (LibraryModel::GeneratorList() + << GeneratorPtr(new SubsonicDynamicPlaylist( + tr("Newest"), + SubsonicDynamicPlaylist::QueryStat_Newest)) + << GeneratorPtr(new SubsonicDynamicPlaylist( + tr("Random"), + SubsonicDynamicPlaylist::QueryStat_Random)) + << GeneratorPtr(new SubsonicDynamicPlaylist( + tr("Frequently Played"), + SubsonicDynamicPlaylist::QueryStat_Frequent)) + << GeneratorPtr(new SubsonicDynamicPlaylist( + tr("Top Rated"), + SubsonicDynamicPlaylist::QueryStat_Highest)) + << GeneratorPtr(new SubsonicDynamicPlaylist( + tr("Recently Played"), + SubsonicDynamicPlaylist::QueryStat_Recent)) + << GeneratorPtr(new SubsonicDynamicPlaylist( + tr("Starred"), + SubsonicDynamicPlaylist::QueryStat_Starred)) + )); library_filter_ = new LibraryFilterWidget(0); library_filter_->SetSettingsGroup(kSettingsGroup); diff --git a/src/internet/subsonic/subsonicservice.h b/src/internet/subsonic/subsonicservice.h index a9df65c68..6ce80b3b5 100644 --- a/src/internet/subsonic/subsonicservice.h +++ b/src/internet/subsonic/subsonicservice.h @@ -26,6 +26,7 @@ #include "internet/core/internetmodel.h" #include "internet/core/internetservice.h" +#include "internet/subsonic/subsonicdynamicplaylist.h" class QNetworkAccessManager; class QNetworkReply; @@ -109,6 +110,8 @@ class SubsonicService : public InternetService { // boilerplate. QNetworkReply* Send(const QUrl& url); + friend PlaylistItemList SubsonicDynamicPlaylist::GenerateMore(int); + static const char* kServiceName; static const char* kSettingsGroup; static const char* kApiVersion; diff --git a/src/smartplaylists/generator.cpp b/src/smartplaylists/generator.cpp index 961e9c4f9..c3c7c4b75 100644 --- a/src/smartplaylists/generator.cpp +++ b/src/smartplaylists/generator.cpp @@ -19,6 +19,7 @@ #include "querygenerator.h" #include "core/logging.h" #include "internet/jamendo/jamendodynamicplaylist.h" +#include "internet/subsonic/subsonicdynamicplaylist.h" #include @@ -35,6 +36,9 @@ GeneratorPtr Generator::Create(const QString& type) { return GeneratorPtr(new QueryGenerator); else if (type == "Jamendo") return GeneratorPtr(new JamendoDynamicPlaylist); + else if (type == "Subsonic") { + return GeneratorPtr(new SubsonicDynamicPlaylist); + } qLog(Warning) << "Invalid playlist generator type:" << type; return GeneratorPtr();