Added basic lyrics display in UI

This commit is contained in:
emeric
2024-10-27 23:03:46 +01:00
parent f613d16fc2
commit 41504bf4d7
17 changed files with 227 additions and 19 deletions
+5
View File
@@ -441,6 +441,11 @@ namespace lms::db
_trackLyrics.insert(getDboPtr(lyrics));
}
bool Track::hasLyrics() const
{
return !_trackLyrics.empty();
}
std::optional<std::string> Track::getCopyright() const
{
return _copyright != "" ? std::make_optional<std::string>(_copyright) : std::nullopt;
+31 -6
View File
@@ -34,6 +34,34 @@
namespace lms::db
{
namespace
{
Wt::Dbo::Query<Wt::Dbo::ptr<TrackLyrics>> createQuery(Session& session, const TrackLyrics::FindParameters& params)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackLyrics>>("SELECT t_lrc from track_lyrics t_lrc") };
if (params.track.isValid())
query.where("t_lrc.track_id = ?").bind(params.track);
if (params.external.has_value())
query.where("t_lrc.absolute_file_path " + std::string{ *params.external ? "<>" : "=" } + " ''");
switch (params.sortMethod)
{
case TrackLyricsSortMethod::None:
break;
case TrackLyricsSortMethod::ExternalFirst:
query.orderBy("CASE WHEN absolute_file_path <> '' THEN 0 ELSE 1 END");
break;
case TrackLyricsSortMethod::EmbeddedFirst:
query.orderBy("CASE WHEN absolute_file_path = '' THEN 0 ELSE 1 END");
break;
}
return query;
}
} // namespace
TrackLyrics::pointer TrackLyrics::create(Session& session)
{
return session.getDboSession()->add(std::unique_ptr<TrackLyrics>{ new TrackLyrics{} });
@@ -67,15 +95,12 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<TrackLyrics>>("SELECT t_lrc from track_lyrics t_lrc").where("t_lrc.absolute_file_path = ?").bind(path));
}
void TrackLyrics::find(Session& session, TrackId trackId, const std::function<void(const TrackLyrics::pointer&)>& func)
void TrackLyrics::find(Session& session, const FindParameters& params, const std::function<void(const TrackLyrics::pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackLyrics>>("SELECT t_lrc from track_lyrics t_lrc").where("t_lrc.track_id = ?").bind(trackId) };
utils::forEachQueryResult(query, [&](const TrackLyrics::pointer& lyrics) {
func(lyrics);
});
auto query{ createQuery(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
void TrackLyrics::find(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function<void(const TrackLyrics::pointer&)>& func)
@@ -274,6 +274,7 @@ namespace lms::db
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
bool hasCover() const { return _hasCover; }
bool hasLyrics() const;
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyright() const;
@@ -45,12 +45,41 @@ namespace lms::db
public:
TrackLyrics() = default;
struct FindParameters
{
std::optional<Range> range;
TrackId track;
std::optional<bool> external; // if set, true means external, false means embedded
TrackLyricsSortMethod sortMethod{ TrackLyricsSortMethod::None };
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setExternal(std::optional<bool> _external)
{
external = _external;
return *this;
}
FindParameters& setSortMethod(TrackLyricsSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
};
// Find utilities
static std::size_t getCount(Session& session);
static std::size_t getExternalLyricsCount(Session& session);
static pointer find(Session& session, TrackLyricsId id);
static pointer find(Session& session, const std::filesystem::path& file);
static void find(Session& session, TrackId trackId, const std::function<void(const TrackLyrics::pointer&)>& func);
static void find(Session& session, const FindParameters& params, const std::function<void(const TrackLyrics::pointer&)>& func);
static void find(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function<void(const TrackLyrics::pointer&)>& func);
static RangeResults<TrackLyricsId> findOrphanIds(Session& session, std::optional<Range> range);
@@ -166,6 +166,13 @@ namespace lms::db
TrackList, // order by asc order in tracklist
};
enum class TrackLyricsSortMethod
{
None,
ExternalFirst,
EmbeddedFirst,
};
enum class TrackArtistLinkType
{
Artist = 0, // regular track artist
+41 -6
View File
@@ -157,12 +157,47 @@ namespace lms::db::tests
EXPECT_EQ(TrackLyrics::getCount(session), 1);
EXPECT_EQ(TrackLyrics::getExternalLyricsCount(session), 1);
bool visited{};
TrackLyrics::find(session, track.getId(), [&](const TrackLyrics::pointer& lyrics) {
EXPECT_EQ(lyrics->getAbsoluteFilePath(), "/tmp/test.lrc");
visited = true;
});
EXPECT_TRUE(visited);
{
bool visited{};
TrackLyrics::find(session, TrackLyrics::FindParameters{}.setTrack(track.getId()), [&](const TrackLyrics::pointer& lyrics) {
EXPECT_EQ(lyrics->getAbsoluteFilePath(), "/tmp/test.lrc");
visited = true;
});
EXPECT_TRUE(visited);
}
{
bool visited{};
TrackLyrics::find(session, TrackLyrics::FindParameters{}.setExternal(true), [&](const TrackLyrics::pointer& lyrics) {
EXPECT_EQ(lyrics->getAbsoluteFilePath(), "/tmp/test.lrc");
visited = true;
});
EXPECT_TRUE(visited);
}
{
bool visited{};
TrackLyrics::find(session, TrackLyrics::FindParameters{}.setExternal(false), [&](const TrackLyrics::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
{
bool visited{};
TrackLyrics::find(session, TrackLyrics::FindParameters{}.setSortMethod(TrackLyricsSortMethod::EmbeddedFirst), [&](const TrackLyrics::pointer&) {
visited = true;
});
EXPECT_TRUE(visited);
}
{
bool visited{};
TrackLyrics::find(session, TrackLyrics::FindParameters{}.setSortMethod(TrackLyricsSortMethod::ExternalFirst), [&](const TrackLyrics::pointer&) {
visited = true;
});
EXPECT_TRUE(visited);
}
}
}
} // namespace lms::db::tests
@@ -211,12 +211,13 @@ namespace lms::api::subsonic
if (tracks.results.size() == 1)
{
// Choice: we return only the first lyrics if the track has many lyrics
bool lyricsSet{};
db::TrackLyrics::find(context.dbSession, tracks.results[0], [&](const db::TrackLyrics::pointer& lyrics) {
if (lyricsSet)
return;
db::TrackLyrics::FindParameters params;
params.setTrack(tracks.results[0]);
params.setSortMethod(TrackLyricsSortMethod::ExternalFirst);
params.setRange(db::Range{ 0, 1 });
db::TrackLyrics::find(context.dbSession, params, [&](const db::TrackLyrics::pointer& lyrics) {
response.addNode("lyrics", createLyricsNode(context, lyrics));
lyricsSet = true;
});
}
@@ -236,9 +237,23 @@ namespace lms::api::subsonic
const db::Track::pointer track{ db::Track::find(context.dbSession, id) };
if (track)
{
db::TrackLyrics::find(context.dbSession, track->getId(), [&](const db::TrackLyrics::pointer& lyrics) {
db::TrackLyrics::FindParameters params;
params.setTrack(track->getId());
params.setExternal(true); // First try to only report external lyrics as they are often duplicate of embedded lyrics and support more features
bool hasExternalLyrics{};
db::TrackLyrics::find(context.dbSession, params, [&](const db::TrackLyrics::pointer& lyrics) {
lyricsList.addArrayChild("structuredLyrics", createStructuredLyricsNode(context, lyrics));
hasExternalLyrics = true;
});
if (!hasExternalLyrics)
{
params.setExternal(false);
db::TrackLyrics::find(context.dbSession, params, [&](const db::TrackLyrics::pointer& lyrics) {
lyricsList.addArrayChild("structuredLyrics", createStructuredLyricsNode(context, lyrics));
});
}
}
return response;
+8
View File
@@ -466,6 +466,14 @@ namespace lms::ui
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
->clicked()
.connect([this, trackId] { TrackListHelpers::showTrackInfoModal(trackId, _filters); });
if (track->hasLyrics())
{
entry->setCondition("if-has-lyrics", true);
entry->bindNew<Wt::WPushButton>("track-lyrics", Wt::WString::tr("Lms.Explore.track-lyrics"))
->clicked()
.connect([trackId] { TrackListHelpers::showTrackLyricsModal(trackId); });
}
}
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
+55
View File
@@ -33,6 +33,7 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackLyrics.hpp"
#include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
@@ -155,6 +156,52 @@ namespace lms::ui::TrackListHelpers
LmsApp->getModalManager().show(std::move(trackInfo));
}
void showTrackLyricsModal(db::TrackId trackId)
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
auto trackLyrics{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.track-lyrics")) };
Wt::WWidget* trackLyricsPtr{ trackLyrics.get() };
trackLyrics->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* lyricsContainer{ trackLyrics->bindNew<Wt::WContainerWidget>("lyrics") };
// limitation: display only first lyrics for now
db::TrackLyrics::FindParameters params;
params.setTrack(trackId);
params.setSortMethod(db::TrackLyricsSortMethod::ExternalFirst);
params.setRange(db::Range{ 0, 1 });
db::TrackLyrics::find(LmsApp->getDbSession(), params, [&](const db::TrackLyrics::pointer& lyrics) {
std::string text;
auto addLine = [&](std::string_view line) {
if (!text.empty())
text += '\n';
text += line;
};
if (lyrics->isSynchronized())
{
for (const auto& [timestamp, line] : lyrics->getSynchronizedLines())
addLine(line);
}
else
{
for (const auto& line : lyrics->getUnsynchronizedLines())
addLine(line);
}
lyricsContainer->addNew<Wt::WText>(Wt::WString::fromUTF8(text), Wt::TextFormat::Plain);
});
Wt::WPushButton* okBtn{ trackLyrics->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
okBtn->clicked().connect([=] {
LmsApp->getModalManager().dispose(trackLyricsPtr);
});
LmsApp->getModalManager().show(std::move(trackLyrics));
}
std::unique_ptr<Wt::WWidget> createEntry(const db::ObjectPtr<db::Track>& track, PlayQueueController& playQueueController, Filters& filters)
{
auto entry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry")) };
@@ -240,6 +287,14 @@ namespace lms::ui::TrackListHelpers
->clicked()
.connect([trackId, &filters] { showTrackInfoModal(trackId, filters); });
if (track->hasLyrics())
{
entry->setCondition("if-has-lyrics", true);
entry->bindNew<Wt::WPushButton>("track-lyrics", Wt::WString::tr("Lms.Explore.track-lyrics"))
->clicked()
.connect([trackId] { showTrackLyricsModal(trackId); });
}
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=](db::TrackId loadedTrackId) {
entryPtr->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
});
+1
View File
@@ -40,5 +40,6 @@ namespace lms::ui
namespace lms::ui::TrackListHelpers
{
void showTrackInfoModal(db::TrackId trackId, Filters& filters);
void showTrackLyricsModal(db::TrackId trackId);
std::unique_ptr<Wt::WWidget> createEntry(const db::ObjectPtr<db::Track>& track, PlayQueueController& playQueueController, Filters& filters);
} // namespace lms::ui::TrackListHelpers