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..c655dc061 --- /dev/null +++ b/src/internet/subsonic/subsonicdynamicplaylist.cpp @@ -0,0 +1,241 @@ +/* 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 "subsonicservice.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 "core/waitforsignal.h" +#include "internet/core/internetplaylistitem.h" + +#include + +// 500 limit per subsonic api +const int SubsonicDynamicPlaylist::kMaxCount = 500; +const int SubsonicDynamicPlaylist::kDefaultCount = 10; +const int SubsonicDynamicPlaylist::kDefaultOffset = 0; + +SubsonicDynamicPlaylist::SubsonicDynamicPlaylist() + : stat_(QueryStat_Newest), offset_(kDefaultOffset) {} + +SubsonicDynamicPlaylist::SubsonicDynamicPlaylist(const QString& name, + QueryStat stat) + : stat_(stat), offset_(kDefaultOffset) { + 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(kDefaultCount); +} + +PlaylistItemList SubsonicDynamicPlaylist::GenerateMore(int count) { + SubsonicService* service = InternetModel::Service(); + const int task_id = + service->app_->task_manager()->StartTask(tr("Fetching Playlist Items")); + + BOOST_SCOPE_EXIT((service)(task_id)) { + // stop task when we're done + service->app_->task_manager()->SetTaskFinished(task_id); + } + BOOST_SCOPE_EXIT_END + + QUrl url = service->BuildRequestUrl("GetAlbumList"); + QNetworkAccessManager network; + + if (count > kMaxCount) count = kMaxCount; + + url.addQueryItem("type", GetTypeString()); + url.addQueryItem("size", QString::number(count)); + url.addQueryItem("offset", QString::number(offset_)); + + PlaylistItemList items; + + QNetworkReply* reply = Send(network, url, service->usesslv3_); + WaitForSignal(reply, SIGNAL(finished())); + + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qLog(Warning) << "HTTP error returned from Subsonic:" + << reply->errorString() << ", url:" << url.toString(); + return items; // empty + } + + QXmlStreamReader reader(reply); + reader.readNextStartElement(); + if (reader.name() != "subsonic-response") { + qLog(Warning) << "Not a subsonic-response, aboring playlist fetch"; + 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: " << error + << " Message: " + << reader.attributes().value("message").toString(); + } + + reader.readNextStartElement(); + if (reader.name() != "albumList") { + qLog(Warning) << "albumList tag expected. Aboring playlist fetch"; + return items; + } + + while (reader.readNextStartElement()) { + if (reader.name() != "album") { + qLog(Warning) << "album tag expected. Aboring playlist fetch"; + return items; + } + + qLog(Debug) << "Getting album: " + << reader.attributes().value("album").toString(); + GetAlbum(service, items, reader.attributes().value("id").toString(), + network, service->usesslv3_); + reader.skipCurrentElement(); + } + offset_ += count; + 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); + WaitForSignal(reply, SIGNAL(finished())); + reply->deleteLater(); + + 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..31231f3b3 --- /dev/null +++ b/src/internet/subsonic/subsonicdynamicplaylist.h @@ -0,0 +1,96 @@ +/* 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); + + static const int kMaxCount; + static const int kDefaultCount; + static const int kDefaultOffset; + + 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); + QString GetTypeString() const { + switch (stat_) { + case QueryStat::QueryStat_Newest: + return "newest"; + case QueryStat::QueryStat_Highest: + return "highest"; + case QueryStat::QueryStat_Frequent: + return "frequent"; + case QueryStat::QueryStat_Recent: + return "recent"; + case QueryStat::QueryStat_Starred: + return "starred"; + case QueryStat::QueryStat_Random: + return "random"; + default: + return "newest"; + } + } + + private: + QueryStat stat_; + int offset_; + SubsonicService* service_; +}; + +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();