diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index 7e22c4bec..f5b6efe97 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(lmscore SHARED impl/IOContextRunner.cpp impl/Logger.cpp impl/NetAddress.cpp + impl/PartialDateTime.cpp impl/Path.cpp impl/Random.cpp impl/RecursiveSharedMutex.cpp diff --git a/src/libs/core/impl/PartialDateTime.cpp b/src/libs/core/impl/PartialDateTime.cpp new file mode 100644 index 000000000..a29439442 --- /dev/null +++ b/src/libs/core/impl/PartialDateTime.cpp @@ -0,0 +1,155 @@ +/* + * 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 "core/PartialDateTime.hpp" + +#include +#include +#include + +namespace lms::core +{ + PartialDateTime::PartialDateTime(int year) + : _year{ static_cast(year) } + , _precision{ Precision::Year } + { + } + + PartialDateTime::PartialDateTime(int year, unsigned month) + : _year{ static_cast(year) } + , _month{ static_cast(month) } + , _precision{ Precision::Month } + { + } + + PartialDateTime::PartialDateTime(int year, unsigned month, unsigned day) + : _year{ static_cast(year) } + , _month{ static_cast(month) } + , _day{ static_cast(day) } + , _precision{ Precision::Day } + { + } + + PartialDateTime::PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec) + : _year{ static_cast(year) } + , _month{ static_cast(month) } + , _day{ static_cast(day) } + , _hour{ static_cast(hour) } + , _min{ static_cast(min) } + , _sec{ static_cast(sec) } + , _precision{ Precision::Sec } + { + } + + PartialDateTime PartialDateTime::fromString(std::string_view str) + { + PartialDateTime res; + + const std::string dateTimeStr{ str }; + + static constexpr const char* formats[]{ + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M:%S", + }; + + for (const char* format : formats) + { + PartialDateTime candidate; + + std::tm tm{}; + tm.tm_year = std::numeric_limits::min(); + tm.tm_mon = std::numeric_limits::min(); + tm.tm_mday = std::numeric_limits::min(); + tm.tm_hour = std::numeric_limits::min(); + tm.tm_min = std::numeric_limits::min(); + tm.tm_sec = std::numeric_limits::min(); + + std::istringstream ss{ dateTimeStr }; + ss >> std::get_time(&tm, format); + if (ss.fail()) + continue; + + if (tm.tm_sec != std::numeric_limits::min()) + { + candidate._sec = tm.tm_sec; + candidate._precision = Precision::Sec; + } + if (tm.tm_min != std::numeric_limits::min()) + { + candidate._min = tm.tm_min; + if (candidate._precision == Precision::Invalid) + candidate._precision = Precision::Min; + } + if (tm.tm_hour != std::numeric_limits::min()) + { + candidate._hour = tm.tm_hour; + if (candidate._precision == Precision::Invalid) + candidate._precision = Precision::Hour; + } + if (tm.tm_mday != std::numeric_limits::min()) + { + candidate._day = tm.tm_mday; + if (candidate._precision == Precision::Invalid) + candidate._precision = Precision::Day; + } + if (tm.tm_mon != std::numeric_limits::min()) + { + candidate._month = tm.tm_mon + 1; // tm.tm_mon is [0, 11] + if (candidate._precision == Precision::Invalid) + candidate._precision = Precision::Month; + } + if (tm.tm_year != std::numeric_limits::min()) + { + candidate._year = tm.tm_year + 1900; // tm.tm_year is years since 1900 + if (candidate._precision == Precision::Invalid) + candidate._precision = Precision::Year; + } + + if (candidate > res) + res = candidate; + + if (res._precision == Precision::Sec) + break; + } + + return res; + } + + std::string PartialDateTime::toISO8601String() const + { + if (_precision == Precision::Invalid) + return ""; + + std::ostringstream ss; + + ss << std::setfill('0') << std::setw(4) << _year; + if (_precision >= Precision::Month) + ss << "-" << std::setw(2) << static_cast(_month); + if (_precision >= Precision::Day) + ss << "-" << std::setw(2) << static_cast(_day); + if (_precision >= Precision::Hour) + ss << 'T' << std::setw(2) << static_cast(_hour); + if (_precision >= Precision::Min) + ss << ':' << std::setw(2) << static_cast(_min); + if (_precision >= Precision::Sec) + ss << ':' << std::setw(2) << static_cast(_sec); + + return ss.str(); + } +} // namespace lms::core diff --git a/src/libs/core/include/core/PartialDateTime.hpp b/src/libs/core/include/core/PartialDateTime.hpp new file mode 100644 index 000000000..87000499b --- /dev/null +++ b/src/libs/core/include/core/PartialDateTime.hpp @@ -0,0 +1,67 @@ +/* + * 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 + +namespace lms::core +{ + class PartialDateTime + { + public: + constexpr PartialDateTime() = default; + PartialDateTime(int year); + PartialDateTime(int year, unsigned month); + PartialDateTime(int year, unsigned month, unsigned day); + PartialDateTime(int year, unsigned month, unsigned day, unsigned hour, unsigned min, unsigned sec); + + static PartialDateTime fromString(std::string_view str); + std::string toISO8601String() const; + + bool isValid() const { return _precision != Precision::Invalid; } + + 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 auto operator<=>(const PartialDateTime& other) const = default; + + private: + std::int16_t _year{}; + std::uint8_t _month{}; // 1 to 12 + std::uint8_t _day{}; // 1 to 31 + 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/CMakeLists.txt b/src/libs/core/test/CMakeLists.txt index 7b6aced5e..7b255e10b 100644 --- a/src/libs/core/test/CMakeLists.txt +++ b/src/libs/core/test/CMakeLists.txt @@ -3,6 +3,7 @@ include(GoogleTest) add_executable(test-core EnumSet.cpp LiteralString.cpp + PartialDateTime.cpp Path.cpp RecursiveSharedMutex.cpp Service.cpp diff --git a/src/libs/core/test/PartialDateTime.cpp b/src/libs/core/test/PartialDateTime.cpp new file mode 100644 index 000000000..3afd4daca --- /dev/null +++ b/src/libs/core/test/PartialDateTime.cpp @@ -0,0 +1,115 @@ +/* + * 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 + +#include "core/PartialDateTime.hpp" + +namespace lms::core::stringUtils::tests +{ + TEST(PartialDateTime, year) + { + EXPECT_EQ(PartialDateTime{}.getYear(), std::nullopt); + EXPECT_EQ(PartialDateTime{ 1992 }.getYear(), 1992); + } + + TEST(PartialDateTime, month) + { + EXPECT_EQ(PartialDateTime{}.getMonth(), std::nullopt); + EXPECT_EQ((PartialDateTime{ 1992, 3 }.getMonth()), std::optional{ 3 }); + } + TEST(PartialDateTime, day) + { + EXPECT_EQ(PartialDateTime{}.getDay(), std::nullopt); + EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.getDay()), std::optional{ 27 }); + } + + TEST(PartialDateTime, comparison) + { + EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 3, 27 })); + EXPECT_EQ((PartialDateTime{ 1992, 3 }), (PartialDateTime{ 1992, 3 })); + EXPECT_EQ(PartialDateTime{ 1992 }, PartialDateTime{ 1992 }); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 3 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992 })); + EXPECT_NE((PartialDateTime{ 1992, 3 }), (PartialDateTime{ 1992 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 3, 28 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1992, 4, 27 })); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_GT((PartialDateTime{ 1993, 3, 28 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_GT((PartialDateTime{ 1993, 4 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_GT((PartialDateTime{ 1994 }), (PartialDateTime{ 1993, 3, 27 })); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }), (PartialDateTime{ 1993, 3, 28 })); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }), (PartialDateTime{ 1993, 4 })); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }), (PartialDateTime{ 1994 })); + } + + TEST(PartialDateTime, stringComparison) + { + EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 27 }.toISO8601String())); + EXPECT_EQ((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String())); + EXPECT_EQ(PartialDateTime{ 1992 }.toISO8601String(), PartialDateTime{ 1992 }.toISO8601String()); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 28 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 4, 27 }.toISO8601String())); + EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_GT((PartialDateTime{ 1993, 3, 28 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_GT((PartialDateTime{ 1993, 4 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_GT((PartialDateTime{ 1994 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String())); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 28 }.toISO8601String())); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 4 }.toISO8601String())); + EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1994 }.toISO8601String())); + } + + TEST(PartialDateTime, stringConversions) + { + struct TestCase + { + std::string_view input; + std::string_view expectedOutput; + }; + + constexpr TestCase tests[]{ + { "1992", "1992" }, + { "1992-03", "1992-03" }, + { "1992-03-27", "1992-03-27" }, + { "1992-03-27T15", "1992-03-27T15" }, + { "1992-03-27T15:08", "1992-03-27T15:08" }, + { "1992-03-27T15:08:57", "1992-03-27T15:08:57" }, + + { "1992-03-27 15", "1992-03-27T15" }, + { "1992-03-27 15:08", "1992-03-27T15:08" }, + { "1992-03-27 15:08:57", "1992-03-27T15:08:57" }, + + { "1992", "1992" }, + { "1992/03", "1992-03" }, + { "1992/03/27", "1992-03-27" }, + { "1992/03/27 15", "1992-03-27T15" }, + { "1992/03/27 15:08", "1992-03-27T15:08" }, + { "1992/03/27 15:08:57", "1992-03-27T15:08:57" }, + }; + + for (const TestCase& test : tests) + { + const PartialDateTime dateTime{ PartialDateTime::fromString(test.input) }; + EXPECT_EQ(dateTime.toISO8601String(), test.expectedOutput) << "Input = '" << test.input; + } + } +} // namespace lms::core::stringUtils::tests diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 0df933ebc..c5be754e0 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{ 79 }; + static constexpr Version LMS_DATABASE_VERSION{ 80 }; } VersionInfo::VersionInfo() @@ -88,6 +88,14 @@ namespace lms::db::Migration namespace { + void dropIndexes(Session& session) + { + // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all + std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; + for (const auto& indexName : indexeNames) + utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName); + } + void migrateFromV33(Session& session) { // remove name from track_artist_link @@ -461,9 +469,7 @@ SELECT void migrateFromV56(Session& session) { // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all - std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; - for (const auto& indexName : indexeNames) - utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName); + dropIndexes(session); } void migrateFromV57(Session& session) @@ -1044,6 +1050,19 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV79(Session& session) + { + // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all + dropIndexes(session); + + // New partial date/time support + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN year"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN original_year"); + + // 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)" }; @@ -1099,6 +1118,7 @@ FROM tracklist)"); { 76, migrateFromV76 }, { 77, migrateFromV77 }, { 78, migrateFromV78 }, + { 79, migrateFromV79 }, }; bool migrationPerformed{}; @@ -1144,4 +1164,4 @@ FROM tracklist)"); return migrationPerformed; } -} // namespace lms::db::Migration +} // namespace lms::db::Migration \ No newline at end of file diff --git a/src/libs/database/impl/PartialDateTimeTraits.hpp b/src/libs/database/impl/PartialDateTimeTraits.hpp new file mode 100644 index 000000000..f2b35acee --- /dev/null +++ b/src/libs/database/impl/PartialDateTimeTraits.hpp @@ -0,0 +1,54 @@ +/* + * 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 "core/PartialDateTime.hpp" + +#include + +namespace Wt::Dbo +{ + template<> + struct sql_value_traits + { + static std::string type(SqlConnection* conn, int /*size*/) + { + return conn->dateTimeType(SqlDateTimeType::DateTime); + } + + static void bind(const lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int /* size */) + { + if (!dateTime.isValid()) + statement->bindNull(column); + else + statement->bind(column, dateTime.toISO8601String()); + } + + static bool read(lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int size) + { + std::string str; + if (!statement->getResult(column, &str, size)) + return false; + + dateTime = lms::core::PartialDateTime::fromString(str); + return true; + } + }; +} // namespace Wt::Dbo diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 4c40b5064..99cd72c29 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -21,7 +21,7 @@ #include -#include "core/ILogger.hpp" +#include "core/PartialDateTime.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" @@ -32,6 +32,7 @@ #include "EnumSetTraits.hpp" #include "IdTypeTraits.hpp" +#include "PartialDateTimeTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" #include "Utils.hpp" @@ -90,8 +91,8 @@ namespace lms::db if (params.dateRange) { - query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) >= ?").bind(params.dateRange->begin); - query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) <= ?").bind(params.dateRange->end); + query.where("CAST(SUBSTR(t.date, 1, 4) AS INTEGER) >= ?").bind(params.dateRange->begin); + query.where("CAST(SUBSTR(t.date, 1, 4) AS INTEGER) <= ?").bind(params.dateRange->end); } if (!params.name.empty()) @@ -210,16 +211,16 @@ namespace lms::db query.orderBy("t.file_last_write DESC"); break; case ReleaseSortMethod::DateAsc: - query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) ASC, r.name COLLATE NOCASE"); + query.orderBy("t.date ASC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::DateDesc: - query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC, r.name COLLATE NOCASE"); + query.orderBy("t.date DESC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::OriginalDate: - query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)), r.name COLLATE NOCASE"); + query.orderBy("COALESCE(t.original_date, t.date), r.name COLLATE NOCASE"); break; case ReleaseSortMethod::OriginalDateDesc: - query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)) DESC, r.name COLLATE NOCASE"); + query.orderBy("COALESCE(t.original_date, t.date) DESC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::StarredDateDesc: assert(params.starringUser.isValid()); @@ -453,22 +454,22 @@ namespace lms::db return discs; } - Wt::WDate Release::getDate() const + core::PartialDateTime Release::getDate() const { return getDate(false); } - Wt::WDate Release::getOriginalDate() const + core::PartialDateTime Release::getOriginalDate() const { return getDate(true); } - Wt::WDate Release::getDate(bool original) const + core::PartialDateTime Release::getDate(bool original) const { assert(session()); const char* field{ original ? "original_date" : "date" }; - auto query{ (session()->query(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").groupBy(field).bind(getId())) }; + auto query{ (session()->query(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").groupBy(field).bind(getId())) }; const auto dates{ utils::fetchQueryResults(query) }; @@ -493,17 +494,25 @@ namespace lms::db { assert(session()); - const char* field{ original ? "original_year" : "year" }; - auto query{ session()->query>(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").bind(getId()).groupBy(field) }; + const char* field{ original ? "original_date" : "date" }; + auto query{ session()->query(std::string{ "SELECT " } + "t." + field + " FROM track t").where("t.release_id = ?").bind(getId()).groupBy(field) }; + + bool multiYears{}; + std::optional year{}; + utils::forEachQueryResult(query, [&](core::PartialDateTime dateTime) { + assert(dateTime.isValid()); - const auto years{ utils::fetchQueryResults(query) }; + if (!year) + year = dateTime.getYear().value(); + else if (*year != dateTime.getYear().value()) + multiYears = true; + }); - // various years => invalid years - const std::size_t count{ years.size() }; - if (count == 0 || count > 1) + if (multiYears) return std::nullopt; - return years.front(); + assert(year); + return *year; } std::optional Release::getCopyright() const diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index da854cd1e..4f0a40922 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -52,6 +52,7 @@ #include "EnumSetTraits.hpp" #include "Migration.hpp" +#include "PartialDateTimeTraits.hpp" #include "PathTraits.hpp" #include "Utils.hpp" @@ -247,12 +248,10 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_year_idx ON track(release_id, year)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)"); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index a2cbecd39..3dd53fcdf 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -34,6 +34,7 @@ #include "database/User.hpp" #include "IdTypeTraits.hpp" +#include "PartialDateTimeTraits.hpp" #include "PathTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" @@ -450,6 +451,16 @@ namespace lms::db _trackLyrics.insert(getDboPtr(lyrics)); } + std::optional Track::getYear() const + { + return _date.getYear(); + } + + std::optional Track::getOriginalYear() const + { + return _originalDate.getYear(); + } + bool Track::hasLyrics() const { return !_trackLyrics.empty(); diff --git a/src/libs/database/impl/Types.cpp b/src/libs/database/impl/Types.cpp index fb5220f9f..6100b86a8 100644 --- a/src/libs/database/impl/Types.cpp +++ b/src/libs/database/impl/Types.cpp @@ -41,9 +41,4 @@ namespace lms::db { return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); } - - DateRange DateRange::fromYearRange(int from, int to) - { - return DateRange{ from, to }; - } } // namespace lms::db diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index a381c2cb1..699c02366 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include #include @@ -30,6 +29,7 @@ #include #include "core/EnumSet.hpp" +#include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" @@ -126,7 +126,7 @@ namespace lms::db ReleaseSortMethod sortMethod{ ReleaseSortMethod::None }; std::optional range; Wt::WDateTime writtenAfter; - std::optional dateRange; + std::optional dateRange; UserId starringUser; // only releases starred by this user std::optional feedbackBackend; // and for this backend ArtistId artist; // only releases that involved this user @@ -167,7 +167,7 @@ namespace lms::db writtenAfter = _after; return *this; } - FindParameters& setDateRange(const std::optional& _dateRange) + FindParameters& setDateRange(const std::optional& _dateRange) { dateRange = _dateRange; return *this; @@ -227,9 +227,9 @@ namespace lms::db std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const; // Utility functions (if all tracks have the same values, which is legit to not be the case) - Wt::WDate getDate() const; + core::PartialDateTime getDate() const; std::optional getYear() const; - Wt::WDate getOriginalDate() const; + core::PartialDateTime getOriginalDate() const; std::optional getOriginalYear() const; std::optional getCopyright() const; std::optional getCopyrightURL() const; @@ -302,7 +302,7 @@ namespace lms::db Release(const std::string& name, const std::optional& MBID = {}); static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); - Wt::WDate getDate(bool original) const; + core::PartialDateTime getDate(bool original) const; std::optional getYear(bool original) const; static constexpr std::size_t _maxNameLength{ 512 }; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 6594f17b4..f42089d33 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -33,6 +33,7 @@ #include #include "core/EnumSet.hpp" +#include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" @@ -223,16 +224,14 @@ namespace lms::db void setRelativeFilePath(const std::filesystem::path& filePath); void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } - void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } + void setAddedTime(core::PartialDateTime 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; } void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; } void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; } - void setDate(const Wt::WDate& date) { _date = date; } - void setYear(std::optional year) { _year = year; } - void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } - void setOriginalYear(std::optional year) { _originalYear = year; } + void setDate(const core::PartialDateTime& date) { _date = date; } + void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; } void setHasCover(bool hasCover) { _hasCover = hasCover; } void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } @@ -268,12 +267,12 @@ namespace lms::db std::chrono::milliseconds getDuration() const { return _duration; } std::size_t getSampleRate() const { return _sampleRate; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } - const Wt::WDate& getDate() const { return _date; } - std::optional getYear() const { return _year; } - const Wt::WDate& getOriginalDate() const { return _originalDate; } - std::optional getOriginalYear() const { return _originalYear; }; + const core::PartialDateTime& getDate() const { return _date; } + std::optional getYear() const; + const core::PartialDateTime& getOriginalDate() const { return _originalDate; } + std::optional getOriginalYear() const; const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } - const Wt::WDateTime& getAddedTime() const { return _fileAdded; } + const core::PartialDateTime& getAddedTime() const { return _fileAdded; } bool hasCover() const { return _hasCover; } bool hasLyrics() const; std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } @@ -314,9 +313,7 @@ namespace lms::db Wt::Dbo::field(a, _channelCount, "channel_count"); Wt::Dbo::field(a, _sampleRate, "sample_rate"); Wt::Dbo::field(a, _date, "date"); - Wt::Dbo::field(a, _year, "year"); Wt::Dbo::field(a, _originalDate, "original_date"); - Wt::Dbo::field(a, _originalYear, "original_year"); Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path"); Wt::Dbo::field(a, _relativeFilePath, "relative_file_path"); Wt::Dbo::field(a, _fileStem, "file_stem"); @@ -362,17 +359,15 @@ namespace lms::db int _channelCount{}; std::chrono::duration _duration{}; int _sampleRate{}; - Wt::WDate _date; - std::optional _year; - Wt::WDate _originalDate; - std::optional _originalYear; + core::PartialDateTime _date; + core::PartialDateTime _originalDate; std::filesystem::path _absoluteFilePath; // full path std::filesystem::path _relativeFilePath; // relative to root (that may be deleted) std::filesystem::path _fileStem; std::filesystem::path _fileName; long long _fileSize{}; Wt::WDateTime _fileLastWrite; - Wt::WDateTime _fileAdded; + core::PartialDateTime _fileAdded; bool _hasCover{}; std::string _trackMBID; std::string _recordingMBID; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 7330c80a5..5fb4a3b49 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -100,12 +100,10 @@ namespace lms::db } }; - struct DateRange + struct YearRange { - int begin; - int end; - - static DateRange fromYearRange(int from, int to); + int begin{}; + int end{}; }; struct DiscInfo diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index d3ee941a2..39e316114 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -19,6 +19,7 @@ #include "Common.hpp" +#include "core/PartialDateTime.hpp" #include "database/Image.hpp" namespace lms::db::tests @@ -462,8 +463,8 @@ namespace lms::db::tests { ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; - const Wt::WDate release1Date{ Wt::WDate{ 1994, 2, 3 } }; - const Wt::WDate release1OriginalDate{ Wt::WDate{ 1993, 4, 5 } }; + const core::PartialDateTime release1Date{ 1994, 2, 3 }; + const core::PartialDateTime release1OriginalDate{ 1993, 4, 5 }; ScopedTrack track1A{ session }; ScopedTrack track1B{ session }; @@ -473,7 +474,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(0, 3000))) }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ -3000, 3000 })) }; EXPECT_EQ(releases.results.size(), 0); } @@ -492,20 +493,23 @@ namespace lms::db::tests EXPECT_EQ(release1.get()->getDate(), release1Date); EXPECT_EQ(release1.get()->getOriginalDate(), release1OriginalDate); + + EXPECT_EQ(release1.get()->getYear(), release1Date.getYear()); + EXPECT_EQ(release1.get()->getOriginalYear(), release1OriginalDate.getYear()); } { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1950, 2000))) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1950, 2000 })) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1994, 1994 })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1993, 1993 })); ASSERT_EQ(releases.results.size(), 0); } } @@ -525,7 +529,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(0, 3000))) }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 0, 3000 })) }; EXPECT_EQ(releases.results.size(), 0); } @@ -537,10 +541,10 @@ namespace lms::db::tests track2A.get().modify()->setRelease(release2.get()); track2B.get().modify()->setRelease(release2.get()); - track1A.get().modify()->setYear(release1Year); - track1B.get().modify()->setYear(release1Year); - track1A.get().modify()->setOriginalYear(release1OriginalYear); - track1B.get().modify()->setOriginalYear(release1OriginalYear); + track1A.get().modify()->setDate(core::PartialDateTime{ release1Year }); + track1B.get().modify()->setDate(core::PartialDateTime{ release1Year }); + track1A.get().modify()->setOriginalDate(core::PartialDateTime{ release1OriginalYear }); + track1B.get().modify()->setOriginalDate(core::PartialDateTime{ release1OriginalYear }); EXPECT_EQ(release1.get()->getYear(), release1Year); EXPECT_EQ(release1.get()->getOriginalYear(), release1OriginalYear); @@ -549,15 +553,15 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1950, 2000))) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1950, 2000 })) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1994, 1994 })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993))); + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1993, 1993 })); ASSERT_EQ(releases.results.size(), 0); } } @@ -957,11 +961,11 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_sortMethod) { ScopedRelease release1{ session, "MyRelease1" }; - const Wt::WDate release1Date{ Wt::WDate{ 2000, 2, 3 } }; - const Wt::WDate release1OriginalDate{ Wt::WDate{ 1993, 4, 5 } }; + const core::PartialDateTime release1Date{ 2000, 2, 3 }; + const core::PartialDateTime release1OriginalDate{ 1993, 4, 5 }; ScopedRelease release2{ session, "MyRelease2" }; - const Wt::WDate release2Date{ Wt::WDate{ 1994, 2, 3 } }; + const core::PartialDateTime release2Date{ 1994, 2, 3 }; ScopedTrack track1{ session }; ScopedTrack track2{ session }; diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 8c9a13d87..8d9edc888 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -259,8 +259,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_date) { ScopedTrack track{ session }; - const Wt::WDate date{ 1995, 5, 5 }; - const Wt::WDate originalDate{ 1994, 2, 2 }; + const core::PartialDateTime date{ 1995, 5, 5 }; + const core::PartialDateTime originalDate{ 1994, 2, 2 }; { auto transaction{ session.createReadTransaction() }; EXPECT_EQ(track->getYear(), std::nullopt); @@ -275,22 +275,16 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(track->getYear(), std::nullopt); - EXPECT_EQ(track->getOriginalYear(), std::nullopt); + EXPECT_EQ(track->getYear(), 1995); + EXPECT_EQ(track->getOriginalYear(), 1994); EXPECT_EQ(track->getDate(), date); EXPECT_EQ(track->getOriginalDate(), originalDate); } - { - auto transaction{ session.createWriteTransaction() }; - track.get().modify()->setYear(date.year()); - track.get().modify()->setOriginalYear(originalDate.year()); - } - { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(track->getYear(), date.year()); - EXPECT_EQ(track->getOriginalYear(), originalDate.year()); + EXPECT_EQ(track->getYear(), date.getYear()); + EXPECT_EQ(track->getOriginalYear(), originalDate.getYear()); } } diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp index 4e8e0948d..564ce5d66 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/Parser.cpp @@ -22,6 +22,7 @@ #include #include "core/ILogger.hpp" +#include "core/PartialDateTime.hpp" #include "core/String.hpp" #include "metadata/Exception.hpp" @@ -360,44 +361,27 @@ namespace lms::metadata track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); track.acoustID = getTagValueAs(tagReader, TagType::AcoustID); track.position = getTagValueAs(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine - if (auto dateStr = getTagValueAs(tagReader, TagType::Date)) + if (const auto dateStr{ getTagValueAs(tagReader, TagType::Date) }) { - if (const Wt::WDate date{ utils::parseDate(*dateStr) }; date.isValid()) - { + if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid()) track.date = date; - track.year = date.year(); - } - else - { - track.year = utils::parseYear(*dateStr); - } } - if (auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseDate)) + if (const auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseDate)) { - if (const Wt::WDate date{ utils::parseDate(*dateStr) }; date.isValid()) - { + if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid()) track.originalDate = date; - track.originalYear = date.year(); - } - else - { - track.originalYear = utils::parseYear(*dateStr); - } } - if (auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseYear)) - { + if (const auto dateStr{ getTagValueAs(tagReader, TagType::OriginalReleaseYear) }) track.originalYear = utils::parseYear(*dateStr); + + if (const auto encodingTimeStr{ getTagValueAs(tagReader, TagType::EncodingTime) }) + { + if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*encodingTimeStr) }; date.isValid()) + track.encodingTime = date; } track.advisory = getAdvisory(tagReader); - if (const auto encodingTime{ getTagValueAs(tagReader, TagType::EncodingTime) }) - { - if (auto dateTime{ core::stringUtils::fromISO8601String(*encodingTime) }; dateTime.isValid()) - track.encodingTime = dateTime; - else if (const Wt::WDate date{ utils::parseDate(*encodingTime) }; date.isValid()) - track.encodingTime = Wt::WDateTime{ date }; - } track.lyrics = getLyrics(tagReader); // no custom delimiter on lyrics track.comments = getTagValuesAs(tagReader, TagType::Comment, {} /* no custom delimiter on comments */); track.copyright = getTagValueAs(tagReader, TagType::Copyright).value_or(""); @@ -432,13 +416,9 @@ namespace lms::metadata track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported - // If a file has date but no year, set it - if (!track.year && track.date.isValid()) - track.year = track.date.year(); - // If a file has originalDate but no originalYear, set it - if (!track.originalYear && track.originalDate.isValid()) - track.originalYear = track.originalDate.year(); + if (!track.originalYear) + track.originalYear = track.originalDate.getYear(); } std::optional Parser::getMedium(const ITagReader& tagReader) diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index 6f87545f6..1f7158a9c 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -18,6 +18,7 @@ */ #include "Utils.hpp" + #include #include #include diff --git a/src/libs/metadata/include/metadata/Types.hpp b/src/libs/metadata/include/metadata/Types.hpp index 6843cac0a..021f56682 100644 --- a/src/libs/metadata/include/metadata/Types.hpp +++ b/src/libs/metadata/include/metadata/Types.hpp @@ -26,9 +26,7 @@ #include #include -#include -#include - +#include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "Lyrics.hpp" @@ -120,12 +118,11 @@ namespace lms::metadata std::vector moods; std::vector languages; Tags userExtraTags; - std::optional year{}; - Wt::WDate date; - std::optional originalYear{}; - Wt::WDate originalDate; + core::PartialDateTime date; + std::optional originalYear; + core::PartialDateTime originalDate; std::optional advisory; - Wt::WDateTime encodingTime; + core::PartialDateTime encodingTime; bool hasCover{}; std::optional acoustID; std::string copyright; diff --git a/src/libs/metadata/test/Parser.cpp b/src/libs/metadata/test/Parser.cpp index e9df1e703..7d0a5e146 100644 --- a/src/libs/metadata/test/Parser.cpp +++ b/src/libs/metadata/test/Parser.cpp @@ -125,9 +125,9 @@ namespace lms::metadata EXPECT_EQ(track->copyright, "MyCopyright"); EXPECT_EQ(track->copyrightURL, "MyCopyrightURL"); ASSERT_TRUE(track->date.isValid()); - EXPECT_EQ(track->date.year(), 2020); - EXPECT_EQ(track->date.month(), 3); - EXPECT_EQ(track->date.day(), 4); + EXPECT_EQ(track->date.getYear(), 2020); + EXPECT_EQ(track->date.getMonth(), 3); + EXPECT_EQ(track->date.getDay(), 4); EXPECT_FALSE(track->hasCover); ASSERT_EQ(track->genres.size(), 2); EXPECT_EQ(track->genres[0], "Genre1"); @@ -157,9 +157,9 @@ namespace lms::metadata EXPECT_EQ(track->moods[0], "Mood1"); EXPECT_EQ(track->moods[1], "Mood2"); ASSERT_TRUE(track->originalDate.isValid()); - EXPECT_EQ(track->originalDate.year(), 2019); - EXPECT_EQ(track->originalDate.month(), 2); - EXPECT_EQ(track->originalDate.day(), 3); + EXPECT_EQ(track->originalDate.getYear(), 2019); + EXPECT_EQ(track->originalDate.getMonth(), 2); + EXPECT_EQ(track->originalDate.getDay(), 3); ASSERT_TRUE(track->originalYear.has_value()); EXPECT_EQ(track->originalYear.value(), 2019); ASSERT_TRUE(track->performerArtists.contains("Rolea")); @@ -188,8 +188,6 @@ namespace lms::metadata ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"].size(), 2); EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][0], "MyTagValue1ForTagB"); EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][1], "MyTagValue2ForTagB"); - ASSERT_TRUE(track->year.has_value()); - EXPECT_EQ(track->year.value(), 2020); // Medium ASSERT_TRUE(track->medium.has_value()); @@ -615,7 +613,7 @@ namespace lms::metadata TEST(Parser, encodingTime) { - auto doTest = [](std::string_view value, Wt::WDateTime expectedValue) { + auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { const TestTagReader testTags{ { { TagType::EncodingTime, { value } }, @@ -628,10 +626,36 @@ namespace lms::metadata ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'"; }; - doTest("", Wt::WDateTime{}); - doTest("foo", Wt::WDateTime{}); - doTest("2020-01-03T09:08:11.075", Wt::WDateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } }); - doTest("2020-01-03", Wt::WDateTime{ Wt::WDate{ 2020, 01, 03 } }); - doTest("2020/01/03", Wt::WDateTime{ Wt::WDate{ 2020, 01, 03 } }); + doTest("", core::PartialDateTime{}); + doTest("foo", core::PartialDateTime{}); + doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 }); + doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); } + + TEST(Parser, date) + { + auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { + const TestTagReader testTags{ + { + { TagType::Date, { value } }, + } + }; + + Parser parser; + std::unique_ptr track{ Parser{}.parse(testTags) }; + + ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'"; + }; + + doTest("", core::PartialDateTime{}); + doTest("foo", core::PartialDateTime{}); + doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020-01", core::PartialDateTime{ 2020, 1 }); + doTest("2020", core::PartialDateTime{ 2020 }); + doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020/01", core::PartialDateTime{ 2020, 1 }); + doTest("2020", core::PartialDateTime{ 2020 }); + } + } // namespace lms::metadata diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index b275f0f0f..ad226eee4 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -22,6 +22,7 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" +#include "core/PartialDateTime.hpp" #include "core/Path.hpp" #include "core/Service.hpp" #include "database/Artist.hpp" @@ -479,7 +480,17 @@ namespace lms::scanner { track = dbSession.create(); track.modify()->setAbsoluteFilePath(_file); - track.modify()->setAddedTime(fileInfo->lastWriteTime); // may be erased by encodingTime + + 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; } @@ -555,18 +566,14 @@ namespace lms::scanner track.modify()->setTrackNumber(_parsedTrack->position); track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt); track.modify()->setDate(_parsedTrack->date); - track.modify()->setYear(_parsedTrack->year); track.modify()->setOriginalDate(_parsedTrack->originalDate); - track.modify()->setOriginalYear(_parsedTrack->originalYear); + if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) + track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear }); // If a file has an OriginalDate but no date, set it to ease filtering if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid()) track.modify()->setDate(_parsedTrack->originalDate); - // If a file has an OriginalYear but no Year, set it to ease filtering - if (!_parsedTrack->year && _parsedTrack->originalYear) - track.modify()->setYear(_parsedTrack->originalYear); - track.modify()->setRecordingMBID(_parsedTrack->recordingMBID); track.modify()->setTrackMBID(_parsedTrack->mbid); if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) diff --git a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp index 916086be0..49dc83447 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp @@ -25,6 +25,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -106,7 +107,7 @@ namespace lms::api::subsonic Release::FindParameters params; params.setSortMethod(fromYear > toYear ? ReleaseSortMethod::DateDesc : ReleaseSortMethod::DateAsc); params.setRange(range); - params.setDateRange(DateRange::fromYearRange(std::min(fromYear, toYear), std::max(fromYear, toYear))); + params.setDateRange(YearRange{ std::min(fromYear, toYear), std::max(fromYear, toYear) }); params.setMediaLibrary(mediaLibraryId); releases = Release::findIds(context.dbSession, params); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index cb790e239..051ff8693 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -204,7 +204,7 @@ namespace lms::api::subsonic albumNode.createEmptyArrayChild("artists"); albumNode.setAttribute("displayArtist", ""); } - albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate(), release->getOriginalYear())); + albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate())); albumNode.setAttribute("isCompilation", release->isCompilation()); diff --git a/src/libs/subsonic/impl/responses/ItemDate.cpp b/src/libs/subsonic/impl/responses/ItemDate.cpp index 703e25577..c5c92c997 100644 --- a/src/libs/subsonic/impl/responses/ItemDate.cpp +++ b/src/libs/subsonic/impl/responses/ItemDate.cpp @@ -23,20 +23,16 @@ namespace lms::api::subsonic { - Response::Node createItemDateNode(const Wt::WDate& date, std::optional year) + Response::Node createItemDateNode(const core::PartialDateTime& date) { Response::Node itemDateNode; - if (date.isValid()) - { - itemDateNode.setAttribute("year", date.year()); - itemDateNode.setAttribute("month", date.month()); - itemDateNode.setAttribute("day", date.day()); - } - else if (year) - { + if (auto year{ date.getYear() }) itemDateNode.setAttribute("year", *year); - } + if (auto month{ date.getMonth() }) + itemDateNode.setAttribute("month", *month); + if (auto day{ date.getDay() }) + itemDateNode.setAttribute("day", *day); return itemDateNode; } diff --git a/src/libs/subsonic/impl/responses/ItemDate.hpp b/src/libs/subsonic/impl/responses/ItemDate.hpp index c071c136c..ffa3a1e75 100644 --- a/src/libs/subsonic/impl/responses/ItemDate.hpp +++ b/src/libs/subsonic/impl/responses/ItemDate.hpp @@ -19,11 +19,11 @@ #pragma once -#include +#include "core/PartialDateTime.hpp" #include "SubsonicResponse.hpp" namespace lms::api::subsonic { - Response::Node createItemDateNode(const Wt::WDate& date, std::optional year); + Response::Node createItemDateNode(const core::PartialDateTime& date); } diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index 20465ae81..09bda9be2 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -174,7 +174,9 @@ namespace lms::ui::releaseHelpers { Wt::WString res; - // Year can be here, but originalYear can't be here without year (enforced by scanner) + // Year could be here, but originalYear can't be here without year (enforced by scanner) + assert(year || !originalYear); + if (!year) return res; diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index b6bb61688..dcec02f9b 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -235,12 +235,10 @@ namespace lms::metadata std::cout << "Position: " << *track->position << std::endl; if (track->date.isValid()) - std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl; - if (track->year) - std::cout << "Year: " << *track->year << std::endl; + std::cout << "Date: " << track->date.toISO8601String() << std::endl; if (track->originalDate.isValid()) - std::cout << "Original date: " << track->originalDate.toString("yyyy-MM-dd") << std::endl; + std::cout << "Original date: " << track->originalDate.toISO8601String() << std::endl; if (track->originalYear) std::cout << "Original year: " << *track->originalYear << std::endl; @@ -269,7 +267,7 @@ namespace lms::metadata std::cout << "Advisory: " << *track->advisory << std::endl; if (track->encodingTime.isValid()) - std::cout << "Encoding time: " << core::stringUtils::toISO8601String(track->encodingTime) << std::endl; + std::cout << "Encoding time: " << track->encodingTime.toISO8601String() << std::endl; if (track->medium) std::cout << "Medium: " << *track->medium;