diff --git a/approot/messages.xml b/approot/messages.xml
index 31f6b7ae..188c07af 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -97,6 +97,7 @@
Cannot get track duration
+Cannot parse artist info file
Cannot parse audio file
Cannot read file
Cannot parse image file
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index bbcb1be8..3b5421ed 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -97,6 +97,7 @@
Impossible de récupérer la durée de la piste
+Impossible d'analyser le fichier d'informations sur l'artiste
Impossible d'analyser le fichier audio
Impossible de lire le fichier
Impossible d'analyser le fichier image
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index ae9553e2..74361e68 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -97,6 +97,7 @@
Non sono stato in grado di determinare la durata della traccia
+Impossibile analizzare il file delle informazioni sull'artista
Impossibile analizzare il file audio
Non in grado di leggere il file
Impossibile analizzare il file immagine
diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml
index 286be68a..372b5c18 100644
--- a/approot/messages_pl.xml
+++ b/approot/messages_pl.xml
@@ -98,6 +98,7 @@
Nie udało się ustalić długości ścieżki
+Nie można przetworzyć pliku z informacjami o artyście"
Nie można przeanalizować pliku audio
Nie udało się odczytać pliku
Nie można przeanalizować pliku obrazu
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index 3e892392..0ea9cbe9 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -97,6 +97,7 @@
无法获得音轨时间
+
无法解析文件
无法读取文件
diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt
index 12aee1b8..bfd6bd4f 100644
--- a/src/libs/database/CMakeLists.txt
+++ b/src/libs/database/CMakeLists.txt
@@ -1,5 +1,6 @@
add_library(lmsdatabase STATIC
impl/Artist.cpp
+ impl/ArtistInfo.cpp
impl/AuthToken.cpp
impl/Cluster.cpp
impl/Db.cpp
diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp
index f0c5638f..93062c23 100644
--- a/src/libs/database/impl/Artist.cpp
+++ b/src/libs/database/impl/Artist.cpp
@@ -265,7 +265,18 @@ namespace lms::db
RangeResults Artist::findOrphanIds(Session& session, std::optional range)
{
session.checkReadTransaction();
- auto query{ session.getDboSession()->query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") };
+ auto query{ session.getDboSession()->query(R"(SELECT DISTINCT a.id FROM artist a
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM track t
+ INNER JOIN track_artist_link t_a_l
+ ON t_a_l.artist_id = a.id
+ WHERE t.id = t_a_l.track_id
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM artist_info ai
+ WHERE ai.artist_id = a.id))") };
return utils::execRangeQuery(query, range);
}
diff --git a/src/libs/database/impl/ArtistInfo.cpp b/src/libs/database/impl/ArtistInfo.cpp
new file mode 100644
index 00000000..fdd9ff65
--- /dev/null
+++ b/src/libs/database/impl/ArtistInfo.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "database/ArtistInfo.hpp"
+
+#include
+
+#include "database/Artist.hpp"
+#include "database/ArtistInfoId.hpp"
+#include "database/Directory.hpp"
+#include "database/Session.hpp"
+
+#include "Utils.hpp"
+#include "traits/IdTypeTraits.hpp"
+#include "traits/PathTraits.hpp"
+
+namespace lms::db
+{
+ ArtistInfo::pointer ArtistInfo::create(Session& session)
+ {
+ return session.getDboSession()->add(std::unique_ptr{ new ArtistInfo{} });
+ }
+
+ std::size_t ArtistInfo::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM artist_info"));
+ }
+
+ ArtistInfo::pointer ArtistInfo::find(Session& session, const std::filesystem::path& p)
+ {
+ session.checkReadTransaction();
+
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a_i from artist_info a_i").where("a_i.absolute_file_path = ?").bind(p));
+ }
+
+ ArtistInfo::pointer ArtistInfo::find(Session& session, ArtistInfoId id)
+ {
+ session.checkReadTransaction();
+
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a_i from artist_info a_i").where("a_i.id = ?").bind(id));
+ }
+
+ void ArtistInfo::find(Session& session, ArtistId id, std::optional range, const std::function& func)
+ {
+ session.checkReadTransaction();
+
+ auto query{ session.getDboSession()->query>("SELECT a_i from artist_info a_i").where("a_i.artist_id = ?").bind(id) };
+ utils::forEachQueryRangeResult(query, range, [&](const ArtistInfo::pointer& entry) {
+ func(entry);
+ });
+ }
+
+ void ArtistInfo::find(Session& session, ArtistId id, const std::function& func)
+ {
+ find(session, id, std::nullopt, std::move(func));
+ }
+
+ void ArtistInfo::find(Session& session, ArtistInfoId& lastRetrievedId, std::size_t count, const std::function& func)
+ {
+ session.checkReadTransaction();
+
+ auto query{ session.getDboSession()->query>("SELECT a_i from artist_info a_i").orderBy("a_i.id").where("a_i.id > ?").bind(lastRetrievedId).limit(static_cast(count)) };
+
+ utils::forEachQueryResult(query, [&](const ArtistInfo::pointer& entry) {
+ func(entry);
+ lastRetrievedId = entry->getId();
+ });
+ }
+
+ Artist::pointer ArtistInfo::getArtist() const
+ {
+ return _artist;
+ }
+
+ Directory::pointer ArtistInfo::getDirectory() const
+ {
+ return _directory;
+ }
+
+ void ArtistInfo::setAbsoluteFilePath(const std::filesystem::path& filePath)
+ {
+ assert(filePath.is_absolute());
+ _absoluteFilePath = filePath;
+ _fileStem = filePath.stem();
+ }
+
+ void ArtistInfo::setDirectory(ObjectPtr directory)
+ {
+ _directory = getDboPtr(directory);
+ }
+
+ void ArtistInfo::setArtist(ObjectPtr artist)
+ {
+ _artist = getDboPtr(artist);
+ }
+} // namespace lms::db
diff --git a/src/libs/database/impl/Directory.cpp b/src/libs/database/impl/Directory.cpp
index 6f4f1778..da595498 100644
--- a/src/libs/database/impl/Directory.cpp
+++ b/src/libs/database/impl/Directory.cpp
@@ -176,11 +176,13 @@ namespace lms::db
query.leftJoin("image i ON d.id = i.directory_id");
query.leftJoin("track_lyrics l_lrc ON d.id = l_lrc.directory_id");
query.leftJoin("playlist_file pl_f ON d.id = pl_f.directory_id");
+ query.leftJoin("artist_info a_i ON d.id = a_i.directory_id");
query.where("d_child.id IS NULL");
query.where("t.directory_id IS NULL");
query.where("i.directory_id IS NULL");
query.where("l_lrc.directory_id IS NULL");
query.where("pl_f.directory_id IS NULL");
+ query.where("a_i.directory_id IS NULL");
return utils::execRangeQuery(query, range);
}
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index 1b0a857f..471a4e04 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.cpp
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
- static constexpr Version LMS_DATABASE_VERSION{ 84 };
+ static constexpr Version LMS_DATABASE_VERSION{ 85 };
}
VersionInfo::VersionInfo()
@@ -1137,6 +1137,28 @@ FROM tracklist)");
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
+ void migrateFromV84(Session& session)
+ {
+ // New artist info feature
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artist_info" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "absolute_file_path" text not null,
+ "file_last_write" text,
+ "type" text not null,
+ "gender" text not null,
+ "disambiguation" text not null,
+ "biography" text not null,
+ "directory_id" bigint,
+ "artist_id" bigint,
+ constraint "fk_artist_info_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
+ constraint "fk_artist_info_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
+ ))");
+
+ // Just increment the scan version of the settings to make the next scan rescan everything
+ utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
+ }
+
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1197,6 +1219,7 @@ FROM tracklist)");
{ 81, migrateFromV81 },
{ 82, migrateFromV82 },
{ 83, migrateFromV83 },
+ { 84, migrateFromV84 },
};
bool migrationPerformed{};
diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp
index 6c44db6e..671a4960 100644
--- a/src/libs/database/impl/Session.cpp
+++ b/src/libs/database/impl/Session.cpp
@@ -19,10 +19,10 @@
#include "database/Session.hpp"
-#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "database/Artist.hpp"
+#include "database/ArtistInfo.hpp"
#include "database/AuthToken.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -100,8 +100,8 @@ namespace lms::db
{
_session.setConnectionPool(_db.getConnectionPool());
- _session.mapClass("version_info");
_session.mapClass("artist");
+ _session.mapClass("artist_info");
_session.mapClass("auth_token");
_session.mapClass("cluster");
_session.mapClass("cluster_type");
@@ -133,6 +133,7 @@ namespace lms::db
_session.mapClass("track_lyrics");
_session.mapClass("ui_state");
_session.mapClass("user");
+ _session.mapClass("version_info");
}
WriteTransaction Session::createWriteTransaction()
@@ -198,6 +199,10 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_path_idx ON artist_info(absolute_file_path)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_directory_id_idx ON artist_info(directory_id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
+
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
diff --git a/src/libs/database/include/database/ArtistInfo.hpp b/src/libs/database/include/database/ArtistInfo.hpp
new file mode 100644
index 00000000..2dd00690
--- /dev/null
+++ b/src/libs/database/include/database/ArtistInfo.hpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "database/ArtistId.hpp"
+#include "database/ArtistInfoId.hpp"
+#include "database/DirectoryId.hpp"
+#include "database/Object.hpp"
+#include "database/Types.hpp"
+
+namespace lms::db
+{
+ class Artist;
+ class Directory;
+ class Session;
+
+ class ArtistInfo final : public Object
+ {
+ public:
+ ArtistInfo() = default;
+
+ // find
+ static std::size_t getCount(Session& session);
+ static pointer find(Session& session, ArtistInfoId id);
+ static void find(Session& session, ArtistId id, std::optional range, const std::function& func);
+ static void find(Session& session, ArtistId id, const std::function& func);
+ static pointer find(Session& session, const std::filesystem::path& path);
+ static void find(Session& session, ArtistInfoId& lastRetrievedId, std::size_t count, const std::function& func);
+
+ // getters
+ const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
+ const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
+ ObjectPtr getDirectory() const;
+ ObjectPtr getArtist() const;
+ DirectoryId getDirectoryId() const { return _directory.id(); }
+ std::string_view getType() const { return _type; }
+ std::string_view getGender() const { return _gender; }
+ std::string_view getDisambiguation() const { return _disambiguation; }
+ std::string_view getBiography() const { return _biography; }
+
+ // setters
+ void setAbsoluteFilePath(const std::filesystem::path& filePath);
+ void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
+ void setDirectory(ObjectPtr directory);
+ void setArtist(ObjectPtr artist);
+ void setType(std::string_view type) { _type = type; }
+ void setGender(std::string_view gender) { _gender = gender; }
+ void setDisambiguation(std::string_view disambiguation) { _disambiguation = disambiguation; }
+ void setBiography(std::string_view biography) { _biography = biography; };
+
+ template
+ void persist(Action& a)
+ {
+ Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
+ Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
+
+ Wt::Dbo::field(a, _type, "type");
+ Wt::Dbo::field(a, _gender, "gender");
+ Wt::Dbo::field(a, _disambiguation, "disambiguation");
+ Wt::Dbo::field(a, _biography, "biography");
+
+ Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
+ Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
+ }
+
+ private:
+ friend class Session;
+ static pointer create(Session& session);
+
+ // Set when coming from artist info file
+ std::filesystem::path _absoluteFilePath;
+ std::string _fileStem;
+ Wt::WDateTime _fileLastWrite;
+
+ std::string _type;
+ std::string _gender;
+ std::string _disambiguation;
+ std::string _biography;
+
+ Wt::Dbo::ptr _directory;
+ Wt::Dbo::ptr _artist;
+ };
+} // namespace lms::db
diff --git a/src/libs/database/include/database/ArtistInfoId.hpp b/src/libs/database/include/database/ArtistInfoId.hpp
new file mode 100644
index 00000000..1056f37c
--- /dev/null
+++ b/src/libs/database/include/database/ArtistInfoId.hpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "database/IdType.hpp"
+
+LMS_DECLARE_IDTYPE(ArtistInfoId)
diff --git a/src/libs/database/test/ArtistInfo.cpp b/src/libs/database/test/ArtistInfo.cpp
new file mode 100644
index 00000000..177b5a34
--- /dev/null
+++ b/src/libs/database/test/ArtistInfo.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "Common.hpp"
+
+#include "database/Artist.hpp"
+#include "database/ArtistInfo.hpp"
+#include "database/Directory.hpp"
+
+namespace lms::db::tests
+{
+ using ScopedArtistInfo = ScopedEntity;
+ using ScopedDirectory = ScopedEntity;
+
+ TEST_F(DatabaseFixture, ArtistInfo)
+ {
+ {
+ auto transaction{ session.createReadTransaction() };
+ EXPECT_EQ(ArtistInfo::getCount(session), 0);
+ }
+
+ ScopedArtistInfo artistInfo{ session };
+
+ {
+ auto transaction{ session.createReadTransaction() };
+ EXPECT_EQ(ArtistInfo::getCount(session), 1);
+ }
+
+ {
+ auto transaction{ session.createReadTransaction() };
+
+ const ArtistInfo::pointer dbArtistInfo{ ArtistInfo::find(session, artistInfo.getId()) };
+
+ EXPECT_EQ(dbArtistInfo->getAbsoluteFilePath(), "");
+ EXPECT_EQ(dbArtistInfo->getLastWriteTime(), Wt::WDateTime{});
+
+ EXPECT_EQ(dbArtistInfo->getArtist(), Artist::pointer{});
+ EXPECT_EQ(dbArtistInfo->getDirectory(), Directory::pointer{});
+ EXPECT_EQ(dbArtistInfo->getType(), "");
+ EXPECT_EQ(dbArtistInfo->getGender(), "");
+ EXPECT_EQ(dbArtistInfo->getDisambiguation(), "");
+ EXPECT_EQ(dbArtistInfo->getBiography(), "");
+ }
+
+ ScopedArtist artist{ session, "MyArtist" };
+ ScopedDirectory directory{ session, "/tmp" };
+
+ const Wt::WDateTime dateTime{ Wt::WDate{ 2024, 30, 1 }, Wt::WTime{ 12, 58, 29 } };
+
+ // Now change some values
+ {
+ auto transaction{ session.createWriteTransaction() };
+ ArtistInfo::pointer dbArtistInfo{ ArtistInfo::find(session, artistInfo.getId()) };
+
+ dbArtistInfo.modify()->setAbsoluteFilePath("/tmp/artist.nfo");
+ dbArtistInfo.modify()->setLastWriteTime(dateTime);
+
+ dbArtistInfo.modify()->setArtist(artist.get());
+ dbArtistInfo.modify()->setDirectory(directory.get());
+ dbArtistInfo.modify()->setType("MyType");
+ dbArtistInfo.modify()->setGender("MyGender");
+ dbArtistInfo.modify()->setDisambiguation("MyDisambiguation");
+ dbArtistInfo.modify()->setBiography("MyBiography");
+ }
+
+ // Check values are reflected
+ {
+ auto transaction{ session.createReadTransaction() };
+
+ const ArtistInfo::pointer dbArtistInfo{ ArtistInfo::find(session, artistInfo.getId()) };
+
+ EXPECT_EQ(dbArtistInfo->getAbsoluteFilePath(), "/tmp/artist.nfo");
+ EXPECT_EQ(dbArtistInfo->getLastWriteTime(), dateTime);
+
+ EXPECT_EQ(dbArtistInfo->getArtist(), artist.get());
+ EXPECT_EQ(dbArtistInfo->getDirectory(), directory.get());
+ EXPECT_EQ(dbArtistInfo->getDirectoryId(), directory.getId());
+ EXPECT_EQ(dbArtistInfo->getType(), "MyType");
+ EXPECT_EQ(dbArtistInfo->getGender(), "MyGender");
+ EXPECT_EQ(dbArtistInfo->getDisambiguation(), "MyDisambiguation");
+ EXPECT_EQ(dbArtistInfo->getBiography(), "MyBiography");
+ }
+
+ {
+ auto transaction{ session.createReadTransaction() };
+
+ bool visited{};
+ ArtistInfo::find(session, artist.getId(), [&](const ArtistInfo::pointer& dbArtistInfo) {
+ ASSERT_NE(dbArtistInfo, ArtistInfo::pointer{});
+ EXPECT_EQ(dbArtistInfo->getId(), artistInfo.getId());
+ visited = true;
+ });
+ EXPECT_TRUE(visited);
+ }
+ }
+} // namespace lms::db::tests
\ No newline at end of file
diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt
index ae3767b2..754d684a 100644
--- a/src/libs/database/test/CMakeLists.txt
+++ b/src/libs/database/test/CMakeLists.txt
@@ -1,7 +1,8 @@
add_executable(test-database
- AuthToken.cpp
Artist.cpp
+ ArtistInfo.cpp
+ AuthToken.cpp
Cluster.cpp
Common.cpp
DatabaseTest.cpp
diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp
index bdb1da45..eb6aa77f 100644
--- a/src/libs/database/test/Migration.cpp
+++ b/src/libs/database/test/Migration.cpp
@@ -20,6 +20,8 @@
#include "Common.hpp"
#include "core/String.hpp"
+#include "database/Artist.hpp"
+#include "database/ArtistInfo.hpp"
#include "database/AuthToken.hpp"
#include "database/Db.hpp"
#include "database/Directory.hpp"
@@ -341,6 +343,7 @@ VALUES
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Artist::find(session, ArtistId{}));
+ EXPECT_FALSE(ArtistInfo::find(session, ArtistInfoId{}));
EXPECT_FALSE(AuthToken::find(session, AuthTokenId{}));
EXPECT_FALSE(Country::find(session, CountryId{}));
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt
index 63d93046..d5dfc9fd 100644
--- a/src/libs/metadata/CMakeLists.txt
+++ b/src/libs/metadata/CMakeLists.txt
@@ -9,10 +9,11 @@ if (BUILD_BENCHMARKS)
endif()
add_library(lmsmetadata STATIC
+ impl/ArtistInfo.cpp
+ impl/AudioFileParser.cpp
impl/AvFormatImageReader.cpp
impl/AvFormatTagReader.cpp
impl/Lyrics.cpp
- impl/AudioFileParser.cpp
impl/PlayList.cpp
impl/TagLibImageReader.cpp
impl/TagLibTagReader.cpp
diff --git a/src/libs/metadata/impl/ArtistInfo.cpp b/src/libs/metadata/impl/ArtistInfo.cpp
new file mode 100644
index 00000000..7fd35a2e
--- /dev/null
+++ b/src/libs/metadata/impl/ArtistInfo.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "metadata/ArtistInfo.hpp"
+
+#include
+#include
+
+#include "core/ILogger.hpp"
+
+namespace lms::metadata
+{
+ std::span getSupportedInfoFileExtensions()
+ {
+ static const std::array fileExtensions{ ".nfo" };
+ return fileExtensions;
+ }
+
+ ArtistInfo parseArtistInfo(std::istream& is)
+ {
+ try
+ {
+ ArtistInfo artistInfo;
+
+ boost::property_tree::ptree root;
+ boost::property_tree::read_xml(is, root);
+
+ const auto& artistNode{ root.get_child("artist") };
+
+ artistInfo.mbid = core::UUID::fromString(artistNode.get_optional("musicBrainzArtistID").value_or(""));
+ artistInfo.name = artistNode.get_optional("name").value_or("");
+ artistInfo.sortName = artistNode.get_optional("sortname").value_or("");
+ artistInfo.type = artistNode.get_optional("type").value_or("");
+ artistInfo.gender = artistNode.get_optional("gender").value_or("");
+ artistInfo.disambiguation = artistNode.get_optional("disambiguation").value_or("");
+ artistInfo.biography = artistNode.get_optional("biography").value_or("");
+
+ return artistInfo;
+ }
+ catch (boost::property_tree::ptree_error& error)
+ {
+ LMS_LOG(RECOMMENDATION, ERROR, "Cannot read artist xml info: " << error.what());
+ throw ArtistInfoParseException{ error.what() };
+ }
+ }
+} // namespace lms::metadata
\ No newline at end of file
diff --git a/src/libs/metadata/include/metadata/ArtistInfo.hpp b/src/libs/metadata/include/metadata/ArtistInfo.hpp
new file mode 100644
index 00000000..5b981af1
--- /dev/null
+++ b/src/libs/metadata/include/metadata/ArtistInfo.hpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "core/UUID.hpp"
+
+#include "metadata/Exception.hpp"
+
+namespace lms::metadata
+{
+ // See:
+ // - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists
+ // - for the definition of some mb fields: https://musicbrainz.org/doc/Artist
+ struct ArtistInfo
+ {
+ std::string name;
+ std::optional mbid;
+ std::string sortName; // mb
+ std::string type; // mb
+ std::string gender; // mb
+ std::string disambiguation; // mb
+ std::string biography;
+ };
+
+ class ArtistInfoParseException : public Exception
+ {
+ public:
+ using Exception::Exception;
+ };
+
+ std::span getSupportedInfoFileExtensions();
+ ArtistInfo parseArtistInfo(std::istream& is);
+} // namespace lms::metadata
diff --git a/src/libs/metadata/test/ArtistInfo.cpp b/src/libs/metadata/test/ArtistInfo.cpp
new file mode 100644
index 00000000..e1e86374
--- /dev/null
+++ b/src/libs/metadata/test/ArtistInfo.cpp
@@ -0,0 +1,79 @@
+
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+
+#include "metadata/ArtistInfo.hpp"
+
+namespace lms::metadata::tests
+{
+ TEST(ArtistInfo, basic)
+ {
+ std::istringstream is{ R"(
+
+ Tim Taylor
+ 38811c52-85e3-4e2e-3319-ab7d9f2cfa5b
+ Taylor, Tim
+ Timothy Taylor
+ DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.
+
+He moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&R for JSE Agency & Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack & Womack, Jungle Brothers, Ice-T, and Guru Josh.
+ https://i.discogs.com/zY8kWeJfDfWgDDJZ44uYARjNEzDLLqRiXk23LUlik-c/rs:fit/g:sm/q:90/h:800/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTAwLmpwZWc.jpeg
+ https://i.discogs.com/7Do2Xbok8HnWJEjcW6b0u9hyYMpNleGY3HRIEhNlxlM/rs:fit/g:sm/q:90/h:387/w:281/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTEyMTg2NTk2MC5q/cGc.jpeg
+ https://i.discogs.com/fOeq1muY2Cu-gAJZGo5yK0AHIS1PJ1rcWqu8p_e3opY/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi05/NDc3LmpwZWc.jpeg
+ https://i.discogs.com/vhMFP7ICq7VyJcZaGim2X0x4nfKNGXkk7U5u153owfs/rs:fit/g:sm/q:90/h:540/w:364/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS05/NzQ3LmpwZWc.jpeg
+ https://i.discogs.com/wsJi9gfDDaoamUjsxFm0R02VAllhW4iaFsCnwVfouO4/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS03/NDYwLmpwZWc.jpeg
+ https://i.discogs.com/fjy0PGAGHsHIXex5HqMDitJI0Yh3MesiPL6ZOyko4bk/rs:fit/g:sm/q:90/h:902/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy02/MTg5LmpwZWc.jpeg
+ https://i.discogs.com/k79VVA9du3LW57naLLjXLlWesxbtygfstnwrHp6Ku84/rs:fit/g:sm/q:90/h:450/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi0x/NDgzLmpwZWc.jpeg
+ https://i.discogs.com/TLjVejJmVWFkQuAhXndIV0Ovt-1GJ5mHE5NWr3MNXGk/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTY1LmpwZWc.jpeg
+ https://i.discogs.com/Y8V1WqvgdSmcIsgC2CAq_VNhfaWTX9gWJi9bQ6c0Vno/rs:fit/g:sm/q:90/h:398/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy0y/Mjc5LmpwZWc.jpeg
+ https://i.discogs.com/OD2sPGIfSZGnrT6JyfKmlO7kuX4ZadJhP-iNaEzbbuE/rs:fit/g:sm/q:90/h:902/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy00/MzcyLmpwZWc.jpeg
+ Acid House / Hardcore / Techno / Acid / Breakbeat / Minimal / Tech House / Tribal
+
+ The Penguin / Scissorhands
+ 1996
+
+
+ The Minneapolis Sessions (2016 Reissue)
+ 1997
+
+
+ Over The Hill
+ 2001
+
+
+ Over The Hill Remixes
+ 2001
+
+
+ Pleasure Unit
+ 2016
+
+)" };
+
+ const ArtistInfo artistInfo{ parseArtistInfo(is) };
+
+ EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b"));
+ EXPECT_EQ(artistInfo.name, "Tim Taylor");
+ ASSERT_EQ(artistInfo.sortName, "Taylor, Tim");
+ ASSERT_EQ(artistInfo.disambiguation, "Timothy Taylor");
+ ASSERT_EQ(artistInfo.biography, "DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.\r\n\r\nHe moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&R for JSE Agency & Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack & Womack, Jungle Brothers, Ice-T, and Guru Josh.");
+ }
+} // namespace lms::metadata::tests
\ No newline at end of file
diff --git a/src/libs/metadata/test/CMakeLists.txt b/src/libs/metadata/test/CMakeLists.txt
index e51d98c9..d957cd3b 100644
--- a/src/libs/metadata/test/CMakeLists.txt
+++ b/src/libs/metadata/test/CMakeLists.txt
@@ -1,6 +1,7 @@
include(GoogleTest)
add_executable(test-metadata
+ ArtistInfo.cpp
Lyrics.cpp
Metadata.cpp
AudioFileParser.cpp
diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt
index b4072e3a..82e16780 100644
--- a/src/libs/services/scanner/CMakeLists.txt
+++ b/src/libs/services/scanner/CMakeLists.txt
@@ -1,4 +1,5 @@
add_library(lmsscanner STATIC
+ impl/scanners/ArtistInfoFileScanner.cpp
impl/scanners/AudioFileScanner.cpp
impl/scanners/ImageFileScanner.cpp
impl/scanners/LyricsFileScanner.cpp
diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp
index 8d8d25ed..0e3fadf0 100644
--- a/src/libs/services/scanner/impl/ScannerService.cpp
+++ b/src/libs/services/scanner/impl/ScannerService.cpp
@@ -29,6 +29,7 @@
#include "database/MediaLibrary.hpp"
#include "database/ScanSettings.hpp"
+#include "scanners/ArtistInfoFileScanner.hpp"
#include "scanners/AudioFileScanner.hpp"
#include "scanners/ImageFileScanner.hpp"
#include "scanners/LyricsFileScanner.hpp"
@@ -347,6 +348,7 @@ namespace lms::scanner
} };
_fileScanners.clear();
+ _fileScanners.emplace_back(std::make_unique(_db));
_fileScanners.emplace_back(std::make_unique(_db, _settings));
_fileScanners.emplace_back(std::make_unique(_db));
_fileScanners.emplace_back(std::make_unique(_db));
diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp
new file mode 100644
index 00000000..25e29335
--- /dev/null
+++ b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "ArtistInfoFileScanner.hpp"
+
+#include
+
+#include "core/ILogger.hpp"
+#include "database/Artist.hpp"
+#include "database/ArtistInfo.hpp"
+#include "database/Db.hpp"
+#include "database/MediaLibrary.hpp"
+#include "database/Session.hpp"
+#include "metadata/ArtistInfo.hpp"
+
+#include "IFileScanOperation.hpp"
+#include "ScanContext.hpp"
+#include "Utils.hpp"
+
+namespace lms::scanner
+{
+ namespace
+ {
+ class ArtistInfoFileScanOperation : public IFileScanOperation
+ {
+ public:
+ ArtistInfoFileScanOperation(const FileToScan& file, db::Db& db)
+ : _file{ file.file }
+ , _mediaLibrary{ file.mediaLibrary }
+ , _db{ db } {}
+ ~ArtistInfoFileScanOperation() override = default;
+ ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete;
+ ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete;
+
+ private:
+ const std::filesystem::path& getFile() const override { return _file; };
+ core::LiteralString getName() const override { return "ScanArtistInfoFile"; }
+ void scan() override;
+ void processResult(ScanContext& context) override;
+
+ std::string getArtistNameFromArtistInfoFilePath();
+
+ const std::filesystem::path _file;
+ const MediaLibraryInfo _mediaLibrary;
+ db::Db& _db;
+
+ std::optional _parsedArtistInfo;
+ };
+
+ void ArtistInfoFileScanOperation::scan()
+ {
+ try
+ {
+ std::ifstream ifs{ _file };
+ if (!ifs)
+ {
+ LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file);
+ return;
+ }
+
+ _parsedArtistInfo = metadata::parseArtistInfo(ifs);
+ if (!_parsedArtistInfo->mbid.has_value())
+ {
+ LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no mbid set");
+ _parsedArtistInfo.reset();
+ }
+ else if (_parsedArtistInfo->name.empty())
+ {
+ LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no name set");
+ _parsedArtistInfo.reset();
+ }
+ }
+ catch (const metadata::ArtistInfoParseException& e)
+ {
+ LMS_LOG(DBUPDATER, ERROR, "Cannot read artist info in file " << _file << ": " << e.what());
+ }
+ }
+
+ void ArtistInfoFileScanOperation::processResult(ScanContext& context)
+ {
+ ScanStats& stats{ context.stats };
+
+ const std::optional fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
+ if (!fileInfo)
+ {
+ stats.skips++;
+ return;
+ }
+
+ db::Session& dbSession{ _db.getTLSSession() };
+ db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, _file) };
+ if (!_parsedArtistInfo)
+ {
+ if (artistInfo)
+ {
+ artistInfo.remove();
+ stats.deletions++;
+ LMS_LOG(DBUPDATER, DEBUG, "Removed artist info file " << _file);
+ }
+ context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadArtistInfoFile);
+ return;
+ }
+
+ const bool added{ !artistInfo };
+ if (!artistInfo)
+ {
+ artistInfo = dbSession.create();
+ artistInfo.modify()->setAbsoluteFilePath(_file);
+ }
+
+ artistInfo.modify()->setLastWriteTime(fileInfo->lastWriteTime);
+ artistInfo.modify()->setType(_parsedArtistInfo->type);
+ artistInfo.modify()->setGender(_parsedArtistInfo->gender);
+ artistInfo.modify()->setDisambiguation(_parsedArtistInfo->disambiguation);
+ artistInfo.modify()->setBiography(_parsedArtistInfo->biography);
+
+ db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
+ artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
+
+ db::Artist::pointer artist{ db::Artist::find(dbSession, *_parsedArtistInfo->mbid) };
+ if (!artist)
+ artist = dbSession.create(_parsedArtistInfo->name, _parsedArtistInfo->mbid);
+
+ artist.modify()->setName(_parsedArtistInfo->name);
+ artist.modify()->setSortName(_parsedArtistInfo->sortName);
+ artistInfo.modify()->setArtist(artist);
+
+ if (added)
+ {
+ LMS_LOG(DBUPDATER, DEBUG, "Added artist info file " << _file);
+ stats.additions++;
+ }
+ else
+ {
+ LMS_LOG(DBUPDATER, DEBUG, "Updated artist info file '" << _file);
+ stats.updates++;
+ }
+ }
+ } // namespace
+
+ ArtistInfoFileScanner::ArtistInfoFileScanner(db::Db& db)
+ : _db{ db }
+ {
+ }
+
+ core::LiteralString ArtistInfoFileScanner::getName() const
+ {
+ return "Artist info scanner ";
+ }
+
+ std::span ArtistInfoFileScanner::getSupportedExtensions() const
+ {
+ return metadata::getSupportedInfoFileExtensions();
+ }
+
+ bool ArtistInfoFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
+ {
+ const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
+ // Should rarely fail as we are currently iterating it
+ if (!lastWriteTime.isValid())
+ {
+ context.stats.skips++;
+ return false;
+ }
+
+ db::Session& dbSession{ _db.getTLSSession() };
+ auto transaction{ dbSession.createReadTransaction() };
+ db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, file.file) };
+ if (!artistInfo)
+ return true;
+
+ return artistInfo->getLastWriteTime() != lastWriteTime;
+ }
+
+ std::unique_ptr ArtistInfoFileScanner::createScanOperation(const FileToScan& fileToScan) const
+ {
+ return std::make_unique(fileToScan, _db);
+ }
+} // namespace lms::scanner
\ No newline at end of file
diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp
new file mode 100644
index 00000000..91b30561
--- /dev/null
+++ b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "IFileScanner.hpp"
+
+namespace lms
+{
+ namespace db
+ {
+ class Db;
+ }
+} // namespace lms
+
+namespace lms::scanner
+{
+ class ArtistInfoFileScanner : public IFileScanner
+ {
+ public:
+ ArtistInfoFileScanner(db::Db& db);
+ ~ArtistInfoFileScanner() override = default;
+ ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete;
+ ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete;
+
+ private:
+ core::LiteralString getName() const override;
+ std::span getSupportedExtensions() const override;
+ bool needsScan(ScanContext& context, const FileToScan& file) const override;
+ std::unique_ptr createScanOperation(const FileToScan& fileToScan) const override;
+
+ db::Db& _db;
+ };
+} // namespace lms::scanner
\ No newline at end of file
diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp
index 181f5efc..ef33f67c 100644
--- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp
+++ b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp
@@ -94,6 +94,7 @@ namespace lms::scanner
{
trackLyrics.remove();
stats.deletions++;
+ LMS_LOG(DBUPDATER, DEBUG, "Removed lyrics file " << _file);
}
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadLyricsFile);
return;
diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp
index 4a7b8c1c..cfef85dd 100644
--- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp
+++ b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp
@@ -98,9 +98,9 @@ namespace lms::scanner
{
playList.remove();
stats.deletions++;
+ LMS_LOG(DBUPDATER, DEBUG, "Removed playlist file " << _file);
}
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadPlayListFile);
- LMS_LOG(DBUPDATER, DEBUG, "Removed playlist file " << _file);
return;
}
diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp
index 8405d9ca..91d06948 100644
--- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp
+++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp
@@ -28,7 +28,9 @@
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
+#include "core/String.hpp"
#include "database/Artist.hpp"
+#include "database/ArtistInfo.hpp"
#include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
@@ -57,14 +59,14 @@ namespace lms::scanner
std::span artistFileNames;
};
- db::Image::pointer findImageInDirectory(SearchImageContext& searchContext, const std::filesystem::path& directoryPath)
+ db::Image::pointer findImageInDirectory(SearchImageContext& searchContext, const std::filesystem::path& directoryPath, std::span fileStemsToSearch)
{
db::Image::pointer image;
const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) };
if (directory) // may not exist for artists that are split on different media libraries
{
- for (std::string_view fileStem : searchContext.artistFileNames)
+ for (std::string_view fileStem : fileStemsToSearch)
{
db::Image::FindParameters params;
params.setDirectory(directory->getId());
@@ -96,6 +98,24 @@ namespace lms::scanner
return image;
}
+ db::Image::pointer searchImageInArtistInfoDirectory(SearchImageContext& searchContext, db::ArtistId artistId)
+ {
+ db::Image::pointer image;
+
+ std::vector fileInfoPaths;
+ db::ArtistInfo::find(searchContext.session, artistId, [&](const db::ArtistInfo::pointer& artistInfo) {
+ fileInfoPaths.push_back(artistInfo->getAbsoluteFilePath());
+
+ if (!image)
+ image = findImageInDirectory(searchContext, artistInfo->getDirectory()->getAbsolutePath(), std::array{ "thumb", "folder" });
+ });
+
+ if (fileInfoPaths.size() > 1)
+ LMS_LOG(DBUPDATER, DEBUG, "Found " << fileInfoPaths.size() << " artist info files for same artist: " << core::stringUtils::joinStrings(fileInfoPaths, ", "));
+
+ return image;
+ }
+
db::Image::pointer searchImageInDirectories(SearchImageContext& searchContext, db::ArtistId artistId)
{
db::Image::pointer image;
@@ -123,7 +143,7 @@ namespace lms::scanner
std::filesystem::path directoryToInspect{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
while (true)
{
- image = findImageInDirectory(searchContext, directoryToInspect);
+ image = findImageInDirectory(searchContext, directoryToInspect, searchContext.artistFileNames);
if (image)
return image;
@@ -140,7 +160,7 @@ namespace lms::scanner
// /someOtherUserConfiguredArtistFile.jpg
for (const std::filesystem::path& releasePath : releasePaths)
{
- image = findImageInDirectory(searchContext, releasePath);
+ image = findImageInDirectory(searchContext, releasePath, searchContext.artistFileNames);
if (image)
return image;
}
@@ -156,6 +176,9 @@ namespace lms::scanner
if (const auto mbid{ artist->getMBID() })
image = getImageFromMbid(searchContext, *mbid);
+ if (!image)
+ image = searchImageInArtistInfoDirectory(searchContext, artist->getId());
+
if (!image)
image = searchImageInDirectories(searchContext, artist->getId());
diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp
index 9fd2c596..817261f3 100644
--- a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp
+++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp
@@ -21,9 +21,9 @@
#include
-#include "ScannerSettings.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
+#include "database/ArtistInfo.hpp"
#include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/PlayListFile.hpp"
@@ -32,6 +32,8 @@
#include "database/TrackLyrics.hpp"
#include "scanners/IFileScanner.hpp"
+#include "ScannerSettings.hpp"
+
namespace lms::scanner
{
namespace
@@ -53,6 +55,7 @@ namespace lms::scanner
context.currentStepStats.totalElems += db::Image::getCount(session);
context.currentStepStats.totalElems += db::TrackLyrics::getExternalLyricsCount(session);
context.currentStepStats.totalElems += db::PlayListFile::getCount(session);
+ context.currentStepStats.totalElems += db::ArtistInfo::getCount(session);
}
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
@@ -67,6 +70,7 @@ namespace lms::scanner
checkForRemovedFiles(context, supportedFileExtensions);
checkForRemovedFiles(context, supportedFileExtensions);
checkForRemovedFiles(context, supportedFileExtensions);
+ checkForRemovedFiles(context, supportedFileExtensions);
}
template
diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp
index b9ff240e..f334bc94 100644
--- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp
+++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp
@@ -31,6 +31,7 @@ namespace lms::scanner
enum class ScanErrorType
{
CannotReadFile,
+ CannotReadArtistInfoFile,
CannotReadAudioFile,
CannotReadImageFile,
CannotReadLyricsFile,
diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp
index 75e9ea45..9f8ba747 100644
--- a/src/libs/subsonic/impl/endpoints/Browsing.cpp
+++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp
@@ -23,6 +23,7 @@
#include "core/Random.hpp"
#include "core/Service.hpp"
#include "database/Artist.hpp"
+#include "database/ArtistInfo.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp"
@@ -544,6 +545,11 @@ namespace lms::api::subsonic
break;
}
}
+
+ ArtistInfo::find(context.dbSession, id, Range{ .offset = 0, .size = 1 }, [&](const ArtistInfo::pointer& artistInfo) {
+ if (!artistInfo->getBiography().empty())
+ artistInfoNode.setAttribute("biography", artistInfo->getBiography());
+ });
}
auto similarArtistsId{ core::Service::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) };
diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp
index f0a45a5e..09b38c1d 100644
--- a/src/lms/ui/admin/ScannerController.cpp
+++ b/src/lms/ui/admin/ScannerController.cpp
@@ -116,6 +116,8 @@ namespace lms::ui
{
case scanner::ScanErrorType::CannotReadFile:
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file");
+ case scanner::ScanErrorType::CannotReadArtistInfoFile:
+ return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-artist-info-file");
case scanner::ScanErrorType::CannotReadAudioFile:
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-audio-file");
case scanner::ScanErrorType::CannotReadImageFile: