WIP replaygain support

This commit is contained in:
emeric
2020-04-25 14:39:49 +02:00
parent 4749fd7548
commit f41b6c38a7
22 changed files with 480 additions and 216 deletions
+27 -34
View File
@@ -22,10 +22,10 @@ LMS.mediaplayer = function () {
var _offset = 0; var _offset = 0;
var _duration = 0; var _duration = 0;
var _audioNativeSrc; var _audioNativeSrc;
var _audioTranscodedSrc; var _audioTranscodeSrc;
var _transcodeMode = TranscodeMode.Never; var _settings = {};
var _transcodeFormat = 0; var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var _transcodeBitrate = 0; var _gainNode = audioCtx.createGain();
var _updateControls = function() { var _updateControls = function() {
if (_elems.audio.paused) { if (_elems.audio.paused) {
@@ -81,27 +81,15 @@ LMS.mediaplayer = function () {
_setVolume(_elems.volumeslider.value); _setVolume(_elems.volumeslider.value);
} }
var _initTranscodeSettings = function(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { var _initDefaultSettings = function(defaultSettings) {
if (typeof(Storage) !== "undefined" && localStorage.transcodeMode) { if (typeof(Storage) !== "undefined" && localStorage.settings) {
_transcodeMode = Number(localStorage.transcodeMode); _settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
} }
else { else {
_transcodeMode = defaultTranscodeMode; _settings = defaultSettings;
}
if (typeof(Storage) !== "undefined" && localStorage.transcodeFormat) {
_transcodeFormat = Number(localStorage.transcodeFormat);
}
else {
_transcodeFormat = defaultTranscodeFormat;
}
if (typeof(Storage) !== "undefined" && localStorage.transcodeBitrate) {
_transcodeBitrate = Number(localStorage.transcodeBitrate);
}
else {
_transcodeBitrate = defaultTranscodeBitrate;
} }
Wt.emit(_root, "settingsLoaded", _transcodeMode, _transcodeFormat, _transcodeBitrate); Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
} }
var _setVolume = function(volume) { var _setVolume = function(volume) {
@@ -132,7 +120,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; _root = root;
_elems.audio = document.getElementById("lms-mp-audio"); _elems.audio = document.getElementById("lms-mp-audio");
@@ -146,6 +138,10 @@ LMS.mediaplayer = function () {
_elems.volume = document.getElementById("lms-mp-volume"); _elems.volume = document.getElementById("lms-mp-volume");
_elems.volumeslider = document.getElementById("lms-mp-volume-slider"); _elems.volumeslider = document.getElementById("lms-mp-volume-slider");
var source = audioCtx.createMediaElementSource(_elems.audio);
source.connect(_gainNode);
_gainNode.connect(audioCtx.destination);
_elems.playpause.addEventListener("click", function() { _elems.playpause.addEventListener("click", function() {
if (_elems.audio.paused) { if (_elems.audio.paused) {
if (_elems.audio.firstChild) if (_elems.audio.firstChild)
@@ -199,7 +195,7 @@ LMS.mediaplayer = function () {
}); });
_initVolume(); _initVolume();
_initTranscodeSettings(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate); _initDefaultSettings(defaultSettings);
_elems.volumeslider.addEventListener("input", function() { _elems.volumeslider.addEventListener("input", function() {
_setVolume(_elems.volumeslider.value); _setVolume(_elems.volumeslider.value);
@@ -251,23 +247,24 @@ LMS.mediaplayer = function () {
var loadTrack = function(params, autoplay) { var loadTrack = function(params, autoplay) {
_offset = 0; _offset = 0;
_duration = params.duration; _duration = params.duration;
_audioNativeSrc = params.native_resource; _audioNativeSrc = params.nativeResource;
_audioTranscodeSrc = params.transcode_resource + "&bitrate=" + _transcodeBitrate + "&format=" + _transcodeFormat; _audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format;
_elems.seek.max = _duration; _elems.seek.max = _duration;
_removeAudioSources(); _removeAudioSources();
// ! order is important // ! order is important
if (_transcodeMode == TranscodeMode.Never || _transcodeMode == TranscodeMode.IfFormatNotSupported) if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
{ {
_addAudioSource(_audioNativeSrc); _addAudioSource(_audioNativeSrc);
} }
if (_transcodeMode == TranscodeMode.Always || _transcodeMode == TranscodeMode.IfFormatNotSupported) if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
{ {
_addAudioSource(_audioTranscodeSrc); _addAudioSource(_audioTranscodeSrc);
} }
_elems.audio.load(); _elems.audio.load();
_setReplayGain(params.replayGain);
_elems.curtime.innerHTML = _durationToString(_offset); _elems.curtime.innerHTML = _durationToString(_offset);
_elems.duration.innerHTML = _durationToString(_duration); _elems.duration.innerHTML = _durationToString(_duration);
@@ -289,15 +286,11 @@ LMS.mediaplayer = function () {
_elems.audio.pause(); _elems.audio.pause();
} }
var setSettings = function(transcodeMode, transcodeFormat, transcodeBitrate) { var setSettings = function(settings) {
_transcodeMode = transcodeMode; _settings = settings;
_transcodeFormat = transcodeFormat;
_transcodeBitrate = transcodeBitrate;
if (typeof(Storage) !== "undefined") { if (typeof(Storage) !== "undefined") {
localStorage.transcodeMode = _transcodeMode; localStorage.settings = JSON.stringify(_settings);
localStorage.transcodeFormat = _transcodeFormat;
localStorage.transcodeBitrate = _transcodeBitrate;
} }
} }
+9 -1
View File
@@ -40,7 +40,7 @@
namespace Database { namespace Database {
#define LMS_DATABASE_VERSION 21 #define LMS_DATABASE_VERSION 22
using Version = std::size_t; using Version = std::size_t;
@@ -246,6 +246,14 @@ CREATE TABLE "user_backup" (
{ {
_session.execute("DROP TABLE subsonic_settings"); _session.execute("DROP TABLE subsonic_settings");
} }
else if (version == 21)
{
_session.execute("ALTER TABLE track ADD track_replay_gain REAL");
_session.execute("ALTER TABLE track ADD release_replay_gain REAL");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else else
{ {
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
+25 -4
View File
@@ -38,6 +38,14 @@ _filePath( p.string() )
{ {
} }
std::size_t
Track::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
}
std::vector<Track::pointer> std::vector<Track::pointer>
Track::getAll(Session& session, std::optional<std::size_t> limit) Track::getAll(Session& session, std::optional<std::size_t> limit)
{ {
@@ -107,13 +115,26 @@ Track::create(Session& session, const std::filesystem::path& p)
return res; return res;
} }
std::vector<std::filesystem::path> std::vector<std::pair<IdType, std::filesystem::path>>
Track::getAllPaths(Session& session) Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
{ {
using QueryResultType = std::tuple<IdType, std::string>;
session.checkSharedLocked(); session.checkSharedLocked();
Wt::Dbo::collection<std::string> res = session.getDboSession().query<std::string>("SELECT file_path FROM track"); Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
return std::vector<std::filesystem::path>(res.begin(), res.end()); .limit(size ? static_cast<int>(*size) + 1 : -1)
.offset(offset ? static_cast<int>(*offset) : -1);
std::vector<std::pair<IdType, std::filesystem::path>> result;
result.reserve(queryRes.size());
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
[](const QueryResultType& queryResult)
{
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
});
return result;
} }
std::vector<Track::pointer> std::vector<Track::pointer>
+11 -1
View File
@@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo<Track>
Track(const std::filesystem::path& p); Track(const std::filesystem::path& p);
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session);
static pointer getByPath(Session& session, const std::filesystem::path& p); static pointer getByPath(Session& session, const std::filesystem::path& p);
static pointer getById(Session& session, IdType id); static pointer getById(Session& session, IdType id);
static pointer getByMBID(Session& session, const UUID& MBID); static pointer getByMBID(Session& session, const UUID& MBID);
@@ -74,7 +75,7 @@ class Track : public Wt::Dbo::Dbo<Track>
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {}); static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {}); static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
static std::vector<IdType> getAllIds(Session& session); static std::vector<IdType> getAllIds(Session& session);
static std::vector<std::filesystem::path> getAllPaths(Session& session); static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
static std::vector<pointer> getMBIDDuplicates(Session& session); static std::vector<pointer> getMBIDDuplicates(Session& session);
static std::vector<pointer> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1); static std::vector<pointer> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session); static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
@@ -100,6 +101,8 @@ class Track : public Wt::Dbo::Dbo<Track>
void setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; } void setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; }
void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; }
void clearArtistLinks(); void clearArtistLinks();
void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink); void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink);
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; } void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
@@ -122,6 +125,9 @@ class Track : public Wt::Dbo::Dbo<Track>
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); } std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const; std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
@@ -152,6 +158,8 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url"); Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
@@ -184,6 +192,8 @@ class Track : public Wt::Dbo::Dbo<Track>
std::string _MBID; // Musicbrainz Identifier std::string _MBID; // Musicbrainz Identifier
std::string _copyright; std::string _copyright;
std::string _copyrightURL; std::string _copyrightURL;
std::optional<float> _trackReplayGain;
std::optional<float> _releaseReplayGain;
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
+37 -3
View File
@@ -19,13 +19,16 @@
#include "metadata/TagLibParser.hpp" #include "metadata/TagLibParser.hpp"
#include <taglib/apetag.h>
#include <taglib/asffile.h> #include <taglib/asffile.h>
#include <taglib/id3v2tag.h> #include <taglib/id3v2tag.h>
#include <taglib/fileref.h> #include <taglib/fileref.h>
#include <taglib/flacfile.h> #include <taglib/flacfile.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h> #include <taglib/mpegfile.h>
#include <taglib/tag.h> #include <taglib/tag.h>
#include <taglib/tpropertymap.h> #include <taglib/tpropertymap.h>
#include <taglib/wavpackfile.h>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
@@ -263,6 +266,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
track.copyright = value; track.copyright = value;
else if (tag == "COPYRIGHTURL") else if (tag == "COPYRIGHTURL")
track.copyrightURL = value; track.copyrightURL = value;
else if (tag == "REPLAYGAIN_ALBUM_GAIN")
track.albumReplayGain = StringUtils::readAs<float>(value);
else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.trackReplayGain = StringUtils::readAs<float>(value);
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{ {
std::set<std::string> clusterNames; std::set<std::string> clusterNames;
@@ -311,6 +318,21 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
TagLib::PropertyMap properties {f.file()->properties()}; TagLib::PropertyMap properties {f.file()->properties()};
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
{
if (!apeTag)
return;
for (const auto& [name, values] : apeTag->properties())
{
if (debug)
std::cout << "APE property: '" << name << "'" << std::endl;
if (!properties.contains(name))
properties.insert(name, values);
}
};
// Not that good embedded pictures handling // Not that good embedded pictures handling
// WMA // WMA
@@ -337,9 +359,10 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
if (!stringAttributeList.isEmpty()) if (!stringAttributeList.isEmpty())
{ {
if (debug) if (debug)
std::cout << "Property: '" << name << "'" << std::endl; std::cout << "ASF property: '" << name << "'" << std::endl;
properties.insert(name, stringAttributeList); if (!properties.contains(name))
properties.insert(name, stringAttributeList);
} }
} }
} }
@@ -352,6 +375,17 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty()) if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
track.hasCover = true; track.hasCover = true;
} }
getAPETags(mp3File->APETag());
}
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())})
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File* wavPackFile {dynamic_cast<TagLib::WavPack::File*>(f.file())})
{
getAPETags(wavPackFile->APETag());
} }
// FLAC // FLAC
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())}) else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())})
@@ -360,7 +394,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.hasCover = true; track.hasCover = true;
} }
for(const auto& property : properties) for (const auto& property : properties)
{ {
const std::string tag {property.first.upper().to8Bit(true)}; const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second}; const TagLib::StringList& values {property.second};
@@ -75,6 +75,8 @@ namespace MetaData
std::optional<UUID> acoustID; std::optional<UUID> acoustID;
std::string copyright; std::string copyright;
std::string copyrightURL; std::string copyrightURL;
std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain;
}; };
class IParser class IParser
+44 -12
View File
@@ -111,7 +111,6 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti
// Sortname may have been updated // Sortname may have been updated
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() ) if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() )
{ {
LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'";
artist.modify()->setSortName(*artistInfo.sortName); artist.modify()->setSortName(*artistInfo.sortName);
} }
} }
@@ -751,6 +750,10 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
track.modify()->setHasCover(trackInfo->hasCover); track.modify()->setHasCover(trackInfo->hasCover);
track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyright(trackInfo->copyright);
track.modify()->setCopyrightURL(trackInfo->copyrightURL); track.modify()->setCopyrightURL(trackInfo->copyrightURL);
if (trackInfo->trackReplayGain)
track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain);
if (trackInfo->albumReplayGain)
track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain);
} }
void void
@@ -819,32 +822,61 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
void void
MediaScanner::removeMissingTracks(ScanStats& stats) MediaScanner::removeMissingTracks(ScanStats& stats)
{ {
std::vector<std::filesystem::path> trackPaths; static constexpr std::size_t batchSize {50};
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
std::size_t trackCount {};
{ {
auto transaction {_dbSession.createSharedTransaction()}; auto transaction {_dbSession.createSharedTransaction()};
trackPaths = Track::getAllPaths(_dbSession);; trackCount = Track::getCount(_dbSession);
} }
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks..."; std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
for (const auto& trackPath : trackPaths) std::vector<IdType> tracksToRemove;
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
{ {
if (!_running) trackPaths.clear();
return; tracksToRemove.clear();
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) {
auto transaction {_dbSession.createSharedTransaction()};
trackPaths = Track::getAllPaths(_dbSession, i, batchSize);
}
for (const auto& [trackId, trackPath] : trackPaths)
{
if (!_running)
return;
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
tracksToRemove.push_back(trackId);
}
if (!tracksToRemove.empty())
{ {
auto transaction {_dbSession.createUniqueTransaction()}; auto transaction {_dbSession.createUniqueTransaction()};
Track::pointer track {Track::getByPath(_dbSession, trackPath)}; for (const IdType trackId : tracksToRemove)
if (track)
{ {
track.remove(); Track::pointer track {Track::getById(_dbSession, trackId)};
stats.deletions++; if (track)
{
track.remove();
stats.deletions++;
}
} }
} }
notifyInProgressIfNeeded(stats); notifyInProgressIfNeeded(stats);
if (i == 0)
break;
} }
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
} }
void void
+4 -6
View File
@@ -544,16 +544,14 @@ LmsApplication::createHome()
playqueue->playNext(); playqueue->playNext();
}); });
playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play) playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play, float replayGain)
{ {
_events.lastLoadedTrackId = trackId; _mediaPlayer->loadTrack(trackId, play, replayGain);
_events.trackLoaded(trackId, play);
}); });
playqueue->trackUnselected.connect([=] playqueue->trackUnselected.connect([=] ()
{ {
_events.lastLoadedTrackId.reset(); _mediaPlayer->stop();
_events.trackUnloaded();
}); });
// Events from MediaScanner // Events from MediaScanner
-7
View File
@@ -53,13 +53,6 @@ struct Events
Wt::Signal<LmsApplicationInfo> appOpen; Wt::Signal<LmsApplicationInfo> appOpen;
Wt::Signal<LmsApplicationInfo> appClosed; Wt::Signal<LmsApplicationInfo> appClosed;
// A track is being loaded
Wt::Signal<Database::IdType /* trackId */, bool /* play */> trackLoaded;
std::optional<Database::IdType> lastLoadedTrackId;
Wt::Signal<> mediaPlayerSettingsAvailable;
// Unload current track
Wt::Signal<> trackUnloaded;
// Database events // Database events
Wt::Signal<> dbScanned; Wt::Signal<> dbScanned;
Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress; Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress;
+176 -94
View File
@@ -19,6 +19,10 @@
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
@@ -36,6 +40,108 @@
namespace UserInterface { namespace UserInterface {
static std::string settingsToJSString(const MediaPlayer::Settings& settings)
{
namespace Json = Wt::Json;
Json::Object res;
{
Json::Object transcode;
transcode["mode"] = static_cast<int>(settings.transcode.mode);
transcode["format"] = static_cast<int>(settings.transcode.format);
transcode["bitrate"] = static_cast<int>(settings.transcode.bitrate);
res["transcode"] = std::move(transcode);
}
{
Json::Object replayGain;
replayGain["mode"] = "TODO";
replayGain["preAmpGain"] = 0;
replayGain["clippingPreventionMode"] = "TODO";
res["replayGain"] = std::move(replayGain);
}
return Json::serialize(res);
}
static
std::optional<MediaPlayer::Settings::Transcode::Mode>
modeFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
MediaPlayer::Settings::Transcode::Mode mode {static_cast<MediaPlayer::Settings::Transcode::Mode>(*value)};
switch (mode)
{
case MediaPlayer::Settings::Transcode::Mode::Never:
case MediaPlayer::Settings::Transcode::Mode::Always:
case MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported:
return mode;
}
return std::nullopt;
}
static
std::optional<MediaPlayer::Format>
formatFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
MediaPlayer::Format format {static_cast<MediaPlayer::Format>(*value)};
switch (format)
{
case MediaPlayer::Format::MP3:
case MediaPlayer::Format::OGG_OPUS:
case MediaPlayer::Format::MATROSKA_OPUS:
case MediaPlayer::Format::OGG_VORBIS:
case MediaPlayer::Format::WEBM_VORBIS:
return format;
}
return std::nullopt;
}
static
std::optional<MediaPlayer::Bitrate>
bitrateFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates))
return *value;
return std::nullopt;
}
static 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() MediaPlayer::MediaPlayer()
: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")},
@@ -48,119 +154,99 @@ MediaPlayer::MediaPlayer()
_artist = bindNew<Wt::WAnchor>("artist"); _artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release"); _release = bindNew<Wt::WAnchor>("release");
_settingsLoaded.connect([this](int mode, int format, int bitrate) _settingsLoaded.connect([this](const std::string& settings)
{ {
LMS_LOG(UI, DEBUG) << "Settings loaded! mode = " << mode << ", format = " << format << ", bitrate = " << bitrate; LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
Settings settings; _settings = settingsfromJSString(settings);
switch (static_cast<TranscodeMode>(mode)) settingsLoaded.emit();
{
case TranscodeMode::Always:
case TranscodeMode::Never:
case TranscodeMode::IfFormatNotSupported:
settings.mode = static_cast<TranscodeMode>(mode);
break;
}
switch (static_cast<Format>(format))
{
case Format::MP3:
case Format::OGG_OPUS:
case Format::MATROSKA_OPUS:
case Format::OGG_VORBIS:
case Format::WEBM_VORBIS:
settings.format = static_cast<Format>(format);
break;
}
if (Database::User::audioTranscodeAllowedBitrates.find(bitrate) != std::cend(Database::User::audioTranscodeAllowedBitrates))
settings.bitrate = bitrate;
_settings = settings;
LmsApp->getEvents().mediaPlayerSettingsAvailable.emit();
}); });
{ {
Settings defaultSettings;
std::ostringstream oss; std::ostringstream oss;
oss << "LMS.mediaplayer.init(" oss << "LMS.mediaplayer.init("
<< jsRef() << jsRef()
<< ", " << static_cast<int>(defaultTranscodeMode) << ", defaultSettings = " << settingsToJSString(defaultSettings)
<< ", " << static_cast<int>(defaultTranscodeFormat)
<< ", " << static_cast<int>(defaultTranscodeBitrate)
<< ")"; << ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str()); doJavaScript(oss.str());
} }
LmsApp->getEvents().trackLoaded.connect(this, &MediaPlayer::loadTrack);
LmsApp->getEvents().trackUnloaded.connect(this, &MediaPlayer::stop);
} }
void void
MediaPlayer::loadTrack(Database::IdType trackId, bool play) MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
{ {
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId; LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId;
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
const auto artists {track->getArtists()};
std::ostringstream oss; std::ostringstream oss;
oss {
<< "var params = {" auto transaction {LmsApp->getDbSession().createSharedTransaction()};
<< " native_resource: \"" << nativeResource << "\","
<< " transcode_resource: \"" << transcodeResource << "\"," const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "," if (!track)
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," return;
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" }," const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" }," const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]" const auto artists {track->getArtists()};
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay oss
<< "var params = {"
<< " nativeResource: \"" << nativeResource << "\","
<< " transcodeResource: \"" << transcodeResource << "\","
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
<< " replayGain: " << replayGain << ","
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]"
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
}
if (track->getRelease())
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink({});
}
}
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
}
if (track->getRelease())
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink({});
}
wApp->doJavaScript(oss.str()); wApp->doJavaScript(oss.str());
_trackIdLoaded = trackId;
trackLoaded.emit(*_trackIdLoaded);
} }
void void
@@ -176,11 +262,7 @@ MediaPlayer::setSettings(const Settings& settings)
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "LMS.mediaplayer.setSettings(" oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
<< static_cast<int>(_settings->mode)
<< ", " << static_cast<int>(_settings->format)
<< ", " << static_cast<int>(_settings->bitrate)
<< ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str()); doJavaScript(oss.str());
+58 -19
View File
@@ -34,24 +34,57 @@ class MediaPlayer : public Wt::WTemplate
public: public:
using Bitrate = Database::Bitrate; using Bitrate = Database::Bitrate;
using Format = Database::AudioFormat; using Format = Database::AudioFormat;
using Gain = float;
// Do not change this enum as it may be stored locally in browser // Do not change enum values as they may be stored locally in browser
// Keep it sync with LMS.mediaplayer js // Keep it sync with LMS.mediaplayer js
enum class TranscodeMode
{
Never = 0,
Always = 1,
IfFormatNotSupported = 2,
};
static inline constexpr TranscodeMode defaultTranscodeMode {TranscodeMode::IfFormatNotSupported};
static inline constexpr Format defaultTranscodeFormat {Format::OGG_OPUS};
static inline constexpr Bitrate defaultTranscodeBitrate {128000};
struct Settings struct Settings
{ {
TranscodeMode mode {defaultTranscodeMode}; struct Transcode
Format format {defaultTranscodeFormat}; {
Bitrate bitrate {defaultTranscodeBitrate}; enum class Mode
{
Never = 0,
Always = 1,
IfFormatNotSupported = 2,
};
static inline constexpr Mode defaultMode {Mode::IfFormatNotSupported};
static inline constexpr Format defaultFormat {Format::OGG_OPUS};
static inline constexpr Bitrate defaultBitrate {128000};
Mode mode {defaultMode};
Format format {defaultFormat};
Bitrate bitrate {defaultBitrate};
};
struct ReplayGain
{
enum class Mode
{
None = 0,
Auto = 1,
Track = 2,
Release = 3,
};
enum class ClippingPreventionMode
{
None = 0,
LowerVolume = 1,
};
static inline constexpr Mode defaultMode {Mode::None};
static inline constexpr Gain defaultPreAmpGain {};
static inline constexpr ClippingPreventionMode defaultClippingPreventionMode {ClippingPreventionMode::None};
Mode mode;
Gain preAmpGain;
ClippingPreventionMode clippingPreventionMode;
};
Transcode transcode;
ReplayGain replayGain;
}; };
MediaPlayer(); MediaPlayer();
@@ -61,21 +94,27 @@ class MediaPlayer : public Wt::WTemplate
MediaPlayer& operator=(const MediaPlayer&) = delete; MediaPlayer& operator=(const MediaPlayer&) = delete;
MediaPlayer& operator=(MediaPlayer&&) = delete; MediaPlayer& operator=(MediaPlayer&&) = delete;
std::optional<Database::IdType> getTrackLoaded() const { return _trackIdLoaded; }
void loadTrack(Database::IdType trackId, bool play, float replayGain);
void stop();
std::optional<Settings> getSettings() const { return _settings; } std::optional<Settings> getSettings() const { return _settings; }
void setSettings(const Settings& settings); void setSettings(const Settings& settings);
// Signals // Signals
Wt::JSignal<> playbackEnded; Wt::JSignal<> playbackEnded;
Wt::JSignal<> playPrevious; Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext; Wt::JSignal<> playNext;
Wt::Signal<Database::IdType> trackLoaded;
Wt::Signal<> settingsLoaded;
private: private:
void stop();
void loadTrack(Database::IdType trackId, bool play);
std::optional<Database::IdType> _trackIdLoaded;
std::optional<Settings> _settings; std::optional<Settings> _settings;
Wt::JSignal<int, int, int> _settingsLoaded; Wt::JSignal<std::string> _settingsLoaded;
Wt::WText* _title; Wt::WText* _title;
Wt::WAnchor* _release; Wt::WAnchor* _release;
Wt::WAnchor* _artist; Wt::WAnchor* _artist;
+2 -1
View File
@@ -30,6 +30,7 @@
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
namespace { namespace {
@@ -99,7 +100,7 @@ PlayHistory::PlayHistory()
addSome(); addSome();
}); });
LmsApp->getEvents().trackLoaded.connect([=](Database::IdType trackId, bool /* play */) LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId)
{ {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
+31 -2
View File
@@ -33,8 +33,9 @@
#include "utils/String.hpp" #include "utils/String.hpp"
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "TrackStringUtils.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp"
namespace UserInterface { namespace UserInterface {
@@ -209,6 +210,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
Database::IdType trackId {}; Database::IdType trackId {};
bool addRadioTrack {}; bool addRadioTrack {};
std::optional<float> replayGain {};
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
@@ -235,6 +237,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
trackId = track.id(); trackId = track.id();
replayGain = getReplayGain(track);
if (!LmsApp->getUser()->isDemo()) if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setCurPlayingTrackPos(pos); LmsApp->getUser().modify()->setCurPlayingTrackPos(pos);
} }
@@ -244,7 +248,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
updateCurrentTrack(true); updateCurrentTrack(true);
trackSelected.emit(trackId, play); trackSelected.emit(trackId, play, replayGain ? *replayGain : 0);
} }
void void
@@ -432,5 +436,30 @@ PlayQueue::enqueueRadioTrack()
enqueueTracks(trackToAddIds); enqueueTracks(trackToAddIds);
} }
std::optional<float>
PlayQueue::getReplayGain(const Database::Track::pointer& track) const
{
const auto& settings {LmsApp->getMediaPlayer()->getSettings()};
if (!settings)
return std::nullopt;
switch (settings->replayGain.mode)
{
case MediaPlayer::Settings::ReplayGain::Mode::None:
return std::nullopt;
case MediaPlayer::Settings::ReplayGain::Mode::Track:
return track->getTrackReplayGain();
case MediaPlayer::Settings::ReplayGain::Mode::Release:
return track->getReleaseReplayGain();
case MediaPlayer::Settings::ReplayGain::Mode::Auto:
return track->getTrackReplayGain();
}
return std::nullopt;
}
} // namespace UserInterface } // namespace UserInterface
+3 -1
View File
@@ -33,6 +33,7 @@ namespace Similarity {
} }
namespace Database { namespace Database {
class Track;
class TrackList; class TrackList;
} }
@@ -53,7 +54,7 @@ class PlayQueue : public Wt::WTemplate
void playPrevious(); void playPrevious();
// Signal emitted when a track is to be load(and optionally played) // Signal emitted when a track is to be load(and optionally played)
Wt::Signal<Database::IdType /*trackId*/, bool /*play*/> trackSelected; Wt::Signal<Database::IdType /*trackId*/, bool /*play*/, float /* replayGain */> trackSelected;
// Signal emitted when track is unselected (has to be stopped) // Signal emitted when track is unselected (has to be stopped)
Wt::Signal<> trackUnselected; Wt::Signal<> trackUnselected;
@@ -76,6 +77,7 @@ class PlayQueue : public Wt::WTemplate
void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder); void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder);
void addRadioTrackFromClusters(); void addRadioTrackFromClusters();
std::optional<float> getReplayGain(const Wt::Dbo::ptr<Database::Track>& track) const;
bool _repeatAll {}; bool _repeatAll {};
bool _radioMode {}; bool _radioMode {};
+14 -14
View File
@@ -59,7 +59,7 @@ class SettingsModel : public Wt::WFormModel
static inline const Field PasswordField {"password"}; static inline const Field PasswordField {"password"};
static inline const Field PasswordConfirmField {"password-confirm"}; static inline const Field PasswordConfirmField {"password-confirm"};
using TranscodeModeModel = ValueStringModel<MediaPlayer::TranscodeMode>; using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>;
SettingsModel(bool withOldPassword) SettingsModel(bool withOldPassword)
: _withOldPassword {withOldPassword} : _withOldPassword {withOldPassword}
@@ -118,15 +118,15 @@ class SettingsModel : public Wt::WFormModel
auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))}; auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))};
if (transcodeModeRow) if (transcodeModeRow)
settings.mode = _transcodeModeModel->getValue(*transcodeModeRow); settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow);
auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))};
if (transcodeFormatRow) if (transcodeFormatRow)
settings.format = _transcodeFormatModel->getValue(*transcodeFormatRow); settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow);
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
if (transcodeBitrateRow) if (transcodeBitrateRow)
settings.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow);
LmsApp->getMediaPlayer()->setSettings(settings); LmsApp->getMediaPlayer()->setSettings(settings);
} }
@@ -165,15 +165,15 @@ class SettingsModel : public Wt::WFormModel
{ {
const auto& settings {*LmsApp->getMediaPlayer()->getSettings()}; const auto& settings {*LmsApp->getMediaPlayer()->getSettings()};
auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.mode)}; auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.transcode.mode)};
if (transcodeModeRow) if (transcodeModeRow)
setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow));
auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.format)}; auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.transcode.format)};
if (transcodeFormatRow) if (transcodeFormatRow)
setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow));
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.bitrate)}; auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate)};
if (transcodeBitrateRow) if (transcodeBitrateRow)
setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow));
} }
@@ -271,9 +271,9 @@ class SettingsModel : public Wt::WFormModel
{ {
_transcodeModeModel = std::make_shared<TranscodeModeModel>(); _transcodeModeModel = std::make_shared<TranscodeModeModel>();
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::TranscodeMode::Always); _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::TranscodeMode::Never); _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::TranscodeMode::IfFormatNotSupported); _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported);
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>(); _subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>();
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists);
@@ -303,12 +303,12 @@ class SettingsModel : public Wt::WFormModel
SettingsView::SettingsView() SettingsView::SettingsView()
{ {
wApp->internalPathChanged().connect(std::bind([=] wApp->internalPathChanged().connect([=]
{ {
refreshView(); refreshView();
})); });
LmsApp->getEvents().mediaPlayerSettingsAvailable.connect([=]() LmsApp->getMediaPlayer()->settingsLoaded.connect([=]()
{ {
refreshView(); refreshView();
}); });
@@ -379,7 +379,7 @@ SettingsView::refreshView()
transcodeModeRaw->sactivated().connect([=]() transcodeModeRaw->sactivated().connect([=]()
{ {
auto row {model->getTranscodeModeModel()->getRowFromString(model->valueText(SettingsModel::TranscodeModeField))}; 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::TranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
t->updateModel(model.get()); t->updateModel(model.get());
+2 -1
View File
@@ -26,6 +26,7 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "ArtistLink.hpp" #include "ArtistLink.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database; using namespace Database;
@@ -44,7 +45,7 @@ ArtistsInfo::ArtistsInfo()
refreshRecentlyAdded(); refreshRecentlyAdded();
}); });
LmsApp->getEvents().trackLoaded.connect([=] LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{ {
refreshMostPlayed(); refreshMostPlayed();
}); });
+11 -9
View File
@@ -37,6 +37,7 @@
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "LmsApplicationException.hpp" #include "LmsApplicationException.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -46,10 +47,10 @@ namespace UserInterface {
Release::Release(Filters* filters) Release::Release(Filters* filters)
: _filters(filters) : _filters(filters)
{ {
wApp->internalPathChanged().connect(std::bind([=] wApp->internalPathChanged().connect([=]()
{ {
refresh(); refresh();
})); });
refresh(); refresh();
@@ -217,27 +218,28 @@ Release::refresh()
} }
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)}; Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
playBtn->clicked().connect(std::bind([=] playBtn->clicked().connect([=]()
{ {
tracksPlay.emit({trackId}); tracksPlay.emit({trackId});
})); });
Wt::WText* addBtn {entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; Wt::WText* addBtn {entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
addBtn->clicked().connect(std::bind([=] addBtn->clicked().connect([=]()
{ {
tracksAdd.emit({trackId}); tracksAdd.emit({trackId});
})); });
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/) LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
{ {
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
}); });
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId) if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
{ {
entry->bindString("is-playing", "Lms-entry-playing"); if (*trackIdLoaded == trackId)
entry->bindString("is-playing", "Lms-entry-playing");
} }
} }
} }
+2 -1
View File
@@ -27,6 +27,7 @@
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "ReleaseLink.hpp" #include "ReleaseLink.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database; using namespace Database;
@@ -45,7 +46,7 @@ ReleasesInfo::ReleasesInfo()
refreshRecentlyAdded(); refreshRecentlyAdded();
}); });
LmsApp->getEvents().trackLoaded.connect([=] LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{ {
refreshMostPlayed(); refreshMostPlayed();
}); });
+3 -2
View File
@@ -27,6 +27,7 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database; using namespace Database;
@@ -49,7 +50,7 @@ void addEntries(Wt::WContainerWidget *container, const std::vector<Track::pointe
namespace UserInterface { namespace UserInterface {
TracksInfo::TracksInfo() TracksInfo::TracksInfo()
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.TracksInfo.template")) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.TracksInfo.template")}
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
@@ -61,7 +62,7 @@ TracksInfo::TracksInfo()
refreshRecentlyAdded(); refreshRecentlyAdded();
}); });
LmsApp->getEvents().trackLoaded.connect([=] LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{ {
refreshMostPlayed(); refreshMostPlayed();
}); });
+5 -3
View File
@@ -34,6 +34,7 @@
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -174,14 +175,15 @@ Tracks::addSome()
})); }));
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/) LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
{ {
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
}); });
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId) if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
{ {
entry->bindString("is-playing", "Lms-entry-playing"); if (*trackIdLoaded == trackId)
entry->bindString("is-playing", "Lms-entry-playing");
} }
} }
+8 -1
View File
@@ -154,12 +154,19 @@ static
void void
testSingleTrack(Session& session) testSingleTrack(Session& session)
{ {
{
auto transaction {session.createSharedTransaction()};
CHECK(Track::getCount(session) == 0);
}
ScopedTrack track {session, "MyTrackFile"}; ScopedTrack track {session, "MyTrackFile"};
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createSharedTransaction()};
CHECK(Track::getAll(session).size() == 1); CHECK(Track::getAll(session).size() == 1);
CHECK(Track::getCount(session) == 1);
} }
} }
+6
View File
@@ -124,6 +124,12 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const auto& audioStream : track->audioStreams) for (const auto& audioStream : track->audioStreams)
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl; std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl;
if (track->trackReplayGain)
std::cout << "Track replay gain: " << *track->trackReplayGain << std::endl;
if (track->albumReplayGain)
std::cout << "Album replay gain: " << *track->albumReplayGain << std::endl;
if (track->acoustID) if (track->acoustID)
std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl; std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl;