Merge branch 'develop' for release v3.35.0
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,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!
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)))};
|
||||
|
||||
Reference in New Issue
Block a user