Merge branch 'develop' for release v3.35.0

This commit is contained in:
emeric
2023-01-07 16:27:41 +01:00
28 changed files with 261 additions and 220 deletions
+16 -7
View File
@@ -26,18 +26,18 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Packages (cpp) - if: matrix.language == 'cpp'
if: ${{ matrix.language == 'cpp' }} name: Install dependencies (cpp)
run: | run: |
sudo apt-get update 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 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 export WT_INSTALL_PREFIX=/usr
git clone https://github.com/emweb/wt.git wt git clone https://github.com/emweb/wt.git /tmp/wt
pushd wt pushd /tmp/wt
git checkout ${WT_VERSION} git checkout ${WT_VERSION}
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${WT_INSTALL_PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF 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 sudo make install
popd popd
@@ -47,9 +47,18 @@ jobs:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-and-quality queries: +security-and-quality
- name: Autobuild - if: matrix.language == 'javascript'
name: Autobuild
uses: github/codeql-action/autobuild@v2 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 - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v2
with: with:
-34
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
# LMS - Lightweight Music Server # LMS - Lightweight Music Server
[![Last Release](https://img.shields.io/github/v/release/epoupon/lms?logo=github&label=latest)](https://github.com/epoupon/lms/releases) [![Build](https://img.shields.io/github/workflow/status/epoupon/lms/Build?logo=github)](https://github.com/epoupon/lms/actions) [![Last Release](https://img.shields.io/github/v/release/epoupon/lms?logo=github&label=latest)](https://github.com/epoupon/lms/releases)
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! _LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
+5 -1
View File
@@ -44,6 +44,10 @@
</div> </div>
</message> </message>
<message id="Lms.MediaPlayer.template.playqueue-btn"><i class="fa fa-fw fa-th-list"/></message> <message id="Lms.MediaPlayer.template.playqueue-btn">
<span>
<i class="fa fa-fw fa-th-list d-inline"/><div class="d-none d-sm-inline ms-2 small">{1}</div>
</span>
</message>
</messages> </messages>
-4
View File
@@ -209,10 +209,6 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Clear</message> <message id="Lms.PlayQueue.clear">Clear</message>
<message id="Lms.PlayQueue.create-tracklist">Create new playlist</message> <message id="Lms.PlayQueue.create-tracklist">Create new playlist</message>
<message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">Added {1} track</plural>
<plural case="1">Added {1} tracks</plural>
</message>
<message id="Lms.PlayQueue.playqueue">Play Queue</message> <message id="Lms.PlayQueue.playqueue">Play Queue</message>
<message id="Lms.PlayQueue.playqueue-full">Play Queue full!</message> <message id="Lms.PlayQueue.playqueue-full">Play Queue full!</message>
<message id="Lms.PlayQueue.radio-mode">Radio mode</message> <message id="Lms.PlayQueue.radio-mode">Radio mode</message>
-4
View File
@@ -209,10 +209,6 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Effacer</message> <message id="Lms.PlayQueue.clear">Effacer</message>
<message id="Lms.PlayQueue.create-tracklist">Créer une nouvelle liste de lecture</message> <message id="Lms.PlayQueue.create-tracklist">Créer une nouvelle liste de lecture</message>
<message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">{1} piste ajoutée</plural>
<plural case="1">{1} pistes ajoutées</plural>
</message>
<message id="Lms.PlayQueue.playqueue">Liste de lecture</message> <message id="Lms.PlayQueue.playqueue">Liste de lecture</message>
<message id="Lms.PlayQueue.playqueue-full">Liste de lecture pleine!</message> <message id="Lms.PlayQueue.playqueue-full">Liste de lecture pleine!</message>
<message id="Lms.PlayQueue.radio-mode">Mode radio</message> <message id="Lms.PlayQueue.radio-mode">Mode radio</message>
-4
View File
@@ -209,10 +209,6 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Cancella</message> <message id="Lms.PlayQueue.clear">Cancella</message>
<message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">Aggiunta {1} traccia</plural>
<plural case="1">Aggiunte {1} tracce</plural>
</message>
<message id="Lms.PlayQueue.playqueue">Coda di riproduzione</message> <message id="Lms.PlayQueue.playqueue">Coda di riproduzione</message>
<message id="Lms.PlayQueue.playqueue-full">Coda di riproduzione piena!</message> <message id="Lms.PlayQueue.playqueue-full">Coda di riproduzione piena!</message>
<message id="Lms.PlayQueue.radio-mode">Modalità radio</message> <message id="Lms.PlayQueue.radio-mode">Modalità radio</message>
+9 -9
View File
@@ -114,15 +114,15 @@ LMS.mediaplayer = function () {
} }
let _durationToString = function (duration) { let _durationToString = function (duration) {
let minutes = parseInt(duration / 60, 10); const seconds = parseInt(duration, 10);
let seconds = parseInt(duration, 10) % 60; const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
let res = ""; const s = Math.round(seconds % 60);
return [
res += minutes + ":"; h,
res += (seconds < 10 ? "0" + seconds : seconds); m > 9 ? m : (h ? '0' + m : m || '0'),
s > 9 ? s : '0' + s
return res; ].filter(Boolean).join(':');
} }
let _playTrack = function() { let _playTrack = function() {
@@ -46,6 +46,8 @@ namespace Av
{ {
if (_estimatedContentLength) if (_estimatedContentLength)
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength; LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
else
LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length";
} }
Wt::Http::ResponseContinuation* Wt::Http::ResponseContinuation*
@@ -58,8 +60,8 @@ namespace Av
if (_bytesReadyCount > 0) if (_bytesReadyCount > 0)
{ {
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount); response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
_bytesReadyCount = 0;
_totalServedByteCount += _bytesReadyCount; _totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0;
} }
if (!_transcoder.finished()) if (!_transcoder.finished())
-13
View File
@@ -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 void
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback) Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
{ {
-3
View File
@@ -40,9 +40,6 @@ namespace Av
Transcoder(Transcoder&&) = delete; Transcoder(Transcoder&&) = delete;
Transcoder& operator=(Transcoder&&) = delete; Transcoder& operator=(Transcoder&&) = delete;
using WaitCallback = std::function<void()>;
void asyncWaitForData(WaitCallback cb);
// non blocking calls // non blocking calls
using ReadCallback = std::function<void(std::size_t nbReadBytes)>; using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
+2 -4
View File
@@ -140,6 +140,7 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
PerformerContainer performers; PerformerContainer performers;
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) // 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) // PERFORMER: artist (role)
if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)}; !artistNames.empty()) if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)}; !artistNames.empty())
{ {
@@ -151,11 +152,9 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
} }
} }
// PERFORMER:role (MP3) // 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 performerStr {key.to8Bit(true)};
std::string role; std::string role;
@@ -169,7 +168,6 @@ getPerformerArtists(const TagLib::PropertyMap& properties,
performers[role].push_back(Artist {value.to8Bit(true)}); performers[role].push_back(Artist {value.to8Bit(true)});
} }
} }
}
return performers; return performers;
} }
@@ -194,7 +194,6 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr
[this] { return _loadCancelled; }); [this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE"; LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks..."; LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
TrackPositions trackPositions; TrackPositions trackPositions;
for (std::size_t i {}; i < samples.size(); ++i) for (std::size_t i {}; i < samples.size(); ++i)
@@ -351,7 +350,7 @@ FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings(); trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
loadFromTraining(trainSettings, progressCallback); loadFromTraining(trainSettings, progressCallback);
if (!_loadCancelled) if (!_loadCancelled && _network)
toCache().write(); toCache().write();
} }
@@ -19,6 +19,7 @@
#include "ListenBrainzScrobbler.hpp" #include "ListenBrainzScrobbler.hpp"
#include <tuple>
#include <boost/asio/bind_executor.hpp> #include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h> #include <Wt/Json/Array.h>
#include <Wt/Json/Object.h> #include <Wt/Json/Object.h>
@@ -234,8 +235,7 @@ namespace Scrobbling::ListenBrainz
auto itContext {_userContexts.find(userId)}; auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts)) if (itContext == std::cend(_userContexts))
{ {
[[maybe_unused]] auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)}; std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId);
itContext = itNewContext;
} }
return itContext->second; return itContext->second;
@@ -246,8 +246,7 @@ namespace Scrobbling::ListenBrainz
{ {
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{ {
[[maybe_unused]] const auto& [userId, context] {contextEntry}; return contextEntry.second.syncing;
return context.syncing;
}); });
} }
@@ -19,6 +19,7 @@
#include "ListenBrainzScrobbler.hpp" #include "ListenBrainzScrobbler.hpp"
#include <tuple>
#include <boost/asio/bind_executor.hpp> #include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h> #include <Wt/Json/Array.h>
#include <Wt/Json/Object.h> #include <Wt/Json/Object.h>
@@ -354,8 +355,7 @@ namespace Scrobbling::ListenBrainz
auto itContext {_userContexts.find(userId)}; auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts)) if (itContext == std::cend(_userContexts))
{ {
[[maybe_unused]] auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)}; std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId);
itContext = itNewContext;
} }
return itContext->second; return itContext->second;
@@ -366,8 +366,7 @@ namespace Scrobbling::ListenBrainz
{ {
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{ {
[[maybe_unused]] const auto& [userId, context] {contextEntry}; return contextEntry.second.syncing;
return context.syncing;
}); });
} }
@@ -80,5 +80,12 @@ namespace API::Subsonic
return *res; return *res;
} }
inline
bool
hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
return parameterMap.find(param) != std::cend(parameterMap);
}
} }
+96 -31
View File
@@ -1169,49 +1169,103 @@ handleGetArtistsRequest(RequestContext& context)
} }
static static
Response std::vector<TrackId>
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
{ {
// Mandatory params // API says: "Returns a random collection of songs from the given artist and similar artists"
const ArtistId artistId {getMandatoryParameterAs<ArtistId>(context.parameters, "id")}; const std::size_t similarArtistCount {count / 5};
std::vector<ArtistId> artistIds {Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, similarArtistCount)};
artistIds.push_back(artistId);
// Optional params const std::size_t meanTrackCountPerArtist {(count / artistIds.size()) + 1};
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
const auto similarArtistIds {Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)};
auto transaction {context.dbSession.createSharedTransaction()}; auto transaction {context.dbSession.createSharedTransaction()};
const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; std::vector<TrackId> tracks;
if (!artist) tracks.reserve(count);
throw RequestedDataNotFoundError {};
const User::pointer user {User::find(context.dbSession, context.userId)}; for (const ArtistId id : artistIds)
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<TrackId> tracks {trackResults.results};
for (const ArtistId similarArtistId : similarArtistIds)
{ {
const auto similarArtistTracks {Track::find(context.dbSession, Track::FindParameters {} Track::FindParameters params;
.setArtist(similarArtistId) params.setArtist(id);
.setRange({0, (count / 2) / 5}) params.setRange({0, meanTrackCountPerArtist});
.setSortMethod(TrackSortMethod::Random))}; params.setSortMethod(TrackSortMethod::Random);
const auto artistTracks {Track::find(context.dbSession, params)};
tracks.insert(std::end(tracks), tracks.insert(std::end(tracks),
std::begin(similarArtistTracks.results), std::begin(artistTracks.results),
std::end(similarArtistTracks.results)); std::end(artistTracks.results));
} }
return tracks;
}
static
std::vector<TrackId>
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<ReleaseId> releaseIds {Service<Recommendation::IRecommendationService>::get()->getSimilarReleases(releaseId, similarReleaseCount)};
releaseIds.push_back(releaseId);
const std::size_t meanTrackCountPerRelease {(count / releaseIds.size()) + 1};
auto transaction {context.dbSession.createSharedTransaction()};
std::vector<TrackId> 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<TrackId>
findSimilarSongs(RequestContext&, TrackId trackId, std::size_t count)
{
return Service<Recommendation::IRecommendationService>::get()->findSimilarTracks({trackId}, count);
}
static
Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
{
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
std::vector<TrackId> tracks;
if (const auto artistId {getParameterAs<ArtistId>(context.parameters, "id")})
tracks = findSimilarSongs(context, *artistId, count);
else if (const auto releaseId {getParameterAs<ReleaseId>(context.parameters, "id")})
tracks = findSimilarSongs(context, *releaseId, count);
else if (const auto trackId {getParameterAs<TrackId>(context.parameters, "id")})
tracks = findSimilarSongs(context, *trackId, count);
else
throw BadParameterGenericError {"id"};
Random::shuffleContainer(tracks); 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 response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
for (const TrackId trackId : tracks) for (const TrackId trackId : tracks)
@@ -1445,7 +1499,12 @@ Response
handleSearchRequestCommon(RequestContext& context, bool id3) handleSearchRequestCommon(RequestContext& context, bool id3)
{ {
// Mandatory params // Mandatory params
std::string query {getMandatoryParameterAs<std::string>(context.parameters, "query")}; std::string queryString {getMandatoryParameterAs<std::string>(context.parameters, "query")};
std::string_view query {queryString};
// Symfonium adds extra ""
if (context.clientInfo.name == "Symfonium")
query = StringUtils::stringTrim(query, "\"");
std::vector<std::string_view> keywords {StringUtils::splitString(query, " ")}; std::vector<std::string_view> keywords {StringUtils::splitString(query, " ")};
@@ -1466,6 +1525,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")};
if (artistCount > 0)
{ {
Artist::FindParameters params; Artist::FindParameters params;
params.setKeywords(keywords); params.setKeywords(keywords);
@@ -1480,6 +1540,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
} }
} }
if (albumCount > 0)
{ {
Release::FindParameters params; Release::FindParameters params;
params.setKeywords(keywords); params.setKeywords(keywords);
@@ -1494,6 +1555,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
} }
} }
if (songCount > 0)
{ {
Track::FindParameters params; Track::FindParameters params;
params.setKeywords(keywords); params.setKeywords(keywords);
@@ -2052,6 +2114,9 @@ SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters)
{ {
ClientInfo res; ClientInfo res;
if (hasParameter(parameters, "t"))
throw TokenAuthenticationNotSupportedForLDAPUsersError {};
// Mandatory parameters // Mandatory parameters
res.name = getMandatoryParameterAs<std::string>(parameters, "c"); res.name = getMandatoryParameterAs<std::string>(parameters, "c");
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v"); res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
@@ -48,6 +48,7 @@ class Error
ClientMustUpgrade = 20, ClientMustUpgrade = 20,
ServerMustUpgrade = 30, ServerMustUpgrade = 30,
WrongUsernameOrPassword = 40, WrongUsernameOrPassword = 40,
TokenAuthenticationNotSupportedForLDAPUsers = 41,
UserNotAuthorized = 50, UserNotAuthorized = 50,
RequestedDataNotFound = 70, RequestedDataNotFound = 70,
}; };
@@ -105,6 +106,14 @@ class WrongUsernameOrPasswordError : public Error
std::string getMessage() const override { return "Wrong username or password."; } 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 class UserNotAuthorizedError : public Error
{ {
public: public:
+22 -37
View File
@@ -44,7 +44,7 @@ namespace
{ {
public: public:
SystemException(int err, const std::string& errMsg) 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) SystemException(boost::system::error_code ec, const std::string& errMsg)
@@ -116,8 +116,6 @@ ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesy
} }
ChildProcess::~ChildProcess() ChildProcess::~ChildProcess()
{
if (!_waited)
{ {
LMS_LOG(CHILDPROCESS, DEBUG) << "Closing child process..."; LMS_LOG(CHILDPROCESS, DEBUG) << "Closing child process...";
{ {
@@ -126,33 +124,29 @@ ChildProcess::~ChildProcess()
if (closeError) if (closeError)
LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message(); LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
} }
if (!_finished)
kill(); kill();
wait(true); wait(true);
} }
}
void
ChildProcess::drain()
{
char buf[128];
while (boost::asio::read(_childStdout, boost::asio::buffer(buf)) > 0)
LMS_LOG(CHILDPROCESS, DEBUG) << "drained some bytes" << std::endl;
}
void void
ChildProcess::kill() ChildProcess::kill()
{ {
// process may already have finished
LMS_LOG(CHILDPROCESS, DEBUG) << "Killing child process..."; LMS_LOG(CHILDPROCESS, DEBUG) << "Killing child process...";
::kill(_childPID, SIGKILL); if (::kill(_childPID, SIGKILL) == -1)
LMS_LOG(CHILDPROCESS, DEBUG) << "Kill failed: " << ::strerror(errno);
} }
bool bool
ChildProcess::wait(bool block) 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) if (pid == -1)
throw SystemException {errno, "waitpid failed!"}; throw SystemException {errno, "waitpid failed!"};
@@ -160,18 +154,22 @@ ChildProcess::wait(bool block)
return false; return false;
if (WIFEXITED(wstatus)) if (WIFEXITED(wstatus))
{
_exitCode = WEXITSTATUS(wstatus); _exitCode = WEXITSTATUS(wstatus);
LMS_LOG(CHILDPROCESS, DEBUG) << "Exit code = " << *_exitCode;
}
_waited = true; _waited = true;
return true; return true;
} }
void void
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
{ {
assert(!finished()); assert(!finished());
LMS_LOG(CHILDPROCESS, DEBUG) << "Async read, bufferSize = " << bufferSize;
boost::asio::async_read(_childStdout, boost::asio::buffer(data, 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) [this, callback {std::move(callback)}](const boost::system::error_code& error, std::size_t bytesTransferred)
{ {
@@ -180,30 +178,17 @@ ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback ca
ReadResult readResult {ReadResult::Success}; ReadResult readResult {ReadResult::Success};
if (error) if (error)
{ {
_finished = true; if (error != boost::asio::error::eof)
{
if (error == boost::asio::error::eof) // forbidden to read any captured param here as the ChildProcess instance may already have been killed
readResult = ReadResult::EndOfFile;
else
return; return;
} }
callback(readResult, bytesTransferred); readResult = ReadResult::EndOfFile;
}); _finished = true;
} }
void callback(readResult, bytesTransferred);
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();
}); });
} }
@@ -220,7 +205,7 @@ ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
} }
bool bool
ChildProcess::finished() ChildProcess::finished() const
{ {
return _finished; return _finished;
} }
+1 -3
View File
@@ -40,12 +40,10 @@ class ChildProcess : public IChildProcess
private: private:
void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override; 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; std::size_t readSome(std::byte* data, std::size_t bufferSize) override;
bool finished() override; bool finished() const override;
void kill(); void kill();
void drain();
bool wait(bool block); // return true if waited bool wait(bool block); // return true if waited
using FileDescriptor = boost::asio::posix::stream_descriptor; using FileDescriptor = boost::asio::posix::stream_descriptor;
@@ -48,9 +48,7 @@ class IChildProcess
using ReadCallback = std::function<void(ReadResult, std::size_t)>; using ReadCallback = std::function<void(ReadResult, std::size_t)>;
virtual void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) = 0; virtual void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) = 0;
using WaitCallback = std::function<void(void)>;
virtual void asyncWaitForData(WaitCallback cb) = 0;
virtual std::size_t readSome(std::byte* data, std::size_t bufferSize) = 0; virtual std::size_t readSome(std::byte* data, std::size_t bufferSize) = 0;
virtual bool finished() = 0; virtual bool finished() const = 0;
}; };
+20 -4
View File
@@ -224,9 +224,7 @@ LmsApplication::init()
enableUpdates(true); enableUpdates(true);
if (_authenticatedUser) if (_authenticatedUser)
{
onUserLoggedIn(); onUserLoggedIn();
}
else if (Service<::Auth::IPasswordService>::exists()) else if (Service<::Auth::IPasswordService>::exists())
processPasswordAuth(); processPasswordAuth();
} }
@@ -339,7 +337,6 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'"; 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) for (const auto& view : views)
{ {
if (wApp->internalPathMatches(view.path)) if (wApp->internalPathMatches(view.path))
@@ -350,6 +347,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
stack.setCurrentIndex(view.index); stack.setCurrentIndex(view.index);
if (view.title) if (view.title)
LmsApp->setTitle(*view.title); LmsApp->setTitle(*view.title);
LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + view.path +"')");
return; return;
} }
} }
@@ -398,9 +397,21 @@ LmsApplication::createHome()
_coverResource = std::make_shared<CoverResource>(); _coverResource = std::make_shared<CoverResource>();
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}"); 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::WTemplate>(Wt::WString::tr("Lms.main.template")))}; Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.main.template")))};
main->addFunction("tr", &Wt::WTemplate::Functions::tr); main->addFunction("tr", &Wt::WTemplate::Functions::tr);
Template* navbar {main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar"))}; Template* navbar {main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
@@ -507,6 +518,11 @@ LmsApplication::createHome()
{ {
_mediaPlayer->stop(); _mediaPlayer->stop();
}); });
_playQueue->trackCountChanged.connect([this] (std::size_t trackCount)
{
_mediaPlayer->onPlayQueueUpdated(trackCount);
});
_mediaPlayer->onPlayQueueUpdated(_playQueue->getCount());
const bool isAdmin {getUserType() == Database::UserType::ADMIN}; const bool isAdmin {getUserType() == Database::UserType::ADMIN};
if (isAdmin) if (isAdmin)
+9 -6
View File
@@ -210,12 +210,9 @@ MediaPlayer::MediaPlayer()
_title = bindNew<Wt::WText>("title"); _title = bindNew<Wt::WText>("title");
_artist = bindNew<Wt::WAnchor>("artist"); _artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release"); _release = bindNew<Wt::WAnchor>("release");
_playQueue = bindNew<Wt::WPushButton>("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML);
{ _playQueue->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"});
Wt::WPushButton* playQueueBtn {bindNew<Wt::WPushButton>("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn"), Wt::TextFormat::XHTML)}; _playQueue->setToolTip(tr("Lms.PlayQueue.playqueue"));
playQueueBtn->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"});
playQueueBtn->setToolTip(tr("Lms.PlayQueue.playqueue"));
}
_settingsLoaded.connect([this](const std::string& settings) _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 } // namespace UserInterface
+3
View File
@@ -103,6 +103,8 @@ class MediaPlayer : public Wt::WTemplate
std::optional<Settings> getSettings() const { return _settings; } std::optional<Settings> getSettings() const { return _settings; }
void setSettings(const Settings& settings); void setSettings(const Settings& settings);
void onPlayQueueUpdated(std::size_t trackCount);
// Signals // Signals
Wt::JSignal<> playPrevious; Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext; Wt::JSignal<> playNext;
@@ -126,6 +128,7 @@ class MediaPlayer : public Wt::WTemplate
Wt::WText* _title {}; Wt::WText* _title {};
Wt::WAnchor* _release {}; Wt::WAnchor* _release {};
Wt::WAnchor* _artist {}; Wt::WAnchor* _artist {};
Wt::WPushButton* _playQueue {};
}; };
} // namespace UserInterface } // namespace UserInterface
+12 -20
View File
@@ -343,6 +343,13 @@ PlayQueue::playNext()
loadTrack(*_trackPos + 1, true); loadTrack(*_trackPos + 1, true);
} }
std::size_t
PlayQueue::getCount()
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
return getQueue()->getCount();
}
void void
PlayQueue::initTrackLists() PlayQueue::initTrackLists()
{ {
@@ -373,9 +380,10 @@ PlayQueue::updateInfo()
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::TrackList::pointer queue {getQueue()}; 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)); _nbTracks->setText(Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount));
_duration->setText(Utils::durationToString(queue->getDuration())); _duration->setText(Utils::durationToString(queue->getDuration()));
trackCountChanged.emit(trackCount);
} }
void void
@@ -435,40 +443,24 @@ PlayQueue::playShuffled(const std::vector<Database::TrackId>& trackIds)
clearTracks(); clearTracks();
std::vector<Database::TrackId> shuffledTrackIds {trackIds}; std::vector<Database::TrackId> shuffledTrackIds {trackIds};
Random::shuffleContainer(shuffledTrackIds); Random::shuffleContainer(shuffledTrackIds);
const std::size_t nbAddedTracks {enqueueTracks(shuffledTrackIds)}; enqueueTracks(shuffledTrackIds);
loadTrack(0, true); loadTrack(0, true);
notifyAddedTracks(nbAddedTracks);
} }
void void
PlayQueue::playOrAddLast(const std::vector<Database::TrackId>& trackIds) PlayQueue::playOrAddLast(const std::vector<Database::TrackId>& trackIds)
{ {
const std::size_t nbAddedTracks {enqueueTracks(trackIds)}; enqueueTracks(trackIds);
if (!_trackPos) if (!_trackPos)
loadTrack(0, true); loadTrack(0, true);
notifyAddedTracks(nbAddedTracks);
} }
void void
PlayQueue::playAtIndex(const std::vector<Database::TrackId>& trackIds, std::size_t index) PlayQueue::playAtIndex(const std::vector<Database::TrackId>& trackIds, std::size_t index)
{ {
clearTracks(); clearTracks();
const std::size_t nbAddedTracks {enqueueTracks(trackIds)}; enqueueTracks(trackIds);
loadTrack(index, true); 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 void
+4
View File
@@ -66,7 +66,11 @@ class PlayQueue : public Template
// Signal emitted when track is unselected (has to be stopped) // Signal emitted when track is unselected (has to be stopped)
Wt::Signal<> trackUnselected; Wt::Signal<> trackUnselected;
// Signal emitted when track count changed
Wt::Signal<std::size_t> trackCountChanged;
constexpr std::size_t getCapacity() const { return _capacity; } constexpr std::size_t getCapacity() const { return _capacity; }
std::size_t getCount();
private: private:
void initTrackLists(); void initTrackLists();
+8 -2
View File
@@ -77,7 +77,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
if (artistIds.results.empty()) if (artistIds.results.empty())
return; 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) for (ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId); artistMap[typeStr].insert(artistId);
}; };
@@ -109,6 +109,13 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists(); 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) for (const auto& [role, artistIds] : artistMap)
{ {
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))}; std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
@@ -118,7 +125,6 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
artistTable->addWidget(std::move(artistsEntry)); artistTable->addWidget(std::move(artistsEntry));
} }
// TODO: save in DB and mean all this // TODO: save in DB and mean all this
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results) for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results)
{ {
+7
View File
@@ -103,6 +103,13 @@ namespace UserInterface::TrackListHelpers
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists(); 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) for (const auto& [role, artistIds] : artistMap)
{ {
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))}; std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};