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/explore.xml b/approot/explore.xml index 36eef544..38d64d00 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -41,4 +41,16 @@
+ +
+
+ ${type} +
+
+ ${artist-container data-bs-dismiss="modal"} +
+
+
+ + diff --git a/approot/messages.xml b/approot/messages.xml index 19b1a95e..f805b9fa 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 @@ -132,10 +136,12 @@ Recently added Recently modified Recently played +Release information Albums Search Star Starred +Track information Playlists Tracks Type @@ -149,13 +155,38 @@ All artists -Track artists -Composers -Lyricists -Mixers -Producers -Album artists -Remixers + + Track artist + Track artists + + + Composer + Composers + + + Conductor + Conductors + + + 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..1c32c26b 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 @@ -132,10 +136,12 @@ Ajoutés récemment Modifiés récemment Joués récemment +Informations sur l'album Albums Rechercher Ajouter aux favoris Favoris +Informations sur la piste Playlists Pistes Type @@ -149,13 +155,38 @@ Tous les artistes -Artistes de piste -Compositeurs -Paroliers -Mixers -Producteurs -Artistes d'album -Remixers + + Artiste de piste + Artistes de piste + + + Compositeur + Compositeurs + + + Chef d'orchestre + Chefs d'orchestre + + + 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..0f000814 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 @@ -132,10 +136,13 @@ Aggiunti di recente Riprodotti di recente + Album Ricerca Aggiungi ai preferiti Preferiti + + Tracce Tipo Rimuovi dai preferiti @@ -148,13 +155,38 @@ 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..9472f65a 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 @@ 添加过滤器 全部 歌手 + + 下载 + 过滤器已添加 过滤器 播放最多 @@ -129,10 +136,12 @@ 最近添加 最近更改 最近播放 + 专辑 搜索 收藏 已收藏 + 播放列表 音轨 类型 @@ -147,12 +156,37 @@ 所有歌手 音轨歌手 -作曲 -作词 -混响 -制作人 -专辑歌手 -混音师 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 相似专辑 @@ -171,7 +205,10 @@ 清除 创建新播放列表 -已添加 {1} 音轨 + + + + 播放队列 播放队列已满! 广播模式 diff --git a/approot/release.xml b/approot/release.xml index 944437db..16545709 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -28,6 +28,7 @@
  • ${tr:Lms.Explore.musicbrainz-release}
  • ${}
  • ${download class="dropdown-item"}
  • +
  • ${release-info class="dropdown-item"}
  • @@ -77,10 +78,52 @@
  • ${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..731fb8a2 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,63 @@ ${artist class="text-decoration-none link-secondary"} + + + + diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index a90636ef..fa730e72 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -38,7 +38,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 +104,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 +140,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 +164,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 +237,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..b3588fb5 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -94,7 +94,11 @@ 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() + || params.release.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 +161,12 @@ createQuery(Session& session, const Artist::FindParameters& params) query.where(oss.str()); } + if (params.track.isValid()) + query.where("t.id = ?").bind(params.track); + + if (params.release.isValid()) + query.where("t.release_id = ?").bind(params.release); + 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 04067a9c..dd032a60 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,8 @@ 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 + ReleaseId release; // artists involved in this release FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } @@ -69,6 +72,8 @@ 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; } + FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } }; Artist() = default; @@ -112,7 +117,7 @@ class Artist : public Object } private: - static const std::size_t _maxNameLength = 128; + static constexpr std::size_t _maxNameLength {128}; friend class Session; // Create diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 2079fc41..5bb00607 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -134,7 +134,7 @@ class Release : public Object Release(const std::string& name, const std::optional& MBID = {}); static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); - static const std::size_t _maxNameLength {128}; + static constexpr std::size_t _maxNameLength {128}; std::string _name; std::string _MBID; diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index d7f211e2..60edfc95 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -207,9 +207,9 @@ class Track : public Object Track(const std::filesystem::path& p); static pointer create(Session& session, const std::filesystem::path& p); - static const std::size_t _maxNameLength = 128; - static const std::size_t _maxCopyrightLength = 128; - static const std::size_t _maxCopyrightURLLength = 128; + static constexpr std::size_t _maxNameLength {128}; + static constexpr std::size_t _maxCopyrightLength {128}; + static constexpr std::size_t _maxCopyrightURLLength {128}; int _scanVersion {}; int _trackNumber {}; diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index ce7b1f31..bad6e5e7 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); @@ -366,9 +372,45 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks) { auto transaction {session.createSharedTransaction()}; - const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist->getId()).setNonRelease(true))}; + const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))}; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track2.getId()); } } +TEST_F(DatabaseFixture, Artist_findByRelease) +{ + ScopedArtist artist {session, "artist"}; + ScopedTrack track {session, "MyTrack"}; + ScopedRelease release{session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; + EXPECT_EQ(artists.results.size(), 0); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; + EXPECT_EQ(artists.results.size(), 0); + } + + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); + } +} + 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/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp index dc05c06d..c4b3bc43 100644 --- a/src/lms/ui/LmsTheme.cpp +++ b/src/lms/ui/LmsTheme.cpp @@ -16,9 +16,6 @@ namespace UserInterface LmsTheme::init(Wt::WApplication* app) const { app->require("js/bootstrap.bundle.min.js"); - Wt::WString v = app->metaHeader(Wt::MetaHeaderType::Meta, "viewport"); - if (v.empty()) - app->addMetaHeader("viewport", "width=device-width, initial-scale=1, user-scalable=no"); } std::string 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..760c727b 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(); @@ -148,6 +148,7 @@ Artists::refreshArtistLinkTypes() addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist"); addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist"); addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); + addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer"); addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 73195990..27d79f43 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -41,6 +41,7 @@ Filters::showDialog() auto dialog {std::make_unique(Wt::WString::tr("Lms.Explore.template.add-filter"))}; Wt::WWidget* dialogPtr {dialog.get()}; dialog->addFunction("tr", &Wt::WTemplate::Functions::tr); + dialog->addFunction("id", &Wt::WTemplate::Functions::id); Wt::WComboBox* typeCombo {dialog->bindNew("type")}; Wt::WComboBox* valueCombo {dialog->bindNew("value")}; 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..257db1f7 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -23,6 +23,8 @@ #include #include +#include "av/IAudioFile.hpp" +#include "services/database/Artist.hpp" #include "services/database/Cluster.hpp" #include "services/database/Release.hpp" #include "services/database/ScanSettings.hpp" @@ -37,15 +39,88 @@ #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" +#include "ModalManager.hpp" #include "Utils.hpp" using namespace Database; namespace UserInterface { +void +showReleaseInfoModal(Database::ReleaseId releaseId) +{ + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)}; + if (!release) + return; + + auto releaseInfo {std::make_unique