diff --git a/approot/messages.xml b/approot/messages.xml
index 467646ca..1912f986 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -15,6 +15,7 @@
Confirm password
Passwords don't match
Are you sure?
+Another session has been open. Reopen this one?
Save
diff --git a/src/Makefile.am b/src/Makefile.am
index f3774a12..a8d7ec97 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -13,7 +13,7 @@ lms_SOURCES = \
$(srcdir)/database/ScanSettings.cpp \
$(srcdir)/database/SqlQuery.cpp \
$(srcdir)/database/Track.cpp \
- $(srcdir)/database/TrackStats.cpp \
+ $(srcdir)/database/TrackStats.cpp \
$(srcdir)/database/User.cpp \
$(srcdir)/image/Image.cpp \
$(srcdir)/metadata/AvFormat.cpp \
@@ -21,6 +21,7 @@ lms_SOURCES = \
$(srcdir)/scanner/MediaScanner.cpp \
$(srcdir)/ui/Auth.cpp \
$(srcdir)/ui/LmsApplication.cpp \
+ $(srcdir)/ui/LmsApplicationGroup.cpp \
$(srcdir)/ui/MediaPlayer.cpp \
$(srcdir)/ui/PlayQueueView.cpp \
$(srcdir)/ui/SettingsView.cpp \
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 9a3820f4..44eb8740 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -122,12 +122,13 @@ int main(int argc, char* argv[])
// Initializing a connection pool to the database that will be shared along services
auto connectionPool = Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db");
+ UserInterface::LmsApplicationGroups appGroups;
Scanner::MediaScanner scanner(*connectionPool);
// bind entry point
server.addEntryPoint(Wt::EntryPointType::Application,
std::bind(UserInterface::LmsApplication::create,
- std::placeholders::_1, std::ref(*connectionPool), std::ref(scanner)));
+ std::placeholders::_1, std::ref(*connectionPool), std::ref(appGroups), std::ref(scanner)));
// Start
LMS_LOG(MAIN, INFO) << "Starting Media scanner...";
diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp
index a0eb5030..5749bb0b 100644
--- a/src/ui/LmsApplication.cpp
+++ b/src/ui/LmsApplication.cpp
@@ -50,13 +50,13 @@
namespace UserInterface {
std::unique_ptr
-LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, Scanner::MediaScanner& scanner)
+LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroups& appGroups, Scanner::MediaScanner& scanner)
{
/*
* You could read information from the environment to decide whether
* the user has permission to start a new application
*/
- return std::make_unique(env, connectionPool, scanner);
+ return std::make_unique(env, connectionPool, appGroups, scanner);
}
LmsApplication*
@@ -71,9 +71,10 @@ LmsApplication::instance()
* constructor so it is typically also an argument for your custom
* application constructor.
*/
-LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, Scanner::MediaScanner& scanner)
+LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroups& appGroups, Scanner::MediaScanner& scanner)
: Wt::WApplication(env),
_db(connectionPool),
+ _appGroups(appGroups),
_scanner(scanner),
_imageResource(nullptr),
_transcodeResource(nullptr)
@@ -125,23 +126,23 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
}
else
{
- LmsApp->getDb().getLogin().changed().connect(std::bind([=]
- {
- try
- {
- handleAuthEvent();
- }
- catch (std::exception& e)
- {
- LMS_LOG(UI, ERROR) << "Error while handling auth event: " << e.what();
- throw std::runtime_error("Internal error");
- }
- }));
-
+ LmsApp->getDb().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
_auth = root()->addNew();
}
}
+LmsApplication::~LmsApplication()
+{
+ LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
+
+ getApplicationGroup().postOthers([info]
+ {
+ LmsApp->getGroupEvents().appClosed(info);
+ });
+
+ getApplicationGroup().leave();
+}
+
std::unique_ptr
LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addText)
{
@@ -250,25 +251,56 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
wApp->setInternalPath("/artists", true);
}
-void
-LmsApplication::handleAuthEvent(void)
+LmsApplicationGroup&
+LmsApplication::getApplicationGroup()
{
- if (!LmsApp->getDb().getLogin().loggedIn())
+ return _appGroups[_userIdentity];
+}
+
+void
+LmsApplication::handleAuthEvent()
+{
+ try
{
- LMS_LOG(UI, INFO) << "User logged out, session = " << Wt::WApplication::instance()->sessionId();
+ if (!getDb().getLogin().loggedIn())
+ {
+ LMS_LOG(UI, INFO) << "User '" << _userIdentity << " 'logged out, session = " << sessionId();
- goHomeAndQuit();
- return;
+ goHomeAndQuit();
+ return;
+ }
+ else
+ {
+ _userIdentity = getAuthUser().identity(Wt::Auth::Identity::LoginName);
+ LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
+
+ LMS_LOG(UI, INFO) << "User '" << _userIdentity << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", session = " << sessionId();
+ getApplicationGroup().join(info);
+
+ getApplicationGroup().postOthers([info]
+ {
+ LmsApp->getGroupEvents().appOpen(info);
+ });
+
+ createHome();
+ }
}
+ catch (std::exception& e)
+ {
+ LMS_LOG(UI, ERROR) << "Error while handling auth event: " << e.what();
+ throw std::runtime_error("Internal error");
+ }
+}
- 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
+void
+LmsApplication::createHome()
+{
+ // Handle Media Scanner events and other session events
enableUpdates(true);
{
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
- _isAdmin = LmsApp->getCurrentUser()->isAdmin();
+ _isAdmin = LmsApp->getUser()->isAdmin();
}
_imageResource = std::make_shared(_db);
@@ -355,33 +387,33 @@ LmsApplication::handleAuthEvent(void)
mainStack->addNew();
}
- explore->tracksAdd.connect(std::bind([=] (std::vector tracks)
+ explore->tracksAdd.connect([=] (std::vector tracks)
{
playqueue->addTracks(tracks);
- }, std::placeholders::_1));
+ });
- explore->tracksPlay.connect(std::bind([=] (std::vector tracks)
+ explore->tracksPlay.connect([=] (std::vector tracks)
{
playqueue->playTracks(tracks);
- }, std::placeholders::_1));
+ });
// MediaPlayer
MediaPlayer* player = main->bindNew("player");
// Events from MediaPlayer
- player->playNext.connect(std::bind([=]
+ player->playNext.connect([=]
{
playqueue->playNext();
- }));
- player->playPrevious.connect(std::bind([=]
+ });
+ player->playPrevious.connect([=]
{
playqueue->playPrevious();
- }));
- player->playbackEnded.connect(std::bind([=]
+ });
+ player->playbackEnded.connect([=]
{
playqueue->playNext();
- }));
+ });
// Events from the PlayQueue
playqueue->playTrack.connect(explore, &Explore::handleTrackPlayed);
@@ -390,7 +422,7 @@ LmsApplication::handleAuthEvent(void)
playqueue->playbackStop.connect(player, &MediaPlayer::stop);
// Events from MediaScanner
- std::string sessionId = this->sessionId();
+ std::string sessionId = LmsApp->sessionId();
_scanner.scanComplete().connect([=] (Scanner::MediaScanner::Stats stats)
{
Wt::WServer::instance()->post(sessionId, [=]
@@ -420,6 +452,19 @@ LmsApplication::handleAuthEvent(void)
});
});
+ // Events from Application group
+ _groupEvents.appOpen.connect([=] (LmsApplicationInfo info)
+ {
+ // Only one active session by user
+ setConfirmCloseMessage("");
+ quit(Wt::WString::tr("Lms.quit-other-session"));
+ });
+
+ _groupEvents.appClosed.connect([=] (LmsApplicationInfo info)
+ {
+ ;
+ });
+
internalPathChanged().connect(std::bind([=]
{
handlePathChange(mainStack, _isAdmin);
@@ -449,6 +494,7 @@ LmsApplication::notifyMsg(const Wt::WString& message)
root()->addNew(message);
}
+
} // namespace UserInterface
diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp
index b03a66d5..7733e691 100644
--- a/src/ui/LmsApplication.hpp
+++ b/src/ui/LmsApplication.hpp
@@ -30,6 +30,7 @@
#include "database/Cluster.hpp"
#include "database/Release.hpp"
+#include "LmsApplicationGroup.hpp"
#include "Auth.hpp"
namespace UserInterface {
@@ -37,13 +38,20 @@ namespace UserInterface {
class TranscodeResource;
class ImageResource;
+struct GroupEvents
+{
+ Wt::Signal appOpen;
+ Wt::Signal appClosed;
+};
+
class LmsApplication : public Wt::WApplication
{
public:
- LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, Scanner::MediaScanner& scanner);
+ LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroups& appGroups, Scanner::MediaScanner& scanner);
+ ~LmsApplication();
static std::unique_ptr create(const Wt::WEnvironment& env,
- Wt::Dbo::SqlConnectionPool& connectionPool, Scanner::MediaScanner& scanner);
+ Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroups& appGroups, Scanner::MediaScanner& scanner);
static LmsApplication* instance();
// Session application data
@@ -52,11 +60,14 @@ class LmsApplication : public Wt::WApplication
Database::Handler& getDb() { return _db;}
Wt::Dbo::Session& getDboSession() { return _db.getSession();}
- const Wt::Auth::User& getCurrentAuthUser() { return _db.getLogin().user(); }
- Database::User::pointer getCurrentUser() { return _db.getCurrentUser(); }
+ const Wt::Auth::User& getAuthUser() { return _db.getLogin().user(); }
+ Database::User::pointer getUser() { return _db.getCurrentUser(); }
+ Wt::WString getUserIdentity() { return _userIdentity; }
Scanner::MediaScanner& getMediaScanner() { return _scanner; }
+ GroupEvents& getGroupEvents() { return _groupEvents; }
+
// Utils
void goHome();
void goHomeAndQuit();
@@ -68,10 +79,18 @@ class LmsApplication : public Wt::WApplication
private:
- void handleAuthEvent(void);
+ LmsApplicationGroup& getApplicationGroup();
+
+ // Events
+ void handleAuthEvent();
void notify(const Wt::WEvent& event) override;
+ void createHome();
+
Database::Handler _db;
+ LmsApplicationGroups& _appGroups;
+ GroupEvents _groupEvents;
+ Wt::WString _userIdentity;
Auth* _auth;
Scanner::MediaScanner& _scanner;
std::shared_ptr _imageResource;
@@ -79,7 +98,8 @@ class LmsApplication : public Wt::WApplication
bool _isAdmin = false;
};
-// Helper to get session data
+
+// Helper to get session instance
#define LmsApp LmsApplication::instance()
} // namespace UserInterface
diff --git a/src/ui/LmsApplicationGroup.cpp b/src/ui/LmsApplicationGroup.cpp
new file mode 100644
index 00000000..7e64ca65
--- /dev/null
+++ b/src/ui/LmsApplicationGroup.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS 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.
+ *
+ * LMS 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 LMS. If not, see .
+ */
+
+#include "LmsApplicationGroup.hpp"
+
+#include
+
+#include
+#include
+
+
+namespace UserInterface {
+
+LmsApplicationInfo
+LmsApplicationInfo::fromEnvironment(const Wt::WEnvironment& env)
+{
+ LmsApplicationInfo info = {.userAgent = wApp->environment().agent()};
+ return info;
+}
+
+void
+LmsApplicationGroup::join(LmsApplicationInfo info)
+{
+
+ _apps.emplace(wApp->sessionId(), std::move(info));
+}
+
+void
+LmsApplicationGroup::leave()
+{
+ _apps.erase(wApp->sessionId());
+}
+
+std::vector
+LmsApplicationGroup::getOtherSessionIds() const
+{
+ std::vector res;
+
+ for (auto const& app : _apps)
+ {
+ if (app.first != wApp->sessionId())
+ res.push_back(app.first);
+ }
+
+ return res;
+}
+
+void
+LmsApplicationGroup::postOthers(std::function func) const
+{
+ for (auto const& sessionId : getOtherSessionIds())
+ {
+ Wt::WServer::instance()->post(sessionId, [=]
+ {
+ func();
+ wApp->triggerUpdate();
+ });
+ }
+}
+
+
+} // UserInterface
diff --git a/src/ui/LmsApplicationGroup.hpp b/src/ui/LmsApplicationGroup.hpp
new file mode 100644
index 00000000..e18cc456
--- /dev/null
+++ b/src/ui/LmsApplicationGroup.hpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS 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.
+ *
+ * LMS 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 LMS. If not, see .
+ */
+
+#pragma once
+
+#include