Merge branch 'develop' for release v3.31.0

This commit is contained in:
emeric
2022-09-07 21:08:18 +02:00
70 changed files with 1047 additions and 627 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ ARG BUILD_PACKAGES="\
taglib \
wt"
RUN pacman -Syy
RUN pacman -Syu --noconfirm
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
# LMS
+3 -2
View File
@@ -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>
+2
View File
@@ -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>
+2 -1
View File
@@ -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>
+2
View File
@@ -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>
+3
View File
@@ -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
View File
@@ -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") {
+1 -1
View File
@@ -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)
{
+42 -9
View File
@@ -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;
};
}
+20 -20
View File
@@ -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");
+5 -5
View File
@@ -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);
}
+1
View File
@@ -5,6 +5,7 @@ endif()
add_library(lmsmetadata SHARED
impl/AvFormatParser.cpp
impl/Factory.cpp
impl/TagLibParser.cpp
impl/Utils.cpp
)
+1 -1
View File
@@ -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>
+47
View File
@@ -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"};
}
}
+26 -3
View File
@@ -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)};
@@ -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
+15
View File
@@ -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"};
}
}
+3
View File
@@ -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
+1 -1
View File
@@ -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:
+14 -2
View File
@@ -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
{
+2 -2
View File
@@ -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;
+8 -8
View File
@@ -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();
+9 -4
View File
@@ -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
{
-13
View File
@@ -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 {};
}
}
-4
View File
@@ -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);
}
+24 -1
View File
@@ -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())));
+1 -1
View File
@@ -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());
+12
View File
@@ -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)
{
+5
View File
@@ -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);
+9
View File
@@ -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);
}
+1
View File
@@ -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
+73 -46
View File
@@ -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)
{
+1
View File
@@ -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
View File
@@ -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});
+6 -2
View File
@@ -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;
+8 -2
View File
@@ -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)
+12 -9
View File
@@ -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;
}
+5 -8
View File
@@ -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
View File
@@ -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
+5 -14
View File
@@ -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
+1 -1
View File
@@ -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()));
+178
View File
@@ -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 {};
};
}
+17 -12
View File
@@ -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); }};
+5 -7
View File
@@ -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
+8 -6
View File
@@ -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"));
+3 -4
View File
@@ -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;
+7 -7
View File
@@ -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));
}
}
+3 -6
View File
@@ -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;
+6 -7
View File
@@ -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});
});
{
+6 -2
View File
@@ -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
+12 -9
View File
@@ -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);
+3 -5
View File
@@ -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 {};
};
-3
View File
@@ -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:
+11 -9
View File
@@ -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);
+3 -4
View File
@@ -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;
+46 -43
View File
@@ -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
{
+7 -8
View File
@@ -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);
}
}
}