diff --git a/approot/artists.xml b/approot/artists.xml index c5c70050..23425af9 100644 --- a/approot/artists.xml +++ b/approot/artists.xml @@ -7,6 +7,12 @@

${tr:Lms.Explore.most-played-artists}

${most-played-artists} + + + ${recently-added-artists} + @@ -20,7 +26,7 @@ ${show-more} - + ${name} diff --git a/approot/messages.xml b/approot/messages.xml index 5312e568..e50b99e3 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -58,6 +58,8 @@ Artists Most played artists Most played releases +Recently added artists +Recently added releases Albums Search... Show more diff --git a/approot/releases.xml b/approot/releases.xml index a77d74c6..92cf00ce 100644 --- a/approot/releases.xml +++ b/approot/releases.xml @@ -7,6 +7,12 @@

${tr:Lms.Explore.most-played-releases}

${most-played-releases} + + + ${recently-added-releases} + @@ -28,7 +34,7 @@
- + ${cover} ${release-name} ${}${artist-name}${} diff --git a/src/database/Artist.cpp b/src/database/Artist.cpp index 2da5adbf..194b8581 100644 --- a/src/database/Artist.cpp +++ b/src/database/Artist.cpp @@ -18,6 +18,8 @@ */ #include "Artist.hpp" +#include + #include "utils/Logger.hpp" #include "Cluster.hpp" @@ -140,6 +142,18 @@ Artist::getByFilter(Wt::Dbo::Session& session, return res; } +std::vector +Artist::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit) +{ + Wt::Dbo::collection res = session.query("SELECT a from artist a INNER JOIN track t ON a.id = t.artist_id") + .where("t.file_added > ?").bind(after) + .groupBy("a.id") + .orderBy("t.file_added DESC") + .limit(limit); + + return std::vector(res.begin(), res.end()); +} + std::vector> Artist::getReleases(const std::set& clusterIds) const { diff --git a/src/database/Artist.hpp b/src/database/Artist.hpp index 1c814a0d..2ae1acda 100644 --- a/src/database/Artist.hpp +++ b/src/database/Artist.hpp @@ -22,6 +22,7 @@ #include #include +#include #include #include "Types.hpp" @@ -56,6 +57,7 @@ class Artist : public Wt::Dbo::Dbo static std::vector getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1); static std::vector getAllOrphans(Wt::Dbo::Session& session); // No track related + static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1); // Accessors std::string getName(void) const { return _name; } diff --git a/src/database/Release.cpp b/src/database/Release.cpp index cc49954f..7cabd1d8 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -74,6 +74,18 @@ Release::getAllOrphans(Wt::Dbo::Session& session) return std::vector(res.begin(), res.end()); } +std::vector +Release::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit) +{ + Wt::Dbo::collection res = session.query("SELECT r from release r INNER JOIN track t ON r.id = t.release_id") + .where("t.file_added > ?").bind(after) + .groupBy("r.id") + .orderBy("t.file_added DESC") + .limit(limit); + + return std::vector(res.begin(), res.end()); +} + static Wt::Dbo::Query getQuery(Wt::Dbo::Session& session, diff --git a/src/database/Release.hpp b/src/database/Release.hpp index 32cd1f10..1122314a 100644 --- a/src/database/Release.hpp +++ b/src/database/Release.hpp @@ -21,7 +21,7 @@ #include -#include +#include #include "Types.hpp" @@ -49,6 +49,7 @@ class Release : public Wt::Dbo::Dbo static pointer getById(Wt::Dbo::Session& session, IdType id); static std::vector getAllOrphans(Wt::Dbo::Session& session); // no track related static std::vector getAll(Wt::Dbo::Session& session, int offset, int size); + static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1); static std::vector getByFilter(Wt::Dbo::Session& session, const std::set& clusters, // at least one track that belongs to these clusters diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index fb25b9cb..90df1480 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -263,6 +263,9 @@ LmsApplication::handleAuthEvent(void) LMS_LOG(UI, INFO) << "User '" << LmsApp->getCurrentAuthUser().identity(Wt::Auth::Identity::LoginName) << "' logged in from '" << Wt::WApplication::instance()->environment().clientAddress() << "', user agent = " << Wt::WApplication::instance()->environment().userAgent() << ", session = " << Wt::WApplication::instance()->sessionId(); + // Since we plug into the Media Scanner events, we need to enable updates + enableUpdates(true); + { Wt::Dbo::Transaction transaction (LmsApp->getDboSession()); _isAdmin = LmsApp->getCurrentUser()->isAdmin(); @@ -387,7 +390,6 @@ LmsApplication::handleAuthEvent(void) // Events from MediaScanner if (_isAdmin) { - enableUpdates(true); std::string sessionId = this->sessionId(); _scanner.scanComplete().connect(std::bind([=] (Scanner::MediaScanner::Stats stats) { diff --git a/src/ui/explore/ArtistsView.cpp b/src/ui/explore/ArtistsView.cpp index d7e32bf0..26d89f67 100644 --- a/src/ui/explore/ArtistsView.cpp +++ b/src/ui/explore/ArtistsView.cpp @@ -20,8 +20,10 @@ #include "ArtistsView.hpp" #include -#include #include +#include +#include +#include #include "database/Artist.hpp" #include "database/Setting.hpp" @@ -35,6 +37,22 @@ using namespace Database; +namespace { + +using namespace UserInterface; + +void addCompactEntries(Wt::WContainerWidget *container, const std::vector& artists) +{ + for (auto artist : artists) + { + Wt::WTemplate* entry = container->addNew(Wt::WString::tr("Lms.Explore.Artists.template.compact-entry")); + + entry->bindWidget("name", LmsApplication::createArtistAnchor(artist)); + } +} + +} + namespace UserInterface { Artists::Artists(Filters* filters) @@ -47,8 +65,9 @@ Artists::Artists(Filters* filters) _search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder")); _search->textInput().connect(this, &Artists::refresh); - _artistsContainer = bindNew("artists"); - _mostPlayedArtistsContainer = bindNew("most-played-artists"); + _container = bindNew("artists"); + _mostPlayedContainer = bindNew("most-played-artists"); + _recentlyAddedContainer = bindNew("recently-added-artists"); _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.template.show-more")); _showMore->addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -60,30 +79,50 @@ Artists::Artists(Filters* filters) refresh(); refreshMostPlayed(); + refreshRecentlyAdded(); filters->updated().connect(this, &Artists::refresh); + + std::string sessionId = LmsApp->sessionId(); + LmsApp->getMediaScanner().scanComplete().connect([=] (Scanner::MediaScanner::Stats stats) + { + if (stats.nbChanges() > 0) + { + Wt::WServer::instance()->post(sessionId, [=] + { + refreshRecentlyAdded(); + LmsApp->triggerUpdate(); + }); + } + }); +} + +void +Artists::refreshRecentlyAdded() +{ + auto after = Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-1); + + Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); + auto artists = Artist::getLastAdded(LmsApp->getDboSession(), after, 5); + + _recentlyAddedContainer->clear(); + addCompactEntries(_recentlyAddedContainer, artists); } void Artists::refreshMostPlayed() { - _mostPlayedArtistsContainer->clear(); - Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); auto artists = TrackStats::getMostPlayedArtists(LmsApp->getDboSession(), LmsApp->getCurrentUser(), 5); - for (auto artist : artists) - { - Wt::WTemplate* entry = _mostPlayedArtistsContainer->addNew(Wt::WString::tr("Lms.Explore.Artists.template.most-played-entry")); - - entry->bindWidget("name", LmsApplication::createArtistAnchor(artist)); - } + _mostPlayedContainer->clear(); + addCompactEntries(_mostPlayedContainer, artists); } void Artists::refresh() { - _artistsContainer->clear(); + _container->clear(); addSome(); } @@ -100,11 +139,11 @@ Artists::addSome() auto artists = Artist::getByFilter(LmsApp->getDboSession(), clusterIds, searchKeywords, - _artistsContainer->count(), 20, moreResults); + _container->count(), 20, moreResults); for (auto artist : artists) { - Wt::WTemplate* entry = _artistsContainer->addNew(Wt::WString::tr("Lms.Explore.Artists.template.entry")); + Wt::WTemplate* entry = _container->addNew(Wt::WString::tr("Lms.Explore.Artists.template.entry")); entry->bindInt("nb-release", artist->getReleases(clusterIds).size()); entry->bindWidget("name", LmsApplication::createArtistAnchor(artist)); diff --git a/src/ui/explore/ArtistsView.hpp b/src/ui/explore/ArtistsView.hpp index 78c5b019..3a6f6974 100644 --- a/src/ui/explore/ArtistsView.hpp +++ b/src/ui/explore/ArtistsView.hpp @@ -39,6 +39,7 @@ class Artists : public Wt::WTemplate Wt::Signal artistPlay; private: + void refreshRecentlyAdded(); void refreshMostPlayed(); void refresh(); void addSome(); @@ -46,8 +47,9 @@ class Artists : public Wt::WTemplate Filters* _filters; Wt::WTemplate* _showMore; Wt::WLineEdit* _search; - Wt::WContainerWidget* _artistsContainer; - Wt::WContainerWidget* _mostPlayedArtistsContainer; + Wt::WContainerWidget* _container; + Wt::WContainerWidget* _mostPlayedContainer; + Wt::WContainerWidget* _recentlyAddedContainer; }; } // namespace UserInterface diff --git a/src/ui/explore/ReleasesView.cpp b/src/ui/explore/ReleasesView.cpp index 3220cefd..bdbecaf4 100644 --- a/src/ui/explore/ReleasesView.cpp +++ b/src/ui/explore/ReleasesView.cpp @@ -19,9 +19,10 @@ #include "ReleasesView.hpp" -#include +#include #include #include +#include #include #include @@ -38,46 +39,15 @@ using namespace Database; -namespace UserInterface { +namespace { -Releases::Releases(Filters* filters) -: Wt::WTemplate(Wt::WString::tr("Lms.Explore.Releases.template")), -_filters(filters) +using namespace UserInterface; + +void addCompactEntries(Wt::WContainerWidget* container, const std::vector& releases) { - addFunction("tr", &Wt::WTemplate::Functions::tr); - - _search = bindNew("search"); - _search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder")); - _search->textInput().connect(this, &Releases::refresh); - - _releasesContainer = bindNew("releases"); - _mostPlayedReleasesContainer = bindNew("most-played-releases"); - - _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); - _showMore->addFunction("tr", &Wt::WTemplate::Functions::tr); - _showMore->clicked().connect(std::bind([=] - { - addSome(); - })); - - refreshMostPlayed(); - refresh(); - - filters->updated().connect(this, &Releases::refresh); -} - -void -Releases::refreshMostPlayed() -{ - _mostPlayedReleasesContainer->clear(); - - Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); - - auto releases = TrackStats::getMostPlayedReleases(LmsApp->getDboSession(), LmsApp->getCurrentUser(), 5); - for (auto release : releases) { - Wt::WTemplate* entry = _mostPlayedReleasesContainer->addNew(Wt::WString::tr("Lms.Explore.Releases.template.most-played-entry")); + Wt::WTemplate* entry = container->addNew(Wt::WString::tr("Lms.Explore.Releases.template.compact-entry")); entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release)); @@ -100,10 +70,80 @@ Releases::refreshMostPlayed() } } +} // namespace + +namespace UserInterface { + +Releases::Releases(Filters* filters) +: Wt::WTemplate(Wt::WString::tr("Lms.Explore.Releases.template")), +_filters(filters) +{ + addFunction("tr", &Wt::WTemplate::Functions::tr); + + _search = bindNew("search"); + _search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder")); + _search->textInput().connect(this, &Releases::refresh); + + _container = bindNew("releases"); + _mostPlayedContainer = bindNew("most-played-releases"); + _recentlyAddedContainer = bindNew("recently-added-releases"); + + _showMore = bindNew("show-more", Wt::WString::tr("Lms.Explore.show-more")); + _showMore->addFunction("tr", &Wt::WTemplate::Functions::tr); + _showMore->clicked().connect(std::bind([=] + { + addSome(); + })); + + refreshRecentlyAdded(); + refreshMostPlayed(); + refresh(); + + filters->updated().connect(this, &Releases::refresh); + + std::string sessionId = LmsApp->sessionId(); + LmsApp->getMediaScanner().scanComplete().connect([=] (Scanner::MediaScanner::Stats stats) + { + if (stats.nbChanges() > 0) + { + Wt::WServer::instance()->post(sessionId, [=] + { + refreshRecentlyAdded(); + LmsApp->triggerUpdate(); + }); + } + }); +} + +void +Releases::refreshRecentlyAdded() +{ + auto after = Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-1); + + Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); + + auto releases = Release::getLastAdded(LmsApp->getDboSession(), after, 5); + + _recentlyAddedContainer->clear(); + addCompactEntries(_recentlyAddedContainer, releases); +} + +void +Releases::refreshMostPlayed() +{ + + Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); + + auto releases = TrackStats::getMostPlayedReleases(LmsApp->getDboSession(), LmsApp->getCurrentUser(), 5); + + _mostPlayedContainer->clear(); + addCompactEntries(_mostPlayedContainer, releases); +} + void Releases::refresh() { - _releasesContainer->clear(); + _container->clear(); addSome(); } @@ -117,13 +157,13 @@ Releases::addSome() Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); bool moreResults; - auto releases = Release::getByFilter(LmsApp->getDboSession(), clusterIds, searchKeywords, _releasesContainer->count(), 20, moreResults); + auto releases = Release::getByFilter(LmsApp->getDboSession(), clusterIds, searchKeywords, _container->count(), 20, moreResults); for (auto release : releases) { auto releaseId = release.id(); - Wt::WTemplate* entry = _releasesContainer->addNew(Wt::WString::tr("Lms.Explore.Releases.template.entry")); + Wt::WTemplate* entry = _container->addNew(Wt::WString::tr("Lms.Explore.Releases.template.entry")); entry->addFunction("tr", Wt::WTemplate::Functions::tr); Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); diff --git a/src/ui/explore/ReleasesView.hpp b/src/ui/explore/ReleasesView.hpp index 934afeeb..5c8374a2 100644 --- a/src/ui/explore/ReleasesView.hpp +++ b/src/ui/explore/ReleasesView.hpp @@ -39,6 +39,7 @@ class Releases : public Wt::WTemplate Wt::Signal releasePlay; private: + void refreshRecentlyAdded(); void refreshMostPlayed(); void refresh(); void addSome(); @@ -46,8 +47,9 @@ class Releases : public Wt::WTemplate Filters* _filters; Wt::WTemplate* _showMore; Wt::WLineEdit* _search; - Wt::WContainerWidget* _releasesContainer; - Wt::WContainerWidget* _mostPlayedReleasesContainer; + Wt::WContainerWidget* _container; + Wt::WContainerWidget* _mostPlayedContainer; + Wt::WContainerWidget* _recentlyAddedContainer; }; } // namespace UserInterface