Merge branch 'develop' for release v3.35.0
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
# 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!
|
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,23 +152,20 @@ 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 role;
|
||||||
|
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
|
||||||
{
|
{
|
||||||
std::string performerStr {key.to8Bit(true)};
|
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
|
||||||
std::string role;
|
StringUtils::capitalize(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)});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; });
|
[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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -117,42 +117,36 @@ 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...";
|
boost::system::error_code closeError;
|
||||||
{
|
_childStdout.close(closeError);
|
||||||
boost::system::error_code closeError;
|
if (closeError)
|
||||||
_childStdout.close(closeError);
|
LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
|
||||||
if (closeError)
|
|
||||||
LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
|
|
||||||
}
|
|
||||||
kill();
|
|
||||||
wait(true);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
if (!_finished)
|
||||||
ChildProcess::drain()
|
kill();
|
||||||
{
|
|
||||||
char buf[128];
|
|
||||||
|
|
||||||
while (boost::asio::read(_childStdout, boost::asio::buffer(buf)) > 0)
|
wait(true);
|
||||||
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,33 +178,20 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
readResult = ReadResult::EndOfFile;
|
||||||
|
_finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(readResult, bytesTransferred);
|
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
|
std::size_t
|
||||||
ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
||||||
{
|
{
|
||||||
@@ -220,7 +205,7 @@ ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
ChildProcess::finished()
|
ChildProcess::finished() const
|
||||||
{
|
{
|
||||||
return _finished;
|
return _finished;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ class MediaPlayer : public Wt::WTemplate
|
|||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
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;
|
||||||
@@ -123,9 +125,10 @@ class MediaPlayer : public Wt::WTemplate
|
|||||||
|
|
||||||
Wt::JSignal<std::string> _settingsLoaded;
|
Wt::JSignal<std::string> _settingsLoaded;
|
||||||
|
|
||||||
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
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)))};
|
||||||
|
|||||||
Reference in New Issue
Block a user