diff --git a/CMakeLists.txt b/CMakeLists.txt index d56176ec..b13f35b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP) pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib) pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++) pkg_check_modules(GraphicsMagick++ IMPORTED_TARGET GraphicsMagick++) -pkg_check_modules(LIBAV IMPORTED_TARGET libavutil libavformat) +pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat) find_package(PAM) find_package(STB) diff --git a/approot/admin-database.xml b/approot/admin-database.xml index a81dc5ab..857b6611 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -44,12 +44,12 @@
-
diff --git a/approot/messages.xml b/approot/messages.xml index 19b1a95e..3a640a49 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -7,6 +7,7 @@ Administration Apply Cancel +Tags Create Delete Discard @@ -15,6 +16,7 @@ Login Logout Not a directory +Ok Password Bad login / password combination Login throttled, please try again later @@ -58,7 +60,6 @@ Scan launched! Scan options New settings saved! -Tags Update period Update start time Weekly @@ -119,7 +120,10 @@ Add filter All Artists +Bitrate +Codec Download +Duration Filter added Filters Most played @@ -136,6 +140,7 @@ Search Star Starred +Track information Playlists Tracks Type @@ -149,13 +154,34 @@ All artists -Track artists -Composers -Lyricists -Mixers -Producers -Album artists -Remixers + + Track artist + Track artists + + + Composer + Composers + + + Lyricist + Lyricists + + + Mixer + Mixers + + + Producer + Producers + + + Album artists + Album artist + + + Remixers + Remixer + Similar albums diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 571e40c4..b5c778bc 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -7,6 +7,7 @@ Administration Appliquer Annuler +Tags Créer Supprimer Annuler @@ -15,6 +16,7 @@ Login Quitter N'est pas un répertoire +Ok Mot de passe Mauvaise combinaison login / mot de passe Trop de tentatives de connexion, veuillez réessayer plus tard @@ -58,7 +60,6 @@ Scan lancé ! Options Nouveaux paramètres sauvegardés ! -Tags Périodicité des mises à jour Heure de départ de la mise à jour Toutes les semaines @@ -119,7 +120,10 @@ Ajouter filtre Tous Artistes +Bitrate +Codec Télécharger +Durée Filtre ajouté Filtres Plus joués @@ -136,6 +140,7 @@ Rechercher Ajouter aux favoris Favoris +Informations sur la piste Playlists Pistes Type @@ -149,13 +154,34 @@ Tous les artistes -Artistes de piste -Compositeurs -Paroliers -Mixers -Producteurs -Artistes d'album -Remixers + + Artiste de piste + Artistes de piste + + + Compositeur + Compositeurs + + + Parolier + Paroliers + + + Mixeur + Mixeurs + + + Producteur + Producteurs + + + Artiste d'album + Artistes d'album + + + Remixeur + Remixeurs + Albums similaires diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 9dc877b8..300f3305 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -7,6 +7,7 @@ Amministrazione Applica Annulla +Tags Crea Elimina Annulla @@ -15,6 +16,7 @@ Login Logout Non è una directory + Password Errata combinazione di Login / Password Superati i tentativi di accesso, riprova più tardi @@ -58,7 +60,6 @@ Scansione avviata! Impostazioni scansione Impostazioni salvate! -Tags Frequenza di aggiornamento Orario di aggiornamento Settimanale @@ -119,7 +120,10 @@ Aggiungi filtro Tutti Artisti + + Download + Filtro aggiunto Filtri Più ascoltati @@ -136,6 +140,8 @@ Ricerca Aggiungi ai preferiti Preferiti + + Tracce Tipo Rimuovi dai preferiti @@ -148,13 +154,34 @@ Tutti gli artisti -Artisti tracce -Compositori -Scrittori -Mixers -Produttori -Artisti album -Remixers + + + + + + + + + + + + + + + + + + + + + + + + + + + + Album simili diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index cdfb1a8d..db2f6714 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -7,14 +7,16 @@ 管理 应用 取消 +标签 新建 -删除 +删除 撤销 编辑 加载中... 登陆 注销 不是一个目录 + 密码 用户名或密码无效 登录已被限制,请稍后再试 @@ -26,7 +28,10 @@ 密码不匹配 另一个会话已打开,重新打开这一个? 保存 -{1} 个音轨 + + + + 用户 @@ -55,7 +60,6 @@ 扫描已完成! 扫描选项 新设置已保存! -标签 更新周期 更新开始时间 每周 @@ -116,7 +120,10 @@ 添加过滤器 全部 歌手 + + 下载 + 过滤器已添加 过滤器 播放最多 @@ -133,6 +140,7 @@ 搜索 收藏 已收藏 + 播放列表 音轨 类型 @@ -147,12 +155,33 @@ 所有歌手 音轨歌手 -作曲 -作词 -混响 -制作人 -专辑歌手 -混音师 + + + + + + + + + + + + + + + + + + + + + + + + + + + 相似专辑 @@ -171,7 +200,10 @@ 清除 创建新播放列表 -已添加 {1} 音轨 + + + + 播放队列 播放队列已满! 广播模式 diff --git a/approot/release.xml b/approot/release.xml index 944437db..deb0cca8 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -77,6 +77,7 @@
  • ${play-last class="dropdown-item"}
  • ${star class="dropdown-item"}
  • ${download class="dropdown-item"}
  • +
  • ${track-info class="dropdown-item"}
  • diff --git a/approot/settings.xml b/approot/settings.xml index a07498b3..537f4f76 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -30,7 +30,7 @@
    +;
    ${transcode-bitrate class="form-control"} kbps diff --git a/approot/tracks.xml b/approot/tracks.xml index 60a9bac2..4643c2d7 100644 --- a/approot/tracks.xml +++ b/approot/tracks.xml @@ -57,6 +57,7 @@
  • ${play-last class="dropdown-item"}
  • ${star class="dropdown-item"}
  • ${download class="dropdown-item"}
  • +
  • ${track-info class="dropdown-item"}
  • @@ -67,4 +68,74 @@ ${artist class="text-decoration-none link-secondary"} + + + + + +
    +
    + ${type} +
    +
    + ${artist-container data-bs-dismiss="modal"} +
    +
    +
    + diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index a90636ef..734b3538 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -23,6 +23,7 @@ extern "C" { #define __STDC_CONSTANT_MACROS #include +#include #include #include } @@ -38,7 +39,7 @@ static std::string averror_to_string(int error) { std::array buf = {0}; - if (av_strerror(error, buf.data(), buf.size()) == 0) + if (::av_strerror(error, buf.data(), buf.size()) == 0) return &buf[0]; else return "Unknown error"; @@ -104,7 +105,7 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& re return; AVDictionaryEntry *tag = NULL; - while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) + while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) { res[StringUtils::stringToUpper(tag->key)] = tag->value; } @@ -140,31 +141,18 @@ AudioFile::getStreamInfo() const for (std::size_t i {}; i < _context->nb_streams; ++i) { - AVStream* avstream { _context->streams[i]}; - - // Skip attached pics - if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) - continue; - - if (!avstream->codecpar) - { - LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; - continue; - } - - if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) - continue; - - res.push_back( {i, static_cast(avstream->codecpar->bit_rate)} ); + std::optional streamInfo {getStreamInfo(i)}; + if (streamInfo) + res.emplace_back(std::move(*streamInfo)); } return res; } std::optional -AudioFile::getBestStream() const +AudioFile::getBestStreamIndex() const { - int res = av_find_best_stream(_context, + int res = ::av_find_best_stream(_context, AVMEDIA_TYPE_AUDIO, -1, // Auto -1, // Auto @@ -177,6 +165,18 @@ AudioFile::getBestStream() const return res; } +std::optional +AudioFile::getBestStreamInfo() const +{ + std::optional res; + + std::optional bestStreamIndex {getBestStreamIndex()}; + if (bestStreamIndex) + res = getStreamInfo(*bestStreamIndex); + + return res; +} + bool AudioFile::hasAttachedPictures() const { @@ -238,10 +238,39 @@ AudioFile::visitAttachedPictures(std::function func) const } } -std::optional -guessMediaFileFormat(const std::filesystem::path& file) +std::optional +AudioFile::getStreamInfo(std::size_t streamIndex) const { - const AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)}; + std::optional res; + + AVStream* avstream { _context->streams[streamIndex]}; + assert(avstream); + + if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) + return res; + + if (!avstream->codecpar) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set"; + return res; + } + + if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) + return res; + + res.emplace(); + res->index = streamIndex; + res->bitrate = static_cast(avstream->codecpar->bit_rate); + res->codec = ::avcodec_get_name(avstream->codecpar->codec_id); + assert(!res->codec.empty()); + + return res; +} + +std::optional +guessAudioFileFormat(const std::filesystem::path& file) +{ + const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)}; if (!format || !format->name) return {}; diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp index a0253bd1..823b7e00 100644 --- a/src/libs/av/impl/AudioFile.hpp +++ b/src/libs/av/impl/AudioFile.hpp @@ -43,11 +43,14 @@ namespace Av std::chrono::milliseconds getDuration() const override; MetadataMap getMetaData() const override; std::vector getStreamInfo() const override; - std::optional getBestStream() const override; + std::optional getBestStreamInfo() const override; + std::optional getBestStreamIndex() const override; bool hasAttachedPictures() const override; void visitAttachedPictures(std::function func) const override; private: + std::optional getStreamInfo(std::size_t streamIndex) const; + const std::filesystem::path _p; AVFormatContext* _context {}; }; diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp index 3a5b9a9c..3083056a 100644 --- a/src/libs/av/include/av/IAudioFile.hpp +++ b/src/libs/av/include/av/IAudioFile.hpp @@ -37,13 +37,14 @@ namespace Av { std::string mimeType; const std::byte* data {}; - std::size_t dataSize; + std::size_t dataSize {}; }; struct StreamInfo { - size_t id; - std::size_t bitrate; + size_t index {}; + std::size_t bitrate {}; + std::string codec; }; class IAudioFile @@ -57,7 +58,8 @@ namespace Av virtual std::chrono::milliseconds getDuration() const = 0; virtual MetadataMap getMetaData() const = 0; virtual std::vector getStreamInfo() const = 0; - virtual std::optional getBestStream() const = 0; // none if failure/unknown + virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown + virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown virtual bool hasAttachedPictures() const = 0; virtual void visitAttachedPictures(std::function func) const = 0; }; diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index 84038255..5ed14990 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -94,7 +94,10 @@ createQuery(Session& session, const Artist::FindParameters& params) session.checkSharedLocked(); auto query {session.getDboSession().query("SELECT DISTINCT a.id FROM artist a")}; - if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType) + if (params.sortMethod == ArtistSortMethod::LastWritten + || params.writtenAfter.isValid() + || params.linkType + || params.track.isValid()) { query.join("track t ON t.id = t_a_l.track_id"); query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); @@ -157,6 +160,11 @@ createQuery(Session& session, const Artist::FindParameters& params) query.where(oss.str()); } + if (params.track.isValid()) + { + query.where("t.id = ?").bind(params.track); + } + switch (params.sortMethod) { case ArtistSortMethod::None: diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp index 669aa1ff..160aa618 100644 --- a/src/libs/services/database/include/services/database/Artist.hpp +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -33,6 +33,7 @@ #include "services/database/ReleaseId.hpp" #include "services/database/Types.hpp" #include "services/database/UserId.hpp" +#include "services/database/TrackId.hpp" #include "utils/EnumSet.hpp" #include "utils/UUID.hpp" @@ -61,6 +62,7 @@ class Artist : public Object Wt::WDateTime writtenAfter; UserId starringUser; // only artists starred by this user std::optional scrobbler; // and for this scrobbler + TrackId track; // artists involved in this track FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } @@ -69,6 +71,7 @@ class Artist : public Object FindParameters& setRange(Range _range) {range = _range; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + FindParameters& setTrack(TrackId _track) { track = _track; return *this; } }; Artist() = default; diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index ce7b1f31..654228dd 100644 --- a/src/libs/services/database/test/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -92,21 +92,27 @@ TEST_F(DatabaseFixture, Artist_singleTrack) } { - auto transaction {session.createUniqueTransaction()}; + auto transaction {session.createSharedTransaction()}; auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))}; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); } { - auto transaction {session.createUniqueTransaction()}; + auto transaction {session.createSharedTransaction()}; auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))}; EXPECT_EQ(tracks.results.size(), 0); } { - auto transaction {session.createUniqueTransaction()}; + auto transaction {session.createSharedTransaction()}; auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))}; EXPECT_EQ(tracks.results.size(), 0); } + { + auto transaction {session.createSharedTransaction()}; + auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); + } } TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) @@ -198,7 +204,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) } { - auto transaction {session.createUniqueTransaction()}; + auto transaction {session.createSharedTransaction()}; auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))}; ASSERT_EQ(tracks.results.size(), 1); diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index e5108f36..0e09f8ea 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -278,71 +278,6 @@ LmsApplication::finalize() preQuit().emit(); } -Wt::WLink -LmsApplication::createArtistLink(Database::Artist::pointer artist) -{ - if (const auto mbid {artist->getMBID()}) - return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}}; - else - return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()}; -} - -std::unique_ptr -LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addText) -{ - auto res = std::make_unique(createArtistLink(artist)); - - if (addText) - { - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(Wt::WString::fromUTF8(artist->getName())); - res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); - } - - return res; -} - -Wt::WLink -LmsApplication::createReleaseLink(Database::Release::pointer release) -{ - if (const auto mbid {release->getMBID()}) - return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}}; - else - return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()}; -} - -std::unique_ptr -LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool addText) -{ - auto res = std::make_unique(createReleaseLink(release)); - - if (addText) - { - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(Wt::WString::fromUTF8(release->getName())); - res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); - } - - return res; -} - -std::unique_ptr -LmsApplication::createTrackListAnchor(Database::TrackList::pointer trackList, bool addText) -{ - Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()}; - auto res {std::make_unique(link)}; - - if (addText) - { - const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})}; - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(name); - res->setToolTip(name, Wt::TextFormat::Plain); - } - - return res; -} - void LmsApplication::handleException(LmsApplicationException& e) { diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 83d07dfb..8ce00780 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -31,12 +31,8 @@ namespace Database { - class Artist; - class Cluster; class Db; - class Release; class Session; - class TrackList; class User; } @@ -80,12 +76,6 @@ class LmsApplication : public Wt::WApplication // Used to classify the message sent to the user void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000}); - static Wt::WLink createArtistLink(Database::ObjectPtr artist); - static std::unique_ptr createArtistAnchor(Database::ObjectPtr artist, bool addText = true); - static Wt::WLink createReleaseLink(Database::ObjectPtr release); - static std::unique_ptr createReleaseAnchor(Database::ObjectPtr release, bool addText = true); - static std::unique_ptr createTrackListAnchor(Database::ObjectPtr trackList, bool addText = true); - MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; } PlayQueue& getPlayQueue() const { return *_playQueue; } ModalManager& getModalManager() const { return *_modalManager; } diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index b8debb00..4bd40a2b 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -42,6 +42,7 @@ #include "utils/Utils.hpp" #include "LmsApplication.hpp" +#include "Utils.hpp" namespace UserInterface { @@ -155,7 +156,7 @@ replayGainPreAmpGainFromString(const std::string& str) if (!value) return std::nullopt; - return Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + return ::Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); } static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings) @@ -281,7 +282,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) { _artist->setTextFormat(Wt::TextFormat::Plain); _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); - _artist->setLink(LmsApp->createArtistLink(artists.front())); + _artist->setLink(Utils::createArtistLink(artists.front())); } else { @@ -293,7 +294,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) { _release->setTextFormat(Wt::TextFormat::Plain); _release->setText(Wt::WString::fromUTF8(track->getRelease()->getName())); - _release->setLink(LmsApp->createReleaseLink(track->getRelease())); + _release->setLink(Utils::createReleaseLink(track->getRelease())); } else { diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 4b37bb1d..6face3a5 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -500,9 +500,9 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) if (release) { entry->setCondition("if-has-release", true); - entry->bindWidget("release", LmsApplication::createReleaseAnchor(release)); + entry->bindWidget("release", Utils::createReleaseAnchor(release)); { - Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))}; + Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))}; auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)}; cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK anchor->setImage(std::move(cover)); diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index c1560d93..26da3f58 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -22,10 +22,17 @@ #include #include +#include #include +#include "services/database/Artist.hpp" #include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" #include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "explore/Filters.hpp" #include "LmsApplication.hpp" namespace UserInterface::Utils @@ -112,4 +119,123 @@ namespace UserInterface::Utils return res; } + + std::unique_ptr + createArtistContainer(const std::vector& artistIds) + { + using namespace Database; + + std::unique_ptr artistContainer {std::make_unique()}; + + bool firstArtist {true}; + + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + for (const ArtistId artistId : artistIds) + { + const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)}; + if (!artist) + continue; + + if (!firstArtist) + artistContainer->addNew(" · "); + + auto anchor {createArtistAnchor(artist)}; + anchor->addStyleClass("link-success text-decoration-none"); // hack + artistContainer->addWidget(std::move(anchor)); + firstArtist = false; + } + + return artistContainer; + } + + Wt::WLink + createArtistLink(Database::Artist::pointer artist) + { + if (const auto mbid {artist->getMBID()}) + return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}}; + else + return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()}; + } + + std::unique_ptr + createArtistAnchor(Database::Artist::pointer artist, bool setText) + { + auto res = std::make_unique(createArtistLink(artist)); + + if (setText) + { + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(Wt::WString::fromUTF8(artist->getName())); + res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); + } + + return res; + } + + Wt::WLink + createReleaseLink(Database::Release::pointer release) + { + if (const auto mbid {release->getMBID()}) + return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}}; + else + return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()}; + } + + std::unique_ptr + createReleaseAnchor(Database::Release::pointer release, bool setText) + { + auto res = std::make_unique(createReleaseLink(release)); + + if (setText) + { + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(Wt::WString::fromUTF8(release->getName())); + res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); + } + + return res; + } + + std::unique_ptr + createTrackListAnchor(Database::TrackList::pointer trackList, bool setText) + { + Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()}; + auto res {std::make_unique(link)}; + + if (setText) + { + const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})}; + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(name); + res->setToolTip(name, Wt::TextFormat::Plain); + } + + return res; + } + + std::unique_ptr + createClustersForTrack(Database::Track::pointer track, Filters& filters) + { + using namespace Database; + + std::unique_ptr clusterContainer {std::make_unique()}; + + const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()}; + const auto clusterGroups {track->getClusterGroups(clusterTypes, 3)}; + + for (const auto& clusters : clusterGroups) + { + for (const Cluster::pointer& cluster : clusters) + { + const ClusterId clusterId {cluster->getId()}; + Wt::WInteractWidget* entry {clusterContainer->addWidget(Utils::createCluster(clusterId))}; + entry->clicked().connect([&filters, clusterId] + { + filters.add(clusterId); + }); + } + } + + return clusterContainer; + } } diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index 9a4f9fbc..70ad067c 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -22,22 +22,48 @@ #include #include +#include #include #include +#include "services/database/ArtistId.hpp" #include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" #include "services/database/ReleaseId.hpp" #include "services/database/TrackId.hpp" #include "resource/CoverResource.hpp" +namespace Database +{ + class Artist; + class Cluster; + class Release; + class Track; + class TrackList; +} + +namespace UserInterface +{ + class Filters; +} + namespace UserInterface::Utils { - std::string - durationToString(std::chrono::milliseconds msDuration); + std::string durationToString(std::chrono::milliseconds msDuration); std::unique_ptr createCover(Database::ReleaseId releaseId, CoverResource::Size size); std::unique_ptr createCover(Database::TrackId trackId, CoverResource::Size size); std::unique_ptr createCluster(Database::ClusterId clusterId, bool canDelete = false); + + std::unique_ptr createArtistContainer(const std::vector& artists); + + Wt::WLink createArtistLink(Database::ObjectPtr artist); + std::unique_ptr createArtistAnchor(Database::ObjectPtr artist, bool setText = true); + Wt::WLink createReleaseLink(Database::ObjectPtr release); + std::unique_ptr createReleaseAnchor(Database::ObjectPtr release, bool setText = true); + std::unique_ptr createTrackListAnchor(Database::ObjectPtr trackList, bool setText = true); + + std::unique_ptr createClustersForTrack(Database::ObjectPtr track, Filters& filters); } diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index f0459b98..9436de4f 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -53,7 +53,7 @@ class DatabaseSettingsModel : public Wt::WFormModel static inline constexpr Field UpdatePeriodField {"update-period"}; static inline constexpr Field UpdateStartTimeField {"update-start-time"}; static inline constexpr Field RecommendationEngineTypeField {"recommendation-engine-type"}; - static inline constexpr Field TagsField {"tags"}; + static inline constexpr Field ClustersField {"clusters"}; using UpdatePeriodModel = ValueStringModel; @@ -65,7 +65,7 @@ class DatabaseSettingsModel : public Wt::WFormModel addField(UpdatePeriodField); addField(UpdateStartTimeField); addField(RecommendationEngineTypeField); - addField(TagsField); + addField(ClustersField); auto dirValidator {createDirectoryValidator()}; dirValidator->setMandatory(true); @@ -114,7 +114,7 @@ class DatabaseSettingsModel : public Wt::WFormModel { std::vector names; std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](auto clusterType) { return clusterType->getName(); }); - setValue(TagsField, StringUtils::joinStrings(names, " ")); + setValue(ClustersField, StringUtils::joinStrings(names, " ")); } } @@ -138,7 +138,7 @@ class DatabaseSettingsModel : public Wt::WFormModel if (recommendationEngineTypeRow) scanSettings.modify()->setRecommendationEngineType(_recommendationEngineTypeModel->getValue(*recommendationEngineTypeRow)); - auto clusterTypes {StringUtils::splitStringCopy(valueText(TagsField).toUTF8(), " ")}; + auto clusterTypes {StringUtils::splitStringCopy(valueText(ClustersField).toUTF8(), " ")}; scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set(clusterTypes.begin(), clusterTypes.end())); } @@ -215,8 +215,8 @@ DatabaseSettingsView::refreshView() recommendationEngineType->setModel(model->recommendationEngineTypeModel()); t->setFormWidget(DatabaseSettingsModel::RecommendationEngineTypeField, std::move(recommendationEngineType)); - // Tags - t->setFormWidget(DatabaseSettingsModel::TagsField, std::make_unique()); + // Clusters + t->setFormWidget(DatabaseSettingsModel::ClustersField, std::make_unique()); // Buttons Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique(Wt::WString::tr("Lms.apply"))); diff --git a/src/lms/ui/explore/ArtistListHelpers.cpp b/src/lms/ui/explore/ArtistListHelpers.cpp index 78cca4b7..f8bd2c29 100644 --- a/src/lms/ui/explore/ArtistListHelpers.cpp +++ b/src/lms/ui/explore/ArtistListHelpers.cpp @@ -21,7 +21,7 @@ #include #include "services/database/Artist.hpp" -#include "LmsApplication.hpp" +#include "Utils.hpp" namespace UserInterface::ArtistListHelpers { @@ -29,7 +29,7 @@ namespace UserInterface::ArtistListHelpers createEntry(const Database::ObjectPtr& artist) { auto res {std::make_unique(Wt::WString::tr("Lms.Explore.Artists.template.entry"))}; - res->bindWidget("name", LmsApplication::createArtistAnchor(artist)); + res->bindWidget("name", Utils::createArtistAnchor(artist)); return res; } diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 48f5cc9d..885ec56b 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -335,7 +335,7 @@ Artist::addSomeNonReleaseTracks() } const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; - _trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController)); + _trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController, _filters)); areTracksAdded = true; } diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index d67a601f..021f57cf 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -137,7 +137,7 @@ Artists::refreshArtistLinkTypes() if (!usedLinkTypes.contains(linkType)) return; - linkTypeModel->add(Wt::WString::tr(std::string {stringKey}), linkType); + linkTypeModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType); }}; linkTypeModel->clear(); diff --git a/src/lms/ui/explore/ReleaseListHelpers.cpp b/src/lms/ui/explore/ReleaseListHelpers.cpp index 12dfb8a0..dc69fa88 100644 --- a/src/lms/ui/explore/ReleaseListHelpers.cpp +++ b/src/lms/ui/explore/ReleaseListHelpers.cpp @@ -26,7 +26,6 @@ #include "services/database/Artist.hpp" #include "services/database/Release.hpp" -#include "LmsApplication.hpp" #include "Utils.hpp" using namespace Database; @@ -39,11 +38,11 @@ namespace UserInterface::ReleaseListHelpers { auto entry {std::make_unique(Wt::WString::tr(templateKey))}; - entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release)); + entry->bindWidget("release-name", Utils::createReleaseAnchor(release)); entry->addFunction("tr", &Wt::WTemplate::Functions::tr); { - Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))}; + Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))}; auto cover {Utils::createCover(release->getId(), CoverResource::Size::Large)}; cover->addStyleClass("Lms-cover-release Lms-cover-anchor"); anchor->setImage(std::move(cover)); @@ -62,7 +61,7 @@ namespace UserInterface::ReleaseListHelpers else if (artists.size() == 1 && !isSameArtist) { entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front())); + entry->bindWidget("artist-name", Utils::createArtistAnchor(artists.front())); } if (showYear) diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 3c0c6094..884f2c4c 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -37,6 +37,7 @@ #include "explore/Filters.hpp" #include "explore/PlayQueueController.hpp" #include "explore/ReleaseListHelpers.hpp" +#include "explore/TrackListHelpers.hpp" #include "LmsApplication.hpp" #include "LmsApplicationException.hpp" #include "MediaPlayer.hpp" @@ -270,7 +271,7 @@ Release::refreshView() { if (!firstArtist) artistsContainer->addNew(" · "); - auto anchor {LmsApplication::createArtistAnchor(artist)}; + auto anchor {Utils::createArtistAnchor(artist)}; anchor->addStyleClass("link-success text-decoration-none"); // hack artistsContainer->addWidget(std::move(anchor)); firstArtist = false; @@ -319,6 +320,9 @@ Release::refreshView() entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(trackId)}); + + entry->bindNew("track-info", Wt::WString::tr("Lms.Explore.track-info")) + ->clicked().connect([=] { TrackListHelpers::showTrackInfoModal(trackId, _filters); }); } entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); @@ -361,7 +365,7 @@ Release::refreshReleaseArtists(const Database::Release::pointer& release) for (const auto& artist : artists) { Wt::WTemplate* artistTemplate {artistsContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry-release-artist"))}; - artistTemplate->bindWidget("artist", LmsApplication::createArtistAnchor(artist)); + artistTemplate->bindWidget("artist", Utils::createArtistAnchor(artist)); } } } diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index b01030c5..92d0df1f 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -38,6 +38,7 @@ namespace UserInterface SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController) : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")} , _playQueueController {playQueueController} + , _filters {filters} , _artistCollector {filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)} , _releaseCollector {filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)} , _trackCollector {filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)} @@ -152,7 +153,7 @@ namespace UserInterface for (const TrackId trackId : trackIds.results) { const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; - _tracks->add(TrackListHelpers::createEntry(track, _playQueueController)); + _tracks->add(TrackListHelpers::createEntry(track, _playQueueController, _filters)); } } diff --git a/src/lms/ui/explore/SearchView.hpp b/src/lms/ui/explore/SearchView.hpp index 9c22aa6e..337a3a0c 100644 --- a/src/lms/ui/explore/SearchView.hpp +++ b/src/lms/ui/explore/SearchView.hpp @@ -72,6 +72,7 @@ namespace UserInterface void addSomeTracks(); PlayQueueController& _playQueueController; + Filters& _filters; ArtistCollector _artistCollector; ReleaseCollector _releaseCollector; TrackCollector _trackCollector; diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index c296df60..26fc40e7 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -23,6 +23,7 @@ #include #include +#include "av/IAudioFile.hpp" #include "services/database/Artist.hpp" #include "services/database/Release.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -36,29 +37,96 @@ #include "resource/CoverResource.hpp" #include "LmsApplication.hpp" #include "MediaPlayer.hpp" +#include "ModalManager.hpp" #include "Utils.hpp" using namespace Database; namespace UserInterface::TrackListHelpers { + void + showTrackInfoModal(Database::TrackId trackId, Filters& filters) + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; + if (!track) + return; + + auto trackInfo {std::make_unique