diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index b67055d1..0d730c78 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -26,18 +26,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- - name: Install Packages (cpp)
- if: ${{ matrix.language == 'cpp' }}
+ - if: matrix.language == 'cpp'
+ name: Install dependencies (cpp)
run: |
sudo apt-get update
sudo apt-get install --yes build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev libstb-dev libtag1-dev libpam0g-dev libgtest-dev
- export WT_VERSION=4.7.2
+ export WT_VERSION=4.9.0
export WT_INSTALL_PREFIX=/usr
- git clone https://github.com/emweb/wt.git wt
- pushd wt
+ git clone https://github.com/emweb/wt.git /tmp/wt
+ pushd /tmp/wt
git checkout ${WT_VERSION}
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${WT_INSTALL_PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF
- make
+ make -j$(nproc)
sudo make install
popd
@@ -47,9 +47,18 @@ jobs:
languages: ${{ matrix.language }}
queries: +security-and-quality
- - name: Autobuild
+ - if: matrix.language == 'javascript'
+ name: Autobuild
uses: github/codeql-action/autobuild@v2
+ - if: matrix.language == 'cpp'
+ name: Build
+ run: |
+ mkdir -p build
+ cd build
+ cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=Release ..
+ make -j$(nproc)
+
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
diff --git a/.lgtm.yml b/.lgtm.yml
deleted file mode 100644
index 11ce21a0..00000000
--- a/.lgtm.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-path_classifiers:
- test:
- - src/test
-
-extraction:
- cpp:
- prepare:
- packages:
- - build-essential
- - cmake
- - libboost-all-dev
- - libconfig++-dev
- - libavcodec-dev
- - libavutil-dev
- - libavformat-dev
- - libstb-dev
- - libtag1-dev
- - libpam0g-dev
- after_prepare:
- - export WT_VERSION=4.7.2
- - export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION}
- - pushd ${LGTM_WORKSPACE}
- - git clone https://github.com/emweb/wt.git ${LGTM_WORKSPACE}/wt
- - pushd ${LGTM_WORKSPACE}/wt
- - git checkout ${WT_VERSION}
- - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${WT_INSTALL_PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF
- - make install
- - popd
- configure:
- command:
- - export WT_VERSION=4.7.2
- - export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION}
- - cmake -DCMAKE_PREFIX_PATH=${WT_INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=Release
-
diff --git a/README.md b/README.md
index c0fabf8a..82a56945 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# LMS - Lightweight Music Server
-[](https://github.com/epoupon/lms/releases) [](https://github.com/epoupon/lms/actions)
+[](https://github.com/epoupon/lms/releases)
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
diff --git a/approot/mediaplayer.xml b/approot/mediaplayer.xml
index 3494fa2b..f37292f4 100644
--- a/approot/mediaplayer.xml
+++ b/approot/mediaplayer.xml
@@ -44,6 +44,10 @@
-
+
+
+ {1}
+
+
diff --git a/approot/messages.xml b/approot/messages.xml
index 241caf7a..1bbe9621 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -209,10 +209,6 @@
Clear
Create new playlist
-
- Added {1} track
- Added {1} tracks
-
Play Queue
Play Queue full!
Radio mode
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index 0439a9e5..48d8c295 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -209,10 +209,6 @@
Effacer
Créer une nouvelle liste de lecture
-
- {1} piste ajoutée
- {1} pistes ajoutées
-
Liste de lecture
Liste de lecture pleine!
Mode radio
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index 57d57209..3ed491c8 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -209,10 +209,6 @@
Cancella
-
- Aggiunta {1} traccia
- Aggiunte {1} tracce
-
Coda di riproduzione
Coda di riproduzione piena!
Modalità radio
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js
index 279554c2..ea174380 100644
--- a/docroot/js/mediaplayer.js
+++ b/docroot/js/mediaplayer.js
@@ -114,15 +114,15 @@ LMS.mediaplayer = function () {
}
let _durationToString = function (duration) {
- let minutes = parseInt(duration / 60, 10);
- let seconds = parseInt(duration, 10) % 60;
-
- let res = "";
-
- res += minutes + ":";
- res += (seconds < 10 ? "0" + seconds : seconds);
-
- return res;
+ const seconds = parseInt(duration, 10);
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = Math.round(seconds % 60);
+ return [
+ h,
+ m > 9 ? m : (h ? '0' + m : m || '0'),
+ s > 9 ? s : '0' + s
+ ].filter(Boolean).join(':');
}
let _playTrack = function() {
diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp
index 0f2687d3..5e85b72e 100644
--- a/src/libs/av/impl/TranscodeResourceHandler.cpp
+++ b/src/libs/av/impl/TranscodeResourceHandler.cpp
@@ -46,6 +46,8 @@ namespace Av
{
if (_estimatedContentLength)
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
+ else
+ LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length";
}
Wt::Http::ResponseContinuation*
@@ -58,8 +60,8 @@ namespace Av
if (_bytesReadyCount > 0)
{
response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount);
- _bytesReadyCount = 0;
_totalServedByteCount += _bytesReadyCount;
+ _bytesReadyCount = 0;
}
if (!_transcoder.finished())
diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp
index 4b828d5a..5519021d 100644
--- a/src/libs/av/impl/Transcoder.cpp
+++ b/src/libs/av/impl/Transcoder.cpp
@@ -178,19 +178,6 @@ Transcoder::start()
}
}
-void
-Transcoder::asyncWaitForData(WaitCallback cb)
-{
- assert(_childProcess);
-
- LOG(DEBUG) << "Want to wait for data";
-
- _childProcess->asyncWaitForData([cb = std::move(cb)]
- {
- cb();
- });
-}
-
void
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
{
diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp
index a4de2976..0f6cf4c7 100644
--- a/src/libs/av/impl/Transcoder.hpp
+++ b/src/libs/av/impl/Transcoder.hpp
@@ -40,9 +40,6 @@ namespace Av
Transcoder(Transcoder&&) = delete;
Transcoder& operator=(Transcoder&&) = delete;
- using WaitCallback = std::function;
- void asyncWaitForData(WaitCallback cb);
-
// non blocking calls
using ReadCallback = std::function;
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp
index d2e21215..1227c11d 100644
--- a/src/libs/metadata/impl/TagLibParser.cpp
+++ b/src/libs/metadata/impl/TagLibParser.cpp
@@ -140,6 +140,7 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
PerformerContainer performers;
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
+ // We may hit both styles for the same track
// PERFORMER: artist (role)
if (const std::vector artistNames {getPropertyValuesFirstMatchAs(properties, artistTagNames)}; !artistNames.empty())
{
@@ -151,23 +152,20 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
}
}
// PERFORMER:role (MP3)
- else
+ for (const auto& [key, values] : properties)
{
- for (const auto& [key, values] : properties)
+ if (key.startsWith("PERFORMER:"))
{
- if (key.startsWith("PERFORMER"))
+ std::string performerStr {key.to8Bit(true)};
+ std::string role;
+ if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
{
- std::string performerStr {key.to8Bit(true)};
- std::string role;
- if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
- {
- role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
- StringUtils::capitalize(role);
- }
-
- for (const auto& value : values)
- performers[role].push_back(Artist {value.to8Bit(true)});
+ role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
+ StringUtils::capitalize(role);
}
+
+ for (const auto& value : values)
+ performers[role].push_back(Artist {value.to8Bit(true)});
}
}
diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp
index fc44eb48..1cfb6811 100644
--- a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp
+++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp
@@ -194,7 +194,6 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr
[this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
-
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
TrackPositions trackPositions;
for (std::size_t i {}; i < samples.size(); ++i)
@@ -351,7 +350,7 @@ FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
loadFromTraining(trainSettings, progressCallback);
- if (!_loadCancelled)
+ if (!_loadCancelled && _network)
toCache().write();
}
diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp
index 1e3e654d..e9265122 100644
--- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp
+++ b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp
@@ -19,6 +19,7 @@
#include "ListenBrainzScrobbler.hpp"
+#include
#include
#include
#include
@@ -234,8 +235,7 @@ namespace Scrobbling::ListenBrainz
auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts))
{
- [[maybe_unused]] auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
- itContext = itNewContext;
+ std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId);
}
return itContext->second;
@@ -246,8 +246,7 @@ namespace Scrobbling::ListenBrainz
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
- [[maybe_unused]] const auto& [userId, context] {contextEntry};
- return context.syncing;
+ return contextEntry.second.syncing;
});
}
diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp
index 2542ede7..f1a79df0 100644
--- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp
+++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp
@@ -19,6 +19,7 @@
#include "ListenBrainzScrobbler.hpp"
+#include
#include
#include
#include
@@ -354,8 +355,7 @@ namespace Scrobbling::ListenBrainz
auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts))
{
- [[maybe_unused]] auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
- itContext = itNewContext;
+ std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId);
}
return itContext->second;
@@ -366,8 +366,7 @@ namespace Scrobbling::ListenBrainz
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
- [[maybe_unused]] const auto& [userId, context] {contextEntry};
- return context.syncing;
+ return contextEntry.second.syncing;
});
}
diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp
index 2773a08e..98635c68 100644
--- a/src/libs/subsonic/impl/ParameterParsing.hpp
+++ b/src/libs/subsonic/impl/ParameterParsing.hpp
@@ -80,5 +80,12 @@ namespace API::Subsonic
return *res;
}
+
+ inline
+ bool
+ hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
+ {
+ return parameterMap.find(param) != std::cend(parameterMap);
+ }
}
diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp
index 82d1efa9..c8ffad2d 100644
--- a/src/libs/subsonic/impl/SubsonicResource.cpp
+++ b/src/libs/subsonic/impl/SubsonicResource.cpp
@@ -1169,49 +1169,103 @@ handleGetArtistsRequest(RequestContext& context)
}
static
-Response
-handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
+std::vector
+findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
{
- // Mandatory params
- const ArtistId artistId {getMandatoryParameterAs(context.parameters, "id")};
+ // API says: "Returns a random collection of songs from the given artist and similar artists"
+ const std::size_t similarArtistCount {count / 5};
+ std::vector artistIds {Service::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, similarArtistCount)};
+ artistIds.push_back(artistId);
- // Optional params
- std::size_t count {getParameterAs(context.parameters, "count").value_or(50)};
-
- const auto similarArtistIds {Service::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)};
+ const std::size_t meanTrackCountPerArtist {(count / artistIds.size()) + 1};
auto transaction {context.dbSession.createSharedTransaction()};
- const Artist::pointer artist {Artist::find(context.dbSession, artistId)};
- if (!artist)
- throw RequestedDataNotFoundError {};
+ std::vector tracks;
+ tracks.reserve(count);
- const User::pointer user {User::find(context.dbSession, context.userId)};
- if (!user)
- throw UserNotAuthorizedError {};
-
- // "Returns a random collection of songs from the given artist and similar artists"
- const auto trackResults {Track::find(context.dbSession, Track::FindParameters {}
- .setArtist(artist->getId())
- .setRange({0, count / 2})
- .setSortMethod(TrackSortMethod::Random))};
-
- std::vector tracks {trackResults.results};
-
- for (const ArtistId similarArtistId : similarArtistIds)
+ for (const ArtistId id : artistIds)
{
- const auto similarArtistTracks {Track::find(context.dbSession, Track::FindParameters {}
- .setArtist(similarArtistId)
- .setRange({0, (count / 2) / 5})
- .setSortMethod(TrackSortMethod::Random))};
+ Track::FindParameters params;
+ params.setArtist(id);
+ params.setRange({0, meanTrackCountPerArtist});
+ params.setSortMethod(TrackSortMethod::Random);
+ const auto artistTracks {Track::find(context.dbSession, params)};
tracks.insert(std::end(tracks),
- std::begin(similarArtistTracks.results),
- std::end(similarArtistTracks.results));
+ std::begin(artistTracks.results),
+ std::end(artistTracks.results));
}
+ return tracks;
+}
+
+static
+std::vector
+findSimilarSongs(RequestContext& context, ReleaseId releaseId, std::size_t count)
+{
+ // API says: "Returns a random collection of songs from the given artist and similar artists"
+ // so let's extend this for release
+ const std::size_t similarReleaseCount {count / 5};
+ std::vector releaseIds {Service::get()->getSimilarReleases(releaseId, similarReleaseCount)};
+ releaseIds.push_back(releaseId);
+
+ const std::size_t meanTrackCountPerRelease {(count / releaseIds.size()) + 1};
+
+ auto transaction {context.dbSession.createSharedTransaction()};
+
+ std::vector tracks;
+ tracks.reserve(count);
+
+ for (const ReleaseId id : releaseIds)
+ {
+ Track::FindParameters params;
+ params.setRelease(id);
+ params.setRange({0, meanTrackCountPerRelease});
+ params.setSortMethod(TrackSortMethod::Random);
+
+ const auto releaseTracks {Track::find(context.dbSession, params)};
+ tracks.insert(std::end(tracks),
+ std::begin(releaseTracks.results),
+ std::end(releaseTracks.results));
+ }
+
+ return tracks;
+}
+
+static
+std::vector
+findSimilarSongs(RequestContext&, TrackId trackId, std::size_t count)
+{
+ return Service::get()->findSimilarTracks({trackId}, count);
+}
+
+static
+Response
+handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
+{
+ // Optional params
+ std::size_t count {getParameterAs(context.parameters, "count").value_or(50)};
+
+ std::vector tracks;
+
+ if (const auto artistId {getParameterAs(context.parameters, "id")})
+ tracks = findSimilarSongs(context, *artistId, count);
+ else if (const auto releaseId {getParameterAs(context.parameters, "id")})
+ tracks = findSimilarSongs(context, *releaseId, count);
+ else if (const auto trackId {getParameterAs(context.parameters, "id")})
+ tracks = findSimilarSongs(context, *trackId, count);
+ else
+ throw BadParameterGenericError {"id"};
+
Random::shuffleContainer(tracks);
+ auto transaction {context.dbSession.createSharedTransaction()};
+
+ User::pointer user {User::find(context.dbSession, context.userId)};
+ if (!user)
+ throw UserNotAuthorizedError {};
+
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
for (const TrackId trackId : tracks)
@@ -1445,7 +1499,12 @@ Response
handleSearchRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
- std::string query {getMandatoryParameterAs(context.parameters, "query")};
+ std::string queryString {getMandatoryParameterAs(context.parameters, "query")};
+ std::string_view query {queryString};
+
+ // Symfonium adds extra ""
+ if (context.clientInfo.name == "Symfonium")
+ query = StringUtils::stringTrim(query, "\"");
std::vector keywords {StringUtils::splitString(query, " ")};
@@ -1466,6 +1525,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")};
+ if (artistCount > 0)
{
Artist::FindParameters params;
params.setKeywords(keywords);
@@ -1480,6 +1540,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
}
}
+ if (albumCount > 0)
{
Release::FindParameters params;
params.setKeywords(keywords);
@@ -1494,6 +1555,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
}
}
+ if (songCount > 0)
{
Track::FindParameters params;
params.setKeywords(keywords);
@@ -2052,6 +2114,9 @@ SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters)
{
ClientInfo res;
+ if (hasParameter(parameters, "t"))
+ throw TokenAuthenticationNotSupportedForLDAPUsersError {};
+
// Mandatory parameters
res.name = getMandatoryParameterAs(parameters, "c");
res.version = getMandatoryParameterAs(parameters, "v");
diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp
index 321d7c76..d2d5294f 100644
--- a/src/libs/subsonic/impl/SubsonicResponse.hpp
+++ b/src/libs/subsonic/impl/SubsonicResponse.hpp
@@ -48,6 +48,7 @@ class Error
ClientMustUpgrade = 20,
ServerMustUpgrade = 30,
WrongUsernameOrPassword = 40,
+ TokenAuthenticationNotSupportedForLDAPUsers = 41,
UserNotAuthorized = 50,
RequestedDataNotFound = 70,
};
@@ -105,6 +106,14 @@ class WrongUsernameOrPasswordError : public Error
std::string getMessage() const override { return "Wrong username or password."; }
};
+class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
+{
+ public:
+ TokenAuthenticationNotSupportedForLDAPUsersError() : Error {Code::TokenAuthenticationNotSupportedForLDAPUsers} {}
+ private:
+ std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
+};
+
class UserNotAuthorizedError : public Error
{
public:
diff --git a/src/libs/utils/impl/ChildProcess.cpp b/src/libs/utils/impl/ChildProcess.cpp
index 90b44881..627bbcd9 100644
--- a/src/libs/utils/impl/ChildProcess.cpp
+++ b/src/libs/utils/impl/ChildProcess.cpp
@@ -44,7 +44,7 @@ namespace
{
public:
SystemException(int err, const std::string& errMsg)
- : ChildProcessException {errMsg + ": " + strerror(err)}
+ : ChildProcessException {errMsg + ": " + ::strerror(err)}
{}
SystemException(boost::system::error_code ec, const std::string& errMsg)
@@ -117,42 +117,36 @@ ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesy
ChildProcess::~ChildProcess()
{
- if (!_waited)
+ LMS_LOG(CHILDPROCESS, DEBUG) << "Closing child process...";
{
- LMS_LOG(CHILDPROCESS, DEBUG) << "Closing child process...";
- {
- boost::system::error_code closeError;
- _childStdout.close(closeError);
- if (closeError)
- LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
- }
- kill();
- wait(true);
+ boost::system::error_code closeError;
+ _childStdout.close(closeError);
+ if (closeError)
+ LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
}
-}
-void
-ChildProcess::drain()
-{
- char buf[128];
+ if (!_finished)
+ kill();
- while (boost::asio::read(_childStdout, boost::asio::buffer(buf)) > 0)
- LMS_LOG(CHILDPROCESS, DEBUG) << "drained some bytes" << std::endl;
+ wait(true);
}
void
ChildProcess::kill()
{
+ // process may already have finished
LMS_LOG(CHILDPROCESS, DEBUG) << "Killing child process...";
- ::kill(_childPID, SIGKILL);
+ if (::kill(_childPID, SIGKILL) == -1)
+ LMS_LOG(CHILDPROCESS, DEBUG) << "Kill failed: " << ::strerror(errno);
}
bool
ChildProcess::wait(bool block)
{
- int wstatus {};
+ assert(!_waited);
- pid_t pid {waitpid(_childPID, &wstatus, block ? 0 : WNOHANG)};
+ int wstatus {};
+ const pid_t pid {waitpid(_childPID, &wstatus, block ? 0 : WNOHANG)};
if (pid == -1)
throw SystemException {errno, "waitpid failed!"};
@@ -160,18 +154,22 @@ ChildProcess::wait(bool block)
return false;
if (WIFEXITED(wstatus))
+ {
_exitCode = WEXITSTATUS(wstatus);
+ LMS_LOG(CHILDPROCESS, DEBUG) << "Exit code = " << *_exitCode;
+ }
_waited = true;
return true;
}
-
void
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
{
assert(!finished());
+ LMS_LOG(CHILDPROCESS, DEBUG) << "Async read, bufferSize = " << bufferSize;
+
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
[this, callback {std::move(callback)}](const boost::system::error_code& error, std::size_t bytesTransferred)
{
@@ -180,33 +178,20 @@ ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback ca
ReadResult readResult {ReadResult::Success};
if (error)
{
- _finished = true;
-
- if (error == boost::asio::error::eof)
- readResult = ReadResult::EndOfFile;
- else
+ if (error != boost::asio::error::eof)
+ {
+ // forbidden to read any captured param here as the ChildProcess instance may already have been killed
return;
+ }
+
+ readResult = ReadResult::EndOfFile;
+ _finished = true;
}
callback(readResult, bytesTransferred);
});
}
-void
-ChildProcess::asyncWaitForData(WaitCallback cb)
-{
- LMS_LOG(CHILDPROCESS, DEBUG) << "Async wait requested";
- assert(!finished());
-
- _childStdout.async_wait(boost::asio::posix::stream_descriptor::wait_read,
- [cb {std::move(cb)}](const boost::system::error_code& ec)
- {
- LMS_LOG(CHILDPROCESS, DEBUG) << "Wait CB, error = " << ec.message();
- if (!ec)
- cb();
- });
-}
-
std::size_t
ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
{
@@ -220,7 +205,7 @@ ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
}
bool
-ChildProcess::finished()
+ChildProcess::finished() const
{
return _finished;
}
diff --git a/src/libs/utils/impl/ChildProcess.hpp b/src/libs/utils/impl/ChildProcess.hpp
index 46d7bcb0..8b5752dc 100644
--- a/src/libs/utils/impl/ChildProcess.hpp
+++ b/src/libs/utils/impl/ChildProcess.hpp
@@ -40,12 +40,10 @@ class ChildProcess : public IChildProcess
private:
void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override;
- void asyncWaitForData(WaitCallback cb) override;
std::size_t readSome(std::byte* data, std::size_t bufferSize) override;
- bool finished() override;
+ bool finished() const override;
void kill();
- void drain();
bool wait(bool block); // return true if waited
using FileDescriptor = boost::asio::posix::stream_descriptor;
diff --git a/src/libs/utils/include/utils/IChildProcess.hpp b/src/libs/utils/include/utils/IChildProcess.hpp
index 7de5731a..72768892 100644
--- a/src/libs/utils/include/utils/IChildProcess.hpp
+++ b/src/libs/utils/include/utils/IChildProcess.hpp
@@ -48,9 +48,7 @@ class IChildProcess
using ReadCallback = std::function;
virtual void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) = 0;
- using WaitCallback = std::function;
- virtual void asyncWaitForData(WaitCallback cb) = 0;
virtual std::size_t readSome(std::byte* data, std::size_t bufferSize) = 0;
- virtual bool finished() = 0;
+ virtual bool finished() const = 0;
};
diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp
index 0e09f8ea..9aaf02af 100644
--- a/src/lms/ui/LmsApplication.cpp
+++ b/src/lms/ui/LmsApplication.cpp
@@ -224,9 +224,7 @@ LmsApplication::init()
enableUpdates(true);
if (_authenticatedUser)
- {
onUserLoggedIn();
- }
else if (Service<::Auth::IPasswordService>::exists())
processPasswordAuth();
}
@@ -339,7 +337,6 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
- LmsApp->doJavaScript(R"($('.navbar-nav a.active').removeClass('active'); $('.navbar-nav a[href="' + location.pathname + '"]').closest('a').addClass('active');)");
for (const auto& view : views)
{
if (wApp->internalPathMatches(view.path))
@@ -350,6 +347,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
stack.setCurrentIndex(view.index);
if (view.title)
LmsApp->setTitle(*view.title);
+
+ LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + view.path +"')");
return;
}
}
@@ -398,9 +397,21 @@ LmsApplication::createHome()
_coverResource = std::make_shared();
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
+ declareJavaScriptFunction("updateActiveNav",
+R"(function(current) {
+ const menuItems = document.querySelectorAll('.nav-item a[href]:not([href=""])');
+ for (const menuItem of menuItems) {
+ if (menuItem.getAttribute("href").indexOf(current) !== -1) {
+ menuItem.classList.add('active');
+ }
+ else {
+ menuItem.classList.remove('active');
+ }
+
+ }
+})");
Wt::WTemplate* main {root()->addWidget(std::make_unique(Wt::WString::tr("Lms.main.template")))};
-
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
Template* navbar {main->bindNew("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
@@ -507,6 +518,11 @@ LmsApplication::createHome()
{
_mediaPlayer->stop();
});
+ _playQueue->trackCountChanged.connect([this] (std::size_t trackCount)
+ {
+ _mediaPlayer->onPlayQueueUpdated(trackCount);
+ });
+ _mediaPlayer->onPlayQueueUpdated(_playQueue->getCount());
const bool isAdmin {getUserType() == Database::UserType::ADMIN};
if (isAdmin)
diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp
index 4bd40a2b..a5a2b21b 100644
--- a/src/lms/ui/MediaPlayer.cpp
+++ b/src/lms/ui/MediaPlayer.cpp
@@ -210,12 +210,9 @@ MediaPlayer::MediaPlayer()
_title = bindNew("title");
_artist = bindNew("artist");
_release = bindNew("release");
-
- {
- Wt::WPushButton* playQueueBtn {bindNew("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn"), Wt::TextFormat::XHTML)};
- playQueueBtn->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"});
- playQueueBtn->setToolTip(tr("Lms.PlayQueue.playqueue"));
- }
+ _playQueue = bindNew("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML);
+ _playQueue->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"});
+ _playQueue->setToolTip(tr("Lms.PlayQueue.playqueue"));
_settingsLoaded.connect([this](const std::string& settings)
{
@@ -330,5 +327,11 @@ MediaPlayer::setSettings(const Settings& settings)
}
}
+void
+MediaPlayer::onPlayQueueUpdated(std::size_t trackCount)
+{
+ _playQueue->setText(Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(trackCount));
+}
+
} // namespace UserInterface
diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp
index 40307c4b..512e0270 100644
--- a/src/lms/ui/MediaPlayer.hpp
+++ b/src/lms/ui/MediaPlayer.hpp
@@ -101,7 +101,9 @@ class MediaPlayer : public Wt::WTemplate
void stop();
std::optional getSettings() const { return _settings; }
- void setSettings(const Settings& settings);
+ void setSettings(const Settings& settings);
+
+ void onPlayQueueUpdated(std::size_t trackCount);
// Signals
Wt::JSignal<> playPrevious;
@@ -123,9 +125,10 @@ class MediaPlayer : public Wt::WTemplate
Wt::JSignal _settingsLoaded;
- Wt::WText* _title {};
- Wt::WAnchor* _release {};
- Wt::WAnchor* _artist {};
+ Wt::WText* _title {};
+ Wt::WAnchor* _release {};
+ Wt::WAnchor* _artist {};
+ Wt::WPushButton* _playQueue {};
};
} // namespace UserInterface
diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp
index 6face3a5..b128b563 100644
--- a/src/lms/ui/PlayQueue.cpp
+++ b/src/lms/ui/PlayQueue.cpp
@@ -343,6 +343,13 @@ PlayQueue::playNext()
loadTrack(*_trackPos + 1, true);
}
+std::size_t
+PlayQueue::getCount()
+{
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+ return getQueue()->getCount();
+}
+
void
PlayQueue::initTrackLists()
{
@@ -373,9 +380,10 @@ PlayQueue::updateInfo()
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::TrackList::pointer queue {getQueue()};
- const auto trackCount {queue->getCount()};
+ const std::size_t trackCount {queue->getCount()};
_nbTracks->setText(Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount));
_duration->setText(Utils::durationToString(queue->getDuration()));
+ trackCountChanged.emit(trackCount);
}
void
@@ -435,40 +443,24 @@ PlayQueue::playShuffled(const std::vector& trackIds)
clearTracks();
std::vector shuffledTrackIds {trackIds};
Random::shuffleContainer(shuffledTrackIds);
- const std::size_t nbAddedTracks {enqueueTracks(shuffledTrackIds)};
+ enqueueTracks(shuffledTrackIds);
loadTrack(0, true);
-
- notifyAddedTracks(nbAddedTracks);
}
void
PlayQueue::playOrAddLast(const std::vector& trackIds)
{
- const std::size_t nbAddedTracks {enqueueTracks(trackIds)};
+ enqueueTracks(trackIds);
if (!_trackPos)
loadTrack(0, true);
-
- notifyAddedTracks(nbAddedTracks);
}
void
PlayQueue::playAtIndex(const std::vector& trackIds, std::size_t index)
{
clearTracks();
- const std::size_t nbAddedTracks {enqueueTracks(trackIds)};
+ enqueueTracks(trackIds);
loadTrack(index, true);
-
- notifyAddedTracks(nbAddedTracks);
-}
-
-void
-PlayQueue::notifyAddedTracks(std::size_t nbAddedTracks) const
-{
- if (nbAddedTracks > 0)
- LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::seconds {2});
-
- if (isFull())
- LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::seconds {2});
}
void
diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp
index bef41f68..0599eb9e 100644
--- a/src/lms/ui/PlayQueue.hpp
+++ b/src/lms/ui/PlayQueue.hpp
@@ -66,7 +66,11 @@ class PlayQueue : public Template
// Signal emitted when track is unselected (has to be stopped)
Wt::Signal<> trackUnselected;
+ // Signal emitted when track count changed
+ Wt::Signal trackCountChanged;
+
constexpr std::size_t getCapacity() const { return _capacity; }
+ std::size_t getCount();
private:
void initTrackLists();
diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp
index 3c1d4176..1cd3561f 100644
--- a/src/lms/ui/explore/ReleaseView.cpp
+++ b/src/lms/ui/explore/ReleaseView.cpp
@@ -77,7 +77,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
if (artistIds.results.empty())
return;
- Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
+ Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};
for (ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
@@ -109,6 +109,13 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
+ if (auto itRolelessPerformers {artistMap.find("")}; itRolelessPerformers != std::cend(artistMap))
+ {
+ Wt::WString performersStr {Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size())};
+ artistMap[performersStr] = std::move(itRolelessPerformers->second);
+ artistMap.erase(itRolelessPerformers);
+ }
+
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
@@ -118,7 +125,6 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
artistTable->addWidget(std::move(artistsEntry));
}
-
// TODO: save in DB and mean all this
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results)
{
diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp
index d7f082fc..6da0f2f4 100644
--- a/src/lms/ui/explore/TrackListHelpers.cpp
+++ b/src/lms/ui/explore/TrackListHelpers.cpp
@@ -103,6 +103,13 @@ namespace UserInterface::TrackListHelpers
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
+ if (auto itRolelessPerformers {artistMap.find("")}; itRolelessPerformers != std::cend(artistMap))
+ {
+ Wt::WString performersStr {Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size())};
+ artistMap[performersStr] = std::move(itRolelessPerformers->second);
+ artistMap.erase(itRolelessPerformers);
+ }
+
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};