Merge branch 'develop' for release v3.31.0
This commit is contained in:
@@ -13,7 +13,7 @@ ARG BUILD_PACKAGES="\
|
||||
taglib \
|
||||
wt"
|
||||
|
||||
RUN pacman -Syy
|
||||
RUN pacman -Syu --noconfirm
|
||||
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
|
||||
|
||||
# LMS
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
|
||||
<message id="Lms.Admin.User.user-create">New user</message>
|
||||
<message id="Lms.Admin.User.user-created">New user created!</message>
|
||||
<message id="Lms.Admin.User.user-edit">Edit user '{1}'</message>
|
||||
<message id="Lms.Admin.User.user-edit">User '{1}'</message>
|
||||
<message id="Lms.Admin.User.user-updated">User updated!</message>
|
||||
|
||||
<!--Wizard-->
|
||||
@@ -133,6 +133,7 @@
|
||||
<message id="Lms.Explore.recently-modified">Recently modified</message>
|
||||
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.search">Search</message>
|
||||
<message id="Lms.Explore.star">Star</message>
|
||||
<message id="Lms.Explore.starred">Starred</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
@@ -187,7 +188,6 @@
|
||||
<!--PlayHistory-->
|
||||
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
||||
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
||||
@@ -208,6 +208,7 @@
|
||||
<message id="Lms.Settings.scrobbling.scrobbler.internal">Internal</message>
|
||||
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
||||
<message id="Lms.Settings.scrobbling.listenbrainz-token">ListenBrainz token</message>
|
||||
<message id="Lms.Settings.settings">Settings</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
|
||||
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.search">Rechercher</message>
|
||||
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
||||
<message id="Lms.Explore.starred">Favoris</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
@@ -207,6 +208,7 @@
|
||||
<message id="Lms.Settings.scrobbling.scrobbler.internal">Interne</message>
|
||||
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
||||
<message id="Lms.Settings.scrobbling.listenbrainz-token">Jeton ListenBrainz</message>
|
||||
<message id="Lms.Settings.settings">Paramètres</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
|
||||
|
||||
@@ -133,9 +133,9 @@
|
||||
|
||||
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
|
||||
<message id="Lms.Explore.releases">Album</message>
|
||||
<message id="Lms.Explore.search">Ricerca</message>
|
||||
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
|
||||
<message id="Lms.Explore.starred">Preferiti</message>
|
||||
|
||||
<message id="Lms.Explore.tracks">Tracce</message>
|
||||
<message id="Lms.Explore.type">Tipo</message>
|
||||
<message id="Lms.Explore.unstar">Rimuovi dai preferiti</message>
|
||||
@@ -207,6 +207,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
<message id="Lms.Explore.recently-modified">最近更改</message>
|
||||
<message id="Lms.Explore.recently-played">最近播放</message>
|
||||
<message id="Lms.Explore.releases">专辑</message>
|
||||
<message id="Lms.Explore.search">搜索</message>
|
||||
<message id="Lms.Explore.star">收藏</message>
|
||||
<message id="Lms.Explore.starred">已收藏</message>
|
||||
<message id="Lms.Explore.tracklists">播放列表</message>
|
||||
@@ -201,6 +202,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||
|
||||
@@ -80,3 +80,6 @@ cover-preferred-file-names = ("cover", "front" );
|
||||
|
||||
# Set to true if you want to hide duplicate tracks
|
||||
scanner-skip-duplicate-recording-mbid = false;
|
||||
|
||||
# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate'
|
||||
scanner-parser-read-style = "accurate";
|
||||
|
||||
+109
-73
@@ -16,58 +16,112 @@ const Mode = {
|
||||
Object.freeze(Mode);
|
||||
|
||||
LMS.mediaplayer = function () {
|
||||
let _root = {};
|
||||
let _elems = {};
|
||||
let _offset = 0;
|
||||
let _trackId = null;
|
||||
let _duration = 0;
|
||||
let _audioNativeSrc;
|
||||
let _audioTranscodeSrc;
|
||||
let _settings = {};
|
||||
let _playedDuration = 0;
|
||||
let _lastStartPlaying = null;
|
||||
let _audioIsInit = false;
|
||||
let _pendingTrackParameters = null;
|
||||
let _gainNode = null;
|
||||
let _audioCtx = null;
|
||||
|
||||
var _root = {};
|
||||
var _elems = {};
|
||||
var _offset = 0;
|
||||
var _trackId = null;
|
||||
var _duration = 0;
|
||||
var _audioNativeSrc;
|
||||
var _audioTranscodeSrc;
|
||||
var _settings = {};
|
||||
var _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
var _gainNode = _audioCtx.createGain();
|
||||
var _playedDuration = 0;
|
||||
var _lastStartPlaying = null;
|
||||
let _unlock = function() {
|
||||
document.removeEventListener("touchstart", _unlock);
|
||||
document.removeEventListener("touchend", _unlock);
|
||||
document.removeEventListener("click", _unlock);
|
||||
_initAudioCtx();
|
||||
};
|
||||
|
||||
var _updateControls = function() {
|
||||
document.addEventListener("touchstart", _unlock);
|
||||
document.addEventListener("touchend", _unlock);
|
||||
document.addEventListener("click", _unlock);
|
||||
|
||||
let _initAudioCtx = function() {
|
||||
if (_audioIsInit) {
|
||||
_audioCtx.resume(); // not sure of this
|
||||
return;
|
||||
}
|
||||
|
||||
_audioIsInit = true;
|
||||
|
||||
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
_gainNode = _audioCtx.createGain();
|
||||
source = _audioCtx.createMediaElementSource(_elems.audio);
|
||||
source.connect(_gainNode);
|
||||
_gainNode.connect(_audioCtx.destination);
|
||||
_audioCtx.resume(); // not sure of this
|
||||
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.setActionHandler("play", function() {
|
||||
_playPause();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("pause", function() {
|
||||
_playPause();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("previoustrack", function() {
|
||||
_playPrevious();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("nexttrack", function() {
|
||||
_playNext();
|
||||
});
|
||||
}
|
||||
|
||||
if (_pendingTrackParameters != null) {
|
||||
_applyAudioTrackParameters(_pendingTrackParameters);
|
||||
_pendingTrackParameters = null;
|
||||
}
|
||||
}
|
||||
|
||||
let _updateControls = function() {
|
||||
const pauseClass = "fa-pause";
|
||||
const playClass = "fa-play";
|
||||
|
||||
if (_elems.audio.paused) {
|
||||
_elems.playpause.firstElementChild.classList.remove(pauseClass);
|
||||
_elems.playpause.firstElementChild.classList.add(playClass);
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.playbackState = "paused";
|
||||
}
|
||||
}
|
||||
else {
|
||||
_elems.playpause.firstElementChild.classList.remove(playClass);
|
||||
_elems.playpause.firstElementChild.classList.add(pauseClass);
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.playbackState = "playing";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _startTimer = function() {
|
||||
let _startTimer = function() {
|
||||
if (_lastStartPlaying == null)
|
||||
Wt.emit(_root, "scrobbleListenNow", _trackId);
|
||||
_lastStartPlaying = Date.now();
|
||||
}
|
||||
|
||||
var _stopTimer = function() {
|
||||
let _stopTimer = function() {
|
||||
if (_lastStartPlaying != null) {
|
||||
_playedDuration += Date.now() - _lastStartPlaying;
|
||||
}
|
||||
}
|
||||
|
||||
var _resetTimer = function() {
|
||||
let _resetTimer = function() {
|
||||
if (_lastStartPlaying != null)
|
||||
Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration);
|
||||
_playedDuration = 0;
|
||||
_lastStartPlaying = null;
|
||||
}
|
||||
|
||||
var _durationToString = function (duration) {
|
||||
var minutes = parseInt(duration / 60, 10);
|
||||
var seconds = parseInt(duration, 10) % 60;
|
||||
let _durationToString = function (duration) {
|
||||
let minutes = parseInt(duration / 60, 10);
|
||||
let seconds = parseInt(duration, 10) % 60;
|
||||
|
||||
var res = "";
|
||||
let res = "";
|
||||
|
||||
res += minutes + ":";
|
||||
res += (seconds < 10 ? "0" + seconds : seconds);
|
||||
@@ -75,21 +129,15 @@ LMS.mediaplayer = function () {
|
||||
return res;
|
||||
}
|
||||
|
||||
var _playTrack = function() {
|
||||
var playPromise = _elems.audio.play();
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(_ => {
|
||||
// Automatic playback started
|
||||
})
|
||||
.catch(error => {
|
||||
// Auto-play was prevented
|
||||
});
|
||||
}
|
||||
let _playTrack = function() {
|
||||
_elems.audio.play()
|
||||
.then(_ => {})
|
||||
.catch(error => { console.log("Cannot play audio: " + error); });
|
||||
}
|
||||
|
||||
var _playPause = function() {
|
||||
_audioCtx.resume();
|
||||
let _playPause = function() {
|
||||
_initAudioCtx();
|
||||
|
||||
if (_elems.audio.paused && _elems.audio.children.length > 0) {
|
||||
_playTrack();
|
||||
}
|
||||
@@ -97,25 +145,17 @@ LMS.mediaplayer = function () {
|
||||
_elems.audio.pause();
|
||||
}
|
||||
|
||||
var _playPrevious = function() {
|
||||
_audioCtx.resume();
|
||||
_requestPreviousTrack();
|
||||
}
|
||||
|
||||
var _playNext = function() {
|
||||
_audioCtx.resume();
|
||||
_requestNextTrack();
|
||||
}
|
||||
|
||||
var _requestPreviousTrack = function() {
|
||||
let _playPrevious = function() {
|
||||
_initAudioCtx();
|
||||
Wt.emit(_root, "playPrevious");
|
||||
}
|
||||
|
||||
var _requestNextTrack = function() {
|
||||
let _playNext = function() {
|
||||
_initAudioCtx();
|
||||
Wt.emit(_root, "playNext");
|
||||
}
|
||||
|
||||
var _initVolume = function() {
|
||||
let _initVolume = function() {
|
||||
if (typeof(Storage) !== "undefined" && localStorage.volume) {
|
||||
_elems.volumeslider.value = Number(localStorage.volume);
|
||||
}
|
||||
@@ -123,7 +163,7 @@ LMS.mediaplayer = function () {
|
||||
_setVolume(_elems.volumeslider.value);
|
||||
}
|
||||
|
||||
var _initDefaultSettings = function(defaultSettings) {
|
||||
let _initDefaultSettings = function(defaultSettings) {
|
||||
if (typeof(Storage) !== "undefined" && localStorage.settings) {
|
||||
_settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
|
||||
}
|
||||
@@ -134,7 +174,7 @@ LMS.mediaplayer = function () {
|
||||
Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
|
||||
}
|
||||
|
||||
var _setVolume = function(volume) {
|
||||
let _setVolume = function(volume) {
|
||||
_elems.lastvolume = _elems.audio.volume;
|
||||
|
||||
_elems.audio.volume = volume;
|
||||
@@ -162,11 +202,11 @@ LMS.mediaplayer = function () {
|
||||
}
|
||||
}
|
||||
|
||||
var _setReplayGain = function (replayGain) {
|
||||
let _setReplayGain = function (replayGain) {
|
||||
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
|
||||
}
|
||||
|
||||
var init = function(root, defaultSettings) {
|
||||
let init = function(root, defaultSettings) {
|
||||
_root = root;
|
||||
|
||||
_elems.audio = document.getElementById("lms-mp-audio");
|
||||
@@ -181,10 +221,6 @@ LMS.mediaplayer = function () {
|
||||
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
||||
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
|
||||
|
||||
var source = _audioCtx.createMediaElementSource(_elems.audio);
|
||||
source.connect(_gainNode);
|
||||
_gainNode.connect(_audioCtx.destination);
|
||||
|
||||
_elems.playpause.addEventListener("click", function() {
|
||||
_playPause();
|
||||
});
|
||||
@@ -196,7 +232,7 @@ LMS.mediaplayer = function () {
|
||||
_playNext();
|
||||
});
|
||||
_elems.seek.addEventListener("change", function() {
|
||||
_audioCtx.resume();
|
||||
_initAudioCtx();
|
||||
let mode = _getAudioMode();
|
||||
if (!mode)
|
||||
return;
|
||||
@@ -285,30 +321,21 @@ LMS.mediaplayer = function () {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler("previoustrack", function() {
|
||||
_requestPreviousTrack();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("nexttrack", function() {
|
||||
_requestNextTrack();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var _removeAudioSources = function() {
|
||||
let _removeAudioSources = function() {
|
||||
while ( _elems.audio.lastElementChild) {
|
||||
_elems.audio.removeChild( _elems.audio.lastElementChild);
|
||||
}
|
||||
}
|
||||
|
||||
var _addAudioSource = function(audioSrc) {
|
||||
let _addAudioSource = function(audioSrc) {
|
||||
let source = document.createElement('source');
|
||||
source.src = audioSrc;
|
||||
_elems.audio.appendChild(source);
|
||||
}
|
||||
|
||||
var _getAudioMode = function() {
|
||||
let _getAudioMode = function() {
|
||||
if (_elems.audio.currentSrc) {
|
||||
if (_elems.audio.currentSrc.includes("format"))
|
||||
return Mode.Transcode;
|
||||
@@ -319,7 +346,7 @@ LMS.mediaplayer = function () {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var loadTrack = function(params, autoplay) {
|
||||
let loadTrack = function(params, autoplay) {
|
||||
_stopTimer();
|
||||
_resetTimer();
|
||||
|
||||
@@ -343,15 +370,24 @@ LMS.mediaplayer = function () {
|
||||
}
|
||||
_elems.audio.load();
|
||||
|
||||
_setReplayGain(params.replayGain);
|
||||
|
||||
_elems.curtime.innerHTML = _durationToString(_offset);
|
||||
_elems.duration.innerHTML = _durationToString(_duration);
|
||||
|
||||
if (!_audioIsInit) {
|
||||
_pendingTrackParameters = params;
|
||||
return;
|
||||
}
|
||||
|
||||
_applyAudioTrackParameters(params);
|
||||
|
||||
if (autoplay && _audioCtx.state == "running")
|
||||
_playTrack();
|
||||
}
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
let _applyAudioTrackParameters = function(params)
|
||||
{
|
||||
_setReplayGain(params.replayGain);
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: params.title,
|
||||
artist: params.artist,
|
||||
@@ -361,11 +397,11 @@ LMS.mediaplayer = function () {
|
||||
}
|
||||
}
|
||||
|
||||
var stop = function() {
|
||||
let stop = function() {
|
||||
_elems.audio.pause();
|
||||
}
|
||||
|
||||
var setSettings = function(settings) {
|
||||
let setSettings = function(settings) {
|
||||
_settings = settings;
|
||||
|
||||
if (typeof(Storage) !== "undefined") {
|
||||
|
||||
@@ -178,7 +178,7 @@ AudioFile::getBestStream() const
|
||||
}
|
||||
|
||||
bool
|
||||
AudioFile::hasAttachedPictures(void) const
|
||||
AudioFile::hasAttachedPictures() const
|
||||
{
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
{
|
||||
|
||||
@@ -18,32 +18,48 @@
|
||||
*/
|
||||
|
||||
#include "TranscodeResourceHandler.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::size_t
|
||||
doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
|
||||
{
|
||||
const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::milliseconds>(inputFileParameters.duration).count()) / 1000};
|
||||
return estimatedContentLength;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<IResourceHandler>
|
||||
createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters)
|
||||
createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
|
||||
{
|
||||
return std::make_unique<TranscodeResourceHandler>(trackPath, parameters);
|
||||
return std::make_unique<TranscodeResourceHandler>(inputFileParameters, transcodeParameters, estimateContentLength);
|
||||
}
|
||||
|
||||
// TODO set some nice HTTP return code
|
||||
|
||||
TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters)
|
||||
: _transcoder {trackPath, parameters}
|
||||
TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
|
||||
: _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt}
|
||||
, _transcoder {inputFileParameters, transcodeParameters}
|
||||
{
|
||||
if (_estimatedContentLength)
|
||||
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
|
||||
}
|
||||
|
||||
Wt::Http::ResponseContinuation*
|
||||
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||
{
|
||||
if (_estimatedContentLength)
|
||||
response.setContentLength(*_estimatedContentLength);
|
||||
response.setMimeType(_transcoder.getOutputMimeType());
|
||||
|
||||
if (_nbBytesReady > 0)
|
||||
if (_bytesReadyCount > 0)
|
||||
{
|
||||
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _nbBytesReady);
|
||||
_nbBytesReady = 0;
|
||||
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
|
||||
_bytesReadyCount = 0;
|
||||
_totalServedByteCount += _bytesReadyCount;
|
||||
}
|
||||
|
||||
if (!_transcoder.finished())
|
||||
@@ -52,13 +68,30 @@ namespace Av
|
||||
continuation->waitForMoreData();
|
||||
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
||||
{
|
||||
assert(_nbBytesReady == 0);
|
||||
_nbBytesReady = nbBytesRead;
|
||||
assert(_bytesReadyCount == 0);
|
||||
_bytesReadyCount = nbBytesRead;
|
||||
continuation->haveMoreData();
|
||||
});
|
||||
|
||||
return continuation;
|
||||
}
|
||||
else
|
||||
{
|
||||
// pad with 0 if necessary as duration may not be accurate
|
||||
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
|
||||
{
|
||||
const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount};
|
||||
|
||||
LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes";
|
||||
|
||||
for (std::size_t i {}; i < padSize; ++i)
|
||||
response.out().put(0);
|
||||
|
||||
_totalServedByteCount += padSize;
|
||||
}
|
||||
|
||||
LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "utils/IResourceHandler.hpp"
|
||||
@@ -28,19 +29,19 @@
|
||||
|
||||
namespace Av
|
||||
{
|
||||
|
||||
class TranscodeResourceHandler final : public IResourceHandler
|
||||
{
|
||||
public:
|
||||
TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
||||
TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
|
||||
|
||||
private:
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
||||
|
||||
static constexpr std::size_t _chunkSize {32768};
|
||||
std::optional<std::size_t> _estimatedContentLength;
|
||||
std::array<std::byte, _chunkSize> _buffer;
|
||||
std::size_t _nbBytesReady {};
|
||||
const std::filesystem::path _trackPath;
|
||||
std::size_t _bytesReadyCount {};
|
||||
std::size_t _totalServedByteCount {};
|
||||
Transcoder _transcoder;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
namespace Av {
|
||||
|
||||
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - "
|
||||
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - "
|
||||
|
||||
static std::atomic<size_t> globalId {};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
@@ -43,10 +43,10 @@ Transcoder::init()
|
||||
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
|
||||
}
|
||||
|
||||
Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters)
|
||||
: _id {globalId++}
|
||||
, _filePath {filePath}
|
||||
, _parameters {parameters}
|
||||
Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
|
||||
: _debugId {globalId++}
|
||||
, _inputFileParameters {inputFileParameters}
|
||||
, _transcodeParameters {transcodeParameters}
|
||||
{
|
||||
start();
|
||||
}
|
||||
@@ -61,17 +61,17 @@ Transcoder::start()
|
||||
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(_filePath))
|
||||
throw Exception {"File '" + _filePath.string() + "' does not exist!"};
|
||||
else if (!std::filesystem::is_regular_file( _filePath) )
|
||||
throw Exception {"File '" + _filePath.string() + "' is not regular!"};
|
||||
if (!std::filesystem::exists(_inputFileParameters.trackPath))
|
||||
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
|
||||
else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
|
||||
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
throw Exception {"File error '" + _filePath.string() + "': " + e.what()};
|
||||
throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()};
|
||||
}
|
||||
|
||||
LOG(INFO) << "Transcoding file '" << _filePath.string() << "'";
|
||||
LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'";
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
@@ -90,22 +90,22 @@ Transcoder::start()
|
||||
args.emplace_back("-ss");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_parameters.offset.count() / float {1000});
|
||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000});
|
||||
args.emplace_back(oss.str());
|
||||
}
|
||||
|
||||
// Input file
|
||||
args.emplace_back("-i");
|
||||
args.emplace_back(_filePath.string());
|
||||
args.emplace_back(_inputFileParameters.trackPath.string());
|
||||
|
||||
// Stream mapping, if set
|
||||
if (_parameters.stream)
|
||||
if (_transcodeParameters.stream)
|
||||
{
|
||||
args.emplace_back("-map");
|
||||
args.emplace_back("0:" + std::to_string(*_parameters.stream));
|
||||
args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream));
|
||||
}
|
||||
|
||||
if (_parameters.stripMetadata)
|
||||
if (_transcodeParameters.stripMetadata)
|
||||
{
|
||||
// Strip metadata
|
||||
args.emplace_back("-map_metadata");
|
||||
@@ -117,10 +117,10 @@ Transcoder::start()
|
||||
|
||||
// Output bitrates
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(_parameters.bitrate));
|
||||
args.emplace_back(std::to_string(_transcodeParameters.bitrate));
|
||||
|
||||
// Codecs and formats
|
||||
switch (_parameters.format)
|
||||
switch (_transcodeParameters.format)
|
||||
{
|
||||
case Format::MP3:
|
||||
args.emplace_back("-f");
|
||||
@@ -156,10 +156,10 @@ Transcoder::start()
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_parameters.format)) + ")"};
|
||||
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_transcodeParameters.format)) + ")"};
|
||||
}
|
||||
|
||||
_outputMimeType = formatToMimetype(_parameters.format);
|
||||
_outputMimeType = formatToMimetype(_transcodeParameters.format);
|
||||
|
||||
args.emplace_back("pipe:1");
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Av
|
||||
class Transcoder
|
||||
{
|
||||
public:
|
||||
Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters);
|
||||
Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters);
|
||||
~Transcoder();
|
||||
|
||||
Transcoder(const Transcoder&) = delete;
|
||||
@@ -49,7 +49,7 @@ namespace Av
|
||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
|
||||
|
||||
const std::string& getOutputMimeType() const { return _outputMimeType; }
|
||||
const TranscodeParameters& getParameters() const { return _parameters; }
|
||||
const TranscodeParameters& getParameters() const { return _transcodeParameters; }
|
||||
|
||||
bool finished() const;
|
||||
|
||||
@@ -58,9 +58,9 @@ namespace Av
|
||||
|
||||
void start();
|
||||
|
||||
const std::size_t _id {};
|
||||
const std::filesystem::path _filePath;
|
||||
const TranscodeParameters _parameters;
|
||||
const std::size_t _debugId {};
|
||||
const InputFileParameters _inputFileParameters;
|
||||
const TranscodeParameters _transcodeParameters;
|
||||
|
||||
std::unique_ptr<IChildProcess> _childProcess;
|
||||
|
||||
|
||||
@@ -20,12 +20,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
struct InputFileParameters
|
||||
{
|
||||
std::filesystem::path trackPath;
|
||||
std::chrono::milliseconds duration;
|
||||
};
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
Format format;
|
||||
|
||||
@@ -19,15 +19,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "utils/IResourceHandler.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
struct InputFileParameters;
|
||||
struct TranscodeParameters;
|
||||
|
||||
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
||||
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ endif()
|
||||
|
||||
add_library(lmsmetadata SHARED
|
||||
impl/AvFormatParser.cpp
|
||||
impl/Factory.cpp
|
||||
impl/TagLibParser.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "metadata/AvFormatParser.hpp"
|
||||
#include "AvFormatParser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "metadata/IParser.hpp"
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "AvFormatParser.hpp"
|
||||
#include "TagLibParser.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
std::unique_ptr<IParser>
|
||||
createParser(ParserType parserType, ParserReadStyle parserReadStyle)
|
||||
{
|
||||
switch (parserType)
|
||||
{
|
||||
case ParserType::TagLib:
|
||||
LMS_LOG(METADATA, INFO) << "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle);
|
||||
return std::make_unique<TagLibParser>(parserReadStyle);
|
||||
case ParserType::AvFormat:
|
||||
LMS_LOG(METADATA, INFO) << "Creating AvFormat parser";
|
||||
return std::make_unique<AvFormatParser>();
|
||||
}
|
||||
|
||||
throw LmsException {"Unhandled parser type"};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
#include "TagLibParser.hpp"
|
||||
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
@@ -33,7 +33,10 @@
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -145,6 +148,26 @@ getAlbum(const TagLib::PropertyMap& properties)
|
||||
return Album {std::move(albumName.front()), albumMBID.front()};
|
||||
}
|
||||
|
||||
static
|
||||
TagLib::AudioProperties::ReadStyle
|
||||
readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
|
||||
case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
|
||||
case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||
}
|
||||
|
||||
throw LmsException {"Cannot convert read style"};
|
||||
}
|
||||
|
||||
|
||||
TagLibParser::TagLibParser(ParserReadStyle readStyle)
|
||||
: _readStyle {readStyleToTagLibReadStyle(readStyle)}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
|
||||
{
|
||||
@@ -267,7 +290,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
TagLib::FileRef f {p.string().c_str(),
|
||||
true, // read audio properties
|
||||
TagLib::AudioProperties::Fast}; // TODO parametrize this
|
||||
_readStyle};
|
||||
|
||||
if (f.isNull())
|
||||
{
|
||||
@@ -286,7 +309,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
const TagLib::AudioProperties *properties {f.audioProperties() };
|
||||
|
||||
track.duration = std::chrono::milliseconds {properties->length() * 1000};
|
||||
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()};
|
||||
|
||||
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
|
||||
track.audioStreams = {std::move(audioStream)};
|
||||
|
||||
+6
-1
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <taglib/audioproperties.h>
|
||||
#include "metadata/IParser.hpp"
|
||||
|
||||
namespace TagLib
|
||||
@@ -32,10 +33,14 @@ namespace MetaData
|
||||
// Parse that makes use of AvFormat
|
||||
class TagLibParser : public IParser
|
||||
{
|
||||
public:
|
||||
TagLibParser(ParserReadStyle readStyle);
|
||||
|
||||
private:
|
||||
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
||||
|
||||
void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug);
|
||||
|
||||
const TagLib::AudioProperties::ReadStyle _readStyle;
|
||||
};
|
||||
|
||||
} // namespace MetaData
|
||||
@@ -23,6 +23,8 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate
|
||||
@@ -56,5 +58,18 @@ namespace MetaData::Utils
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string_view
|
||||
readStyleToString(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast: return "fast";
|
||||
case ParserReadStyle::Average: return "average";
|
||||
case ParserReadStyle::Accurate: return "accurate";
|
||||
}
|
||||
|
||||
throw LmsException {"Unknown read style"};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "metadata/IParser.hpp"
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate parseDate(const std::string& dateStr);
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,5 +100,18 @@ namespace MetaData
|
||||
std::set<std::string> _clusterTypeNames;
|
||||
};
|
||||
|
||||
enum class ParserType
|
||||
{
|
||||
TagLib,
|
||||
AvFormat,
|
||||
};
|
||||
|
||||
enum class ParserReadStyle
|
||||
{
|
||||
Fast,
|
||||
Average,
|
||||
Accurate,
|
||||
};
|
||||
std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
|
||||
} // namespace MetaData
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::si
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Cluster::pointer& cluster : queryRes)
|
||||
for (Cluster::pointer cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
|
||||
@@ -591,6 +591,15 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV36(Session& session)
|
||||
{
|
||||
// Increased precision for track durations (now in milliseconds instead of secodns)
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
void
|
||||
doDbMigration(Session& session)
|
||||
{
|
||||
@@ -633,6 +642,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
{33, migrateFromV33},
|
||||
{34, migrateFromV34},
|
||||
{35, migrateFromV35},
|
||||
{36, migrateFromV36},
|
||||
};
|
||||
|
||||
while (1)
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {36};
|
||||
static constexpr Version LMS_DATABASE_VERSION {37};
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -264,7 +264,7 @@ Release::find(Session& session, const FindParameters& params)
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Release::getTotalTrack(void) const
|
||||
Release::getTotalTrack() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -276,7 +276,7 @@ Release::getTotalTrack(void) const
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Release::getTotalDisc(void) const
|
||||
Release::getTotalDisc() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -287,6 +287,18 @@ Release::getTotalDisc(void) const
|
||||
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
int res {session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId())};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<int>
|
||||
Release::getReleaseYear(bool original) const
|
||||
{
|
||||
|
||||
@@ -58,7 +58,7 @@ WhereClause::Or(const WhereClause& otherClause)
|
||||
}
|
||||
|
||||
std::string
|
||||
WhereClause::get(void) const
|
||||
WhereClause::get() const
|
||||
{
|
||||
if (!_clause.empty())
|
||||
return "WHERE " + _clause;
|
||||
@@ -176,7 +176,7 @@ FromClause::get() const
|
||||
}
|
||||
|
||||
std::string
|
||||
SqlQuery::get(void) const
|
||||
SqlQuery::get() const
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class WhereClause
|
||||
WhereClause& bind(const std::string& arg);
|
||||
|
||||
std::string get() const;
|
||||
const std::list<std::string>& getBindArgs(void) const {return _bindArgs;}
|
||||
const std::list<std::string>& getBindArgs() const {return _bindArgs;}
|
||||
|
||||
private:
|
||||
std::string _clause; // WHERE clause
|
||||
@@ -101,15 +101,15 @@ class FromClause
|
||||
class SqlQuery
|
||||
{
|
||||
public:
|
||||
SelectStatement& select(void) { return _selectStatement;}
|
||||
SelectStatement& select() { return _selectStatement;}
|
||||
SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; }
|
||||
FromClause& from(void) { return _fromClause; }
|
||||
FromClause& from() { return _fromClause; }
|
||||
FromClause& from(const std::string& clause) { _whereClause = WhereClause(clause); return _fromClause; }
|
||||
InnerJoinClause& innerJoin(void) { return _innerJoinClause; }
|
||||
WhereClause& where(void) { return _whereClause; }
|
||||
const WhereClause& where(void) const { return _whereClause; }
|
||||
GroupByStatement& groupBy(void) { return _groupByStatement; }
|
||||
const GroupByStatement& groupBy(void) const { return _groupByStatement; }
|
||||
InnerJoinClause& innerJoin() { return _innerJoinClause; }
|
||||
WhereClause& where() { return _whereClause; }
|
||||
const WhereClause& where() const { return _whereClause; }
|
||||
GroupByStatement& groupBy() { return _groupByStatement; }
|
||||
const GroupByStatement& groupBy() const { return _groupByStatement; }
|
||||
|
||||
std::string get() const;
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ class Release : public Object<Release, ReleaseId>
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
std::optional<std::size_t> getTotalTrack() const;
|
||||
std::optional<std::size_t> getTotalDisc() const;
|
||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
|
||||
|
||||
@@ -207,5 +207,3 @@ TEST_F(DatabaseFixture, Common_subRange)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -464,3 +464,47 @@ TEST_F(DatabaseFixture, Release_artist)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||
{
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track2.get().modify()->setDiscNumber(6);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "services/recommendation/IRecommendationService.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
@@ -255,16 +255,29 @@ createScannerService(Db& db, Recommendation::IRecommendationService& recommendat
|
||||
return std::make_unique<ScannerService>(db, recommendationService);
|
||||
}
|
||||
|
||||
MetaData::ParserReadStyle
|
||||
getParserReadStyle()
|
||||
{
|
||||
std::string_view readStyle {Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate")};
|
||||
|
||||
if (readStyle == "fast")
|
||||
return MetaData::ParserReadStyle::Fast;
|
||||
else if (readStyle == "average")
|
||||
return MetaData::ParserReadStyle::Average;
|
||||
else if (readStyle == "accurate")
|
||||
return MetaData::ParserReadStyle::Accurate;
|
||||
|
||||
throw LmsException {"Invalid value for 'scanner-parser-read-style'"};
|
||||
}
|
||||
|
||||
ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||
: _recommendationService {recommendationService}
|
||||
, _skipDuplicateRecordingMBID {Service<IConfig>::get()->getBool("scanner-skip-duplicate-recording-mbid", false)}
|
||||
, _dbSession {db}
|
||||
, _metadataParser {MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle())} // For now, always use TagLib
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "skipDuplicateRecordingMBID = " << _skipDuplicateRecordingMBID;
|
||||
|
||||
// For now, always use TagLib
|
||||
_metadataParser = std::make_unique<MetaData::TagLibParser>();
|
||||
|
||||
_ioService.setThreadCount(1);
|
||||
|
||||
refreshScanSettings();
|
||||
|
||||
@@ -54,8 +54,9 @@ userTranscodeFormatToAvFormat(AudioFormat format)
|
||||
|
||||
struct StreamParameters
|
||||
{
|
||||
std::filesystem::path trackPath;
|
||||
Av::InputFileParameters inputFileParameters;
|
||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||
bool estimateContentLength {};
|
||||
};
|
||||
|
||||
static
|
||||
@@ -68,9 +69,12 @@ getStreamParameters(RequestContext& context)
|
||||
// Optional params
|
||||
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
|
||||
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
|
||||
bool estimateContentLength {getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false)};
|
||||
|
||||
StreamParameters parameters;
|
||||
|
||||
parameters.estimateContentLength = estimateContentLength;
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
{
|
||||
@@ -78,7 +82,8 @@ getStreamParameters(RequestContext& context)
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
parameters.trackPath = track->getPath();
|
||||
parameters.inputFileParameters.trackPath = track->getPath();
|
||||
parameters.inputFileParameters.duration = track->getDuration();
|
||||
}
|
||||
|
||||
{
|
||||
@@ -155,9 +160,9 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http
|
||||
{
|
||||
StreamParameters streamParameters {getStreamParameters(context)};
|
||||
if (streamParameters.transcodeParameters)
|
||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters);
|
||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
||||
else
|
||||
resourceHandler = createFileResourceHandler(streamParameters.trackPath);
|
||||
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -136,17 +136,4 @@ namespace StringUtils
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
if (str == "true")
|
||||
return true;
|
||||
else if (str == "false")
|
||||
return false;
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,5 @@ namespace StringUtils
|
||||
template<>
|
||||
std::optional<Database::TrackListId>
|
||||
readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str);
|
||||
}
|
||||
|
||||
|
||||
@@ -872,6 +872,29 @@ handleGetAlbumRequest(RequestContext& context)
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetSongRequest(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
TrackId id {getMandatoryParameterAs<TrackId>(context.parameters, "id")};
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
const Track::pointer track {Track::find(context.dbSession, id)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
User::pointer user {User::find(context.dbSession, context.userId)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
response.addNode("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetArtistRequest(RequestContext& context)
|
||||
@@ -1835,7 +1858,7 @@ static const std::unordered_map<std::string, RequestEntryPointInfo> requestEntry
|
||||
{"/getArtists", {handleGetArtistsRequest}},
|
||||
{"/getArtist", {handleGetArtistRequest}},
|
||||
{"/getAlbum", {handleGetAlbumRequest}},
|
||||
{"/getSong", {handleNotImplemented}},
|
||||
{"/getSong", {handleGetSongRequest}},
|
||||
{"/getVideos", {handleNotImplemented}},
|
||||
{"/getArtistInfo", {handleGetArtistInfoRequest}},
|
||||
{"/getArtistInfo2", {handleGetArtistInfo2Request}},
|
||||
|
||||
@@ -117,6 +117,7 @@ Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||
|
||||
responseNode.setAttribute("status", "ok");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -129,6 +130,7 @@ Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& err
|
||||
|
||||
responseNode.setAttribute("status", "failed");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||
|
||||
Node& errorNode {responseNode.createChild("error")};
|
||||
errorNode.setAttribute("code", std::to_string(static_cast<int>(error.getCode())));
|
||||
|
||||
@@ -168,7 +168,7 @@ ChildProcess::wait(bool block)
|
||||
|
||||
|
||||
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());
|
||||
|
||||
|
||||
@@ -63,6 +63,18 @@ readAs(std::string_view str)
|
||||
return std::string {str};
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
if (str == "1" || str == "true")
|
||||
return true;
|
||||
else if (str == "0" || str == "false")
|
||||
return false;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
splitStringCopy(std::string_view string, std::string_view separators)
|
||||
{
|
||||
|
||||
@@ -85,6 +85,11 @@ template<>
|
||||
std::optional<std::string>
|
||||
readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||
|
||||
@@ -109,3 +109,12 @@ TEST(StringUtils, escapeString)
|
||||
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
||||
}
|
||||
|
||||
TEST(StringUtils, readAs)
|
||||
{
|
||||
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
|
||||
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
|
||||
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
|
||||
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
|
||||
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
|
||||
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ add_executable(lms
|
||||
ui/explore/DatabaseCollectorBase.cpp
|
||||
ui/explore/Explore.cpp
|
||||
ui/explore/Filters.cpp
|
||||
ui/explore/PlayQueueController.cpp
|
||||
ui/explore/ReleaseCollector.cpp
|
||||
ui/explore/ReleaseListHelpers.cpp
|
||||
ui/explore/ReleasesView.cpp
|
||||
|
||||
@@ -64,6 +64,48 @@
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
static
|
||||
std::shared_ptr<Wt::WMessageResourceBundle>
|
||||
createMessageResourceBundle()
|
||||
{
|
||||
const std::string appRoot {Wt::WApplication::appRoot()};
|
||||
|
||||
auto res {std::make_shared<Wt::WMessageResourceBundle>()};
|
||||
res->use(appRoot + "admin-database");
|
||||
res->use(appRoot + "admin-initwizard");
|
||||
res->use(appRoot + "admin-scannercontroller");
|
||||
res->use(appRoot + "admin-user");
|
||||
res->use(appRoot + "admin-users");
|
||||
res->use(appRoot + "artist");
|
||||
res->use(appRoot + "artists");
|
||||
res->use(appRoot + "error");
|
||||
res->use(appRoot + "explore");
|
||||
res->use(appRoot + "login");
|
||||
res->use(appRoot + "main");
|
||||
res->use(appRoot + "mediaplayer");
|
||||
res->use(appRoot + "messages");
|
||||
res->use(appRoot + "misc");
|
||||
res->use(appRoot + "notifications");
|
||||
res->use(appRoot + "playqueue");
|
||||
res->use(appRoot + "release");
|
||||
res->use(appRoot + "releases");
|
||||
res->use(appRoot + "search");
|
||||
res->use(appRoot + "settings");
|
||||
res->use(appRoot + "tracklist");
|
||||
res->use(appRoot + "tracklists");
|
||||
res->use(appRoot + "tracks");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::shared_ptr<Wt::WMessageResourceBundle>
|
||||
getOrCreateMessageBundle()
|
||||
{
|
||||
static std::shared_ptr<Wt::WMessageResourceBundle> res {createMessageResourceBundle()};
|
||||
return res;
|
||||
}
|
||||
|
||||
static constexpr const char* defaultPath {"/releases"};
|
||||
|
||||
std::unique_ptr<Wt::WApplication>
|
||||
@@ -169,32 +211,8 @@ LmsApplication::init()
|
||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||
require("js/mediaplayer.js");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
// Add a resource bundle
|
||||
messageResourceBundle().use(appRoot() + "admin-database");
|
||||
messageResourceBundle().use(appRoot() + "admin-initwizard");
|
||||
messageResourceBundle().use(appRoot() + "admin-scannercontroller");
|
||||
messageResourceBundle().use(appRoot() + "admin-user");
|
||||
messageResourceBundle().use(appRoot() + "admin-users");
|
||||
messageResourceBundle().use(appRoot() + "artist");
|
||||
messageResourceBundle().use(appRoot() + "artists");
|
||||
messageResourceBundle().use(appRoot() + "error");
|
||||
messageResourceBundle().use(appRoot() + "explore");
|
||||
messageResourceBundle().use(appRoot() + "login");
|
||||
messageResourceBundle().use(appRoot() + "main");
|
||||
messageResourceBundle().use(appRoot() + "mediaplayer");
|
||||
messageResourceBundle().use(appRoot() + "messages");
|
||||
messageResourceBundle().use(appRoot() + "misc");
|
||||
messageResourceBundle().use(appRoot() + "notifications");
|
||||
messageResourceBundle().use(appRoot() + "playqueue");
|
||||
messageResourceBundle().use(appRoot() + "release");
|
||||
messageResourceBundle().use(appRoot() + "releases");
|
||||
messageResourceBundle().use(appRoot() + "search");
|
||||
messageResourceBundle().use(appRoot() + "settings");
|
||||
messageResourceBundle().use(appRoot() + "tracklist");
|
||||
messageResourceBundle().use(appRoot() + "tracklists");
|
||||
messageResourceBundle().use(appRoot() + "tracks");
|
||||
setTitle();
|
||||
setLocalizedStrings(getOrCreateMessageBundle());
|
||||
|
||||
// Handle Media Scanner events and other session events
|
||||
enableUpdates(true);
|
||||
@@ -360,21 +378,22 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||
std::string path;
|
||||
int index;
|
||||
bool admin;
|
||||
std::optional<Wt::WString> title;
|
||||
} views[] =
|
||||
{
|
||||
{ "/artists", IdxExplore, false },
|
||||
{ "/artist", IdxExplore, false },
|
||||
{ "/releases", IdxExplore, false },
|
||||
{ "/release", IdxExplore, false },
|
||||
{ "/search", IdxExplore, false },
|
||||
{ "/tracks", IdxExplore, false },
|
||||
{ "/tracklists", IdxExplore, false },
|
||||
{ "/tracklist", IdxExplore, false },
|
||||
{ "/playqueue", IdxPlayQueue, false },
|
||||
{ "/settings", IdxSettings, false },
|
||||
{ "/admin/database", IdxAdminDatabase, true },
|
||||
{ "/admin/users", IdxAdminUsers, true },
|
||||
{ "/admin/user", IdxAdminUser, true },
|
||||
{ "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") },
|
||||
{ "/artist", IdxExplore, false, std::nullopt },
|
||||
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
|
||||
{ "/release", IdxExplore, false, std::nullopt },
|
||||
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
|
||||
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
|
||||
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
|
||||
{ "/tracklist", IdxExplore, false, std::nullopt },
|
||||
{ "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") },
|
||||
{ "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") },
|
||||
{ "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") },
|
||||
{ "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") },
|
||||
{ "/admin/user", IdxAdminUser, true, std::nullopt },
|
||||
};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
||||
@@ -388,6 +407,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||
break;
|
||||
|
||||
stack.setCurrentIndex(view.index);
|
||||
if (view.title)
|
||||
LmsApp->setTitle(*view.title);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -481,8 +502,9 @@ LmsApplication::createHome()
|
||||
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
|
||||
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||
|
||||
Explore* explore {mainStack->addNew<Explore>(filters)};
|
||||
_playQueue = mainStack->addNew<PlayQueue>();
|
||||
std::unique_ptr<PlayQueue> playQueue {std::make_unique<PlayQueue>()};
|
||||
Explore* explore {mainStack->addNew<Explore>(*filters, *playQueue)};
|
||||
_playQueue = mainStack->addWidget(std::move(playQueue));
|
||||
mainStack->addNew<SettingsView>();
|
||||
|
||||
searchEdit->enterPressed().connect([=]
|
||||
@@ -504,11 +526,7 @@ LmsApplication::createHome()
|
||||
mainStack->addNew<UserView>();
|
||||
}
|
||||
|
||||
explore->setMaxTrackCountForAction(_playQueue->getCapacity());
|
||||
explore->tracksAction.connect([this] (PlayQueueAction action, const std::vector<Database::TrackId>& trackIds)
|
||||
{
|
||||
_playQueue->processTracks(action, trackIds);
|
||||
});
|
||||
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
|
||||
|
||||
// Events from MediaPlayer
|
||||
_mediaPlayer->playNext.connect([this]
|
||||
@@ -599,6 +617,15 @@ LmsApplication::post(std::function<void()> func)
|
||||
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::setTitle(const Wt::WString& title)
|
||||
{
|
||||
if (title.empty())
|
||||
WApplication::setTitle("LMS");
|
||||
else
|
||||
WApplication::setTitle(title);
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||
{
|
||||
|
||||
@@ -75,6 +75,7 @@ class LmsApplication : public Wt::WApplication
|
||||
|
||||
// Utils
|
||||
void post(std::function<void()> func);
|
||||
void setTitle(const Wt::WString& title = "");
|
||||
|
||||
// Used to classify the message sent to the user
|
||||
void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
|
||||
|
||||
+34
-26
@@ -416,38 +416,46 @@ PlayQueue::enqueueTracks(const std::vector<Database::TrackId>& trackIds)
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::TrackId>& trackIds)
|
||||
PlayQueue::play(const std::vector<Database::TrackId>& trackIds)
|
||||
{
|
||||
std::size_t nbAddedTracks {};
|
||||
playAtIndex(trackIds, 0);
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case PlayQueueAction::PlayLast:
|
||||
nbAddedTracks = enqueueTracks(trackIds);
|
||||
if (!_trackPos)
|
||||
loadTrack(0, true);
|
||||
break;
|
||||
void
|
||||
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)};
|
||||
loadTrack(0, true);
|
||||
|
||||
case PlayQueueAction::Play:
|
||||
clearTracks();
|
||||
nbAddedTracks = enqueueTracks(trackIds);
|
||||
loadTrack(0, true);
|
||||
notifyAddedTracks(nbAddedTracks);
|
||||
}
|
||||
|
||||
break;
|
||||
void
|
||||
PlayQueue::playOrAddLast(const std::vector<Database::TrackId>& trackIds)
|
||||
{
|
||||
const std::size_t nbAddedTracks {enqueueTracks(trackIds)};
|
||||
if (!_trackPos)
|
||||
loadTrack(0, true);
|
||||
|
||||
case PlayQueueAction::PlayShuffled:
|
||||
{
|
||||
clearTracks();
|
||||
{
|
||||
std::vector<Database::TrackId> shuffledTrackIds {trackIds};
|
||||
Random::shuffleContainer(shuffledTrackIds);
|
||||
nbAddedTracks = enqueueTracks(shuffledTrackIds);
|
||||
}
|
||||
loadTrack(0, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
notifyAddedTracks(nbAddedTracks);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::playAtIndex(const std::vector<Database::TrackId>& trackIds, std::size_t index)
|
||||
{
|
||||
clearTracks();
|
||||
const std::size_t nbAddedTracks {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});
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/database/TrackListId.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
|
||||
@@ -53,7 +53,10 @@ class PlayQueue : public Template
|
||||
public:
|
||||
PlayQueue();
|
||||
|
||||
void processTracks(PlayQueueAction action, const std::vector<Database::TrackId>& trackIds);
|
||||
void play(const std::vector<Database::TrackId>& trackIds);
|
||||
void playShuffled(const std::vector<Database::TrackId>& trackIds);
|
||||
void playOrAddLast(const std::vector<Database::TrackId>& trackIds); // play if queue empty, otherwise just add last
|
||||
void playAtIndex(const std::vector<Database::TrackId>& trackIds, std::size_t index);
|
||||
|
||||
// play the next track in the queue
|
||||
void playNext();
|
||||
@@ -70,6 +73,7 @@ class PlayQueue : public Template
|
||||
constexpr std::size_t getCapacity() const { return _capacity; }
|
||||
|
||||
private:
|
||||
void notifyAddedTracks(std::size_t nbAddedTracks) const;
|
||||
Database::ObjectPtr<Database::TrackList> getTrackList() const;
|
||||
bool isFull() const;
|
||||
|
||||
|
||||
@@ -215,16 +215,22 @@ UserView::refreshView()
|
||||
if (!user)
|
||||
throw UserNotFoundException {};
|
||||
|
||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName()), Wt::TextFormat::Plain);
|
||||
const Wt::WString title {Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName())};
|
||||
LmsApp->setTitle(title);
|
||||
|
||||
t->bindString("title", title, Wt::TextFormat::Plain);
|
||||
t->setCondition("if-has-last-login", true);
|
||||
t->bindString("last-login", user->getLastLogin().toString(), Wt::TextFormat::Plain);
|
||||
}
|
||||
else
|
||||
{
|
||||
const Wt::WString title {Wt::WString::tr("Lms.Admin.User.user-create")};
|
||||
LmsApp->setTitle(title);
|
||||
|
||||
// Login
|
||||
t->setCondition("if-has-login", true);
|
||||
t->setFormWidget(UserModel::LoginField, std::make_unique<Wt::WLineEdit>());
|
||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create"));
|
||||
t->bindString("title", title);
|
||||
}
|
||||
|
||||
if (authPasswordService)
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "PlayQueueController.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "TrackListHelpers.hpp"
|
||||
#include "Utils.hpp"
|
||||
@@ -47,9 +48,10 @@ using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Artist::Artist(Filters* filters)
|
||||
Artist::Artist(Filters& filters, PlayQueueController& controller)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Artist.template")}
|
||||
, _filters {filters}
|
||||
, _playQueueController {controller}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
@@ -59,7 +61,7 @@ Artist::Artist(Filters* filters)
|
||||
refreshView();
|
||||
});
|
||||
|
||||
filters->updated().connect([this]
|
||||
filters.updated().connect([this]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
@@ -109,6 +111,7 @@ Artist::refreshView()
|
||||
if (!artist)
|
||||
throw ArtistNotFoundException {};
|
||||
|
||||
LmsApp->setTitle(artist->getName());
|
||||
_artistId = *artistId;
|
||||
|
||||
std::size_t sectionCount{};
|
||||
@@ -139,7 +142,7 @@ Artist::refreshView()
|
||||
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
|
||||
entry->clicked().connect([=]
|
||||
{
|
||||
_filters->add(clusterId);
|
||||
_filters.add(clusterId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -150,18 +153,18 @@ Artist::refreshView()
|
||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
artistsAction.emit(PlayQueueAction::Play, {_artistId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::Play, {_artistId});
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
artistsAction.emit(PlayQueueAction::PlayShuffled, {_artistId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {_artistId});
|
||||
});
|
||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
artistsAction.emit(PlayQueueAction::PlayLast, {_artistId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {_artistId});
|
||||
});
|
||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)});
|
||||
@@ -287,7 +290,7 @@ Artist::addSomeReleases(InfiniteScrollingContainer& releaseContainer, EnumSet<Da
|
||||
const Range range {static_cast<std::size_t>(releaseContainer.getCount()), _releasesBatchSize};
|
||||
|
||||
Release::FindParameters params;
|
||||
params.setClusters(_filters->getClusterIds());
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
params.setArtist(_artistId, linkTypes, excludedLinkTypes);
|
||||
params.setRange(range);
|
||||
params.setSortMethod(ReleaseSortMethod::DateDesc);
|
||||
@@ -314,7 +317,7 @@ Artist::addSomeNonReleaseTracks()
|
||||
const Range range {static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize};
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setClusters(_filters->getClusterIds());
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
params.setArtist(_artistId);
|
||||
params.setRange(range);
|
||||
params.setSortMethod(TrackSortMethod::Name);
|
||||
@@ -332,7 +335,7 @@ Artist::addSomeNonReleaseTracks()
|
||||
}
|
||||
|
||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||
_trackContainer->add(TrackListHelpers::createEntry(track, tracksAction));
|
||||
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
|
||||
areTracksAdded = true;
|
||||
}
|
||||
|
||||
@@ -19,13 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "common/Template.hpp"
|
||||
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
@@ -34,17 +33,14 @@ namespace Database
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
class Filters;
|
||||
class PlayQueueController;
|
||||
class InfiniteScrollingContainer;
|
||||
|
||||
class Artist : public Template
|
||||
{
|
||||
public:
|
||||
Artist(Filters* filters);
|
||||
|
||||
PlayQueueActionArtistSignal artistsAction;
|
||||
PlayQueueActionTrackSignal tracksAction;
|
||||
Artist(Filters& filters, PlayQueueController& controller);
|
||||
|
||||
private:
|
||||
void refreshView();
|
||||
@@ -60,7 +56,8 @@ namespace UserInterface
|
||||
static constexpr std::size_t _tracksBatchSize {6};
|
||||
static constexpr std::size_t _tracksMaxCount {160};
|
||||
|
||||
Filters* _filters {};
|
||||
Filters& _filters;
|
||||
PlayQueueController& _playQueueController;
|
||||
InfiniteScrollingContainer* _releaseContainer {};
|
||||
InfiniteScrollingContainer* _appearsOnReleaseContainer {};
|
||||
InfiniteScrollingContainer* _trackContainer {};
|
||||
|
||||
+81
-209
@@ -19,17 +19,8 @@
|
||||
|
||||
#include "Explore.hpp"
|
||||
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "ArtistsView.hpp"
|
||||
#include "ArtistView.hpp"
|
||||
@@ -43,215 +34,96 @@
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
namespace {
|
||||
namespace {
|
||||
|
||||
void
|
||||
handleContentsPathChange(Wt::WStackedWidget* stack)
|
||||
{
|
||||
enum Idx
|
||||
{
|
||||
IdxArtists = 0,
|
||||
IdxArtist,
|
||||
IdxTrackLists,
|
||||
IdxTrackList,
|
||||
IdxReleases,
|
||||
IdxRelease,
|
||||
IdxSearch,
|
||||
IdxTracks,
|
||||
};
|
||||
|
||||
static const std::map<std::string, int> indexes =
|
||||
{
|
||||
{ "/artists", IdxArtists },
|
||||
{ "/artist", IdxArtist },
|
||||
{ "/tracklists", IdxTrackLists },
|
||||
{ "/tracklist", IdxTrackList },
|
||||
{ "/releases", IdxReleases },
|
||||
{ "/release", IdxRelease },
|
||||
{ "/search", IdxSearch },
|
||||
{ "/tracks", IdxTracks },
|
||||
};
|
||||
|
||||
for (const auto& index : indexes)
|
||||
{
|
||||
if (wApp->internalPathMatches(index.first))
|
||||
void
|
||||
handleContentsPathChange(Wt::WStackedWidget* stack)
|
||||
{
|
||||
stack->setCurrentIndex(index.second);
|
||||
return;
|
||||
enum Idx
|
||||
{
|
||||
IdxArtists = 0,
|
||||
IdxArtist,
|
||||
IdxTrackLists,
|
||||
IdxTrackList,
|
||||
IdxReleases,
|
||||
IdxRelease,
|
||||
IdxSearch,
|
||||
IdxTracks,
|
||||
};
|
||||
|
||||
static const std::map<std::string, int> indexes =
|
||||
{
|
||||
{ "/artists", IdxArtists },
|
||||
{ "/artist", IdxArtist },
|
||||
{ "/tracklists", IdxTrackLists },
|
||||
{ "/tracklist", IdxTrackList },
|
||||
{ "/releases", IdxReleases },
|
||||
{ "/release", IdxRelease },
|
||||
{ "/search", IdxSearch },
|
||||
{ "/tracks", IdxTracks },
|
||||
};
|
||||
|
||||
for (const auto& index : indexes)
|
||||
{
|
||||
if (wApp->internalPathMatches(index.first))
|
||||
{
|
||||
stack->setCurrentIndex(index.second);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
Explore::Explore(Filters* filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
|
||||
, _filters {filters}
|
||||
{
|
||||
addFunction("tr", &Functions::tr);
|
||||
|
||||
// Contents
|
||||
Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")};
|
||||
contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||
|
||||
// same order as enum Idx
|
||||
auto artists = std::make_unique<Artists>(*_filters);
|
||||
contentsStack->addWidget(std::move(artists));
|
||||
|
||||
auto artist = std::make_unique<Artist>(_filters);
|
||||
artist->artistsAction.connect(this, &Explore::handleArtistsAction);
|
||||
artist->tracksAction.connect(this, &Explore::handleTracksAction);
|
||||
contentsStack->addWidget(std::move(artist));
|
||||
|
||||
auto trackLists {std::make_unique<TrackLists>(*_filters)};
|
||||
TrackLists* trackListsPtr {trackLists.get()};
|
||||
trackLists->trackListAction.connect(this, &Explore::handleTrackListAction);
|
||||
contentsStack->addWidget(std::move(trackLists));
|
||||
|
||||
auto trackList {std::make_unique<TrackList>(*_filters)};
|
||||
TrackList* trackListPtr {trackList.get()};
|
||||
trackList->trackListAction.connect(this, &Explore::handleTrackListAction);
|
||||
trackList->tracksAction.connect(this, &Explore::handleTracksAction);
|
||||
contentsStack->addWidget(std::move(trackList));
|
||||
|
||||
trackListPtr->trackListDeleted.connect(trackListsPtr, &TrackLists::onTrackListDeleted);
|
||||
|
||||
auto releases = std::make_unique<Releases>(*_filters);
|
||||
releases->releasesAction.connect(this, &Explore::handleReleasesAction);
|
||||
contentsStack->addWidget(std::move(releases));
|
||||
|
||||
auto release = std::make_unique<Release>(_filters);
|
||||
release->releasesAction.connect(this, &Explore::handleReleasesAction);
|
||||
release->tracksAction.connect(this, &Explore::handleTracksAction);
|
||||
contentsStack->addWidget(std::move(release));
|
||||
|
||||
auto search = std::make_unique<SearchView>(_filters);
|
||||
search->tracksAction.connect(this, &Explore::handleTracksAction);
|
||||
_search = search.get();
|
||||
contentsStack->addWidget(std::move(search));
|
||||
|
||||
auto tracks = std::make_unique<Tracks>(*_filters);
|
||||
tracks->tracksAction.connect(this, &Explore::handleTracksAction);
|
||||
contentsStack->addWidget(std::move(tracks));
|
||||
|
||||
wApp->internalPathChanged().connect(this, [=]
|
||||
Explore::Explore(Filters& filters, PlayQueue& playQueue)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
|
||||
, _playQueueController {filters, playQueue}
|
||||
{
|
||||
addFunction("tr", &Functions::tr);
|
||||
|
||||
// Contents
|
||||
Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")};
|
||||
contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||
|
||||
// same order as enum Idx
|
||||
auto artists = std::make_unique<Artists>(filters);
|
||||
contentsStack->addWidget(std::move(artists));
|
||||
|
||||
auto artist = std::make_unique<Artist>(filters, _playQueueController);
|
||||
contentsStack->addWidget(std::move(artist));
|
||||
|
||||
auto trackLists {std::make_unique<TrackLists>(filters)};
|
||||
contentsStack->addWidget(std::move(trackLists));
|
||||
|
||||
auto trackList {std::make_unique<TrackList>(filters, _playQueueController)};
|
||||
trackList->trackListDeleted.connect(trackLists.get(), &TrackLists::onTrackListDeleted);
|
||||
contentsStack->addWidget(std::move(trackList));
|
||||
|
||||
auto releases = std::make_unique<Releases>(filters, _playQueueController);
|
||||
contentsStack->addWidget(std::move(releases));
|
||||
|
||||
auto release = std::make_unique<Release>(filters, _playQueueController);
|
||||
contentsStack->addWidget(std::move(release));
|
||||
|
||||
auto search = std::make_unique<SearchView>(filters, _playQueueController);
|
||||
_search = search.get();
|
||||
contentsStack->addWidget(std::move(search));
|
||||
|
||||
auto tracks = std::make_unique<Tracks>(filters, _playQueueController);
|
||||
contentsStack->addWidget(std::move(tracks));
|
||||
|
||||
wApp->internalPathChanged().connect(this, [=]
|
||||
{
|
||||
handleContentsPathChange(contentsStack);
|
||||
});
|
||||
|
||||
handleContentsPathChange(contentsStack);
|
||||
});
|
||||
|
||||
handleContentsPathChange(contentsStack);
|
||||
}
|
||||
|
||||
void
|
||||
Explore::search(const Wt::WString& searchText)
|
||||
{
|
||||
_search->refreshView(searchText);
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Database::TrackId>
|
||||
getArtistsTracks(Database::Session& session, const std::vector<Database::ArtistId>& artistsId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
{
|
||||
assert(maxTrackCount);
|
||||
|
||||
std::vector<Database::TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (const Database::ArtistId artistId : artistsId)
|
||||
{
|
||||
Database::Track::FindParameters params;
|
||||
params.setArtist(artistId);
|
||||
params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease);
|
||||
params.setClusters(clusters);
|
||||
params.setRange({0, maxTrackCount - res.size()});
|
||||
|
||||
const auto tracks {Database::Track::find(session, params)};
|
||||
|
||||
res.reserve(res.size() + tracks.results.size());
|
||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||
|
||||
if (res.size() == maxTrackCount)
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Database::TrackId>
|
||||
getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace Database;
|
||||
assert(maxTrackCount);
|
||||
|
||||
std::vector<TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (const ReleaseId releaseId : releasesId)
|
||||
void
|
||||
Explore::search(const Wt::WString& searchText)
|
||||
{
|
||||
Database::Track::FindParameters params;
|
||||
params.setRelease(releaseId);
|
||||
params.setSortMethod(Database::TrackSortMethod::Release);
|
||||
params.setClusters(clusters);
|
||||
params.setRange({0, maxTrackCount - res.size()});
|
||||
|
||||
const auto tracks {Database::Track::find(session, params)};
|
||||
|
||||
res.reserve(res.size() + tracks.results.size());
|
||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||
|
||||
if (res.size() == maxTrackCount)
|
||||
break;
|
||||
_search->refreshView(searchText);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Database::TrackId>
|
||||
getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace Database;
|
||||
assert(maxTrackCount);
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
Database::Track::FindParameters params;
|
||||
params.setTrackList(trackListId);
|
||||
params.setClusters(clusters);
|
||||
params.setRange({0, maxTrackCount});
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
params.setDistinct(false);
|
||||
|
||||
return Database::Track::find(session, params).results;
|
||||
}
|
||||
|
||||
void
|
||||
Explore::handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId)
|
||||
{
|
||||
tracksAction.emit(action, getArtistsTracks(LmsApp->getDbSession(), artistsId, _filters->getClusterIds(), _maxTrackCount));
|
||||
}
|
||||
|
||||
void
|
||||
Explore::handleReleasesAction(PlayQueueAction action, const std::vector<Database::ReleaseId>& releasesId)
|
||||
{
|
||||
tracksAction.emit(action, getReleasesTracks(LmsApp->getDbSession(), releasesId, _filters->getClusterIds(), _maxTrackCount));
|
||||
}
|
||||
|
||||
void
|
||||
Explore::handleTrackListAction(PlayQueueAction action, Database::TrackListId trackListId)
|
||||
{
|
||||
tracksAction.emit(action, getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters->getClusterIds(), _maxTrackCount));
|
||||
}
|
||||
|
||||
void
|
||||
Explore::handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId)
|
||||
{
|
||||
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
|
||||
tracksAction.emit(action, tracksId);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -20,34 +20,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
#include "PlayQueueController.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class Filters;
|
||||
class SearchView;
|
||||
class PlayQueue;
|
||||
|
||||
class Explore : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
Explore(Filters* filters);
|
||||
Explore(Filters& filters, PlayQueue& playQueue);
|
||||
|
||||
void search(const Wt::WString& searchText);
|
||||
|
||||
PlayQueueActionTrackSignal tracksAction;
|
||||
void setMaxTrackCountForAction(std::size_t maxTrackCount) { _maxTrackCount = maxTrackCount; }
|
||||
PlayQueueController& getPlayQueueController() { return _playQueueController; }
|
||||
|
||||
private:
|
||||
void handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId);
|
||||
void handleReleasesAction(PlayQueueAction action, const std::vector<Database::ReleaseId>& releasesId);
|
||||
void handleTrackListAction(PlayQueueAction action, Database::TrackListId trackiListId);
|
||||
void handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId);
|
||||
|
||||
Filters* _filters {};
|
||||
PlayQueueController _playQueueController;
|
||||
SearchView* _search {};
|
||||
std::size_t _maxTrackCount {};
|
||||
};
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Filters::showDialog()
|
||||
{
|
||||
const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front())};
|
||||
|
||||
for (const Cluster::pointer cluster : clusterType->getClusters())
|
||||
for (const Cluster::pointer& cluster : clusterType->getClusters())
|
||||
{
|
||||
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end())
|
||||
valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName()));
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "PlayQueue.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
static
|
||||
std::vector<Database::TrackId>
|
||||
getArtistsTracks(Database::Session& session, const std::vector<Database::ArtistId>& artistsId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
{
|
||||
assert(maxTrackCount);
|
||||
|
||||
std::vector<Database::TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (const Database::ArtistId artistId : artistsId)
|
||||
{
|
||||
Database::Track::FindParameters params;
|
||||
params.setArtist(artistId);
|
||||
params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease);
|
||||
params.setClusters(clusters);
|
||||
params.setRange({0, maxTrackCount - res.size()});
|
||||
|
||||
const auto tracks {Database::Track::find(session, params)};
|
||||
|
||||
res.reserve(res.size() + tracks.results.size());
|
||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||
|
||||
if (res.size() == maxTrackCount)
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Database::TrackId>
|
||||
getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace Database;
|
||||
assert(maxTrackCount);
|
||||
|
||||
std::vector<TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (const ReleaseId releaseId : releasesId)
|
||||
{
|
||||
Database::Track::FindParameters params;
|
||||
params.setRelease(releaseId);
|
||||
params.setSortMethod(Database::TrackSortMethod::Release);
|
||||
params.setClusters(clusters);
|
||||
params.setRange({0, maxTrackCount - res.size()});
|
||||
|
||||
const auto tracks {Database::Track::find(session, params)};
|
||||
|
||||
res.reserve(res.size() + tracks.results.size());
|
||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||
|
||||
if (res.size() == maxTrackCount)
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Database::TrackId>
|
||||
getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace Database;
|
||||
assert(maxTrackCount);
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
Database::Track::FindParameters params;
|
||||
params.setTrackList(trackListId);
|
||||
params.setClusters(clusters);
|
||||
params.setRange({0, maxTrackCount});
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
params.setDistinct(false);
|
||||
|
||||
return Database::Track::find(session, params).results;
|
||||
}
|
||||
|
||||
PlayQueueController::PlayQueueController(Filters& filters, PlayQueue& playQueue)
|
||||
: _filters {filters}
|
||||
, _playQueue {playQueue}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueueController::processCommand(Command command, const std::vector<Database::ArtistId>& artistIds)
|
||||
{
|
||||
const std::vector<Database::TrackId> tracks {getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueueController::processCommand(Command command, const std::vector<Database::ReleaseId>& releaseIds)
|
||||
{
|
||||
const std::vector<Database::TrackId> tracks {getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueueController::processCommand(Command command, const std::vector<Database::TrackId>& trackIds)
|
||||
{
|
||||
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
|
||||
switch (command)
|
||||
{
|
||||
case Command::Play:
|
||||
_playQueue.play(trackIds);
|
||||
break;
|
||||
case Command::PlayShuffled:
|
||||
_playQueue.playShuffled(trackIds);
|
||||
break;
|
||||
case Command::PlayOrAddLast:
|
||||
_playQueue.playOrAddLast(trackIds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueueController::processCommand(Command command, Database::TrackListId trackListId)
|
||||
{
|
||||
const std::vector<Database::TrackId> tracks {getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueueController::playTrackInRelease(Database::TrackId trackId)
|
||||
{
|
||||
Database::ReleaseId releaseId;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)};
|
||||
if (!track || !track->getRelease())
|
||||
return;
|
||||
|
||||
releaseId = track->getRelease()->getId();
|
||||
}
|
||||
|
||||
const std::vector<Database::TrackId> tracks {getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||
auto itTrack {std::find(std::cbegin(tracks), std::cend(tracks), trackId)};
|
||||
if (itTrack == std::cend(tracks))
|
||||
return;
|
||||
|
||||
const std::size_t index {static_cast<std::size_t>(std::distance(std::cbegin(tracks), itTrack))};
|
||||
_playQueue.playAtIndex(tracks, index);
|
||||
}
|
||||
} // namespace UserInterface
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -20,7 +20,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
@@ -29,16 +28,34 @@
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
enum class PlayQueueAction
|
||||
{
|
||||
Play,
|
||||
PlayLast,
|
||||
PlayShuffled,
|
||||
};
|
||||
class Filters;
|
||||
class PlayQueue;
|
||||
|
||||
using PlayQueueActionArtistSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ArtistId>&>;
|
||||
using PlayQueueActionReleaseSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ReleaseId>&>;
|
||||
using PlayQueueActionTrackSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::TrackId>&>;
|
||||
using PlayQueueActionTrackListSignal = Wt::Signal<PlayQueueAction, Database::TrackListId>;
|
||||
// Used to interact with the play queue, using the current exploration filters
|
||||
class PlayQueueController
|
||||
{
|
||||
public:
|
||||
PlayQueueController(Filters& filters, PlayQueue& playQueue);
|
||||
|
||||
enum class Command
|
||||
{
|
||||
Play,
|
||||
PlayOrAddLast,
|
||||
PlayShuffled,
|
||||
};
|
||||
|
||||
void processCommand(Command command, const std::vector<Database::ArtistId>&);
|
||||
void processCommand(Command command, const std::vector<Database::ReleaseId>&);
|
||||
void processCommand(Command command, const std::vector<Database::TrackId>&);
|
||||
void processCommand(Command command, Database::TrackListId);
|
||||
void playTrackInRelease(Database::TrackId);
|
||||
|
||||
void setMaxTrackCountToEnqueue(std::size_t maxTrackCount) { _maxTrackCountToEnqueue = maxTrackCount; }
|
||||
|
||||
private:
|
||||
Filters& _filters;
|
||||
PlayQueue& _playQueue;
|
||||
std::size_t _maxTrackCountToEnqueue {};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,20 +34,22 @@
|
||||
|
||||
#include "common/Template.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "explore/ReleaseListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Release::Release(Filters* filters)
|
||||
Release::Release(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Release.template")}
|
||||
, _filters {filters}
|
||||
, _playQueueController {playQueueController}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
@@ -57,7 +59,7 @@ Release::Release(Filters* filters)
|
||||
refreshView();
|
||||
});
|
||||
|
||||
filters->updated().connect([this]
|
||||
_filters.updated().connect([this]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
@@ -106,6 +108,8 @@ Release::refreshView()
|
||||
if (!release)
|
||||
throw ReleaseNotFoundException {};
|
||||
|
||||
LmsApp->setTitle(release->getName());
|
||||
|
||||
refreshCopyright(release);
|
||||
refreshLinks(release);
|
||||
refreshSimilarReleases(similarReleasesIds);
|
||||
@@ -145,7 +149,7 @@ Release::refreshView()
|
||||
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
|
||||
entry->clicked().connect([=]
|
||||
{
|
||||
_filters->add(clusterId);
|
||||
_filters.add(clusterId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -154,19 +158,19 @@ Release::refreshView()
|
||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
releasesAction.emit(PlayQueueAction::Play, {*releaseId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::Play, {*releaseId});
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
releasesAction.emit(PlayQueueAction::PlayShuffled, {*releaseId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {*releaseId});
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
releasesAction.emit(PlayQueueAction::PlayLast, {*releaseId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {*releaseId});
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
@@ -195,7 +199,8 @@ Release::refreshView()
|
||||
|
||||
const bool variousArtists {release->hasVariousArtists()};
|
||||
const auto totalDisc {release->getTotalDisc()};
|
||||
const bool isReleaseMultiDisc {totalDisc && *totalDisc > 1};
|
||||
const std::size_t discCount {release->getDiscCount()};
|
||||
const bool isReleaseMultiDisc {(discCount > 1) || (totalDisc && *totalDisc > 1)};
|
||||
|
||||
// Expect to be called in asc order
|
||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
||||
@@ -235,7 +240,7 @@ Release::refreshView()
|
||||
Database::Track::FindParameters params;
|
||||
params.setRelease(*releaseId);
|
||||
params.setSortMethod(Database::TrackSortMethod::Release);
|
||||
params.setClusters(_filters->getClusterIds());
|
||||
params.setClusters(_filters.getClusterIds());
|
||||
|
||||
const auto tracks {Database::Track::find(LmsApp->getDbSession(), params)};
|
||||
for (const Database::TrackId trackId : tracks.results)
|
||||
@@ -282,7 +287,7 @@ Release::refreshView()
|
||||
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::Play, {trackId});
|
||||
_playQueueController.playTrackInRelease(trackId);
|
||||
});
|
||||
|
||||
{
|
||||
@@ -290,7 +295,7 @@ Release::refreshView()
|
||||
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||
->clicked().connect([=]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
|
||||
});
|
||||
|
||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "common/Template.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -31,15 +31,12 @@ namespace Database
|
||||
namespace UserInterface
|
||||
{
|
||||
class Filters;
|
||||
|
||||
class PlayQueueController;
|
||||
|
||||
class Release : public Template
|
||||
{
|
||||
public:
|
||||
Release(Filters* filters);
|
||||
|
||||
PlayQueueActionReleaseSignal releasesAction;
|
||||
PlayQueueActionTrackSignal tracksAction;
|
||||
Release(Filters& filters, PlayQueueController& playQueueController);
|
||||
|
||||
private:
|
||||
void refreshView();
|
||||
@@ -48,7 +45,8 @@ namespace UserInterface
|
||||
void refreshLinks(const Database::ObjectPtr<Database::Release>& release);
|
||||
void refreshSimilarReleases(const std::vector<Database::ReleaseId>& similarReleasesId);
|
||||
|
||||
Filters* _filters {};
|
||||
Filters& _filters;
|
||||
PlayQueueController& _playQueueController;
|
||||
};
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -26,16 +26,18 @@
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "common/Template.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "explore/ReleaseListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Releases::Releases(Filters& filters)
|
||||
Releases::Releases(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Releases.template")}
|
||||
, _playQueueController {playQueueController}
|
||||
, _releaseCollector {filters, _defaultMode, _maxCount}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
@@ -69,18 +71,18 @@ Releases::Releases(Filters& filters)
|
||||
Wt::WPushButton* playBtn {bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([this]
|
||||
{
|
||||
releasesAction.emit(PlayQueueAction::Play, getAllReleases());
|
||||
_playQueueController.processCommand(PlayQueueController::Command::Play, getAllReleases());
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
releasesAction.emit(PlayQueueAction::PlayShuffled, getAllReleases());
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, getAllReleases());
|
||||
});
|
||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
releasesAction.emit(PlayQueueAction::PlayLast, getAllReleases());
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, getAllReleases());
|
||||
});
|
||||
|
||||
_container = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
|
||||
|
||||
@@ -22,20 +22,18 @@
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
#include "ReleaseCollector.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class Filters;
|
||||
class InfiniteScrollingContainer;
|
||||
class PlayQueueController;
|
||||
|
||||
class Releases : public Template
|
||||
{
|
||||
public:
|
||||
Releases(Filters& filters);
|
||||
|
||||
PlayQueueActionReleaseSignal releasesAction;
|
||||
Releases(Filters& filters, PlayQueueController& playQueueController);
|
||||
|
||||
private:
|
||||
void refreshView();
|
||||
@@ -48,6 +46,7 @@ namespace UserInterface
|
||||
static constexpr std::size_t _batchSize {_maxItemsPerLine};
|
||||
static constexpr std::size_t _maxCount {_maxItemsPerLine * 500};
|
||||
|
||||
PlayQueueController& _playQueueController;
|
||||
Wt::WWidget* _currentActiveItem {};
|
||||
InfiniteScrollingContainer* _container {};
|
||||
ReleaseCollector _releaseCollector;
|
||||
|
||||
@@ -35,12 +35,12 @@ using namespace Database;
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
SearchView::SearchView(Filters* filters)
|
||||
SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
|
||||
, _filters {filters}
|
||||
, _artistCollector {*filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
|
||||
, _releaseCollector {*filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
|
||||
, _trackCollector {*filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
|
||||
, _playQueueController {playQueueController}
|
||||
, _artistCollector {filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
|
||||
, _releaseCollector {filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
|
||||
, _trackCollector {filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace UserInterface
|
||||
_tracks = bindNew<InfiniteScrollingContainer>("tracks");
|
||||
_tracks->onRequestElements.connect([this] { addSomeTracks(); });
|
||||
|
||||
_filters->updated().connect([=]
|
||||
filters.updated().connect([=]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
@@ -152,7 +152,7 @@ namespace UserInterface
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||
_tracks->add(TrackListHelpers::createEntry(track, tracksAction));
|
||||
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,20 +27,17 @@
|
||||
#include "ArtistCollector.hpp"
|
||||
#include "ReleaseCollector.hpp"
|
||||
#include "TrackCollector.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
class InfiniteScrollingContainer;
|
||||
class Filters;
|
||||
class PlayQueueController;
|
||||
|
||||
class SearchView : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
SearchView(Filters* filters);
|
||||
|
||||
PlayQueueActionTrackSignal tracksAction;
|
||||
SearchView(Filters& filters, PlayQueueController& playQueueController);
|
||||
|
||||
void refreshView(const Wt::WString& searchText);
|
||||
|
||||
@@ -74,7 +71,7 @@ namespace UserInterface
|
||||
void addSomeReleases();
|
||||
void addSomeTracks();
|
||||
|
||||
Filters* _filters {};
|
||||
PlayQueueController& _playQueueController;
|
||||
ArtistCollector _artistCollector;
|
||||
ReleaseCollector _releaseCollector;
|
||||
TrackCollector _trackCollector;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "TrackListHelpers.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
@@ -29,10 +28,10 @@
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
@@ -44,7 +43,7 @@ using namespace Database;
|
||||
namespace UserInterface::TrackListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WWidget>
|
||||
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueActionTrackSignal& tracksAction)
|
||||
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController)
|
||||
{
|
||||
auto entry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
||||
auto* entryPtr {entry.get()};
|
||||
@@ -74,16 +73,16 @@ namespace UserInterface::TrackListHelpers
|
||||
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([trackId, &tracksAction]
|
||||
playBtn->clicked().connect([trackId, &playQueueController]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::Play, {trackId});
|
||||
playQueueController.processCommand(PlayQueueController::Command::Play, {trackId});
|
||||
});
|
||||
|
||||
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
|
||||
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||
->clicked().connect([=, &tracksAction]
|
||||
->clicked().connect([=, &playQueueController]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
||||
playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@@ -23,15 +23,19 @@
|
||||
|
||||
#include <Wt/WWidget.h>
|
||||
#include "services/database/Object.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Track;
|
||||
}
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class PlayQueueController;
|
||||
}
|
||||
|
||||
namespace UserInterface::TrackListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueActionTrackSignal& tracksAction);
|
||||
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController);
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -30,12 +30,13 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "explore/TrackListHelpers.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
#include "TrackListHelpers.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -52,9 +53,10 @@ namespace
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
TrackList::TrackList(Filters& filters)
|
||||
TrackList::TrackList(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Template {Wt::WString::tr("Lms.Explore.TrackList.template")}
|
||||
, _filters {filters}
|
||||
, _filters {_filters}
|
||||
, _playQueueController {playQueueController}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
@@ -64,7 +66,7 @@ namespace UserInterface
|
||||
refreshView();
|
||||
});
|
||||
|
||||
_filters.updated().connect([this]
|
||||
filters.updated().connect([this]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
@@ -88,6 +90,7 @@ namespace UserInterface
|
||||
if (!trackList)
|
||||
throw TrackListNotFoundException {};
|
||||
|
||||
LmsApp->setTitle(std::string {trackList->getName()});
|
||||
_trackListId = *trackListId;
|
||||
|
||||
clear();
|
||||
@@ -119,19 +122,19 @@ namespace UserInterface
|
||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
trackListAction.emit(PlayQueueAction::Play, *trackListId);
|
||||
_playQueueController.processCommand(PlayQueueController::Command::Play, *trackListId);
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
trackListAction.emit(PlayQueueAction::PlayShuffled, *trackListId);
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, *trackListId);
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
trackListAction.emit(PlayQueueAction::PlayLast, *trackListId);
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, *trackListId);
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
@@ -196,7 +199,7 @@ namespace UserInterface
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
||||
_container->add(TrackListHelpers::createEntry(track, tracksAction));
|
||||
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
}
|
||||
|
||||
_container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount);
|
||||
|
||||
@@ -23,20 +23,17 @@
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class Filters;
|
||||
class InfiniteScrollingContainer;
|
||||
class PlayQueueController;
|
||||
|
||||
class TrackList : public Template
|
||||
{
|
||||
public:
|
||||
TrackList(Filters& filters);
|
||||
|
||||
PlayQueueActionTrackListSignal trackListAction;
|
||||
PlayQueueActionTrackSignal tracksAction;
|
||||
TrackList(Filters& filters, PlayQueueController& playQueueController);
|
||||
|
||||
Wt::Signal<Database::TrackListId> trackListDeleted;
|
||||
|
||||
@@ -48,6 +45,7 @@ namespace UserInterface
|
||||
static constexpr std::size_t _maxCount {8000};
|
||||
|
||||
Filters& _filters;
|
||||
PlayQueueController& _playQueueController;
|
||||
Database::TrackListId _trackListId;
|
||||
InfiniteScrollingContainer* _container {};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -44,8 +43,6 @@ namespace UserInterface
|
||||
public:
|
||||
TrackLists(Filters& filters);
|
||||
|
||||
PlayQueueActionTrackListSignal trackListAction;
|
||||
|
||||
void onTrackListDeleted(Database::TrackListId trackListId);
|
||||
|
||||
private:
|
||||
|
||||
@@ -26,17 +26,19 @@
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "explore/TrackListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "TrackListHelpers.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Tracks::Tracks(Filters& filters)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Tracks.template")},
|
||||
_trackCollector {filters, _defaultMode, _maxCount}
|
||||
Tracks::Tracks(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Tracks.template")}
|
||||
, _playQueueController {playQueueController}
|
||||
, _trackCollector {filters, _defaultMode, _maxCount}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
@@ -69,18 +71,18 @@ _trackCollector {filters, _defaultMode, _maxCount}
|
||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::Play, getAllTracks());
|
||||
_playQueueController.processCommand(PlayQueueController::Command::Play, getAllTracks());
|
||||
});
|
||||
|
||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayShuffled, getAllTracks());
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, getAllTracks());
|
||||
});
|
||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||
->clicked().connect([=]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayLast, getAllTracks());
|
||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, getAllTracks());
|
||||
});
|
||||
|
||||
_container = bindNew<InfiniteScrollingContainer>("tracks");
|
||||
@@ -122,7 +124,7 @@ Tracks::addSome()
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
||||
_container->add(TrackListHelpers::createEntry(track, tracksAction));
|
||||
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
}
|
||||
|
||||
_container->setHasMore(trackIds.moreResults);
|
||||
|
||||
@@ -22,20 +22,18 @@
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
#include "TrackCollector.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class Filters;
|
||||
class InfiniteScrollingContainer;
|
||||
class PlayQueueController;
|
||||
|
||||
class Tracks : public Template
|
||||
{
|
||||
public:
|
||||
Tracks(Filters& filters);
|
||||
|
||||
PlayQueueActionTrackSignal tracksAction;
|
||||
Tracks(Filters& filters, PlayQueueController& playQueueController);
|
||||
|
||||
private:
|
||||
void refreshView();
|
||||
@@ -48,6 +46,7 @@ namespace UserInterface
|
||||
static constexpr std::size_t _batchSize {6};
|
||||
static constexpr std::size_t _maxCount {8000};
|
||||
|
||||
PlayQueueController& _playQueueController;
|
||||
Wt::WWidget* _currentActiveItem {};
|
||||
InfiniteScrollingContainer* _container {};
|
||||
TrackCollector _trackCollector;
|
||||
|
||||
@@ -118,59 +118,62 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa
|
||||
return res;
|
||||
}
|
||||
|
||||
struct TranscodeParameters
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path file;
|
||||
Av::TranscodeParameters transcodeParameters;
|
||||
};
|
||||
|
||||
static
|
||||
std::optional<TranscodeParameters>
|
||||
readTranscodeParameters(const Wt::Http::Request& request)
|
||||
{
|
||||
TranscodeParameters parameters;
|
||||
|
||||
// mandatory parameters
|
||||
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")};
|
||||
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
|
||||
if (!avFormat)
|
||||
return std::nullopt;
|
||||
|
||||
// optional parameter
|
||||
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
struct TranscodeParameters
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
Av::InputFileParameters inputFileParameters;
|
||||
Av::TranscodeParameters transcodeParameters;
|
||||
};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(ERROR) << "Missing track";
|
||||
std::optional<TranscodeParameters>
|
||||
readTranscodeParameters(const Wt::Http::Request& request)
|
||||
{
|
||||
TranscodeParameters parameters;
|
||||
|
||||
// mandatory parameters
|
||||
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")};
|
||||
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
parameters.file = track->getPath();
|
||||
|
||||
if (!Database::isAudioBitrateAllowed(*bitrate))
|
||||
{
|
||||
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
|
||||
if (!avFormat)
|
||||
return std::nullopt;
|
||||
|
||||
// optional parameter
|
||||
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(ERROR) << "Missing track";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
parameters.inputFileParameters.trackPath = track->getPath();
|
||||
parameters.inputFileParameters.duration = track->getDuration();
|
||||
}
|
||||
|
||||
parameters.transcodeParameters.stripMetadata = true;
|
||||
parameters.transcodeParameters.format = *avFormat;
|
||||
parameters.transcodeParameters.bitrate = *bitrate;
|
||||
parameters.transcodeParameters.offset = std::chrono::seconds {offset};
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
parameters.transcodeParameters.stripMetadata = true;
|
||||
parameters.transcodeParameters.format = *avFormat;
|
||||
parameters.transcodeParameters.bitrate = *bitrate;
|
||||
parameters.transcodeParameters.offset = std::chrono::seconds {offset};
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -186,7 +189,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
|
||||
{
|
||||
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
|
||||
if (parameters)
|
||||
resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters);
|
||||
resourceHandler = Av::createTranscodeResourceHandler(parameters->inputFileParameters, parameters->transcodeParameters, false /* estimate content length */);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "metadata/AvFormatParser.hpp"
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "utils/StreamLogger.hpp"
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist)
|
||||
@@ -69,7 +68,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
}
|
||||
const auto end {std::chrono::steady_clock::now()};
|
||||
|
||||
std::cout << "Parsing time: " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000. << "ms" << std::endl;
|
||||
std::cout << "Parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000. << "ms" << std::endl;
|
||||
|
||||
std::cout << "Track metadata:" << std::endl;
|
||||
|
||||
@@ -117,7 +116,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Duration: " << track->duration.count() / 1000 << "s" << std::endl;
|
||||
std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl;
|
||||
|
||||
if (track->trackNumber)
|
||||
std::cout << "Track: " << *track->trackNumber << std::endl;
|
||||
@@ -184,14 +183,14 @@ int main(int argc, char *argv[])
|
||||
|
||||
{
|
||||
std::cout << "Using av:" << std::endl;
|
||||
MetaData::AvFormatParser parser;
|
||||
parse(parser, file);
|
||||
auto parser {MetaData::createParser(MetaData::ParserType::AvFormat, MetaData::ParserReadStyle::Accurate)};
|
||||
parse(*parser, file);
|
||||
}
|
||||
|
||||
{
|
||||
std::cout << "Using TagLib:" << std::endl;
|
||||
MetaData::TagLibParser parser;
|
||||
parse(parser, file);
|
||||
auto parser {MetaData::createParser(MetaData::ParserType::TagLib, MetaData::ParserReadStyle::Accurate)};
|
||||
parse(*parser, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user