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
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:
-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
[![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!
+5 -1
View File
@@ -44,6 +44,10 @@
</div>
</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>
-4
View File
@@ -209,10 +209,6 @@
<!--Playqueue-->
<message id="Lms.PlayQueue.clear">Clear</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-full">Play Queue full!</message>
<message id="Lms.PlayQueue.radio-mode">Radio mode</message>
-4
View File
@@ -209,10 +209,6 @@
<!--Playqueue-->
<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.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-full">Liste de lecture pleine!</message>
<message id="Lms.PlayQueue.radio-mode">Mode radio</message>
-4
View File
@@ -209,10 +209,6 @@
<!--Playqueue-->
<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-full">Coda di riproduzione piena!</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 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() {
@@ -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<const char *>(&_buffer[0]), _bytesReadyCount);
_bytesReadyCount = 0;
_totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0;
}
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
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
{
-3
View File
@@ -40,9 +40,6 @@ namespace Av
Transcoder(Transcoder&&) = delete;
Transcoder& operator=(Transcoder&&) = delete;
using WaitCallback = std::function<void()>;
void asyncWaitForData(WaitCallback cb);
// non blocking calls
using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
+11 -13
View File
@@ -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<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(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)});
}
}
@@ -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();
}
@@ -19,6 +19,7 @@
#include "ListenBrainzScrobbler.hpp"
#include <tuple>
#include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
@@ -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;
});
}
@@ -19,6 +19,7 @@
#include "ListenBrainzScrobbler.hpp"
#include <tuple>
#include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
@@ -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;
});
}
@@ -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);
}
}
+96 -31
View File
@@ -1169,49 +1169,103 @@ handleGetArtistsRequest(RequestContext& context)
}
static
Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
std::vector<TrackId>
findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
{
// Mandatory params
const ArtistId artistId {getMandatoryParameterAs<ArtistId>(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<ArtistId> artistIds {Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, similarArtistCount)};
artistIds.push_back(artistId);
// Optional params
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)};
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<TrackId> 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<TrackId> 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<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);
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<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, " ")};
@@ -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<std::string>(parameters, "c");
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
@@ -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:
+28 -43
View File
@@ -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;
}
+1 -3
View File
@@ -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;
@@ -48,9 +48,7 @@ class IChildProcess
using ReadCallback = std::function<void(ReadResult, std::size_t)>;
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 bool finished() = 0;
virtual bool finished() const = 0;
};
+20 -4
View File
@@ -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<CoverResource>();
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")))};
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
Template* navbar {main->bindNew<Template>("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)
+9 -6
View File
@@ -210,12 +210,9 @@ MediaPlayer::MediaPlayer()
_title = bindNew<Wt::WText>("title");
_artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release");
{
Wt::WPushButton* playQueueBtn {bindNew<Wt::WPushButton>("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<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"});
_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
+7 -4
View File
@@ -101,7 +101,9 @@ class MediaPlayer : public Wt::WTemplate
void stop();
std::optional<Settings> 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<std::string> _settingsLoaded;
Wt::WText* _title {};
Wt::WAnchor* _release {};
Wt::WAnchor* _artist {};
Wt::WText* _title {};
Wt::WAnchor* _release {};
Wt::WAnchor* _artist {};
Wt::WPushButton* _playQueue {};
};
} // namespace UserInterface
+12 -20
View File
@@ -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<Database::TrackId>& trackIds)
clearTracks();
std::vector<Database::TrackId> 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<Database::TrackId>& trackIds)
{
const std::size_t nbAddedTracks {enqueueTracks(trackIds)};
enqueueTracks(trackIds);
if (!_trackPos)
loadTrack(0, true);
notifyAddedTracks(nbAddedTracks);
}
void
PlayQueue::playAtIndex(const std::vector<Database::TrackId>& 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
+4
View File
@@ -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<std::size_t> trackCountChanged;
constexpr std::size_t getCapacity() const { return _capacity; }
std::size_t getCount();
private:
void initTrackLists();
+8 -2
View File
@@ -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<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));
}
// TODO: save in DB and mean all this
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");
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<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};