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: