Merge branch 'replaygain' into develop. Fixes #38

This commit is contained in:
emeric
2020-04-26 13:30:55 +02:00
26 changed files with 755 additions and 372 deletions
+6
View File
@@ -164,6 +164,12 @@
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message> <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-bad">Bad password</message>
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</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">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.all-artists">All artists</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
+7
View File
@@ -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.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-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.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">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.all-artists">Tous les artistes</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</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.Yes">Oui</message>
<message id="Wt.WMessageBox.No">Non</message> <message id="Wt.WMessageBox.No">Non</message>
<message id="Wt.WValidator.Invalid">Ce champ ne peut pas être vide</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> </messages>
+143 -147
View File
@@ -4,161 +4,157 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Settings.template"> <message id="Lms.Settings.template">
<legend>${tr:Lms.Settings.appearance}</legend> <div class="row">
<div class="form-horizontal"> <div class="col-lg-6">
<div class="form-group"> <legend>${tr:Lms.Settings.appearance}</legend>
<label class="col-sm-2 control-label" for="${id:dark-mode}"> <div class="form-horizontal">
${tr:Lms.Settings.dark-mode} <div class="form-group">
</label> <div class="col-lg-offset-3 col-lg-9">
<div class="col-sm-5"> <div class="checkbox">
<div class="checkbox"><label>${dark-mode}</label></div> <label>${dark-mode}${tr:Lms.Settings.dark-mode}</label>
${dark-mode-info clas="help-block"}
</div>
</div>
</div>
</div> </div>
<div class="col-sm-5 help-block"> <legend>${tr:Lms.Settings.audio}</legend>
${dark-mode-info} <div class="alert alert-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">
${tr:Lms.Settings.audio-settings-are-local} ${tr:Lms.Settings.audio-settings-are-local}
</div> </div>
</div> <div class="form-horizontal">
</div> <div class="form-group">
<div class="form-horizontal"> <label class="col-lg-3 control-label" for="${id:transcode-mode}">
<div class="form-group"> ${tr:Lms.Settings.transcode-mode}
<label class="col-sm-2 control-label" for="${id:transcode-mode}"> </label>
${tr:Lms.Settings.transcode-mode} <div class="col-lg-9">
</label> ${transcode-mode}
<div class="col-sm-5"> ${transcode-mode-info class="help-block"}
${transcode-mode} </div>
</div> </div>
<div class="col-sm-5 help-block"> <div class="form-group">
${transcode-mode-info} <label class="control-label col-lg-3" for="${id:transcode-format}">
</div> ${tr:Lms.Settings.transcode-format}
</div> </label>
<div class="form-group"> <div class="col-lg-9">
<label class="control-label col-sm-2" for="${id:transcode-format}"> ${transcode-format}
${tr:Lms.Settings.transcode-format} ${transcode-format-info class="help-block"}
</label> </div>
<div class="col-sm-5"> </div>
${transcode-format} <div class="form-group">
</div> <label class="control-label col-lg-3" for="${id:transcode-bitrate}">
<div class="help-block col-sm-5"> ${tr:Lms.Settings.transcode-bitrate}
${transcode-format-info} </label>
</div> <div class="col-lg-9">
</div> <div class="input-group">
<div class="form-group"> ${transcode-bitrate}
<label class="control-label col-sm-2" for="${id:transcode-bitrate}"> <span class="input-group-addon">kbps</span>
${tr:Lms.Settings.transcode-bitrate} </div>
</label> ${transcode-bitrate-info class="help-block"}
<div class="col-sm-5"> </div>
<div class="input-group"> </div>
${transcode-bitrate}
<span class="input-group-addon">kbps</span> <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> </div>
<div class="help-block col-sm-5"> ${<if-has-subsonic-api>}
${transcode-bitrate-info} <legend>${tr:Lms.Settings.subsonic-api}</legend>
</div> <div class="form-horizontal">
</div> <div class="form-group">
</div> <div class="col-lg-offset-3 col-lg-9">
${<if-has-subsonic-api>} <div class="checkbox">
<legend>${tr:Lms.Settings.subsonic-api}</legend> <label>${subsonic-transcode-enable}${tr:Lms.Settings.transcode-enable}</label>
<div class="form-horizontal"> ${subsonic-transcode-enable-info class="help-block"}
</div>
<div class="form-group"> </div>
<label class="col-sm-2 control-label" for="${id:subsonic-transcode-enable}"> </div>
${tr:Lms.Settings.transcode-enable} <div class="form-group">
</label> <label class="control-label col-lg-3" for="${id:subsonic-transcode-format}">
<div class="col-sm-5"> ${tr:Lms.Settings.transcode-format}
<div class="checkbox"><label>${subsonic-transcode-enable}</label></div> </label>
</div> <div class="col-lg-9">
<div class="col-sm-5 help-block"> ${subsonic-transcode-format}
${subsonic-transcode-enable-info} ${subsonic-transcode-format-info class="help-block"}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2" for="${id:subsonic-transcode-format}"> <label class="control-label col-lg-3" for="${id:subsonic-transcode-bitrate}">
${tr:Lms.Settings.transcode-format} ${tr:Lms.Settings.transcode-bitrate}
</label> </label>
<div class="col-sm-5"> <div class="col-lg-9">
${subsonic-transcode-format} <div class="input-group">
</div> ${subsonic-transcode-bitrate}
<div class="col-sm-5 help-block"> <span class="input-group-addon">kbps</span>
${subsonic-transcode-format-info} </div>
</div> ${subsonic-transcode-bitrate-info class="help-block"}
</div> </div>
<div class="form-group"> </div>
<label class="control-label col-sm-2" for="${id:subsonic-transcode-bitrate}"> <div class="form-group">
${tr:Lms.Settings.transcode-bitrate} <label class="control-label col-lg-3" for="${id:subsonic-artist-list-mode}">
</label> ${tr:Lms.Settings.subsonic-artist-list-mode}
<div class="col-sm-5"> </label>
<div class="input-group"> <div class="col-lg-9">
${subsonic-transcode-bitrate} ${subsonic-artist-list-mode}
<span class="input-group-addon">kbps</span> ${subsonic-artist-list-mode-info class="help-block"}
</div>
</div> </div>
</div> </div>
<div class="help-block col-sm-5"> ${</if-has-subsonic-api>}
${subsonic-transcode-bitrate-info} <legend>${tr:Lms.Settings.change-password}</legend>
</div> <div class="form-horizontal">
</div> ${<if-has-old-password>}
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2" for="${id:subsonic-artist-list-mode}"> <label class="control-label col-lg-3" for="${id:password-old}">
${tr:Lms.Settings.subsonic-artist-list-mode} ${tr:Lms.password-old}
</label> </label>
<div class="col-sm-5"> <div class="col-lg-9">
${subsonic-artist-list-mode} ${password-old}
</div> ${password-old-info class="help-block"}
<div class="help-block col-sm-5"> </div>
${subsonic-artist-list-mode-info} </div>
</div> ${</if-has-old-password>}
</div> <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> <div class="form-group">
${</if-has-subsonic-api>} <div class="col-lg-offset-3 col-lg-9">
<legend>${tr:Lms.Settings.change-password}</legend> ${apply-btn class="btn-primary"} ${discard-btn}
<div class="form-horizontal"> </div>
${<if-has-old-password>} </div>
<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> </div>
</div> </div>
</div> </div>
+28 -34
View File
@@ -22,10 +22,10 @@ LMS.mediaplayer = function () {
var _offset = 0; var _offset = 0;
var _duration = 0; var _duration = 0;
var _audioNativeSrc; var _audioNativeSrc;
var _audioTranscodedSrc; var _audioTranscodeSrc;
var _transcodeMode = TranscodeMode.Never; var _settings = {};
var _transcodeFormat = 0; var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var _transcodeBitrate = 0; var _gainNode = audioCtx.createGain();
var _updateControls = function() { var _updateControls = function() {
if (_elems.audio.paused) { if (_elems.audio.paused) {
@@ -81,27 +81,15 @@ LMS.mediaplayer = function () {
_setVolume(_elems.volumeslider.value); _setVolume(_elems.volumeslider.value);
} }
var _initTranscodeSettings = function(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { var _initDefaultSettings = function(defaultSettings) {
if (typeof(Storage) !== "undefined" && localStorage.transcodeMode) { if (typeof(Storage) !== "undefined" && localStorage.settings) {
_transcodeMode = Number(localStorage.transcodeMode); _settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
} }
else { else {
_transcodeMode = defaultTranscodeMode; _settings = defaultSettings;
}
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;
} }
Wt.emit(_root, "settingsLoaded", _transcodeMode, _transcodeFormat, _transcodeBitrate); Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
} }
var _setVolume = function(volume) { 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; _root = root;
_elems.audio = document.getElementById("lms-mp-audio"); _elems.audio = document.getElementById("lms-mp-audio");
@@ -146,6 +139,10 @@ LMS.mediaplayer = function () {
_elems.volume = document.getElementById("lms-mp-volume"); _elems.volume = document.getElementById("lms-mp-volume");
_elems.volumeslider = document.getElementById("lms-mp-volume-slider"); _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() { _elems.playpause.addEventListener("click", function() {
if (_elems.audio.paused) { if (_elems.audio.paused) {
if (_elems.audio.firstChild) if (_elems.audio.firstChild)
@@ -199,7 +196,7 @@ LMS.mediaplayer = function () {
}); });
_initVolume(); _initVolume();
_initTranscodeSettings(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate); _initDefaultSettings(defaultSettings);
_elems.volumeslider.addEventListener("input", function() { _elems.volumeslider.addEventListener("input", function() {
_setVolume(_elems.volumeslider.value); _setVolume(_elems.volumeslider.value);
@@ -251,23 +248,24 @@ LMS.mediaplayer = function () {
var loadTrack = function(params, autoplay) { var loadTrack = function(params, autoplay) {
_offset = 0; _offset = 0;
_duration = params.duration; _duration = params.duration;
_audioNativeSrc = params.native_resource; _audioNativeSrc = params.nativeResource;
_audioTranscodeSrc = params.transcode_resource + "&bitrate=" + _transcodeBitrate + "&format=" + _transcodeFormat; _audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format;
_elems.seek.max = _duration; _elems.seek.max = _duration;
_removeAudioSources(); _removeAudioSources();
// ! order is important // ! order is important
if (_transcodeMode == TranscodeMode.Never || _transcodeMode == TranscodeMode.IfFormatNotSupported) if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
{ {
_addAudioSource(_audioNativeSrc); _addAudioSource(_audioNativeSrc);
} }
if (_transcodeMode == TranscodeMode.Always || _transcodeMode == TranscodeMode.IfFormatNotSupported) if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
{ {
_addAudioSource(_audioTranscodeSrc); _addAudioSource(_audioTranscodeSrc);
} }
_elems.audio.load(); _elems.audio.load();
_setReplayGain(params.replayGain);
_elems.curtime.innerHTML = _durationToString(_offset); _elems.curtime.innerHTML = _durationToString(_offset);
_elems.duration.innerHTML = _durationToString(_duration); _elems.duration.innerHTML = _durationToString(_duration);
@@ -289,15 +287,11 @@ LMS.mediaplayer = function () {
_elems.audio.pause(); _elems.audio.pause();
} }
var setSettings = function(transcodeMode, transcodeFormat, transcodeBitrate) { var setSettings = function(settings) {
_transcodeMode = transcodeMode; _settings = settings;
_transcodeFormat = transcodeFormat;
_transcodeBitrate = transcodeBitrate;
if (typeof(Storage) !== "undefined") { if (typeof(Storage) !== "undefined") {
localStorage.transcodeMode = _transcodeMode; localStorage.settings = JSON.stringify(_settings);
localStorage.transcodeFormat = _transcodeFormat;
localStorage.transcodeBitrate = _transcodeBitrate;
} }
} }
+9 -1
View File
@@ -40,7 +40,7 @@
namespace Database { namespace Database {
#define LMS_DATABASE_VERSION 21 #define LMS_DATABASE_VERSION 22
using Version = std::size_t; using Version = std::size_t;
@@ -246,6 +246,14 @@ CREATE TABLE "user_backup" (
{ {
_session.execute("DROP TABLE subsonic_settings"); _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 else
{ {
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
+25 -4
View File
@@ -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> std::vector<Track::pointer>
Track::getAll(Session& session, std::optional<std::size_t> limit) 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; return res;
} }
std::vector<std::filesystem::path> std::vector<std::pair<IdType, std::filesystem::path>>
Track::getAllPaths(Session& session) 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(); session.checkSharedLocked();
Wt::Dbo::collection<std::string> res = session.getDboSession().query<std::string>("SELECT file_path FROM track"); Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
return std::vector<std::filesystem::path>(res.begin(), res.end()); .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> std::vector<Track::pointer>
+11 -1
View File
@@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo<Track>
Track(const std::filesystem::path& p); Track(const std::filesystem::path& p);
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session);
static pointer getByPath(Session& session, const std::filesystem::path& p); static pointer getByPath(Session& session, const std::filesystem::path& p);
static pointer getById(Session& session, IdType id); static pointer getById(Session& session, IdType id);
static pointer getByMBID(Session& session, const UUID& MBID); 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> 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<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
static std::vector<IdType> getAllIds(Session& session); 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> 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> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session); 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 setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } 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 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 clearArtistLinks();
void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink); void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink);
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; } 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<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() 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<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<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() 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, _MBID, "mbid");
Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url"); 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::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); 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 _MBID; // Musicbrainz Identifier
std::string _copyright; std::string _copyright;
std::string _copyrightURL; std::string _copyrightURL;
std::optional<float> _trackReplayGain;
std::optional<float> _releaseReplayGain;
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
+37 -3
View File
@@ -19,13 +19,16 @@
#include "metadata/TagLibParser.hpp" #include "metadata/TagLibParser.hpp"
#include <taglib/apetag.h>
#include <taglib/asffile.h> #include <taglib/asffile.h>
#include <taglib/id3v2tag.h> #include <taglib/id3v2tag.h>
#include <taglib/fileref.h> #include <taglib/fileref.h>
#include <taglib/flacfile.h> #include <taglib/flacfile.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h> #include <taglib/mpegfile.h>
#include <taglib/tag.h> #include <taglib/tag.h>
#include <taglib/tpropertymap.h> #include <taglib/tpropertymap.h>
#include <taglib/wavpackfile.h>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
@@ -263,6 +266,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
track.copyright = value; track.copyright = value;
else if (tag == "COPYRIGHTURL") else if (tag == "COPYRIGHTURL")
track.copyrightURL = value; 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()) else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{ {
std::set<std::string> clusterNames; std::set<std::string> clusterNames;
@@ -311,6 +318,21 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
TagLib::PropertyMap properties {f.file()->properties()}; 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 // Not that good embedded pictures handling
// WMA // WMA
@@ -337,9 +359,10 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
if (!stringAttributeList.isEmpty()) if (!stringAttributeList.isEmpty())
{ {
if (debug) 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()) if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
track.hasCover = true; 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 // FLAC
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())}) 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; track.hasCover = true;
} }
for(const auto& property : properties) for (const auto& property : properties)
{ {
const std::string tag {property.first.upper().to8Bit(true)}; const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second}; const TagLib::StringList& values {property.second};
@@ -75,6 +75,8 @@ namespace MetaData
std::optional<UUID> acoustID; std::optional<UUID> acoustID;
std::string copyright; std::string copyright;
std::string copyrightURL; std::string copyrightURL;
std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain;
}; };
class IParser class IParser
+44 -12
View File
@@ -111,7 +111,6 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti
// Sortname may have been updated // Sortname may have been updated
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() ) if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() )
{ {
LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'";
artist.modify()->setSortName(*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()->setHasCover(trackInfo->hasCover);
track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyright(trackInfo->copyright);
track.modify()->setCopyrightURL(trackInfo->copyrightURL); track.modify()->setCopyrightURL(trackInfo->copyrightURL);
if (trackInfo->trackReplayGain)
track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain);
if (trackInfo->albumReplayGain)
track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain);
} }
void void
@@ -819,32 +822,61 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
void void
MediaScanner::removeMissingTracks(ScanStats& stats) 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()}; 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..."; std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
for (const auto& trackPath : trackPaths) std::vector<IdType> tracksToRemove;
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
{ {
if (!_running) trackPaths.clear();
return; 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()}; auto transaction {_dbSession.createUniqueTransaction()};
Track::pointer track {Track::getByPath(_dbSession, trackPath)}; for (const IdType trackId : tracksToRemove)
if (track)
{ {
track.remove(); Track::pointer track {Track::getById(_dbSession, trackId)};
stats.deletions++; if (track)
{
track.remove();
stats.deletions++;
}
} }
} }
notifyInProgressIfNeeded(stats); notifyInProgressIfNeeded(stats);
if (i == 0)
break;
} }
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
} }
void void
+4 -6
View File
@@ -544,16 +544,14 @@ LmsApplication::createHome()
playqueue->playNext(); playqueue->playNext();
}); });
playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play) playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play, float replayGain)
{ {
_events.lastLoadedTrackId = trackId; _mediaPlayer->loadTrack(trackId, play, replayGain);
_events.trackLoaded(trackId, play);
}); });
playqueue->trackUnselected.connect([=] playqueue->trackUnselected.connect([=] ()
{ {
_events.lastLoadedTrackId.reset(); _mediaPlayer->stop();
_events.trackUnloaded();
}); });
// Events from MediaScanner // Events from MediaScanner
-7
View File
@@ -53,13 +53,6 @@ struct Events
Wt::Signal<LmsApplicationInfo> appOpen; Wt::Signal<LmsApplicationInfo> appOpen;
Wt::Signal<LmsApplicationInfo> appClosed; 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 // Database events
Wt::Signal<> dbScanned; Wt::Signal<> dbScanned;
Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress; Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress;
+2 -2
View File
@@ -86,8 +86,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
case Database::User::UITheme::Dark: case Database::User::UITheme::Dark:
return return
{ {
{"css/bootstrap-darkly.min.css"},
{"resources/themes/bootstrap/3/wt.css"}, {"resources/themes/bootstrap/3/wt.css"},
{"css/bootstrap-darkly.min.css"},
{"css/lms.css"}, {"css/lms.css"},
{"css/lms-darkly.css"}, {"css/lms-darkly.css"},
}; };
@@ -95,8 +95,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
case Database::User::UITheme::Light: case Database::User::UITheme::Light:
return return
{ {
{"css/bootstrap-flatly.min.css"},
{"resources/themes/bootstrap/3/wt.css"}, {"resources/themes/bootstrap/3/wt.css"},
{"css/bootstrap-flatly.min.css"},
{"css/lms.css"}, {"css/lms.css"},
{"css/lms-flatly.css"}, {"css/lms-flatly.css"},
}; };
+219 -94
View File
@@ -19,6 +19,10 @@
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
@@ -31,11 +35,156 @@
#include "resource/AudioFileResource.hpp" #include "resource/AudioFileResource.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "utils/Utils.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
namespace UserInterface { 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() MediaPlayer::MediaPlayer()
: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")},
@@ -48,119 +197,99 @@ MediaPlayer::MediaPlayer()
_artist = bindNew<Wt::WAnchor>("artist"); _artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release"); _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)) settingsLoaded.emit();
{
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();
}); });
{ {
Settings defaultSettings;
std::ostringstream oss; std::ostringstream oss;
oss << "LMS.mediaplayer.init(" oss << "LMS.mediaplayer.init("
<< jsRef() << jsRef()
<< ", " << static_cast<int>(defaultTranscodeMode) << ", defaultSettings = " << settingsToJSString(defaultSettings)
<< ", " << static_cast<int>(defaultTranscodeFormat)
<< ", " << static_cast<int>(defaultTranscodeBitrate)
<< ")"; << ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str()); doJavaScript(oss.str());
} }
LmsApp->getEvents().trackLoaded.connect(this, &MediaPlayer::loadTrack);
LmsApp->getEvents().trackUnloaded.connect(this, &MediaPlayer::stop);
} }
void 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; 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; std::ostringstream oss;
oss {
<< "var params = {" auto transaction {LmsApp->getDbSession().createSharedTransaction()};
<< " native_resource: \"" << nativeResource << "\","
<< " transcode_resource: \"" << transcodeResource << "\"," const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "," if (!track)
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," return;
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" }," const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" }," const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]" const auto artists {track->getArtists()};
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay 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() << "'"; 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()); wApp->doJavaScript(oss.str());
_trackIdLoaded = trackId;
trackLoaded.emit(*_trackIdLoaded);
} }
void void
@@ -176,11 +305,7 @@ MediaPlayer::setSettings(const Settings& settings)
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "LMS.mediaplayer.setSettings(" oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
<< static_cast<int>(_settings->mode)
<< ", " << static_cast<int>(_settings->format)
<< ", " << static_cast<int>(_settings->bitrate)
<< ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str()); doJavaScript(oss.str());
+60 -19
View File
@@ -34,24 +34,59 @@ class MediaPlayer : public Wt::WTemplate
public: public:
using Bitrate = Database::Bitrate; using Bitrate = Database::Bitrate;
using Format = Database::AudioFormat; 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 // 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 struct Settings
{ {
TranscodeMode mode {defaultTranscodeMode}; struct Transcode
Format format {defaultTranscodeFormat}; {
Bitrate bitrate {defaultTranscodeBitrate}; 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(); MediaPlayer();
@@ -61,21 +96,27 @@ class MediaPlayer : public Wt::WTemplate
MediaPlayer& operator=(const MediaPlayer&) = delete; MediaPlayer& operator=(const MediaPlayer&) = delete;
MediaPlayer& operator=(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; } std::optional<Settings> getSettings() const { return _settings; }
void setSettings(const Settings& settings); void setSettings(const Settings& settings);
// Signals // Signals
Wt::JSignal<> playbackEnded; Wt::JSignal<> playbackEnded;
Wt::JSignal<> playPrevious; Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext; Wt::JSignal<> playNext;
Wt::Signal<Database::IdType> trackLoaded;
Wt::Signal<> settingsLoaded;
private: private:
void stop();
void loadTrack(Database::IdType trackId, bool play);
std::optional<Database::IdType> _trackIdLoaded;
std::optional<Settings> _settings; std::optional<Settings> _settings;
Wt::JSignal<int, int, int> _settingsLoaded; Wt::JSignal<std::string> _settingsLoaded;
Wt::WText* _title; Wt::WText* _title;
Wt::WAnchor* _release; Wt::WAnchor* _release;
Wt::WAnchor* _artist; Wt::WAnchor* _artist;
+2 -1
View File
@@ -30,6 +30,7 @@
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
namespace { namespace {
@@ -99,7 +100,7 @@ PlayHistory::PlayHistory()
addSome(); addSome();
}); });
LmsApp->getEvents().trackLoaded.connect([=](Database::IdType trackId, bool /* play */) LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId)
{ {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
+37 -3
View File
@@ -33,8 +33,9 @@
#include "utils/String.hpp" #include "utils/String.hpp"
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "TrackStringUtils.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp"
namespace UserInterface { namespace UserInterface {
@@ -135,8 +136,13 @@ PlayQueue::PlayQueue()
if (!LmsApp->getUser()->isDemo()) if (!LmsApp->getUser()->isDemo())
{ {
LmsApp->post([=] LmsApp->getMediaPlayer()->settingsLoaded.connect([=]
{ {
if (_mediaPlayerSettingsLoaded)
return;
_mediaPlayerSettingsLoaded = true;
std::size_t trackPos {}; std::size_t trackPos {};
{ {
@@ -209,6 +215,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
Database::IdType trackId {}; Database::IdType trackId {};
bool addRadioTrack {}; bool addRadioTrack {};
std::optional<float> replayGain {};
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
@@ -235,6 +242,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
trackId = track.id(); trackId = track.id();
replayGain = getReplayGain(track);
if (!LmsApp->getUser()->isDemo()) if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setCurPlayingTrackPos(pos); LmsApp->getUser().modify()->setCurPlayingTrackPos(pos);
} }
@@ -244,7 +253,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
updateCurrentTrack(true); updateCurrentTrack(true);
trackSelected.emit(trackId, play); trackSelected.emit(trackId, play, replayGain ? *replayGain : 0);
} }
void void
@@ -432,5 +441,30 @@ PlayQueue::enqueueRadioTrack()
enqueueTracks(trackToAddIds); 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 } // namespace UserInterface
+4 -1
View File
@@ -33,6 +33,7 @@ namespace Similarity {
} }
namespace Database { namespace Database {
class Track;
class TrackList; class TrackList;
} }
@@ -53,7 +54,7 @@ class PlayQueue : public Wt::WTemplate
void playPrevious(); void playPrevious();
// Signal emitted when a track is to be load(and optionally played) // 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) // Signal emitted when track is unselected (has to be stopped)
Wt::Signal<> trackUnselected; Wt::Signal<> trackUnselected;
@@ -76,9 +77,11 @@ class PlayQueue : public Wt::WTemplate
void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder); void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder);
void addRadioTrackFromClusters(); void addRadioTrackFromClusters();
std::optional<float> getReplayGain(const Wt::Dbo::ptr<Database::Track>& track) const;
bool _repeatAll {}; bool _repeatAll {};
bool _radioMode {}; bool _radioMode {};
bool _mediaPlayerSettingsLoaded {};
Database::IdType _tracklistId {}; Database::IdType _tracklistId {};
Wt::WContainerWidget* _entriesContainer {}; Wt::WContainerWidget* _entriesContainer {};
Wt::WPushButton* _showMore {}; Wt::WPushButton* _showMore {};
+78 -20
View File
@@ -21,6 +21,8 @@
#include <Wt/WCheckBox.h> #include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h> #include <Wt/WComboBox.h>
#include <Wt/WDoubleValidator.h>
#include <Wt/WDoubleSpinBox.h>
#include <Wt/WFormModel.h> #include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h> #include <Wt/WLineEdit.h>
#include <Wt/WPushButton.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 TranscodeModeField {"transcode-mode"};
static inline const Field TranscodeFormatField {"transcode-format"}; static inline const Field TranscodeFormatField {"transcode-format"};
static inline const Field TranscodeBitrateField {"transcode-bitrate"}; 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 SubsonicArtistListModeField {"subsonic-artist-list-mode"};
static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"}; static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"};
static inline const Field SubsonicTranscodeFormatField {"subsonic-transcode-format"}; 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 PasswordField {"password"};
static inline const Field PasswordConfirmField {"password-confirm"}; 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) SettingsModel(bool withOldPassword)
: _withOldPassword {withOldPassword} : _withOldPassword {withOldPassword}
@@ -70,6 +75,8 @@ class SettingsModel : public Wt::WFormModel
addField(TranscodeModeField); addField(TranscodeModeField);
addField(TranscodeBitrateField); addField(TranscodeBitrateField);
addField(TranscodeFormatField); addField(TranscodeFormatField);
addField(ReplayGainModeField);
addField(ReplayGainPreAmpGainField);
addField(SubsonicTranscodeEnableField); addField(SubsonicTranscodeEnableField);
addField(SubsonicTranscodeBitrateField); addField(SubsonicTranscodeBitrateField);
addField(SubsonicTranscodeFormatField); addField(SubsonicTranscodeFormatField);
@@ -83,6 +90,12 @@ class SettingsModel : public Wt::WFormModel
setValidator(TranscodeModeField, createMandatoryValidator()); setValidator(TranscodeModeField, createMandatoryValidator());
setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator());
setValidator(TranscodeFormatField, 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(SubsonicTranscodeBitrateField, createMandatoryValidator());
setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); setValidator(SubsonicTranscodeFormatField, createMandatoryValidator());
@@ -92,6 +105,7 @@ class SettingsModel : public Wt::WFormModel
std::shared_ptr<TranscodeModeModel> getTranscodeModeModel() { return _transcodeModeModel; } std::shared_ptr<TranscodeModeModel> getTranscodeModeModel() { return _transcodeModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeBitrateModel() { return _transcodeBitrateModel; } std::shared_ptr<Wt::WAbstractItemModel> getTranscodeBitrateModel() { return _transcodeBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeFormatModel() { return _transcodeFormatModel; } std::shared_ptr<Wt::WAbstractItemModel> getTranscodeFormatModel() { return _transcodeFormatModel; }
std::shared_ptr<ReplayGainModeModel> getReplayGainModeModel() { return _replayGainModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; }
void saveData() void saveData()
@@ -118,15 +132,21 @@ class SettingsModel : public Wt::WFormModel
auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))}; auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))};
if (transcodeModeRow) if (transcodeModeRow)
settings.mode = _transcodeModeModel->getValue(*transcodeModeRow); settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow);
auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))};
if (transcodeFormatRow) if (transcodeFormatRow)
settings.format = _transcodeFormatModel->getValue(*transcodeFormatRow); settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow);
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
if (transcodeBitrateRow) 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); LmsApp->getMediaPlayer()->setSettings(settings);
} }
@@ -165,17 +185,23 @@ class SettingsModel : public Wt::WFormModel
{ {
const auto& settings {*LmsApp->getMediaPlayer()->getSettings()}; const auto& settings {*LmsApp->getMediaPlayer()->getSettings()};
auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.mode)}; auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.transcode.mode)};
if (transcodeModeRow) if (transcodeModeRow)
setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow));
auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.format)}; auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.transcode.format)};
if (transcodeFormatRow) if (transcodeFormatRow)
setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow));
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.bitrate)}; auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate)};
if (transcodeBitrateRow) if (transcodeBitrateRow)
setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*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()); setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable());
@@ -271,13 +297,9 @@ class SettingsModel : public Wt::WFormModel
{ {
_transcodeModeModel = std::make_shared<TranscodeModeModel>(); _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.always"), MediaPlayer::Settings::Transcode::Mode::Always);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::TranscodeMode::Never); _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::TranscodeMode::IfFormatNotSupported); _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::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);
_transcodeBitrateModel = std::make_shared<ValueStringModel<Bitrate>>(); _transcodeBitrateModel = std::make_shared<ValueStringModel<Bitrate>>();
for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates) 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.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.ogg_vorbis"), AudioFormat::OGG_VORBIS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_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 {}; bool _withOldPassword {};
@@ -298,17 +330,18 @@ class SettingsModel : public Wt::WFormModel
std::shared_ptr<TranscodeModeModel> _transcodeModeModel; std::shared_ptr<TranscodeModeModel> _transcodeModeModel;
std::shared_ptr<ValueStringModel<Bitrate>> _transcodeBitrateModel; std::shared_ptr<ValueStringModel<Bitrate>> _transcodeBitrateModel;
std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel; std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel;
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
std::shared_ptr<ValueStringModel<User::SubsonicArtistListMode>> _subsonicArtistListModeModel; std::shared_ptr<ValueStringModel<User::SubsonicArtistListMode>> _subsonicArtistListModeModel;
}; };
SettingsView::SettingsView() SettingsView::SettingsView()
{ {
wApp->internalPathChanged().connect(std::bind([=] wApp->internalPathChanged().connect([=]
{ {
refreshView(); refreshView();
})); });
LmsApp->getEvents().mediaPlayerSettingsAvailable.connect([=]() LmsApp->getMediaPlayer()->settingsLoaded.connect([=]()
{ {
refreshView(); refreshView();
}); });
@@ -376,15 +409,40 @@ SettingsView::refreshView()
transcodeBitrate->setModel(model->getTranscodeBitrateModel()); transcodeBitrate->setModel(model->getTranscodeBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); 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 {model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never};
const bool enable = (row && (model->getTranscodeModeModel()->getValue(*row) != MediaPlayer::TranscodeMode::Never));
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
t->updateModel(model.get()); t->updateModel(model.get());
t->updateView(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 // Subsonic
+2 -1
View File
@@ -26,6 +26,7 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "ArtistLink.hpp" #include "ArtistLink.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database; using namespace Database;
@@ -44,7 +45,7 @@ ArtistsInfo::ArtistsInfo()
refreshRecentlyAdded(); refreshRecentlyAdded();
}); });
LmsApp->getEvents().trackLoaded.connect([=] LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{ {
refreshMostPlayed(); refreshMostPlayed();
}); });
+11 -9
View File
@@ -37,6 +37,7 @@
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "LmsApplicationException.hpp" #include "LmsApplicationException.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -46,10 +47,10 @@ namespace UserInterface {
Release::Release(Filters* filters) Release::Release(Filters* filters)
: _filters(filters) : _filters(filters)
{ {
wApp->internalPathChanged().connect(std::bind([=] wApp->internalPathChanged().connect([=]()
{ {
refresh(); refresh();
})); });
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)}; 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}); tracksPlay.emit({trackId});
})); });
Wt::WText* addBtn {entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; 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}); tracksAdd.emit({trackId});
})); });
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); 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" : ""); 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");
} }
} }
} }
+2 -1
View File
@@ -27,6 +27,7 @@
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "ReleaseLink.hpp" #include "ReleaseLink.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database; using namespace Database;
@@ -45,7 +46,7 @@ ReleasesInfo::ReleasesInfo()
refreshRecentlyAdded(); refreshRecentlyAdded();
}); });
LmsApp->getEvents().trackLoaded.connect([=] LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{ {
refreshMostPlayed(); refreshMostPlayed();
}); });
+3 -2
View File
@@ -27,6 +27,7 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database; using namespace Database;
@@ -49,7 +50,7 @@ void addEntries(Wt::WContainerWidget *container, const std::vector<Track::pointe
namespace UserInterface { namespace UserInterface {
TracksInfo::TracksInfo() 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); addFunction("tr", &Wt::WTemplate::Functions::tr);
@@ -61,7 +62,7 @@ TracksInfo::TracksInfo()
refreshRecentlyAdded(); refreshRecentlyAdded();
}); });
LmsApp->getEvents().trackLoaded.connect([=] LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{ {
refreshMostPlayed(); refreshMostPlayed();
}); });
+5 -3
View File
@@ -34,6 +34,7 @@
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; 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" : ""); 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");
} }
} }
+8 -1
View File
@@ -154,12 +154,19 @@ static
void void
testSingleTrack(Session& session) testSingleTrack(Session& session)
{ {
{
auto transaction {session.createSharedTransaction()};
CHECK(Track::getCount(session) == 0);
}
ScopedTrack track {session, "MyTrackFile"}; ScopedTrack track {session, "MyTrackFile"};
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createSharedTransaction()};
CHECK(Track::getAll(session).size() == 1); CHECK(Track::getAll(session).size() == 1);
CHECK(Track::getCount(session) == 1);
} }
} }
+6
View File
@@ -124,6 +124,12 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const auto& audioStream : track->audioStreams) for (const auto& audioStream : track->audioStreams)
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl; 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) if (track->acoustID)
std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl; std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl;