From cfa59cb32c977519e5bf9009d76bf4bf42eae448 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 24 Jan 2025 08:37:12 +0100 Subject: [PATCH 01/46] Rollback the full static binary in the docker image as it is currently not working on all archs --- Dockerfile-release | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Dockerfile-release b/Dockerfile-release index 7a7c0485..1574ca2f 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -56,8 +56,8 @@ RUN \ --disable-postproc \ --disable-pixelutils \ --disable-network \ - --disable-shared \ - --enable-static \ + --enable-shared \ + --disable-static \ --enable-gpl \ --enable-small \ --enable-version3 \ @@ -88,7 +88,7 @@ RUN \ RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ - cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DSHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ + cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ make -j$(nproc) && \ make install @@ -110,7 +110,7 @@ RUN \ RUN \ DIR=/tmp/taglib && mkdir -p ${DIR} && cd ${DIR} && \ - CXXFLAGS="-I/usr/include/utf8cpp" cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_SHARED_LIBS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DTRACE_IN_RELEASE=OFF -DWITH_ZLIB=ON && \ + CXXFLAGS="-I/usr/include/utf8cpp" cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_SHARED_LIBS=ON -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DBUILD_TESTING=OFF -DTRACE_IN_RELEASE=OFF -DWITH_ZLIB=ON && \ make -j$(nproc) && \ make install @@ -133,6 +133,11 @@ RUN \ strip --strip-all $bin && \ cp $bin /tmp/fakeroot/bin/; \ done && \ + for lib in ${PREFIX}/lib/*.so; \ + do \ + strip --strip-all $lib; \ + done && \ + cp -r ${PREFIX}/lib /tmp/fakeroot/lib && \ cp -r ${PREFIX}/share /tmp/fakeroot/share && \ rm -rf /tmp/fakeroot/share/doc && \ rm -rf /tmp/fakeroot/share/man From 534df42f9de6873a807b2fcc952fa25ef69441a3 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 24 Jan 2025 17:59:25 +0100 Subject: [PATCH 02/46] Distinguish initial scan to import last time write as added time, ref #601 --- src/libs/core/impl/PartialDateTime.cpp | 16 ++++++++++++++++ .../core/include/core/PartialDateTime.hpp | 6 ++++++ src/libs/database/impl/MediaLibrary.cpp | 7 +++++++ .../include/database/MediaLibrary.hpp | 1 + src/libs/database/test/Track.cpp | 16 ++++++++++++++-- .../scanner/impl/MediaLibraryInfo.hpp | 1 + .../services/scanner/impl/ScannerService.cpp | 7 ++++++- .../impl/scanners/AudioFileScanner.cpp | 19 +++++++------------ 8 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/libs/core/impl/PartialDateTime.cpp b/src/libs/core/impl/PartialDateTime.cpp index 24173de9..bfa849ad 100644 --- a/src/libs/core/impl/PartialDateTime.cpp +++ b/src/libs/core/impl/PartialDateTime.cpp @@ -22,6 +22,10 @@ #include #include +#include +#include +#include + namespace lms::core { PartialDateTime::PartialDateTime(int year) @@ -84,6 +88,18 @@ namespace lms::core return res; } + PartialDateTime PartialDateTime::fromWtDateTime(const Wt::WDateTime& dateTime) + { + return core::PartialDateTime{ + dateTime.date().year(), + static_cast(dateTime.date().month()), + static_cast(dateTime.date().day()), + static_cast(dateTime.time().hour()), + static_cast(dateTime.time().minute()), + static_cast(dateTime.time().second()) + }; + } + std::string PartialDateTime::toISO8601String() const { if (_precision == Precision::Invalid) diff --git a/src/libs/core/include/core/PartialDateTime.hpp b/src/libs/core/include/core/PartialDateTime.hpp index a16a44d8..37b05302 100644 --- a/src/libs/core/include/core/PartialDateTime.hpp +++ b/src/libs/core/include/core/PartialDateTime.hpp @@ -23,6 +23,11 @@ #include #include +namespace Wt +{ + class WDateTime; +} + namespace lms::core { class PartialDateTime @@ -35,6 +40,7 @@ namespace lms::core PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec); static PartialDateTime fromString(std::string_view str); + static PartialDateTime fromWtDateTime(const Wt::WDateTime& dateTime); std::string toISO8601String() const; bool isValid() const { return _precision != Precision::Invalid; } diff --git a/src/libs/database/impl/MediaLibrary.cpp b/src/libs/database/impl/MediaLibrary.cpp index a01b291c..81033c7b 100644 --- a/src/libs/database/impl/MediaLibrary.cpp +++ b/src/libs/database/impl/MediaLibrary.cpp @@ -78,6 +78,13 @@ namespace lms::db }); } + bool MediaLibrary::isEmpty() const + { + assert(session()); + auto query{ session()->query("SELECT EXISTS (SELECT 1 FROM track WHERE media_library_id = ? LIMIT 1) AS is_media_library_empty").bind(getId()) }; + return !utils::fetchQuerySingleResult(query); + } + void MediaLibrary::setPath(const std::filesystem::path& p) { assert(p.is_absolute()); diff --git a/src/libs/database/include/database/MediaLibrary.hpp b/src/libs/database/include/database/MediaLibrary.hpp index 32426058..0649674e 100644 --- a/src/libs/database/include/database/MediaLibrary.hpp +++ b/src/libs/database/include/database/MediaLibrary.hpp @@ -50,6 +50,7 @@ namespace lms::db // getters std::string_view getName() const { return _name; } const std::filesystem::path& getPath() const { return _path; } + bool isEmpty() const; // setters void setName(std::string_view name) { _name = name; } diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index f5ab80fc..67be5338 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -151,19 +151,31 @@ namespace lms::db::tests ScopedMediaLibrary library{ session, "MyLibrary", "/root" }; ScopedMediaLibrary otherLibrary{ session, "OtherLibrary", "/otherRoot" }; + { + auto transaction{ session.createReadTransaction() }; + EXPECT_TRUE(library->isEmpty()); + EXPECT_TRUE(otherLibrary->isEmpty()); + } + { auto transaction{ session.createWriteTransaction() }; track.get().modify()->setMediaLibrary(library.get()); } { - auto transaction{ session.createWriteTransaction() }; + auto transaction{ session.createReadTransaction() }; + EXPECT_FALSE(library->isEmpty()); + EXPECT_TRUE(otherLibrary->isEmpty()); + } + + { + auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setMediaLibrary(library->getId())) }; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); } { - auto transaction{ session.createWriteTransaction() }; + auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setMediaLibrary(otherLibrary->getId())) }; EXPECT_EQ(tracks.results.size(), 0); } diff --git a/src/libs/services/scanner/impl/MediaLibraryInfo.hpp b/src/libs/services/scanner/impl/MediaLibraryInfo.hpp index 5219a1e3..5b62eb83 100644 --- a/src/libs/services/scanner/impl/MediaLibraryInfo.hpp +++ b/src/libs/services/scanner/impl/MediaLibraryInfo.hpp @@ -29,6 +29,7 @@ namespace lms::scanner { db::MediaLibraryId id; std::filesystem::path rootDirectory; + bool firstScan{}; auto operator<=>(const MediaLibraryInfo& other) const = default; }; diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index fc4af817..6da34523 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -395,7 +395,12 @@ namespace lms::scanner newSettings.updatePeriod = scanSettings->getUpdatePeriod(); MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) { - newSettings.mediaLibraries.push_back(MediaLibraryInfo{ .id = mediaLibrary->getId(), .rootDirectory = mediaLibrary->getPath().lexically_normal() }); + MediaLibraryInfo info; + info.firstScan = mediaLibrary->isEmpty(); + info.id = mediaLibrary->getId(); + info.rootDirectory = mediaLibrary->getPath().lexically_normal(); + + newSettings.mediaLibraries.push_back(info); }); { diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index ad226eee..5899e4d4 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -479,19 +479,14 @@ namespace lms::scanner if (!track) { track = dbSession.create(); - track.modify()->setAbsoluteFilePath(_file); - - const core::PartialDateTime addedTime{ - fileInfo->lastWriteTime.date().year(), - static_cast(fileInfo->lastWriteTime.date().month()), - static_cast(fileInfo->lastWriteTime.date().day()), - static_cast(fileInfo->lastWriteTime.time().hour()), - static_cast(fileInfo->lastWriteTime.time().minute()), - static_cast(fileInfo->lastWriteTime.time().second()) - }; - - track.modify()->setAddedTime(addedTime); // may be erased by encodingTime added = true; + + track.modify()->setAbsoluteFilePath(_file); + if (!_parsedTrack->encodingTime.isValid()) + { + const core::PartialDateTime addedTime{ core::PartialDateTime::fromWtDateTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()) }; + track.modify()->setAddedTime(addedTime); // may be erased by encodingTime + } } // Track related data From 8c11bef1b6ce3eae3e9ae2cde6e9db3ac45b56cd Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Jan 2025 14:27:32 +0100 Subject: [PATCH 03/46] Try to set mbids for composers, conductors, etc. if the mbid is found in other fields for the same name --- src/libs/metadata/impl/Parser.cpp | 50 ++++++++++++ src/libs/metadata/test/Parser.cpp | 126 +++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp index 564ce5d6..9cd9b38f 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/Parser.cpp @@ -266,6 +266,54 @@ namespace lms::metadata return std::nullopt; } + + void fillInArtistsWithMbid(std::span artists, std::unordered_map& artistsWithMbid) + { + for (const Artist& artist : artists) + { + if (artist.mbid.has_value()) + { + // there may collisions, we don't want to replace + artistsWithMbid.emplace(artist.name, *artist.mbid); + } + } + } + + void fillInMbids(std::span artists, const std::unordered_map& artistsWithMbid) + { + for (Artist& artist : artists) + { + if (!artist.mbid) + { + const auto it{ artistsWithMbid.find(artist.name) }; + if (it != std::cend(artistsWithMbid)) + artist.mbid = it->second; + } + } + } + + void fillMissingMbids(Track& track) + { + // first pass: collect all artists that have mbids + std::unordered_map artistsWithMbid; + + // For now, mbids can only set in artist and album artist tags + // filling order is important: we estimate track-level artists are more likely + // to be set in other fields than album artists + fillInArtistsWithMbid(track.artists, artistsWithMbid); + if (track.medium && track.medium->release) + fillInArtistsWithMbid(track.medium->release->artists, artistsWithMbid); + + // second pass: fill in all artists that have no mbid set with the same name + fillInMbids(track.conductorArtists, artistsWithMbid); + fillInMbids(track.composerArtists, artistsWithMbid); + fillInMbids(track.lyricistArtists, artistsWithMbid); + fillInMbids(track.mixerArtists, artistsWithMbid); + fillInMbids(track.producerArtists, artistsWithMbid); + fillInMbids(track.remixerArtists, artistsWithMbid); + for (auto& [role, artists] : track.performerArtists) + fillInMbids(artists, artistsWithMbid); + } } // namespace std::unique_ptr createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle) @@ -416,6 +464,8 @@ namespace lms::metadata track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported + fillMissingMbids(track); + // If a file has originalDate but no originalYear, set it if (!track.originalYear) track.originalYear = track.originalDate.getYear(); diff --git a/src/libs/metadata/test/Parser.cpp b/src/libs/metadata/test/Parser.cpp index 7d0a5e14..912d53a5 100644 --- a/src/libs/metadata/test/Parser.cpp +++ b/src/libs/metadata/test/Parser.cpp @@ -584,6 +584,131 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name } + TEST(Parser, MBIDs_fallback) + { + TestTagReader testTags{ + { + { TagType::Artist, { "Artist1", "Artist2" } }, + { TagType::Album, { "MyAlbum" } }, + { TagType::AlbumArtists, { "Artist3", "Artist4" } }, + { TagType::MusicBrainzArtistID, { "6643f584-5edc-45ce-927d-0a4ab25c2673", "481c5912-bf1a-47f7-b03c-d34e49711706" } }, + { TagType::MusicBrainzReleaseArtistID, { "ed42bcaf-e147-4f34-8f26-d74acc97670a", "6fc64a4b-26f5-441f-993c-fd511290233b" } }, + { TagType::Composer, { "Artist1", "Artist3" } }, + { TagType::Conductor, { "Artist1", "Artist3" } }, + { TagType::Lyricist, { "Artist1", "Artist3" } }, + { TagType::Mixer, { "Artist1", "Artist3" } }, + { TagType::Producer, { "Artist1", "Artist3" } }, + { TagType::Remixers, { "Artist1", "Artist3" } }, + } + }; + + testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } }, + { "RoleB", { "Artist2", "Artist4" } } }); + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->artists.size(), 2); + EXPECT_EQ(track->artists[0].name, "Artist1"); + ASSERT_TRUE(track->artists[0].mbid.has_value()); + EXPECT_EQ(track->artists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->artists[1].name, "Artist2"); + ASSERT_TRUE(track->artists[1].mbid.has_value()); + EXPECT_EQ(track->artists[1].mbid.value(), core::UUID::fromString("481c5912-bf1a-47f7-b03c-d34e49711706")); + + ASSERT_TRUE(track->medium.has_value()); + ASSERT_TRUE(track->medium->release.has_value()); + ASSERT_EQ(track->medium->release->artists.size(), 2); + EXPECT_EQ(track->medium->release->artists[0].name, "Artist3"); + ASSERT_TRUE(track->medium->release->artists[0].mbid.has_value()); + EXPECT_EQ(track->medium->release->artists[0].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + EXPECT_EQ(track->medium->release->artists[1].name, "Artist4"); + ASSERT_TRUE(track->medium->release->artists[1].mbid.has_value()); + EXPECT_EQ(track->medium->release->artists[1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b")); + + ASSERT_EQ(track->composerArtists.size(), 2); + EXPECT_EQ(track->composerArtists[0].name, "Artist1"); + ASSERT_TRUE(track->composerArtists[0].mbid.has_value()); + EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->composerArtists[1].name, "Artist3"); + ASSERT_TRUE(track->composerArtists[1].mbid.has_value()); + EXPECT_EQ(track->composerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + + ASSERT_EQ(track->conductorArtists.size(), 2); + EXPECT_EQ(track->conductorArtists[0].name, "Artist1"); + ASSERT_TRUE(track->conductorArtists[0].mbid.has_value()); + EXPECT_EQ(track->conductorArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->conductorArtists[1].name, "Artist3"); + ASSERT_TRUE(track->conductorArtists[1].mbid.has_value()); + EXPECT_EQ(track->conductorArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + + ASSERT_EQ(track->lyricistArtists.size(), 2); + EXPECT_EQ(track->lyricistArtists[0].name, "Artist1"); + ASSERT_TRUE(track->lyricistArtists[0].mbid.has_value()); + EXPECT_EQ(track->lyricistArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->lyricistArtists[1].name, "Artist3"); + ASSERT_TRUE(track->lyricistArtists[1].mbid.has_value()); + EXPECT_EQ(track->lyricistArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + + ASSERT_EQ(track->mixerArtists.size(), 2); + EXPECT_EQ(track->mixerArtists[0].name, "Artist1"); + ASSERT_TRUE(track->mixerArtists[0].mbid.has_value()); + EXPECT_EQ(track->mixerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->mixerArtists[1].name, "Artist3"); + ASSERT_TRUE(track->mixerArtists[1].mbid.has_value()); + EXPECT_EQ(track->mixerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + + ASSERT_EQ(track->producerArtists.size(), 2); + EXPECT_EQ(track->producerArtists[0].name, "Artist1"); + ASSERT_TRUE(track->producerArtists[0].mbid.has_value()); + EXPECT_EQ(track->producerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->producerArtists[1].name, "Artist3"); + ASSERT_TRUE(track->producerArtists[1].mbid.has_value()); + EXPECT_EQ(track->producerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + + ASSERT_EQ(track->remixerArtists.size(), 2); + EXPECT_EQ(track->remixerArtists[0].name, "Artist1"); + ASSERT_TRUE(track->remixerArtists[0].mbid.has_value()); + EXPECT_EQ(track->remixerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->remixerArtists[1].name, "Artist3"); + ASSERT_TRUE(track->remixerArtists[1].mbid.has_value()); + EXPECT_EQ(track->remixerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + + ASSERT_TRUE(track->performerArtists.contains("Rolea")); + ASSERT_EQ(track->performerArtists["Rolea"].size(), 2); + EXPECT_EQ(track->performerArtists["Rolea"][0].name, "Artist1"); + ASSERT_TRUE(track->performerArtists["Rolea"][0].mbid.has_value()); + EXPECT_EQ(track->performerArtists["Rolea"][0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + EXPECT_EQ(track->performerArtists["Rolea"][1].name, "Artist3"); + ASSERT_TRUE(track->performerArtists["Rolea"][1].mbid.has_value()); + EXPECT_EQ(track->performerArtists["Rolea"][1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a")); + ASSERT_EQ(track->performerArtists["Roleb"].size(), 2); + EXPECT_EQ(track->performerArtists["Roleb"][0].name, "Artist2"); + ASSERT_TRUE(track->performerArtists["Roleb"][0].mbid.has_value()); + EXPECT_EQ(track->performerArtists["Roleb"][0].mbid.value(), core::UUID::fromString("481c5912-bf1a-47f7-b03c-d34e49711706")); + EXPECT_EQ(track->performerArtists["Roleb"][1].name, "Artist4"); + ASSERT_TRUE(track->performerArtists["Roleb"][1].mbid.has_value()); + EXPECT_EQ(track->performerArtists["Roleb"][1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b")); + } + + TEST(Parser, MBIDs_fallback_priority) + { + const TestTagReader testTags{ + { + { TagType::Artist, { "Artist1" } }, + { TagType::Album, { "MyAlbum" } }, + { TagType::AlbumArtists, { "Artist1" } }, + { TagType::MusicBrainzArtistID, { "6643f584-5edc-45ce-927d-0a4ab25c2673" } }, + { TagType::MusicBrainzReleaseArtistID, { "ed42bcaf-e147-4f34-8f26-d74acc97670a" } }, + { TagType::Composer, { "Artist1" } }, + } + }; + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->composerArtists.size(), 1); + EXPECT_EQ(track->composerArtists[0].name, "Artist1"); + ASSERT_TRUE(track->composerArtists[0].mbid.has_value()); + EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); + } + TEST(Parser, advisory) { auto doTest = [](std::string_view value, std::optional expectedValue) { @@ -657,5 +782,4 @@ namespace lms::metadata doTest("2020/01", core::PartialDateTime{ 2020, 1 }); doTest("2020", core::PartialDateTime{ 2020 }); } - } // namespace lms::metadata From e553e840d3a94088f2d765efcc35ab6e2fd5d8d0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 25 Jan 2025 14:44:49 +0100 Subject: [PATCH 04/46] Subsonic: use file added info for 'created' fields, ref #601 --- src/libs/database/impl/Release.cpp | 9 ++- .../database/include/database/Release.hpp | 1 + src/libs/database/test/Release.cpp | 62 +++++++++++++++++++ src/libs/subsonic/impl/responses/Album.cpp | 2 +- src/libs/subsonic/impl/responses/Song.cpp | 2 +- 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 9cfb04e9..b27dd380 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -708,7 +708,14 @@ namespace lms::db { assert(session()); - return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t").where("t.release_id = ?").bind(getId())); + return utils::fetchQuerySingleResult(session()->query("SELECT MAX(file_last_write) FROM track t").where("t.release_id = ?").bind(getId())); + } + + core::PartialDateTime Release::getAddedTime() const + { + assert(session()); + + return utils::fetchQuerySingleResult(session()->query("SELECT MAX(file_added) FROM track t").where("t.release_id = ?").bind(getId())); } std::vector> Release::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 699c0236..d07d300c 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -244,6 +244,7 @@ namespace lms::db std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) std::vector getDiscs() const; std::chrono::milliseconds getDuration() const; + core::PartialDateTime getAddedTime() const; Wt::WDateTime getLastWritten() const; std::string_view getArtistDisplayName() const { return _artistDisplayName; } bool isCompilation() const { return _isCompilation; } diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 21416541..dce0a8d8 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1201,4 +1201,66 @@ namespace lms::db::tests EXPECT_EQ(releases.results[3], releaseC.getId()); } } + + TEST_F(DatabaseFixture, Release_LastWritten) + { + ScopedRelease release{ session, "relA" }; + + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + + { + auto transaction{ session.createReadTransaction() }; + + Wt::WDateTime lastWritten{ release.get()->getLastWritten() }; + EXPECT_FALSE(lastWritten.isValid()); + } + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + track2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); + track1.get().modify()->setRelease(release.get()); + track2.get().modify()->setRelease(release.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + Wt::WDateTime lastWritten{ release.get()->getLastWritten() }; + ASSERT_TRUE(lastWritten.isValid()); + EXPECT_EQ(lastWritten, track2.get()->getLastWriteTime()); + } + } + + TEST_F(DatabaseFixture, Release_AddedTime) + { + ScopedRelease release{ session, "relA" }; + + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + + { + auto transaction{ session.createReadTransaction() }; + + core::PartialDateTime addedTime{ release.get()->getAddedTime() }; + EXPECT_FALSE(addedTime.isValid()); + } + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); + track2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); + track1.get().modify()->setRelease(release.get()); + track2.get().modify()->setRelease(release.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + core::PartialDateTime addedTime{ release.get()->getAddedTime() }; + ASSERT_TRUE(addedTime.isValid()); + EXPECT_EQ(addedTime, track2.get()->getAddedTime()); + } + } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 051ff869..6b019879 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -85,7 +85,7 @@ namespace lms::api::subsonic albumNode.setAttribute("isDir", true); } - albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten())); + albumNode.setAttribute("created", release->getAddedTime().toISO8601String()); if (const auto image{ release->getImage() }) { const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index bdc8b3a6..6149589f 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -145,7 +145,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000)); trackResponse.setAttribute("type", "music"); - trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten())); + trackResponse.setAttribute("created", track->getAddedTime().toISO8601String()); trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension())); if (const auto rating{ core::Service::get()->getRating(context.user->getId(), track->getId()) }) trackResponse.setAttribute("userRating", *rating); From 814eaf0a3e6ed5cfc803dee85a4302ac75a30b0d Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Jan 2025 15:08:31 +0100 Subject: [PATCH 05/46] Switched the file added info back to a full date time, ref #601 --- src/libs/core/impl/String.cpp | 18 ++++++++++--- .../core/include/core/PartialDateTime.hpp | 27 +++++++++++-------- src/libs/core/test/String.cpp | 27 ++++++++++++++++--- src/libs/database/impl/Release.cpp | 6 ++--- .../database/include/database/Release.hpp | 4 +-- src/libs/database/include/database/Track.hpp | 8 +++--- src/libs/database/test/Artist.cpp | 10 +++---- src/libs/database/test/Release.cpp | 22 +++++++-------- src/libs/database/test/Track.cpp | 6 ++--- .../impl/scanners/AudioFileScanner.cpp | 19 ++++++++----- src/libs/subsonic/impl/responses/Album.cpp | 2 +- src/libs/subsonic/impl/responses/Song.cpp | 2 +- 12 files changed, 96 insertions(+), 55 deletions(-) diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp index 3be9817b..fd437c02 100644 --- a/src/libs/core/impl/String.cpp +++ b/src/libs/core/impl/String.cpp @@ -466,14 +466,24 @@ namespace lms::core::stringUtils std::string toISO8601String(const Wt::WDateTime& dateTime) { - // assume UTC - return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8(); + if (dateTime.isValid()) + { + // assume UTC + return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8(); + } + + return ""; } std::string toISO8601String(const Wt::WDate& date) { - // assume UTC - return date.toString("yyyy-MM-dd").toUTF8(); + if (date.isValid()) + { + // assume UTC + return date.toString("yyyy-MM-dd").toUTF8(); + } + + return ""; } Wt::WDateTime fromISO8601String(std::string_view dateTime) diff --git a/src/libs/core/include/core/PartialDateTime.hpp b/src/libs/core/include/core/PartialDateTime.hpp index 37b05302..05947c2e 100644 --- a/src/libs/core/include/core/PartialDateTime.hpp +++ b/src/libs/core/include/core/PartialDateTime.hpp @@ -43,11 +43,26 @@ namespace lms::core static PartialDateTime fromWtDateTime(const Wt::WDateTime& dateTime); std::string toISO8601String() const; - bool isValid() const { return _precision != Precision::Invalid; } + constexpr bool isValid() const { return _precision != Precision::Invalid; } + + enum class Precision : std::uint8_t + { + Invalid, + Year, + Month, + Day, + Hour, + Min, + Sec, + }; + constexpr Precision getPrecision() const { return _precision; } constexpr std::optional getYear() const { return (_precision >= Precision::Year ? std::make_optional(_year) : std::nullopt); } constexpr std::optional getMonth() const { return (_precision >= Precision::Month ? std::make_optional(_month) : std::nullopt); } constexpr std::optional getDay() const { return (_precision >= Precision::Day ? std::make_optional(_day) : std::nullopt); } + constexpr std::optional getHour() const { return (_precision >= Precision::Hour ? std::make_optional(_hour) : std::nullopt); } + constexpr std::optional getMin() const { return (_precision >= Precision::Min ? std::make_optional(_min) : std::nullopt); } + constexpr std::optional getSec() const { return (_precision >= Precision::Sec ? std::make_optional(_sec) : std::nullopt); } constexpr auto operator<=>(const PartialDateTime& other) const = default; @@ -60,16 +75,6 @@ namespace lms::core std::uint8_t _hour{}; // 0 to 23 std::uint8_t _min{}; // 0 to 59 std::uint8_t _sec{}; // 0 to 59 - enum class Precision : std::uint8_t - { - Invalid, - Year, - Month, - Day, - Hour, - Min, - Sec, - }; Precision _precision{ Precision::Invalid }; }; } // namespace lms::core \ No newline at end of file diff --git a/src/libs/core/test/String.cpp b/src/libs/core/test/String.cpp index a1fa628e..b137854b 100644 --- a/src/libs/core/test/String.cpp +++ b/src/libs/core/test/String.cpp @@ -301,14 +301,33 @@ namespace lms::core::stringUtils::tests TEST(Stringutils, DateToString) { - const Wt::WDate date{ 2020, 01, 03 }; - EXPECT_EQ(toISO8601String(date), "2020-01-03"); + { + const Wt::WDate date{ 2020, 01, 03 }; + EXPECT_EQ(toISO8601String(date), "2020-01-03"); + } + + { + const Wt::WDate date; + EXPECT_EQ(toISO8601String(date), ""); + } } TEST(Stringutils, DateTimeToString) { - const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } }; - EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075"); + { + const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } }; + EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075"); + } + + { + const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 } }; + EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T00:00:00.000"); + } + + { + const Wt::WDateTime dateTime; + EXPECT_EQ(toISO8601String(dateTime), ""); + } } TEST(Stringutils, DateTimeFromString) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index b27dd380..89f7a9ed 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -704,18 +704,18 @@ namespace lms::db return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t").where("t.release_id = ?").bind(getId())); } - Wt::WDateTime Release::getLastWritten() const + Wt::WDateTime Release::getLastWrittenTime() const { assert(session()); return utils::fetchQuerySingleResult(session()->query("SELECT MAX(file_last_write) FROM track t").where("t.release_id = ?").bind(getId())); } - core::PartialDateTime Release::getAddedTime() const + Wt::WDateTime Release::getAddedTime() const { assert(session()); - return utils::fetchQuerySingleResult(session()->query("SELECT MAX(file_added) FROM track t").where("t.release_id = ?").bind(getId())); + return utils::fetchQuerySingleResult(session()->query("SELECT MAX(file_added) FROM track t").where("t.release_id = ?").bind(getId())); } std::vector> Release::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index d07d300c..22050468 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -244,8 +244,8 @@ namespace lms::db std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) std::vector getDiscs() const; std::chrono::milliseconds getDuration() const; - core::PartialDateTime getAddedTime() const; - Wt::WDateTime getLastWritten() const; + Wt::WDateTime getAddedTime() const; + Wt::WDateTime getLastWrittenTime() const; std::string_view getArtistDisplayName() const { return _artistDisplayName; } bool isCompilation() const { return _isCompilation; } std::size_t getTrackCount() const; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index f42089d3..02cca4d0 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -223,8 +223,8 @@ namespace lms::db void setAbsoluteFilePath(const std::filesystem::path& filePath); void setRelativeFilePath(const std::filesystem::path& filePath); void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } - void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } - void setAddedTime(core::PartialDateTime time) { _fileAdded = time; } + void setLastWriteTime(const Wt::WDateTime& time) { _fileLastWrite = time; } + void setAddedTime(const Wt::WDateTime& time) { _fileAdded = time; } void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } void setBitsPerSample(std::size_t bitsPerSample) { _bitsPerSample = bitsPerSample; } void setDuration(std::chrono::milliseconds duration) { _duration = duration; } @@ -272,7 +272,7 @@ namespace lms::db const core::PartialDateTime& getOriginalDate() const { return _originalDate; } std::optional getOriginalYear() const; const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } - const core::PartialDateTime& getAddedTime() const { return _fileAdded; } + const Wt::WDateTime& getAddedTime() const { return _fileAdded; } bool hasCover() const { return _hasCover; } bool hasLyrics() const; std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } @@ -367,7 +367,7 @@ namespace lms::db std::filesystem::path _fileName; long long _fileSize{}; Wt::WDateTime _fileLastWrite; - core::PartialDateTime _fileAdded; + Wt::WDateTime _fileAdded; bool _hasCover{}; std::string _trackMBID; std::string _recordingMBID; diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 0ee1c8da..04395c36 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -751,11 +751,11 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - trackA1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); - trackB1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 }); - trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); - trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 3 }); - trackA2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 4 }); + trackA1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + trackB1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } }); + trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); + trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); + trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index dce0a8d8..f90e7a61 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1138,11 +1138,11 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - trackA1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); - trackB1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 }); - trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); - trackD1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 3 }); - trackA2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 4 }); + trackA1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + trackB1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } }); + trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); + trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); + trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); trackA1.get().modify()->setRelease(releaseA.get()); trackA2.get().modify()->setRelease(releaseA.get()); @@ -1212,7 +1212,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - Wt::WDateTime lastWritten{ release.get()->getLastWritten() }; + Wt::WDateTime lastWritten{ release.get()->getLastWrittenTime() }; EXPECT_FALSE(lastWritten.isValid()); } @@ -1227,7 +1227,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - Wt::WDateTime lastWritten{ release.get()->getLastWritten() }; + Wt::WDateTime lastWritten{ release.get()->getLastWrittenTime() }; ASSERT_TRUE(lastWritten.isValid()); EXPECT_EQ(lastWritten, track2.get()->getLastWriteTime()); } @@ -1243,14 +1243,14 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - core::PartialDateTime addedTime{ release.get()->getAddedTime() }; + const Wt::WDateTime addedTime{ release.get()->getAddedTime() }; EXPECT_FALSE(addedTime.isValid()); } { auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); - track2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); + track1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + track2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); track1.get().modify()->setRelease(release.get()); track2.get().modify()->setRelease(release.get()); } @@ -1258,7 +1258,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - core::PartialDateTime addedTime{ release.get()->getAddedTime() }; + const Wt::WDateTime addedTime{ release.get()->getAddedTime() }; ASSERT_TRUE(addedTime.isValid()); EXPECT_EQ(addedTime, track2.get()->getAddedTime()); } diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 67be5338..392c2df0 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -399,9 +399,9 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2 }); - track2.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 1 }); - track4.get().modify()->setAddedTime(core::PartialDateTime{ 2021, 1, 2, 15, 36, 24 }); + track1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 } }); + track2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 1 } }); + track4.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 2 }, Wt::WTime{ 15, 36, 24 } }); } { diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index 5899e4d4..fa814041 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -482,11 +482,7 @@ namespace lms::scanner added = true; track.modify()->setAbsoluteFilePath(_file); - if (!_parsedTrack->encodingTime.isValid()) - { - const core::PartialDateTime addedTime{ core::PartialDateTime::fromWtDateTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()) }; - track.modify()->setAddedTime(addedTime); // may be erased by encodingTime - } + track.modify()->setAddedTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime } // Track related data @@ -504,7 +500,18 @@ namespace lms::scanner track.modify()->setLastWriteTime(fileInfo->lastWriteTime); if (_parsedTrack->encodingTime.isValid()) - track.modify()->setAddedTime(_parsedTrack->encodingTime); + { + const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime }; + Wt::WDate date; + Wt::WTime time; + if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day) + date = Wt::WDate{ *encodingTime.getYear(), *encodingTime.getMonth(), *encodingTime.getDay() }; + if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Sec) + time = Wt::WTime{ *encodingTime.getHour(), *encodingTime.getMin(), *encodingTime.getSec() }; + + if (date.isValid()) + track.modify()->setAddedTime(Wt::WDateTime{ date, time }); + } db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this track.modify()->setMediaLibrary(mediaLibrary); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 6b019879..7132e011 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -85,7 +85,7 @@ namespace lms::api::subsonic albumNode.setAttribute("isDir", true); } - albumNode.setAttribute("created", release->getAddedTime().toISO8601String()); + albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime())); if (const auto image{ release->getImage() }) { const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 6149589f..ee1cc5c2 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -145,7 +145,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000)); trackResponse.setAttribute("type", "music"); - trackResponse.setAttribute("created", track->getAddedTime().toISO8601String()); + trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getAddedTime())); trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension())); if (const auto rating{ core::Service::get()->getRating(context.user->getId(), track->getId()) }) trackResponse.setAttribute("userRating", *rating); From aab5dbee2b1992adc85c42cbbc669ca15e182ed2 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 26 Jan 2025 19:01:59 +0100 Subject: [PATCH 06/46] Fixed a badly constructed Wt::WdateTime from partial date times, ref #601 --- src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index fa814041..f79f1094 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -510,7 +510,7 @@ namespace lms::scanner time = Wt::WTime{ *encodingTime.getHour(), *encodingTime.getMin(), *encodingTime.getSec() }; if (date.isValid()) - track.modify()->setAddedTime(Wt::WDateTime{ date, time }); + track.modify()->setAddedTime(time.isValid() ? Wt::WDateTime{ date, time } : Wt::WDateTime{ date }); } db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this From 22e30322c46a6f33339e0f442a925c723c3b484e Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 27 Jan 2025 08:39:49 +0100 Subject: [PATCH 07/46] Added explicitStatus in docs --- SUBSONIC.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SUBSONIC.md b/SUBSONIC.md index 9948280e..0ef54e21 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -25,6 +25,7 @@ The following extra fields are implemented: * `artists` * `discTitles`: discs with no subtitle are omitted * `displayArtist` + * `explicitStatus` * `genres` * `isCompilation` * `played` @@ -44,6 +45,7 @@ The following extra fields are implemented: * `contributors` * `displayAlbumArtist` * `displayArtist` + * `explicitStatus` * `genres` * `mediaType` * `moods` From ce5fb17722cbc9978eaed2bcf6413b976154cb02 Mon Sep 17 00:00:00 2001 From: Tuukka Ojala Date: Sun, 2 Feb 2025 13:46:24 +0200 Subject: [PATCH 08/46] Add keyboard shortcuts for seeking - Seeks by 5 seconds back / forward - Note: Pressing these shortcuts quickly in succession while transcoding is active will throw an error since the previous request hasn't had time to finish before the next one. This bug also happens with the seek bar so might perhaps be best addressed in a separate pr / commit. --- README.md | 2 ++ docroot/js/mediaplayer.js | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6dbca37..35cc1679 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ _LMS_ supports lyrics in `lrc` files, `txt` files, and embedded track metadata. * Next track: Ctrl + Right * Decrease volume: Ctrl + Down * Increase volume: Ctrl + Up +* Seek back by 5 seconds: Ctrl + Shift + Left +* Seek forward by 5 seconds: Ctrl + Shift + Right ## Installation diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index d926c5aa..f1857145 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -18,6 +18,9 @@ Object.freeze(Mode); // How much to increase / decrease volume when adjusting it with keyboard shortcuts const volumeStepAmount = 0.05; +// How much to seek back / forward (in seconds) with keyboard shortcuts +const seekAmount = 5; + LMS.mediaplayer = function () { let _root = {}; let _elems = {}; @@ -245,6 +248,18 @@ LMS.mediaplayer = function () { _updateMediaSessionState(); } + let _seekBack = function() { + let currentPosition = _offset + _elems.audio.currentTime; + let newPosition = currentPosition - seekAmount; + _seekTo(Math.max(newPosition, 0)); + } + + let _seekForward = function() { + let currentPosition = _offset + _elems.audio.currentTime; + let newPosition = currentPosition + seekAmount; + _seekTo(Math.min(newPosition, _duration)); + } + let _updateMediaSessionState = function() { if ("mediaSession" in navigator) { navigator.mediaSession.setPositionState({ @@ -346,11 +361,11 @@ LMS.mediaplayer = function () { _playPause(); handled = true; } - else if (event.ctrlKey && event.keyCode == 37) { + else if (event.ctrlKey && !event.shiftKey && event.keyCode == 37) { _playPrevious(); handled = true; } - else if (event.ctrlKey && event.keyCode == 39) { + else if (event.ctrlKey && !event.shiftKey && event.keyCode == 39) { _playNext(); handled = true; } @@ -362,6 +377,14 @@ LMS.mediaplayer = function () { _stepVolumeUp(); handled = true; } + else if (event.ctrlKey && event.shiftKey && event.keyCode == 37) { + _seekBack(); + handled = true; + } + else if (event.ctrlKey && event.shiftKey && event.keyCode == 39) { + _seekForward(); + handled = true; + } if (handled) event.preventDefault(); From 9f90dee032b5c657b3ca9ede7d5352bc3b0cff5e Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 2 Feb 2025 14:11:49 +0100 Subject: [PATCH 09/46] Removed useless dependency --- Dockerfile-release | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile-release b/Dockerfile-release index 1574ca2f..2b11fed7 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -80,7 +80,7 @@ RUN \ make distclean # WT -ARG WT_VERSION=4.11.1 +ARG WT_VERSION=4.11.2 RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \ @@ -88,7 +88,7 @@ RUN \ RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ - cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ + cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_CXX_STANDARD=17 -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ make -j$(nproc) && \ make install @@ -164,7 +164,6 @@ ARG RUNTIME_PACKAGES=" \ boost-filesystem \ boost-iostreams \ boost-program_options \ - boost-system \ boost-thread \ libarchive \ libconfig++ \ From 2e2cf2049a90f3186f7ac377316d5faf748f223d Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 27 Jan 2025 08:39:49 +0100 Subject: [PATCH 10/46] Added explicitStatus in docs --- SUBSONIC.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SUBSONIC.md b/SUBSONIC.md index 9948280e..0ef54e21 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -25,6 +25,7 @@ The following extra fields are implemented: * `artists` * `discTitles`: discs with no subtitle are omitted * `displayArtist` + * `explicitStatus` * `genres` * `isCompilation` * `played` @@ -44,6 +45,7 @@ The following extra fields are implemented: * `contributors` * `displayAlbumArtist` * `displayArtist` + * `explicitStatus` * `genres` * `mediaType` * `moods` From fdea16adb61fe10510a862ee6e13d6baaa6640d6 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 2 Feb 2025 14:11:49 +0100 Subject: [PATCH 11/46] Removed useless dependency --- Dockerfile-release | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile-release b/Dockerfile-release index 1574ca2f..2b11fed7 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -80,7 +80,7 @@ RUN \ make distclean # WT -ARG WT_VERSION=4.11.1 +ARG WT_VERSION=4.11.2 RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \ @@ -88,7 +88,7 @@ RUN \ RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ - cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ + cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_CXX_STANDARD=17 -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \ make -j$(nproc) && \ make install @@ -164,7 +164,6 @@ ARG RUNTIME_PACKAGES=" \ boost-filesystem \ boost-iostreams \ boost-program_options \ - boost-system \ boost-thread \ libarchive \ libconfig++ \ From df934257487df651afb77fcd423dae958109ad48 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 7 Feb 2025 17:01:59 +0100 Subject: [PATCH 12/46] json serialize seems to allow corrupted urf8 string, add a check when reading back to avoid crashing, ref #607 --- src/libs/database/impl/PlayListFile.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/database/impl/PlayListFile.cpp b/src/libs/database/impl/PlayListFile.cpp index 20df473a..776b1af4 100644 --- a/src/libs/database/impl/PlayListFile.cpp +++ b/src/libs/database/impl/PlayListFile.cpp @@ -81,6 +81,8 @@ namespace lms::db std::vector PlayListFile::getFiles() const { std::vector files; + + try { Wt::Json::Object root; Wt::Json::parse(_entries, root); @@ -90,6 +92,10 @@ namespace lms::db for (const Wt::Json::Value& file : filesArray) files.push_back(static_cast(file.toString())); } + catch (const Wt::Json::ParseError& e) + { + LMS_LOG(DB, ERROR, "Cannot parse files saved in " << _absoluteFilePath << ": corrupted data?"); + } return files; } From 4fe6edb87fcf39610dfe29190bc57552276578c8 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 5 Feb 2025 20:01:30 +0100 Subject: [PATCH 13/46] Added release comment support, ref #568 --- src/libs/database/impl/Migration.cpp | 12 +++++- .../database/include/database/Release.hpp | 4 ++ src/libs/metadata/impl/AvFormatTagReader.cpp | 1 + src/libs/metadata/impl/ITagReader.hpp | 1 + src/libs/metadata/impl/Parser.cpp | 1 + src/libs/metadata/impl/TagLibTagReader.cpp | 1 + src/libs/metadata/include/metadata/Types.hpp | 1 + src/libs/metadata/test/Parser.cpp | 43 ++++++++++--------- .../impl/scanners/AudioFileScanner.cpp | 2 + src/tools/metadata/LmsMetadata.cpp | 9 ++-- 10 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index c5be754e..704d3e42 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{ 80 }; + static constexpr Version LMS_DATABASE_VERSION{ 81 }; } VersionInfo::VersionInfo() @@ -1063,6 +1063,15 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV80(Session& session) + { + // Add release comment support + utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD COLUMN comment TEXT NOT NULL DEFAULT ''"); + + // 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)" }; @@ -1119,6 +1128,7 @@ FROM tracklist)"); { 77, migrateFromV77 }, { 78, migrateFromV78 }, { 79, migrateFromV79 }, + { 80, migrateFromV80 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 22050468..50e65b14 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -255,6 +255,7 @@ namespace lms::db void visitLabels(const std::function& _func) const; core::EnumSet getAdvisories() const; std::string_view getBarcode() const { return _barcode; } + std::string_view getComment() const { return _comment; } ObjectPtr getImage() const; // Setters @@ -270,6 +271,7 @@ namespace lms::db void addLabel(ObjectPtr