Merge branch 'replaygain' into develop. Fixes #38
This commit is contained in:
@@ -164,6 +164,12 @@
|
||||
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
||||
<message id="Lms.Settings.password-bad">Bad password</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
||||
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
|
||||
<message id="Lms.Settings.replaygain-mode.none">No ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-mode.auto">Auto</message>
|
||||
<message id="Lms.Settings.replaygain-mode.track">Track</message>
|
||||
<message id="Lms.Settings.replaygain-mode.release">Album</message>
|
||||
<message id="Lms.Settings.replaygain-preamp">Replay gain preAmp</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>
|
||||
|
||||
@@ -164,6 +164,12 @@
|
||||
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
||||
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
||||
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-mode.none">Pas de ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-mode.auto">Automatique</message>
|
||||
<message id="Lms.Settings.replaygain-mode.track">Piste</message>
|
||||
<message id="Lms.Settings.replaygain-mode.release">Album</message>
|
||||
<message id="Lms.Settings.replaygain-preamp">PreAmplification ReplayGain</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>
|
||||
@@ -188,4 +194,5 @@
|
||||
<message id="Wt.WMessageBox.Yes">Oui</message>
|
||||
<message id="Wt.WMessageBox.No">Non</message>
|
||||
<message id="Wt.WValidator.Invalid">Ce champ ne peut pas être vide</message>
|
||||
<message id="Wt.WDoubleValidator.BadRange">Le nombre doit être compris entre {1} et {2}</message>
|
||||
</messages>
|
||||
|
||||
+143
-147
@@ -4,161 +4,157 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Settings.template">
|
||||
<legend>${tr:Lms.Settings.appearance}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="${id:dark-mode}">
|
||||
${tr:Lms.Settings.dark-mode}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<div class="checkbox"><label>${dark-mode}</label></div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<legend>${tr:Lms.Settings.appearance}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
<div class="checkbox">
|
||||
<label>${dark-mode}${tr:Lms.Settings.dark-mode}</label>
|
||||
${dark-mode-info clas="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5 help-block">
|
||||
${dark-mode-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<legend>${tr:Lms.Settings.audio}</legend>
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="alert alert-info col-sm-7">
|
||||
<legend>${tr:Lms.Settings.audio}</legend>
|
||||
<div class="alert alert-info">
|
||||
${tr:Lms.Settings.audio-settings-are-local}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="${id:transcode-mode}">
|
||||
${tr:Lms.Settings.transcode-mode}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${transcode-mode}
|
||||
</div>
|
||||
<div class="col-sm-5 help-block">
|
||||
${transcode-mode-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${transcode-format}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${transcode-format-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
${transcode-bitrate}
|
||||
<span class="input-group-addon">kbps</span>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:transcode-mode}">
|
||||
${tr:Lms.Settings.transcode-mode}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${transcode-mode}
|
||||
${transcode-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${transcode-format}
|
||||
${transcode-format-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${transcode-bitrate}
|
||||
<span class="input-group-addon">kbps</span>
|
||||
</div>
|
||||
${transcode-bitrate-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:replaygain-mode}">
|
||||
${tr:Lms.Settings.replaygain-mode}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${replaygain-mode}
|
||||
${replaygain-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:replaygain-preamp}">
|
||||
${tr:Lms.Settings.replaygain-preamp}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${replaygain-preamp}
|
||||
<span class="input-group-addon">dB</span>
|
||||
</div>
|
||||
${replaygain-preamp-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${transcode-bitrate-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.subsonic-api}</legend>
|
||||
<div class="form-horizontal">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="${id:subsonic-transcode-enable}">
|
||||
${tr:Lms.Settings.transcode-enable}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<div class="checkbox"><label>${subsonic-transcode-enable}</label></div>
|
||||
</div>
|
||||
<div class="col-sm-5 help-block">
|
||||
${subsonic-transcode-enable-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:subsonic-transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${subsonic-transcode-format}
|
||||
</div>
|
||||
<div class="col-sm-5 help-block">
|
||||
${subsonic-transcode-format-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:subsonic-transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
${subsonic-transcode-bitrate}
|
||||
<span class="input-group-addon">kbps</span>
|
||||
${<if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.subsonic-api}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
<div class="checkbox">
|
||||
<label>${subsonic-transcode-enable}${tr:Lms.Settings.transcode-enable}</label>
|
||||
${subsonic-transcode-enable-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:subsonic-transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${subsonic-transcode-format}
|
||||
${subsonic-transcode-format-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:subsonic-transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${subsonic-transcode-bitrate}
|
||||
<span class="input-group-addon">kbps</span>
|
||||
</div>
|
||||
${subsonic-transcode-bitrate-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:subsonic-artist-list-mode}">
|
||||
${tr:Lms.Settings.subsonic-artist-list-mode}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${subsonic-artist-list-mode}
|
||||
${subsonic-artist-list-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${subsonic-transcode-bitrate-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:subsonic-artist-list-mode}">
|
||||
${tr:Lms.Settings.subsonic-artist-list-mode}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${subsonic-artist-list-mode}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${subsonic-artist-list-mode-info}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.change-password}</legend>
|
||||
<div class="form-horizontal">
|
||||
${<if-has-old-password>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:password-old}">
|
||||
${tr:Lms.password-old}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password-old}
|
||||
${password-old-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-old-password>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password}
|
||||
${password-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:password-confirm}">
|
||||
${tr:Lms.password-confirm}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password-confirm}
|
||||
${password-confirm-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
${</if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.change-password}</legend>
|
||||
<div class="form-horizontal">
|
||||
${<if-has-old-password>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:password-old}">
|
||||
${tr:Lms.password-old}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${password-old}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${password-old-info}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-old-password>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${password}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${password-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:password-confirm}">
|
||||
${tr:Lms.password-confirm}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${password-confirm}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${password-confirm-info}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+28
-34
@@ -22,10 +22,10 @@ LMS.mediaplayer = function () {
|
||||
var _offset = 0;
|
||||
var _duration = 0;
|
||||
var _audioNativeSrc;
|
||||
var _audioTranscodedSrc;
|
||||
var _transcodeMode = TranscodeMode.Never;
|
||||
var _transcodeFormat = 0;
|
||||
var _transcodeBitrate = 0;
|
||||
var _audioTranscodeSrc;
|
||||
var _settings = {};
|
||||
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
var _gainNode = audioCtx.createGain();
|
||||
|
||||
var _updateControls = function() {
|
||||
if (_elems.audio.paused) {
|
||||
@@ -81,27 +81,15 @@ LMS.mediaplayer = function () {
|
||||
_setVolume(_elems.volumeslider.value);
|
||||
}
|
||||
|
||||
var _initTranscodeSettings = function(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) {
|
||||
if (typeof(Storage) !== "undefined" && localStorage.transcodeMode) {
|
||||
_transcodeMode = Number(localStorage.transcodeMode);
|
||||
var _initDefaultSettings = function(defaultSettings) {
|
||||
if (typeof(Storage) !== "undefined" && localStorage.settings) {
|
||||
_settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
|
||||
}
|
||||
else {
|
||||
_transcodeMode = defaultTranscodeMode;
|
||||
}
|
||||
if (typeof(Storage) !== "undefined" && localStorage.transcodeFormat) {
|
||||
_transcodeFormat = Number(localStorage.transcodeFormat);
|
||||
}
|
||||
else {
|
||||
_transcodeFormat = defaultTranscodeFormat;
|
||||
}
|
||||
if (typeof(Storage) !== "undefined" && localStorage.transcodeBitrate) {
|
||||
_transcodeBitrate = Number(localStorage.transcodeBitrate);
|
||||
}
|
||||
else {
|
||||
_transcodeBitrate = defaultTranscodeBitrate;
|
||||
_settings = defaultSettings;
|
||||
}
|
||||
|
||||
Wt.emit(_root, "settingsLoaded", _transcodeMode, _transcodeFormat, _transcodeBitrate);
|
||||
Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
|
||||
}
|
||||
|
||||
var _setVolume = function(volume) {
|
||||
@@ -132,7 +120,12 @@ LMS.mediaplayer = function () {
|
||||
}
|
||||
}
|
||||
|
||||
var init = function(root, defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) {
|
||||
var _setReplayGain = function (replayGain) {
|
||||
console.log("Applying gain " + replayGain);
|
||||
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
|
||||
}
|
||||
|
||||
var init = function(root, defaultSettings) {
|
||||
_root = root;
|
||||
|
||||
_elems.audio = document.getElementById("lms-mp-audio");
|
||||
@@ -146,6 +139,10 @@ LMS.mediaplayer = function () {
|
||||
_elems.volume = document.getElementById("lms-mp-volume");
|
||||
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
||||
|
||||
var source = audioCtx.createMediaElementSource(_elems.audio);
|
||||
source.connect(_gainNode);
|
||||
_gainNode.connect(audioCtx.destination);
|
||||
|
||||
_elems.playpause.addEventListener("click", function() {
|
||||
if (_elems.audio.paused) {
|
||||
if (_elems.audio.firstChild)
|
||||
@@ -199,7 +196,7 @@ LMS.mediaplayer = function () {
|
||||
});
|
||||
|
||||
_initVolume();
|
||||
_initTranscodeSettings(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate);
|
||||
_initDefaultSettings(defaultSettings);
|
||||
|
||||
_elems.volumeslider.addEventListener("input", function() {
|
||||
_setVolume(_elems.volumeslider.value);
|
||||
@@ -251,23 +248,24 @@ LMS.mediaplayer = function () {
|
||||
var loadTrack = function(params, autoplay) {
|
||||
_offset = 0;
|
||||
_duration = params.duration;
|
||||
_audioNativeSrc = params.native_resource;
|
||||
_audioTranscodeSrc = params.transcode_resource + "&bitrate=" + _transcodeBitrate + "&format=" + _transcodeFormat;
|
||||
_audioNativeSrc = params.nativeResource;
|
||||
_audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format;
|
||||
|
||||
_elems.seek.max = _duration;
|
||||
|
||||
_removeAudioSources();
|
||||
// ! order is important
|
||||
if (_transcodeMode == TranscodeMode.Never || _transcodeMode == TranscodeMode.IfFormatNotSupported)
|
||||
if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
|
||||
{
|
||||
_addAudioSource(_audioNativeSrc);
|
||||
}
|
||||
if (_transcodeMode == TranscodeMode.Always || _transcodeMode == TranscodeMode.IfFormatNotSupported)
|
||||
if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
|
||||
{
|
||||
_addAudioSource(_audioTranscodeSrc);
|
||||
}
|
||||
|
||||
_elems.audio.load();
|
||||
_setReplayGain(params.replayGain);
|
||||
|
||||
_elems.curtime.innerHTML = _durationToString(_offset);
|
||||
_elems.duration.innerHTML = _durationToString(_duration);
|
||||
@@ -289,15 +287,11 @@ LMS.mediaplayer = function () {
|
||||
_elems.audio.pause();
|
||||
}
|
||||
|
||||
var setSettings = function(transcodeMode, transcodeFormat, transcodeBitrate) {
|
||||
_transcodeMode = transcodeMode;
|
||||
_transcodeFormat = transcodeFormat;
|
||||
_transcodeBitrate = transcodeBitrate;
|
||||
var setSettings = function(settings) {
|
||||
_settings = settings;
|
||||
|
||||
if (typeof(Storage) !== "undefined") {
|
||||
localStorage.transcodeMode = _transcodeMode;
|
||||
localStorage.transcodeFormat = _transcodeFormat;
|
||||
localStorage.transcodeBitrate = _transcodeBitrate;
|
||||
localStorage.settings = JSON.stringify(_settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 21
|
||||
#define LMS_DATABASE_VERSION 22
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -246,6 +246,14 @@ CREATE TABLE "user_backup" (
|
||||
{
|
||||
_session.execute("DROP TABLE subsonic_settings");
|
||||
}
|
||||
else if (version == 21)
|
||||
{
|
||||
_session.execute("ALTER TABLE track ADD track_replay_gain REAL");
|
||||
_session.execute("ALTER TABLE track ADD release_replay_gain REAL");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
|
||||
@@ -38,6 +38,14 @@ _filePath( p.string() )
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Track::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAll(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
@@ -107,13 +115,26 @@ Track::create(Session& session, const std::filesystem::path& p)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path>
|
||||
Track::getAllPaths(Session& session)
|
||||
std::vector<std::pair<IdType, std::filesystem::path>>
|
||||
Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
{
|
||||
using QueryResultType = std::tuple<IdType, std::string>;
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<std::string> res = session.getDboSession().query<std::string>("SELECT file_path FROM track");
|
||||
return std::vector<std::filesystem::path>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
std::vector<std::pair<IdType, std::filesystem::path>> result;
|
||||
result.reserve(queryRes.size());
|
||||
|
||||
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
|
||||
[](const QueryResultType& queryResult)
|
||||
{
|
||||
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
|
||||
@@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
Track(const std::filesystem::path& p);
|
||||
|
||||
// Find utility functions
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer getByPath(Session& session, const std::filesystem::path& p);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
@@ -74,7 +75,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<std::filesystem::path> getAllPaths(Session& session);
|
||||
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
||||
static std::vector<pointer> getMBIDDuplicates(Session& session);
|
||||
static std::vector<pointer> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
|
||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||
@@ -100,6 +101,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
void setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
|
||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
||||
void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; }
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink);
|
||||
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
||||
@@ -122,6 +125,9 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
|
||||
@@ -152,6 +158,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _copyright, "copyright");
|
||||
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain");
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -184,6 +192,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
std::string _copyright;
|
||||
std::string _copyrightURL;
|
||||
std::optional<float> _trackReplayGain;
|
||||
std::optional<float> _releaseReplayGain;
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
|
||||
@@ -19,13 +19,16 @@
|
||||
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
@@ -263,6 +266,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
track.copyright = value;
|
||||
else if (tag == "COPYRIGHTURL")
|
||||
track.copyrightURL = value;
|
||||
else if (tag == "REPLAYGAIN_ALBUM_GAIN")
|
||||
track.albumReplayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||
track.trackReplayGain = StringUtils::readAs<float>(value);
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
std::set<std::string> clusterNames;
|
||||
@@ -311,6 +318,21 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
TagLib::PropertyMap properties {f.file()->properties()};
|
||||
|
||||
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
||||
{
|
||||
if (!apeTag)
|
||||
return;
|
||||
|
||||
for (const auto& [name, values] : apeTag->properties())
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "APE property: '" << name << "'" << std::endl;
|
||||
|
||||
if (!properties.contains(name))
|
||||
properties.insert(name, values);
|
||||
}
|
||||
};
|
||||
|
||||
// Not that good embedded pictures handling
|
||||
|
||||
// WMA
|
||||
@@ -337,9 +359,10 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
if (!stringAttributeList.isEmpty())
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "Property: '" << name << "'" << std::endl;
|
||||
std::cout << "ASF property: '" << name << "'" << std::endl;
|
||||
|
||||
properties.insert(name, stringAttributeList);
|
||||
if (!properties.contains(name))
|
||||
properties.insert(name, stringAttributeList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +375,17 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
|
||||
track.hasCover = true;
|
||||
}
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
}
|
||||
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())})
|
||||
{
|
||||
getAPETags(mpcFile->APETag());
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File* wavPackFile {dynamic_cast<TagLib::WavPack::File*>(f.file())})
|
||||
{
|
||||
getAPETags(wavPackFile->APETag());
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())})
|
||||
@@ -360,7 +394,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.hasCover = true;
|
||||
}
|
||||
|
||||
for(const auto& property : properties)
|
||||
for (const auto& property : properties)
|
||||
{
|
||||
const std::string tag {property.first.upper().to8Bit(true)};
|
||||
const TagLib::StringList& values {property.second};
|
||||
|
||||
@@ -75,6 +75,8 @@ namespace MetaData
|
||||
std::optional<UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::optional<float> trackReplayGain;
|
||||
std::optional<float> albumReplayGain;
|
||||
};
|
||||
|
||||
class IParser
|
||||
|
||||
@@ -111,7 +111,6 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti
|
||||
// Sortname may have been updated
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() )
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'";
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
@@ -751,6 +750,10 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
track.modify()->setHasCover(trackInfo->hasCover);
|
||||
track.modify()->setCopyright(trackInfo->copyright);
|
||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
||||
if (trackInfo->trackReplayGain)
|
||||
track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain);
|
||||
if (trackInfo->albumReplayGain)
|
||||
track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -819,32 +822,61 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
|
||||
void
|
||||
MediaScanner::removeMissingTracks(ScanStats& stats)
|
||||
{
|
||||
std::vector<std::filesystem::path> trackPaths;
|
||||
static constexpr std::size_t batchSize {50};
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
|
||||
std::size_t trackCount {};
|
||||
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
trackPaths = Track::getAllPaths(_dbSession);;
|
||||
trackCount = Track::getCount(_dbSession);
|
||||
}
|
||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
|
||||
for (const auto& trackPath : trackPaths)
|
||||
std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
|
||||
std::vector<IdType> tracksToRemove;
|
||||
|
||||
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
|
||||
{
|
||||
if (!_running)
|
||||
return;
|
||||
trackPaths.clear();
|
||||
tracksToRemove.clear();
|
||||
|
||||
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
trackPaths = Track::getAllPaths(_dbSession, i, batchSize);
|
||||
}
|
||||
|
||||
for (const auto& [trackId, trackPath] : trackPaths)
|
||||
{
|
||||
if (!_running)
|
||||
return;
|
||||
|
||||
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
|
||||
tracksToRemove.push_back(trackId);
|
||||
}
|
||||
|
||||
if (!tracksToRemove.empty())
|
||||
{
|
||||
auto transaction {_dbSession.createUniqueTransaction()};
|
||||
|
||||
Track::pointer track {Track::getByPath(_dbSession, trackPath)};
|
||||
if (track)
|
||||
for (const IdType trackId : tracksToRemove)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
Track::pointer track {Track::getById(_dbSession, trackId)};
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyInProgressIfNeeded(stats);
|
||||
|
||||
if (i == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -544,16 +544,14 @@ LmsApplication::createHome()
|
||||
playqueue->playNext();
|
||||
});
|
||||
|
||||
playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play)
|
||||
playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play, float replayGain)
|
||||
{
|
||||
_events.lastLoadedTrackId = trackId;
|
||||
_events.trackLoaded(trackId, play);
|
||||
_mediaPlayer->loadTrack(trackId, play, replayGain);
|
||||
});
|
||||
|
||||
playqueue->trackUnselected.connect([=]
|
||||
playqueue->trackUnselected.connect([=] ()
|
||||
{
|
||||
_events.lastLoadedTrackId.reset();
|
||||
_events.trackUnloaded();
|
||||
_mediaPlayer->stop();
|
||||
});
|
||||
|
||||
// Events from MediaScanner
|
||||
|
||||
@@ -53,13 +53,6 @@ struct Events
|
||||
Wt::Signal<LmsApplicationInfo> appOpen;
|
||||
Wt::Signal<LmsApplicationInfo> appClosed;
|
||||
|
||||
// A track is being loaded
|
||||
Wt::Signal<Database::IdType /* trackId */, bool /* play */> trackLoaded;
|
||||
std::optional<Database::IdType> lastLoadedTrackId;
|
||||
Wt::Signal<> mediaPlayerSettingsAvailable;
|
||||
// Unload current track
|
||||
Wt::Signal<> trackUnloaded;
|
||||
|
||||
// Database events
|
||||
Wt::Signal<> dbScanned;
|
||||
Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress;
|
||||
|
||||
@@ -86,8 +86,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
|
||||
case Database::User::UITheme::Dark:
|
||||
return
|
||||
{
|
||||
{"css/bootstrap-darkly.min.css"},
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/bootstrap-darkly.min.css"},
|
||||
{"css/lms.css"},
|
||||
{"css/lms-darkly.css"},
|
||||
};
|
||||
@@ -95,8 +95,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
|
||||
case Database::User::UITheme::Light:
|
||||
return
|
||||
{
|
||||
{"css/bootstrap-flatly.min.css"},
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/bootstrap-flatly.min.css"},
|
||||
{"css/lms.css"},
|
||||
{"css/lms-flatly.css"},
|
||||
};
|
||||
|
||||
+219
-94
@@ -19,6 +19,10 @@
|
||||
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Value.h>
|
||||
#include <Wt/Json/Serializer.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
@@ -31,11 +35,156 @@
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
|
||||
#include "utils/String.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
static std::string settingsToJSString(const MediaPlayer::Settings& settings)
|
||||
{
|
||||
namespace Json = Wt::Json;
|
||||
|
||||
Json::Object res;
|
||||
|
||||
{
|
||||
Json::Object transcode;
|
||||
transcode["mode"] = static_cast<int>(settings.transcode.mode);
|
||||
transcode["format"] = static_cast<int>(settings.transcode.format);
|
||||
transcode["bitrate"] = static_cast<int>(settings.transcode.bitrate);
|
||||
res["transcode"] = std::move(transcode);
|
||||
}
|
||||
|
||||
{
|
||||
Json::Object replayGain;
|
||||
replayGain["mode"] = static_cast<int>(settings.replayGain.mode);
|
||||
replayGain["preAmpGain"] = settings.replayGain.preAmpGain;
|
||||
res["replayGain"] = std::move(replayGain);
|
||||
}
|
||||
|
||||
return Json::serialize(res);
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<MediaPlayer::Settings::Transcode::Mode>
|
||||
transcodeModeFromString(const std::string& str)
|
||||
{
|
||||
const auto value {StringUtils::readAs<int>(str)};
|
||||
if (!value)
|
||||
return std::nullopt;
|
||||
|
||||
MediaPlayer::Settings::Transcode::Mode mode {static_cast<MediaPlayer::Settings::Transcode::Mode>(*value)};
|
||||
switch (mode)
|
||||
{
|
||||
case MediaPlayer::Settings::Transcode::Mode::Never:
|
||||
case MediaPlayer::Settings::Transcode::Mode::Always:
|
||||
case MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported:
|
||||
return mode;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<MediaPlayer::Format>
|
||||
formatFromString(const std::string& str)
|
||||
{
|
||||
const auto value {StringUtils::readAs<int>(str)};
|
||||
if (!value)
|
||||
return std::nullopt;
|
||||
|
||||
MediaPlayer::Format format {static_cast<MediaPlayer::Format>(*value)};
|
||||
switch (format)
|
||||
{
|
||||
case MediaPlayer::Format::MP3:
|
||||
case MediaPlayer::Format::OGG_OPUS:
|
||||
case MediaPlayer::Format::MATROSKA_OPUS:
|
||||
case MediaPlayer::Format::OGG_VORBIS:
|
||||
case MediaPlayer::Format::WEBM_VORBIS:
|
||||
return format;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<MediaPlayer::Bitrate>
|
||||
bitrateFromString(const std::string& str)
|
||||
{
|
||||
const auto value {StringUtils::readAs<int>(str)};
|
||||
if (!value)
|
||||
return std::nullopt;
|
||||
|
||||
if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates))
|
||||
return *value;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<MediaPlayer::Settings::ReplayGain::Mode>
|
||||
replayGainModeFromString(const std::string& str)
|
||||
{
|
||||
const auto value {StringUtils::readAs<int>(str)};
|
||||
if (!value)
|
||||
return std::nullopt;
|
||||
|
||||
MediaPlayer::Settings::ReplayGain::Mode mode {static_cast<MediaPlayer::Settings::ReplayGain::Mode>(*value)};
|
||||
switch (mode)
|
||||
{
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::None:
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Auto:
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Track:
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Release:
|
||||
return mode;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<float>
|
||||
replayGainPreAmpGainFromString(const std::string& str)
|
||||
{
|
||||
const auto value {StringUtils::readAs<double>(str)};
|
||||
if (!value)
|
||||
return std::nullopt;
|
||||
|
||||
return clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
}
|
||||
|
||||
static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings)
|
||||
{
|
||||
using Settings = MediaPlayer::Settings;
|
||||
namespace Json = Wt::Json;
|
||||
Json::Object parsedSettings;
|
||||
|
||||
Json::parse(strSettings, parsedSettings);
|
||||
|
||||
MediaPlayer::Settings settings;
|
||||
|
||||
{
|
||||
const Json::Value transcodeValue {parsedSettings.get("transcode")};
|
||||
if (transcodeValue.type() == Json::Type::Object)
|
||||
{
|
||||
const Json::Object transcode {transcodeValue};
|
||||
settings.transcode.mode = transcodeModeFromString(transcode.get("mode").toString().orIfNull("")).value_or(Settings::Transcode::defaultMode);
|
||||
settings.transcode.format = formatFromString(transcode.get("format").toString().orIfNull("")).value_or(Settings::Transcode::defaultFormat);
|
||||
settings.transcode.bitrate = bitrateFromString(transcode.get("bitrate").toString().orIfNull("")).value_or(Settings::Transcode::defaultBitrate);
|
||||
}
|
||||
}
|
||||
{
|
||||
const Json::Value replayGainValue {parsedSettings.get("replayGain")};
|
||||
if (replayGainValue.type() == Json::Type::Object)
|
||||
{
|
||||
const Json::Object replayGain {replayGainValue};
|
||||
settings.replayGain.mode = replayGainModeFromString(replayGain.get("mode").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultMode);
|
||||
settings.replayGain.preAmpGain = replayGainPreAmpGainFromString(replayGain.get("preAmpGain").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain);
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
MediaPlayer::MediaPlayer()
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")},
|
||||
@@ -48,119 +197,99 @@ MediaPlayer::MediaPlayer()
|
||||
_artist = bindNew<Wt::WAnchor>("artist");
|
||||
_release = bindNew<Wt::WAnchor>("release");
|
||||
|
||||
_settingsLoaded.connect([this](int mode, int format, int bitrate)
|
||||
_settingsLoaded.connect([this](const std::string& settings)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Settings loaded! mode = " << mode << ", format = " << format << ", bitrate = " << bitrate;
|
||||
LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
|
||||
|
||||
Settings settings;
|
||||
_settings = settingsfromJSString(settings);
|
||||
|
||||
switch (static_cast<TranscodeMode>(mode))
|
||||
{
|
||||
case TranscodeMode::Always:
|
||||
case TranscodeMode::Never:
|
||||
case TranscodeMode::IfFormatNotSupported:
|
||||
settings.mode = static_cast<TranscodeMode>(mode);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (static_cast<Format>(format))
|
||||
{
|
||||
case Format::MP3:
|
||||
case Format::OGG_OPUS:
|
||||
case Format::MATROSKA_OPUS:
|
||||
case Format::OGG_VORBIS:
|
||||
case Format::WEBM_VORBIS:
|
||||
settings.format = static_cast<Format>(format);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Database::User::audioTranscodeAllowedBitrates.find(bitrate) != std::cend(Database::User::audioTranscodeAllowedBitrates))
|
||||
settings.bitrate = bitrate;
|
||||
|
||||
_settings = settings;
|
||||
|
||||
LmsApp->getEvents().mediaPlayerSettingsAvailable.emit();
|
||||
settingsLoaded.emit();
|
||||
});
|
||||
|
||||
{
|
||||
Settings defaultSettings;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "LMS.mediaplayer.init("
|
||||
<< jsRef()
|
||||
<< ", " << static_cast<int>(defaultTranscodeMode)
|
||||
<< ", " << static_cast<int>(defaultTranscodeFormat)
|
||||
<< ", " << static_cast<int>(defaultTranscodeBitrate)
|
||||
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
|
||||
<< ")";
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||
doJavaScript(oss.str());
|
||||
}
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect(this, &MediaPlayer::loadTrack);
|
||||
LmsApp->getEvents().trackUnloaded.connect(this, &MediaPlayer::stop);
|
||||
}
|
||||
|
||||
void
|
||||
MediaPlayer::loadTrack(Database::IdType trackId, bool play)
|
||||
MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId;
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
|
||||
|
||||
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
|
||||
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
|
||||
|
||||
const auto artists {track->getArtists()};
|
||||
|
||||
std::ostringstream oss;
|
||||
oss
|
||||
<< "var params = {"
|
||||
<< " native_resource: \"" << nativeResource << "\","
|
||||
<< " transcode_resource: \"" << transcodeResource << "\","
|
||||
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
|
||||
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
|
||||
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
|
||||
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
||||
<< " artwork: ["
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " ]"
|
||||
<< "};";
|
||||
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
|
||||
|
||||
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
|
||||
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
|
||||
|
||||
const auto artists {track->getArtists()};
|
||||
|
||||
oss
|
||||
<< "var params = {"
|
||||
<< " nativeResource: \"" << nativeResource << "\","
|
||||
<< " transcodeResource: \"" << transcodeResource << "\","
|
||||
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
|
||||
<< " replayGain: " << replayGain << ","
|
||||
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
|
||||
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
|
||||
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
||||
<< " artwork: ["
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " ]"
|
||||
<< "};";
|
||||
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||
|
||||
_title->setTextFormat(Wt::TextFormat::Plain);
|
||||
_title->setText(Wt::WString::fromUTF8(track->getName()));
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
_artist->setTextFormat(Wt::TextFormat::Plain);
|
||||
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
|
||||
_artist->setLink(LmsApp->createArtistLink(artists.front()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_artist->setText("");
|
||||
_artist->setLink({});
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
_release->setTextFormat(Wt::TextFormat::Plain);
|
||||
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
|
||||
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_release->setText("");
|
||||
_release->setLink({});
|
||||
}
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||
|
||||
_title->setTextFormat(Wt::TextFormat::Plain);
|
||||
_title->setText(Wt::WString::fromUTF8(track->getName()));
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
_artist->setTextFormat(Wt::TextFormat::Plain);
|
||||
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
|
||||
_artist->setLink(LmsApp->createArtistLink(artists.front()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_artist->setText("");
|
||||
_artist->setLink({});
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
_release->setTextFormat(Wt::TextFormat::Plain);
|
||||
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
|
||||
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_release->setText("");
|
||||
_release->setLink({});
|
||||
}
|
||||
|
||||
wApp->doJavaScript(oss.str());
|
||||
|
||||
_trackIdLoaded = trackId;
|
||||
trackLoaded.emit(*_trackIdLoaded);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -176,11 +305,7 @@ MediaPlayer::setSettings(const Settings& settings)
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "LMS.mediaplayer.setSettings("
|
||||
<< static_cast<int>(_settings->mode)
|
||||
<< ", " << static_cast<int>(_settings->format)
|
||||
<< ", " << static_cast<int>(_settings->bitrate)
|
||||
<< ")";
|
||||
oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||
doJavaScript(oss.str());
|
||||
|
||||
+60
-19
@@ -34,24 +34,59 @@ class MediaPlayer : public Wt::WTemplate
|
||||
public:
|
||||
using Bitrate = Database::Bitrate;
|
||||
using Format = Database::AudioFormat;
|
||||
using Gain = float;
|
||||
|
||||
// Do not change this enum as it may be stored locally in browser
|
||||
// Do not change enum values as they may be stored locally in browser
|
||||
// Keep it sync with LMS.mediaplayer js
|
||||
enum class TranscodeMode
|
||||
{
|
||||
Never = 0,
|
||||
Always = 1,
|
||||
IfFormatNotSupported = 2,
|
||||
};
|
||||
static inline constexpr TranscodeMode defaultTranscodeMode {TranscodeMode::IfFormatNotSupported};
|
||||
static inline constexpr Format defaultTranscodeFormat {Format::OGG_OPUS};
|
||||
static inline constexpr Bitrate defaultTranscodeBitrate {128000};
|
||||
|
||||
struct Settings
|
||||
{
|
||||
TranscodeMode mode {defaultTranscodeMode};
|
||||
Format format {defaultTranscodeFormat};
|
||||
Bitrate bitrate {defaultTranscodeBitrate};
|
||||
struct Transcode
|
||||
{
|
||||
enum class Mode
|
||||
{
|
||||
Never = 0,
|
||||
Always = 1,
|
||||
IfFormatNotSupported = 2,
|
||||
};
|
||||
static inline constexpr Mode defaultMode {Mode::IfFormatNotSupported};
|
||||
static inline constexpr Format defaultFormat {Format::OGG_OPUS};
|
||||
static inline constexpr Bitrate defaultBitrate {128000};
|
||||
|
||||
Mode mode {defaultMode};
|
||||
Format format {defaultFormat};
|
||||
Bitrate bitrate {defaultBitrate};
|
||||
};
|
||||
|
||||
struct ReplayGain
|
||||
{
|
||||
enum class Mode
|
||||
{
|
||||
None = 0,
|
||||
Auto = 1,
|
||||
Track = 2,
|
||||
Release = 3,
|
||||
};
|
||||
|
||||
enum class ClippingPreventionMode
|
||||
{
|
||||
None = 0,
|
||||
LowerVolume = 1,
|
||||
};
|
||||
|
||||
static inline constexpr Mode defaultMode {Mode::None};
|
||||
static inline constexpr Gain defaultPreAmpGain {};
|
||||
static inline constexpr Gain minPreAmpGain {-15};
|
||||
static inline constexpr Gain maxPreAmpGain {15};
|
||||
static inline constexpr ClippingPreventionMode defaultClippingPreventionMode {ClippingPreventionMode::None};
|
||||
|
||||
Mode mode;
|
||||
Gain preAmpGain;
|
||||
ClippingPreventionMode clippingPreventionMode;
|
||||
};
|
||||
|
||||
Transcode transcode;
|
||||
ReplayGain replayGain;
|
||||
};
|
||||
|
||||
MediaPlayer();
|
||||
@@ -61,21 +96,27 @@ class MediaPlayer : public Wt::WTemplate
|
||||
MediaPlayer& operator=(const MediaPlayer&) = delete;
|
||||
MediaPlayer& operator=(MediaPlayer&&) = delete;
|
||||
|
||||
std::optional<Database::IdType> getTrackLoaded() const { return _trackIdLoaded; }
|
||||
|
||||
void loadTrack(Database::IdType trackId, bool play, float replayGain);
|
||||
void stop();
|
||||
|
||||
std::optional<Settings> getSettings() const { return _settings; }
|
||||
void setSettings(const Settings& settings);
|
||||
|
||||
// Signals
|
||||
Wt::JSignal<> playbackEnded;
|
||||
Wt::JSignal<> playPrevious;
|
||||
Wt::JSignal<> playNext;
|
||||
Wt::JSignal<> playbackEnded;
|
||||
Wt::JSignal<> playPrevious;
|
||||
Wt::JSignal<> playNext;
|
||||
Wt::Signal<Database::IdType> trackLoaded;
|
||||
Wt::Signal<> settingsLoaded;
|
||||
|
||||
private:
|
||||
void stop();
|
||||
void loadTrack(Database::IdType trackId, bool play);
|
||||
|
||||
std::optional<Database::IdType> _trackIdLoaded;
|
||||
std::optional<Settings> _settings;
|
||||
|
||||
Wt::JSignal<int, int, int> _settingsLoaded;
|
||||
Wt::JSignal<std::string> _settingsLoaded;
|
||||
Wt::WText* _title;
|
||||
Wt::WAnchor* _release;
|
||||
Wt::WAnchor* _artist;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -99,7 +100,7 @@ PlayHistory::PlayHistory()
|
||||
addSome();
|
||||
});
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect([=](Database::IdType trackId, bool /* play */)
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
@@ -135,8 +136,13 @@ PlayQueue::PlayQueue()
|
||||
|
||||
if (!LmsApp->getUser()->isDemo())
|
||||
{
|
||||
LmsApp->post([=]
|
||||
LmsApp->getMediaPlayer()->settingsLoaded.connect([=]
|
||||
{
|
||||
if (_mediaPlayerSettingsLoaded)
|
||||
return;
|
||||
|
||||
_mediaPlayerSettingsLoaded = true;
|
||||
|
||||
std::size_t trackPos {};
|
||||
|
||||
{
|
||||
@@ -209,6 +215,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
|
||||
|
||||
Database::IdType trackId {};
|
||||
bool addRadioTrack {};
|
||||
std::optional<float> replayGain {};
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
@@ -235,6 +242,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
|
||||
|
||||
trackId = track.id();
|
||||
|
||||
replayGain = getReplayGain(track);
|
||||
|
||||
if (!LmsApp->getUser()->isDemo())
|
||||
LmsApp->getUser().modify()->setCurPlayingTrackPos(pos);
|
||||
}
|
||||
@@ -244,7 +253,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
|
||||
|
||||
updateCurrentTrack(true);
|
||||
|
||||
trackSelected.emit(trackId, play);
|
||||
trackSelected.emit(trackId, play, replayGain ? *replayGain : 0);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -432,5 +441,30 @@ PlayQueue::enqueueRadioTrack()
|
||||
enqueueTracks(trackToAddIds);
|
||||
}
|
||||
|
||||
std::optional<float>
|
||||
PlayQueue::getReplayGain(const Database::Track::pointer& track) const
|
||||
{
|
||||
const auto& settings {LmsApp->getMediaPlayer()->getSettings()};
|
||||
if (!settings)
|
||||
return std::nullopt;
|
||||
|
||||
switch (settings->replayGain.mode)
|
||||
{
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::None:
|
||||
return std::nullopt;
|
||||
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Track:
|
||||
return track->getTrackReplayGain();
|
||||
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Release:
|
||||
return track->getReleaseReplayGain();
|
||||
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Auto:
|
||||
return track->getTrackReplayGain();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Similarity {
|
||||
}
|
||||
|
||||
namespace Database {
|
||||
class Track;
|
||||
class TrackList;
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ class PlayQueue : public Wt::WTemplate
|
||||
void playPrevious();
|
||||
|
||||
// Signal emitted when a track is to be load(and optionally played)
|
||||
Wt::Signal<Database::IdType /*trackId*/, bool /*play*/> trackSelected;
|
||||
Wt::Signal<Database::IdType /*trackId*/, bool /*play*/, float /* replayGain */> trackSelected;
|
||||
|
||||
// Signal emitted when track is unselected (has to be stopped)
|
||||
Wt::Signal<> trackUnselected;
|
||||
@@ -76,9 +77,11 @@ class PlayQueue : public Wt::WTemplate
|
||||
|
||||
void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder);
|
||||
void addRadioTrackFromClusters();
|
||||
std::optional<float> getReplayGain(const Wt::Dbo::ptr<Database::Track>& track) const;
|
||||
|
||||
bool _repeatAll {};
|
||||
bool _radioMode {};
|
||||
bool _mediaPlayerSettingsLoaded {};
|
||||
Database::IdType _tracklistId {};
|
||||
Wt::WContainerWidget* _entriesContainer {};
|
||||
Wt::WPushButton* _showMore {};
|
||||
|
||||
+78
-20
@@ -21,6 +21,8 @@
|
||||
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WDoubleValidator.h>
|
||||
#include <Wt/WDoubleSpinBox.h>
|
||||
#include <Wt/WFormModel.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
@@ -51,6 +53,8 @@ class SettingsModel : public Wt::WFormModel
|
||||
static inline const Field TranscodeModeField {"transcode-mode"};
|
||||
static inline const Field TranscodeFormatField {"transcode-format"};
|
||||
static inline const Field TranscodeBitrateField {"transcode-bitrate"};
|
||||
static inline const Field ReplayGainModeField {"replaygain-mode"};
|
||||
static inline const Field ReplayGainPreAmpGainField {"replaygain-preamp"};
|
||||
static inline const Field SubsonicArtistListModeField {"subsonic-artist-list-mode"};
|
||||
static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"};
|
||||
static inline const Field SubsonicTranscodeFormatField {"subsonic-transcode-format"};
|
||||
@@ -59,7 +63,8 @@ class SettingsModel : public Wt::WFormModel
|
||||
static inline const Field PasswordField {"password"};
|
||||
static inline const Field PasswordConfirmField {"password-confirm"};
|
||||
|
||||
using TranscodeModeModel = ValueStringModel<MediaPlayer::TranscodeMode>;
|
||||
using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>;
|
||||
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
|
||||
|
||||
SettingsModel(bool withOldPassword)
|
||||
: _withOldPassword {withOldPassword}
|
||||
@@ -70,6 +75,8 @@ class SettingsModel : public Wt::WFormModel
|
||||
addField(TranscodeModeField);
|
||||
addField(TranscodeBitrateField);
|
||||
addField(TranscodeFormatField);
|
||||
addField(ReplayGainModeField);
|
||||
addField(ReplayGainPreAmpGainField);
|
||||
addField(SubsonicTranscodeEnableField);
|
||||
addField(SubsonicTranscodeBitrateField);
|
||||
addField(SubsonicTranscodeFormatField);
|
||||
@@ -83,6 +90,12 @@ class SettingsModel : public Wt::WFormModel
|
||||
setValidator(TranscodeModeField, createMandatoryValidator());
|
||||
setValidator(TranscodeBitrateField, createMandatoryValidator());
|
||||
setValidator(TranscodeFormatField, createMandatoryValidator());
|
||||
setValidator(ReplayGainModeField, createMandatoryValidator());
|
||||
{
|
||||
auto preampGainValidator {std::make_unique<Wt::WDoubleValidator>()};
|
||||
preampGainValidator->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
setValidator(ReplayGainPreAmpGainField, std::move(preampGainValidator));
|
||||
}
|
||||
setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator());
|
||||
setValidator(SubsonicTranscodeFormatField, createMandatoryValidator());
|
||||
|
||||
@@ -92,6 +105,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
std::shared_ptr<TranscodeModeModel> getTranscodeModeModel() { return _transcodeModeModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeBitrateModel() { return _transcodeBitrateModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeFormatModel() { return _transcodeFormatModel; }
|
||||
std::shared_ptr<ReplayGainModeModel> getReplayGainModeModel() { return _replayGainModeModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; }
|
||||
|
||||
void saveData()
|
||||
@@ -118,15 +132,21 @@ class SettingsModel : public Wt::WFormModel
|
||||
|
||||
auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))};
|
||||
if (transcodeModeRow)
|
||||
settings.mode = _transcodeModeModel->getValue(*transcodeModeRow);
|
||||
settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow);
|
||||
|
||||
auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))};
|
||||
if (transcodeFormatRow)
|
||||
settings.format = _transcodeFormatModel->getValue(*transcodeFormatRow);
|
||||
settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow);
|
||||
|
||||
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
|
||||
if (transcodeBitrateRow)
|
||||
settings.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow);
|
||||
settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow);
|
||||
|
||||
auto replayGainModeRow {_replayGainModeModel->getRowFromString(valueText(ReplayGainModeField))};
|
||||
if (replayGainModeRow)
|
||||
settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow);
|
||||
|
||||
settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField));
|
||||
|
||||
LmsApp->getMediaPlayer()->setSettings(settings);
|
||||
}
|
||||
@@ -165,17 +185,23 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
const auto& settings {*LmsApp->getMediaPlayer()->getSettings()};
|
||||
|
||||
auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.mode)};
|
||||
auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.transcode.mode)};
|
||||
if (transcodeModeRow)
|
||||
setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow));
|
||||
|
||||
auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.format)};
|
||||
auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.transcode.format)};
|
||||
if (transcodeFormatRow)
|
||||
setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow));
|
||||
|
||||
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.bitrate)};
|
||||
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate)};
|
||||
if (transcodeBitrateRow)
|
||||
setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow));
|
||||
|
||||
auto replayGainModeRow {_replayGainModeModel->getRowFromValue(settings.replayGain.mode)};
|
||||
if (replayGainModeRow)
|
||||
setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow));
|
||||
|
||||
setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain);
|
||||
}
|
||||
|
||||
setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable());
|
||||
@@ -271,13 +297,9 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
|
||||
_transcodeModeModel = std::make_shared<TranscodeModeModel>();
|
||||
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::TranscodeMode::Always);
|
||||
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::TranscodeMode::Never);
|
||||
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::TranscodeMode::IfFormatNotSupported);
|
||||
|
||||
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>();
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists);
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists);
|
||||
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always);
|
||||
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never);
|
||||
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported);
|
||||
|
||||
_transcodeBitrateModel = std::make_shared<ValueStringModel<Bitrate>>();
|
||||
for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates)
|
||||
@@ -291,6 +313,16 @@ class SettingsModel : public Wt::WFormModel
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS);
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS);
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS);
|
||||
|
||||
_replayGainModeModel = std::make_shared<ReplayGainModeModel>();
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None);
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto);
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track);
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release);
|
||||
|
||||
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>();
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists);
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists);
|
||||
}
|
||||
|
||||
bool _withOldPassword {};
|
||||
@@ -298,17 +330,18 @@ class SettingsModel : public Wt::WFormModel
|
||||
std::shared_ptr<TranscodeModeModel> _transcodeModeModel;
|
||||
std::shared_ptr<ValueStringModel<Bitrate>> _transcodeBitrateModel;
|
||||
std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel;
|
||||
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
|
||||
std::shared_ptr<ValueStringModel<User::SubsonicArtistListMode>> _subsonicArtistListModeModel;
|
||||
};
|
||||
|
||||
SettingsView::SettingsView()
|
||||
{
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
wApp->internalPathChanged().connect([=]
|
||||
{
|
||||
refreshView();
|
||||
}));
|
||||
});
|
||||
|
||||
LmsApp->getEvents().mediaPlayerSettingsAvailable.connect([=]()
|
||||
LmsApp->getMediaPlayer()->settingsLoaded.connect([=]()
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
@@ -376,15 +409,40 @@ SettingsView::refreshView()
|
||||
transcodeBitrate->setModel(model->getTranscodeBitrateModel());
|
||||
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate));
|
||||
|
||||
transcodeModeRaw->sactivated().connect([=]()
|
||||
transcodeModeRaw->activated().connect([=](int row)
|
||||
{
|
||||
auto row {model->getTranscodeModeModel()->getRowFromString(model->valueText(SettingsModel::TranscodeModeField))};
|
||||
const bool enable = (row && (model->getTranscodeModeModel()->getValue(*row) != MediaPlayer::TranscodeMode::Never));
|
||||
const bool enable {model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never};
|
||||
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
|
||||
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
|
||||
t->updateModel(model.get());
|
||||
t->updateView(model.get());
|
||||
});
|
||||
if (LmsApp->getMediaPlayer()->getSettings()->transcode.mode == MediaPlayer::Settings::Transcode::Mode::Never)
|
||||
{
|
||||
model->setReadOnly(SettingsModel::TranscodeFormatField, true);
|
||||
model->setReadOnly(SettingsModel::TranscodeBitrateField, true);
|
||||
}
|
||||
|
||||
// Replay gain mode
|
||||
auto replayGainMode {std::make_unique<Wt::WComboBox>()};
|
||||
auto* replayGainModeRaw {replayGainMode.get()};
|
||||
replayGainMode->setModel(model->getReplayGainModeModel());
|
||||
t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode));
|
||||
|
||||
// Replay gain preampGain
|
||||
auto replayGainPreampGain {std::make_unique<Wt::WDoubleSpinBox>()};
|
||||
replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain));
|
||||
|
||||
replayGainModeRaw->activated().connect([=](int row)
|
||||
{
|
||||
const bool enable {model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None};
|
||||
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable);
|
||||
t->updateModel(model.get());
|
||||
t->updateView(model.get());
|
||||
});
|
||||
if (LmsApp->getMediaPlayer()->getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None)
|
||||
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true);
|
||||
}
|
||||
|
||||
// Subsonic
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "database/User.hpp"
|
||||
#include "ArtistLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
@@ -44,7 +45,7 @@ ArtistsInfo::ArtistsInfo()
|
||||
refreshRecentlyAdded();
|
||||
});
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect([=]
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
||||
{
|
||||
refreshMostPlayed();
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -46,10 +47,10 @@ namespace UserInterface {
|
||||
Release::Release(Filters* filters)
|
||||
: _filters(filters)
|
||||
{
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
wApp->internalPathChanged().connect([=]()
|
||||
{
|
||||
refresh();
|
||||
}));
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
@@ -217,27 +218,28 @@ Release::refresh()
|
||||
}
|
||||
|
||||
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect(std::bind([=]
|
||||
playBtn->clicked().connect([=]()
|
||||
{
|
||||
tracksPlay.emit({trackId});
|
||||
}));
|
||||
});
|
||||
|
||||
Wt::WText* addBtn {entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||
addBtn->clicked().connect(std::bind([=]
|
||||
addBtn->clicked().connect([=]()
|
||||
{
|
||||
tracksAdd.emit({trackId});
|
||||
}));
|
||||
});
|
||||
|
||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/)
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
|
||||
{
|
||||
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
|
||||
});
|
||||
|
||||
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId)
|
||||
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
|
||||
{
|
||||
entry->bindString("is-playing", "Lms-entry-playing");
|
||||
if (*trackIdLoaded == trackId)
|
||||
entry->bindString("is-playing", "Lms-entry-playing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "ReleaseLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
@@ -45,7 +46,7 @@ ReleasesInfo::ReleasesInfo()
|
||||
refreshRecentlyAdded();
|
||||
});
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect([=]
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
||||
{
|
||||
refreshMostPlayed();
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
@@ -49,7 +50,7 @@ void addEntries(Wt::WContainerWidget *container, const std::vector<Track::pointe
|
||||
namespace UserInterface {
|
||||
|
||||
TracksInfo::TracksInfo()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.TracksInfo.template"))
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.TracksInfo.template")}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
@@ -61,7 +62,7 @@ TracksInfo::TracksInfo()
|
||||
refreshRecentlyAdded();
|
||||
});
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect([=]
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
||||
{
|
||||
refreshMostPlayed();
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -174,14 +175,15 @@ Tracks::addSome()
|
||||
}));
|
||||
|
||||
|
||||
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/)
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
|
||||
{
|
||||
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
|
||||
});
|
||||
|
||||
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId)
|
||||
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
|
||||
{
|
||||
entry->bindString("is-playing", "Lms-entry-playing");
|
||||
if (*trackIdLoaded == trackId)
|
||||
entry->bindString("is-playing", "Lms-entry-playing");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,12 +154,19 @@ static
|
||||
void
|
||||
testSingleTrack(Session& session)
|
||||
{
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
CHECK(Track::getCount(session) == 0);
|
||||
}
|
||||
|
||||
ScopedTrack track {session, "MyTrackFile"};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
CHECK(Track::getAll(session).size() == 1);
|
||||
CHECK(Track::getCount(session) == 1);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,12 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
for (const auto& audioStream : track->audioStreams)
|
||||
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl;
|
||||
|
||||
if (track->trackReplayGain)
|
||||
std::cout << "Track replay gain: " << *track->trackReplayGain << std::endl;
|
||||
|
||||
if (track->albumReplayGain)
|
||||
std::cout << "Album replay gain: " << *track->albumReplayGain << std::endl;
|
||||
|
||||
if (track->acoustID)
|
||||
std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user