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