Added basic lyrics display in UI
This commit is contained in:
@@ -184,6 +184,7 @@
|
||||
<message id="Lms.Explore.star">Star</message>
|
||||
<message id="Lms.Explore.starred">Starred</message>
|
||||
<message id="Lms.Explore.track-info">Track information</message>
|
||||
<message id="Lms.Explore.track-lyrics">Lyrics</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
<message id="Lms.Explore.tracks">Tracks</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
||||
<message id="Lms.Explore.starred">Favoris</message>
|
||||
<message id="Lms.Explore.track-info">Informations sur la piste</message>
|
||||
<message id="Lms.Explore.track-lyrics">Paroles</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
<message id="Lms.Explore.tracks">Pistes</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
|
||||
<message id="Lms.Explore.starred">Preferiti</message>
|
||||
<message id="Lms.Explore.track-info">Informazioni sulla traccia</message>
|
||||
<message id="Lms.Explore.track-lyrics">Testo</message>
|
||||
<message id="Lms.Explore.tracklists">Playlist</message>
|
||||
<message id="Lms.Explore.tracks">Tracce</message>
|
||||
<message id="Lms.Explore.type">Tipo</message>
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
<message id="Lms.Explore.star">Wyróżnij</message>
|
||||
<message id="Lms.Explore.starred">Wyróżnione</message>
|
||||
<message id="Lms.Explore.track-info">Informacje o ścieżce</message>
|
||||
<message id="Lms.Explore.track-lyrics">Tekst</message>
|
||||
<message id="Lms.Explore.tracklists">Listy odtwarzania</message>
|
||||
<message id="Lms.Explore.tracks">Ścieżki</message>
|
||||
<message id="Lms.Explore.type">Rodzaj</message>
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
<message id="Lms.Explore.star">收藏</message>
|
||||
<message id="Lms.Explore.starred">已收藏</message>
|
||||
|
||||
|
||||
<message id="Lms.Explore.tracklists">播放列表</message>
|
||||
<message id="Lms.Explore.tracks">音轨</message>
|
||||
<message id="Lms.Explore.type">类型</message>
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
<li>${track-info class="dropdown-item"}</li>
|
||||
${<if-has-lyrics>}<li>${track-lyrics class="dropdown-item"}</li>${</if-has-lyrics>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
<li>${track-info class="dropdown-item"}</li>
|
||||
${<if-has-lyrics>}<li>${track-lyrics class="dropdown-item"}</li>${</if-has-lyrics>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,4 +140,24 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.track-lyrics">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Explore.track-lyrics}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${lyrics}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${ok-btn class="btn btn-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user