From 13c96dcef6c63ddeeb5675ff2c73fd691b923776 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 18 Apr 2020 19:13:22 +0200 Subject: [PATCH 01/13] fixed playqueue button selected state visual --- docroot/css/lms.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docroot/css/lms.css b/docroot/css/lms.css index 7ca888f9..b0735192 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -239,6 +239,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { .text-success .Lms-playqueue-btn { filter: brightness(130%); filter: drop-shadow(0 0 0.65rem var(--hover-color)); + color: var(--hover-color); } .Lms-playqueue-entry-controls { From d8d1935e57d391dd0b3d6fbbf9c5d286cb271bb0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 18 Apr 2020 20:46:47 +0200 Subject: [PATCH 02/13] Small Users view visual tweaks --- approot/admin-users.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/approot/admin-users.xml b/approot/admin-users.xml index dc0b1eb2..3e27948c 100644 --- a/approot/admin-users.xml +++ b/approot/admin-users.xml @@ -11,11 +11,11 @@
-
- ${name}${}${tag}${} +
+ ${name}${}${tag}${}
-
- ${}${edit-btn class="Lms-admin-users-entry-btn"}${del-btn class="btn-danger Lms-admin-users-entry-btn"}${} +
+ ${}${edit-btn class="btn-warning btn-sm Lms-admin-users-entry-btn"}${del-btn class="btn-danger btn-sm Lms-admin-users-entry-btn"}${}
From 1fe87fec4c2488d4075c910cc82f24a0424c991e Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 18 Apr 2020 20:49:32 +0200 Subject: [PATCH 03/13] Fixed bad transcode field mapping. fixes #49 --- src/lms/ui/SettingsView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index dae47c9c..52367564 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -47,8 +47,8 @@ class SettingsModel : public Wt::WFormModel // Associate each field with a unique string literal. static inline const Field DarkModeField {"dark-mode"}; static inline const Field TranscodeEnableField {"transcoding-enable"}; - static inline const Field TranscodeFormatField {"transcoding-bitrate"}; - static inline const Field TranscodeBitrateField {"transcoding-format"}; + static inline const Field TranscodeFormatField {"transcoding-format"}; + static inline const Field TranscodeBitrateField {"transcoding-bitrate"}; static inline const Field PasswordOldField {"password-old"}; static inline const Field PasswordField {"password"}; static inline const Field PasswordConfirmField {"password-confirm"}; From 7d8ec6ff1e895d3846a278c2c6206cfda3209937 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 19 Apr 2020 14:59:39 +0200 Subject: [PATCH 04/13] First step: make the player handle both transcode and native audio streams --- approot/mediaplayer.xml | 3 ++- docroot/js/mediaplayer.js | 50 ++++++++++++++++++++++++++++++-------- src/lms/ui/MediaPlayer.cpp | 20 +++++---------- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/approot/mediaplayer.xml b/approot/mediaplayer.xml index 3dbddaf6..d8e29f3b 100644 --- a/approot/mediaplayer.xml +++ b/approot/mediaplayer.xml @@ -3,7 +3,8 @@ - +
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 78affb86..68039c38 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -14,8 +14,8 @@ LMS.mediaplayer = function () { var _elems = {}; var _offset = 0; var _duration = 0; - var _audioSrc; - var _mode = Mode.File; + var _audioNativeSrc; + var _audioTranscodedSrc; var _updateControls = function() { if (_elems.audio.paused) { @@ -115,8 +115,8 @@ LMS.mediaplayer = function () { _elems.playpause.addEventListener("click", function() { if (_elems.audio.paused) { - if (_elems.audio.hasAttribute("src")) - _elems.audio.play(); + if (_elems.audio.firstChild) + _playTrack(); } else _elems.audio.pause(); @@ -129,15 +129,17 @@ LMS.mediaplayer = function () { _requestNextTrack(); }); _elems.seek.addEventListener("change", function() { - if (!_elems.audio.hasAttribute("src")) + let mode = _getAudioMode(); + if (!mode) return; let selectedOffset = parseInt(_elems.seek.value, 10); - switch (_mode) { + switch (mode) { case Mode.Transcode: _offset = selectedOffset; - _elems.audio.src = _audioSrc + "&offset=" + _offset; + _removeAudioSources(); + _addAudioSourceIfSet(_audioTranscodeSrc + "&offset=" + _offset); _elems.audio.load(); _elems.audio.currentTime = 0; _playTrack(); @@ -189,14 +191,42 @@ LMS.mediaplayer = function () { } + var _removeAudioSources = function() { + while ( _elems.audio.lastElementChild) { + _elems.audio.removeChild( _elems.audio.lastElementChild); + } + } + + var _addAudioSourceIfSet = function(audioSrc) { + let source = document.createElement('source'); + source.src = audioSrc; + _elems.audio.appendChild(source); + } + + var _getAudioMode = function() { + if (_elems.audio.currentSrc) { + if (_elems.audio.currentSrc.includes("format")) + return Mode.Transcode; + else + return Mode.File; + } + else + return undefined; + } + var loadTrack = function(params, autoplay) { _offset = 0; _duration = params.duration; - _audioSrc = params.resource; - _mode = params.mode; + _audioNativeSrc = params.native_resource; + _audioTranscodeSrc = params.transcode_resource; _elems.seek.max = _duration; - _elems.audio.src = _audioSrc; + + _removeAudioSources(); + // ! order is important + _addAudioSourceIfSet(_audioNativeSrc); + _addAudioSourceIfSet(_audioTranscodeSrc); + _elems.audio.load(); _elems.curtime.innerHTML = _durationToString(_offset); _elems.duration.innerHTML = _durationToString(_duration); diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index bd7d7f36..a87489d3 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -62,26 +62,18 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play) const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()}; - std::string resource; - bool transcode; - if (LmsApp->getUser()->getAudioTranscodeEnable()) - { - resource = LmsApp->getAudioTranscodeResource()->getUrlForUser(trackId, LmsApp->getUser()); - transcode = true; - } - else - { - resource = LmsApp->getAudioFileResource()->getUrl(trackId); - transcode = false; - } + std::string nativeResource; + const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrlForUser(trackId, LmsApp->getUser())}; +// if (!LmsApp->getUser()->getAudioTranscodeEnable()) + nativeResource = LmsApp->getAudioFileResource()->getUrl(trackId); const auto artists {track->getArtists()}; std::ostringstream oss; oss << "var params = {" - << " mode: " << (transcode ? "Mode.Transcode" : "Mode.File") << "," - << " resource: \"" << resource << "\"," + << " native_resource: \"" << nativeResource << "\"," + << " transcode_resource: \"" << transcodeResource << "\"," << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," << " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\"," From 4749fd7548dd59758b19d79ec2898491fd413f4d Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 20 Apr 2020 20:54:16 +0200 Subject: [PATCH 05/13] Added a new transcoding mode: transcode if and only if the format is not handled by the browser. Made the subsonic API configurable again in conf file. Moved the subsonic's artist list mode in the per-user settings page. fixes #46 --- approot/admin-subsonic.xml | 40 --- approot/admin-user.xml | 15 -- approot/messages.xml | 35 +-- approot/messages_fr.xml | 39 +-- approot/settings.xml | 85 ++++++- conf/lms.conf | 3 + docroot/js/mediaplayer.js | 66 ++++- src/libs/av/impl/AvTranscoder.cpp | 14 +- src/libs/av/impl/AvTypes.cpp | 14 +- src/libs/av/include/av/AvTranscoder.hpp | 2 +- src/libs/av/include/av/AvTypes.hpp | 14 +- src/libs/database/CMakeLists.txt | 1 - src/libs/database/impl/Session.cpp | 43 +++- src/libs/database/impl/SubsonicSettings.cpp | 48 ---- src/libs/database/impl/User.cpp | 68 +---- .../include/database/SubsonicSettings.hpp | 69 ----- src/libs/database/include/database/User.hpp | 63 +++-- src/libs/subsonic/impl/Stream.cpp | 30 +-- src/libs/subsonic/impl/SubsonicResource.cpp | 23 +- src/lms/CMakeLists.txt | 1 - src/lms/main.cpp | 10 +- src/lms/ui/LmsApplication.cpp | 21 +- src/lms/ui/LmsApplication.hpp | 5 + src/lms/ui/MediaPlayer.cpp | 81 +++++- src/lms/ui/MediaPlayer.hpp | 34 +++ src/lms/ui/SettingsView.cpp | 240 +++++++++++++----- src/lms/ui/admin/SubsonicView.cpp | 180 ------------- src/lms/ui/admin/SubsonicView.hpp | 37 --- src/lms/ui/admin/UserView.cpp | 52 +--- .../ui/resource/AudioTranscodeResource.cpp | 33 +-- .../ui/resource/AudioTranscodeResource.hpp | 2 +- 31 files changed, 615 insertions(+), 753 deletions(-) delete mode 100644 approot/admin-subsonic.xml delete mode 100644 src/libs/database/impl/SubsonicSettings.cpp delete mode 100644 src/libs/database/include/database/SubsonicSettings.hpp delete mode 100644 src/lms/ui/admin/SubsonicView.cpp delete mode 100644 src/lms/ui/admin/SubsonicView.hpp diff --git a/approot/admin-subsonic.xml b/approot/admin-subsonic.xml deleted file mode 100644 index f7ce118d..00000000 --- a/approot/admin-subsonic.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - ${tr:Lms.Admin.Subsonic.subsonic} -
- -
- -
-
-
-
- ${enable-info} -
-
-
- -
- ${artist-list-mode} -
-
- ${artist-list-mode-info} -
-
- -
-
- ${apply-btn class="btn-primary"} ${discard-btn} -
-
-
-
- -
diff --git a/approot/admin-user.xml b/approot/admin-user.xml index f5d732cd..2274c278 100644 --- a/approot/admin-user.xml +++ b/approot/admin-user.xml @@ -42,21 +42,6 @@ ${password-info}
- -
- -
-
- ${audio-transcode-bitrate-limit} - kbps -
-
-
- ${audio-transcode-bitrate-limit-info} -
-
${}
${tr:Lms.Settings.audio} +
+
+
+ ${tr:Lms.Settings.audio-settings-are-local} +
+
+
-
-
-
+ ${} + ${tr:Lms.Settings.subsonic-api} +
+ +
+ +
+
+
+
+ ${subsonic-transcode-enable-info} +
+
+
+ +
+ ${subsonic-transcode-format} +
+
+ ${subsonic-transcode-format-info} +
+
+
+ +
+
+ ${subsonic-transcode-bitrate} + kbps +
+
+
+ ${subsonic-transcode-bitrate-info} +
+
+
+ +
+ ${subsonic-artist-list-mode} +
+
+ ${subsonic-artist-list-mode-info} +
+
+ +
+ ${
} ${tr:Lms.Settings.change-password}
${} diff --git a/conf/lms.conf b/conf/lms.conf index e84bfbdf..e1666f60 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -34,6 +34,9 @@ deploy-path = "/"; # Acoustic brainz's root API acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"; +# API +api-subsonic = true; + # Turn on this option to allow the demo account creation/use demo = false; diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 68039c38..6135c86a 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -2,6 +2,13 @@ var LMS = LMS || {}; +// Keep in sync with MediaPlayer::TranscodeMode cpp +const TranscodeMode = { + Never: 0, + Always: 1, + IfFormatNotSupported: 2, +} + const Mode = { Transcode: 1, File: 2, @@ -16,6 +23,9 @@ LMS.mediaplayer = function () { var _duration = 0; var _audioNativeSrc; var _audioTranscodedSrc; + var _transcodeMode = TranscodeMode.Never; + var _transcodeFormat = 0; + var _transcodeBitrate = 0; var _updateControls = function() { if (_elems.audio.paused) { @@ -71,6 +81,29 @@ LMS.mediaplayer = function () { _setVolume(_elems.volumeslider.value); } + var _initTranscodeSettings = function(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { + if (typeof(Storage) !== "undefined" && localStorage.transcodeMode) { + _transcodeMode = Number(localStorage.transcodeMode); + } + 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; + } + + Wt.emit(_root, "settingsLoaded", _transcodeMode, _transcodeFormat, _transcodeBitrate); + } + var _setVolume = function(volume) { _elems.lastvolume = _elems.audio.volume; @@ -99,7 +132,7 @@ LMS.mediaplayer = function () { } } - var init = function(root) { + var init = function(root, defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { _root = root; _elems.audio = document.getElementById("lms-mp-audio"); @@ -139,7 +172,7 @@ LMS.mediaplayer = function () { case Mode.Transcode: _offset = selectedOffset; _removeAudioSources(); - _addAudioSourceIfSet(_audioTranscodeSrc + "&offset=" + _offset); + _addAudioSource(_audioTranscodeSrc + "&offset=" + _offset); _elems.audio.load(); _elems.audio.currentTime = 0; _playTrack(); @@ -166,6 +199,7 @@ LMS.mediaplayer = function () { }); _initVolume(); + _initTranscodeSettings(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate); _elems.volumeslider.addEventListener("input", function() { _setVolume(_elems.volumeslider.value); @@ -197,7 +231,7 @@ LMS.mediaplayer = function () { } } - var _addAudioSourceIfSet = function(audioSrc) { + var _addAudioSource = function(audioSrc) { let source = document.createElement('source'); source.src = audioSrc; _elems.audio.appendChild(source); @@ -218,14 +252,21 @@ LMS.mediaplayer = function () { _offset = 0; _duration = params.duration; _audioNativeSrc = params.native_resource; - _audioTranscodeSrc = params.transcode_resource; + _audioTranscodeSrc = params.transcode_resource + "&bitrate=" + _transcodeBitrate + "&format=" + _transcodeFormat; _elems.seek.max = _duration; _removeAudioSources(); // ! order is important - _addAudioSourceIfSet(_audioNativeSrc); - _addAudioSourceIfSet(_audioTranscodeSrc); + if (_transcodeMode == TranscodeMode.Never || _transcodeMode == TranscodeMode.IfFormatNotSupported) + { + _addAudioSource(_audioNativeSrc); + } + if (_transcodeMode == TranscodeMode.Always || _transcodeMode == TranscodeMode.IfFormatNotSupported) + { + _addAudioSource(_audioTranscodeSrc); + } + _elems.audio.load(); _elems.curtime.innerHTML = _durationToString(_offset); @@ -248,10 +289,23 @@ LMS.mediaplayer = function () { _elems.audio.pause(); } + var setSettings = function(transcodeMode, transcodeFormat, transcodeBitrate) { + _transcodeMode = transcodeMode; + _transcodeFormat = transcodeFormat; + _transcodeBitrate = transcodeBitrate; + + if (typeof(Storage) !== "undefined") { + localStorage.transcodeMode = _transcodeMode; + localStorage.transcodeFormat = _transcodeFormat; + localStorage.transcodeBitrate = _transcodeBitrate; + } + } + return { init: init, loadTrack: loadTrack, stop: stop, + setSettings: setSettings, }; }(); diff --git a/src/libs/av/impl/AvTranscoder.cpp b/src/libs/av/impl/AvTranscoder.cpp index fd6576f2..50e7991d 100644 --- a/src/libs/av/impl/AvTranscoder.cpp +++ b/src/libs/av/impl/AvTranscoder.cpp @@ -104,35 +104,35 @@ Transcoder::start() args.emplace_back(std::to_string(_parameters.bitrate)); // Codecs and formats - switch (_parameters.encoding) + switch (_parameters.format) { - case Encoding::MP3: + case Format::MP3: args.emplace_back("-f"); args.emplace_back("mp3"); break; - case Encoding::OGG_OPUS: + case Format::OGG_OPUS: args.emplace_back("-acodec"); args.emplace_back("libopus"); args.emplace_back("-f"); args.emplace_back("ogg"); break; - case Encoding::MATROSKA_OPUS: + case Format::MATROSKA_OPUS: args.emplace_back("-acodec"); args.emplace_back("libopus"); args.emplace_back("-f"); args.emplace_back("matroska"); break; - case Encoding::OGG_VORBIS: + case Format::OGG_VORBIS: args.emplace_back("-acodec"); args.emplace_back("libvorbis"); args.emplace_back("-f"); args.emplace_back("ogg"); break; - case Encoding::WEBM_VORBIS: + case Format::WEBM_VORBIS: args.emplace_back("-acodec"); args.emplace_back("libvorbis"); args.emplace_back("-f"); @@ -143,7 +143,7 @@ Transcoder::start() return false; } - _outputMimeType = encodingToMimetype(_parameters.encoding); + _outputMimeType = formatToMimetype(_parameters.format); args.emplace_back("pipe:1"); diff --git a/src/libs/av/impl/AvTypes.cpp b/src/libs/av/impl/AvTypes.cpp index 7e556294..ca83978f 100644 --- a/src/libs/av/impl/AvTypes.cpp +++ b/src/libs/av/impl/AvTypes.cpp @@ -21,15 +21,15 @@ namespace Av { -const char* encodingToMimetype(Encoding encoding) +const char* formatToMimetype(Format format) { - switch (encoding) + switch (format) { - case Encoding::MP3: return "audio/mpeg"; - case Encoding::OGG_OPUS: return "audio/opus"; - case Encoding::MATROSKA_OPUS: return "audio/x-matroska"; - case Encoding::OGG_VORBIS: return "audio/ogg"; - case Encoding::WEBM_VORBIS: return "audio/webm"; + case Format::MP3: return "audio/mpeg"; + case Format::OGG_OPUS: return "audio/opus"; + case Format::MATROSKA_OPUS: return "audio/x-matroska"; + case Format::OGG_VORBIS: return "audio/ogg"; + case Format::WEBM_VORBIS: return "audio/webm"; } throw AvException {"Invalid encoding"}; diff --git a/src/libs/av/include/av/AvTranscoder.hpp b/src/libs/av/include/av/AvTranscoder.hpp index 60093ebb..4dfff1bb 100644 --- a/src/libs/av/include/av/AvTranscoder.hpp +++ b/src/libs/av/include/av/AvTranscoder.hpp @@ -33,7 +33,7 @@ namespace Av { struct TranscodeParameters { - Encoding encoding; + Format format; std::size_t bitrate {128000}; std::optional stream; // Id of the stream to be transcoded (auto detect by default) std::optional offset; diff --git a/src/libs/av/include/av/AvTypes.hpp b/src/libs/av/include/av/AvTypes.hpp index 3eca84d3..f2c942a9 100644 --- a/src/libs/av/include/av/AvTypes.hpp +++ b/src/libs/av/include/av/AvTypes.hpp @@ -31,17 +31,17 @@ class AvException : public LmsException AvException(const std::string& msg) : LmsException(msg) {} }; -enum class Encoding +enum class Format { // Values are important and must not be changed - MP3, - OGG_OPUS, - MATROSKA_OPUS, - OGG_VORBIS, - WEBM_VORBIS, + MP3 = 0, + OGG_OPUS = 1, + MATROSKA_OPUS = 2, + OGG_VORBIS = 3, + WEBM_VORBIS = 4, }; -const char* encodingToMimetype(Encoding encoding); +const char* formatToMimetype(Format encoding); } diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index cb6fc22e..a7eba7e2 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -11,7 +11,6 @@ add_library(lmsdatabase SHARED impl/Session.cpp impl/SessionPool.cpp impl/SqlQuery.cpp - impl/SubsonicSettings.cpp impl/Track.cpp impl/TrackBookmark.cpp impl/User.cpp diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 3aa85a27..22a7b250 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -31,7 +31,6 @@ #include "database/Db.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" -#include "database/SubsonicSettings.hpp" #include "database/Track.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackArtistLink.hpp" @@ -41,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 19 +#define LMS_DATABASE_VERSION 21 using Version = std::size_t; @@ -198,11 +197,8 @@ CREATE TABLE "release_backup" ( "mbid" text not null ))"); _session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); - _session.execute("DROP TABLE release;"); + _session.execute("DROP TABLE release"); _session.execute("ALTER TABLE release_backup RENAME TO release"); - _session.execute("CREATE INDEX release_name_idx ON release(name)"); - _session.execute("CREATE INDEX release_name_nocase_idx ON release(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX release_mbid_idx ON release(mbid)"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); @@ -217,6 +213,39 @@ CREATE TABLE IF NOT EXISTS "subsonic_settings" ( "artist_list_mode" integer not null ))"); } + else if (version == 19) + { + _session.execute(R"( +CREATE TABLE "user_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "login_name" text not null, + "password_salt" text not null, + "password_hash" text not null, + "last_login" text, + "subsonic_transcode_enable" boolean not null, + "subsonic_transcode_format" integer not null, + "subsonic_transcode_bitrate" integer not null, + "subsonic_artist_list_mode" integer not null, + "ui_theme" integer not null, + "cur_playing_track_pos" integer not null, + "repeat_all" boolean not null, + "radio" boolean not null +))"); + _session.execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "} + + (User::defaultSubsonicTranscodeEnable ? "1" : "0") + + ", " + std::to_string(static_cast(User::defaultSubsonicTranscodeFormat)) + + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate) + + ", " + std::to_string(static_cast(User::defaultSubsonicArtistListMode)) + + ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); + _session.execute("DROP TABLE user"); + _session.execute("ALTER TABLE user_backup RENAME TO user"); + } + else if (version == 20) + { + _session.execute("DROP TABLE subsonic_settings"); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; @@ -239,7 +268,6 @@ Session::Session(Db& db) _session.mapClass("cluster_type"); _session.mapClass("release"); _session.mapClass("scan_settings"); - _session.mapClass("subsonic_settings"); _session.mapClass("track"); _session.mapClass("track_bookmark"); _session.mapClass("track_artist_link"); @@ -365,7 +393,6 @@ Session::prepareTables() auto uniqueTransaction {createUniqueTransaction()}; ScanSettings::init(*this); - SubsonicSettings::init(*this); } } diff --git a/src/libs/database/impl/SubsonicSettings.cpp b/src/libs/database/impl/SubsonicSettings.cpp deleted file mode 100644 index 3393afe6..00000000 --- a/src/libs/database/impl/SubsonicSettings.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "database/SubsonicSettings.hpp" - -#include "database/Session.hpp" - -namespace Database { - -void -SubsonicSettings::init(Session& session) -{ - session.checkUniqueLocked(); - - pointer settings {get(session)}; - if (settings) - return; - - settings = session.getDboSession().add(std::make_unique()); -} - -SubsonicSettings::pointer -SubsonicSettings::get(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().find(); -} - - -} // ns Database - diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index c471b40b..97fa06ab 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -70,28 +70,11 @@ AuthToken::getByValue(Session& session, const std::string& value) static const std::string playedListName {"__played_tracks__"}; static const std::string queuedListName {"__queued_tracks__"}; -const std::set -User::audioTranscodeAllowedBitrates = -{ - 64000, - 96000, - 128000, - 192000, - 320000, -}; - -User::User() -: _maxAudioTranscodeBitrate {static_cast(*audioTranscodeAllowedBitrates.rbegin())} -{ - -} - User::User(const std::string& loginName, const PasswordHash& passwordHash) -: User() +: _loginName {loginName} +, _passwordSalt {passwordHash.salt} +, _passwordHash {passwordHash.hash} { - _loginName = loginName; - _passwordHash = passwordHash.hash; - _passwordSalt = passwordHash.salt; } std::vector @@ -141,27 +124,10 @@ User::getByLoginName(Session& session, const std::string& name) } void -User::setAudioTranscodeBitrate(Bitrate bitrate) +User::setSubsonicTranscodeBitrate(Bitrate bitrate) { - _audioTranscodeBitrate = std::min(bitrate, static_cast(_maxAudioTranscodeBitrate)); -} - -void -User::setMaxAudioTranscodeBitrate(Bitrate requestedBitrate) -{ - Bitrate bitrate {*audioTranscodeAllowedBitrates.begin()}; - - for (auto allowedBitrate : audioTranscodeAllowedBitrates) - { - if (requestedBitrate < allowedBitrate) - break; - - bitrate = allowedBitrate; - } - - _maxAudioTranscodeBitrate = bitrate; - if (_audioTranscodeBitrate > _maxAudioTranscodeBitrate) - _audioTranscodeBitrate = _maxAudioTranscodeBitrate; + assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend()); + _subsonicTranscodeBitrate = bitrate; } void @@ -170,18 +136,6 @@ User::clearAuthTokens() _authTokens.clear(); } -Bitrate -User::getAudioTranscodeBitrate(void) const -{ - return _audioTranscodeBitrate; -} - -std::size_t -User::getMaxAudioTranscodeBitrate(void) const -{ - return _maxAudioTranscodeBitrate; -} - Wt::Dbo::ptr User::getPlayedTrackList(Session& session) const { @@ -283,16 +237,6 @@ User::getStarredTracks() const return std::vector>(_starredTracks.begin(), _starredTracks.end()); } - -bool -User::checkBitrate(Database::Bitrate bitrate) const -{ - if (audioTranscodeAllowedBitrates.find(bitrate) == std::cend(audioTranscodeAllowedBitrates)) - return false; - - return static_cast(_maxAudioTranscodeBitrate) >= bitrate; -} - } // namespace Database diff --git a/src/libs/database/include/database/SubsonicSettings.hpp b/src/libs/database/include/database/SubsonicSettings.hpp deleted file mode 100644 index 55154186..00000000 --- a/src/libs/database/include/database/SubsonicSettings.hpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include - -namespace Database { - -class Session; - -class SubsonicSettings : public Wt::Dbo::Dbo -{ - public: - using pointer = Wt::Dbo::ptr; - - enum class ArtistListMode - { - AllArtists, - ReleaseArtists, - }; - - static inline constexpr bool defaultSubsonicAPIEnabled {true}; - static inline constexpr ArtistListMode defaultArtistListMode {ArtistListMode::AllArtists}; - - static void init(Session& session); - - static pointer get(Session& session); - - - // Getters - bool isAPIEnabled() const { return _isAPIEnabled; } - ArtistListMode getArtistListMode() const { return _artistListMode; } - - // Setters - void setAPIEnabled(bool enabled) { _isAPIEnabled = enabled; } - void setArtistListMode(ArtistListMode mode) {_artistListMode = mode; } - - template - void persist(Action& a) - { - Wt::Dbo::field(a, _isAPIEnabled, "api_enabled"); - Wt::Dbo::field(a, _artistListMode, "artist_list_mode"); - } - - private: - - bool _isAPIEnabled {defaultSubsonicAPIEnabled}; - ArtistListMode _artistListMode {defaultArtistListMode}; -}; - -} // namespace Database - diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index c7c0f888..24410c47 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -36,7 +36,7 @@ class Session; class TrackList; class Track; -// User selectable audio formats +// User selectable audio file formats // Do not change values enum class AudioFormat { @@ -113,16 +113,33 @@ class User : public Wt::Dbo::Dbo Dark = 1, }; - static const std::set audioTranscodeAllowedBitrates; + // Do not remove values! + static inline const std::set audioTranscodeAllowedBitrates + { + 64000, + 96000, + 128000, + 192000, + 320000, + }; + + // Do not change enum values! + enum class SubsonicArtistListMode + { + AllArtists = 0, + ReleaseArtists = 1, + }; + static inline const std::size_t MinNameLength {3}; static inline const std::size_t MaxNameLength {15}; - static inline const bool defaultAudioTranscodeEnable {true}; - static inline const AudioFormat defaultAudioTranscodeFormat {AudioFormat::OGG_OPUS}; - static inline const Bitrate defaultAudioTranscodeBitrate {128000}; + static inline const bool defaultSubsonicTranscodeEnable {true}; + static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS}; + static inline const Bitrate defaultSubsonicTranscodeBitrate {128000}; static inline const UITheme defaultUITheme {UITheme::Dark}; + static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists}; - User(); + User() = default; User(const std::string& loginName, const PasswordHash& passwordHash); // utility @@ -143,27 +160,27 @@ class User : public Wt::Dbo::Dbo void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } void setType(Type type) { _type = type; } - void setAudioTranscodeEnable(bool value) { _audioTranscodeEnable = value; } - void setAudioTranscodeFormat(AudioFormat format) { _audioTranscodeFormat = format; } - void setAudioTranscodeBitrate(Bitrate bitrate); - void setMaxAudioTranscodeBitrate(Bitrate bitrate); + void setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; } + void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; } + void setSubsonicTranscodeBitrate(Bitrate bitrate); void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setRadio(bool val) { _radio = val; } void setRepeatAll(bool val) { _repeatAll = val; } void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } void clearAuthTokens(); + void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } // read bool isAdmin() const { return _type == Type::ADMIN; } bool isDemo() const { return _type == Type::DEMO; } - bool getAudioTranscodeEnable() const { return _audioTranscodeEnable; } - Bitrate getAudioTranscodeBitrate() const; - AudioFormat getAudioTranscodeFormat() const { return _audioTranscodeFormat; } - Bitrate getMaxAudioTranscodeBitrate() const; + bool getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; } + AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; } + Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; } std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } bool isRepeatAllSet() const { return _repeatAll; } bool isRadioSet() const { return _radio; } UITheme getUITheme() const { return _uiTheme; } + SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } Wt::Dbo::ptr getPlayedTrackList(Session& session) const; Wt::Dbo::ptr getQueuedTrackList(Session& session) const; @@ -184,8 +201,6 @@ class User : public Wt::Dbo::Dbo bool hasStarredTrack(Wt::Dbo::ptr track) const; std::vector> getStarredTracks() const; - bool checkBitrate(Bitrate bitrate) const; - template void persist(Action& a) { @@ -194,10 +209,10 @@ class User : public Wt::Dbo::Dbo Wt::Dbo::field(a, _passwordSalt, "password_salt"); Wt::Dbo::field(a, _passwordHash, "password_hash"); Wt::Dbo::field(a, _lastLogin, "last_login"); - Wt::Dbo::field(a, _maxAudioTranscodeBitrate, "max_audio_bitrate"); - Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable"); - Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate"); - Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format"); + Wt::Dbo::field(a, _subsonicTranscodeEnable, "subsonic_transcode_enable"); + Wt::Dbo::field(a, _subsonicTranscodeFormat, "subsonic_transcode_format"); + Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate"); + Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); Wt::Dbo::field(a, _uiTheme, "ui_theme"); // User's dynamic data Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); @@ -219,13 +234,13 @@ class User : public Wt::Dbo::Dbo UITheme _uiTheme {defaultUITheme}; // Admin defined settings - int _maxAudioTranscodeBitrate; Type _type {Type::REGULAR}; // User defined settings - bool _audioTranscodeEnable {defaultAudioTranscodeEnable}; - AudioFormat _audioTranscodeFormat {defaultAudioTranscodeFormat}; - int _audioTranscodeBitrate {defaultAudioTranscodeBitrate}; + SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode}; + bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable}; + AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat}; + int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate}; // User's dynamic data (UI) int _curPlayingTrackPos {}; // Current track position in queue diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index a87ad6e5..cd7645be 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -37,17 +37,17 @@ namespace API::Subsonic::Stream { static -Av::Encoding -userTranscodeFormatToAvEncoding(AudioFormat format) +Av::Format +userTranscodeFormatToAvFormat(AudioFormat format) { switch (format) { - case AudioFormat::MP3: return Av::Encoding::MP3; - case AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS; - case AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS; - case AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS; - case AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS; - default: return Av::Encoding::OGG_OPUS; + case AudioFormat::MP3: return Av::Format::MP3; + case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; + case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; + case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; + case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; + default: return Av::Format::OGG_OPUS; } } @@ -87,19 +87,19 @@ getStreamParameters(RequestContext& context) throw UserNotAuthorizedError {}; // format = "raw" => no transcode. Other format values will be ignored - const bool transcode {(!format || (*format != "raw")) && user->getAudioTranscodeEnable()}; + const bool transcode {(!format || (*format != "raw")) && user->getSubsonicTranscodeEnable()}; if (transcode) { - // "If set to zero, no limit is imposed" - if (!maxBitRate || *maxBitRate == 0) - maxBitRate = user->getAudioTranscodeBitrate() / 1000; + std::size_t bitRate {user->getSubsonicTranscodeBitrate() / 1000}; - *maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000); + // "If set to zero, no limit is imposed" + if (maxBitRate && *maxBitRate != 0) + bitRate = clamp(*maxBitRate, std::size_t {48}, bitRate); Av::TranscodeParameters transcodeParameters; - transcodeParameters.bitrate = *maxBitRate * 1000; - transcodeParameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat()); + transcodeParameters.bitrate = bitRate * 1000; + transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat()); transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) parameters.transcodeParameters = std::move(transcodeParameters); diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 5b97899b..b34211a9 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -30,7 +30,6 @@ #include "database/Db.hpp" #include "database/Release.hpp" #include "database/Session.hpp" -#include "database/SubsonicSettings.hpp" #include "database/Track.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackList.hpp" @@ -579,7 +578,6 @@ handleCreateUserRequest(RequestContext& context) throw UserAlreadyExistsGenericError {}; User::pointer user {User::create(context.dbSession, username, hash)}; - user.modify()->setMaxAudioTranscodeBitrate(128000); return Response::createOkResponse(); } @@ -903,11 +901,11 @@ handleGetArtistsRequest(RequestContext& context) throw UserNotAuthorizedError {}; std::optional linkType; - switch (SubsonicSettings::get(context.dbSession)->getArtistListMode()) + switch (user->getSubsonicArtistListMode()) { - case SubsonicSettings::ArtistListMode::AllArtists: + case User::SubsonicArtistListMode::AllArtists: break; - case SubsonicSettings::ArtistListMode::ReleaseArtists: + case User::SubsonicArtistListMode::ReleaseArtists: linkType = TrackArtistLink::Type::ReleaseArtist; break; } @@ -1046,11 +1044,11 @@ handleGetIndexesRequest(RequestContext& context) throw UserNotAuthorizedError {}; std::optional linkType; - switch (SubsonicSettings::get(context.dbSession)->getArtistListMode()) + switch (user->getSubsonicArtistListMode()) { - case SubsonicSettings::ArtistListMode::AllArtists: + case User::SubsonicArtistListMode::AllArtists: break; - case SubsonicSettings::ArtistListMode::ReleaseArtists: + case User::SubsonicArtistListMode::ReleaseArtists: linkType = TrackArtistLink::Type::ReleaseArtist; break; } @@ -1520,7 +1518,6 @@ handleUpdateUserRequest(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::optional password {getParameterAs(context.parameters, "password")}; - std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; User::PasswordHash hash; if (password) @@ -1538,14 +1535,6 @@ handleUpdateUserRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - if (maxBitRate) - { - if (*maxBitRate == 0) - *maxBitRate = 320; - - user.modify()->setMaxAudioTranscodeBitrate(*maxBitRate * 1000); - } - if (password) { user.modify()->setPasswordHash(hash); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index a3f94268..bac11ebd 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -13,7 +13,6 @@ add_executable(lms ui/admin/DatabaseSettingsView.cpp ui/admin/DatabaseStatus.cpp ui/admin/InitWizardView.cpp - ui/admin/SubsonicView.cpp ui/admin/UserView.cpp ui/admin/UsersView.cpp ui/common/Validators.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 44e3ea62..bbaa1985 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -28,7 +28,6 @@ #include "av/AvTranscoder.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Db.hpp" -#include "database/SubsonicSettings.hpp" #include "scanner/IMediaScanner.hpp" #include "recommendation/IEngine.hpp" #include "subsonic/SubsonicResource.hpp" @@ -170,13 +169,8 @@ int main(int argc, char* argv[]) API::Subsonic::SubsonicResource subsonicResource {database}; // bind API resources - { - Database::Session session {database}; - auto transaction {session.createSharedTransaction()}; - - if (Database::SubsonicSettings::get(session)->isAPIEnabled()) - server.addResource(&subsonicResource, subsonicResource.getPath()); - } + if (ServiceProvider::get()->getBool("api-subsonic", true)) + server.addResource(&subsonicResource, subsonicResource.getPath()); // bind UI entry point server.addEntryPoint(Wt::EntryPointType::Application, diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index c27171e8..eb57811b 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -43,7 +43,6 @@ #include "admin/DatabaseSettingsView.hpp" #include "admin/UserView.hpp" #include "admin/UsersView.hpp" -#include "admin/SubsonicView.hpp" #include "resource/AudioFileResource.hpp" #include "resource/AudioTranscodeResource.hpp" #include "resource/ImageResource.hpp" @@ -123,7 +122,6 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, // Add a resource bundle messageResourceBundle().use(appRoot() + "admin-database"); messageResourceBundle().use(appRoot() + "admin-initwizard"); - messageResourceBundle().use(appRoot() + "admin-subsonic"); messageResourceBundle().use(appRoot() + "admin-user"); messageResourceBundle().use(appRoot() + "admin-users"); messageResourceBundle().use(appRoot() + "artist"); @@ -344,7 +342,6 @@ enum IdxRoot IdxAdminDatabase, IdxAdminUsers, IdxAdminUser, - IdxAdminSubsonic, }; static void @@ -368,7 +365,6 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin) { "/admin/database", IdxAdminDatabase, true }, { "/admin/users", IdxAdminUsers, true }, { "/admin/user", IdxAdminUser, true }, - { "/admin/subsonic", IdxAdminSubsonic, true }, }; LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'"; @@ -445,6 +441,9 @@ LmsApplication::createHome() navbar->setTitle("LMS", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}); navbar->setResponsive(true); + // MediaPlayer + _mediaPlayer = main->bindNew("player"); + Wt::WMenu* menu {navbar->addMenu(std::make_unique())}; { auto menuItem = menu->insertItem(0, Wt::WString::tr("Lms.Explore.artists")); @@ -488,10 +487,6 @@ LmsApplication::createHome() usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users")); usersSettings->setSelectable(false); - auto subsonicSettings = admin->insertItem(2, Wt::WString::tr("Lms.Admin.Subsonic.subsonic")); - subsonicSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/subsonic")); - subsonicSettings->setSelectable(false); - menuItem->setMenu(std::move(admin)); } @@ -522,7 +517,6 @@ LmsApplication::createHome() mainStack->addNew(); mainStack->addNew(); mainStack->addNew(); - mainStack->addNew(); } explore->tracksAdd.connect([=] (const std::vector& trackIds) @@ -536,19 +530,16 @@ LmsApplication::createHome() }); - // MediaPlayer - MediaPlayer* player = main->bindNew("player"); - // Events from MediaPlayer - player->playNext.connect([=] + _mediaPlayer->playNext.connect([=] { playqueue->playNext(); }); - player->playPrevious.connect([=] + _mediaPlayer->playPrevious.connect([=] { playqueue->playPrevious(); }); - player->playbackEnded.connect([=] + _mediaPlayer->playbackEnded.connect([=] { playqueue->playNext(); }); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 19928d31..9ec6968f 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -44,6 +44,7 @@ class AudioFileResource; class Auth; class ImageResource; class LmsApplicationException; +class MediaPlayer; // Events that can be listen from anywhere in the application struct Events @@ -55,6 +56,7 @@ struct Events // A track is being loaded Wt::Signal trackLoaded; std::optional lastLoadedTrackId; + Wt::Signal<> mediaPlayerSettingsAvailable; // Unload current track Wt::Signal<> trackUnloaded; @@ -105,6 +107,8 @@ class LmsApplication : public Wt::WApplication static std::unique_ptr createReleaseAnchor(Wt::Dbo::ptr release, bool addText = true); static std::unique_ptr createCluster(Wt::Dbo::ptr cluster, bool canDelete = false); + MediaPlayer* getMediaPlayer() const { return _mediaPlayer; } + // Signal emitted just before the session ends (user may already be logged out) Wt::Signal<>& preQuit() { return _preQuit; } @@ -133,6 +137,7 @@ class LmsApplication : public Wt::WApplication std::shared_ptr _audioTranscodeResource; std::shared_ptr _audioFileResource; std::shared_ptr _imageResource; + MediaPlayer* _mediaPlayer {}; }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index a87489d3..6127b2d5 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -36,17 +36,64 @@ namespace UserInterface { + MediaPlayer::MediaPlayer() -: Wt::WTemplate(Wt::WString::tr("Lms.MediaPlayer.template")), - playbackEnded(this, "playbackEnded"), - playPrevious(this, "playPrevious"), - playNext(this, "playNext") +: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")}, + playbackEnded {this, "playbackEnded"}, + playPrevious {this, "playPrevious"}, + playNext {this, "playNext"}, + _settingsLoaded {this, "settingsLoaded"} { _title = bindNew("title"); _artist = bindNew("artist"); _release = bindNew("release"); - wApp->doJavaScript("LMS.mediaplayer.init(" + jsRef() + ")"); + _settingsLoaded.connect([this](int mode, int format, int bitrate) + { + LMS_LOG(UI, DEBUG) << "Settings loaded! mode = " << mode << ", format = " << format << ", bitrate = " << bitrate; + + Settings settings; + + switch (static_cast(mode)) + { + case TranscodeMode::Always: + case TranscodeMode::Never: + case TranscodeMode::IfFormatNotSupported: + settings.mode = static_cast(mode); + break; + } + + switch (static_cast(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); + break; + } + + if (Database::User::audioTranscodeAllowedBitrates.find(bitrate) != std::cend(Database::User::audioTranscodeAllowedBitrates)) + settings.bitrate = bitrate; + + _settings = settings; + + LmsApp->getEvents().mediaPlayerSettingsAvailable.emit(); + }); + + { + std::ostringstream oss; + oss << "LMS.mediaplayer.init(" + << jsRef() + << ", " << static_cast(defaultTranscodeMode) + << ", " << static_cast(defaultTranscodeFormat) + << ", " << static_cast(defaultTranscodeBitrate) + << ")"; + + 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); @@ -62,10 +109,8 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play) const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()}; - std::string nativeResource; - const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrlForUser(trackId, LmsApp->getUser())}; -// if (!LmsApp->getUser()->getAudioTranscodeEnable()) - nativeResource = LmsApp->getAudioFileResource()->getUrl(trackId); + const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; + const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; const auto artists {track->getArtists()}; @@ -124,5 +169,23 @@ MediaPlayer::stop() wApp->doJavaScript("LMS.mediaplayer.stop()"); } +void +MediaPlayer::setSettings(const Settings& settings) +{ + _settings = settings; + + { + std::ostringstream oss; + oss << "LMS.mediaplayer.setSettings(" + << static_cast(_settings->mode) + << ", " << static_cast(_settings->format) + << ", " << static_cast(_settings->bitrate) + << ")"; + + LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; + doJavaScript(oss.str()); + } +} + } // namespace UserInterface diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 538d64b1..bfb02c5f 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -25,14 +25,45 @@ #include #include "database/Types.hpp" +#include "database/User.hpp" namespace UserInterface { class MediaPlayer : public Wt::WTemplate { public: + using Bitrate = Database::Bitrate; + using Format = Database::AudioFormat; + + // Do not change this enum as it 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}; + }; + MediaPlayer(); + MediaPlayer(const MediaPlayer&) = delete; + MediaPlayer(MediaPlayer&&) = delete; + MediaPlayer& operator=(const MediaPlayer&) = delete; + MediaPlayer& operator=(MediaPlayer&&) = delete; + + std::optional getSettings() const { return _settings; } + void setSettings(const Settings& settings); + // Signals Wt::JSignal<> playbackEnded; Wt::JSignal<> playPrevious; @@ -42,6 +73,9 @@ class MediaPlayer : public Wt::WTemplate void stop(); void loadTrack(Database::IdType trackId, bool play); + std::optional _settings; + + Wt::JSignal _settingsLoaded; Wt::WText* _title; Wt::WAnchor* _release; Wt::WAnchor* _artist; diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 52367564..73bd3b6b 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -31,11 +31,13 @@ #include "common/ValueStringModel.hpp" #include "auth/IPasswordService.hpp" +#include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" #include "LmsApplication.hpp" #include "LmsTheme.hpp" +#include "MediaPlayer.hpp" namespace UserInterface { @@ -46,22 +48,31 @@ class SettingsModel : public Wt::WFormModel public: // Associate each field with a unique string literal. static inline const Field DarkModeField {"dark-mode"}; - static inline const Field TranscodeEnableField {"transcoding-enable"}; - static inline const Field TranscodeFormatField {"transcoding-format"}; - static inline const Field TranscodeBitrateField {"transcoding-bitrate"}; + 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 SubsonicArtistListModeField {"subsonic-artist-list-mode"}; + static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"}; + static inline const Field SubsonicTranscodeFormatField {"subsonic-transcode-format"}; + static inline const Field SubsonicTranscodeBitrateField {"subsonic-transcode-bitrate"}; static inline const Field PasswordOldField {"password-old"}; static inline const Field PasswordField {"password"}; static inline const Field PasswordConfirmField {"password-confirm"}; + using TranscodeModeModel = ValueStringModel; + SettingsModel(bool withOldPassword) : _withOldPassword {withOldPassword} { initializeModels(); addField(DarkModeField); - addField(TranscodeEnableField); + addField(TranscodeModeField); addField(TranscodeBitrateField); addField(TranscodeFormatField); + addField(SubsonicTranscodeEnableField); + addField(SubsonicTranscodeBitrateField); + addField(SubsonicTranscodeFormatField); if (_withOldPassword) addField(PasswordOldField); @@ -69,14 +80,19 @@ class SettingsModel : public Wt::WFormModel addField(PasswordField); addField(PasswordConfirmField); + setValidator(TranscodeModeField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeFormatField, createMandatoryValidator()); + setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); + setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); loadData(); } - std::shared_ptr transcodeBitrateModel() { return _transcodeBitrateModel; } - std::shared_ptr transcodeFormatModel() { return _transcodeFormatModel; } + std::shared_ptr getTranscodeModeModel() { return _transcodeModeModel; } + std::shared_ptr getTranscodeBitrateModel() { return _transcodeBitrateModel; } + std::shared_ptr getTranscodeFormatModel() { return _transcodeFormatModel; } + std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } void saveData() { @@ -97,43 +113,89 @@ class SettingsModel : public Wt::WFormModel user.modify()->setUITheme(newTheme); } - user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField))); + { + MediaPlayer::Settings settings; - auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; - if (transcodeBitrateRow) - user.modify()->setAudioTranscodeBitrate(_transcodeBitrateModel->getValue(*transcodeBitrateRow)); + auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))}; + if (transcodeModeRow) + settings.mode = _transcodeModeModel->getValue(*transcodeModeRow); - auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; - if (transcodeFormatRow) - user.modify()->setAudioTranscodeFormat(_transcodeFormatModel->getValue(*transcodeFormatRow)); + auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; + if (transcodeFormatRow) + settings.format = _transcodeFormatModel->getValue(*transcodeFormatRow); + + auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; + if (transcodeBitrateRow) + settings.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); + + LmsApp->getMediaPlayer()->setSettings(settings); + } + + { + user.modify()->setSubsonicTranscodeEnable(Wt::asNumber(value(SubsonicTranscodeEnableField))); + + auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))}; + if (subsonicTranscodeBitrateRow) + user.modify()->setSubsonicTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); + + auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))}; + if (subsonicTranscodeFormatRow) + user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); + } if (!valueText(PasswordField).empty()) { user.modify()->setPasswordHash(passwordHash); user.modify()->clearAuthTokens(); } + + auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))}; + if (subsonicArtistListModeRow) + user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); } void loadData() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - setValue(DarkModeField, LmsApp->getUser()->getUITheme() == User::UITheme::Dark); + User::pointer user {LmsApp->getUser()}; + + setValue(DarkModeField, user->getUITheme() == User::UITheme::Dark); - setValue(TranscodeEnableField, LmsApp->getUser()->getAudioTranscodeEnable()); - if (!LmsApp->getUser()->getAudioTranscodeEnable()) { - setReadOnly(TranscodeFormatField, true); - setReadOnly(TranscodeBitrateField, true); + const auto& settings {*LmsApp->getMediaPlayer()->getSettings()}; + + auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.mode)}; + if (transcodeModeRow) + setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); + + auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.format)}; + if (transcodeFormatRow) + setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); + + auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.bitrate)}; + if (transcodeBitrateRow) + setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); } - auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(LmsApp->getUser()->getAudioTranscodeBitrate())}; - if (transcodeBitrateRow) - setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); + setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable()); + if (!LmsApp->getUser()->getSubsonicTranscodeEnable()) + { + setReadOnly(SubsonicTranscodeFormatField, true); + setReadOnly(SubsonicTranscodeBitrateField, true); + } - auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(LmsApp->getUser()->getAudioTranscodeFormat())}; - if (transcodeFormatRow) - setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); + auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicTranscodeBitrate())}; + if (subsonicTranscodeBitrateRow) + setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow)); + + auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicTranscodeFormat())}; + if (subsonicTranscodeFormatRow) + setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow)); + + auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode())}; + if (subsonicArtistListModeRow) + setValue(SubsonicArtistListModeField, _subsonicArtistListModeModel->getString(*subsonicArtistListModeRow)); } private: @@ -207,33 +269,36 @@ class SettingsModel : public Wt::WFormModel void initializeModels() { - Bitrate maxAudioBitrate; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - maxAudioBitrate = LmsApp->getUser()->getMaxAudioTranscodeBitrate(); - } + + _transcodeModeModel = std::make_shared(); + _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>(); + _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>(); for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates) { - if (bitrate > maxAudioBitrate) - break; - _transcodeBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); } _transcodeFormatModel = std::make_shared>(); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.mp3"), AudioFormat::MP3); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.ogg_opus"), AudioFormat::OGG_OPUS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.matroska_opus"), AudioFormat::MATROSKA_OPUS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.ogg_vorbis"), AudioFormat::OGG_VORBIS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.webm_vorbis"), AudioFormat::WEBM_VORBIS); + _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), AudioFormat::MP3); + _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), AudioFormat::OGG_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.webm_vorbis"), AudioFormat::WEBM_VORBIS); } bool _withOldPassword {}; - std::shared_ptr> _transcodeBitrateModel; - std::shared_ptr> _transcodeFormatModel; + std::shared_ptr _transcodeModeModel; + std::shared_ptr> _transcodeBitrateModel; + std::shared_ptr> _transcodeFormatModel; + std::shared_ptr> _subsonicArtistListModeModel; }; SettingsView::SettingsView() @@ -243,6 +308,11 @@ SettingsView::SettingsView() refreshView(); })); + LmsApp->getEvents().mediaPlayerSettingsAvailable.connect([=]() + { + refreshView(); + }); + refreshView(); } @@ -254,6 +324,10 @@ SettingsView::refreshView() clear(); + // Hack to wait for the audio player know the settings applied + if (!LmsApp->getMediaPlayer()->getSettings()) + return; + auto t {addNew(Wt::WString::tr("Lms.Settings.template"))}; auto model {std::make_shared(!LmsApp->isUserAuthStrong())}; @@ -268,6 +342,7 @@ SettingsView::refreshView() if (!LmsApp->isUserAuthStrong()) { t->setCondition("if-has-old-password", true); + auto oldPassword {std::make_unique()}; oldPassword->setEchoMode(Wt::EchoMode::Password); t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); @@ -283,35 +358,74 @@ SettingsView::refreshView() passwordConfirm->setEchoMode(Wt::EchoMode::Password); t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); - // Transcoding - auto transcode {std::make_unique()}; - auto* transcodeRaw {transcode.get()}; - t->setFormWidget(SettingsModel::TranscodeEnableField, std::move(transcode)); - - // Format - auto transcodeFormat {std::make_unique()}; - transcodeFormat->setModel(model->transcodeFormatModel()); - t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat)); - - // Bitrate - auto transcodeBitrate {std::make_unique()}; - transcodeBitrate->setModel(model->transcodeBitrateModel()); - t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); - - transcodeRaw->changed().connect([=]() + // Audio { - bool enable {transcodeRaw->checkState() == Wt::CheckState::Checked}; - model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); - model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); + // Transcode + auto transcodeMode {std::make_unique()}; + auto* transcodeModeRaw {transcodeMode.get()}; + transcodeMode->setModel(model->getTranscodeModeModel()); + t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode)); + + // Format + auto transcodeFormat {std::make_unique()}; + transcodeFormat->setModel(model->getTranscodeFormatModel()); + t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat)); + + // Bitrate + auto transcodeBitrate {std::make_unique()}; + transcodeBitrate->setModel(model->getTranscodeBitrateModel()); + t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); + + transcodeModeRaw->sactivated().connect([=]() + { + auto row {model->getTranscodeModeModel()->getRowFromString(model->valueText(SettingsModel::TranscodeModeField))}; + const bool enable = (row && (model->getTranscodeModeModel()->getValue(*row) != MediaPlayer::TranscodeMode::Never)); + model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); + model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + } + + // Subsonic + { + t->setCondition("if-has-subsonic-api", ServiceProvider::get()->getBool("api-subsonic", true)); + + // Transcode + auto transcode {std::make_unique()}; + auto* transcodeRaw {transcode.get()}; + t->setFormWidget(SettingsModel::SubsonicTranscodeEnableField, std::move(transcode)); + + // Format + auto transcodeFormat {std::make_unique()}; + transcodeFormat->setModel(model->getTranscodeFormatModel()); + t->setFormWidget(SettingsModel::SubsonicTranscodeFormatField, std::move(transcodeFormat)); + + // Bitrate + auto transcodeBitrate {std::make_unique()}; + transcodeBitrate->setModel(model->getTranscodeBitrateModel()); + t->setFormWidget(SettingsModel::SubsonicTranscodeBitrateField, std::move(transcodeBitrate)); + + // Artist list mode + auto artistListMode = std::make_unique(); + artistListMode->setModel(model->getSubsonicArtistListModeModel()); + t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode)); + + transcodeRaw->changed().connect([=]() + { + const bool enable {transcodeRaw->checkState() == Wt::CheckState::Checked}; + model->setReadOnly(SettingsModel::SubsonicTranscodeFormatField, !enable); + model->setReadOnly(SettingsModel::SubsonicTranscodeBitrateField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + } // Buttons Wt::WPushButton *saveBtn {t->bindWidget("apply-btn", std::make_unique(Wt::WString::tr("Lms.apply")))}; Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard")))}; - saveBtn->clicked().connect(std::bind([=] () + saveBtn->clicked().connect([=]() { { @@ -334,7 +448,7 @@ SettingsView::refreshView() // Udate the view: Delete any validation message in the view, etc. t->updateView(model.get()); - })); + }); discardBtn->clicked().connect(std::bind([=] () { diff --git a/src/lms/ui/admin/SubsonicView.cpp b/src/lms/ui/admin/SubsonicView.cpp deleted file mode 100644 index 9e289eb7..00000000 --- a/src/lms/ui/admin/SubsonicView.cpp +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2018 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "SubsonicView.hpp" - -#include -#include -#include -#include - -#include - -#include "database/SubsonicSettings.hpp" -#include "utils/Logger.hpp" - -#include "common/ValueStringModel.hpp" -#include "LmsApplication.hpp" - -namespace UserInterface { - -using namespace Database; - -class SubsonicModel : public Wt::WFormModel -{ - - public: - static inline const Field EnableField {"enable"}; - static inline const Field ArtistListModeField {"artist-list-mode"}; - - SubsonicModel() - { - addField(EnableField); - addField(ArtistListModeField); - - initializeModels(); - - loadData(); - } - - std::shared_ptr getArtistListModeModel() { return _artistListModeModel; } - - void saveData() - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - auto settings {SubsonicSettings::get(LmsApp->getDbSession())}; - - settings.modify()->setAPIEnabled(Wt::asNumber(value(EnableField))); - auto artistListModeRow {_artistListModeModel->getRowFromString(valueText(ArtistListModeField))}; - if (artistListModeRow) - settings.modify()->setArtistListMode(_artistListModeModel->getValue(*artistListModeRow)); - } - - void loadData() - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto settings {SubsonicSettings::get(LmsApp->getDbSession())}; - - setValue(EnableField, settings->isAPIEnabled()); - auto artistListModeRow {_artistListModeModel->getRowFromValue(settings->getArtistListMode())}; - - if (artistListModeRow) - setValue(ArtistListModeField, _artistListModeModel->getString(*artistListModeRow)); - } - - void initializeModels() - { - _artistListModeModel = std::make_shared>(); - - _artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.all-artists"), SubsonicSettings::ArtistListMode::AllArtists); - _artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.release-artists"), SubsonicSettings::ArtistListMode::ReleaseArtists); - } - - private: - - bool validateField(Field field) override - { - if (field == EnableField) - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - if (SubsonicSettings::get(LmsApp->getDbSession())->isAPIEnabled() != Wt::asNumber(value(EnableField))) - setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid, Wt::WString::tr("Lms.Admin.Subsonic.need-restart"))); - else - setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid)); - - return true; - } - - return Wt::WFormModel::validateField(field); - } - - std::shared_ptr> _artistListModeModel; -}; - -SubsonicView::SubsonicView() -{ - wApp->internalPathChanged().connect([this]() - { - refreshView(); - }); - - refreshView(); -} - -void -SubsonicView::refreshView() -{ - if (!wApp->internalPathMatches("/admin/subsonic")) - return; - - clear(); - - Wt::WTemplateFormView* t {addNew(Wt::WString::tr("Lms.Admin.Subsonic.template"))}; - - auto model {std::make_shared()}; - - // Enable - auto enable {std::make_unique()}; - auto* enableRaw {enable.get()}; - t->setFormWidget(SubsonicModel::EnableField, std::move(enable)); - - // Artis tlist mode - auto artistListMode = std::make_unique(); - artistListMode->setModel(model->getArtistListModeModel()); - t->setFormWidget(SubsonicModel::ArtistListModeField, std::move(artistListMode)); - - enableRaw->changed().connect([=]() - { - bool enable {enableRaw->checkState() == Wt::CheckState::Checked}; - model->setReadOnly(SubsonicModel::ArtistListModeField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - - Wt::WPushButton* saveBtn = t->bindNew("apply-btn", Wt::WString::tr("Lms.apply")); - Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard")))}; - - saveBtn->clicked().connect([=]() - { - t->updateModel(model.get()); - - if (model->validate()) - { - model->saveData(); - LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved")); - } - t->updateView(model.get()); - }); - - discardBtn->clicked().connect([=]() - { - model->loadData(); - model->validate(); - t->updateView(model.get()); - }); - - t->updateView(model.get()); -} - -} // namespace UserInterface - - diff --git a/src/lms/ui/admin/SubsonicView.hpp b/src/lms/ui/admin/SubsonicView.hpp deleted file mode 100644 index 775057eb..00000000 --- a/src/lms/ui/admin/SubsonicView.hpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2018 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include - -namespace UserInterface { - -class SubsonicView : public Wt::WContainerWidget -{ - public: - SubsonicView(); - - private: - void refreshView(); -}; - -} // namespace UserInterface - - diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 154c64f5..3bb52e6d 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -48,10 +48,9 @@ class UserModel : public Wt::WFormModel { public: - static const Field LoginField; - static const Field PasswordField; - static const Field AudioTranscodeBitrateLimitField; - static const Field DemoField; + static inline const Field LoginField {"login"}; + static inline const Field PasswordField {"password"}; + static inline const Field DemoField {"demo"}; UserModel(std::optional userId) : _userId {userId} @@ -63,20 +62,15 @@ class UserModel : public Wt::WFormModel } addField(PasswordField); - addField(AudioTranscodeBitrateLimitField); addField(DemoField); if (!_userId) setValidator(PasswordField, createMandatoryValidator()); - initializeModels(); - // populate the model with initial data loadData(); } - std::shared_ptr bitrateModel() { return _bitrateModel; } - void saveData() { std::optional passwordHash; @@ -96,20 +90,12 @@ class UserModel : public Wt::WFormModel user.modify()->setPasswordHash(*passwordHash); user.modify()->clearAuthTokens(); } - - auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))}; - if (transcodeBitrateLimitRow) - user.modify()->setMaxAudioTranscodeBitrate(_bitrateModel->getValue(*transcodeBitrateLimitRow)); } else { // Create user Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8(), *passwordHash)}; - auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))}; - if (transcodeBitrateLimitRow ) - user.modify()->setMaxAudioTranscodeBitrate(_bitrateModel->getValue(*transcodeBitrateLimitRow)); - if (Wt::asNumber(value(DemoField))) user.modify()->setType(Database::User::Type::DEMO); } @@ -129,10 +115,6 @@ class UserModel : public Wt::WFormModel throw UserNotFoundException {*_userId}; else if (user == LmsApp->getUser()) throw UserNotAllowedException {}; - - auto transcodeBitrateLimitRow {_bitrateModel->getRowFromValue(user->getMaxAudioTranscodeBitrate())}; - if (transcodeBitrateLimitRow) - setValue(AudioTranscodeBitrateLimitField, _bitrateModel->getString(*transcodeBitrateLimitRow)); } std::string getLoginName() const @@ -194,30 +176,15 @@ class UserModel : public Wt::WFormModel return false; } - - void initializeModels() - { - _bitrateModel = std::make_shared>(); - - for (auto bitrate : Database::User::audioTranscodeAllowedBitrates) - _bitrateModel->add( Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate ); - } - - std::shared_ptr> _bitrateModel; std::optional _userId; }; -const Wt::WFormModel::Field UserModel::LoginField = "login"; -const Wt::WFormModel::Field UserModel::PasswordField = "password"; -const Wt::WFormModel::Field UserModel::AudioTranscodeBitrateLimitField = "audio-transcode-bitrate-limit"; -const Wt::WFormModel::Field UserModel::DemoField = "demo"; - UserView::UserView() { - wApp->internalPathChanged().connect(std::bind([=] + wApp->internalPathChanged().connect([this]() { refreshView(); - })); + }); refreshView(); } @@ -262,18 +229,13 @@ UserView::refreshView() passwordEdit->setEchoMode(Wt::EchoMode::Password); t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit)); - // Transcode bitrate limit - auto bitrate = std::make_unique(); - bitrate->setModel(model->bitrateModel()); - t->setFormWidget(UserModel::AudioTranscodeBitrateLimitField, std::move(bitrate)); - // Demo account t->setFormWidget(UserModel::DemoField, std::make_unique()); if (!userId && ServiceProvider::get()->getBool("demo", false)) t->setCondition("if-demo", true); Wt::WPushButton* saveBtn = t->bindNew("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create")); - saveBtn->clicked().connect(std::bind([=] + saveBtn->clicked().connect([=]() { t->updateModel(model.get()); @@ -287,7 +249,7 @@ UserView::refreshView() { t->updateView(model.get()); } - })); + }); t->updateView(model.get()); } diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index ece62937..8288247c 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -73,32 +73,27 @@ AudioTranscodeResource:: ~AudioTranscodeResource() } static -std::optional -AudioFormatToAvEncoding(Database::AudioFormat format) +std::optional +AudioFormatToAvFormat(Database::AudioFormat format) { switch (format) { - case Database::AudioFormat::MP3: return Av::Encoding::MP3; - case Database::AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS; - case Database::AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS; - case Database::AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS; - case Database::AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS; + case Database::AudioFormat::MP3: return Av::Format::MP3; + case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; + case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; + case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; + case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; } - LOG(ERROR) << "Cannot convert from audio format to encoding"; + LOG(ERROR) << "Cannot convert from audio format to AV format"; return std::nullopt; } std::string -AudioTranscodeResource::getUrlForUser(Database::IdType trackId, Database::User::pointer user) const +AudioTranscodeResource::getUrl(Database::IdType trackId) const { - std::string computedUrl {url() + "&trackid=" + std::to_string(trackId)}; - - computedUrl += "&format=" + std::to_string(static_cast(user->getAudioTranscodeFormat())); - computedUrl += "&bitrate=" + std::to_string(LmsApp->getUser()->getAudioTranscodeBitrate()); - - return computedUrl; + return url() + "&trackid=" + std::to_string(trackId); } template @@ -144,8 +139,8 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, if (!trackId || !format || !bitrate) return; - auto encoding {AudioFormatToAvEncoding(*format)}; - if (!encoding) + auto avFormat {AudioFormatToAvFormat(*format)}; + if (!avFormat) return; // optional parameter @@ -166,7 +161,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, trackPath = track->getPath(); - if (!LmsApp->getUser()->checkBitrate(*bitrate)) + if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates)) { LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; return; @@ -175,7 +170,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, Av::TranscodeParameters parameters {}; parameters.stripMetadata = true; - parameters.encoding = *encoding; + parameters.format = *avFormat; parameters.bitrate = *bitrate; parameters.offset = std::chrono::seconds {offset ? *offset : 0}; diff --git a/src/lms/ui/resource/AudioTranscodeResource.hpp b/src/lms/ui/resource/AudioTranscodeResource.hpp index fa5d2ed7..a23e6602 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.hpp +++ b/src/lms/ui/resource/AudioTranscodeResource.hpp @@ -37,7 +37,7 @@ class AudioTranscodeResource : public Wt::WResource ~AudioTranscodeResource(); // Url depends on the user since settings are used in parameters - std::string getUrlForUser(Database::IdType trackId, Wt::Dbo::ptr user) const; + std::string getUrl(Database::IdType trackId) const; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); From f41b6c38a7366eea56929332894b6d8407bec985 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Apr 2020 14:39:49 +0200 Subject: [PATCH 06/13] WIP replaygain support --- docroot/js/mediaplayer.js | 61 ++-- src/libs/database/impl/Session.cpp | 10 +- src/libs/database/impl/Track.cpp | 29 +- src/libs/database/include/database/Track.hpp | 12 +- src/libs/metadata/impl/TagLibParser.cpp | 40 ++- .../metadata/include/metadata/IParser.hpp | 2 + src/libs/scanner/impl/MediaScanner.cpp | 56 +++- src/lms/ui/LmsApplication.cpp | 10 +- src/lms/ui/LmsApplication.hpp | 7 - src/lms/ui/MediaPlayer.cpp | 270 ++++++++++++------ src/lms/ui/MediaPlayer.hpp | 77 +++-- src/lms/ui/PlayHistoryView.cpp | 3 +- src/lms/ui/PlayQueueView.cpp | 33 ++- src/lms/ui/PlayQueueView.hpp | 4 +- src/lms/ui/SettingsView.cpp | 28 +- src/lms/ui/explore/ArtistsInfoView.cpp | 3 +- src/lms/ui/explore/ReleaseView.cpp | 20 +- src/lms/ui/explore/ReleasesInfoView.cpp | 3 +- src/lms/ui/explore/TracksInfoView.cpp | 5 +- src/lms/ui/explore/TracksView.cpp | 8 +- src/test/database/DatabaseTest.cpp | 9 +- src/tools/metadata/LmsMetadata.cpp | 6 + 22 files changed, 480 insertions(+), 216 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 6135c86a..bf460e12 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -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,11 @@ LMS.mediaplayer = function () { } } - var init = function(root, defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { + var _setReplayGain = function (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 +138,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 +195,7 @@ LMS.mediaplayer = function () { }); _initVolume(); - _initTranscodeSettings(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate); + _initDefaultSettings(defaultSettings); _elems.volumeslider.addEventListener("input", function() { _setVolume(_elems.volumeslider.value); @@ -251,23 +247,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 +286,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); } } diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 22a7b250..2e96ffd1 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -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"; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index e62f875f..f0deedab 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -38,6 +38,14 @@ _filePath( p.string() ) { } +std::size_t +Track::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track"); +} + std::vector Track::getAll(Session& session, std::optional limit) { @@ -107,13 +115,26 @@ Track::create(Session& session, const std::filesystem::path& p) return res; } -std::vector -Track::getAllPaths(Session& session) +std::vector> +Track::getAllPaths(Session& session, std::optional offset, std::optional size) { + using QueryResultType = std::tuple; session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query("SELECT file_path FROM track"); - return std::vector(res.begin(), res.end()); + Wt::Dbo::collection queryRes = session.getDboSession().query("SELECT id,file_path FROM track") + .limit(size ? static_cast(*size) + 1 : -1) + .offset(offset ? static_cast(*offset) : -1); + + std::vector> 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 diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 2bbeb9ee..9dbd4083 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo 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 static std::vector getAll(Session& session, std::optional limit = {}); static std::vector getAllRandom(Session& session, std::optional limit = {}); static std::vector getAllIds(Session& session); - static std::vector getAllPaths(Session& session); + static std::vector> getAllPaths(Session& session, std::optional offset = std::nullopt, std::optional size = std::nullopt); static std::vector getMBIDDuplicates(Session& session); static std::vector getLastAdded(Session& session, const Wt::WDateTime& after, std::optional size = 1); static std::vector getAllWithMBIDAndMissingFeatures(Session& session); @@ -100,6 +101,8 @@ class Track : public Wt::Dbo::Dbo void setMBID(const std::optional& 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& artistLink); void setRelease(Wt::Dbo::ptr release) { _release = release; } @@ -122,6 +125,9 @@ class Track : public Wt::Dbo::Dbo std::optional getMBID() const { return UUID::fromString(_MBID); } std::optional getCopyright() const; std::optional getCopyrightURL() const; + std::optional getTrackReplayGain() const { return _trackReplayGain; } + std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + std::vector> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector> getArtistLinks() const; @@ -152,6 +158,8 @@ class Track : public Wt::Dbo::Dbo 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 std::string _MBID; // Musicbrainz Identifier std::string _copyright; std::string _copyrightURL; + std::optional _trackReplayGain; + std::optional _releaseReplayGain; Wt::Dbo::ptr _release; Wt::Dbo::collection> _trackArtistLinks; diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 2fe31d73..d6a9f3c5 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -19,13 +19,16 @@ #include "metadata/TagLibParser.hpp" +#include #include #include #include #include +#include #include #include #include +#include #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(value); + else if (tag == "REPLAYGAIN_TRACK_GAIN") + track.trackReplayGain = StringUtils::readAs(value); else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { std::set 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(f.file())}) + { + getAPETags(mpcFile->APETag()); + } + // WavPack + else if (TagLib::WavPack::File* wavPackFile {dynamic_cast(f.file())}) + { + getAPETags(wavPackFile->APETag()); } // FLAC else if (TagLib::FLAC::File* flacFile {dynamic_cast(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}; diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 014fed3e..68b72b4d 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -75,6 +75,8 @@ namespace MetaData std::optional acoustID; std::string copyright; std::string copyrightURL; + std::optional trackReplayGain; + std::optional albumReplayGain; }; class IParser diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index f8a8fa66..c9ead16d 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -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 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> trackPaths; + std::vector 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 diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index eb57811b..ff5df69f 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -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 diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 9ec6968f..e66d7ec0 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -53,13 +53,6 @@ struct Events Wt::Signal appOpen; Wt::Signal appClosed; - // A track is being loaded - Wt::Signal trackLoaded; - std::optional lastLoadedTrackId; - Wt::Signal<> mediaPlayerSettingsAvailable; - // Unload current track - Wt::Signal<> trackUnloaded; - // Database events Wt::Signal<> dbScanned; Wt::Signal dbScanInProgress; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 6127b2d5..f141282a 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -19,6 +19,10 @@ #include "MediaPlayer.hpp" +#include +#include +#include + #include "utils/Logger.hpp" #include "database/Artist.hpp" @@ -36,6 +40,108 @@ namespace UserInterface { +static std::string settingsToJSString(const MediaPlayer::Settings& settings) +{ + namespace Json = Wt::Json; + + Json::Object res; + + { + Json::Object transcode; + transcode["mode"] = static_cast(settings.transcode.mode); + transcode["format"] = static_cast(settings.transcode.format); + transcode["bitrate"] = static_cast(settings.transcode.bitrate); + res["transcode"] = std::move(transcode); + } + + { + Json::Object replayGain; + replayGain["mode"] = "TODO"; + replayGain["preAmpGain"] = 0; + replayGain["clippingPreventionMode"] = "TODO"; + res["replayGain"] = std::move(replayGain); + } + + return Json::serialize(res); +} + +static +std::optional +modeFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + MediaPlayer::Settings::Transcode::Mode mode {static_cast(*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 +formatFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + MediaPlayer::Format format {static_cast(*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 +bitrateFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates)) + return *value; + + return std::nullopt; +} + +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 = modeFromString(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); + } + + return settings; +} MediaPlayer::MediaPlayer() : Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")}, @@ -48,119 +154,99 @@ MediaPlayer::MediaPlayer() _artist = bindNew("artist"); _release = bindNew("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(mode)) - { - case TranscodeMode::Always: - case TranscodeMode::Never: - case TranscodeMode::IfFormatNotSupported: - settings.mode = static_cast(mode); - break; - } - - switch (static_cast(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); - 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(defaultTranscodeMode) - << ", " << static_cast(defaultTranscodeFormat) - << ", " << static_cast(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(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(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 +262,7 @@ MediaPlayer::setSettings(const Settings& settings) { std::ostringstream oss; - oss << "LMS.mediaplayer.setSettings(" - << static_cast(_settings->mode) - << ", " << static_cast(_settings->format) - << ", " << static_cast(_settings->bitrate) - << ")"; + oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; doJavaScript(oss.str()); diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index bfb02c5f..594624e2 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -34,24 +34,57 @@ 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 ClippingPreventionMode defaultClippingPreventionMode {ClippingPreventionMode::None}; + + Mode mode; + Gain preAmpGain; + ClippingPreventionMode clippingPreventionMode; + }; + + Transcode transcode; + ReplayGain replayGain; }; MediaPlayer(); @@ -61,21 +94,27 @@ class MediaPlayer : public Wt::WTemplate MediaPlayer& operator=(const MediaPlayer&) = delete; MediaPlayer& operator=(MediaPlayer&&) = delete; + std::optional getTrackLoaded() const { return _trackIdLoaded; } + + void loadTrack(Database::IdType trackId, bool play, float replayGain); + void stop(); + std::optional 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 trackLoaded; + Wt::Signal<> settingsLoaded; private: - void stop(); - void loadTrack(Database::IdType trackId, bool play); + std::optional _trackIdLoaded; std::optional _settings; - Wt::JSignal _settingsLoaded; + Wt::JSignal _settingsLoaded; Wt::WText* _title; Wt::WAnchor* _release; Wt::WAnchor* _artist; diff --git a/src/lms/ui/PlayHistoryView.cpp b/src/lms/ui/PlayHistoryView.cpp index 6c08ddc6..0fd2c956 100644 --- a/src/lms/ui/PlayHistoryView.cpp +++ b/src/lms/ui/PlayHistoryView.cpp @@ -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()}; diff --git a/src/lms/ui/PlayQueueView.cpp b/src/lms/ui/PlayQueueView.cpp index 1ca16743..e50b81ac 100644 --- a/src/lms/ui/PlayQueueView.cpp +++ b/src/lms/ui/PlayQueueView.cpp @@ -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 { @@ -209,6 +210,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play) Database::IdType trackId {}; bool addRadioTrack {}; + std::optional replayGain {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -235,6 +237,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 +248,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 +436,30 @@ PlayQueue::enqueueRadioTrack() enqueueTracks(trackToAddIds); } +std::optional +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 diff --git a/src/lms/ui/PlayQueueView.hpp b/src/lms/ui/PlayQueueView.hpp index 4bc8f808..222b53f0 100644 --- a/src/lms/ui/PlayQueueView.hpp +++ b/src/lms/ui/PlayQueueView.hpp @@ -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 trackSelected; + Wt::Signal trackSelected; // Signal emitted when track is unselected (has to be stopped) Wt::Signal<> trackUnselected; @@ -76,6 +77,7 @@ class PlayQueue : public Wt::WTemplate void addRadioTrackFromSimilarity(std::shared_ptr similarityFinder); void addRadioTrackFromClusters(); + std::optional getReplayGain(const Wt::Dbo::ptr& track) const; bool _repeatAll {}; bool _radioMode {}; diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 73bd3b6b..244da836 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -59,7 +59,7 @@ class SettingsModel : public Wt::WFormModel static inline const Field PasswordField {"password"}; static inline const Field PasswordConfirmField {"password-confirm"}; - using TranscodeModeModel = ValueStringModel; + using TranscodeModeModel = ValueStringModel; SettingsModel(bool withOldPassword) : _withOldPassword {withOldPassword} @@ -118,15 +118,15 @@ 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); LmsApp->getMediaPlayer()->setSettings(settings); } @@ -165,15 +165,15 @@ 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)); } @@ -271,9 +271,9 @@ class SettingsModel : public Wt::WFormModel { _transcodeModeModel = std::make_shared(); - _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); + _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); _subsonicArtistListModeModel = std::make_shared>(); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); @@ -303,12 +303,12 @@ class SettingsModel : public Wt::WFormModel SettingsView::SettingsView() { - wApp->internalPathChanged().connect(std::bind([=] + wApp->internalPathChanged().connect([=] { refreshView(); - })); + }); - LmsApp->getEvents().mediaPlayerSettingsAvailable.connect([=]() + LmsApp->getMediaPlayer()->settingsLoaded.connect([=]() { refreshView(); }); @@ -379,7 +379,7 @@ SettingsView::refreshView() transcodeModeRaw->sactivated().connect([=]() { auto row {model->getTranscodeModeModel()->getRowFromString(model->valueText(SettingsModel::TranscodeModeField))}; - const bool enable = (row && (model->getTranscodeModeModel()->getValue(*row) != MediaPlayer::TranscodeMode::Never)); + const bool enable = (row && (model->getTranscodeModeModel()->getValue(*row) != MediaPlayer::Settings::Transcode::Mode::Never)); model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); t->updateModel(model.get()); diff --git a/src/lms/ui/explore/ArtistsInfoView.cpp b/src/lms/ui/explore/ArtistsInfoView.cpp index 18e8d118..e9181a64 100644 --- a/src/lms/ui/explore/ArtistsInfoView.cpp +++ b/src/lms/ui/explore/ArtistsInfoView.cpp @@ -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(); }); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 5df3ba4c..3604b266 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -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("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("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"); } } } diff --git a/src/lms/ui/explore/ReleasesInfoView.cpp b/src/lms/ui/explore/ReleasesInfoView.cpp index 14ccb04d..0693cc32 100644 --- a/src/lms/ui/explore/ReleasesInfoView.cpp +++ b/src/lms/ui/explore/ReleasesInfoView.cpp @@ -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(); }); diff --git a/src/lms/ui/explore/TracksInfoView.cpp b/src/lms/ui/explore/TracksInfoView.cpp index 58469612..d05cc200 100644 --- a/src/lms/ui/explore/TracksInfoView.cpp +++ b/src/lms/ui/explore/TracksInfoView.cpp @@ -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::vectorgetEvents().trackLoaded.connect([=] + LmsApp->getMediaPlayer()->trackLoaded.connect([=] { refreshMostPlayed(); }); diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index 14adef53..5ccf0252 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -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"); } } diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index 42c39505..6b57df85 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -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); + } } diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index b3af8687..7cbfd29b 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -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; From 0de476c5d312536efd9f8ca0a574f435c39f5331 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Apr 2020 22:00:22 +0200 Subject: [PATCH 07/13] WIP replaygain support --- approot/messages.xml | 6 + approot/messages_fr.xml | 7 + approot/settings.xml | 290 +++++++++++++++++------------------ docroot/js/mediaplayer.js | 1 + src/lms/ui/LmsTheme.cpp | 4 +- src/lms/ui/MediaPlayer.cpp | 63 ++++++-- src/lms/ui/MediaPlayer.hpp | 2 + src/lms/ui/PlayQueueView.cpp | 7 +- src/lms/ui/PlayQueueView.hpp | 1 + src/lms/ui/SettingsView.cpp | 72 ++++++++- 10 files changed, 286 insertions(+), 167 deletions(-) diff --git a/approot/messages.xml b/approot/messages.xml index ede3290f..580032ec 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -164,6 +164,12 @@ Cannot save using a demo account! Bad password Old password must be filled in +ReplayGain mode +No ReplayGain +Auto +Track +Album +Replay gain preAmp Artist list mode All artists Album artists diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 571aae0a..78dcda2f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -164,6 +164,12 @@ Impossible de sauvegarder en utilisant un compte de démo ! Mauvais mot de passe L'ancien mot de passe doit être renseigné +Mode ReplayGain +Pas de ReplayGain +Automatique +Piste +Album +PreAmplification ReplayGain Mode de listage des artistes Tous les artistes Tous les artistes d'album @@ -188,4 +194,5 @@ Oui Non Ce champ ne peut pas être vide +Le nombre doit être compris entre {1} et {2} diff --git a/approot/settings.xml b/approot/settings.xml index cf267f9d..6d8e2409 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -4,161 +4,157 @@ - ${tr:Lms.Settings.appearance} -
-
- -
-
+
+
+ ${tr:Lms.Settings.appearance} +
+
+
+
+ + ${dark-mode-info clas="help-block"} +
+
+
-
- ${dark-mode-info} -
-
-
- ${tr:Lms.Settings.audio} -
-
-
+ ${tr:Lms.Settings.audio} +
${tr:Lms.Settings.audio-settings-are-local}
-
-
-
-
- -
- ${transcode-mode} -
-
- ${transcode-mode-info} -
-
-
- -
- ${transcode-format} -
-
- ${transcode-format-info} -
-
-
- -
-
- ${transcode-bitrate} - kbps +
+
+ +
+ ${transcode-mode} + ${transcode-mode-info class="help-block"} +
+
+
+ +
+ ${transcode-format} + ${transcode-format-info class="help-block"} +
+
+
+ +
+
+ ${transcode-bitrate} + kbps +
+ ${transcode-bitrate-info class="help-block"} +
+
+ +
+ +
+ ${replaygain-mode} + ${replaygain-mode-info class="help-block"} +
+
+
+ +
+
+ ${replaygain-preamp} + dB +
+ ${replaygain-preamp-info class="help-block"} +
-
- ${transcode-bitrate-info} -
-
-
- ${} - ${tr:Lms.Settings.subsonic-api} -
- -
- -
-
-
-
- ${subsonic-transcode-enable-info} -
-
-
- -
- ${subsonic-transcode-format} -
-
- ${subsonic-transcode-format-info} -
-
-
- -
-
- ${subsonic-transcode-bitrate} - kbps + ${} + ${tr:Lms.Settings.subsonic-api} +
+
+
+
+ + ${subsonic-transcode-enable-info class="help-block"} +
+
+
+
+ +
+ ${subsonic-transcode-format} + ${subsonic-transcode-format-info class="help-block"} +
+
+
+ +
+
+ ${subsonic-transcode-bitrate} + kbps +
+ ${subsonic-transcode-bitrate-info class="help-block"} +
+
+
+ +
+ ${subsonic-artist-list-mode} + ${subsonic-artist-list-mode-info class="help-block"} +
-
- ${subsonic-transcode-bitrate-info} -
-
-
- -
- ${subsonic-artist-list-mode} -
-
- ${subsonic-artist-list-mode-info} -
-
+ ${} + ${tr:Lms.Settings.change-password} +
+ ${} +
+ +
+ ${password-old} + ${password-old-info class="help-block"} +
+
+ ${
} +
+ +
+ ${password} + ${password-info class="help-block"} +
+
+
+ +
+ ${password-confirm} + ${password-confirm-info class="help-block"} +
+
-
- ${} - ${tr:Lms.Settings.change-password} -
- ${} -
- -
- ${password-old} -
-
- ${password-old-info} -
-
- ${
} -
- -
- ${password} -
-
- ${password-info} -
-
-
- -
- ${password-confirm} -
-
- ${password-confirm-info} -
-
- -
-
- ${apply-btn class="btn-primary"} ${discard-btn} +
+
+ ${apply-btn class="btn-primary"} ${discard-btn} +
+
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index bf460e12..c5ebcb3f 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -121,6 +121,7 @@ LMS.mediaplayer = function () { } var _setReplayGain = function (replayGain) { + console.log("Applying gain " + replayGain); _gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20); } diff --git a/src/lms/ui/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp index 9fcc0df1..1ab59b3a 100644 --- a/src/lms/ui/LmsTheme.cpp +++ b/src/lms/ui/LmsTheme.cpp @@ -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"}, }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index f141282a..632d025d 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -35,6 +35,7 @@ #include "resource/AudioFileResource.hpp" #include "utils/String.hpp" +#include "utils/Utils.hpp" #include "LmsApplication.hpp" @@ -56,9 +57,8 @@ static std::string settingsToJSString(const MediaPlayer::Settings& settings) { Json::Object replayGain; - replayGain["mode"] = "TODO"; - replayGain["preAmpGain"] = 0; - replayGain["clippingPreventionMode"] = "TODO"; + replayGain["mode"] = static_cast(settings.replayGain.mode); + replayGain["preAmpGain"] = settings.replayGain.preAmpGain; res["replayGain"] = std::move(replayGain); } @@ -67,7 +67,7 @@ static std::string settingsToJSString(const MediaPlayer::Settings& settings) static std::optional -modeFromString(const std::string& str) +transcodeModeFromString(const std::string& str) { const auto value {StringUtils::readAs(str)}; if (!value) @@ -121,6 +121,38 @@ bitrateFromString(const std::string& str) return std::nullopt; } +static +std::optional +replayGainModeFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + MediaPlayer::Settings::ReplayGain::Mode mode {static_cast(*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 +replayGainPreAmpGainFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(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; @@ -131,13 +163,24 @@ static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings MediaPlayer::Settings settings; - const Json::Value transcodeValue {parsedSettings.get("transcode")}; - if (transcodeValue.type() == Json::Type::Object) { - const Json::Object transcode {transcodeValue}; - settings.transcode.mode = modeFromString(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 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; diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 594624e2..6150756c 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -76,6 +76,8 @@ class MediaPlayer : public Wt::WTemplate 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; diff --git a/src/lms/ui/PlayQueueView.cpp b/src/lms/ui/PlayQueueView.cpp index e50b81ac..29b1c37f 100644 --- a/src/lms/ui/PlayQueueView.cpp +++ b/src/lms/ui/PlayQueueView.cpp @@ -136,8 +136,13 @@ PlayQueue::PlayQueue() if (!LmsApp->getUser()->isDemo()) { - LmsApp->post([=] + LmsApp->getMediaPlayer()->settingsLoaded.connect([=] { + if (_mediaPlayerSettingsLoaded) + return; + + _mediaPlayerSettingsLoaded = true; + std::size_t trackPos {}; { diff --git a/src/lms/ui/PlayQueueView.hpp b/src/lms/ui/PlayQueueView.hpp index 222b53f0..5bb8c605 100644 --- a/src/lms/ui/PlayQueueView.hpp +++ b/src/lms/ui/PlayQueueView.hpp @@ -81,6 +81,7 @@ class PlayQueue : public Wt::WTemplate bool _repeatAll {}; bool _radioMode {}; + bool _mediaPlayerSettingsLoaded {}; Database::IdType _tracklistId {}; Wt::WContainerWidget* _entriesContainer {}; Wt::WPushButton* _showMore {}; diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 244da836..a06db8b8 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -21,6 +21,8 @@ #include #include +#include +#include #include #include #include @@ -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"}; @@ -60,6 +64,7 @@ class SettingsModel : public Wt::WFormModel static inline const Field PasswordConfirmField {"password-confirm"}; using TranscodeModeModel = ValueStringModel; + using ReplayGainModeModel = ValueStringModel; 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()}; + 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 getTranscodeModeModel() { return _transcodeModeModel; } std::shared_ptr getTranscodeBitrateModel() { return _transcodeBitrateModel; } std::shared_ptr getTranscodeFormatModel() { return _transcodeFormatModel; } + std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } void saveData() @@ -128,6 +142,12 @@ class SettingsModel : public Wt::WFormModel if (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); } @@ -176,6 +196,12 @@ class SettingsModel : public Wt::WFormModel 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()); @@ -275,10 +301,6 @@ class SettingsModel : public Wt::WFormModel _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); - _subsonicArtistListModeModel = std::make_shared>(); - _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>(); 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->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>(); + _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,6 +330,7 @@ class SettingsModel : public Wt::WFormModel std::shared_ptr _transcodeModeModel; std::shared_ptr> _transcodeBitrateModel; std::shared_ptr> _transcodeFormatModel; + std::shared_ptr _replayGainModeModel; std::shared_ptr> _subsonicArtistListModeModel; }; @@ -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::Settings::Transcode::Mode::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()}; + auto* replayGainModeRaw {replayGainMode.get()}; + replayGainMode->setModel(model->getReplayGainModeModel()); + t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode)); + + // Replay gain preampGain + auto replayGainPreampGain {std::make_unique()}; + 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 From 40b6602aa47086c63f9c890c4fe468828e5bd53d Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Apr 2020 13:34:07 +0200 Subject: [PATCH 08/13] removed unused stuff --- src/lms/ui/MediaPlayer.hpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 6150756c..1b78e192 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -68,21 +68,13 @@ class MediaPlayer : public Wt::WTemplate 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; From e60effb1683565ad68355cfa90d0b3dd5dbeaa9e Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Apr 2020 14:29:29 +0200 Subject: [PATCH 09/13] Restored playback on mobile devices --- docroot/js/mediaplayer.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index c5ebcb3f..e64dd6a1 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -24,8 +24,8 @@ LMS.mediaplayer = function () { var _audioNativeSrc; var _audioTranscodeSrc; var _settings = {}; - var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - var _gainNode = audioCtx.createGain(); + var _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + var _gainNode = _audioCtx.createGain(); var _updateControls = function() { if (_elems.audio.paused) { @@ -121,7 +121,6 @@ LMS.mediaplayer = function () { } var _setReplayGain = function (replayGain) { - console.log("Applying gain " + replayGain); _gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20); } @@ -139,23 +138,27 @@ LMS.mediaplayer = function () { _elems.volume = document.getElementById("lms-mp-volume"); _elems.volumeslider = document.getElementById("lms-mp-volume-slider"); - var source = audioCtx.createMediaElementSource(_elems.audio); + var source = _audioCtx.createMediaElementSource(_elems.audio); source.connect(_gainNode); - _gainNode.connect(audioCtx.destination); + _gainNode.connect(_audioCtx.destination); _elems.playpause.addEventListener("click", function() { if (_elems.audio.paused) { - if (_elems.audio.firstChild) + if (_elems.audio.firstChild) { + _audioCtx.resume(); _playTrack(); + } } else _elems.audio.pause(); }); _elems.previous.addEventListener("click", function() { + _audioCtx.resume(); _requestPreviousTrack(); }); _elems.next.addEventListener("click", function() { + _audioCtx.resume(); _requestNextTrack(); }); _elems.seek.addEventListener("change", function() { @@ -263,8 +266,8 @@ LMS.mediaplayer = function () { { _addAudioSource(_audioTranscodeSrc); } - _elems.audio.load(); + _setReplayGain(params.replayGain); _elems.curtime.innerHTML = _durationToString(_offset); From dab89554b4d4965cc7c9c09fd042c3b68abf2eb0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Apr 2020 14:31:26 +0200 Subject: [PATCH 10/13] Actually implementing the auto replay gain mode --- src/lms/ui/PlayQueueView.cpp | 39 +++++++++++++++++++++++++++++------- src/lms/ui/PlayQueueView.hpp | 4 ++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/lms/ui/PlayQueueView.cpp b/src/lms/ui/PlayQueueView.cpp index 29b1c37f..222d5299 100644 --- a/src/lms/ui/PlayQueueView.cpp +++ b/src/lms/ui/PlayQueueView.cpp @@ -182,7 +182,7 @@ PlayQueue::updateRadioBtn() } Database::TrackList::pointer -PlayQueue::getTrackList() +PlayQueue::getTrackList() const { return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); } @@ -242,7 +242,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play) trackId = track.id(); - replayGain = getReplayGain(track); + replayGain = getReplayGain(pos, track); if (!LmsApp->getUser()->isDemo()) LmsApp->getUser().modify()->setCurPlayingTrackPos(pos); @@ -442,27 +442,52 @@ PlayQueue::enqueueRadioTrack() } std::optional -PlayQueue::getReplayGain(const Database::Track::pointer& track) const +PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) const { const auto& settings {LmsApp->getMediaPlayer()->getSettings()}; if (!settings) return std::nullopt; + std::optional gain; + switch (settings->replayGain.mode) { case MediaPlayer::Settings::ReplayGain::Mode::None: - return std::nullopt; + break; case MediaPlayer::Settings::ReplayGain::Mode::Track: - return track->getTrackReplayGain(); + gain = track->getTrackReplayGain(); + break; case MediaPlayer::Settings::ReplayGain::Mode::Release: - return track->getReleaseReplayGain(); + gain = track->getReleaseReplayGain(); + break; case MediaPlayer::Settings::ReplayGain::Mode::Auto: - return track->getTrackReplayGain(); + { + const auto trackList {getTrackList()}; + const auto prevEntry {pos > 0 ? trackList->getEntry(pos - 1) : Database::TrackListEntry::pointer {}}; + const auto nextEntry {trackList->getEntry(pos + 1)}; + const Database::Track::pointer prevTrack {prevEntry ? prevEntry->getTrack() : Database::Track::pointer {}}; + const Database::Track::pointer nextTrack {nextEntry ? nextEntry->getTrack() : Database::Track::pointer {}}; + + if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease()) + || + (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease())) + { + gain = track->getReleaseReplayGain(); + } + else + { + gain = track->getTrackReplayGain(); + } + break; + } } + if (gain) + return *gain + settings->replayGain.preAmpGain; + return std::nullopt; } diff --git a/src/lms/ui/PlayQueueView.hpp b/src/lms/ui/PlayQueueView.hpp index 5bb8c605..4e92c2fe 100644 --- a/src/lms/ui/PlayQueueView.hpp +++ b/src/lms/ui/PlayQueueView.hpp @@ -60,7 +60,7 @@ class PlayQueue : public Wt::WTemplate Wt::Signal<> trackUnselected; private: - Wt::Dbo::ptr getTrackList(); + Wt::Dbo::ptr getTrackList() const; void clearTracks(); void enqueueTracks(const std::vector& trackIds); @@ -77,7 +77,7 @@ class PlayQueue : public Wt::WTemplate void addRadioTrackFromSimilarity(std::shared_ptr similarityFinder); void addRadioTrackFromClusters(); - std::optional getReplayGain(const Wt::Dbo::ptr& track) const; + std::optional getReplayGain(std::size_t pos, const Wt::Dbo::ptr& track) const; bool _repeatAll {}; bool _radioMode {}; From a2b85f7e6b6052f1797c0c4d05fd2d6533bba2fc Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Apr 2020 14:57:33 +0200 Subject: [PATCH 11/13] ReplayGain: added a dedicated preamp gain for tracks that do not have RG info. ref #38 --- approot/messages.xml | 3 ++- approot/settings.xml | 14 +++++++++++++- src/lms/ui/MediaPlayer.cpp | 2 ++ src/lms/ui/MediaPlayer.hpp | 5 +++-- src/lms/ui/PlayQueueView.cpp | 8 ++++++-- src/lms/ui/SettingsView.cpp | 22 ++++++++++++++++++++-- 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/approot/messages.xml b/approot/messages.xml index 580032ec..e6746547 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -169,7 +169,8 @@ Auto Track Album -Replay gain preAmp +ReplayGain preamp +ReplayGain preamp (if no info) Artist list mode All artists Album artists diff --git a/approot/settings.xml b/approot/settings.xml index 6d8e2409..24bd864a 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -5,7 +5,7 @@
-
+
${tr:Lms.Settings.appearance}
@@ -74,6 +74,18 @@ ${replaygain-preamp-info class="help-block"}
+
+ +
+
+ ${replaygain-preamp-no-rg-info} + dB +
+ ${replaygain-preamp-no-rg-info-info class="help-block"} +
+
${} ${tr:Lms.Settings.subsonic-api} diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 632d025d..b61c7236 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -59,6 +59,7 @@ static std::string settingsToJSString(const MediaPlayer::Settings& settings) Json::Object replayGain; replayGain["mode"] = static_cast(settings.replayGain.mode); replayGain["preAmpGain"] = settings.replayGain.preAmpGain; + replayGain["preAmpGainIfNoInfo"] = settings.replayGain.preAmpGainIfNoInfo; res["replayGain"] = std::move(replayGain); } @@ -180,6 +181,7 @@ static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings 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); + settings.replayGain.preAmpGainIfNoInfo = replayGainPreAmpGainFromString(replayGain.get("preAmpGainIfNoInfo").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain); } } diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 1b78e192..58f7b432 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -73,8 +73,9 @@ class MediaPlayer : public Wt::WTemplate static inline constexpr Gain minPreAmpGain {-15}; static inline constexpr Gain maxPreAmpGain {15}; - Mode mode; - Gain preAmpGain; + Mode mode {defaultMode}; + Gain preAmpGain {defaultPreAmpGain}; + Gain preAmpGainIfNoInfo {defaultPreAmpGain}; }; Transcode transcode; diff --git a/src/lms/ui/PlayQueueView.cpp b/src/lms/ui/PlayQueueView.cpp index 222d5299..b634429d 100644 --- a/src/lms/ui/PlayQueueView.cpp +++ b/src/lms/ui/PlayQueueView.cpp @@ -453,7 +453,7 @@ PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) switch (settings->replayGain.mode) { case MediaPlayer::Settings::ReplayGain::Mode::None: - break; + return std::nullopt; case MediaPlayer::Settings::ReplayGain::Mode::Track: gain = track->getTrackReplayGain(); @@ -461,6 +461,8 @@ PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) case MediaPlayer::Settings::ReplayGain::Mode::Release: gain = track->getReleaseReplayGain(); + if (!gain) + gain = track->getTrackReplayGain(); break; case MediaPlayer::Settings::ReplayGain::Mode::Auto: @@ -476,6 +478,8 @@ PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease())) { gain = track->getReleaseReplayGain(); + if (!gain) + gain = track->getTrackReplayGain(); } else { @@ -488,7 +492,7 @@ PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) if (gain) return *gain + settings->replayGain.preAmpGain; - return std::nullopt; + return settings->replayGain.preAmpGainIfNoInfo; } } // namespace UserInterface diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index a06db8b8..92a6aa31 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -55,6 +55,7 @@ class SettingsModel : public Wt::WFormModel 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 ReplayGainPreAmpGainIfNoInfoField {"replaygain-preamp-no-rg-info"}; 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"}; @@ -77,6 +78,7 @@ class SettingsModel : public Wt::WFormModel addField(TranscodeFormatField); addField(ReplayGainModeField); addField(ReplayGainPreAmpGainField); + addField(ReplayGainPreAmpGainIfNoInfoField); addField(SubsonicTranscodeEnableField); addField(SubsonicTranscodeBitrateField); addField(SubsonicTranscodeFormatField); @@ -91,11 +93,16 @@ class SettingsModel : public Wt::WFormModel setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeFormatField, createMandatoryValidator()); setValidator(ReplayGainModeField, createMandatoryValidator()); + + auto createPreAmpValidator = [] { auto preampGainValidator {std::make_unique()}; preampGainValidator->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - setValidator(ReplayGainPreAmpGainField, std::move(preampGainValidator)); - } + return preampGainValidator; + }; + + setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); + setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); @@ -147,6 +154,7 @@ class SettingsModel : public Wt::WFormModel settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow); settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField)); + settings.replayGain.preAmpGainIfNoInfo = Wt::asNumber(value(ReplayGainPreAmpGainIfNoInfoField)); LmsApp->getMediaPlayer()->setSettings(settings); } @@ -202,6 +210,7 @@ class SettingsModel : public Wt::WFormModel setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow)); setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain); + setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); } setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable()); @@ -434,15 +443,24 @@ SettingsView::refreshView() replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain)); + // Replay gain preampGain if no info + auto replayGainPreampGainIfNoInfo {std::make_unique()}; + replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo)); + replayGainModeRaw->activated().connect([=](int row) { const bool enable {model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None}; model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable); + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !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); + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, true); + } } // Subsonic From 907460ff470c009fff058bc67cb6fbe232b8aa33 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Apr 2020 15:23:45 +0200 Subject: [PATCH 12/13] Homogeneize settings stuff --- approot/admin-database.xml | 134 ++++++++++++++++------------------- approot/admin-initwizard.xml | 81 ++++++++++----------- approot/admin-user.xml | 105 +++++++++++++-------------- approot/settings.xml | 16 ++--- 4 files changed, 158 insertions(+), 178 deletions(-) diff --git a/approot/admin-database.xml b/approot/admin-database.xml index 6e2b08ec..e2d504a1 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -4,81 +4,69 @@ - ${tr:Lms.Admin.Database.database} -
- -
- -
- ${media-directory} +
+
+ ${tr:Lms.Admin.Database.database} +
+
+ +
+ ${media-directory} + ${media-directory-info class="help-block"} +
+
+
+ +
+ ${update-period} + ${update-period-info class="help-block"} +
+
+
+ +
+ ${update-start-time} + ${update-start-time-info class="help-block"} +
+
-
- ${media-directory-info} + ${tr:Lms.Admin.Database.scan-options} +
+
+ +
+ ${recommendation-engine-type} + ${recommendation-engine-type-info class="help-block"} +
+
+
+ +
+ ${tags} + ${tags-info class="help-block"} +
+
-
- -
- -
- ${update-period} -
-
- ${update-period-info} -
-
- -
- -
- ${update-start-time} -
-
- ${update-start-time-info} -
-
-
- ${tr:Lms.Admin.Database.scan-options} -
- -
- -
- ${recommendation-engine-type} -
-
- ${recommendation-engine-type-info} -
-
- -
- -
- ${tags} -
-
- ${tags-info} -
-
- -
-
- ${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"} +
+
+ ${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"} +

-
+
${tr:Lms.Admin.Database.Status.status}
@@ -92,27 +80,27 @@
-