diff --git a/approot/messages.xml b/approot/messages.xml
index faac59d5..4e69a9a8 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -184,6 +184,7 @@
Star
Starred
Track information
+Lyrics
Playlists
Tracks
Type
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index 154354d0..d9b6f1e1 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -184,6 +184,7 @@
Ajouter aux favoris
Favoris
Informations sur la piste
+Paroles
Playlists
Pistes
Type
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index 19f6fdc9..9c342433 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -184,6 +184,7 @@
Aggiungi ai preferiti
Preferiti
Informazioni sulla traccia
+Testo
Playlist
Tracce
Tipo
diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml
index 4637126d..242a3c87 100644
--- a/approot/messages_pl.xml
+++ b/approot/messages_pl.xml
@@ -201,6 +201,7 @@
Wyróżnij
Wyróżnione
Informacje o ścieżce
+Tekst
Listy odtwarzania
Ścieżki
Rodzaj
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index f497ca20..7b43987e 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -184,6 +184,7 @@
收藏
已收藏
+
播放列表
音轨
类型
diff --git a/approot/release.xml b/approot/release.xml
index 4428ed2a..448868d9 100644
--- a/approot/release.xml
+++ b/approot/release.xml
@@ -102,6 +102,7 @@
${star class="dropdown-item"}
${download class="dropdown-item"}
${track-info class="dropdown-item"}
+ ${}${track-lyrics class="dropdown-item"}${}
diff --git a/approot/tracks.xml b/approot/tracks.xml
index 25a5984c..f8caea0e 100644
--- a/approot/tracks.xml
+++ b/approot/tracks.xml
@@ -60,6 +60,7 @@
${star class="dropdown-item"}
${download class="dropdown-item"}
${track-info class="dropdown-item"}
+ ${}${track-lyrics class="dropdown-item"}${}
@@ -139,4 +140,24 @@
+
+
+
+
+
diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp
index 9371d526..6abcb4d4 100644
--- a/src/libs/database/impl/Track.cpp
+++ b/src/libs/database/impl/Track.cpp
@@ -441,6 +441,11 @@ namespace lms::db
_trackLyrics.insert(getDboPtr(lyrics));
}
+ bool Track::hasLyrics() const
+ {
+ return !_trackLyrics.empty();
+ }
+
std::optional Track::getCopyright() const
{
return _copyright != "" ? std::make_optional(_copyright) : std::nullopt;
diff --git a/src/libs/database/impl/TrackLyrics.cpp b/src/libs/database/impl/TrackLyrics.cpp
index 7d687744..5bc1b495 100644
--- a/src/libs/database/impl/TrackLyrics.cpp
+++ b/src/libs/database/impl/TrackLyrics.cpp
@@ -34,6 +34,34 @@
namespace lms::db
{
+ namespace
+ {
+ Wt::Dbo::Query> createQuery(Session& session, const TrackLyrics::FindParameters& params)
+ {
+ auto query{ session.getDboSession()->query>("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{ new TrackLyrics{} });
@@ -67,15 +95,12 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query>("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& func)
+ void TrackLyrics::find(Session& session, const FindParameters& params, const std::function& func)
{
session.checkReadTransaction();
- auto query{ session.getDboSession()->query>("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& func)
diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp
index 2c8e9451..3e6ee883 100644
--- a/src/libs/database/include/database/Track.hpp
+++ b/src/libs/database/include/database/Track.hpp
@@ -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 getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
std::optional getCopyright() const;
diff --git a/src/libs/database/include/database/TrackLyrics.hpp b/src/libs/database/include/database/TrackLyrics.hpp
index a1043814..fbb49629 100644
--- a/src/libs/database/include/database/TrackLyrics.hpp
+++ b/src/libs/database/include/database/TrackLyrics.hpp
@@ -45,12 +45,41 @@ namespace lms::db
public:
TrackLyrics() = default;
+ struct FindParameters
+ {
+ std::optional range;
+ TrackId track;
+ std::optional external; // if set, true means external, false means embedded
+ TrackLyricsSortMethod sortMethod{ TrackLyricsSortMethod::None };
+
+ FindParameters& setRange(std::optional _range)
+ {
+ range = _range;
+ return *this;
+ }
+ FindParameters& setTrack(TrackId _track)
+ {
+ track = _track;
+ return *this;
+ }
+ FindParameters& setExternal(std::optional _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& func);
+ static void find(Session& session, const FindParameters& params, const std::function& func);
static void find(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function& func);
static RangeResults findOrphanIds(Session& session, std::optional range);
diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp
index a1494e0e..c0376283 100644
--- a/src/libs/database/include/database/Types.hpp
+++ b/src/libs/database/include/database/Types.hpp
@@ -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
diff --git a/src/libs/database/test/TrackLyrics.cpp b/src/libs/database/test/TrackLyrics.cpp
index cf8d7c2f..323c69cc 100644
--- a/src/libs/database/test/TrackLyrics.cpp
+++ b/src/libs/database/test/TrackLyrics.cpp
@@ -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
\ No newline at end of file
diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp
index e428ce52..b3765648 100644
--- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp
+++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp
@@ -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;
diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp
index 0c969d19..ae69b29c 100644
--- a/src/lms/ui/explore/ReleaseView.cpp
+++ b/src/lms/ui/explore/ReleaseView.cpp
@@ -466,6 +466,14 @@ namespace lms::ui
entry->bindNew("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("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);
diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp
index 6ed07c76..f8bb10bb 100644
--- a/src/lms/ui/explore/TrackListHelpers.cpp
+++ b/src/lms/ui/explore/TrackListHelpers.cpp
@@ -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(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("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::WString::fromUTF8(text), Wt::TextFormat::Plain);
+ });
+
+ Wt::WPushButton* okBtn{ trackLyrics->bindNew("ok-btn", Wt::WString::tr("Lms.ok")) };
+ okBtn->clicked().connect([=] {
+ LmsApp->getModalManager().dispose(trackLyricsPtr);
+ });
+
+ LmsApp->getModalManager().show(std::move(trackLyrics));
+ }
+
std::unique_ptr createEntry(const db::ObjectPtr& track, PlayQueueController& playQueueController, Filters& filters)
{
auto entry{ std::make_unique(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("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);
});
diff --git a/src/lms/ui/explore/TrackListHelpers.hpp b/src/lms/ui/explore/TrackListHelpers.hpp
index 3d4f9873..5d9dd32f 100644
--- a/src/lms/ui/explore/TrackListHelpers.hpp
+++ b/src/lms/ui/explore/TrackListHelpers.hpp
@@ -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 createEntry(const db::ObjectPtr& track, PlayQueueController& playQueueController, Filters& filters);
} // namespace lms::ui::TrackListHelpers