From ed1be02aafabb63dd15a63840e94c1dcf616fea5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 17 Mar 2024 23:36:40 +0100 Subject: [PATCH 01/64] Added a scan tracker to speed up queries with high offset in very large database --- src/libs/core/impl/ChildProcessManager.cpp | 6 +- src/libs/database/impl/Track.cpp | 19 +-- src/libs/database/include/database/Track.hpp | 2 +- src/libs/database/test/Track.cpp | 46 +++++- .../impl/ScanStepRemoveOrphanDbFiles.cpp | 20 ++- src/libs/subsonic/impl/ClientInfo.hpp | 1 + src/libs/subsonic/impl/SubsonicResource.cpp | 7 +- src/libs/subsonic/impl/SubsonicResource.hpp | 2 +- .../subsonic/impl/entrypoints/Searching.cpp | 156 +++++++++++++++--- 9 files changed, 196 insertions(+), 63 deletions(-) diff --git a/src/libs/core/impl/ChildProcessManager.cpp b/src/libs/core/impl/ChildProcessManager.cpp index abe6de1e..e7dc459e 100644 --- a/src/libs/core/impl/ChildProcessManager.cpp +++ b/src/libs/core/impl/ChildProcessManager.cpp @@ -25,8 +25,7 @@ namespace lms::core { - std::unique_ptr - createChildProcessManager(boost::asio::io_context& ioContext) + std::unique_ptr createChildProcessManager(boost::asio::io_context& ioContext) { return std::make_unique(ioContext); } @@ -36,8 +35,7 @@ namespace lms::core { } - std::unique_ptr - ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) + std::unique_ptr ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) { return std::make_unique(_ioContext, path, args); } diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 16d79960..4ff6ed61 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -228,27 +228,22 @@ namespace lms::db .resultValue(); } - void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t batchSize, bool& moreResults, const std::function& func) + void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function& func, MediaLibraryId library) { session.checkReadTransaction(); - auto collection{ session.getDboSession().find() + auto query{ session.getDboSession().find() .orderBy("id") .where("id > ?").bind(lastRetrievedTrack) - .limit(static_cast(batchSize) + 1) - .resultList() }; + .limit(static_cast(count)) }; - moreResults = false; + if (library.isValid()) + query.where("media_library_id = ?").bind(library); + + auto collection{query.resultList()}; - std::size_t count{}; for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { - if (count++ == batchSize) - { - moreResults = true; - break; - } - func(*itResult); lastRetrievedTrack = (*itResult)->getId(); } diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index c52f9f49..6b29414a 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -110,7 +110,7 @@ namespace lms::db static std::size_t getCount(Session& session); static pointer findByPath(Session& session, const std::filesystem::path& p); static pointer find(Session& session, TrackId id); - static void find(Session& session, TrackId& lastRetrievedTrack, std::size_t batchSize, bool& moreResults, const std::function& func); + static void find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function& func, MediaLibraryId library = {}); static bool exists(Session& session, TrackId id); static std::vector findByRecordingMBID(Session& session, const core::UUID& MBID); static std::vector findByMBID(Session& session, const core::UUID& MBID); diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 0d6683af..c3a0df8f 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -68,14 +68,20 @@ namespace lms::db::tests ScopedTrack track1{ session, "MyTrackFile1" }; ScopedTrack track2{ session, "MyTrackFile1" }; ScopedTrack track3{ session, "MyTrackFile1" }; + ScopedMediaLibrary library{ session }; + ScopedMediaLibrary otherLibrary{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track2.get().modify()->setMediaLibrary(library.get()); + } { auto transaction{ session.createReadTransaction() }; - bool moreResults; TrackId lastRetrievedTrackId; std::vector visitedTracks; - Track::find(session, lastRetrievedTrackId, 10, moreResults, [&](const Track::pointer& track) + Track::find(session, lastRetrievedTrackId, 10, [&](const Track::pointer& track) { visitedTracks.push_back(track); }); @@ -83,40 +89,62 @@ namespace lms::db::tests EXPECT_EQ(visitedTracks[0]->getId(), track1.getId()); EXPECT_EQ(visitedTracks[1]->getId(), track2.getId()); EXPECT_EQ(visitedTracks[2]->getId(), track3.getId()); - EXPECT_FALSE(moreResults); EXPECT_EQ(lastRetrievedTrackId, track3.getId()); } { auto transaction{ session.createReadTransaction() }; - bool moreResults; TrackId lastRetrievedTrackId{ track1.getId() }; std::vector visitedTracks; - Track::find(session, lastRetrievedTrackId, 1, moreResults, [&](const Track::pointer& track) + Track::find(session, lastRetrievedTrackId, 1, [&](const Track::pointer& track) { visitedTracks.push_back(track); }); ASSERT_EQ(visitedTracks.size(), 1); EXPECT_EQ(visitedTracks[0]->getId(), track2.getId()); - EXPECT_TRUE(moreResults); EXPECT_EQ(lastRetrievedTrackId, track2.getId()); } { auto transaction{ session.createReadTransaction() }; - bool moreResults; TrackId lastRetrievedTrackId{ track1.getId() }; std::vector visitedTracks; - Track::find(session, lastRetrievedTrackId, 0, moreResults, [&](const Track::pointer& track) + Track::find(session, lastRetrievedTrackId, 0, [&](const Track::pointer& track) { visitedTracks.push_back(track); }); ASSERT_EQ(visitedTracks.size(), 0); - EXPECT_TRUE(moreResults); EXPECT_EQ(lastRetrievedTrackId, track1.getId()); } + + { + auto transaction{ session.createReadTransaction() }; + + TrackId lastRetrievedTrackId{}; + std::vector visitedTracks; + Track::find(session, lastRetrievedTrackId, 10, [&](const Track::pointer& track) + { + visitedTracks.push_back(track); + }, otherLibrary.getId()); + ASSERT_EQ(visitedTracks.size(), 0); + EXPECT_EQ(lastRetrievedTrackId, TrackId{}); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackId lastRetrievedTrackId{}; + std::vector visitedTracks; + Track::find(session, lastRetrievedTrackId, 10, [&](const Track::pointer& track) + { + visitedTracks.push_back(track); + }, library.getId()); + ASSERT_EQ(visitedTracks.size(), 1); + EXPECT_EQ(visitedTracks[0]->getId(), track2.getId()); + EXPECT_EQ(lastRetrievedTrackId, track2.getId()); + } } TEST_F(DatabaseFixture, Track_MediaLibrary) diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp index 00ad637a..a0a30185 100644 --- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp @@ -99,8 +99,8 @@ namespace lms::scanner std::vector tracksToRemove; TrackId lastCheckedTrackID; - bool moreResults{ true }; - while (moreResults) + bool endReached{}; + while (!endReached) { if (_abortScan) break; @@ -108,13 +108,17 @@ namespace lms::scanner tracksToRemove.clear(); { auto transaction{ session.createReadTransaction() }; - Track::find(session, lastCheckedTrackID, batchSize, moreResults, [&](const Track::pointer& track) - { - if (!checkFile(track->getPath())) - tracksToRemove.push_back(track); - context.currentStepStats.processedElems++; - }); + endReached = true; + Track::find(session, lastCheckedTrackID, batchSize, [&](const Track::pointer& track) + { + endReached = false; + + if (!checkFile(track->getPath())) + tracksToRemove.push_back(track); + + context.currentStepStats.processedElems++; + }); } if (!tracksToRemove.empty()) diff --git a/src/libs/subsonic/impl/ClientInfo.hpp b/src/libs/subsonic/impl/ClientInfo.hpp index beec15ba..045bafcf 100644 --- a/src/libs/subsonic/impl/ClientInfo.hpp +++ b/src/libs/subsonic/impl/ClientInfo.hpp @@ -26,6 +26,7 @@ namespace lms::api::subsonic { struct ClientInfo { + std::string ipAddress; std::string name; std::string user; std::string password; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index e1c232b8..e709a96b 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -380,13 +380,16 @@ namespace lms::api::subsonic } } - ClientInfo SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters) + ClientInfo SubsonicResource::getClientInfo(const Wt::Http::Request& request) { + const auto& parameters{ request.getParameterMap() }; ClientInfo res; if (hasParameter(parameters, "t")) throw TokenAuthenticationNotSupportedForLDAPUsersError{}; + res.ipAddress = request.clientAddress(); + // Mandatory parameters res.name = getMandatoryParameterAs(parameters, "c"); res.version = getMandatoryParameterAs(parameters, "v"); @@ -399,7 +402,7 @@ namespace lms::api::subsonic RequestContext SubsonicResource::buildRequestContext(const Wt::Http::Request& request) { const Wt::Http::ParameterMap& parameters{ request.getParameterMap() }; - const ClientInfo clientInfo{ getClientInfo(parameters) }; + const ClientInfo clientInfo{ getClientInfo(request) }; const db::UserId userId{ authenticateUser(request, clientInfo) }; bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) }; bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) }; diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index eda68edf..67db9936 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -47,7 +47,7 @@ namespace lms::api::subsonic ProtocolVersion getServerProtocolVersion(const std::string& clientName) const; static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server); - ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters); + ClientInfo getClientInfo(const Wt::Http::Request& request); RequestContext buildRequestContext(const Wt::Http::Request& request); db::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo); diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index ae876f6b..a8669b99 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -19,6 +19,10 @@ #include "Searching.hpp" +#include +#include +#include + #include "database/Artist.hpp" #include "database/Release.hpp" #include "database/Session.hpp" @@ -29,42 +33,111 @@ #include "responses/Song.hpp" #include "ParameterParsing.hpp" #include "SubsonicId.hpp" - + namespace lms::api::subsonic { using namespace db; + namespace + { + // Search endpoints can be used to scan/sync the database + // This class is used to keep track of the current scans, in order to retrieve the last objectId + // to speed up the query of the following range (avoid the 'offset' cost) + template + class ScanTracker + { + public: + ObjectId extractLastRetrievedObjectId(const ClientInfo& info, std::size_t offset); + void setObjectId(const ClientInfo& info, std::size_t offset, ObjectId lastRetrievedId); + void cleanOutdatedScanEntries(); + + private: + using ClockType = std::chrono::steady_clock; + + struct Client + { + std::string clientAddress; + std::string clientName; + std::string userName; + std::size_t offset{}; + auto operator<=>(const Client&) const = default; + }; + struct Entry + { + ClockType::time_point timePoint; + ObjectId objectId; + }; + + static constexpr ClockType::duration maxEntryDuration{ std::chrono::seconds{30} }; + + std::mutex _mutex; + std::map _ongoingScans; + }; + + template + ObjectId ScanTracker::extractLastRetrievedObjectId(const ClientInfo& info, std::size_t offset) + { + ObjectId res; + + const std::scoped_lock lock{ _mutex }; + auto it{ _ongoingScans.find({ info.ipAddress, info.name, info.user, offset }) }; + if (it != _ongoingScans.end()) + { + res = it->second.objectId; + _ongoingScans.erase(it); + } + + return res; + } + + template + void ScanTracker::setObjectId(const ClientInfo& info, std::size_t offset, ObjectId lastRetrievedId) + { + const std::scoped_lock lock{ _mutex }; + _ongoingScans[Client{ info.ipAddress, info.name, info.user, offset }] = { ClockType::now(), lastRetrievedId }; + } + + template + void ScanTracker::cleanOutdatedScanEntries() + { + const ClockType::time_point now{ ClockType::now() }; + + const std::scoped_lock lock{ _mutex }; + std::erase_if(_ongoingScans, [&](const auto& entry) { return now > entry.second.timePoint + maxEntryDuration; }); + } + } + namespace { Response handleSearchRequestCommon(RequestContext& context, bool id3) { // Mandatory params - std::string queryString{ getMandatoryParameterAs(context.parameters, "query") }; + const std::string queryString{ getMandatoryParameterAs(context.parameters, "query") }; std::string_view query{ queryString }; // Optional params + const std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; + const std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; + const std::size_t albumCount{ getParameterAs(context.parameters, "albumCount").value_or(20) }; + const std::size_t albumOffset{ getParameterAs(context.parameters, "albumOffset").value_or(0) }; + const std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; + const std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; + if (artistCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; + if (albumCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; + if (songCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; + // Symfonium adds extra "" if (context.clientInfo.name == "Symfonium") query = core::stringUtils::stringTrim(query, "\""); - std::vector keywords{ core::stringUtils::splitString(query, ' ') }; - - // Optional params - std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; - std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; - std::size_t albumCount{ getParameterAs(context.parameters, "albumCount").value_or(20) }; - std::size_t albumOffset{ getParameterAs(context.parameters, "albumOffset").value_or(0) }; - std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; - std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; - - if (artistCount > defaultMaxCountSize) - throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; - else if (albumCount > defaultMaxCountSize) - throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; - else if (songCount > defaultMaxCountSize) - throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; + std::vector keywords; + if (!query.empty()) + keywords = core::stringUtils::splitString(query, ' '); Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; @@ -103,15 +176,47 @@ namespace lms::api::subsonic if (songCount > 0) { - Track::FindParameters params; - params.setKeywords(keywords); - params.setRange(Range{ songOffset, songCount }); - params.setMediaLibrary(mediaLibrary); + static ScanTracker currentScansInProgress; + currentScansInProgress.cleanOutdatedScanEntries(); - Track::find(context.dbSession, params, [&](const Track::pointer& track) + TrackId lastRetrievedId; + + auto findTracks{ [&] + { + Track::FindParameters params; + params.setKeywords(keywords); + params.setRange(Range{ songOffset, songCount }); + params.setMediaLibrary(mediaLibrary); + + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); + lastRetrievedId = track->getId(); + }); + } }; + + if (!keywords.empty()) + { + findTracks(); + } + else + { + if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(context.clientInfo, songOffset) }; cachedLastRetrievedId.isValid()) { - searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); - }); + Track::find(context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) + { + searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); + }, mediaLibrary); + lastRetrievedId = cachedLastRetrievedId; + } + else + { + findTracks(); + } + + if (lastRetrievedId.isValid()) + currentScansInProgress.setObjectId(context.clientInfo, songOffset + songCount, lastRetrievedId); + } } return response; @@ -127,5 +232,4 @@ namespace lms::api::subsonic { return handleSearchRequestCommon(context, true /* id3 */); } - } \ No newline at end of file From 5bfd31ff8a6f9312417d66c2140c0d47dde076d6 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 18 Mar 2024 12:01:48 +0100 Subject: [PATCH 02/64] Scan tracker: take into account MediaLibrary --- .../subsonic/impl/entrypoints/Searching.cpp | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index a8669b99..b9a68b91 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -47,21 +47,23 @@ namespace lms::api::subsonic class ScanTracker { public: - ObjectId extractLastRetrievedObjectId(const ClientInfo& info, std::size_t offset); - void setObjectId(const ClientInfo& info, std::size_t offset, ObjectId lastRetrievedId); + struct ScanInfo + { + std::string clientAddress; + std::string clientName; + std::string userName; + MediaLibraryId library; + std::size_t offset{}; + auto operator<=>(const ScanInfo&) const = default; + }; + + ObjectId extractLastRetrievedObjectId(const ScanInfo& info); + void setObjectId(const ScanInfo& info, ObjectId lastRetrievedId); void cleanOutdatedScanEntries(); private: using ClockType = std::chrono::steady_clock; - struct Client - { - std::string clientAddress; - std::string clientName; - std::string userName; - std::size_t offset{}; - auto operator<=>(const Client&) const = default; - }; struct Entry { ClockType::time_point timePoint; @@ -71,16 +73,16 @@ namespace lms::api::subsonic static constexpr ClockType::duration maxEntryDuration{ std::chrono::seconds{30} }; std::mutex _mutex; - std::map _ongoingScans; + std::map _ongoingScans; }; template - ObjectId ScanTracker::extractLastRetrievedObjectId(const ClientInfo& info, std::size_t offset) + ObjectId ScanTracker::extractLastRetrievedObjectId(const ScanInfo& scanInfo) { ObjectId res; const std::scoped_lock lock{ _mutex }; - auto it{ _ongoingScans.find({ info.ipAddress, info.name, info.user, offset }) }; + auto it{ _ongoingScans.find(scanInfo) }; if (it != _ongoingScans.end()) { res = it->second.objectId; @@ -91,10 +93,10 @@ namespace lms::api::subsonic } template - void ScanTracker::setObjectId(const ClientInfo& info, std::size_t offset, ObjectId lastRetrievedId) + void ScanTracker::setObjectId(const ScanInfo& scanInfo, ObjectId lastRetrievedId) { const std::scoped_lock lock{ _mutex }; - _ongoingScans[Client{ info.ipAddress, info.name, info.user, offset }] = { ClockType::now(), lastRetrievedId }; + _ongoingScans[scanInfo] = { ClockType::now(), lastRetrievedId }; } template @@ -201,7 +203,16 @@ namespace lms::api::subsonic } else { - if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(context.clientInfo, songOffset) }; cachedLastRetrievedId.isValid()) + ScanTracker::ScanInfo scanInfo + { + .clientAddress = context.clientInfo.ipAddress, + .clientName = context.clientInfo.name, + .userName = context.clientInfo.user, + .library = mediaLibrary, + .offset = songOffset + }; + + if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) { Track::find(context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) { @@ -215,7 +226,10 @@ namespace lms::api::subsonic } if (lastRetrievedId.isValid()) - currentScansInProgress.setObjectId(context.clientInfo, songOffset + songCount, lastRetrievedId); + { + scanInfo.offset = songOffset + songCount; + currentScansInProgress.setObjectId(scanInfo, lastRetrievedId); + } } } From 78384d5d3f9ea0c5e7647d9b1d1d827d0a392838 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 19 Mar 2024 09:08:43 +0100 Subject: [PATCH 03/64] Added custom allocator for subsonic responses --- src/libs/subsonic/CMakeLists.txt | 3 + src/libs/subsonic/bench/CMakeLists.txt | 14 +++ src/libs/subsonic/bench/SubsonicBench.cpp | 79 ++++++++++++++ src/libs/subsonic/impl/SubsonicResource.cpp | 20 +++- src/libs/subsonic/impl/SubsonicResponse.cpp | 30 ++--- src/libs/subsonic/impl/SubsonicResponse.hpp | 26 +++-- .../impl/SubsonicResponseAllocator.hpp | 75 +++++++++++++ .../impl/TLSMonotonicMemoryResource.hpp | 103 ++++++++++++++++++ 8 files changed, 324 insertions(+), 26 deletions(-) create mode 100644 src/libs/subsonic/bench/CMakeLists.txt create mode 100644 src/libs/subsonic/bench/SubsonicBench.cpp create mode 100644 src/libs/subsonic/impl/SubsonicResponseAllocator.hpp create mode 100644 src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 8341084c..5665725c 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -58,3 +58,6 @@ target_link_libraries(lmssubsonic PUBLIC install(TARGETS lmssubsonic DESTINATION ${CMAKE_INSTALL_LIBDIR}) +if (BUILD_BENCHMARKS) + add_subdirectory(bench) +endif() \ No newline at end of file diff --git a/src/libs/subsonic/bench/CMakeLists.txt b/src/libs/subsonic/bench/CMakeLists.txt new file mode 100644 index 00000000..a16798c4 --- /dev/null +++ b/src/libs/subsonic/bench/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(bench-subsonic + SubsonicBench.cpp + ) + +target_include_directories(bench-subsonic PRIVATE + ../impl + ) + +target_link_libraries(bench-subsonic PRIVATE + lmscore + lmsdatabase + lmssubsonic + benchmark + ) \ No newline at end of file diff --git a/src/libs/subsonic/bench/SubsonicBench.cpp b/src/libs/subsonic/bench/SubsonicBench.cpp new file mode 100644 index 00000000..123bc39a --- /dev/null +++ b/src/libs/subsonic/bench/SubsonicBench.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic::benchs +{ + namespace + { + Response generateFakeResponse() + { + Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + + Response::Node& node{ response.createNode("MyNode") }; + node.setAttribute("Attr1", "value1"); + node.setAttribute("Attr2", "value2"); + + for (std::size_t i{}; i < 100; ++i) + { + Response::Node& childNode{ node.createArrayChild("MyArrayChild") }; + childNode.setAttribute("Attr42", i); + + node.addArrayValue("MyArray1", "value1"); + node.addArrayValue("MyArray1", "value2"); + for (std::size_t j{}; j < i; ++j) + node.addArrayValue("MyArray2", j); + } + + return response; + } + } + + static void BM_SubsonicResponse_generate(benchmark::State& state) + { + for (auto _ : state) + { + Response response{ generateFakeResponse() }; + benchmark::DoNotOptimize(response); + TLSMonotonicMemoryResource::getInstance().reset(); + } + } + + template + static void BM_SubsonicResponse_serialize(benchmark::State& state) + { + const Response response{ generateFakeResponse() }; + + for (auto _ : state) + { + std::ostringstream oss; // TODO find something more optimized + response.write(oss, responseFormat); + } + } + + BENCHMARK(BM_SubsonicResponse_generate)->Threads(1)->Threads(std::thread::hardware_concurrency()); + BENCHMARK(BM_SubsonicResponse_serialize); + BENCHMARK(BM_SubsonicResponse_serialize); +} + +BENCHMARK_MAIN(); \ No newline at end of file diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index e709a96b..b7330368 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -159,7 +159,7 @@ namespace lms::api::subsonic CheckImplementedFunc checkFunc{}; }; - static const std::unordered_map requestEntryPoints + const std::unordered_map requestEntryPoints { // System {"/ping", {handlePingRequest}}, @@ -266,13 +266,26 @@ namespace lms::api::subsonic }; using MediaRetrievalHandlerFunc = std::function; - static std::unordered_map mediaRetrievalHandlers + const std::unordered_map mediaRetrievalHandlers { // Media retrieval {"/download", handleDownload}, {"/stream", handleStream}, {"/getCoverArt", handleGetCoverArt}, }; + + struct TLSMonotonicMemoryResourceCleaner + { + TLSMonotonicMemoryResourceCleaner() = default; + ~TLSMonotonicMemoryResourceCleaner() + { + TLSMonotonicMemoryResource::getInstance().reset(); + } + + private: + TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete; + TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete; + }; } SubsonicResource::SubsonicResource(db::Db& db) @@ -287,7 +300,8 @@ namespace lms::api::subsonic { static std::atomic curRequestId{}; - const std::size_t requestId{ curRequestId++ }; + const std::size_t requestId{ curRequestId++ }; + TLSMonotonicMemoryResourceCleaner memoryResourceCleaner; LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap())); diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index ea1cc831..f82f7305 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -44,7 +44,7 @@ namespace lms::api::subsonic void Response::Node::setValue(std::string_view value) { assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty()); - _value = std::string{ value }; + _value = string{ value }; } void Response::Node::setValue(long long value) @@ -55,13 +55,13 @@ namespace lms::api::subsonic void Response::Node::setAttribute(Key key, std::string_view value) { - _attributes[key] = std::string{ value }; + _attributes[key] = string{ value }; } void Response::Node::addChild(Key key, Node&& node) { assert(!_value); - assert(_children.find(key) == std::cend(_children)); + assert(!_children.contains(key)); _children[key] = std::move(node); } @@ -69,29 +69,29 @@ namespace lms::api::subsonic { assert(!_value); assert(_children.find(key) == std::cend(_children)); - _childrenArrays.emplace(key, std::vector{}); + _childrenArrays.emplace(key, vector{}); } void Response::Node::addArrayChild(Key key, Node&& node) { assert(!_value); - assert(_children.find(key) == std::cend(_children)); + assert(!_children.contains(key)); _childrenArrays[key].emplace_back(std::move(node)); } void Response::Node::createEmptyArrayValue(Key key) { assert(!_value); - assert(_children.find(key) == std::cend(_children)); + assert(!_children.contains(key)); _childrenValues.emplace(key, ValuesType{}); } void Response::Node::addArrayValue(Key key, std::string_view value) { assert(!_value); - assert(_children.find(key) == std::cend(_children)); + assert(!_children.contains(key)); auto& values{ _childrenValues[key] }; - values.push_back(std::string{ value }); + values.emplace_back(string{ value }); assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();})); } @@ -99,7 +99,7 @@ namespace lms::api::subsonic { assert(!_value); auto& values{ _childrenValues[key] }; - values.push_back(value); + values.emplace_back(value); assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();})); } @@ -112,7 +112,7 @@ namespace lms::api::subsonic Response::Node& Response::Node::createArrayChild(Key key) { assert(!_value); - assert(_children.find(key) == std::cend(_children)); + assert(!_children.contains(key)); _childrenArrays[key].emplace_back(); return _childrenArrays[key].back(); } @@ -192,8 +192,8 @@ namespace lms::api::subsonic for (const auto& [key, value] : node._attributes) { - if (std::holds_alternative(value)) - res.put("." + std::string{ key.str() }, std::get(value)); + if (std::holds_alternative(value)) + res.put("." + std::string{ key.str() }, std::get(value)); else if (std::holds_alternative(value)) res.put("." + std::string{ key.str() }, std::get(value)); else if (std::holds_alternative(value)) @@ -240,7 +240,7 @@ namespace lms::api::subsonic return res; }; - boost::property_tree::ptree root{ nodeToPropertyTree(_root) }; + const boost::property_tree::ptree root{ nodeToPropertyTree(_root) }; boost::property_tree::write_xml(os, root); } @@ -337,9 +337,9 @@ namespace lms::api::subsonic void Response::JsonSerializer::serializeValue(std::ostream& os, const Node::ValueType& value) { - if (std::holds_alternative(value)) + if (std::holds_alternative(value)) { - serializeEscapedString(os, std::get(value)); + serializeEscapedString(os, std::get(value)); } else if (std::holds_alternative(value)) { diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index cd80fe1c..c1ecc59a 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -27,6 +27,7 @@ #include "core/LiteralString.hpp" #include "RequestContext.hpp" +#include "SubsonicResponseAllocator.hpp" namespace lms::api::subsonic { @@ -240,14 +241,23 @@ namespace lms::api::subsonic void setVersionAttribute(ProtocolVersion version); friend class Response; - using ValueType = std::variant; - std::map _attributes; - std::optional _value; - std::map _children; - std::map> _childrenArrays; - using ValuesType = std::vector; - std::map _childrenValues; + template + using map = std::map< Key, Value, std::less, ResponseAllocator>>; + + template + using vector = std::vector>; + + using string = std::basic_string, ResponseAllocator>; + + using ValueType = std::variant; + map _attributes; + std::optional _value; + map _children; + map> _childrenArrays; + + using ValuesType = vector; + map _childrenValues; }; static Response createOkResponse(ProtocolVersion protocolVersion); @@ -272,7 +282,7 @@ namespace lms::api::subsonic { public: void serializeNode(std::ostream& os, const Node& node); - void serializeValue(std::ostream& os, const Node::ValueType& node); + void serializeValue(std::ostream& os, const Node::ValueType& value); void serializeEscapedString(std::ostream&, std::string_view str); }; diff --git a/src/libs/subsonic/impl/SubsonicResponseAllocator.hpp b/src/libs/subsonic/impl/SubsonicResponseAllocator.hpp new file mode 100644 index 00000000..b8195a08 --- /dev/null +++ b/src/libs/subsonic/impl/SubsonicResponseAllocator.hpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ +#pragma once + +#include "TLSMonotonicMemoryResource.hpp" + +namespace lms::api::subsonic +{ + // Stateless allocator that uses a shared MemoryResource + template + class Allocator + { + public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + constexpr Allocator() noexcept = default; + + template + constexpr Allocator(const Allocator&) noexcept {} + + template + struct rebind + { + using other = Allocator; + }; + + [[nodiscard]] pointer allocate(size_type n) + { + return reinterpret_cast(MemoryResource::getInstance().allocate(n * sizeof(T), alignof(T))); + } + + // Deallocate memory pointed to by p + void deallocate(pointer p, std::size_t) noexcept + { + MemoryResource::getInstance().deallocate(reinterpret_cast(p)); + } + }; + + template + bool operator==(const Allocator &, const Allocator &) + { + return true; + } + + template + bool operator!=(const Allocator &, const Allocator &) + { + return false; + } + + template + using ResponseAllocator = Allocator; +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp b/src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp new file mode 100644 index 00000000..07e03639 --- /dev/null +++ b/src/libs/subsonic/impl/TLSMonotonicMemoryResource.hpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace lms::api::subsonic +{ + class TLSMonotonicMemoryResource + { + public: + static TLSMonotonicMemoryResource& getInstance() + { + static thread_local TLSMonotonicMemoryResource instance; + return instance; + } + + TLSMonotonicMemoryResource() + { + allocateNewBlock(); + } + + [[nodiscard]] std::byte* allocate(std::size_t byteCount, std::size_t alignment) + { + std::byte* currentAddrAligned{ computeAlignedAddr(_currentAddr, alignment) }; + + if (currentAddrAligned + byteCount > &_currentBlock->back() + 1) + { + allocateNewBlock(); + currentAddrAligned = computeAlignedAddr(_currentAddr, alignment); + } + + // Requested too many bytes for blockSize! + if (currentAddrAligned + byteCount > &_currentBlock->back() + 1) + throw std::bad_alloc{}; + + assert(currentAddrAligned >= &_currentBlock->front()); + + std::byte* res{ currentAddrAligned }; + _currentAddr = currentAddrAligned + byteCount; + + return res; + } + + void deallocate(std::byte*) + { + // nothing to do! + } + + void reset() + { + // always keep at least one block + if (_blocks.size() > 1) + _blocks.erase(std::next(std::cbegin(_blocks), 1), std::cend(_blocks)); + + _currentBlock = &_blocks.front(); + _currentAddr = _currentBlock->data(); + } + + private: + void allocateNewBlock() + { + _currentBlock = &_blocks.emplace_back(); + _currentAddr = _currentBlock->data(); + } + + static std::byte* computeAlignedAddr(std::byte* addr, std::size_t alignment) noexcept + { + const std::uintptr_t addrValue{ reinterpret_cast(addr) }; + + const std::uintptr_t mask{ alignment - 1 }; + const std::uintptr_t adjustment{ (alignment - (addrValue & mask)) & mask }; + return reinterpret_cast(addr + adjustment); + } + + static constexpr std::size_t blockSize{ static_cast(1 * 1024 * 1024) }; + using BlockType = std::array; + + std::list _blocks; + BlockType* _currentBlock{}; + std::byte* _currentAddr{}; + }; +} \ No newline at end of file From ac3643709fa1fb40d8dc46a3e819c82f3ec27e8e Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 22 Mar 2024 19:41:41 +0100 Subject: [PATCH 04/64] Added missing include --- src/libs/metadata/impl/TagLibTagReader.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/metadata/impl/TagLibTagReader.cpp b/src/libs/metadata/impl/TagLibTagReader.cpp index 7f26f601..340d6a11 100644 --- a/src/libs/metadata/impl/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/TagLibTagReader.cpp @@ -19,6 +19,8 @@ #include "TagLibTagReader.hpp" +#include + #include #include #include From 1630b7cb6ffec4c2d0dc97bc061a45ecf1cb8772 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 23 Mar 2024 15:55:29 +0100 Subject: [PATCH 05/64] Scan tracker: now handle releases and artists --- src/libs/database/impl/Artist.cpp | 38 +- src/libs/database/impl/Release.cpp | 28 +- src/libs/database/include/database/Artist.hpp | 1 + src/libs/database/include/database/IdType.hpp | 66 ++-- .../database/include/database/Release.hpp | 1 + src/libs/database/test/Artist.cpp | 142 ++++++-- src/libs/database/test/Release.cpp | 90 +++++ .../subsonic/impl/entrypoints/Searching.cpp | 324 ++++++++++++------ 8 files changed, 514 insertions(+), 176 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 3c0bf85e..d0a1d0b1 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -40,6 +40,7 @@ namespace lms::db { session.checkReadTransaction(); + // TODO remove distinct and use group by auto query{ session.getDboSession().query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") }; if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() @@ -64,13 +65,13 @@ namespace lms::db std::vector clauses; std::vector sortClauses; - for (std::string_view keyword : params.keywords) + for (const std::string_view keyword : params.keywords) { clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); } - for (std::string_view keyword : params.keywords) + for (const std::string_view keyword : params.keywords) { sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); @@ -153,7 +154,7 @@ namespace lms::db Wt::Dbo::Query createQuery(Session& session, const Artist::FindParameters& params) { std::string_view itemToSelect; - + if constexpr (std::is_same_v) itemToSelect = "a.id"; else if constexpr (std::is_same_v>) @@ -184,6 +185,31 @@ namespace lms::db return session.getDboSession().query("SELECT COUNT(*) FROM artist"); } + void Artist::find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function& func, MediaLibraryId library) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession().query>("SELECT a FROM artist a") + .orderBy("a.id") + .where("a.id > ?").bind(lastRetrievedArtist) + .limit(static_cast(count)) }; + + if (library.isValid()) + { + query.join("track t ON t.id = t_a_l.track_id"); + query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); + query.where("t.media_library_id = ?").bind(library); + } + + auto collection{ query.resultList() }; + + for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) + { + func(*itResult); + lastRetrievedArtist = (*itResult)->getId(); + } + } + std::vector Artist::find(Session& session, std::string_view name) { session.checkReadTransaction(); @@ -286,7 +312,7 @@ namespace lms::db .groupBy("a.id") .orderBy("COUNT(*) DESC, RANDOM()") }; - for (TrackArtistLinkType type : artistLinkTypes) + for (const TrackArtistLinkType type : artistLinkTypes) query.bind(type); return utils::execQuery(query, range); @@ -304,7 +330,7 @@ namespace lms::db where.And(WhereClause("a.id = ?")).bind(getId().toString()); { WhereClause clusterClause; - for (ClusterTypeId clusterTypeId : clusterTypeIds) + for (const ClusterTypeId clusterTypeId : clusterTypeIds) clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString()); where.And(clusterClause); @@ -320,7 +346,7 @@ namespace lms::db Wt::Dbo::collection> queryRes = query; std::map> clustersByType; - for (Cluster::pointer cluster : queryRes) + for (const Cluster::pointer& cluster : queryRes) { if (clustersByType[cluster->getType()->getId()].size() < size) clustersByType[cluster->getType()->getId()].push_back(cluster); diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index f13769f1..6547ef0f 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -87,7 +87,7 @@ namespace lms::db .where("s_r.sync_state <> ?").bind(SyncState::PendingRemove); } - if (params.artist.isValid() + if (params.artist.isValid() || params.sortMethod == ReleaseSortMethod::ArtistNameThenName) { query.join("artist a ON a.id = t_a_l.artist_id") @@ -293,6 +293,30 @@ namespace lms::db return utils::execQuery(query, range); } + void Release::find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function& func, MediaLibraryId library) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession().query>("SELECT r FROM release r") + .orderBy("r.id") + .where("r.id > ?").bind(lastRetrievedRelease) + .limit(static_cast(count)) }; + + if (library.isValid()) + { + query.join("track t ON t.release_id = r.id"); + query.where("t.media_library_id = ?").bind(library); + } + + auto collection{ query.resultList() }; + + for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) + { + func(*itResult); + lastRetrievedRelease = (*itResult)->getId(); + } + } + RangeResults Release::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); @@ -382,7 +406,7 @@ namespace lms::db return dates.front(); } - + std::optional Release::getYear() const { return getYear(false); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 9fb728c3..27994799 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -86,6 +86,7 @@ namespace lms::db static pointer find(Session& session, const core::UUID& MBID); static pointer find(Session& session, ArtistId id); static std::vector find(Session& session, std::string_view name); // exact match on name field + static void find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function& func, MediaLibraryId library = {}); static RangeResults find(Session& session, const FindParameters& parameters); static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); diff --git a/src/libs/database/include/database/IdType.hpp b/src/libs/database/include/database/IdType.hpp index 678728f7..972c9c46 100644 --- a/src/libs/database/include/database/IdType.hpp +++ b/src/libs/database/include/database/IdType.hpp @@ -26,44 +26,44 @@ namespace lms::db { - class IdType - { - public: - using ValueType = Wt::Dbo::dbo_default_traits::IdType; + class IdType + { + public: + using ValueType = Wt::Dbo::dbo_default_traits::IdType; - IdType() = default; - IdType(ValueType id) : _id {id} { assert(isValid()); } + IdType() = default; + IdType(ValueType id) : _id{ id } { assert(isValid()); } - bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } - std::string toString() const { assert(isValid()); return std::to_string(_id); } + bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } + std::string toString() const { assert(isValid()); return std::to_string(_id); } - ValueType getValue() const { return _id; } + ValueType getValue() const { return _id; } + auto operator<=>(const IdType& other) const = default; \ - auto operator<=>(const IdType& other) const = default; - - private: - Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()}; - }; + private: + Wt::Dbo::dbo_default_traits::IdType _id{ Wt::Dbo::dbo_default_traits::invalidId() }; + }; #define LMS_DECLARE_IDTYPE(name) \ - namespace lms::db { \ - class name : public IdType \ - { \ - public: \ - using IdType::IdType; \ - };\ - } \ - namespace std \ - { \ - template<> \ - class hash \ - { \ - public: \ - size_t operator()(lms::db::name id) const \ - { \ - return std::hash()(id.getValue()); \ - } \ - }; \ - } // ns std + namespace lms::db { \ + class name : public IdType \ + { \ + public: \ + using IdType::IdType; \ + auto operator<=>(const name& other) const = default; \ + };\ + } \ + namespace std \ + { \ + template<> \ + class hash \ + { \ + public: \ + size_t operator()(lms::db::name id) const \ + { \ + return std::hash()(id.getValue()); \ + } \ + }; \ + } // ns std } // namespace lms::db diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 30f4deae..f12a7397 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -122,6 +122,7 @@ namespace lms::db static pointer find(Session& session, const core::UUID& MBID); static std::vector find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory); static pointer find(Session& session, ReleaseId id); + static void find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function& func, MediaLibraryId library = {}); static RangeResults find(Session& session, const FindParameters& parameters); static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 356f7242..bff637df 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -77,6 +77,96 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, Artist_findByRangedIdBased) + { + ScopedTrack track1{ session, "MyTrackFile1" }; + ScopedTrack track2{ session, "MyTrackFile2" }; + ScopedTrack track3{ session, "MyTrackFile3" }; + ScopedArtist artist1{ session, "MyArtist1" }; + ScopedArtist artist2{ session, "MyArtist2" }; + ScopedArtist artist3{ session, "MyArtist3" }; + ScopedMediaLibrary library{ session }; + ScopedMediaLibrary otherLibrary{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track2.get().modify()->setMediaLibrary(library.get()); + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track3.get(), artist3.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction{ session.createReadTransaction() }; + + ArtistId lastRetrievedId; + std::vector visitedArtists; + Artist::find(session, lastRetrievedId, 10, [&](const Artist::pointer& artist) + { + visitedArtists.push_back(artist); + }); + ASSERT_EQ(visitedArtists.size(), 3); + EXPECT_EQ(visitedArtists[0]->getId(), artist1.getId()); + EXPECT_EQ(visitedArtists[1]->getId(), artist2.getId()); + EXPECT_EQ(visitedArtists[2]->getId(), artist3.getId()); + EXPECT_EQ(lastRetrievedId, artist3.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ArtistId lastRetrievedId{ artist1.getId() }; + std::vector visitedArtists; + Artist::find(session, lastRetrievedId, 1, [&](const Artist::pointer& artist) + { + visitedArtists.push_back(artist); + }); + ASSERT_EQ(visitedArtists.size(), 1); + EXPECT_EQ(visitedArtists[0]->getId(), artist2.getId()); + EXPECT_EQ(lastRetrievedId, artist2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ArtistId lastRetrievedId{ artist1.getId() }; + std::vector visitedArtists; + Artist::find(session, lastRetrievedId, 0, [&](const Artist::pointer& artist) + { + visitedArtists.push_back(artist); + }); + ASSERT_EQ(visitedArtists.size(), 0); + EXPECT_EQ(lastRetrievedId, artist1.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ArtistId lastRetrievedId; + std::vector visitedArtists; + Artist::find(session, lastRetrievedId, 10, [&](const Artist::pointer& artist) + { + visitedArtists.push_back(artist); + }, otherLibrary.getId()); + ASSERT_EQ(visitedArtists.size(), 0); + EXPECT_EQ(lastRetrievedId, ArtistId{}); + } + + { + auto transaction{ session.createReadTransaction() }; + + ArtistId lastRetrievedId; + std::vector visitedArtists; + Artist::find(session, lastRetrievedId, 10, [&](const Artist::pointer& artist) + { + visitedArtists.push_back(artist); + }, library.getId()); + ASSERT_EQ(visitedArtists.size(), 1); + EXPECT_EQ(visitedArtists[0]->getId(), artist2.getId()); + EXPECT_EQ(lastRetrievedId, artist2.getId()); + } + } + TEST_F(DatabaseFixture, MultipleArtists) { { @@ -409,46 +499,46 @@ namespace lms::db::tests EXPECT_EQ(artists.front()->getId(), artist1.getId()); EXPECT_EQ(Artist::find(session, R"(MyArtistFoo)").size(), 0); } -{ - const auto artists{ Artist::find(session, R"(%MyArtist)") }; - ASSERT_TRUE(artists.size() == 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + { + const auto artists{ Artist::find(session, R"(%MyArtist)") }; + ASSERT_TRUE(artists.size() == 1); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); EXPECT_EQ(Artist::find(session, R"(FooMyArtist)").size(), 0); + } + { + const auto artists{ Artist::find(session, R"(%_MyArtist)") }; + ASSERT_TRUE(artists.size() == 1); + ASSERT_EQ(artists.front()->getId(), artist3.getId()); + EXPECT_EQ(Artist::find(session, R"(%CMyArtist)").size(), 0); + } } + { - const auto artists{ Artist::find(session, R"(%_MyArtist)") }; - ASSERT_TRUE(artists.size() == 1); - ASSERT_EQ(artists.front()->getId(), artist3.getId()); - EXPECT_EQ(Artist::find(session, R"(%CMyArtist)").size(), 0); -} + auto transaction{ session.createReadTransaction() }; + { + const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) }; + EXPECT_EQ(artists.results.size(), 6); } { - auto transaction{ session.createReadTransaction() }; -{ - const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) }; - EXPECT_EQ(artists.results.size(), 6); - } - - { - const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) }; - ASSERT_EQ(artists.results.size(), 2); + const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) }; + ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], artist1.getId()); - EXPECT_EQ(artists.results[1], artist4.getId()); - } + EXPECT_EQ(artists.results[1], artist4.getId()); + } - { - const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; + { + const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], artist2.getId()); - EXPECT_EQ(artists.results[1], artist5.getId()); + EXPECT_EQ(artists.results[1], artist5.getId()); } { const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist3.getId()); - } + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist3.getId()); + } } } diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 483544bf..22540e51 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -74,6 +74,96 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, Release_findByRangedIdBased) + { + ScopedTrack track1{ session, "MyTrackFile1" }; + ScopedTrack track2{ session, "MyTrackFile2" }; + ScopedTrack track3{ session, "MyTrackFile3" }; + ScopedRelease release1{ session, "MyRelease1" }; + ScopedRelease release2{ session, "MyRelease2" }; + ScopedRelease release3{ session, "MyRelease3" }; + ScopedMediaLibrary library{ session }; + ScopedMediaLibrary otherLibrary{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track2.get().modify()->setMediaLibrary(library.get()); + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + track3.get().modify()->setRelease(release3.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseId lastRetrievedId; + std::vector visitedReleases; + Release::find(session, lastRetrievedId, 10, [&](const Release::pointer& release) + { + visitedReleases.push_back(release); + }); + ASSERT_EQ(visitedReleases.size(), 3); + EXPECT_EQ(visitedReleases[0]->getId(), release1.getId()); + EXPECT_EQ(visitedReleases[1]->getId(), release2.getId()); + EXPECT_EQ(visitedReleases[2]->getId(), release3.getId()); + EXPECT_EQ(lastRetrievedId, release3.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseId lastRetrievedId{ release1.getId() }; + std::vector visitedReleases; + Release::find(session, lastRetrievedId, 1, [&](const Release::pointer& release) + { + visitedReleases.push_back(release); + }); + ASSERT_EQ(visitedReleases.size(), 1); + EXPECT_EQ(visitedReleases[0]->getId(), release2.getId()); + EXPECT_EQ(lastRetrievedId, release2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseId lastRetrievedId{ release1.getId() }; + std::vector visitedReleases; + Release::find(session, lastRetrievedId, 0, [&](const Release::pointer& release) + { + visitedReleases.push_back(release); + }); + ASSERT_EQ(visitedReleases.size(), 0); + EXPECT_EQ(lastRetrievedId, release1.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseId lastRetrievedId; + std::vector visitedReleases; + Release::find(session, lastRetrievedId, 10, [&](const Release::pointer& release) + { + visitedReleases.push_back(release); + }, otherLibrary.getId()); + ASSERT_EQ(visitedReleases.size(), 0); + EXPECT_EQ(lastRetrievedId, ReleaseId{}); + } + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseId lastRetrievedId; + std::vector visitedReleases; + Release::find(session, lastRetrievedId, 10, [&](const Release::pointer& release) + { + visitedReleases.push_back(release); + }, library.getId()); + ASSERT_EQ(visitedReleases.size(), 1); + EXPECT_EQ(visitedReleases[0]->getId(), release2.getId()); + EXPECT_EQ(lastRetrievedId, release2.getId()); + } + } + TEST_F(DatabaseFixture, Release_singleTrack) { ScopedRelease release{ session, "MyRelease" }; diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index b9a68b91..502dd28d 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -23,6 +23,7 @@ #include #include +#include "core/Random.hpp" #include "database/Artist.hpp" #include "database/Release.hpp" #include "database/Session.hpp" @@ -59,7 +60,6 @@ namespace lms::api::subsonic ObjectId extractLastRetrievedObjectId(const ScanInfo& info); void setObjectId(const ScanInfo& info, ObjectId lastRetrievedId); - void cleanOutdatedScanEntries(); private: using ClockType = std::chrono::steady_clock; @@ -70,6 +70,7 @@ namespace lms::api::subsonic ObjectId objectId; }; + static constexpr std::size_t maxScanCount{ 50 }; static constexpr ClockType::duration maxEntryDuration{ std::chrono::seconds{30} }; std::mutex _mutex; @@ -81,12 +82,15 @@ namespace lms::api::subsonic { ObjectId res; - const std::scoped_lock lock{ _mutex }; - auto it{ _ongoingScans.find(scanInfo) }; - if (it != _ongoingScans.end()) { - res = it->second.objectId; - _ongoingScans.erase(it); + const std::scoped_lock lock{ _mutex }; + + auto it{ _ongoingScans.find(scanInfo) }; + if (it != _ongoingScans.end()) + { + res = it->second.objectId; + _ongoingScans.erase(it); + } } return res; @@ -94,18 +98,212 @@ namespace lms::api::subsonic template void ScanTracker::setObjectId(const ScanInfo& scanInfo, ObjectId lastRetrievedId) - { - const std::scoped_lock lock{ _mutex }; - _ongoingScans[scanInfo] = { ClockType::now(), lastRetrievedId }; - } - - template - void ScanTracker::cleanOutdatedScanEntries() { const ClockType::time_point now{ ClockType::now() }; const std::scoped_lock lock{ _mutex }; + + // clean outdated scan entries; we do this to not have to flush everything each time we add/remove entries in the database std::erase_if(_ongoingScans, [&](const auto& entry) { return now > entry.second.timePoint + maxEntryDuration; }); + // prevent the cache size from going out of control + if (_ongoingScans.size() == maxScanCount) + _ongoingScans.erase(core::random::pickRandom(_ongoingScans)); + + _ongoingScans[scanInfo] = { now, lastRetrievedId }; + } + + void findRequestedArtists(RequestContext& context, bool id3, const std::vector& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) + { + static ScanTracker currentScansInProgress; + + const std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; + if (artistCount == 0) + return; + + if (artistCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; + + const std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; + + ArtistId lastRetrievedId; + auto findArtists{ [&] + { + Artist::FindParameters params; + params.setKeywords(keywords); + params.setRange(Range{ artistOffset, artistCount }); + params.setMediaLibrary(mediaLibrary); + + Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) + { + searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + lastRetrievedId = artist->getId(); + }); + } }; + + if (!keywords.empty()) + { + findArtists(); + } + else + { + ScanTracker::ScanInfo scanInfo + { + .clientAddress = context.clientInfo.ipAddress, + .clientName = context.clientInfo.name, + .userName = context.clientInfo.user, + .library = mediaLibrary, + .offset = artistOffset + }; + + if (ArtistId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) + { + Artist::find(context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) + { + searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + }, mediaLibrary); + lastRetrievedId = cachedLastRetrievedId; + } + else + { + findArtists(); + } + + if (lastRetrievedId.isValid()) + { + scanInfo.offset = artistOffset + artistCount; + currentScansInProgress.setObjectId(scanInfo, lastRetrievedId); + } + } + } + + void findRequestedAlbums(RequestContext& context, bool id3, const std::vector& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) + { + static ScanTracker currentScansInProgress; + + const std::size_t albumCount{ getParameterAs(context.parameters, "albumCount").value_or(20) }; + if (albumCount == 0) + return; + + if (albumCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; + + const std::size_t albumOffset{ getParameterAs(context.parameters, "albumOffset").value_or(0) }; + + ReleaseId lastRetrievedId; + + auto findReleases{ [&] + { + Release::FindParameters params; + params.setKeywords(keywords); + params.setRange(Range{ albumOffset, albumCount }); + params.setMediaLibrary(mediaLibrary); + + Release::find(context.dbSession, params, [&](const Release::pointer& release) + { + searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); + lastRetrievedId = release->getId(); + }); + } }; + + if (!keywords.empty()) + { + findReleases(); + } + else + { + ScanTracker::ScanInfo scanInfo + { + .clientAddress = context.clientInfo.ipAddress, + .clientName = context.clientInfo.name, + .userName = context.clientInfo.user, + .library = mediaLibrary, + .offset = albumOffset + }; + + if (ReleaseId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) + { + Release::find(context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) + { + searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); + }, mediaLibrary); + lastRetrievedId = cachedLastRetrievedId; + } + else + { + findReleases(); + } + + if (lastRetrievedId.isValid()) + { + scanInfo.offset = albumOffset + albumCount; + currentScansInProgress.setObjectId(scanInfo, lastRetrievedId); + } + } + } + + void findRequestedTracks(RequestContext& context, const std::vector& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) + { + static ScanTracker currentScansInProgress; + + const std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; + if (songCount == 0) + return; + + if (songCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; + + const std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; + + TrackId lastRetrievedId; + + auto findTracks{ [&] + { + Track::FindParameters params; + params.setKeywords(keywords); + params.setRange(Range{ songOffset, songCount }); + params.setMediaLibrary(mediaLibrary); + + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + searchResultNode.addArrayChild("song", createSongNode(context, track, user)); + lastRetrievedId = track->getId(); + }); + } }; + + if (!keywords.empty()) + { + findTracks(); + } + else + { + ScanTracker::ScanInfo scanInfo + { + .clientAddress = context.clientInfo.ipAddress, + .clientName = context.clientInfo.name, + .userName = context.clientInfo.user, + .library = mediaLibrary, + .offset = songOffset + }; + + if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) + { + Track::find(context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) + { + searchResultNode.addArrayChild("song", createSongNode(context, track, user)); + }, mediaLibrary); + lastRetrievedId = cachedLastRetrievedId; + } + else + { + findTracks(); + } + + if (lastRetrievedId.isValid()) + { + scanInfo.offset = songOffset + songCount; + currentScansInProgress.setObjectId(scanInfo, lastRetrievedId); + } + } } } @@ -118,21 +316,8 @@ namespace lms::api::subsonic std::string_view query{ queryString }; // Optional params - const std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; - const std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; - const std::size_t albumCount{ getParameterAs(context.parameters, "albumCount").value_or(20) }; - const std::size_t albumOffset{ getParameterAs(context.parameters, "albumOffset").value_or(0) }; - const std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; - const std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; - if (artistCount > defaultMaxCountSize) - throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; - if (albumCount > defaultMaxCountSize) - throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; - if (songCount > defaultMaxCountSize) - throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; - // Symfonium adds extra "" if (context.clientInfo.name == "Symfonium") query = core::stringUtils::stringTrim(query, "\""); @@ -142,7 +327,7 @@ namespace lms::api::subsonic keywords = core::stringUtils::splitString(query, ' '); Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; + Response::Node& searchResultNode{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; auto transaction{ context.dbSession.createReadTransaction() }; @@ -150,88 +335,9 @@ namespace lms::api::subsonic if (!user) throw UserNotAuthorizedError{}; - if (artistCount > 0) - { - Artist::FindParameters params; - params.setKeywords(keywords); - params.setRange(Range{ artistOffset, artistCount }); - params.setMediaLibrary(mediaLibrary); - - Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) - { - searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3)); - }); - } - - if (albumCount > 0) - { - Release::FindParameters params; - params.setKeywords(keywords); - params.setRange(Range{ albumOffset, albumCount }); - params.setMediaLibrary(mediaLibrary); - - Release::find(context.dbSession, params, [&](const Release::pointer& release) - { - searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); - }); - } - - if (songCount > 0) - { - static ScanTracker currentScansInProgress; - currentScansInProgress.cleanOutdatedScanEntries(); - - TrackId lastRetrievedId; - - auto findTracks{ [&] - { - Track::FindParameters params; - params.setKeywords(keywords); - params.setRange(Range{ songOffset, songCount }); - params.setMediaLibrary(mediaLibrary); - - Track::find(context.dbSession, params, [&](const Track::pointer& track) - { - searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); - lastRetrievedId = track->getId(); - }); - } }; - - if (!keywords.empty()) - { - findTracks(); - } - else - { - ScanTracker::ScanInfo scanInfo - { - .clientAddress = context.clientInfo.ipAddress, - .clientName = context.clientInfo.name, - .userName = context.clientInfo.user, - .library = mediaLibrary, - .offset = songOffset - }; - - if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) - { - Track::find(context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) - { - searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); - }, mediaLibrary); - lastRetrievedId = cachedLastRetrievedId; - } - else - { - findTracks(); - } - - if (lastRetrievedId.isValid()) - { - scanInfo.offset = songOffset + songCount; - currentScansInProgress.setObjectId(scanInfo, lastRetrievedId); - } - } - } + findRequestedArtists(context, id3, keywords, mediaLibrary, user, searchResultNode); + findRequestedAlbums(context, id3, keywords, mediaLibrary, user, searchResultNode); + findRequestedTracks(context, keywords, mediaLibrary, user, searchResultNode); return response; } From 339b797312c5ac04847c23c45ec4ce25bba82240 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 24 Mar 2024 15:07:45 +0100 Subject: [PATCH 06/64] Subsonic API: fixed getAlbumList with ordering set to alphabeticalByArtist --- src/libs/database/impl/Release.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 6547ef0f..c9ab558d 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -91,8 +91,10 @@ namespace lms::db || params.sortMethod == ReleaseSortMethod::ArtistNameThenName) { query.join("artist a ON a.id = t_a_l.artist_id") - .join("track_artist_link t_a_l ON t_a_l.track_id = t.id") - .where("a.id = ?").bind(params.artist); + .join("track_artist_link t_a_l ON t_a_l.track_id = t.id"); + + if (params.artist.isValid()) + query.where("a.id = ?").bind(params.artist); if (!params.trackArtistLinkTypes.empty()) { From ac69582f4b038b7bf41ee4cae0da462a01028831 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 24 Mar 2024 15:15:36 +0100 Subject: [PATCH 07/64] Changed the default cover image for releases --- approot/images/unknown-cover.jpg | Bin 25322 -> 0 bytes approot/images/unknown-cover.svg | 11 ++++ src/libs/image/CMakeLists.txt | 1 + src/libs/image/impl/SvgImage.cpp | 50 ++++++++++++++++++ src/libs/image/impl/SvgImage.hpp | 42 +++++++++++++++ src/libs/image/include/image/Exception.hpp | 12 ++--- .../image/include/image/IEncodedImage.hpp | 21 ++++---- src/libs/image/include/image/IRawImage.hpp | 21 +++----- src/libs/image/include/image/Image.hpp | 34 ++++++++++++ src/libs/services/cover/impl/CoverService.cpp | 49 +++-------------- src/libs/services/cover/impl/CoverService.hpp | 9 ++-- .../include/services/cover/ICoverService.hpp | 4 +- .../impl/entrypoints/MediaRetrieval.cpp | 2 +- src/lms/main.cpp | 4 +- src/lms/ui/resource/CoverResource.cpp | 4 +- src/tools/cover/LmsCover.cpp | 14 ++--- 16 files changed, 187 insertions(+), 91 deletions(-) delete mode 100755 approot/images/unknown-cover.jpg create mode 100755 approot/images/unknown-cover.svg create mode 100644 src/libs/image/impl/SvgImage.cpp create mode 100644 src/libs/image/impl/SvgImage.hpp create mode 100644 src/libs/image/include/image/Image.hpp diff --git a/approot/images/unknown-cover.jpg b/approot/images/unknown-cover.jpg deleted file mode 100755 index 3adc6dc1dc2f55f423d4e703afb7eb1d88758f14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25322 zcmcG$bzBu)_cwfiL)Rgt^9U;4-F4^?kPbO?x0F)SdFYbvkPZQn?(Q&<5D-zi@fp0X z`+Dx*d*9Fdex5(x&8RbL*7~mR`tCiknHhgC{r&=iD9J0zgOHGrKrrA3^!pAZE~6zU zrLC?dOlJ*evvaYurt{)r=cIe&>2B*}O{b`&K&PZ3r$Fb$&;6K{4sK~>?&;t`=jv(h zVC&(7^?Ma0142PYzF&X_1{PEdR8%k+6$>354Fd-Y2L~Gq8ygpo5Q2+GfQO9@A%zeS z5tERR;NU+XBPAvyBqkxg2SGvsV!)`FsHm94xY)SF|JUtz4~PH*8H5Z%K_UPl6Cj}w zApPzGQGt*^$SC*a{?mX>Fe(~42C$d--|c@n{9Xa!pa3xt6bJ}}bgT7G*#F-vNXUhH zK}pb9#25H~rXtQxl3h=JK$@1$JK<9X+kF$WORe?c|KW#(lHhYZNKrgkB}U$%hmk!S z<)-E{F7$d}_eZ>n=UCwXa0jcAZ>lr6jyJe`r%5x8!ag}AC4o*?GAKmj3hDIt#Xo^m4IgM+BFpS3w$RFvH{iKvZAUWLNHd8Qmp$9^9t^-e??ODc4s4_%Aw^~*=KH-^T0bk3R@S7 zNlkIvXD3CtsS+72usr6JtSYO@0}%4Ae9l4~?nBLnfI(IE!a`+Es2>%rt;U|Bq+GMs z#EF`y!?(8pUbIQYlWvUy5oSZY$eLM0hhx_lGxgI2YPX)G1lKs*GHuDKss+BNkr6DVAr!rB5}MXqUhlu6 zktRg*_mx%1c;l&j8nlaw@@DyPx#hc?Jtoz>m6h-m2Y7Ma>I=~7YP7VpHFRYo%iLg; z#e?&c?GFM$ATnSuMQ*BL`H<@6_w@X zl?8=JZX^lI54+l4v7Y7qRlMLg=qWrV5QkzAA!=TvrD2!J6q#i9No25?oz4wUu}ZRN zfo~)#3Req^HDNx)AD7?g@E~L+H$N7L2lAugL%s2UF@rV9`+_K2L&GMMsl3)PCut_j z(i-}W1Oil3P@kl|$f@l*VgB9=x52C_lmo@;kt!n zyANk6XrGiE5~bQg9`nX(1t)3GbJ}OisO;s0I4pB;n*}NE;!nu|)q_Aj4K90#`vb~s zo0d-ym8pD;56@>SPN+a~NeuJHqyaDxGK%cU_$D4JE(vch*Q-PlF<}tr=BrEo;m#r& zxfhK|c#kwAZsk)DGD*wuSUfuba$=<>v=N`4jq;s2Pe?vL#AuU91YLWT9Y~IlSD`xI zwVuBZLVvWK^K}*)69}x^=kkhc_#0jAp7!QBlM+7R8+t=_bLGpkoXCe~-sp`NS+55p z=Y~fTqCOnl;r;+b;0w#{lBOY`@cVHjig_`03-X&a3#x!+xYn9c%^nNY`u0lhA7CJOx1y$0ja$5LfGCid;#(XP-q_Kx!XFI?#|?TSL6Dn|FYOj& z%j2+1$g4sK9wU%=L~9_X&n+9oEgM6UK&PdlVV_mHDhV8-6QZq5dL~8=lxWl3vXL2} z%iYVVTMQGo*g!9QGZU;yK^tCga0!)|=*+)hu*o zC}5=#EG#UgsHj+?6C|5^21BB@6_}~1X%EuU9*~kExXo_reUEq1f(V&it}s9-W7!D! zaCYxe_f;7=8V&)cY-I35OZoB&xA@LfpsT2o`ZHy5h*(+KpfEBzZCx#Y&J%?NrC2wn z<&&cgIs#EmMRV7riQKHo>CMO_PFP}~Ss(*elp5x_n;9_NSbq%)Rs}M_hZBIYAf>2y zypa&F-#E=+Fb9u zA>E&0+w_Cc1uwd3qP$DRGoqbMRT-w~5VJK_D7Nx=$V6_A`fOMmp`{$ESvL`= zJBdjNH{`n{5Z`^g(M{3+P!Y@a6W>gf6oKsIvC9%;f z9_F8Z^lb;lBcs&_M$XPR8J5j>fQFf6*}vWG6I>sRrVo4u^fdf$F3C?&z60IM39oHk<&RcLc*1}a+z=HI zbAOnl;pfug@sU!x%Al$%l@^GsJ z2I0h~3LrZLfDw+ zO-zDL=Hy!Qe*mGNNrD!|#r;o$^n>M9D49#dkXlu06BA&G!42T(7>a>%DSg78qaG_M z*Lnk-Bq$(EM*k!6bkLtj8kGy73SIb+@L-K_e_srO8z%z{BVc3Ww9kBvYK4=;!K4Qt zu64*l4O1j%(lhX%y4sxqj`R>Z5H8UM`s*j0M!pvFSw+L-pl$}q$Y3;V>^~Wi=>YEL zjL`8Uhh;W4CRpuy_U!I}0q@h0;&Z^Uyo+D(3kFSAMT4pPTE2b5^I}ULv42Ne}6Y{ z->8zdzvYTynsMTwK%=9hk@RcqGq>QdbOvp+EsDX}$rpd_+5T}yMo$;FvdcO8g@a%+ zFi`164$)g=VK9pfNw%eFy^;bB{XeZc;je!+d_uOjgX(<&Y-MakGCB?WWC+V+p-^>g zzut~}_3qpK4dI^_)F;}#jy0B+!&BmjTqyf3(-N*Wy&o^{5%{;m-$epj$jqYK*~iK5 zN;ri4aT&A1%z7(-`}}>A_|MkAt^B!r{I~6YR{!sJpaU}}E(i!28R>q?cR%U-1BQeQ z%>4)m(I7;0^qj;bk_=oL=sZ%SjL?VNyn(>93ImvIA)zAw2Ca9_JPdFZ@1nYnMPIWO zUzcn+T&B#I*s^x8e>!g~dn!WFWmrNO_bR9NdzC0fyLhx#;@Pci1Fd$I*IpgXOKsE| z`GUBj8mG?Za6SX*1|Q#zg}-u>aHWqy#gcMOr|b)Oz?Zj>V5Q zFHsJb(UBhBHw9Z%DtiqNesO0856u~mau_V-cC_q=%7(C5@kYUA&VJkvqV%N!@yX_` zMwW-s)%ww55{9}QlS9LCqVC5tVJ60H8@EL~u3VT2mJWl7GAsexqZ#(B@PPp*@zpa2 zhgapAo-6j#d?(*ac1%sTEw>dYgf8ze)pMF*3Bz70(L?|_*%(_AF0OiqrqucY)ffx% zwzjVPy5owE*rr>o4rP|lF7a(J46Zx)?bgTl5NWYY%H~><=?&LL=$(6XCFWobvRB#|i1n zOa`O+{ED$L0ZZ5X{QOQ%>MxB&-ilbp`*YjfI7DyAa@QB-ym(0h_aYlm#Kk3)O^B#x zy%(l)Cvj*ki`}v+i;{BxnybO}{djCvY{acbLn+^u2jfG*JLqOU)mX7bZx4dCaODb{ zzQ2(xzW|1yplyhV;KnI1IpJ}4(*gjJn8vTUVi8uiSFbPyvdVteXnKPhN=nKH7UfkPs*%0s`LRGb(+?Y>2}rfa z^dqmGe=r#h*URh=40F}RI5-6Cw>`Y%LC7rrhHM9LzVI=h}j^^Icm6B0*PRN9g(_6PX+xng7Eo3BYf)`?Vj%M|1WYpzYq>O?bA z&p)QmFK}fh@Dm&2j`W-7fDgZ1sF&}BDmRcbf71GDn&3A+D}^<*@CXZ?IxO0n)yLz` z!6|+`%R4OFS0M8uBP)n)cEk-LVRN+f}Bb5Jo_TeXsKXR zGe){PV&M^$YP*B0@H1|02qIhDXz<%;x8dTErTHL((3O8((bRBXJv88rNmFFt&+p30 z!fDSse6CW$=j|R<8qU|nI5m$KuX}o-nRf{e6GD|_&iPq0BOT+Mc#j_S(d+h<>13PSOq$l&|Jp9@=Zj#c-25;v{>MxxKV}H#x4nH5xFMK*ts8%UqJUsbAnEJAQ_KIYv zcV}No+e3xyvCR^1`%%Wvte z5|qVC2lah*5w%7MT>p`xkV{!~ieFZ`Imh;EV?!o%gYC}GF1BvoBQ2KA&*UxaZJb(l zQvQi|Ge3!7t;wd%(n^sdY=T z7?mip@5_q`v{kRA^`EoN9S&v6*)PSUE}ct^vMu+En2$ZfHD+h8bOg`s+6>UuWU#uH zJmm}bpi1K#``<81F`AGJHt=V;oFC+tJS~4Z7a(Mnv%dh@klm731gH(jDWUU@k-uRG1`U?%_e(wEnmz>HEH;8sd z3^Lc-3K6_rmR)u2<~k_6O0M)z>!2wvT`b%9{!aU(qmz;OK=5RpWGW%gg~XaDsIIQA z@2e~S556CjcQKBHpENIu&Z7N#xX-sACuWdG$^QmDI8`?Bu<(eBDI|MVU;j!K6?)z8 zYb5ZXHucc7_|z$yYR&io%MXJNJCf&BhX8G&pbuZ;mhE1Z8{!+Y_0w*5S_=#a559L(#uV_~tjWWj&F8@|!? zb*}5w?ae|t*7Jmt$5^s(c~kV$S3Ggd@|vhd5`OTU5KGN(Pwcc8ce05$;G*t7@o#(! z;>B~Pn{Nf49=Hk7Y!7^F5uzW)v)L5Fjrk1{M%gGIJiGAZ*thID@9H5U?&ng8iCB{F zR(A*5%g^(pKK22O#EpBe-n~jvGOE8di1BLgw0cXU=S#b@w}|xN>*UL8#)0%*nL!%C zU5n~CR-4t&3TF$Icb|L9euJ#peuIX7gQ!CL>0w&L{pMRF1+7;hd+HV9b&OY~)=6c) z;-;r366c>C#~BQ*e4B(`wSS_0U(Runsj#>A$(ko|=?*7V9CbF30y7kq@uRRujqi;<&aSDuyL!0iR z;R#1HnkypVFMbM7bcv@;llyb$b~xWEH6gtLB;VDoE1QkeEH@ybLx?o8)8_nB@*Iw!vmZp}jOO`;&gx8V zLR7Kix9`+MKlfXA@aZ&(hBIbtik2pOy^%==(qvE4?Nbl5$Ddr@(pFT(I9`k_{aogv zOh`!2^^jd#Jbko#VZaYqaWMnh!?i&!Km)2c zYp$VfKnkuEw>4#VjUN5hcWMEdR!(?EJcs#W(N9+E0}7_e>BSEXvr^Z}!*#z;YK-iP zG5;{|w~{+}G5ZiVeAQWg;8Sc|UT(C~?v<$ajqhvGs#itHgN%u-n?l)f)(Kuz?Z9zO zc3{7P^CMi<6Q}XIqM1B-V9?N0x<_ZE#7QnPdJVRgJ+HYpQGT{7MzZ;t5@Rf;-*(hZ zbImi>acAkiY4R0+8=o~Fh>zEZ8>lo5;aAxd!m~)<7Rm;|fkyw-k}UR}>jit+E~eCu zn4(jTD4PPUR`tB_M<1;q%DQcrUaO@<(G~dOJgm3*>MEEO86|7Q#Nlqf@M3~#V36>dg!tqoPknrve($k`VhrFxg@;b$v`R9w z{WMf3CXwQdST(lY^2>CtTH&ooNXnH}dFkNKpENFeegdO=S(PQ9O2<1&)#guSQ1Xmo zs0AohtHoC-yogac|>48pb0h!?cb4KeZv!3oACOxMPy%oz-(Dv<>Aw# zj|R=}iwE2X?HgWZmZ!u&KzaU7TMl6DkD&q&6W=jO?sISv%)&69;T!u6YPRiUD+yrd zm@Ky#9CpMfJm0w;b6^&T56dgWjg5`%bbbdJ#(5dazc7d`m1LF*G%%V`ZU*=SWN9@)hz0}IS)6c zFN8~kV;WpOrDSxy7pJOz(Mko=EqJebAHt#av&)l#n~|OiLt&pqVorGBAq_iq8ActglS-$WvTXbs53XeURzhg11eyZ}eKi;#o-elRu&hM?r zS{nQCuYbPwFEd^m_HFsu1~f0Z7xPc$-tC`^v+2-|)?*FrWy<<<=d7@(s!32h@ZvT_ zyKVWAQ>GI{CB}3szmLoH^m3KlSKlhfE0>k^sv)f>lzrcVw)p&WQ)X<2uFXfWo;6X7 z=Nw-JpuV^0lmU70$@bV_F{;EJ$l`EWr{24PoC>)fZ^ z+TWMFhIyx3)6?7QWyr4e13dAyj%XEAl1~n@T)7&fZ0^|lVMEh9tz)|_aHrjrhz*lc zsdJSI*eM#lZry8hoC+)V$YH{F7!CXpN%O7=r-DPrmP-K-&&fIkHxIA;ENGEqoHr@f z8q^0{--Ii(P6@E?z8FeV(3(84WWJJad{vYuQ`}5Ht0gP!`{=D~e~^io_HWRy&-afP z3I6S|MWR0zyJTLP5pGzyPD7-h0lc?eLc7@Rsy)^Q`Mqjg~Ks zqyq1Q`er$GQ=Aw%PgU8+k8uA2!fd$-H;xA=qd>boRbwa;rdp*UmG4*YnhEegOJ{KT{d{w{RK-m1) zSbFnUBJT99@Fn~fnWVhvhEsVqexvHwiir4V)rJS();r1>$rmTVCCZ0n4aJO?TR9AjT zp-cETibO)@TJ&>Xng;G~_weLA>*zcl!>Q!=>QNE7e(BUxSyP#%Vei>E*wsRK!YUFv$XfkM&*xJ7UrP8YO7w;URg4!>3F-(TKr^ZW67P!+85tVovjo zv&pS7QbQ)CO?=&tUGRrRU2?&2^W}mAm7gMz^s9*47)LR7yEUjp?`YPt+q<87W48gm z0n+`a5@~-Gqa5w17dabia$tsT`_$-$jX*P z`-C& z)Oq7~ES^PKRj_oP-Yg)b&-zf;6jUE-@u}apdueH8<5^R5H2Fb6m2At`$TpvL=}(_z zJO}oralDRgcBlig(VTGi7gb1dzNN0l{a82hEXz1|cs~Q>?H1}X!nn-jmxppz~fVqK_-mG7@ zjkJ#is!P{DFXi*umsjP(h1Lg!-%rRFcN;x@QAIlSC35msU!~4{n!sISdL`Y@zR=cn+%Hxk z;NbFje93>kSCCI?d@O)jWZLIUDfSk7a>_r?3oSmFifGY>0>$)P9lc#(r$zX%W9${o zKL9)a)cQ*hTH(~%lFLE>6;V3hmuj!1T232rO|NCpg^LpwPt!Ya?HfazEF)~=DEoou zM8In!RN>~e+xnV#Uu&apm5IW-cg`4&%bg?--OCK*Xqhe!Zqcm2b=i1->rNqGf=zkr z9zq8@o!EtB1TxR#F=aQ5K4;o^u~wg-J;C9{q*23l)Jbg{>g-YDYy322Ov@tTr!g@z z8=;yQE}XAJ-o=i0XqaqOh2C%S;`7#s_cPrP{>9A~UowSRQ9=_uRw9=3vW##mR|jnt z7eY+z2_`}l6YT1AY+GMqKYBVdIR1=T3j19ohT#tsKif|$J_=5+H?KLAjHk{}S9`dw zu~Oc}2@_Q}qj?Tv-1OiIHWXAWTKY>@zQoFGw)>^8iN7oGY!x55VZ@(Q*Rm(d?nQWUOQndvML!_R3z8n>m2bB2ZM6H*ZqY*94-~}-)2&@leQy>euJVN zrIL?487=lP#6@BUWd;icpS3yiFLIzNq|srsx4?IrfCSxi2s;!!lM6KfzY&* z_APf>$48q!L>6PWV=zxb?^(DVp3BG_zb$GF3A)xA6I8*VpL3GjRHj+8IkDfXTz-1Y z@~40SU&=F`RJ82Re%DNGPssIMfKsS_-_7CbzfC-)o)N?bE#A-+S#=F2^VLTY7VEAi#TKxUj6y9OsM2ot*#V% zS>2nI|1dj4tV8=YC*6v=Ok66h%wcwI$AD^fAavW!iz170>9aoHW4BD`RCmanzpVS+v zeq0Njv%J@6NbjOxEqOU4&fF}Y^&qQaepESO|OKN-b zeT7PRdd>JZDE1;0ZieUb55U|^GwM4*fl(AKo@_{q*3H!KU|LSI#q@K&7r!MvQiPAuz6mue(<$zY&5svRD%B-`zN{irqY_b$P4n6JF3{$ z^t__yPu$2OV}~p&ge*h3&MO}OI)U6R=AXFRI$u^DQ^lKi1fVOOj(M%+USd5&$V^qT zpw92uxv$rljuIWGZX0vP`edJZtmO3ZCfK@g>cyyP*SWLF;O1{zpR-*@6gnyIz6^Jz zTg+Lk^OnFllGxzh^;V6a#fcj{3IBom{0j2TojY}JSR*qGiwp;4CBY!P=$y)Y)6S60 z+B>L4YR3rweAO45pYFs>`=j?m&YH;&ba8@3uaKWhlVD2BIRBVarsw~m&UPE>vz)_m zMV>)r9wX2F6-r~H%Z8sg*t%HMiznbqSxosToB6YwV}G5gy2b4d^6HY>v*YP$$*G03 z?{9-{6va%pL$wJ*Yg0(0BEORjQ82^OWxK6jmnwcqcMYQ?b1EmL=RHavWZ~hu&Rg@X z`VErVk?z3XPFqC`jrvj6#Te$?M)XxuiafN03}ToNetihb(P1SbuV5GK#i{SLAjfDn zoERA5>zg-p=cqH$>g8VLyJ;=>fzW3qFzp#^6H8k*l^fE0qTCeD61nyg>n$TKR_gN~ z=v2O=KKXTFd;8>09n%2?(xJ}p+g{dQ2nldB9G3Qaxd$Nh5m=FaKg(a$NLqmTXt(~~9f@BezrBC}>57e@I-@q;VDa_(Fb`;a26`RF0)c8&VQ4r{)h3K6Yk zY$`4PrQ4QD3$e74cNl|xY!5ybnIO?)k5-eSpEA!1kY-v8J#YpCj=bB%rR59#uO*BO zEQ^^a?pWl()(Q2-p)J?cg%+jO$CCxz7~8#%l)D}JdP4A}uh@oXNc@DnT6^;=*upN* zETGfyhdZyhq2qU(M7dw-+sbu$ogPb{jr;8P9GvCbut1gt3yt#9N^voue|PEtpP{ZKOv zLuUJn`#U8o<~P(xO~sz1B;42@g${mGQ+-(3y|_IJaM_{lNSdv>LKm*(uZ7r*Y&XP0 z*-$k&Kc6qhN*#XrD`vTlzE%{ngmXs*&>R*Al3N`&MgI|+9xK*5yxD}tSkJ|E_cMh; zVlTsfV(507@uRiY(JJr6v$uKg{cj#`G;jnto!kz+d5h8!?i`p#Bx{@hYe}q#JyJ6; z7D2#$2{(uiuSAgyR-$;l&4WA)o3X$a46RZuerVRZo~2v zY`aA}`_x0{;_|FVWl9igGLO2+37&nOZKE%yoITP+ZMjv}O5FNv+GxMp`5V;g&Z>Li z7y7zt#bmq_y?Su{EEq#~o%<;hq&Z|tS`Q@)Ef3~C6-!F`tTm{F4<+7)rYNSk?_JIN zXU<42M+7+YU%KXQZ~Cesh}UvorN{P@R!C8|yeiMGWC(}{>5<{c9AzdN{Y}xo zV24@O0WpigC*31G#Fp+VQ54~I^U{uUizRZ!t+I_p*%xf9Wc#erWLA451uW)d&20@+ z+2^~XcM@DL;-v~ky-UlogF9|)B8W|(n2%y)ao@a-E!{Lr@~Xe%mp0Fb7c&?oT)dT1 ze7=)3evADxyf(vt8kBZM3{_y8=%Ex{iC`GV_b@B__K~I-zIDb<8rg)0_ME6lYbr3f z;!A^ylit_QDRF8)=eZt8TfT^sDf%q;$|X77!$Vh=*bOuCB7w(wTwq9eVZnKRN;~!Z z9U&~yk&vFMB|f0e^_z^MvE!hC)3`*!>ccI!f#6KX0dX01dgI|<17~;5$3GXC!rZ)> zJ*r|E25m?e8ht-Z$!1%yV5>kPH4|Ohz!tJ0&ruM@Ds6{9@rUBaKejmUiK@L|^Gute zO=MW9`ihzKr8246LnZ!cicr#ok8@2ApK^HdiA|e>Z3!VRrC3YaKHTuYiWf@G8(q!& zoS95?q5+}uVV<7QVXBVQR$VggmOq>xa2|uQk?6xgjU3GpQPWaldoE z^cgvNjY3<37S!jAvcbN!G;ifc=i#QFukg(4DeH{O^z%RVq6Nb*$R1s7acEv+U|E7M z75z6T=*pfLH10il!1sygoQrK%E@uyx9I0rJGpg;@)be0L+NCL)SIrc3BJeyeOMI+J z+FVkTC?;<&#u)OtHMYcQZ*(ZB#aLT{@CD`PS0^0@_R7Cr%)ml_z1=9`h3P)zg?9Mx z5{)%LR48&*bg*Y`4e6+3>L^6oPh^tdPYt5Ee}1PV-m*(L-$^w!L}U|$NpOS1`MRsi z{R~sl4sBT(9jrCP2OGt=QiGp!$e9}jQ-^Kgc*+KGP2FV;bKvf}WnA%;tP2owg%uBS zK|=dKT-@bMEIk*p0X;0Q)XdBTlTg(cD~WXJsC6~k8}vg)UD;c#F4D3&kp zlrU!$N%StAdZC@Y)S_yQvaaoEOy@?$pGMa*mfLxuZ8N=y;sh?5w5)Y$H@twdL?XtK zOH3v1pCj8@rFU;VNYKU{-=NJInio)OP~?oTi{yZF6|N|BE|fXH9mUI>5e3eD>~ZmFt5$nKuQO>_#-*ZJQYOYR|}-b@LZ6 z$&=bWsJiO`Zm;8})Ao+SI7*?4I{WkQ=e`0|E06*01%;1J&#=foBKQf~J=hE66Plg< z?oJ?ki|@r1r1ixI;RO^e61{dvaF1s z{w-ondLb#fveEyuxt}!WySv~cTJ9%4ousd5EG)e8N^>>EzLInAR@<#8eO*2%dtG6p zEXGTWnnXJ67tqCkqR;a!Z9k&5XjDEgIkZ}eR6keHafe*wX+U!1s>wfNPww~SFQ#Ur11<1Zh(f6{(H%=mTf=ojuL z^cvYI(v;4rMx^>>WH+|3Cb#F#;2Pm2TcJg;mod*{d@SqF4t;Aed{AXV(Y?tl=E2U` zYCiIMBZF>FFXlO~h(gKRhAOwEdiPjO1{Og!vSk+^nRT*y!FTB|VbgM~r#ELrH80P0 zKSoCeQ_)jEeDFdbTt=|gU{u){o}KW%H3^@Wlv;UdN>dG*mdBcY;aVwXfr7WZ%ypPA9ngUPX4Y4+^>`lZenu_xcg8D4?4s+pPsg645+Wf|ABo2}{S7 z6h%eqsDxG)v%24gR_Y$|LHK;{v~RI3}gL>U_EM zwo8HQC($nc(x)g+nXOxB#u3xTk2kXg*Uyu>F^8t>XTELwVJZ=_A__(`ppW!gg*4xU z=+egt%4L(cDvBUj7f|=t1`|3IJmj$Yf#1SCE2oQ7^ulA-7&m%{34)C!drB+Fxr4Sefzkn{1D>G@<8bMwn3KD=I{qAA%#1;Q|5 z*^{EmAgvgjk%|BTaS_UV!W+w`$8@T|Bh2$_?&lAXAhEI$(we-GWlU*Itm0?d7*+x} zQ$h@Itod^NX?bMHtZ4=>1!b{xb*>g?zjE*IUuwhx6|4(yho;s`x4Rtl20~S^ObCfG z9~?nQ;AA9yl6wo;s9(mS#Gpi(OwuYw(kaH$R2hg%^`8B6XrkY>?G3I(s=d6a(Lt`x z>j)7pH1lEe*pgt9vSK5mzP{<#!E7$*I*FmovPX<%yb&-RhP9OOR{t2C_p75dpPp-U z1y58V`fQhvhcd$x=VqJH78uOtUspI{C^FC)u@1d{ZWY6&RVQCTwkuGGp&~DP;gtEB zFtmyyf7DMRgJZvySlq|$j<6yem&XhL(v(J86`h+qri4XLx+s$fJnfAW7s+0oZ^vzK zHU}%qNBN?;>pRTgcX|54cnHS>@iDr`1X~CB-Q@BS;i7(H*AJW&G^QoHq`u5yXlyLv z$?2Ez&#$@kn6)G1)C`mtJYL3ftWXnVQwp(S=n|~I#X{>&o@6|2Cx4Xok==VA&XOdX zB-^*ko5>GX%lO4RPXuhT#?I=r1^QJi7pyU%47qu$beeAqEcvCN6YjBkuMZgczQH>19G~U=}}WS4KG7~X;Ip# zlD_`@rp=G+A*5LN(GDpRrb=>uFBGBbw!y>|6O^PfmEqaEShX4b!?tVt;CV8>O#C;a zw#1u+75?WEb}1p>?Y6=cbr45B6@9KxNFxd;#x4j{(1Fjz)}6))7k8SK+K#SE;f5`4 z{;#@&+mG1P_svj8lWM2jyI(pKnw%BDyBWV|OJNg#&?m0-fMc514Z$oRm-O8R`=146 zXMVNrVMQ^^{RZJiVrHl)E15V8OntjjA)4#m%tmk#IUCCpK7`271<`}G2nlIHLpE?6 z+YbxB`WI_c46bV4hD@@d{8DYX@j|1Cps(lc4&bZauHrI8DnFsM;g$6E+1u|c&Zuyg z)8m51*sa5?t1)v#eP0)to>>;h3oON&Ciq}NtAB$YgBg}=tj=xXrE={^Lq^Z4uFza6 zODn_nO1LgUkA;&-4QIHz?5b2IXI0tb*=e!-gOfq5pM({2%NH%>VBHUqSxe7$`-DjPakb zAY=eg68!I;f2{{%K!ZUb!v9Gl5C(!^(gjP>ffS*^Fi--F?k{H$j4nYDiVT7VDuMz* z_sicPFc2XoG#C?_0Qx({A3!h!Mi+<#Mn-}XBK_@oZYKQ3M4efq;+%Brt#j;0I#>v0!8=Z~`Wn5ZEjEHx7Uq zhynP36Qn@Fn4mx;5C8(?MF5;70db&zi2%?8Z~(XaU6N2j3~(UQJsJcPFarK`KvV(- z-TeXsL0~{EkOfQ#`xAu;1;jxh0g{4||B?dsCxsMH9pDKBf`c)ET~Gi71OURI5D+*) z68M~&?k|5r7{Kj55g-HvP)X`O3$RHDRDpS)0t8UPq=R4p0{$sh3L1z>2jGAJSRhae zga9@He}E4l2n_(`A?AHo0z3nyK**Q?K2RM9kPFxZ z>;NSQ28a-Y4j=DRtOx>d0K$ZlU&32ohZ&pfHRs5Cj7H6j<*0!0uB3gn_z(|MXWN1_bBoLqp;*XtB@t#8h1~3|-!63i~4A}2+ z6hVN(5GimlP_h&h0s@TpO9BR%5U3ZB1p^9<2M{39ADsJeOaLFK0sw>pssFZ1f`JkM z5dwiO1Jpw7||1U2U-M@AO`TuqVUQ$ny zsX+=4wsh~rQNGLhYWt-2Rq2b@G!bt~@9bu(4CWY?OTPFmbWDXdlc1V7oYL#48&Hn~ zv~oThW-Q?h&_McHh0m1tkX&*wPb!duQ#VmCLbT_#6hm3fvTHv9nNFfb8tNIWUjos- z*QSGfiaC>PD*GNha_hCepV<}MRi3n?aMafq@70O(p?%Qw#*dM(MpqbIL3+-$*V2Hb z+u-tJV{UrI%inwMRSV;X$nmWe_{gr&ej+2HXC!*)idlu{|zA8TY z5HVSuYbc)jD!2)R)k_^WsP+S0|0PeB-{$CxDNjAaO3A>p+xKYT(Q2l1wtyQYSG_^^ zY8Q^jN>P%LZu;5y*2xN{NP11%~M%nr0bxopZJ)+|_v~lE&G- zsYl%+`>LNZsH@~Esk7#@b4EpaifNS|J}(A--se4=iIlBcQ@%p(>gFd<6Eb+@{)YAi zQFMtm*PHKeg6p25Vd;*-p!yEb0cKL*N7r;jkJ2s5hYTX`x^ z;C7g1)C_;_=$=opv`u_@hVzcPN}g`2)|t5dvP{AQt5mv#_EjjIN~Fad#^>$oikaiV zTr|DjkY~DIaK4o;7Qr#BP+#bG;domV9|lR0tO>YVUQ1@gjM^1G&qy9S!faBl*XH3T zo9AJ1TXiaKW8kRTQkmqGHflj5Q^~Qk)_c%q(Tb}QSSJjH*7x#`lrN?VYrFC#b|nZ6 znwGlYFT{tg?|;8ReLK{sK+573i;{L?8Rna0*PZ%B=q1i`-P(toKVkMF_AwyauW+Ge zhc*Z7S>)kBc5NzU+ygr$9@Qu}?w*Q=+G}mDWBCtQ+Oit+GukD`-Wp`5AvbBY7Z?j$ z35KtObi1d;%%_*1Y~Y}K-_qjlxp4Hft)ZWX)&cjXc7i&h%RAmb`(DA_4f)#!au$o^ z$~R-DTwFMflmfOG;b?mx(MgR=Vmo;9LWNC6WWw0y!+BBzWjoJ|xMGK;5?vO;syWTb z^wbX77Yg3)7MUrnmMj_X9(!gKId@q61`V@E4iA$rh*}Tf&Nfl!9wiIkoyjn7z)!JCS+4Ap-_(u`Jxk*gD}%9^QTF=kG%g;>%cL`_Ww~fh=193~!O|T!%aq9+K}Ai) zepY8_d7cBUE05o8d2Uh%c^8!_D?qZQ%(%hb*w#7KGjS!oZ!Fs{i&q)PI(E>&eaPN= zwp|*47E-vB-Oh}#xMhvrp-U9F zCigN+TsAN+TugujG01pTo}|Da^;TlK3$Bfv;069OCVv9dxF{{Iud5;I@hkF&oC0+} zq*9f|O#&dz9vjsE4!Q$YGlEU?ThW{TOFID5x-Dx*Sa9t=HWp^)kw z9!>Z24SpfGH}|n20D2his1ot4zaWLYb|=A*zj~mG0N?^?83$#`7}FODdPAhD?Gk0p z5JCV6nS>d{e#g>$2y|rhl;(=u?zx17arD%!vfH=m3;j`C0&bQCN>){KH*0dX;S({^ zEDz!nE6m3^xu5eVe@sDITc#CjajpT}|I?6`#H{49(0yXn7-Ayw@C=Mo- z>Qp{qhGoFMlJlMYBVy|kflSW7rnZb=MQRv1%(mIiP%sOO#?hD?K(ND8tY{6&5p1Fr zG3q@Dh-@@m;rD|lh(Gf41(tr`AZ{%`gg5tG7@F&vd*$q)?(#=IUx#JUODooh7iZa6 zg5o~{q$jr}UrXj}RHfD>=4CfCio{oY%%V_}1AS#cGTsSAw=Hqka>v$XsL+D~xp8iC zjclWtWl`H279P<8;!*-{>lxU&f@XCeFg_%0*d?ygdN-{JnEuovvnEIiMkQaWAuS=hONXE8H=80 zVJPLn%PR56a!K+5p20;?SeWl~sNrUML8-eTor zOY109pnr(Qr36-~amrjS*;tC_c|c%Fc$dG3UPO^!HJWmsx+b@Wpqyhyo4 zpK{mGr|oqR?L}M~-;*XSAas3|2D+%Iy+A*R4IrH$+El_BiFRiOiMzC=gWd>n8xssI zj=Gyrpws4>^$C;5LeSy5&sJO_JSV_DQKNs=)H+gdkV=w}d{hL(5m{R59! zQe;v=*)WoZcLfuPLe#l$iJ4F}i&&=I#70MHVaeVj03Q;N4`@LGN>r|dM#aZ1M+>Mm ztBXTS@Gy-`pB49j-+NE$e<)dPSzdUSS(sS@h1UatoG=Ce)EbVkKaPhm!->L2u=XMH zo3VA$SYlK&F4^h)jt*IS+XA(A^XO5=cQ3LwEY2&d3xCn-krC=GXP`fP#+=X~dymvI zEE+5GQ$i=IOWhV^P!r+fKN7{CraYWSVcZMV+SA;pybOw~KiU2dDz>9=RRFt==#a&{ z#X$LE(=__S6>RPv!Q7a1cpz&Tf?)WR8kaaC)z!sT26R%;;vu~Dmp03=<$56!xL~iV z?_mnd>-U9yv>t@Fp!Gon+Y;MYFMr)L2olGcjS#74;%x~b)g{G-)`2W@Hp~YYHQBbY z%Q-u7kmxiW40(%XQ9)koukIK~*-2RW<@LWwfaMshj~IhR*)1)+IrXkPpu8`Y zO~#yqAH(q%4=@eA82qs-3a6j%h-;xi4g8*-(t)@dn$Tx?um?!JdUEEucIDp%xMx^QDAdz1PLtzc1u3; z`TKGMN$Ix@&lWZR03e>hiCq$$w;w=6{AZe}Y&Can>w?9w`Khi;O05K-I#H%p)hrl% zJpQ}7p{T~&{qF!>0F1h2+;7{NL5VQGAXY;sv*^N0tG>bUZsRgH0cDNrBk>GG9E)uZ z+~b~N!r|a3YSmlWE8(~eI3BttY#mZ-wm67ZUXKXeoB_Js@b~wL7>QHS=*Am>c>%HH zH1ZYFK3#jl*EY^Rc!y|ts0OPM!W;%6cA?P(mnms88dVVlQeuv_yE-4pXVuud}OE+$QN*C9JM-t>Y}D`YMY>z}9K! zuQ2V&-;*VZ-LNfGJG{p2xul)3E{j16Rcl&cg0jSx;EVs+C|UKcM(nVWP=(V4Va%{$S`_vz!nYFSH|t<`Qb4M*ZVs z1;OWiq2c?0S2BduF~p}13g*@GQPNwGct3&r!oJIN1w$*5+R_(tWLXO`sO9fQ?fmn*^59^zkD~>r?HN+7_bC3YTz#TcZRQ*;U=^ z6>`Ein6|gfDU||_Iv+jd1rMI`#_Dq9vKs5y?+v;yAE2@m}Cjy4ATo5g~yk$irv50<>GwTxz0gOBz+;#c+Xv_s@28{zt@{{RFL zZLv(kS&E{>xha?n%UW{8(IFd&%#x=gR5 z{^6{_1nVyA%nlvDXFn37J7dxa=Jpw^zhg1QVmQN#-`O=;^@cG}BI)tOr;vxy3%$1r z*o!V<>ZlD9LK5S|Ys(LodcfCM0BucMD>kTI2R4u^@RM;($B{pnsvHsRf0%9M@BNmB z<56$S3nAFt9#C9d2qZO?7={hSsYSvO{@2-!+RG<;g1e^`a3Y$G_)x)h3vgQa2eh@; z?WuhXm?OLrz8CvYb_}FEkFVQBK}gNOp?!PGT~5Yer^IVJ7ud}3qj{kHKJhNAR}Mx7 z9o$a?Fu=`%OF18Dw?4AP7RO1uzgS}ByR*UcuI3s;RtWov7WWnA!@X}LcQ_xoYwRAG zBkL@7wD?Z@f9h$>QM-VOX6C&6M|mloRvV$AUBK$|C^u35vUwKx5~HcR z6Q2a}Cnujr@PzY6NPDq!bDY3Pu&bj@KH&n2`-p`%uU6CeC;OmyZW|$_ILq80<_>H! z7-_PPOE28A)=^8XM#uV%8VTPotX$0?3!u1>X8DfNJ|MXmV+;kThF!rFiP{530B?0Q z=4*Wi@evO#@O{>FgMUCY96kLTDA(w{p zKfhW1Uq{?i_FUS@@;}7G!oA3VXB{O76Kt=Tf>bcIm25$A@}19^2mMZ~=)>tAu&!UK z7XJX8XD@h>EVgk@e5Lf-0_#8bF(r%~wB38u%2$*+3n)aaG0W29Z-frC-FoU+uq}7K zJi~uuCJNQib(Oymy&C;R!hu`5F>72(_5t$RYfW7Q%c{bx=JW8^?Im}E_Zeq!@~ zGUEYoZ1XHP6|ybiV65>jF}lGOc_pgl<;zFV)9JU8ZG46x5LlBSa}A@!P$nsj+6nFT}z-QiN|5v=_3ZiUSYKB z9ig*9=(xY<(otnf4($OCjm1vPzG0TEsmYp~*AN2z<3^$emZi+9aF`HX)I=`ap8G z5@@J7SyAL-rEdvItjr0n(K}+fWq9UO^_=uF^rPtl>V49R58MKSm723($D|_d+3@%M z83~cENwMAHUo(hU@6n3HM#Xoh9!g;#7a%^0{mM!mS`R}gCV1U`?=PyvPV&fV4HAcP zfTlgek5Bg(>tLN)_RHxhfGwAvUuPbZD3VE%q#qP}%A)HnjetzS2uXxEn8Z%<-WaX6 z1Eyo2Bwf`+gZNAJ6pO<#d?4+)?0iFCiwj;)wq@wrzO+Vc?Q@RhhT-Hhp4>)VqXz0L zWn(O?)3o)RTaEXZtVQlzSkwof+|)EmBj$7*A6$Bwkx{U>$n^dgFcD*@XAh?1F@a!E zoQFqesl=mkLs>bRR^kkygoP5%5p5;|=jdMAn$gvT=(M|QHijKK4)LS7KOND53R=N0 zF9BWPI28fGFy=_rD(5W1(JyV*RN^f)HP2L3!AU^e!7EpAvAAFw%>pm#6xf#Say|b5 z89LL5g&0H0{>ENvlUEpO<B?}*6I1i8$+d&b)xQ&4AJf>r$*Dh{t>yvrWPdR;s5+M^NCB~#znbKUJ3ZFEJ$ ziE;w%8e2O?F4cn9yurQ7rUK~`Zej?~=`;0|*0DBAfhc+>7O4LKv(f{=MRlY7KYhI* z-`JJFo$?>FV#ud`l>R3pL`8?K&Z_`70^GtDqQ1$yn1-%rdEB$Lo1ziCpix(t$ToE< z$``Ym9ESNrv;-gp*JpTnK-8Wke|RvV(H_PhN|qJXvA*N~0HjQ_kr;eD!p7q*!s`t; z1p_Y;%gHc?tIGv|#~8ulCLxVmDB%A9aEs8;1N3!yi7lx*7bB=+tKto9DU{4=s2qk= z>`88OG7?c3ok*DJiI)>uYJ(#Z>JnV`mVHUw8a{oWXlbq!$Newh4gG*wX^XkH_0m(V zJJ5fzlrHfC@|PR90*0UlZ3XfVr zg$=+zd&__aOGnTL%tMUq9~m9uiiKIiW_s&8;-*vI3B*DOWJZ+O4ec;ZaKKWtfzxZfypu8xUO>cONzEN z4g%j1BIH^gh*%V;1r>9#3rulp*@3YP2KgP1c&pU0V?m6Cv|G59xu6)DTUO3WX!c0u zB89vmA*fd+*W`z@G6FmQ0BM32+=>V7h-J=2__{Q14UO5`C$Tw_ffv=!xYuc72QdeO z0qjB?YW#YxeUEe;paDR115<*brbslZC7!W55mA|0xpih33BfVx1;9plJjHN_WHB1d zDLYr&!B^O0Lpao=@Gv}0JN^N`um~lN7=Yo0-8Ww6{YC=W#deI>GVZkoi`3}M+mvwv z&LNdTy`h!`zF^qB5CyV&kP)hULI4o4iGrCzQ?%bb&PKL7UC{TJJyD5f=x`d+F7YXO zs!^%B#XPM`&8>0vP+l9WWJ8drGMj#;DOgKUOLjt=a3yLhNu=SsFr$|Vmg(RP@JEZ{ zntI{9bh&ljtojS|ypT%?yiTicsrHrQQF2WjVtBya>R=WFlQQGXxvH70A&{uN;t~1> zrwQ2-)b`<7Sd}=YECcZ${bd`Os<{6EkZbS;{?iz$^fCGPF+XN3{3G!UbGd^CA)Zbk zW7vpHNr73({M zAb!=Qk?4p{=qP~h{{XFeehWH%#9jJ6CMNV*lwwQ}hZ2UUCD%~5apkt;?I#O_A4eo6H#R3rT#&G0`ooFt2Yj1WXDhZN*`8 zjFea?*AT@z2+05q20O}eJP&wgZeCsN3|>Ol0>?=cwaQ;Az2H&!Lw|NKU14I4-fca4 zWxiN3P7^hyUX8I`oDQ;0r`$uwpGSidn7qg^1mgbOVy%#pSx( zaIZOqfwBq2&JX~CRBO5&(|`6;d&DTzaUQWFLlDad-dM@<5eeQl-!aXi^qYc*3 zaOMe)a4K6_mV*QtowDK!d4qlCXeCJ7LX>neyA0a&csmbOSF2iK02Ca45$pHSD{x@D zF3!nEvs?%=g83DTb@-}Q65gs3c6x76Mi$-xQBeIRe zy`r`=X+}F~DL3_-M5YSeAkcF%)L5t&%K+TQILy0^g5Xg*mn^_$5$`lWWTPs_nNbVB zT?ECQ3m}PG5z)t@-&OoUe}Dp`s789!tDL*dnRr~rOh|fig~B#Kq~o*%01nYYp<)H) z0}b^WcaL%zf>a*AN48?rTYE<>GY0GEH3FHE480&OCga^P>jt#NVqcgeJDxQ=lQ~nV zL9$~r0SB3WWu(ti)SXPA zPZZ{*;1pG2I!BTTa|kt2W%rIVaS-VG;XM(`WSAk0=TKV`*Kn)LE$cGl=o*7^-XP!6 zYmQSB=3S2yA_@?19KlF}mlLEy{9j9m}(bNG8@eGm(o1SFsr8*#9 z<~g|HBya}RVbWQtA2yq(2R!yF{!#!8Df)p&Fv6`mNt z;AAbM%5;0LaOX(y9U^RraRR1;tQrnuG=GSc<~mKW^9JF>Iej2e5-TcZp$=mz1}l$e z(Ljy$^A!s79|);No{Uj=V(lNXa7+b)eVz|dJamD3g$#UA*YyIwIMf^bq#FQ~onw4$iDwj8c<&h!=P zhd6O5xQ*^A+{W|2X@@8y&G8mBQ9ws(lmsro(DjVixtVUm@GjbWMv;9c=LuT8hhT-nEJJokLa%7;m2naUK>@_8 zh;=kXhbA)ZGP#FiQ~5-LF~<>DD~(CWjyP~rURiz^I+tfO^fA@}43vZG(dz5jNpV67OoT7V+>Q{tLUeky+3%hv}CT5I`OX@|M)ASywys=k0 znKKRV3EWa^P*J}~F!_f0g^Jp$ZU<3!xK=AKXhzDSSn$`DqeEZCTZ?_UR-#9@v)$q7RNHFPM|@eHI>zj0d@rw51__3!{<}&;{5HL9)8bM7J+N>5cfu|Zv4tz2 zS@@_EX6OsiKbWdZy+J->1Ov-T!i&RSs1PQ$ jG3daH4OU-XA&h?}KJ{|FfA&dmEB^rG0Mr}*0N?-FZ5Rha diff --git a/approot/images/unknown-cover.svg b/approot/images/unknown-cover.svg new file mode 100755 index 00000000..52ff34f6 --- /dev/null +++ b/approot/images/unknown-cover.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/libs/image/CMakeLists.txt b/src/libs/image/CMakeLists.txt index 32391b0f..f6228210 100644 --- a/src/libs/image/CMakeLists.txt +++ b/src/libs/image/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmsimage SHARED + impl/SvgImage.cpp ) target_include_directories(lmsimage INTERFACE diff --git a/src/libs/image/impl/SvgImage.cpp b/src/libs/image/impl/SvgImage.cpp new file mode 100644 index 00000000..433c58ad --- /dev/null +++ b/src/libs/image/impl/SvgImage.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 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 "SvgImage.hpp" + +#include +#include "image/Exception.hpp" + +namespace lms::image +{ + std::unique_ptr readSvgFile(const std::filesystem::path& p) + { + if (p.extension() != ".svg") + throw Exception{ "Unexpected file extension: '" + p.extension().string() + "', expected .svg" }; + + std::ifstream ifs{ p.string(), std::ios::binary }; + if (!ifs.is_open()) + throw Exception{ "Cannot open file '" + p.string() + "' for reading purpose" }; + + std::vector data; + // read file content + ifs.seekg(0, std::ios::end); + std::streamsize size = ifs.tellg(); + if (size < 0) + throw Exception{ "Cannot determine file size for '" + p.string() + "'" }; + + ifs.seekg(0, std::ios::beg); + data.resize(size); + if (!ifs.read(reinterpret_cast(data.data()), size)) + throw Exception{ "Cannot read file content for '" + p.string() + "'" }; + + return std::make_unique(std::move(data)); + } +} \ No newline at end of file diff --git a/src/libs/image/impl/SvgImage.hpp b/src/libs/image/impl/SvgImage.hpp new file mode 100644 index 00000000..ce54cb30 --- /dev/null +++ b/src/libs/image/impl/SvgImage.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 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 "image/IEncodedImage.hpp" + +namespace lms::image +{ + class SvgImage : public IEncodedImage + { + public: + SvgImage(std::vector&& data) : _data{ std::move(data) } {} + + const std::byte* getData() const { return &_data.front(); } + std::size_t getDataSize() const { return _data.size(); } + std::string_view getMimeType() const { return "image/svg+xml"; } + + private: + const std::vector _data; + }; +} \ No newline at end of file diff --git a/src/libs/image/include/image/Exception.hpp b/src/libs/image/include/image/Exception.hpp index 1991be18..7e0d83ba 100644 --- a/src/libs/image/include/image/Exception.hpp +++ b/src/libs/image/include/image/Exception.hpp @@ -23,11 +23,9 @@ namespace lms::image { - class Exception : public core::LmsException - { - public: - using LmsException::LmsException; - }; - + class Exception : public core::LmsException + { + public: + using LmsException::LmsException; + }; } // namespace lms::cover - diff --git a/src/libs/image/include/image/IEncodedImage.hpp b/src/libs/image/include/image/IEncodedImage.hpp index 0613f214..89685143 100644 --- a/src/libs/image/include/image/IEncodedImage.hpp +++ b/src/libs/image/include/image/IEncodedImage.hpp @@ -24,16 +24,15 @@ namespace lms::image { - using ImageSize = std::size_t; + using ImageSize = std::size_t; - class IEncodedImage - { - public: - virtual ~IEncodedImage() = default; - - virtual const std::byte* getData() const = 0; - virtual std::size_t getDataSize() const = 0; - virtual std::string_view getMimeType() const = 0; - }; -} + class IEncodedImage + { + public: + virtual ~IEncodedImage() = default; + virtual const std::byte* getData() const = 0; + virtual std::size_t getDataSize() const = 0; + virtual std::string_view getMimeType() const = 0; + }; +} \ No newline at end of file diff --git a/src/libs/image/include/image/IRawImage.hpp b/src/libs/image/include/image/IRawImage.hpp index e0949b2c..178da958 100644 --- a/src/libs/image/include/image/IRawImage.hpp +++ b/src/libs/image/include/image/IRawImage.hpp @@ -19,23 +19,16 @@ #pragma once -#include -#include - #include "image/IEncodedImage.hpp" namespace lms::image { - class IRawImage - { - public: - virtual ~IRawImage() = default; - virtual void resize(ImageSize width) = 0; - virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; - }; - - void init(const std::filesystem::path& path); - std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize); - std::unique_ptr decodeImage(const std::filesystem::path& path); + class IRawImage + { + public: + virtual ~IRawImage() = default; + virtual void resize(ImageSize width) = 0; + virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; + }; } diff --git a/src/libs/image/include/image/Image.hpp b/src/libs/image/include/image/Image.hpp new file mode 100644 index 00000000..a0ee4182 --- /dev/null +++ b/src/libs/image/include/image/Image.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "image/IEncodedImage.hpp" +#include "image/IRawImage.hpp" + +namespace lms::image +{ + void init(const std::filesystem::path& path); + std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize); + std::unique_ptr decodeImage(const std::filesystem::path& path); + std::unique_ptr readSvgFile(const std::filesystem::path& path); +} \ No newline at end of file diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 49540e48..605e5a92 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -30,7 +30,7 @@ #include "database/Track.hpp" #include "image/Exception.hpp" -#include "image/IRawImage.hpp" +#include "image/Image.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Path.hpp" @@ -107,18 +107,16 @@ namespace lms::cover } } - std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath) + std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath) { - return std::make_unique(db, execPath, defaultCoverPath); + return std::make_unique(db, defaultSvgCoverPath); } using namespace image; CoverService::CoverService(db::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath) + const std::filesystem::path& defaultSvgCoverPath) : _db{ db } - , _defaultCoverPath{ defaultCoverPath } , _maxCacheSize{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _maxFileSize{ core::Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _preferredFileNames{ constructPreferredFileNames() } @@ -126,25 +124,12 @@ namespace lms::cover { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); - LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'"); + LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'"); LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize); LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize); LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ",")); -#if LMS_SUPPORT_IMAGE_GM - GraphicsMagick::init(execPath); -#else - (void)execPath; -#endif - - try - { - getDefault(512); - } - catch (const image::Exception& e) - { - throw core::LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); - } + _defaultCover = image::readSvgFile(defaultSvgCoverPath); // may throw } std::unique_ptr CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const @@ -189,27 +174,9 @@ namespace lms::cover return image; } - std::shared_ptr CoverService::getDefault(ImageSize width) + std::shared_ptr CoverService::getDefaultSvgCover() { - { - std::shared_lock lock{ _cacheMutex }; - - if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache)) - return it->second; - } - - { - std::unique_lock lock{ _cacheMutex }; - - if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache)) - return it->second; - - std::shared_ptr image{ getFromCoverFile(_defaultCoverPath, width) }; - _defaultCoverCache[width] = image; - LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size()); - - return image; - } + return _defaultCover; } std::unique_ptr CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width, const std::vector& preferredFileNames, bool allowPickRandom) const diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index 30f24bad..40fffe4c 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -84,16 +84,16 @@ namespace lms::cover class CoverService : public ICoverService { public: - CoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); + CoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath); + private: CoverService(const CoverService&) = delete; CoverService& operator=(const CoverService&) = delete; - private: std::shared_ptr getFromTrack(db::TrackId trackId, image::ImageSize width) override; std::shared_ptr getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override; std::shared_ptr getFromArtist(db::ArtistId artistId, image::ImageSize width) override; - std::shared_ptr getDefault(image::ImageSize width) override; + std::shared_ptr getDefaultSvgCover() override; void flushCache() override; void setJpegQuality(unsigned quality) override; @@ -112,7 +112,7 @@ namespace lms::cover std::shared_mutex _cacheMutex; std::unordered_map> _cache; - std::unordered_map> _defaultCoverCache; + std::shared_ptr _defaultCover; std::atomic _cacheMisses{}; std::atomic _cacheHits{}; std::size_t _cacheSize{}; @@ -120,7 +120,6 @@ namespace lms::cover void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); - const std::filesystem::path _defaultCoverPath; const std::size_t _maxCacheSize; static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize const std::size_t _maxFileSize; diff --git a/src/libs/services/cover/include/services/cover/ICoverService.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp index 4825cc2d..6c740d23 100644 --- a/src/libs/services/cover/include/services/cover/ICoverService.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -43,14 +43,14 @@ namespace lms::cover virtual std::shared_ptr getFromRelease(db::ReleaseId releaseId, image::ImageSize width) = 0; virtual std::shared_ptr getFromArtist(db::ArtistId artistId, image::ImageSize width) = 0; - virtual std::shared_ptr getDefault(image::ImageSize width) = 0; + virtual std::shared_ptr getDefaultSvgCover() = 0; virtual void flushCache() = 0; virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 }; - std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); + std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath); } // namespace lms::coverArt diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index 2c729447..7d4ef6b2 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -272,7 +272,7 @@ namespace lms::api::subsonic cover = core::Service::get()->getFromArtist(*artistId, size); if (!cover && context.enableDefaultCover && !artistId) - cover = core::Service::get()->getDefault(size); + cover = core::Service::get()->getDefaultSvgCover(); if (!cover) { diff --git a/src/lms/main.cpp b/src/lms/main.cpp index ed32dba8..4380005f 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -25,7 +25,7 @@ #include #include -#include "image/IRawImage.hpp" +#include "image/Image.hpp" #include "services/auth/IAuthTokenService.hpp" #include "services/auth/IPasswordService.hpp" #include "services/auth/IEnvService.hpp" @@ -310,7 +310,7 @@ namespace lms throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" }; image::init(argv[0]); - core::Service coverService{ cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") }; + core::Service coverService{ cover::createCoverService(database, server.appRoot() + "/images/unknown-cover.svg") }; core::Service recommendationService{ recommendation::createRecommendationService(database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; core::Service scannerService{ scanner::createScannerService(database) }; diff --git a/src/lms/ui/resource/CoverResource.cpp b/src/lms/ui/resource/CoverResource.cpp index d0e7d0bb..6768a560 100644 --- a/src/lms/ui/resource/CoverResource.cpp +++ b/src/lms/ui/resource/CoverResource.cpp @@ -98,7 +98,7 @@ namespace lms::ui cover = core::Service::get()->getFromTrack(*trackId, *size); if (!cover) - cover = core::Service::get()->getDefault(*size); + cover = core::Service::get()->getDefaultSvgCover(); } else if (releaseIdStr) { @@ -110,7 +110,7 @@ namespace lms::ui cover = core::Service::get()->getFromRelease(*releaseId, *size); if (!cover) - cover = core::Service::get()->getDefault(*size); + cover = core::Service::get()->getDefaultSvgCover(); } else { diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp index 2867cacf..b7b22d1a 100644 --- a/src/tools/cover/LmsCover.cpp +++ b/src/tools/cover/LmsCover.cpp @@ -24,15 +24,16 @@ #include -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "services/cover/ICoverService.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Service.hpp" #include "core/StreamLogger.hpp" +#include "database/Db.hpp" +#include "database/Release.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "image/Image.hpp" +#include "services/cover/ICoverService.hpp" namespace lms { @@ -83,9 +84,10 @@ int main(int argc, char* argv[]) return EXIT_SUCCESS; } + image::init(argv[0]); core::Service config{ core::createConfig(vm["conf"].as()) }; db::Db db{ config->getPath("working-dir") / "lms.db" }; - core::Service coverArtService{ cover::createCoverService(db, argv[0], vm["default-cover"].as()) }; + core::Service coverArtService{ cover::createCoverService(db, vm["default-cover"].as()) }; coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as())); From 2d01814edee6ac243118d09d9281cbfcf4c962d2 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 27 Mar 2024 23:42:26 +0100 Subject: [PATCH 08/64] Fixed last track not sent to ListenBrainz, fixes #438 --- docroot/js/mediaplayer.js | 18 ++++--- src/lms/ui/LmsApplication.cpp | 95 ++++++++++++++++++----------------- src/lms/ui/MediaPlayer.cpp | 2 +- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index f6f61110..f966e446 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -97,17 +97,21 @@ LMS.mediaplayer = function () { _lastStartPlaying = Date.now(); } - let _stopTimer = function() { + let _pauseTimer = function() { if (_lastStartPlaying != null) { _playedDuration += Date.now() - _lastStartPlaying; + _lastStartPlaying = null; } } let _resetTimer = function() { if (_lastStartPlaying != null) + _pauseTimer(); + + if (_playedDuration > 0) { Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration); - _playedDuration = 0; - _lastStartPlaying = null; + _playedDuration = 0; + } } let _durationToString = function (duration) { @@ -229,7 +233,7 @@ LMS.mediaplayer = function () { navigator.mediaSession.setPositionState({ duration: _duration, playbackRate: 1, - position: _offset + _elems.audio.currentTime, + position: Math.min(_offset + _elems.audio.currentTime, _duration), }); if (_elems.audio.paused) @@ -276,9 +280,9 @@ LMS.mediaplayer = function () { _elems.audio.addEventListener("playing", _updateMediaSessionState); _elems.audio.addEventListener("pause", _updateMediaSessionState); - _elems.audio.addEventListener("pause", _stopTimer); + _elems.audio.addEventListener("pause", _pauseTimer); _elems.audio.addEventListener("playing", _startTimer); - _elems.audio.addEventListener("waiting", _stopTimer); + _elems.audio.addEventListener("waiting", _pauseTimer); _elems.audio.addEventListener("timeupdate", function() { _elems.progress.style.width = "" + ((_offset + _elems.audio.currentTime) / _duration) * 100 + "%"; @@ -286,6 +290,7 @@ LMS.mediaplayer = function () { }); _elems.audio.addEventListener("ended", function() { + _resetTimer(); Wt.emit(_root, "playbackEnded"); }); @@ -366,7 +371,6 @@ LMS.mediaplayer = function () { } let loadTrack = function(params, autoplay) { - _stopTimer(); _resetTimer(); _trackId = params.trackId; diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index f6268ac7..0d06d730 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -489,10 +489,12 @@ namespace lms::ui // Events from MediaPlayer _mediaPlayer->playNext.connect([this] { + LMS_LOG(UI, DEBUG, "Received playNext from player"); _playQueue->playNext(); }); _mediaPlayer->playPrevious.connect([this] { + LMS_LOG(UI, DEBUG, "Received playPrevious from player"); _playQueue->playPrevious(); }); @@ -501,58 +503,59 @@ namespace lms::ui LMS_LOG(UI, DEBUG, "Received ScrobbleListenNow from player for trackId = " << trackId.toString()); const scrobbling::Listen listen{ getUserId(), trackId }; core::Service::get()->listenStarted(listen); - }); - _mediaPlayer->scrobbleListenFinished.connect([this](db::TrackId trackId, unsigned durationMs) - { - LMS_LOG(UI, DEBUG, "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s"); - const std::chrono::milliseconds duration{ durationMs }; - const scrobbling::Listen listen{ getUserId(), trackId }; - core::Service::get()->listenFinished(listen, std::chrono::duration_cast(duration)); - }); + }); + _mediaPlayer->scrobbleListenFinished.connect([this](db::TrackId trackId, unsigned durationMs) + { + LMS_LOG(UI, DEBUG, "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s"); + const std::chrono::milliseconds duration{ durationMs }; + const scrobbling::Listen listen{ getUserId(), trackId }; + core::Service::get()->listenFinished(listen, std::chrono::duration_cast(duration)); + }); - _mediaPlayer->playbackEnded.connect([this] - { - _playQueue->onPlaybackEnded(); - }); + _mediaPlayer->playbackEnded.connect([this] + { + LMS_LOG(UI, DEBUG, "Received playbackEnded from player"); + _playQueue->onPlaybackEnded(); + }); - _playQueue->trackSelected.connect([this](db::TrackId trackId, bool play, float replayGain) - { - _mediaPlayer->loadTrack(trackId, play, replayGain); - }); + _playQueue->trackSelected.connect([this](db::TrackId trackId, bool play, float replayGain) + { + _mediaPlayer->loadTrack(trackId, play, replayGain); + }); - _playQueue->trackUnselected.connect([this] - { - _mediaPlayer->stop(); - }); - _playQueue->trackCountChanged.connect([this](std::size_t trackCount) - { - _mediaPlayer->onPlayQueueUpdated(trackCount); - }); - _mediaPlayer->onPlayQueueUpdated(_playQueue->getCount()); + _playQueue->trackUnselected.connect([this] + { + _mediaPlayer->stop(); + }); + _playQueue->trackCountChanged.connect([this](std::size_t trackCount) + { + _mediaPlayer->onPlayQueueUpdated(trackCount); + }); + _mediaPlayer->onPlayQueueUpdated(_playQueue->getCount()); - const bool isAdmin{ getUserType() == db::UserType::ADMIN }; - if (isAdmin) - { - _scannerEvents.scanComplete.connect([this](const scanner::ScanStats& stats) - { - notifyMsg(Notification::Type::Info, - Wt::WString::tr("Lms.Admin.Database.database"), - Wt::WString::tr("Lms.Admin.Database.scan-complete") - .arg(static_cast(stats.nbFiles())) - .arg(static_cast(stats.additions)) - .arg(static_cast(stats.updates)) - .arg(static_cast(stats.deletions)) - .arg(static_cast(stats.duplicates.size())) - .arg(static_cast(stats.errors.size()))); - }); - } + const bool isAdmin{ getUserType() == db::UserType::ADMIN }; + if (isAdmin) + { + _scannerEvents.scanComplete.connect([this](const scanner::ScanStats& stats) + { + notifyMsg(Notification::Type::Info, + Wt::WString::tr("Lms.Admin.Database.database"), + Wt::WString::tr("Lms.Admin.Database.scan-complete") + .arg(static_cast(stats.nbFiles())) + .arg(static_cast(stats.additions)) + .arg(static_cast(stats.updates)) + .arg(static_cast(stats.deletions)) + .arg(static_cast(stats.duplicates.size())) + .arg(static_cast(stats.errors.size()))); + }); + } - internalPathChanged().connect(mainStack, [=] - { - handlePathChange(*mainStack, isAdmin); - }); + internalPathChanged().connect(mainStack, [=] + { + handlePathChange(*mainStack, isAdmin); + }); - handlePathChange(*mainStack, isAdmin); + handlePathChange(*mainStack, isAdmin); } void LmsApplication::notify(const Wt::WEvent& event) diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 9d744997..9d295d68 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -253,7 +253,7 @@ namespace lms::ui << " trackId :\"" << trackId.toString() << "\"," << " nativeResource: \"" << nativeResource << "\"," << " transcodingResource: \"" << transcodingResource << "\"," - << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," + << " duration: " << std::chrono::duration_cast>(track->getDuration()).count() << "," << " replayGain: " << replayGain << "," << " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\"," << " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," From fb6670290e022a281a2c437af3566fe017f120e0 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 27 Mar 2024 23:44:36 +0100 Subject: [PATCH 09/64] Subsonic API: ignore time parameter for 'now playing' scrobbles, ref #438 --- src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp b/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp index b7563a89..17924076 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp @@ -94,15 +94,11 @@ namespace lms::api::subsonic const std::vector times{ getMultiParametersAs(context.parameters, "time") }; const bool submission{ getParameterAs(context.parameters, "submission").value_or(true) }; - // playing now => no time to be provided - if (!submission && !times.empty()) - throw BadParameterGenericError{ "time" }; - // playing now => only one at a time if (!submission && ids.size() > 1) throw BadParameterGenericError{ "id" }; - // if multiple submissions, must have times + // if multiple submissions, must have all times if (ids.size() > 1 && ids.size() != times.size()) throw BadParameterGenericError{ "time" }; From 58df9284d42b0ad9e7aad1629f6888ccbe8638c1 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 29 Mar 2024 21:02:36 +0100 Subject: [PATCH 10/64] Extracted image cache --- src/libs/services/cover/impl/CoverService.cpp | 63 ++++------------ src/libs/services/cover/impl/CoverService.hpp | 55 +------------- src/libs/services/cover/impl/ImageCache.cpp | 74 +++++++++++++++++++ src/libs/services/cover/impl/ImageCache.hpp | 72 ++++++++++++++++++ 4 files changed, 162 insertions(+), 102 deletions(-) create mode 100644 src/libs/services/cover/impl/ImageCache.cpp create mode 100644 src/libs/services/cover/impl/ImageCache.hpp diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 605e5a92..49859f4e 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -117,7 +117,7 @@ namespace lms::cover CoverService::CoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath) : _db{ db } - , _maxCacheSize{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } + , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _maxFileSize{ core::Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _preferredFileNames{ constructPreferredFileNames() } , _artistFileNames{ constructArtistFileNames() } @@ -125,7 +125,7 @@ namespace lms::cover setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'"); - LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize); + LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize()); LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize); LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ",")); @@ -199,7 +199,7 @@ namespace lms::cover std::unique_ptr image; - for (std::string_view filename : preferredFileNames) + for (const std::string_view filename : preferredFileNames) { image = tryLoadImageFromFilename(filename); if (image) @@ -267,7 +267,7 @@ namespace lms::cover std::error_code ec; std::filesystem::directory_iterator itPath(directoryPath, ec); - std::filesystem::directory_iterator itEnd; + const std::filesystem::directory_iterator itEnd; while (!ec && itPath != itEnd) { const std::filesystem::path& path{ *itPath }; @@ -306,9 +306,9 @@ namespace lms::cover { using namespace db; - const CacheEntryDesc cacheEntryDesc{ trackId, width }; + const ImageCache::EntryDesc cacheEntryDesc{ trackId, width }; - std::shared_ptr cover{ loadFromCache(cacheEntryDesc) }; + std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; if (cover) return cover; @@ -331,7 +331,7 @@ namespace lms::cover } if (cover) - saveToCache(cacheEntryDesc, cover); + _cache.addImage(cacheEntryDesc, cover); return cover; } @@ -339,9 +339,9 @@ namespace lms::cover std::shared_ptr CoverService::getFromRelease(db::ReleaseId releaseId, ImageSize width) { using namespace db; - const CacheEntryDesc cacheEntryDesc{ releaseId, width }; + const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width }; - std::shared_ptr cover{ loadFromCache(cacheEntryDesc) }; + std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; if (cover) return cover; @@ -380,7 +380,7 @@ namespace lms::cover } if (cover) - saveToCache(cacheEntryDesc, cover); + _cache.addImage(cacheEntryDesc, cover); return cover; } @@ -388,9 +388,9 @@ namespace lms::cover std::shared_ptr CoverService::getFromArtist(db::ArtistId artistId, ImageSize width) { using namespace db; - const CacheEntryDesc cacheEntryDesc{ artistId, width }; + const ImageCache::EntryDesc cacheEntryDesc{ artistId, width }; - std::shared_ptr artistImage{ loadFromCache(cacheEntryDesc) }; + std::shared_ptr artistImage{ _cache.getImage(cacheEntryDesc) }; if (artistImage) return artistImage; @@ -480,20 +480,13 @@ namespace lms::cover } if (artistImage) - saveToCache(cacheEntryDesc, artistImage); + _cache.addImage(cacheEntryDesc, artistImage); return artistImage; } void CoverService::flushCache() { - std::unique_lock lock{ _cacheMutex }; - - LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize); - _cacheHits = 0; - _cacheMisses = 0; - _cacheSize = 0; - _cache.clear(); } void CoverService::setJpegQuality(unsigned quality) @@ -503,35 +496,5 @@ namespace lms::cover LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality); } - void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image) - { - std::unique_lock lock{ _cacheMutex }; - - while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) - { - auto itRandom{ core::random::pickRandom(_cache) }; - _cacheSize -= itRandom->second->getDataSize(); - _cache.erase(itRandom); - } - - _cacheSize += image->getDataSize(); - _cache[entryDesc] = image; - } - - std::shared_ptr CoverService::loadFromCache(const CacheEntryDesc& entryDesc) - { - std::shared_lock lock{ _cacheMutex }; - - auto it{ _cache.find(entryDesc) }; - if (it == std::cend(_cache)) - { - ++_cacheMisses; - return nullptr; - } - - ++_cacheHits; - return it->second; - } - } // namespace lms::cover diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index 40fffe4c..946d3845 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -19,19 +19,14 @@ #pragma once -#include #include #include -#include -#include -#include -#include -#include #include #include "services/cover/ICoverService.hpp" #include "image/IEncodedImage.hpp" #include "database/Types.hpp" +#include "ImageCache.hpp" namespace lms::db { @@ -43,42 +38,6 @@ namespace lms::av class IAudioFile; } -namespace lms::cover -{ - struct CacheEntryDesc - { - std::variant id; - std::size_t size; - - bool operator==(const CacheEntryDesc& other) const - { - return id == other.id - && size == other.size; - } - }; -} // ns Cover - -namespace std -{ - template<> - class hash - { - public: - size_t operator()(const lms::cover::CacheEntryDesc& e) const - { - size_t h{}; - std::visit([&](auto id) - { - using IdType = std::decay_t; - h ^= std::hash{}(id); - }, e.id); - h ^= std::hash{}(e.size) << 1; - return h; - } - }; - -} // ns std - namespace lms::cover { class CoverService : public ICoverService @@ -106,21 +65,13 @@ namespace lms::cover std::unique_ptr getFromDirectory(const std::filesystem::path& directory, image::ImageSize width, const std::vector& preferredFileNames, bool allowPickRandom) const; std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, image::ImageSize width) const; - bool checkCoverFile(const std::filesystem::path& directoryPath) const; + bool checkCoverFile(const std::filesystem::path& filePath) const; db::Db& _db; - std::shared_mutex _cacheMutex; - std::unordered_map> _cache; + ImageCache _cache; std::shared_ptr _defaultCover; - std::atomic _cacheMisses{}; - std::atomic _cacheHits{}; - std::size_t _cacheSize{}; - void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); - std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); - - const std::size_t _maxCacheSize; static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize const std::size_t _maxFileSize; const std::vector _preferredFileNames; diff --git a/src/libs/services/cover/impl/ImageCache.cpp b/src/libs/services/cover/impl/ImageCache.cpp new file mode 100644 index 00000000..1f9b6550 --- /dev/null +++ b/src/libs/services/cover/impl/ImageCache.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 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 "ImageCache.hpp" + +#include + +#include "core/Random.hpp" +#include "core/ILogger.hpp" + +namespace lms::cover +{ + ImageCache::ImageCache(std::size_t maxCacheSize) + : _maxCacheSize{ maxCacheSize } + {} + + void ImageCache::addImage(const EntryDesc& entryDesc, std::shared_ptr image) + { + const std::unique_lock lock{ _mutex }; + + while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) + { + auto itRandom{ core::random::pickRandom(_cache) }; + _cacheSize -= itRandom->second->getDataSize(); + _cache.erase(itRandom); + } + + _cacheSize += image->getDataSize(); + _cache[entryDesc] = image; + } + + std::shared_ptr ImageCache::getImage(const EntryDesc& entryDesc) const + { + const std::shared_lock lock{ _mutex }; + + const auto it{ _cache.find(entryDesc) }; + if (it == std::cend(_cache)) + { + ++_cacheMisses; + return nullptr; + } + + ++_cacheHits; + return it->second; + + } + + void ImageCache::flush() + { + const std::unique_lock lock{ _mutex }; + + LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits.load() << ", misses = " << _cacheMisses.load() << ", nb entries = " << _cache.size() << ", size = " << _cacheSize); + _cacheHits = 0; + _cacheMisses = 0; + _cacheSize = 0; + _cache.clear(); + } +} \ No newline at end of file diff --git a/src/libs/services/cover/impl/ImageCache.hpp b/src/libs/services/cover/impl/ImageCache.hpp new file mode 100644 index 00000000..9ee44e14 --- /dev/null +++ b/src/libs/services/cover/impl/ImageCache.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "database/ArtistId.hpp" +#include "database/ReleaseId.hpp" +#include "database/TrackId.hpp" +#include "image/IEncodedImage.hpp" + +namespace lms::cover +{ + class ImageCache + { + public: + ImageCache(std::size_t maxCacheSize); + + struct EntryDesc + { + using VariantType = std::variant; + VariantType id; + std::size_t size; + + bool operator==(const EntryDesc& other) const = default; + }; + + std::size_t getMaxCacheSize() const { return _maxCacheSize; } + + void addImage(const EntryDesc& entryDesc, std::shared_ptr image); + std::shared_ptr getImage(const EntryDesc& entryDesc) const; + void flush(); + + private: + const std::size_t _maxCacheSize; + + mutable std::shared_mutex _mutex; + + struct EntryHasher + { + constexpr std::size_t operator()(const EntryDesc& entry) const + { + return std::hash{}(entry.id) ^ std::hash{}(entry.size); + } + }; + + std::unordered_map, EntryHasher> _cache; + std::size_t _cacheSize{}; + mutable std::atomic _cacheMisses{}; + mutable std::atomic _cacheHits{}; + }; +} \ No newline at end of file From 98d62c6419e8f8d2e9a14b787491946c6daac29c Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 29 Mar 2024 21:02:59 +0100 Subject: [PATCH 11/64] Updated clang tidy rules --- .clang-tidy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.clang-tidy b/.clang-tidy index 43c1aa0e..1eaf7039 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -12,4 +12,6 @@ CheckOptions: value: '1' - key: cppcoreguidelines-avoid-do-while.IgnoreMacros value: '1' + - key: performance-unnecessary-value-param.AllowedTypes + value: "shared_ptr" ... From bad7ef10061a596ad962fb9325696a1b4bf415c4 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 29 Mar 2024 21:03:22 +0100 Subject: [PATCH 12/64] Added missing file to build --- src/libs/services/cover/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/services/cover/CMakeLists.txt b/src/libs/services/cover/CMakeLists.txt index ebe890f2..2f7c054f 100644 --- a/src/libs/services/cover/CMakeLists.txt +++ b/src/libs/services/cover/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmsservice-cover SHARED + impl/ImageCache.cpp impl/CoverService.cpp ) From a91fc78d800fd29867a1c47c57b8b432183ac0ec Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 29 Mar 2024 23:40:39 +0100 Subject: [PATCH 13/64] Fixed ubuntu build --- src/libs/services/cover/impl/ImageCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/services/cover/impl/ImageCache.hpp b/src/libs/services/cover/impl/ImageCache.hpp index 9ee44e14..a57553e8 100644 --- a/src/libs/services/cover/impl/ImageCache.hpp +++ b/src/libs/services/cover/impl/ImageCache.hpp @@ -58,7 +58,7 @@ namespace lms::cover struct EntryHasher { - constexpr std::size_t operator()(const EntryDesc& entry) const + std::size_t operator()(const EntryDesc& entry) const { return std::hash{}(entry.id) ^ std::hash{}(entry.size); } From 9ca4ac073acdbb5987ab3e0177245c1057e23663 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 30 Mar 2024 00:04:57 +0100 Subject: [PATCH 14/64] Added some traces for image handling --- src/libs/image/impl/SvgImage.cpp | 3 +++ src/libs/image/impl/stb/JPEGImage.cpp | 3 +++ src/libs/image/impl/stb/RawImage.cpp | 8 ++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/libs/image/impl/SvgImage.cpp b/src/libs/image/impl/SvgImage.cpp index 433c58ad..87636937 100644 --- a/src/libs/image/impl/SvgImage.cpp +++ b/src/libs/image/impl/SvgImage.cpp @@ -20,12 +20,15 @@ #include "SvgImage.hpp" #include +#include "core/ITraceLogger.hpp" #include "image/Exception.hpp" namespace lms::image { std::unique_ptr readSvgFile(const std::filesystem::path& p) { + LMS_SCOPED_TRACE_DETAILED("Image", "ReadSVG"); + if (p.extension() != ".svg") throw Exception{ "Unexpected file extension: '" + p.extension().string() + "', expected .svg" }; diff --git a/src/libs/image/impl/stb/JPEGImage.cpp b/src/libs/image/impl/stb/JPEGImage.cpp index 5ecad160..681d93ea 100644 --- a/src/libs/image/impl/stb/JPEGImage.cpp +++ b/src/libs/image/impl/stb/JPEGImage.cpp @@ -22,6 +22,7 @@ #define STB_IMAGE_WRITE_IMPLEMENTATION #include +#include "core/ITraceLogger.hpp" #include "image/Exception.hpp" #include "RawImage.hpp" @@ -29,6 +30,8 @@ namespace lms::image::STB { JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) { + LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG"); + auto writeCb {[](void* ctx, void* writeData, int writeSize) { auto& output {*reinterpret_cast*>(ctx)}; diff --git a/src/libs/image/impl/stb/RawImage.cpp b/src/libs/image/impl/stb/RawImage.cpp index d4344056..c55164f0 100644 --- a/src/libs/image/impl/stb/RawImage.cpp +++ b/src/libs/image/impl/stb/RawImage.cpp @@ -30,19 +30,21 @@ #include #include -#include "JPEGImage.hpp" - +#include "core/ITraceLogger.hpp" #include "image/Exception.hpp" +#include "JPEGImage.hpp" namespace lms::image { std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize) { + LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer"); return std::make_unique(encodedData, encodedDataSize); } std::unique_ptr decodeImage(const std::filesystem::path& path) { + LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile"); return std::make_unique(path); } @@ -71,6 +73,8 @@ namespace lms::image::STB void RawImage::resize(ImageSize width) { + LMS_SCOPED_TRACE_DETAILED("Image", "Resize"); + size_t height; if (_width == _height) { From 4ba83301948970d007ddaaba3df0c3f1697040ed Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 31 Mar 2024 13:07:02 +0200 Subject: [PATCH 15/64] Tracing: use only lower 32bits for thread, to make capture compatible with perfetto UI --- src/libs/core/impl/TraceLogger.cpp | 25 +++++++++++++++++++++++-- src/libs/core/impl/TraceLogger.hpp | 2 ++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/libs/core/impl/TraceLogger.cpp b/src/libs/core/impl/TraceLogger.cpp index 20d4b1c2..614713ae 100644 --- a/src/libs/core/impl/TraceLogger.cpp +++ b/src/libs/core/impl/TraceLogger.cpp @@ -141,7 +141,7 @@ namespace lms::core::tracing os << "\t\t{ "; os << "\"name\" : \"thread_name\", "; os << "\"pid\" : 1, "; - os << "\"tid\" : " << threadId << ", "; + os << "\"tid\" : " << toTraceThreadId(threadId) << ", "; os << "\"ph\" : \"M\", "; os << "\"args\" : { \"name\" : \"" + threadName + "\" }"; os << " }"; @@ -171,7 +171,7 @@ namespace lms::core::tracing os << "\"name\" : \"" << event.name.c_str() << "\", "; os << "\"cat\" : \"" << event.category.c_str() << "\", "; os << "\"pid\": 1, "; - os << "\"tid\" : " << event.threadId << ", "; + os << "\"tid\" : " << toTraceThreadId(event.threadId) << ", "; os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.start - _start).count() << ", "; os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.duration).count() << ", "; os << "\"ph\" : \"X\""; @@ -198,4 +198,25 @@ namespace lms::core::tracing std::scoped_lock lock{ _threadNameMutex }; _threadNames.emplace(id, threadName); } + + std::uint32_t TraceLogger::toTraceThreadId(std::thread::id threadId) const + { + { + auto it{ _cachedTraceThreadIds.find(threadId) }; + if (it != std::cend(_cachedTraceThreadIds)) + return it->second; + } + + // Pefetto UI does not accept 64bits thread ids + std::ostringstream oss; + oss << threadId; + + std::istringstream iss{ oss.str() }; + std::uint64_t id; + iss >> id; + + const std::uint32_t res{ static_cast(id) }; + _cachedTraceThreadIds.emplace(threadId, res); + return res; + } } \ No newline at end of file diff --git a/src/libs/core/impl/TraceLogger.hpp b/src/libs/core/impl/TraceLogger.hpp index 9addf082..17e0cea7 100644 --- a/src/libs/core/impl/TraceLogger.hpp +++ b/src/libs/core/impl/TraceLogger.hpp @@ -42,6 +42,7 @@ namespace lms::core::tracing void write(const CompleteEvent& event) override; void dumpCurrentBuffer(std::ostream& os) override; void setThreadName(std::thread::id id, std::string_view threadName) override; + std::uint32_t toTraceThreadId(std::thread::id threadId) const; static constexpr std::size_t BufferSize{ 32 * 1024 }; @@ -64,6 +65,7 @@ namespace lms::core::tracing std::mutex _threadNameMutex; std::unordered_map _threadNames; + mutable std::unordered_map _cachedTraceThreadIds; std::mutex _mutex; std::deque _freeBuffers; From 418286a1f23cae41c5d2e7b56993fcca9425ded5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 31 Mar 2024 13:13:01 +0200 Subject: [PATCH 16/64] Chaned the way the inner dbo session is accessed + added exec wrappers for queries --- src/libs/database/impl/Artist.cpp | 37 ++-- src/libs/database/impl/AuthToken.cpp | 46 +++-- src/libs/database/impl/Cluster.cpp | 56 +++--- src/libs/database/impl/Listen.cpp | 71 ++++--- src/libs/database/impl/MediaLibrary.cpp | 13 +- src/libs/database/impl/Migration.cpp | 174 +++++++++--------- src/libs/database/impl/Release.cpp | 119 +++++------- src/libs/database/impl/ScanSettings.cpp | 5 +- src/libs/database/impl/StarredArtist.cpp | 16 +- src/libs/database/impl/StarredRelease.cpp | 16 +- src/libs/database/impl/StarredTrack.cpp | 25 ++- src/libs/database/impl/Track.cpp | 64 +++---- src/libs/database/impl/TrackArtistLink.cpp | 18 +- src/libs/database/impl/TrackBookmark.cpp | 18 +- src/libs/database/impl/TrackFeatures.cpp | 18 +- src/libs/database/impl/TrackList.cpp | 47 +++-- src/libs/database/impl/User.cpp | 21 +-- src/libs/database/impl/Utils.hpp | 34 +++- .../database/include/database/Session.hpp | 11 +- 19 files changed, 386 insertions(+), 423 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index d0a1d0b1..b5479f60 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -41,7 +41,7 @@ namespace lms::db session.checkReadTransaction(); // TODO remove distinct and use group by - auto query{ session.getDboSession().query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") }; + auto query{ session.getDboSession()->query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") }; if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType @@ -175,21 +175,21 @@ namespace lms::db Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional& MBID) { - return session.getDboSession().add(std::unique_ptr{ new Artist{ name, MBID } }); + return session.getDboSession()->add(std::unique_ptr{ new Artist{ name, MBID } }); } std::size_t Artist::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM artist"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM artist")); } void Artist::find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function& func, MediaLibraryId library) { session.checkReadTransaction(); - auto query{ session.getDboSession().query>("SELECT a FROM artist a") + auto query{ session.getDboSession()->query>("SELECT a FROM artist a") .orderBy("a.id") .where("a.id > ?").bind(lastRetrievedArtist) .limit(static_cast(count)) }; @@ -201,7 +201,7 @@ namespace lms::db query.where("t.media_library_id = ?").bind(library); } - auto collection{ query.resultList() }; + auto collection{ utils::execMultiResultQuery(query) }; for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { @@ -214,9 +214,9 @@ namespace lms::db { session.checkReadTransaction(); - Wt::Dbo::collection> res = session.getDboSession().find() + Wt::Dbo::collection> res{ utils::execMultiResultQuery(session.getDboSession()->find() .where("name = ?").bind(std::string{ name, 0, _maxNameLength }) - .orderBy("LENGTH(mbid) DESC"); // put mbid entries first + .orderBy("LENGTH(mbid) DESC")) }; // put mbid entries first return std::vector(res.begin(), res.end()); } @@ -224,27 +224,26 @@ namespace lms::db Artist::pointer Artist::find(Session& session, const core::UUID& mbid) { session.checkReadTransaction(); - return session.getDboSession().find().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("mbid = ?").bind(std::string{ mbid.getAsString() })); } Artist::pointer Artist::find(Session& session, ArtistId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } bool Artist::exists(Session& session, ArtistId id) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1; + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1; } - RangeResults Artist::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") }; - return utils::execQuery(query, range); + auto query{ session.getDboSession()->query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") }; + return utils::execRangeQuery(query, range); } RangeResults Artist::findIds(Session& session, const FindParameters& params) @@ -252,7 +251,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery(session, params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Artist::find(Session& session, const FindParameters& params) @@ -260,7 +259,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } void Artist::find(Session& session, const FindParameters& params, std::function func) @@ -268,7 +267,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, params) }; - utils::execQuery(query, params.range, func); + utils::execRangeQuery(query, params.range, func); } RangeResults Artist::findSimilarArtistIds(core::EnumSet artistLinkTypes, std::optional range) const @@ -315,7 +314,7 @@ namespace lms::db for (const TrackArtistLinkType type : artistLinkTypes) query.bind(type); - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } std::vector> Artist::getClusterGroups(std::vector clusterTypeIds, std::size_t size) const @@ -338,12 +337,12 @@ namespace lms::db oss << " " << where.get(); oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC"; - Wt::Dbo::Query> query = session()->query>(oss.str()); + Wt::Dbo::Query> query{ session()->query>(oss.str()) }; for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - Wt::Dbo::collection> queryRes = query; + Wt::Dbo::collection> queryRes{ utils::execMultiResultQuery(query) }; std::map> clustersByType; for (const Cluster::pointer& cluster : queryRes) diff --git a/src/libs/database/impl/AuthToken.cpp b/src/libs/database/impl/AuthToken.cpp index db3e29bb..b5e3e6c9 100644 --- a/src/libs/database/impl/AuthToken.cpp +++ b/src/libs/database/impl/AuthToken.cpp @@ -24,38 +24,34 @@ #include "database/User.hpp" #include "StringViewTraits.hpp" #include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace lms::db { - AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) - : _value {value} - , _expiry {expiry} - , _user {getDboPtr(user)} - { - } + AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + : _value{ value } + , _expiry{ expiry } + , _user{ getDboPtr(user) } + { + } - AuthToken::pointer - AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) - { - return session.getDboSession().add(std::unique_ptr {new AuthToken {value, expiry, user}}); - } + AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + { + return session.getDboSession()->add(std::unique_ptr {new AuthToken{ value, expiry, user }}); + } - void - AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) - { - session.checkWriteTransaction(); + void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) + { + session.checkWriteTransaction(); - session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now); - } + session.getDboSession()->execute("DELETE FROM auth_token WHERE expiry < ?").bind(now); + } - AuthToken::pointer - AuthToken::find(Session& session, std::string_view value) - { - session.checkReadTransaction(); + AuthToken::pointer AuthToken::find(Session& session, std::string_view value) + { + session.checkReadTransaction(); - return session.getDboSession().find() - .where("value = ?").bind(value) - .resultValue(); - } + return utils::execSingleResultQuery(session.getDboSession()->find().where("value = ?").bind(value)); + } } diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 37dc1f89..76da0d42 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -38,7 +38,7 @@ namespace lms::db { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") }; + auto query{ session.getDboSession()->query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") }; if (params.track.isValid() || params.release.isValid()) { @@ -86,14 +86,14 @@ namespace lms::db Cluster::pointer Cluster::create(Session& session, ObjectPtr type, std::string_view name) { - return session.getDboSession().add(std::unique_ptr {new Cluster{ type, name }}); + return session.getDboSession()->add(std::unique_ptr {new Cluster{ type, name }}); } std::size_t Cluster::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM cluster"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM cluster")); } RangeResults Cluster::findIds(Session& session, const FindParameters& params) @@ -101,7 +101,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery(session, params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Cluster::find(Session& session, const FindParameters& params) @@ -109,38 +109,38 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Cluster::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") }; + auto query{ session.getDboSession()->query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } Cluster::pointer Cluster::find(Session& session, ClusterId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return session.getDboSession()->find().where("id = ?").bind(id).resultValue(); } std::size_t Cluster::computeTrackCount(Session& session, ClusterId id) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .where("t_c.cluster_id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("t_c.cluster_id = ?").bind(id)); } std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .where("t_c.cluster_id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("t_c.cluster_id = ?").bind(id)); } void Cluster::addTrack(ObjectPtr track) @@ -155,7 +155,7 @@ namespace lms::db auto query{ session()->query("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .where("c.id = ?").bind(getId()) }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } ClusterType::ClusterType(std::string_view name) @@ -165,61 +165,60 @@ namespace lms::db ClusterType::pointer ClusterType::create(Session& session, std::string_view name) { - return session.getDboSession().add(std::unique_ptr {new ClusterType{ name }}); + return session.getDboSession()->add(std::unique_ptr {new ClusterType{ name }}); } std::size_t ClusterType::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM cluster_type"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM cluster_type")); } - RangeResults ClusterType::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query( + auto query{ session.getDboSession()->query( "SELECT c_t.id from cluster_type c_t" " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") .where("c.id IS NULL") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } RangeResults ClusterType::findUsed(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query( + auto query{ session.getDboSession()->query( "SELECT DISTINCT c_t.id from cluster_type c_t") .join("cluster c ON c_t.id = c.cluster_type_id") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } ClusterType::pointer ClusterType::find(Session& session, std::string_view name) { session.checkReadTransaction(); - return session.getDboSession().find().where("name = ?").bind(std::string{ name }).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("name = ?").bind(std::string{ name })); } ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } RangeResults ClusterType::findIds(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT id from cluster_type") }; + auto query{ session.getDboSession()->query("SELECT id from cluster_type") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } Cluster::pointer ClusterType::getCluster(const std::string& name) const @@ -227,9 +226,9 @@ namespace lms::db assert(self()); assert(session()); - return session()->find() + return utils::execSingleResultQuery(session()->find() .where("name = ?").bind(name) - .where("cluster_type_id = ?").bind(getId()).resultValue(); + .where("cluster_type_id = ?").bind(getId())); } std::vector ClusterType::getClusters() const @@ -237,10 +236,9 @@ namespace lms::db assert(self()); assert(session()); - auto res = session()->find() + auto res {utils::execMultiResultQuery(session()->find() .where("cluster_type_id = ?").bind(getId()) - .orderBy("name") - .resultList(); + .orderBy("name"))}; return std::vector(res.begin(), res.end()); } diff --git a/src/libs/database/impl/Listen.cpp b/src/libs/database/impl/Listen.cpp index 291fe119..fecdbc5f 100644 --- a/src/libs/database/impl/Listen.cpp +++ b/src/libs/database/impl/Listen.cpp @@ -29,9 +29,9 @@ namespace lms::db { namespace { - Wt::Dbo::Query createArtistsQuery(Wt::Dbo::Session& session, const Listen::ArtistStatsFindParameters& params) + Wt::Dbo::Query createArtistsQuery(Session& session, const Listen::ArtistStatsFindParameters& params) { - auto query{ session.query("SELECT a.id from artist a") + auto query{ session.getDboSession()->query("SELECT a.id from artist a") .join("track t ON t.id = t_a_l.track_id") .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") .join("listen l ON l.track_id = t.id") }; @@ -75,9 +75,9 @@ namespace lms::db return query; } - Wt::Dbo::Query createReleasesQuery(Wt::Dbo::Session& session, const Listen::StatsFindParameters& params) + Wt::Dbo::Query createReleasesQuery(Session& session, const Listen::StatsFindParameters& params) { - auto query{ session.query("SELECT r.id from release r") + auto query{ session.getDboSession()->query("SELECT r.id from release r") .join("track t ON t.release_id = r.id") .join("listen l ON l.track_id = t.id") }; @@ -120,9 +120,9 @@ namespace lms::db return query; } - Wt::Dbo::Query createTracksQuery(Wt::Dbo::Session& session, const Listen::StatsFindParameters& params) + Wt::Dbo::Query createTracksQuery(Session& session, const Listen::StatsFindParameters& params) { - auto query{ session.query("SELECT t.id from track t") + auto query{ session.getDboSession()->query("SELECT t.id from track t") .join("listen l ON l.track_id = t.id") }; if (params.user.isValid()) @@ -174,26 +174,26 @@ namespace lms::db Listen::pointer Listen::create(Session& session, ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime) { session.checkWriteTransaction(); - return session.getDboSession().add(std::unique_ptr {new Listen{ user, track, backend, dateTime }}); + return session.getDboSession()->add(std::unique_ptr {new Listen{ user, track, backend, dateTime }}); } std::size_t Listen::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM listen"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM listen")); } Listen::pointer Listen::find(Session& session, ListenId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } RangeResults Listen::find(Session& session, const FindParameters& parameters) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT id FROM listen") + auto query{ session.getDboSession()->query("SELECT id FROM listen") .orderBy("date_time") }; if (parameters.user.isValid()) @@ -205,100 +205,98 @@ namespace lms::db if (parameters.syncState) query.where("sync_state = ?").bind(*parameters.syncState); - return utils::execQuery(query, parameters.range); + return utils::execRangeQuery(query, parameters.range); } Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime) { session.checkReadTransaction(); - return session.getDboSession().find() + return utils::execSingleResultQuery(session.getDboSession()->find() .where("user_id = ?").bind(userId) .where("track_id = ?").bind(trackId) .where("backend = ?").bind(backend) - .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) - .resultValue(); + .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))); } RangeResults Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createArtistsQuery(session.getDboSession(), params) }; + auto query{ createArtistsQuery(session, params) }; auto collection{ query .orderBy("COUNT(a.id) DESC") .groupBy("a.id") }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Listen::getTopReleases(Session& session, const StatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createReleasesQuery(session.getDboSession(), params) + auto query{ createReleasesQuery(session, params) .orderBy("COUNT(r.id) DESC") .groupBy("r.id") }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Listen::getTopTracks(Session& session, const StatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createTracksQuery(session.getDboSession(), params) + auto query{ createTracksQuery(session, params) .orderBy("COUNT(t.id) DESC") .groupBy("t.id") }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createArtistsQuery(session.getDboSession(), params) + auto query{ createArtistsQuery(session, params) .groupBy("a.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Listen::getRecentReleases(Session& session, const StatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createReleasesQuery(session.getDboSession(), params) + auto query{ createReleasesQuery(session, params) .groupBy("r.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } RangeResults Listen::getRecentTracks(Session& session, const StatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createTracksQuery(session.getDboSession(), params) + auto query{ createTracksQuery(session, params) .groupBy("t.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } std::size_t Listen::getCount(Session& session, UserId userId, TrackId trackId) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) from listen l") + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) from listen l") .join("user u ON u.id = l.user_id") .where("l.track_id = ?").bind(trackId) .where("l.user_id = ?").bind(userId) - .where("l.backend = u.scrobbling_backend") - .resultValue(); + .where("l.backend = u.scrobbling_backend")); } std::size_t Listen::getCount(Session& session, UserId userId, ReleaseId releaseId) { session.checkReadTransaction(); - return session.getDboSession().query( + return utils::execSingleResultQuery(session.getDboSession()->query( "SELECT IFNULL(MIN(count_result), 0)" " FROM (" " SELECT COUNT(l.track_id) AS count_result" @@ -308,8 +306,7 @@ namespace lms::db " GROUP BY t.id)") .bind(userId) .bind(userId) - .bind(releaseId) - .resultValue(); + .bind(releaseId)); } Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId) @@ -317,26 +314,24 @@ namespace lms::db session.checkReadTransaction(); // TODO not pending remove? - return session.getDboSession().query>("SELECT l from listen l") + return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT l from listen l") .join("track t ON l.track_id = t.id") .where("t.release_id = ?").bind(releaseId) .where("l.user_id = ?").bind(userId) .where("l.backend = ?").bind(backend) .orderBy("l.date_time DESC") - .limit(1) - .resultValue(); + .limit(1)); } Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) { session.checkReadTransaction(); // TODO not pending remove? - return session.getDboSession().query>("SELECT l from listen l") + return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT l from listen l") .where("l.track_id = ?").bind(trackId) .where("l.user_id = ?").bind(userId) .where("l.backend = ?").bind(backend) .orderBy("l.date_time DESC") - .limit(1) - .resultValue(); + .limit(1)); } } // namespace lms::db diff --git a/src/libs/database/impl/MediaLibrary.cpp b/src/libs/database/impl/MediaLibrary.cpp index cd7e7032..bda69a7a 100644 --- a/src/libs/database/impl/MediaLibrary.cpp +++ b/src/libs/database/impl/MediaLibrary.cpp @@ -25,6 +25,7 @@ #include "IdTypeTraits.hpp" #include "PathTraits.hpp" #include "StringViewTraits.hpp" +#include "Utils.hpp" namespace lms::db { @@ -36,42 +37,42 @@ namespace lms::db MediaLibrary::pointer MediaLibrary::create(Session& session, const std::filesystem::path& p, std::string_view name) { - return session.getDboSession().add(std::unique_ptr{ new MediaLibrary{ p, name } }); + return session.getDboSession()->add(std::unique_ptr{ new MediaLibrary{ p, name } }); } std::size_t MediaLibrary::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM media_library"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM media_library")); } MediaLibrary::pointer MediaLibrary::find(Session& session, MediaLibraryId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } MediaLibrary::pointer MediaLibrary::find(Session& session, std::string_view name) { session.checkReadTransaction(); - return session.getDboSession().find().where("name = ?").bind(name).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("name = ?").bind(name)); } MediaLibrary::pointer MediaLibrary::find(Session& session, const std::filesystem::path& p) { session.checkReadTransaction(); - return session.getDboSession().find().where("path = ?").bind(p).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("path = ?").bind(p)); } void MediaLibrary::find(Session& session, std::function func) { session.checkReadTransaction(); - auto results{ session.getDboSession().find().resultList() }; + auto results{ utils::execMultiResultQuery(session.getDboSession()->find()) }; for (const auto& result : results) func(result); } diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 214e64f0..a2f7121a 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -43,9 +43,9 @@ namespace lms::db { session.checkWriteTransaction(); - pointer versionInfo{ session.getDboSession().find() }; + pointer versionInfo{ session.getDboSession()->find() }; if (!versionInfo) - return session.getDboSession().add(std::make_unique()); + return session.getDboSession()->add(std::make_unique()); return versionInfo; } @@ -54,7 +54,7 @@ namespace lms::db { session.checkReadTransaction(); - return session.getDboSession().find(); + return session.getDboSession()->find(); } } @@ -85,7 +85,7 @@ namespace lms::db::Migration { // remove name from track_artist_link // Drop Auth mode - session.getDboSession().execute(R"( + session.getDboSession()->execute(R"( CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -96,49 +96,49 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred ); ))"); - session.getDboSession().execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); - session.getDboSession().execute("DROP TABLE track_artist_link"); - session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); + session.getDboSession()->execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); + session.getDboSession()->execute("DROP TABLE track_artist_link"); + session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); } static void migrateFromV34(Session& session) { // Add scrobbling state // By default, everything needs to be sent - session.getDboSession().execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); - session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); - session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); + session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); + session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); + session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); } static void migrateFromV35(Session& session) { // Add creattion/last modif date time for tracklists - session.getDboSession().execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); - session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); + session.getDboSession()->execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); + session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); } static void migrateFromV36(Session& session) { // Increased precision for track durations (now in milliseconds instead of secodns) // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } static void migrateFromV37(Session& session) { // Support Performer tags (via subtypes) - session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT"); + session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } static void migrateFromV38(Session& session) { // migrate release-specific tags from Track to Release - session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER"); + session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER"); - session.getDboSession().execute(R"( + session.getDboSession()->execute(R"( CREATE TABLE IF NOT EXISTS "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -165,177 +165,177 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ); ))"); - session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); - session.getDboSession().execute("DROP TABLE track"); - session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); + session.getDboSession()->execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); + session.getDboSession()->execute("DROP TABLE track"); + session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } static void migrateFromV39(Session& session) { // add release type - session.getDboSession().execute("ALTER TABLE release ADD primary_type INTEGER"); - session.getDboSession().execute("ALTER TABLE release ADD secondary_types INTEGER"); + session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER"); + session.getDboSession()->execute("ALTER TABLE release ADD secondary_types INTEGER"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } static void migrateFromV40(Session& session) { // add artist_display_name in Release and Track - session.getDboSession().execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''"); - session.getDboSession().execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } static void migrateFromV41(Session& session) { // add artist_display_name in Release and Track - session.getDboSession().execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format"); - session.getDboSession().execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate"); - session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); + session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format"); + session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate"); + session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); } static void migrateFromV42(Session& session) { - session.getDboSession().execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); - session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); // New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service) - session.getDboSession().execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); - session.getDboSession().execute("ALTER TABLE user ADD feedback_backend INTEGER"); - session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); - session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); - session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); - session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); - session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); + session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER"); + session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend"); + session.getDboSession()->execute("UPDATE user SET feedback_backend = scrobbling_backend"); } static void migrateFromV43(Session& session) { // add counts in genre table - session.getDboSession().execute("ALTER TABLE cluster ADD track_count INTEGER"); - session.getDboSession().execute("ALTER TABLE cluster ADD release_count INTEGER"); + session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER"); + session.getDboSession()->execute("ALTER TABLE cluster ADD release_count INTEGER"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } static void migrateFromV44(Session& session) { // add bitrate - session.getDboSession().execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); + session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void migrateFromV45(Session& session) { // add subsonic_enable_transcoding_by_default, default is disabled - session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")"); + session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")"); } void migrateFromV46(Session& session) { // add extra tags to parse - session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( "id" integer primary key autoincrement, "version" integer not null, "name" text not null );)"); - session.getDboSession().execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type"); - session.getDboSession().execute("DROP TABLE cluster_type"); - session.getDboSession().execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type"); + session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type"); + session.getDboSession()->execute("DROP TABLE cluster_type"); + session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type"); - session.getDboSession().execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT"); + session.getDboSession()->execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void migrateFromV47(Session& session) { // release type, new way - session.getDboSession().execute("ALTER TABLE release DROP primary_type"); - session.getDboSession().execute("ALTER TABLE release DROP secondary_types"); + session.getDboSession()->execute("ALTER TABLE release DROP primary_type"); + session.getDboSession()->execute("ALTER TABLE release DROP secondary_types"); - session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "release_type" ( + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_type" ( "id" integer primary key autoincrement, "version" integer not null, "name" text not null))"); - session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" ( + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" ( "release_type_id" bigint, "release_id" bigint, primary key ("release_type_id", "release_id"), constraint "fk_release_release_type_key1" foreign key ("release_type_id") references "release_type" ("id") on delete cascade deferrable initially deferred, constraint "fk_release_release_type_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))"); - session.getDboSession().execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))"); + session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))"); + session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void migrateFromV48(Session& session) { // Regression for the extra tags not being parsed // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void migrateFromV49(Session& session) { // Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing) - session.getDboSession().execute("ALTER TABLE track ADD year INTEGER"); - session.getDboSession().execute("ALTER TABLE track ADD original_year INTEGER"); + session.getDboSession()->execute("ALTER TABLE track ADD year INTEGER"); + session.getDboSession()->execute("ALTER TABLE track ADD original_year INTEGER"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void migrateFromV50(Session& session) { // MediaLibrary support - session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( "id" integer primary key autoincrement, "version" integer not null, "path" text not null, "name" text not null ))"); - const int scanSettingsId{ session.getDboSession().query("SELECT id FROM scan_settings") }; + const int scanSettingsId{ session.getDboSession()->query("SELECT id FROM scan_settings") }; // Convert the existing media_directory in the scan_settings table to a media_library with id '1' - session.getDboSession().execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name") + session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name") SELECT 1, 0, s_s.media_directory, "Main" FROM scan_settings s_s WHERE id = ?)").bind(scanSettingsId); // Remove the outdated column in scan_settings - session.getDboSession().execute("ALTER TABLE scan_settings DROP media_directory"); + session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory"); // Add the media_library column in tracks, with id '1' - session.getDboSession().execute(R"( + session.getDboSession()->execute(R"( CREATE TABLE IF NOT EXISTS "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -369,7 +369,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( ))"); // Migrate data, with the new media_library_id field set to 1 - session.getDboSession().execute(R"(INSERT INTO track_backup + session.getDboSession()->execute(R"(INSERT INTO track_backup SELECT id, version, @@ -399,33 +399,33 @@ SELECT release_id, 1 FROM track)"); - session.getDboSession().execute("DROP TABLE track"); - session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); + session.getDboSession()->execute("DROP TABLE track"); + session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); } void migrateFromV51(Session& session) { // Add custom artist tag delimiters, no need to rescan since it has no effect when empty - session.getDboSession().execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''"); - session.getDboSession().execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''"); } void migrateFromV52(Session& session) { // Add sort name for releases - session.getDboSession().execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void migrateFromV53(Session& session) { // Add release group mbid - session.getDboSession().execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } void doDbMigration(Session& session) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index c9ab558d..986caa40 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -40,7 +40,7 @@ namespace lms::db template Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params) { - auto query{ session.getDboSession().query("SELECT " + std::string{ itemToSelect } + " from release r") }; + auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " from release r") }; if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName || params.sortMethod == ReleaseSortMethod::LastWritten @@ -206,27 +206,23 @@ namespace lms::db ReleaseType::pointer ReleaseType::create(Session& session, std::string_view name) { - return session.getDboSession().add(std::unique_ptr {new ReleaseType{ name }}); + return session.getDboSession()->add(std::unique_ptr {new ReleaseType{ name }}); } ReleaseType::pointer ReleaseType::find(Session& session, ReleaseTypeId id) { session.checkReadTransaction(); - return session.getDboSession() - .find() - .where("id = ?").bind(id) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("id = ?").bind(id)); } ReleaseType::pointer ReleaseType::find(Session& session, std::string_view name) { session.checkReadTransaction(); - return session.getDboSession() - .find() - .where("name = ?").bind(name) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("name = ?").bind(name)); } Release::Release(const std::string& name, const std::optional& MBID) @@ -237,19 +233,17 @@ namespace lms::db Release::pointer Release::create(Session& session, const std::string& name, const std::optional& MBID) { - return session.getDboSession().add(std::unique_ptr {new Release{ name, MBID }}); + return session.getDboSession()->add(std::unique_ptr {new Release{ name, MBID }}); } std::vector Release::find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory) { session.checkReadTransaction(); - auto res{ session.getDboSession() - .query>("SELECT DISTINCT r from release r") + auto res{ utils::execMultiResultQuery(session.getDboSession()->query>("SELECT DISTINCT r from release r") .join("track t ON t.release_id = r.id") .where("r.name = ?").bind(std::string(name, 0, _maxNameLength)) - .where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%") - .resultList() }; + .where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%"))}; return std::vector(res.begin(), res.end()); } @@ -258,48 +252,44 @@ namespace lms::db { session.checkReadTransaction(); - return session.getDboSession() - .find() - .where("mbid = ?").bind(std::string{ mbid.getAsString() }) - .resultValue();; + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("mbid = ?").bind(mbid.getAsString())); } Release::pointer Release::find(Session& session, ReleaseId id) { session.checkReadTransaction(); - return session.getDboSession() - .find() - .where("id = ?").bind(id) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("id = ?").bind(id)); } bool Release::exists(Session& session, ReleaseId id) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 FROM release").where("id = ?").bind(id)) == 1; } std::size_t Release::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM release"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM release")); } RangeResults Release::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; - return utils::execQuery(query, range); + auto query{ session.getDboSession()->query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; + return utils::execRangeQuery(query, range); } void Release::find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function& func, MediaLibraryId library) { session.checkReadTransaction(); - auto query{ session.getDboSession().query>("SELECT r FROM release r") + auto query{ session.getDboSession()->query>("SELECT r FROM release r") .orderBy("r.id") .where("r.id > ?").bind(lastRetrievedRelease) .limit(static_cast(count)) }; @@ -310,7 +300,7 @@ namespace lms::db query.where("t.media_library_id = ?").bind(library); } - auto collection{ query.resultList() }; + auto collection{ utils::execMultiResultQuery(query) }; for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { @@ -324,7 +314,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, "DISTINCT r", params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } void Release::find(Session& session, const FindParameters& params, std::function func) @@ -332,7 +322,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, "DISTINCT r", params) }; - utils::execQuery(query, params.range, func); + utils::execRangeQuery(query, params.range, func); } RangeResults Release::findIds(Session& session, const FindParameters& params) @@ -340,23 +330,23 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery(session, "DISTINCT r.id", params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } std::size_t Release::getCount(Session& session, const FindParameters& params) { session.checkReadTransaction(); - return createQuery(session, "COUNT(DISTINCT r.id)", params).resultValue(); + return utils::execSingleResultQuery(createQuery(session, "COUNT(DISTINCT r.id)", params)); } std::size_t Release::getDiscCount() const { assert(session()); - int res{ session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") + int res{ utils::execSingleResultQuery(session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") .join("release r ON r.id = t.release_id") .where("r.id = ?") - .bind(getId()) }; + .bind(getId())) }; return res; } @@ -365,12 +355,11 @@ namespace lms::db { assert(session()); using ResultType = std::tuple; - auto results{ session()->query("SELECT DISTINCT disc_number, disc_subtitle FROM track t") + auto results{ utils::execMultiResultQuery(session()->query("SELECT DISTINCT disc_number, disc_subtitle FROM track t") .join("release r ON r.id = t.release_id") .where("r.id = ?") .orderBy("disc_number") - .bind(getId()) - .resultList() }; + .bind(getId()))}; std::vector discs; for (const auto& res : results) @@ -395,12 +384,11 @@ namespace lms::db const char* field{ original ? "original_date" : "date" }; - auto dates{ session()->query( + auto dates{ utils::execMultiResultQuery(session()->query( std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?") .groupBy(field) - .bind(getId()) - .resultList() }; + .bind(getId())) }; // various dates => invalid date if (dates.empty() || dates.size() > 1) @@ -425,14 +413,14 @@ namespace lms::db const char* field{ original ? "original_year" : "year" }; - auto years{ session()->query>( + auto years{ utils::execMultiResultQuery(session()->query>( std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?").bind(getId()) - .groupBy(field) - .resultList() }; + .groupBy(field)) }; // various years => invalid years - if (years.empty() || years.size() > 1) + const std::size_t count{ years.size() }; + if (count == 0 || count > 1) return std::nullopt; return years.front(); @@ -442,11 +430,11 @@ namespace lms::db { assert(session()); - Wt::Dbo::collection copyrights = session()->query + Wt::Dbo::collection copyrights{ utils::execMultiResultQuery(session()->query ("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?") .groupBy("copyright") - .bind(getId()); + .bind(getId())) }; std::vector values(copyrights.begin(), copyrights.end()); @@ -454,17 +442,17 @@ namespace lms::db if (values.empty() || values.size() > 1 || values.front().empty()) return std::nullopt; - return values.front(); + return std::move(values.front()); } std::optional Release::getCopyrightURL() const { assert(session()); - Wt::Dbo::collection copyrights = session()->query + Wt::Dbo::collection copyrights {utils::execMultiResultQuery(session()->query ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?").bind(getId()) - .groupBy("copyright_url"); + .groupBy("copyright_url")) }; std::vector values(copyrights.begin(), copyrights.end()); @@ -472,31 +460,29 @@ namespace lms::db if (values.empty() || values.size() > 1 || values.front().empty()) return std::nullopt; - return values.front(); + return std::move(values.front()); } std::size_t Release::getMeanBitrate() const { assert(session()); - return session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") + return utils::execSingleResultQuery(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") .where("release_id = ?").bind(getId()) - .where("bitrate > 0") - .resultValue(); + .where("bitrate > 0")); } std::vector Release::getArtists(TrackArtistLinkType linkType) const { assert(session()); - auto res{ session()->query>( + auto res{ utils::execMultiResultQuery(session()->query>( "SELECT DISTINCT a FROM artist a" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?").bind(getId()) - .where("t_a_l.type = ?").bind(linkType) - .resultList() }; + .where("t_a_l.type = ?").bind(linkType))}; return std::vector(res.begin(), res.end()); } @@ -506,7 +492,7 @@ namespace lms::db assert(session()); // Select the similar releases using the 5 most used clusters of the release - auto res{ session()->query>( + auto res{ utils::execMultiResultQuery(session()->query>( "SELECT r FROM release r" " INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" @@ -524,8 +510,7 @@ namespace lms::db .groupBy("r.id") .orderBy("COUNT(*) DESC, RANDOM()") .limit(count ? static_cast(*count) : -1) - .offset(offset ? static_cast(*offset) : -1) - .resultList() }; + .offset(offset ? static_cast(*offset) : -1))}; return std::vector(res.begin(), res.end()); } @@ -572,20 +557,16 @@ namespace lms::db using milli = std::chrono::duration; - Wt::Dbo::Query query{ session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") - .where("r.id = ?").bind(getId()) }; - - return query.resultValue(); + return utils::execSingleResultQuery(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") + .where("r.id = ?").bind(getId())); } Wt::WDateTime Release::getLastWritten() const { assert(session()); - Wt::Dbo::Query query{ session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") - .where("r.id = ?").bind(getId()) }; - - return query.resultValue(); + return utils::execSingleResultQuery(session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") + .where("r.id = ?").bind(getId())); } std::vector> Release::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const @@ -612,7 +593,7 @@ namespace lms::db for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - auto queryRes{ query.resultList() }; + auto queryRes{ utils::execMultiResultQuery(query) }; std::map> clustersByType; for (const Wt::Dbo::ptr& cluster : queryRes) diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp index 6daa1496..e76c24cb 100644 --- a/src/libs/database/impl/ScanSettings.cpp +++ b/src/libs/database/impl/ScanSettings.cpp @@ -24,6 +24,7 @@ #include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "core/String.hpp" +#include "Utils.hpp" namespace lms::db { @@ -34,14 +35,14 @@ namespace lms::db if (pointer settings{ get(session) }) return; - session.getDboSession().add(std::make_unique()); + session.getDboSession()->add(std::make_unique()); } ScanSettings::pointer ScanSettings::get(Session& session) { session.checkReadTransaction(); - return session.getDboSession().find().resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find()); } std::vector ScanSettings::getAudioFileExtensions() const diff --git a/src/libs/database/impl/StarredArtist.cpp b/src/libs/database/impl/StarredArtist.cpp index 56c48d11..f8478be5 100644 --- a/src/libs/database/impl/StarredArtist.cpp +++ b/src/libs/database/impl/StarredArtist.cpp @@ -38,40 +38,38 @@ namespace lms::db StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, FeedbackBackend backend) { - return session.getDboSession().add(std::unique_ptr {new StarredArtist{ artist, user, backend }}); + return session.getDboSession()->add(std::unique_ptr {new StarredArtist{ artist, user, backend }}); } std::size_t StarredArtist::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM starred_artist"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM starred_artist")); } StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId) { session.checkReadTransaction(); - return session.getDboSession().query>("SELECT s_a from starred_artist s_a") + return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT s_a from starred_artist s_a") .join("user u ON u.id = s_a.user_id") .where("s_a.artist_id = ?").bind(artistId) .where("s_a.user_id = ?").bind(userId) - .where("s_a.backend = u.feedback_backend") - .resultValue(); + .where("s_a.backend = u.feedback_backend")); } StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend) { session.checkReadTransaction(); - return session.getDboSession().find() + return utils::execSingleResultQuery(session.getDboSession()->find() .where("artist_id = ?").bind(artistId) .where("user_id = ?").bind(userId) - .where("backend = ?").bind(backend) - .resultValue(); + .where("backend = ?").bind(backend)); } void StarredArtist::setDateTime(const Wt::WDateTime& dateTime) diff --git a/src/libs/database/impl/StarredRelease.cpp b/src/libs/database/impl/StarredRelease.cpp index 1e01d4aa..944946e2 100644 --- a/src/libs/database/impl/StarredRelease.cpp +++ b/src/libs/database/impl/StarredRelease.cpp @@ -38,40 +38,38 @@ namespace lms::db StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, FeedbackBackend backend) { - return session.getDboSession().add(std::unique_ptr{new StarredRelease{ release, user, backend }}); + return session.getDboSession()->add(std::unique_ptr{new StarredRelease{ release, user, backend }}); } std::size_t StarredRelease::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM starred_release"); + return session.getDboSession()->query("SELECT COUNT(*) FROM starred_release"); } StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId) { session.checkReadTransaction(); - return session.getDboSession().query>("SELECT s_r from starred_release s_r") + return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT s_r from starred_release s_r") .join("user u ON u.id = s_r.user_id") .where("s_r.release_id = ?").bind(releaseId) .where("s_r.user_id = ?").bind(userId) - .where("s_r.backend = u.feedback_backend") - .resultValue(); + .where("s_r.backend = u.feedback_backend")); } StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend) { session.checkReadTransaction(); - return session.getDboSession().find() + return utils::execSingleResultQuery(session.getDboSession()->find() .where("release_id = ?").bind(releaseId) .where("user_id = ?").bind(userId) - .where("backend = ?").bind(backend) - .resultValue(); + .where("backend = ?").bind(backend)); } void StarredRelease::setDateTime(const Wt::WDateTime& dateTime) diff --git a/src/libs/database/impl/StarredTrack.cpp b/src/libs/database/impl/StarredTrack.cpp index e3061a1a..3a4049de 100644 --- a/src/libs/database/impl/StarredTrack.cpp +++ b/src/libs/database/impl/StarredTrack.cpp @@ -38,56 +38,53 @@ namespace lms::db StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, FeedbackBackend backend) { - return session.getDboSession().add(std::unique_ptr {new StarredTrack{ track, user, backend }}); + return session.getDboSession()->add(std::unique_ptr {new StarredTrack{ track, user, backend }}); } std::size_t StarredTrack::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM starred_track"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM starred_track")); } StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId) { session.checkReadTransaction(); - return session.getDboSession().query>("SELECT s_t from starred_track s_t") + return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT s_t from starred_track s_t") .join("user u ON u.id = s_t.user_id") .where("s_t.track_id = ?").bind(trackId) .where("s_t.user_id = ?").bind(userId) - .where("s_t.backend = u.feedback_backend") - .resultValue(); + .where("s_t.backend = u.feedback_backend")); } StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) { session.checkReadTransaction(); - return session.getDboSession().find() + return utils::execSingleResultQuery(session.getDboSession()->find() .where("track_id = ?").bind(trackId) .where("user_id = ?").bind(userId) - .where("backend = ?").bind(backend) - .resultValue(); + .where("backend = ?").bind(backend)); } bool StarredTrack::exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) { - return session.getDboSession().query("SELECT 1 from starred_track") + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 from starred_track") .where("track_id = ?").bind(trackId) .where("user_id = ?").bind(userId) - .where("backend = ?").bind(backend) - .resultValue() == 1; + .where("backend = ?").bind(backend)); } RangeResults StarredTrack::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT DISTINCT s_t.id FROM starred_track s_t") }; + auto query{ session.getDboSession()->query("SELECT DISTINCT s_t.id FROM starred_track s_t") }; if (params.backend) query.where("s_t.backend = ?").bind(*params.backend); @@ -96,7 +93,7 @@ namespace lms::db if (params.user.isValid()) query.where("s_t.user_id = ?").bind(params.user); - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 4ff6ed61..68266d89 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -45,7 +45,7 @@ namespace lms::db { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT " + std::string{ itemToSelect } + " FROM track t") }; + auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " FROM track t") }; assert(params.keywords.empty() || params.name.empty()); for (std::string_view keyword : params.keywords) @@ -202,37 +202,36 @@ namespace lms::db Track::pointer Track::create(Session& session, const std::filesystem::path& p) { - return session.getDboSession().add(std::unique_ptr {new Track{ p }}); + return session.getDboSession()->add(std::unique_ptr {new Track{ p }}); } std::size_t Track::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM track"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM track")); } Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p) { session.checkReadTransaction(); - return session.getDboSession().find().where("file_path = ?").bind(p.string()).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("file_path = ?").bind(p.string())); } Track::pointer Track::find(Session& session, TrackId id) { session.checkReadTransaction(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("id = ?").bind(id)); } void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function& func, MediaLibraryId library) { session.checkReadTransaction(); - auto query{ session.getDboSession().find() + auto query{ session.getDboSession()->find() .orderBy("id") .where("id > ?").bind(lastRetrievedTrack) .limit(static_cast(count)) }; @@ -240,7 +239,7 @@ namespace lms::db if (library.isValid()) query.where("media_library_id = ?").bind(library); - auto collection{query.resultList()}; + auto collection{ utils::execMultiResultQuery(query) }; for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { @@ -253,16 +252,15 @@ namespace lms::db { session.checkReadTransaction(); - return session.getDboSession().query("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1; + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 from track").where("id = ?").bind(id)) == 1; } std::vector Track::findByMBID(Session& session, const core::UUID& mbid) { session.checkReadTransaction(); - auto res{ session.getDboSession().find() - .where("mbid = ?").bind(std::string {mbid.getAsString()}) - .resultList() }; + auto res{ utils::execMultiResultQuery(session.getDboSession()->find() + .where("mbid = ?").bind(std::string {mbid.getAsString()})) }; return std::vector(res.begin(), res.end()); } @@ -271,9 +269,8 @@ namespace lms::db { session.checkReadTransaction(); - auto res{ session.getDboSession().find() - .where("recording_mbid = ?").bind(std::string {mbid.getAsString()}) - .resultList() }; + auto res{ utils::execMultiResultQuery(session.getDboSession()->find() + .where("recording_mbid = ?").bind(std::string {mbid.getAsString()})) }; return std::vector(res.begin(), res.end()); } @@ -284,9 +281,9 @@ namespace lms::db session.checkReadTransaction(); // TODO Dbo traits on filesystem - auto query{ session.getDboSession().query("SELECT id, file_path FROM track") }; + auto query{ session.getDboSession()->query("SELECT id, file_path FROM track") }; - RangeResults queryResults{ utils::execQuery(query, range) }; + RangeResults queryResults{ utils::execRangeQuery(query, range) }; RangeResults res; res.range = queryResults.range; @@ -306,21 +303,21 @@ namespace lms::db { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") + auto query{ session.getDboSession()->query("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } RangeResults Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT t.id FROM track t") + auto query{ session.getDboSession()->query("SELECT t.id FROM track t") .where("LENGTH(t.recording_mbid) > 0") .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } std::vector Track::getClusters() const @@ -332,10 +329,9 @@ namespace lms::db { assert(session()); - auto res{ session()->query + auto res{ utils::execMultiResultQuery(session()->query ("SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") - .where("t.id = ?").bind(getId()) - .resultList() }; + .where("t.id = ?").bind(getId())) }; return std::vector(res.begin(), res.end()); } @@ -345,7 +341,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery(session, parameters) }; - return utils::execQuery(query, parameters.range); + return utils::execRangeQuery(query, parameters.range); } RangeResults Track::find(Session& session, const FindParameters& parameters) @@ -353,7 +349,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, parameters) }; - return utils::execQuery(query, parameters.range); + return utils::execRangeQuery(query, parameters.range); } void Track::find(Session& session, const FindParameters& params, std::function func) @@ -361,7 +357,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, params)}; - utils::execQuery(query, params.range, func); + utils::execRangeQuery(query, params.range, func); } void Track::find(Session& session, const FindParameters& params, bool& moreResults, std::function func) @@ -369,7 +365,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, params)}; - utils::execQuery(query, params.range, moreResults, func); + utils::execRangeQuery(query, params.range, moreResults, func); } RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, std::optional range) @@ -385,7 +381,7 @@ namespace lms::db oss << "?"; } - auto query{ session.getDboSession().query( + auto query{ session.getDboSession()->query( "SELECT t.id FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " AND t_c.cluster_id IN (SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))" @@ -399,7 +395,7 @@ namespace lms::db for (TrackId trackId : tracks) query.bind(trackId); - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } void Track::clearArtistLinks() @@ -460,7 +456,7 @@ namespace lms::db query.where("t.id = ?").bind(getId()); - auto res{ query.resultList() }; + auto res{ utils::execMultiResultQuery(query) }; return std::vector(std::begin(res), std::end(res)); } @@ -496,7 +492,7 @@ namespace lms::db query.where("t.id = ?").bind(getId()); - auto res{ query.resultList() }; + auto res{ utils::execMultiResultQuery(query) }; return std::vector(std::cbegin(res), std::cend(res)); } @@ -530,7 +526,7 @@ namespace lms::db for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - auto queryRes{ query.resultList() }; + auto queryRes{ utils::execMultiResultQuery(query) }; std::map> clusters; for (const Wt::Dbo::ptr& cluster : queryRes) diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 6a55dd82..38059dda 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -34,7 +34,7 @@ namespace lms::db { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") }; + auto query{ session.getDboSession()->query("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") }; if (params.linkType) query.where("t_a_l.type = ?").bind(*params.linkType); @@ -67,8 +67,8 @@ namespace lms::db { session.checkWriteTransaction(); - TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique(track, artist, type, subType)) }; - session.getDboSession().flush(); + TrackArtistLink::pointer res{ session.getDboSession()->add(std::make_unique(track, artist, type, subType)) }; + session.getDboSession()->flush(); return res; } @@ -76,7 +76,7 @@ namespace lms::db TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } RangeResults TrackArtistLink::find(Session& session, const FindParameters& params) @@ -84,14 +84,14 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery(session, params) }; - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } core::EnumSet TrackArtistLink::findUsedTypes(Session& session) { session.checkReadTransaction(); - auto res{ session.getDboSession().query("SELECT DISTINCT type from track_artist_link").resultList() }; + auto res{ utils::execMultiResultQuery(session.getDboSession()->query("SELECT DISTINCT type from track_artist_link")) }; return core::EnumSet(std::begin(res), std::end(res)); } @@ -100,10 +100,8 @@ namespace lms::db { session.checkReadTransaction(); - auto res{ session.getDboSession() - .query("SELECT DISTINCT type from track_artist_link") - .where("artist_id = ?").bind(artistId) - .resultList() }; + auto res{ utils::execMultiResultQuery(session.getDboSession()->query("SELECT DISTINCT type from track_artist_link") + .where("artist_id = ?").bind(artistId)) }; return core::EnumSet(std::begin(res), std::end(res)); } diff --git a/src/libs/database/impl/TrackBookmark.cpp b/src/libs/database/impl/TrackBookmark.cpp index b14ad45d..d5033ef0 100644 --- a/src/libs/database/impl/TrackBookmark.cpp +++ b/src/libs/database/impl/TrackBookmark.cpp @@ -35,43 +35,41 @@ namespace lms::db TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) { - return session.getDboSession().add(std::unique_ptr {new TrackBookmark{ user, track }}); + return session.getDboSession()->add(std::unique_ptr {new TrackBookmark{ user, track }}); } std::size_t TrackBookmark::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM track_bookmark")); } RangeResults TrackBookmark::find(Session& session, UserId userId, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT id from track_bookmark") + auto query{ session.getDboSession()->query("SELECT id from track_bookmark") .where("user_id = ?").bind(userId) }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId) { session.checkReadTransaction(); - return session.getDboSession().find() + return utils::execSingleResultQuery(session.getDboSession()->find() .where("user_id = ?").bind(userId) - .where("track_id = ?").bind(trackId) - .resultValue(); + .where("track_id = ?").bind(trackId)); } TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id) { session.checkReadTransaction(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("id = ?").bind(id)); } } // namespace lms::db diff --git a/src/libs/database/impl/TrackFeatures.cpp b/src/libs/database/impl/TrackFeatures.cpp index fde099cb..5867114a 100644 --- a/src/libs/database/impl/TrackFeatures.cpp +++ b/src/libs/database/impl/TrackFeatures.cpp @@ -38,41 +38,39 @@ namespace lms::db { TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) { - return session.getDboSession().add(std::unique_ptr {new TrackFeatures{ track, jsonEncodedFeatures }}); + return session.getDboSession()->add(std::unique_ptr {new TrackFeatures{ track, jsonEncodedFeatures }}); } std::size_t TrackFeatures::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM track_features")); } TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id) { session.checkReadTransaction(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("id = ?").bind(id)); } TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId) { session.checkReadTransaction(); - return session.getDboSession().find() - .where("track_id = ?").bind(trackId) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("track_id = ?").bind(trackId)); } RangeResults TrackFeatures::find(Session& session, std::optional range) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT id from track_features") }; + auto query{ session.getDboSession()->query("SELECT id from track_features") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 65820591..d83e0af7 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -48,14 +48,14 @@ namespace lms::db TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) { - return session.getDboSession().add(std::unique_ptr {new TrackList{ name, type, isPublic, user }}); + return session.getDboSession()->add(std::unique_ptr {new TrackList{ name, type, isPublic, user }}); } std::size_t TrackList::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM tracklist")); } @@ -64,17 +64,17 @@ namespace lms::db session.checkReadTransaction(); assert(userId.isValid()); - return session.getDboSession().find() + return utils::execSingleResultQuery(session.getDboSession()->find() .where("name = ?").bind(name) .where("type = ?").bind(type) - .where("user_id = ?").bind(userId).resultValue(); + .where("user_id = ?").bind(userId)); } RangeResults TrackList::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT DISTINCT t_l.id FROM tracklist t_l") }; + auto query{ session.getDboSession()->query("SELECT DISTINCT t_l.id FROM tracklist t_l") }; if (params.user.isValid()) query.where("t_l.user_id = ?").bind(params.user); @@ -117,14 +117,14 @@ namespace lms::db break; } - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } TrackList::pointer TrackList::find(Session& session, TrackListId id) { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } bool TrackList::isEmpty() const @@ -156,29 +156,27 @@ namespace lms::db .where("tracklist_id = ?").bind(getId()) .orderBy("id") }; - return utils::execQuery(query, range); + return utils::execRangeQuery(query, range); } TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const { assert(session()); - return session()->find() + return utils::execSingleResultQuery(session()->find() .where("tracklist_id = ?").bind(getId()) .where("track_id = ?").bind(track->getId()) - .where("date_time = ?").bind(utils::normalizeDateTime(dateTime)) - .resultValue(); + .where("date_time = ?").bind(utils::normalizeDateTime(dateTime))); } std::vector TrackList::getClusters() const { assert(session()); - auto res{ session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") + auto res{ utils::execMultiResultQuery(session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") .where("p.id = ?").bind(getId()) .groupBy("c.id") - .orderBy("COUNT(c.id) DESC") - .resultList() }; + .orderBy("COUNT(c.id) DESC")) }; return std::vector(res.begin(), res.end()); } @@ -218,7 +216,7 @@ namespace lms::db query.groupBy("c.id"); query.orderBy("COUNT(c.id) DESC"); - auto queryRes{ query.resultList() }; + auto queryRes{ utils::execMultiResultQuery(query) }; std::map> clustersByType; for (const Wt::Dbo::ptr& cluster : queryRes) @@ -237,7 +235,7 @@ namespace lms::db { assert(session()); - auto res{ session()->query>( + auto res{ utils::execMultiResultQuery(session()->query>( "SELECT t FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " WHERE " @@ -249,8 +247,7 @@ namespace lms::db .groupBy("t.id") .orderBy("COUNT(*) DESC, RANDOM()") .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1) - .resultList() }; + .offset(offset ? static_cast(*offset) : -1))}; return std::vector(res.begin(), res.end()); } @@ -259,8 +256,8 @@ namespace lms::db { assert(session()); - Wt::Dbo::collection res = session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") - .where("p.id = ?").bind(getId()); + auto res {utils::execMultiResultQuery(session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") + .where("p.id = ?").bind(getId())) }; return std::vector(res.begin(), res.end()); } @@ -271,10 +268,8 @@ namespace lms::db using milli = std::chrono::duration; - Wt::Dbo::Query query{ session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") - .where("p_e.tracklist_id = ?").bind(getId()) }; - - return query.resultValue(); + return utils::execSingleResultQuery(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") + .where("p_e.tracklist_id = ?").bind(getId())); } void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime) @@ -293,7 +288,7 @@ namespace lms::db TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) { - return session.getDboSession().add(std::unique_ptr {new TrackListEntry{ track, tracklist, dateTime }}); + return session.getDboSession()->add(std::unique_ptr {new TrackListEntry{ track, tracklist, dateTime }}); } void TrackListEntry::onPostCreated() @@ -310,6 +305,6 @@ namespace lms::db { session.checkReadTransaction(); - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } } // namespace lms::db diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index 1960a209..e34253b2 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -28,8 +28,8 @@ #include "StringViewTraits.hpp" #include "Utils.hpp" -namespace lms::db { - +namespace lms::db +{ User::User(std::string_view loginName) : _loginName{ loginName } { @@ -37,47 +37,46 @@ namespace lms::db { User::pointer User::create(Session& session, std::string_view loginName) { - return session.getDboSession().add(std::unique_ptr {new User{ loginName }}); + return session.getDboSession()->add(std::unique_ptr {new User{ loginName }}); } std::size_t User::getCount(Session& session) { session.checkReadTransaction(); - return session.getDboSession().query("SELECT COUNT(*) FROM user"); + return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM user")); } RangeResults User::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); - auto query{ session.getDboSession().query("SELECT id FROM user") }; + auto query{ session.getDboSession()->query("SELECT id FROM user") }; if (params.scrobblingBackend) query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend); if (params.feedbackBackend) query.where("feedback_backend = ?").bind(*params.feedbackBackend); - return utils::execQuery(query, params.range); + return utils::execRangeQuery(query, params.range); } User::pointer User::findDemoUser(Session& session) { session.checkReadTransaction(); - return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("type = ?").bind(UserType::DEMO)); } User::pointer User::find(Session& session, UserId id) { - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } User::pointer User::find(Session& session, std::string_view name) { - return session.getDboSession().find() - .where("login_name = ?").bind(name) - .resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find() + .where("login_name = ?").bind(name)); } void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate) diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 8610104e..9604884f 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -45,11 +45,23 @@ namespace lms::db::utils } } - template - RangeResults execQuery(Query& query, std::optional range) + template + auto execSingleResultQuery(const Query& query) { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRange"); + LMS_SCOPED_TRACE_DETAILED("Database", "ExecSingleResultQuery"); + return query.resultValue(); + } + template + auto execMultiResultQuery(const Query& query) + { + LMS_SCOPED_TRACE_DETAILED("Database", "ExecMultiResultQuery"); + return query.resultList(); + } + + template + RangeResults execRangeQuery(Query& query, std::optional range) + { RangeResults res; if (range) @@ -60,7 +72,7 @@ namespace lms::db::utils res.results.reserve(range->size); } - auto collection{ query.resultList() }; + auto collection{ execMultiResultQuery(query) }; for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { if (range && res.results.size() == range->size) @@ -78,20 +90,21 @@ namespace lms::db::utils } template - void execQuery(Query& query, std::optional range, std::function func) + void execRangeQuery(Query& query, std::optional range, const std::function& func) { if (range) applyRange(query, range); - for (const auto& res : query.resultList()) + auto collection{ execMultiResultQuery(query) }; + for (const auto& res : collection) { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryResult"); + LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); func(res); } } template - void execQuery(Query& query, std::optional range, bool& moreResults, std::function func) + void execRangeQuery(Query& query, std::optional range, bool& moreResults, const std::function& func) { if (range) applyRange(query, Range{ range->offset, range->size + 1 }); @@ -99,7 +112,8 @@ namespace lms::db::utils moreResults = false; std::size_t count{}; - for (const auto& res : query.resultList()) + auto collection{ execMultiResultQuery(query) }; + for (const auto& res : collection) { if (range && (count++ == static_cast(range->size))) { @@ -107,7 +121,7 @@ namespace lms::db::utils break; } - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryResult"); + LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); func(res); } } diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index 544ff975..b9f48f0e 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -41,8 +41,8 @@ namespace lms::db WriteTransaction(const WriteTransaction&) = delete; WriteTransaction& operator=(const WriteTransaction&) = delete; - std::unique_lock _lock; - core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "WriteTransaction" }; // before actual transaction + const std::unique_lock _lock; + const core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "WriteTransaction" }; // before actual transaction Wt::Dbo::Transaction _transaction; }; @@ -58,7 +58,7 @@ namespace lms::db ReadTransaction(const ReadTransaction&) = delete; ReadTransaction& operator=(const ReadTransaction&) = delete; - core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "ReadTransaction" }; // before actual transaction + const core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "ReadTransaction" }; // before actual transaction Wt::Dbo::Transaction _transaction; }; @@ -89,7 +89,8 @@ namespace lms::db void prepareTables(); // need to run only once at startup - Wt::Dbo::Session& getDboSession() { return _session; } + // returning a ptr here to ease further wrapping using operator-> + Wt::Dbo::Session* getDboSession() { return &_session; } Db& getDb() { return _db; } template @@ -98,7 +99,7 @@ namespace lms::db checkWriteTransaction(); typename Object::pointer res{ Object::create(*this, std::forward(args)...) }; - getDboSession().flush(); + getDboSession()->flush(); if (res->hasOnPostCreated()) res.modify()->onPostCreated(); From 1703e3eaf94fc5b019ac1e1a46fffe35ec2d6ef2 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 31 Mar 2024 13:13:24 +0200 Subject: [PATCH 17/64] Added missing exec wrappers for queries --- src/libs/database/impl/Cluster.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 76da0d42..ea67b5a2 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -66,7 +66,7 @@ namespace lms::db Wt::Dbo::Query createQuery(Session& session, const Cluster::FindParameters& params) { std::string_view itemToSelect; - + if constexpr (std::is_same_v) itemToSelect = "c.id"; else if constexpr (std::is_same_v>) @@ -96,7 +96,7 @@ namespace lms::db return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM cluster")); } - RangeResults Cluster::findIds(Session& session, const FindParameters& params) + RangeResults Cluster::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; @@ -124,7 +124,7 @@ namespace lms::db { session.checkReadTransaction(); - return session.getDboSession()->find().where("id = ?").bind(id).resultValue(); + return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); } std::size_t Cluster::computeTrackCount(Session& session, ClusterId id) @@ -236,9 +236,9 @@ namespace lms::db assert(self()); assert(session()); - auto res {utils::execMultiResultQuery(session()->find() + auto res{ utils::execMultiResultQuery(session()->find() .where("cluster_type_id = ?").bind(getId()) - .orderBy("name"))}; + .orderBy("name")) }; return std::vector(res.begin(), res.end()); } From 7163b38e7004a3fabe2ad41cca3ce151cf53c5cf Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 31 Mar 2024 15:05:20 +0200 Subject: [PATCH 18/64] Fixed debug buil --- src/libs/database/impl/TransactionChecker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/database/impl/TransactionChecker.cpp b/src/libs/database/impl/TransactionChecker.cpp index 9ab9c98f..9966345d 100644 --- a/src/libs/database/impl/TransactionChecker.cpp +++ b/src/libs/database/impl/TransactionChecker.cpp @@ -80,7 +80,7 @@ namespace lms::db void TransactionChecker::checkWriteTransaction(Session& session) { - checkWriteTransaction(session.getDboSession()); + checkWriteTransaction(*session.getDboSession()); } void TransactionChecker::checkReadTransaction(Wt::Dbo::Session& session) @@ -91,6 +91,6 @@ namespace lms::db void TransactionChecker::checkReadTransaction(Session& session) { - checkReadTransaction(session.getDboSession()); + checkReadTransaction(*session.getDboSession()); } } \ No newline at end of file From a3c53e160969a17e2f9a33700115406eeedb1585 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 31 Mar 2024 23:35:12 +0200 Subject: [PATCH 19/64] Tracing: added some details for db operations --- src/libs/core/bench/TraceLoggerBench.cpp | 26 ++++++++ src/libs/core/impl/TraceLogger.cpp | 67 +++++++++++++++++--- src/libs/core/impl/TraceLogger.hpp | 21 ++++-- src/libs/core/include/core/ITraceLogger.hpp | 25 ++++++-- src/libs/core/include/core/LiteralString.hpp | 1 + src/libs/core/test/TraceLogger.cpp | 11 +++- src/libs/database/impl/Artist.cpp | 1 + src/libs/database/impl/Release.cpp | 2 +- src/libs/database/impl/Track.cpp | 1 + src/libs/database/impl/Utils.hpp | 4 +- 10 files changed, 134 insertions(+), 25 deletions(-) diff --git a/src/libs/core/bench/TraceLoggerBench.cpp b/src/libs/core/bench/TraceLoggerBench.cpp index 7c037c0e..34652885 100644 --- a/src/libs/core/bench/TraceLoggerBench.cpp +++ b/src/libs/core/bench/TraceLoggerBench.cpp @@ -39,6 +39,14 @@ namespace lms::core } } + static void BM_TraceLogger_Overview_withArg(benchmark::State& state) + { + for (auto _ : state) + { + LMS_SCOPED_TRACE_OVERVIEW_WITH_ARG("Cat", "Test", "ArgType", "My arg that can be very very long, and even as long as needed"); + } + } + static void BM_TraceLogger_Detailed(benchmark::State& state) { for (auto _ : state) @@ -48,8 +56,26 @@ namespace lms::core } } + static void BM_TraceLogger_Detailed_withArg(benchmark::State& state) + { + auto someExpensiveArgComputation{ []() -> std::string + { + std::this_thread::sleep_for(std::chrono::microseconds{ 1 }); + return "foo"; + } }; + + for (auto _ : state) + { + // Should do nothing (and cost nothing) + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Cat", "Test", "ArgType", someExpensiveArgComputation()); + } + } + BENCHMARK(BM_TraceLogger_Overview)->Threads(1)->Threads(std::thread::hardware_concurrency()); + BENCHMARK(BM_TraceLogger_Overview_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency()); BENCHMARK(BM_TraceLogger_Detailed)->Threads(1)->Threads(std::thread::hardware_concurrency()); + BENCHMARK(BM_TraceLogger_Detailed_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency()); + } BENCHMARK_MAIN(); \ No newline at end of file diff --git a/src/libs/core/impl/TraceLogger.cpp b/src/libs/core/impl/TraceLogger.cpp index 614713ae..ca56c82b 100644 --- a/src/libs/core/impl/TraceLogger.cpp +++ b/src/libs/core/impl/TraceLogger.cpp @@ -20,6 +20,7 @@ #include "TraceLogger.hpp" #include +#include #include #include #include "core/Exception.hpp" @@ -68,7 +69,7 @@ namespace lms::core::tracing for (Buffer& buffer : _buffers) _freeBuffers.push_back(&buffer); - LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize)); + LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize) << ", entry size = " << sizeof(CompleteEvent) << ", entry count per buffer = " << Buffer::CompleteEventCount); } bool TraceLogger::isLevelActive(Level level) const @@ -110,6 +111,7 @@ namespace lms::core::tracing // Empty new buffer only now (we want to keep history on released buffers since we dump them) buffer->currentDurationIndex = 0; + buffer->threadId = std::this_thread::get_id(); return buffer; } @@ -154,6 +156,8 @@ namespace lms::core::tracing for (Buffer& buffer : _buffers) { + const auto threadId{ toTraceThreadId(buffer.threadId) }; + for (std::size_t i{}; i < buffer.currentDurationIndex; ++i) { // Looks like tracing viewer is not pleased when nested event start at the same timestamp @@ -171,10 +175,23 @@ namespace lms::core::tracing os << "\"name\" : \"" << event.name.c_str() << "\", "; os << "\"cat\" : \"" << event.category.c_str() << "\", "; os << "\"pid\": 1, "; - os << "\"tid\" : " << toTraceThreadId(event.threadId) << ", "; + os << "\"tid\" : " << threadId << ", "; os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.start - _start).count() << ", "; os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.duration).count() << ", "; os << "\"ph\" : \"X\""; + if (event.arg.has_value()) + { + ArgEntryMap::const_iterator itArgEntry; + { + std::shared_lock lock{ _argMutex }; + itArgEntry = _argEntries.find(*event.arg); + assert(itArgEntry != _argEntries.cend()); + } + + os << ", \"args\" : { \"" << itArgEntry->second.type.c_str() << "\" : \""; + stringUtils::writeJSEscapedString(os, std::string_view{ itArgEntry->second.value }); + os << "\" }"; + } os << " }"; } } @@ -199,14 +216,48 @@ namespace lms::core::tracing _threadNames.emplace(id, threadName); } - std::uint32_t TraceLogger::toTraceThreadId(std::thread::id threadId) const + TraceLogger::ArgHashType TraceLogger::computeArgHash(LiteralString type, std::string_view value) { + ArgHashType res{}; + res ^= std::hash{}(type.str()); + res ^= std::hash{}(value); + return res; + } + + TraceLogger::ArgHashType TraceLogger::registerArg(LiteralString argType, std::string_view argValue) + { + const ArgHashType hash{ computeArgHash(argType, argValue) }; + { - auto it{ _cachedTraceThreadIds.find(threadId) }; - if (it != std::cend(_cachedTraceThreadIds)) - return it->second; + const std::shared_lock lock{ _argMutex }; + + auto itArgEntry{ _argEntries.find(hash) }; + if (itArgEntry != std::cend(_argEntries)) + { + assert(itArgEntry->second.type == argType); + assert(itArgEntry->second.value == argValue); + return hash; + } } + { + const std::unique_lock lock{ _argMutex }; + + auto itArgEntry{ _argEntries.find(hash) }; + if (itArgEntry != std::cend(_argEntries)) + { + assert(itArgEntry->second.type == argType); + assert(itArgEntry->second.value == argValue); + return hash; + } + + _argEntries.emplace(hash, ArgEntry{ argType, std::string{ argValue } }); + return hash; + } + } + + std::uint32_t TraceLogger::toTraceThreadId(std::thread::id threadId) + { // Pefetto UI does not accept 64bits thread ids std::ostringstream oss; oss << threadId; @@ -215,8 +266,6 @@ namespace lms::core::tracing std::uint64_t id; iss >> id; - const std::uint32_t res{ static_cast(id) }; - _cachedTraceThreadIds.emplace(threadId, res); - return res; + return static_cast(id); } } \ No newline at end of file diff --git a/src/libs/core/impl/TraceLogger.hpp b/src/libs/core/impl/TraceLogger.hpp index 17e0cea7..ee81090b 100644 --- a/src/libs/core/impl/TraceLogger.hpp +++ b/src/libs/core/impl/TraceLogger.hpp @@ -22,9 +22,10 @@ #include #include #include -#include +#include #include #include +#include #include "core/ITraceLogger.hpp" @@ -42,14 +43,18 @@ namespace lms::core::tracing void write(const CompleteEvent& event) override; void dumpCurrentBuffer(std::ostream& os) override; void setThreadName(std::thread::id id, std::string_view threadName) override; - std::uint32_t toTraceThreadId(std::thread::id threadId) const; + ArgHashType registerArg(LiteralString argType, std::string_view argValue) override; - static constexpr std::size_t BufferSize{ 32 * 1024 }; + static ArgHashType computeArgHash(LiteralString type, std::string_view value); + static std::uint32_t toTraceThreadId(std::thread::id threadId); + + static constexpr std::size_t BufferSize{ 64 * 1024 }; struct alignas(64) Buffer { static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEvent) }; + std::thread::id threadId; std::array durationEvents; std::atomic currentDurationIndex{}; }; @@ -63,9 +68,17 @@ namespace lms::core::tracing std::vector _buffers; // allocated once during construction + std::shared_mutex _argMutex; + struct ArgEntry + { + LiteralString type; + std::string value; + }; + using ArgEntryMap = std::unordered_map; + ArgEntryMap _argEntries; // collisions not handled + std::mutex _threadNameMutex; std::unordered_map _threadNames; - mutable std::unordered_map _cachedTraceThreadIds; std::mutex _mutex; std::deque _freeBuffers; diff --git a/src/libs/core/include/core/ITraceLogger.hpp b/src/libs/core/include/core/ITraceLogger.hpp index 89479dba..dcf87a80 100644 --- a/src/libs/core/include/core/ITraceLogger.hpp +++ b/src/libs/core/include/core/ITraceLogger.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -34,14 +35,19 @@ #define LMS_CONCAT(x, y) LMS_CONCAT_IMPL(x, y) #if LMS_SUPPORT_TRACING -#define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME) ::lms::core::tracing::ScopedTrace LMS_CONCAT(ScopedTrace_, __LINE__){ CATEGORY, LEVEL, NAME } - +#define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME, ARGTYPE, ARGVALUE) \ +std::optional<::lms::core::tracing::ScopedTrace> LMS_CONCAT(ScopedTrace_, __LINE__); \ +if (::lms::core::tracing::ITraceLogger* traceLogger{ ::lms::core::Service<::lms::core::tracing::ITraceLogger>::get() }; traceLogger && traceLogger->isLevelActive(LEVEL)) \ + LMS_CONCAT(ScopedTrace_, __LINE__).emplace(CATEGORY, LEVEL, NAME, ARGTYPE, ARGVALUE, traceLogger); #else #define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME) (void)0 #endif -#define LMS_SCOPED_TRACE_OVERVIEW(CATEGORY, NAME) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Overview, NAME) -#define LMS_SCOPED_TRACE_DETAILED(CATEGORY, NAME) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Detailed, NAME) +#define LMS_SCOPED_TRACE_OVERVIEW_WITH_ARG(CATEGORY, NAME, ARGTYPE, ARGVALUE) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Overview, NAME, ARGTYPE, ARGVALUE) +#define LMS_SCOPED_TRACE_DETAILED_WITH_ARG(CATEGORY, NAME, ARGTYPE, ARGVALUE) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Detailed, NAME, ARGTYPE, ARGVALUE) + +#define LMS_SCOPED_TRACE_OVERVIEW(CATEGORY, NAME) LMS_SCOPED_TRACE_OVERVIEW_WITH_ARG(CATEGORY, NAME, "", "") +#define LMS_SCOPED_TRACE_DETAILED(CATEGORY, NAME) LMS_SCOPED_TRACE_DETAILED_WITH_ARG(CATEGORY, NAME, "", "") namespace lms::core::tracing { @@ -56,13 +62,15 @@ namespace lms::core::tracing class ITraceLogger { public: + using ArgHashType = std::size_t; + struct CompleteEvent { clock::time_point start; clock::duration duration; - std::thread::id threadId; LiteralString name; LiteralString category; + std::optional arg; }; virtual ~ITraceLogger() = default; @@ -71,6 +79,8 @@ namespace lms::core::tracing virtual void write(const CompleteEvent& entry) = 0; virtual void dumpCurrentBuffer(std::ostream& os) = 0; virtual void setThreadName(std::thread::id id, std::string_view threadName) = 0; + + virtual ArgHashType registerArg(LiteralString argType, std::string_view argValue) = 0; }; static constexpr std::size_t MinBufferSizeInMBytes = 16; @@ -79,16 +89,17 @@ namespace lms::core::tracing class ScopedTrace { public: - ScopedTrace(LiteralString category, Level level, LiteralString name, ITraceLogger* traceLogger = Service::get()) + ScopedTrace(LiteralString category, Level level, LiteralString name, LiteralString argType = {}, std::string_view argValue = {}, ITraceLogger* traceLogger = Service::get()) { if (traceLogger && traceLogger->isLevelActive(level)) { _traceLogger = traceLogger; _event.start = clock::now(); - _event.threadId = std::this_thread::get_id(); _event.name = name; _event.category = category; + if (!argType.empty() && !argValue.empty()) + _event.arg = traceLogger->registerArg(argType, argValue); } else { diff --git a/src/libs/core/include/core/LiteralString.hpp b/src/libs/core/include/core/LiteralString.hpp index 63bda2c1..7c0394bd 100644 --- a/src/libs/core/include/core/LiteralString.hpp +++ b/src/libs/core/include/core/LiteralString.hpp @@ -32,6 +32,7 @@ namespace lms::core template constexpr LiteralString(const char(&str)[N]) noexcept : _str{ str, N - 1 } { static_assert(N > 0); } + constexpr bool empty() const noexcept { return _str.empty(); } constexpr const char* c_str() const noexcept { return _str.data(); } constexpr std::size_t length() const noexcept { return _str.length(); } constexpr std::string_view str() const noexcept { return _str; } diff --git a/src/libs/core/test/TraceLogger.cpp b/src/libs/core/test/TraceLogger.cpp index 6d7f1beb..fcee9ea2 100644 --- a/src/libs/core/test/TraceLogger.cpp +++ b/src/libs/core/test/TraceLogger.cpp @@ -35,8 +35,8 @@ namespace lms::core::tracing::tests { threads.emplace_back([&] { - ScopedTrace loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", traceLogger.get() }; - ScopedTrace notLoggedEvent{ "MyCategory", Level::Detailed, "MyEventNotLogged", traceLogger.get() }; + ScopedTrace loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", "SomeArgType", "SomeArg", traceLogger.get() }; + ScopedTrace notLoggedEvent{ "MyNotLoggedCategory", Level::Detailed, "MyEventNotLogged", "SomeNotLoggedArgType", "SomeNotLoggedArg", traceLogger.get() }; }); } @@ -47,6 +47,13 @@ namespace lms::core::tracing::tests traceLogger->dumpCurrentBuffer(oss); EXPECT_NE(oss.str().find("MyEventLogged"), std::string::npos); + EXPECT_NE(oss.str().find("MyCategory"), std::string::npos); + EXPECT_NE(oss.str().find("SomeArgType"), std::string::npos); + EXPECT_NE(oss.str().find("SomeArg"), std::string::npos); + EXPECT_EQ(oss.str().find("MyEventNotLogged"), std::string::npos); + EXPECT_EQ(oss.str().find("MyNotLoggedCategory"), std::string::npos); + EXPECT_EQ(oss.str().find("SomeNotLoggedArgType"), std::string::npos); + EXPECT_EQ(oss.str().find("SomeNotLoggedArg"), std::string::npos); } } \ No newline at end of file diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index b5479f60..001ca984 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -205,6 +205,7 @@ namespace lms::db for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { + LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); func(*itResult); lastRetrievedArtist = (*itResult)->getId(); } diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 986caa40..8e3088d8 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -301,9 +301,9 @@ namespace lms::db } auto collection{ utils::execMultiResultQuery(query) }; - for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { + LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); func(*itResult); lastRetrievedRelease = (*itResult)->getId(); } diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 68266d89..b1140ea8 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -243,6 +243,7 @@ namespace lms::db for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) { + LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); func(*itResult); lastRetrievedTrack = (*itResult)->getId(); } diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 9604884f..dbd0b6a7 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -48,14 +48,14 @@ namespace lms::db::utils template auto execSingleResultQuery(const Query& query) { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecSingleResultQuery"); + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ExecSingleResultQuery", "Query", query.asString()); return query.resultValue(); } template auto execMultiResultQuery(const Query& query) { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecMultiResultQuery"); + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ExecMultiResultQuery", "Query", query.asString()); return query.resultList(); } From 284dc58fce770eb0114137144e24bce2ee705cc1 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 31 Mar 2024 23:36:56 +0200 Subject: [PATCH 20/64] Removed useless include --- src/libs/core/impl/TraceLogger.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/core/impl/TraceLogger.cpp b/src/libs/core/impl/TraceLogger.cpp index ca56c82b..5a6be0bc 100644 --- a/src/libs/core/impl/TraceLogger.cpp +++ b/src/libs/core/impl/TraceLogger.cpp @@ -20,7 +20,6 @@ #include "TraceLogger.hpp" #include -#include #include #include #include "core/Exception.hpp" From 741f0e12edb4e44aa5b5c06748b7bcaf746208aa Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 1 Apr 2024 20:27:52 +0200 Subject: [PATCH 21/64] Tracing: added some more details for db operations --- src/libs/database/impl/Artist.cpp | 41 +++--- src/libs/database/impl/AuthToken.cpp | 3 +- src/libs/database/impl/Cluster.cpp | 22 ++-- src/libs/database/impl/Listen.cpp | 14 +- src/libs/database/impl/MediaLibrary.cpp | 15 ++- src/libs/database/impl/Release.cpp | 120 +++++++++--------- src/libs/database/impl/ScanSettings.cpp | 2 +- src/libs/database/impl/StarredArtist.cpp | 8 +- src/libs/database/impl/StarredRelease.cpp | 6 +- src/libs/database/impl/StarredTrack.cpp | 10 +- src/libs/database/impl/Track.cpp | 73 +++++------ src/libs/database/impl/TrackArtistLink.cpp | 23 +++- src/libs/database/impl/TrackBookmark.cpp | 6 +- src/libs/database/impl/TrackFeatures.cpp | 6 +- src/libs/database/impl/TrackList.cpp | 42 +++--- src/libs/database/impl/User.cpp | 8 +- src/libs/database/impl/Utils.hpp | 114 ++++++++++++----- .../database/include/database/Release.hpp | 2 +- src/libs/database/include/database/Track.hpp | 4 +- 19 files changed, 275 insertions(+), 244 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 001ca984..cabdfc88 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -182,7 +182,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM artist")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM artist")); } void Artist::find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function& func, MediaLibraryId library) @@ -201,43 +201,38 @@ namespace lms::db query.where("t.media_library_id = ?").bind(library); } - auto collection{ utils::execMultiResultQuery(query) }; - - for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) - { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); - func(*itResult); - lastRetrievedArtist = (*itResult)->getId(); - } + utils::forEachQueryResult(query, [&](const Artist::pointer& artist) + { + func(artist); + lastRetrievedArtist = artist->getId(); + }); } std::vector Artist::find(Session& session, std::string_view name) { session.checkReadTransaction(); - Wt::Dbo::collection> res{ utils::execMultiResultQuery(session.getDboSession()->find() + return utils::fetchQueryResults(session.getDboSession()->find() .where("name = ?").bind(std::string{ name, 0, _maxNameLength }) - .orderBy("LENGTH(mbid) DESC")) }; // put mbid entries first - - return std::vector(res.begin(), res.end()); + .orderBy("LENGTH(mbid) DESC")); // put mbid entries first } Artist::pointer Artist::find(Session& session, const core::UUID& mbid) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("mbid = ?").bind(std::string{ mbid.getAsString() })); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("mbid = ?").bind(std::string{ mbid.getAsString() })); } Artist::pointer Artist::find(Session& session, ArtistId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } bool Artist::exists(Session& session, ArtistId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1; + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1; } RangeResults Artist::findOrphanIds(Session& session, std::optional range) @@ -268,7 +263,7 @@ namespace lms::db session.checkReadTransaction(); auto query{ createQuery>(session, params) }; - utils::execRangeQuery(query, params.range, func); + utils::forEachQueryRangeResult(query, params.range, func); } RangeResults Artist::findSimilarArtistIds(core::EnumSet artistLinkTypes, std::optional range) const @@ -343,14 +338,12 @@ namespace lms::db for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - Wt::Dbo::collection> queryRes{ utils::execMultiResultQuery(query) }; - std::map> clustersByType; - for (const Cluster::pointer& cluster : queryRes) - { - if (clustersByType[cluster->getType()->getId()].size() < size) - clustersByType[cluster->getType()->getId()].push_back(cluster); - } + utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster) + { + if (clustersByType[cluster->getType()->getId()].size() < size) + clustersByType[cluster->getType()->getId()].push_back(cluster); + }); std::vector> res; for (const auto& [clusterTypeId, clusters] : clustersByType) diff --git a/src/libs/database/impl/AuthToken.cpp b/src/libs/database/impl/AuthToken.cpp index b5e3e6c9..acdf936d 100644 --- a/src/libs/database/impl/AuthToken.cpp +++ b/src/libs/database/impl/AuthToken.cpp @@ -28,7 +28,6 @@ namespace lms::db { - AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) : _value{ value } , _expiry{ expiry } @@ -52,6 +51,6 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("value = ?").bind(value)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("value = ?").bind(value)); } } diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index ea67b5a2..704a334f 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -93,7 +93,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM cluster")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM cluster")); } RangeResults Cluster::findIds(Session& session, const FindParameters& params) @@ -124,14 +124,14 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } std::size_t Cluster::computeTrackCount(Session& session, ClusterId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .where("t_c.cluster_id = ?").bind(id)); } @@ -139,7 +139,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .where("t_c.cluster_id = ?").bind(id)); } @@ -172,7 +172,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM cluster_type")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM cluster_type")); } RangeResults ClusterType::findOrphanIds(Session& session, std::optional range) @@ -202,14 +202,14 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("name = ?").bind(std::string{ name })); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(std::string{ name })); } ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } RangeResults ClusterType::findIds(Session& session, std::optional range) @@ -226,7 +226,7 @@ namespace lms::db assert(self()); assert(session()); - return utils::execSingleResultQuery(session()->find() + return utils::fetchQuerySingleResult(session()->find() .where("name = ?").bind(name) .where("cluster_type_id = ?").bind(getId())); } @@ -236,10 +236,8 @@ namespace lms::db assert(self()); assert(session()); - auto res{ utils::execMultiResultQuery(session()->find() + return utils::fetchQueryResults(session()->find() .where("cluster_type_id = ?").bind(getId()) - .orderBy("name")) }; - - return std::vector(res.begin(), res.end()); + .orderBy("name")); } } // namespace lms::db diff --git a/src/libs/database/impl/Listen.cpp b/src/libs/database/impl/Listen.cpp index fecdbc5f..bd4b6b47 100644 --- a/src/libs/database/impl/Listen.cpp +++ b/src/libs/database/impl/Listen.cpp @@ -180,13 +180,13 @@ namespace lms::db std::size_t Listen::getCount(Session& session) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM listen")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM listen")); } Listen::pointer Listen::find(Session& session, ListenId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } RangeResults Listen::find(Session& session, const FindParameters& parameters) @@ -212,7 +212,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("user_id = ?").bind(userId) .where("track_id = ?").bind(trackId) .where("backend = ?").bind(backend) @@ -285,7 +285,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) from listen l") + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) from listen l") .join("user u ON u.id = l.user_id") .where("l.track_id = ?").bind(trackId) .where("l.user_id = ?").bind(userId) @@ -296,7 +296,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query( + return utils::fetchQuerySingleResult(session.getDboSession()->query( "SELECT IFNULL(MIN(count_result), 0)" " FROM (" " SELECT COUNT(l.track_id) AS count_result" @@ -314,7 +314,7 @@ namespace lms::db session.checkReadTransaction(); // TODO not pending remove? - return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT l from listen l") + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT l from listen l") .join("track t ON l.track_id = t.id") .where("t.release_id = ?").bind(releaseId) .where("l.user_id = ?").bind(userId) @@ -327,7 +327,7 @@ namespace lms::db { session.checkReadTransaction(); // TODO not pending remove? - return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT l from listen l") + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT l from listen l") .where("l.track_id = ?").bind(trackId) .where("l.user_id = ?").bind(userId) .where("l.backend = ?").bind(backend) diff --git a/src/libs/database/impl/MediaLibrary.cpp b/src/libs/database/impl/MediaLibrary.cpp index bda69a7a..f30105c7 100644 --- a/src/libs/database/impl/MediaLibrary.cpp +++ b/src/libs/database/impl/MediaLibrary.cpp @@ -44,36 +44,37 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM media_library")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM media_library")); } MediaLibrary::pointer MediaLibrary::find(Session& session, MediaLibraryId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } MediaLibrary::pointer MediaLibrary::find(Session& session, std::string_view name) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("name = ?").bind(name)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(name)); } MediaLibrary::pointer MediaLibrary::find(Session& session, const std::filesystem::path& p) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("path = ?").bind(p)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("path = ?").bind(p)); } void MediaLibrary::find(Session& session, std::function func) { session.checkReadTransaction(); - auto results{ utils::execMultiResultQuery(session.getDboSession()->find()) }; - for (const auto& result : results) - func(result); + utils::forEachQueryResult(session.getDboSession()->find(), [&](const MediaLibrary::pointer& mediaLibrary) + { + func(mediaLibrary); + }); } } // namespace lms::db diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 8e3088d8..7c4a844e 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -213,7 +213,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("id = ?").bind(id)); } @@ -221,7 +221,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("name = ?").bind(name)); } @@ -240,19 +240,17 @@ namespace lms::db { session.checkReadTransaction(); - auto res{ utils::execMultiResultQuery(session.getDboSession()->query>("SELECT DISTINCT r from release r") - .join("track t ON t.release_id = r.id") - .where("r.name = ?").bind(std::string(name, 0, _maxNameLength)) - .where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%"))}; - - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(session.getDboSession()->query>("SELECT DISTINCT r from release r") + .join("track t ON t.release_id = r.id") + .where("r.name = ?").bind(std::string(name, 0, _maxNameLength)) + .where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%")); } Release::pointer Release::find(Session& session, const core::UUID& mbid) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("mbid = ?").bind(mbid.getAsString())); } @@ -260,21 +258,21 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("id = ?").bind(id)); } bool Release::exists(Session& session, ReleaseId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 FROM release").where("id = ?").bind(id)) == 1; + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 FROM release").where("id = ?").bind(id)) == 1; } std::size_t Release::getCount(Session& session) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM release")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM release")); } RangeResults Release::findOrphanIds(Session& session, std::optional range) @@ -300,13 +298,11 @@ namespace lms::db query.where("t.media_library_id = ?").bind(library); } - auto collection{ utils::execMultiResultQuery(query) }; - for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) - { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); - func(*itResult); - lastRetrievedRelease = (*itResult)->getId(); - } + utils::forEachQueryResult(query, [&](const Release::pointer& release) + { + func(release); + lastRetrievedRelease = release->getId(); + }); } RangeResults Release::find(Session& session, const FindParameters& params) @@ -317,12 +313,12 @@ namespace lms::db return utils::execRangeQuery(query, params.range); } - void Release::find(Session& session, const FindParameters& params, std::function func) + void Release::find(Session& session, const FindParameters& params, const std::function& func) { session.checkReadTransaction(); auto query{ createQuery>(session, "DISTINCT r", params) }; - utils::execRangeQuery(query, params.range, func); + utils::forEachQueryRangeResult(query, params.range, func); } RangeResults Release::findIds(Session& session, const FindParameters& params) @@ -337,13 +333,13 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(createQuery(session, "COUNT(DISTINCT r.id)", params)); + return utils::fetchQuerySingleResult(createQuery(session, "COUNT(DISTINCT r.id)", params)); } std::size_t Release::getDiscCount() const { assert(session()); - int res{ utils::execSingleResultQuery(session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") + int res{ utils::fetchQuerySingleResult(session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") .join("release r ON r.id = t.release_id") .where("r.id = ?") .bind(getId())) }; @@ -354,16 +350,20 @@ namespace lms::db std::vector Release::getDiscs() const { assert(session()); + + using ResultType = std::tuple; - auto results{ utils::execMultiResultQuery(session()->query("SELECT DISTINCT disc_number, disc_subtitle FROM track t") + const auto query{ session()->query("SELECT DISTINCT disc_number, disc_subtitle FROM track t") .join("release r ON r.id = t.release_id") .where("r.id = ?") .orderBy("disc_number") - .bind(getId()))}; + .bind(getId()) }; std::vector discs; - for (const auto& res : results) - discs.emplace_back(DiscInfo{ static_cast(std::get(res)), std::get(res) }); + utils::forEachQueryResult(query, [&](ResultType&& res) + { + discs.emplace_back(DiscInfo{ static_cast(std::get(res)), std::move(std::get(res)) }); + }); return discs; } @@ -383,13 +383,13 @@ namespace lms::db assert(session()); const char* field{ original ? "original_date" : "date" }; - - auto dates{ utils::execMultiResultQuery(session()->query( - std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") + auto query{ (session()->query(std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?") .groupBy(field) .bind(getId())) }; + const auto dates{ utils::fetchQueryResults(query) }; + // various dates => invalid date if (dates.empty() || dates.size() > 1) return {}; @@ -412,11 +412,11 @@ namespace lms::db assert(session()); const char* field{ original ? "original_year" : "year" }; - - auto years{ utils::execMultiResultQuery(session()->query>( - std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") + auto query{ session()->query>(std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?").bind(getId()) - .groupBy(field)) }; + .groupBy(field) }; + + const auto years{ utils::fetchQueryResults(query) }; // various years => invalid years const std::size_t count{ years.size() }; @@ -430,44 +430,43 @@ namespace lms::db { assert(session()); - Wt::Dbo::collection copyrights{ utils::execMultiResultQuery(session()->query - ("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id") + auto query{ session()->query("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?") .groupBy("copyright") - .bind(getId())) }; + .bind(getId()) }; - std::vector values(copyrights.begin(), copyrights.end()); + const auto copyrights{ utils::fetchQueryResults(query) }; // various copyrights => no copyright - if (values.empty() || values.size() > 1 || values.front().empty()) + if (copyrights.empty() || copyrights.size() > 1 || copyrights.front().empty()) return std::nullopt; - return std::move(values.front()); + return std::move(copyrights.front()); } std::optional Release::getCopyrightURL() const { assert(session()); - Wt::Dbo::collection copyrights {utils::execMultiResultQuery(session()->query + const auto query{ session()->query ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?").bind(getId()) - .groupBy("copyright_url")) }; + .groupBy("copyright_url") }; - std::vector values(copyrights.begin(), copyrights.end()); + const auto copyrights{ utils::fetchQueryResults(query) }; // various copyright URLs => no copyright URL - if (values.empty() || values.size() > 1 || values.front().empty()) + if (copyrights.empty() || copyrights.size() > 1 || copyrights.front().empty()) return std::nullopt; - return std::move(values.front()); + return std::move(copyrights.front()); } std::size_t Release::getMeanBitrate() const { assert(session()); - return utils::execSingleResultQuery(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") + return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") .where("release_id = ?").bind(getId()) .where("bitrate > 0")); } @@ -476,15 +475,15 @@ namespace lms::db { assert(session()); - auto res{ utils::execMultiResultQuery(session()->query>( + const auto query{ session()->query>( "SELECT DISTINCT a FROM artist a" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?").bind(getId()) - .where("t_a_l.type = ?").bind(linkType))}; + .where("t_a_l.type = ?").bind(linkType) }; - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(query); } std::vector Release::getSimilarReleases(std::optional offset, std::optional count) const @@ -492,7 +491,7 @@ namespace lms::db assert(session()); // Select the similar releases using the 5 most used clusters of the release - auto res{ utils::execMultiResultQuery(session()->query>( + auto query{ session()->query>( "SELECT r FROM release r" " INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" @@ -510,9 +509,9 @@ namespace lms::db .groupBy("r.id") .orderBy("COUNT(*) DESC, RANDOM()") .limit(count ? static_cast(*count) : -1) - .offset(offset ? static_cast(*offset) : -1))}; + .offset(offset ? static_cast(*offset) : -1) }; - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(query); } void Release::clearReleaseTypes() @@ -538,6 +537,7 @@ namespace lms::db std::vector> Release::getReleaseTypes() const { + // TODO remove? return std::vector>(_releaseTypes.begin(), _releaseTypes.end()); } @@ -557,7 +557,7 @@ namespace lms::db using milli = std::chrono::duration; - return utils::execSingleResultQuery(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") + return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") .where("r.id = ?").bind(getId())); } @@ -565,7 +565,7 @@ namespace lms::db { assert(session()); - return utils::execSingleResultQuery(session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") + return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") .where("r.id = ?").bind(getId())); } @@ -593,14 +593,12 @@ namespace lms::db for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - auto queryRes{ utils::execMultiResultQuery(query) }; - std::map> clustersByType; - for (const Wt::Dbo::ptr& cluster : queryRes) - { - if (clustersByType[cluster->getType()->getId()].size() < size) - clustersByType[cluster->getType()->getId()].push_back(cluster); - } + utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster) + { + if (clustersByType[cluster->getType()->getId()].size() < size) + clustersByType[cluster->getType()->getId()].push_back(cluster); + }); std::vector> res; for (const auto& [clusterTypeId, clusters] : clustersByType) diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp index e76c24cb..40c39b81 100644 --- a/src/libs/database/impl/ScanSettings.cpp +++ b/src/libs/database/impl/ScanSettings.cpp @@ -42,7 +42,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find()); + return utils::fetchQuerySingleResult(session.getDboSession()->find()); } std::vector ScanSettings::getAudioFileExtensions() const diff --git a/src/libs/database/impl/StarredArtist.cpp b/src/libs/database/impl/StarredArtist.cpp index f8478be5..69b7e340 100644 --- a/src/libs/database/impl/StarredArtist.cpp +++ b/src/libs/database/impl/StarredArtist.cpp @@ -44,19 +44,19 @@ namespace lms::db std::size_t StarredArtist::getCount(Session& session) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM starred_artist")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM starred_artist")); } StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT s_a from starred_artist s_a") + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT s_a from starred_artist s_a") .join("user u ON u.id = s_a.user_id") .where("s_a.artist_id = ?").bind(artistId) .where("s_a.user_id = ?").bind(userId) @@ -66,7 +66,7 @@ namespace lms::db StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("artist_id = ?").bind(artistId) .where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)); diff --git a/src/libs/database/impl/StarredRelease.cpp b/src/libs/database/impl/StarredRelease.cpp index 944946e2..1b42407a 100644 --- a/src/libs/database/impl/StarredRelease.cpp +++ b/src/libs/database/impl/StarredRelease.cpp @@ -50,13 +50,13 @@ namespace lms::db StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT s_r from starred_release s_r") + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT s_r from starred_release s_r") .join("user u ON u.id = s_r.user_id") .where("s_r.release_id = ?").bind(releaseId) .where("s_r.user_id = ?").bind(userId) @@ -66,7 +66,7 @@ namespace lms::db StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("release_id = ?").bind(releaseId) .where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)); diff --git a/src/libs/database/impl/StarredTrack.cpp b/src/libs/database/impl/StarredTrack.cpp index 3a4049de..f2b42ed1 100644 --- a/src/libs/database/impl/StarredTrack.cpp +++ b/src/libs/database/impl/StarredTrack.cpp @@ -44,19 +44,19 @@ namespace lms::db std::size_t StarredTrack::getCount(Session& session) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM starred_track")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM starred_track")); } StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query>("SELECT s_t from starred_track s_t") + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT s_t from starred_track s_t") .join("user u ON u.id = s_t.user_id") .where("s_t.track_id = ?").bind(trackId) .where("s_t.user_id = ?").bind(userId) @@ -66,7 +66,7 @@ namespace lms::db StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("track_id = ?").bind(trackId) .where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)); @@ -74,7 +74,7 @@ namespace lms::db bool StarredTrack::exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) { - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 from starred_track") + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 from starred_track") .where("track_id = ?").bind(trackId) .where("user_id = ?").bind(userId) .where("backend = ?").bind(backend)); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index b1140ea8..068620eb 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -178,12 +178,12 @@ namespace lms::db return query; } - + template Wt::Dbo::Query createQuery(Session& session, const Track::FindParameters& params) { std::string_view itemToSelect; - + if constexpr (std::is_same_v) itemToSelect = "t.id"; else if constexpr (std::is_same_v>) @@ -209,21 +209,21 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM track")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track")); } Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("file_path = ?").bind(p.string())); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("file_path = ?").bind(p.string())); } Track::pointer Track::find(Session& session, TrackId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("id = ?").bind(id)); } @@ -239,41 +239,34 @@ namespace lms::db if (library.isValid()) query.where("media_library_id = ?").bind(library); - auto collection{ utils::execMultiResultQuery(query) }; - - for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) - { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); - func(*itResult); - lastRetrievedTrack = (*itResult)->getId(); - } + utils::forEachQueryResult(query, [&](const Track::pointer& track) + { + func(track); + lastRetrievedTrack = track->getId(); + }); } bool Track::exists(Session& session, TrackId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT 1 from track").where("id = ?").bind(id)) == 1; + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 from track").where("id = ?").bind(id)) == 1; } std::vector Track::findByMBID(Session& session, const core::UUID& mbid) { session.checkReadTransaction(); - auto res{ utils::execMultiResultQuery(session.getDboSession()->find() - .where("mbid = ?").bind(std::string {mbid.getAsString()})) }; - - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(session.getDboSession()->find() + .where("mbid = ?").bind(mbid.getAsString())); } std::vector Track::findByRecordingMBID(Session& session, const core::UUID& mbid) { session.checkReadTransaction(); - auto res{ utils::execMultiResultQuery(session.getDboSession()->find() - .where("recording_mbid = ?").bind(std::string {mbid.getAsString()})) }; - - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(session.getDboSession()->find() + .where("recording_mbid = ?").bind(mbid.getAsString())); } RangeResults Track::findPaths(Session& session, std::optional range) @@ -330,11 +323,11 @@ namespace lms::db { assert(session()); - auto res{ utils::execMultiResultQuery(session()->query + const auto query{ session()->query ("SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") - .where("t.id = ?").bind(getId())) }; + .where("t.id = ?").bind(getId()) }; - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(query); } RangeResults Track::findIds(Session& session, const FindParameters& parameters) @@ -353,20 +346,20 @@ namespace lms::db return utils::execRangeQuery(query, parameters.range); } - void Track::find(Session& session, const FindParameters& params, std::function func) + void Track::find(Session& session, const FindParameters& params, const std::function& func) { session.checkReadTransaction(); - auto query{ createQuery>(session, params)}; - utils::execRangeQuery(query, params.range, func); + auto query{ createQuery>(session, params) }; + utils::forEachQueryRangeResult(query, params.range, func); } - void Track::find(Session& session, const FindParameters& params, bool& moreResults, std::function func) + void Track::find(Session& session, const FindParameters& params, bool& moreResults, const std::function& func) { session.checkReadTransaction(); - auto query{ createQuery>(session, params)}; - utils::execRangeQuery(query, params.range, moreResults, func); + auto query{ createQuery>(session, params) }; + utils::forEachQueryRangeResult(query, params.range, moreResults, func); } RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, std::optional range) @@ -457,8 +450,7 @@ namespace lms::db query.where("t.id = ?").bind(getId()); - auto res{ utils::execMultiResultQuery(query) }; - return std::vector(std::begin(res), std::end(res)); + return utils::fetchQueryResults(query); } std::vector Track::getArtistIds(core::EnumSet linkTypes) const @@ -493,8 +485,7 @@ namespace lms::db query.where("t.id = ?").bind(getId()); - auto res{ utils::execMultiResultQuery(query) }; - return std::vector(std::cbegin(res), std::cend(res)); + return utils::fetchQueryResults(query); } std::vector Track::getArtistLinks() const @@ -527,14 +518,12 @@ namespace lms::db for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - auto queryRes{ utils::execMultiResultQuery(query) }; - std::map> clusters; - for (const Wt::Dbo::ptr& cluster : queryRes) - { - if (clusters[cluster->getType()->getId()].size() < size) - clusters[cluster->getType()->getId()].push_back(cluster); - } + utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster) + { + if (clusters[cluster->getType()->getId()].size() < size) + clusters[cluster->getType()->getId()].push_back(cluster); + }); std::vector> res; for (const auto& [type, clusters] : clusters) diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 38059dda..f3ca0ca9 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -76,7 +76,7 @@ namespace lms::db TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } RangeResults TrackArtistLink::find(Session& session, const FindParameters& params) @@ -91,19 +91,28 @@ namespace lms::db { session.checkReadTransaction(); - auto res{ utils::execMultiResultQuery(session.getDboSession()->query("SELECT DISTINCT type from track_artist_link")) }; + const auto query{ session.getDboSession()->query("SELECT DISTINCT type from track_artist_link") }; - return core::EnumSet(std::begin(res), std::end(res)); + core::EnumSet res; + utils::forEachQueryResult(query, [&](TrackArtistLinkType linkType) + { + res.insert(linkType); + }); + return res; } core::EnumSet TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId) { session.checkReadTransaction(); - auto res{ utils::execMultiResultQuery(session.getDboSession()->query("SELECT DISTINCT type from track_artist_link") - .where("artist_id = ?").bind(artistId)) }; + const auto query{ session.getDboSession()->query("SELECT DISTINCT type from track_artist_link") + .where("artist_id = ?").bind(artistId) }; - return core::EnumSet(std::begin(res), std::end(res)); + core::EnumSet res; + utils::forEachQueryResult(query, [&](TrackArtistLinkType linkType) + { + res.insert(linkType); + }); + return res; } } - diff --git a/src/libs/database/impl/TrackBookmark.cpp b/src/libs/database/impl/TrackBookmark.cpp index d5033ef0..d63c63c4 100644 --- a/src/libs/database/impl/TrackBookmark.cpp +++ b/src/libs/database/impl/TrackBookmark.cpp @@ -42,7 +42,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM track_bookmark")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_bookmark")); } RangeResults TrackBookmark::find(Session& session, UserId userId, std::optional range) @@ -59,7 +59,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("user_id = ?").bind(userId) .where("track_id = ?").bind(trackId)); } @@ -68,7 +68,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("id = ?").bind(id)); } diff --git a/src/libs/database/impl/TrackFeatures.cpp b/src/libs/database/impl/TrackFeatures.cpp index 5867114a..40a2a6d9 100644 --- a/src/libs/database/impl/TrackFeatures.cpp +++ b/src/libs/database/impl/TrackFeatures.cpp @@ -45,14 +45,14 @@ namespace lms::db { { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM track_features")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_features")); } TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id) { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("id = ?").bind(id)); } @@ -60,7 +60,7 @@ namespace lms::db { { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("track_id = ?").bind(trackId)); } diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index d83e0af7..d97e74a8 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -55,7 +55,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM tracklist")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM tracklist")); } @@ -64,7 +64,7 @@ namespace lms::db session.checkReadTransaction(); assert(userId.isValid()); - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("name = ?").bind(name) .where("type = ?").bind(type) .where("user_id = ?").bind(userId)); @@ -124,7 +124,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } bool TrackList::isEmpty() const @@ -163,7 +163,7 @@ namespace lms::db { assert(session()); - return utils::execSingleResultQuery(session()->find() + return utils::fetchQuerySingleResult(session()->find() .where("tracklist_id = ?").bind(getId()) .where("track_id = ?").bind(track->getId()) .where("date_time = ?").bind(utils::normalizeDateTime(dateTime))); @@ -173,12 +173,12 @@ namespace lms::db { assert(session()); - auto res{ utils::execMultiResultQuery(session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") + const auto query{ session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") .where("p.id = ?").bind(getId()) .groupBy("c.id") - .orderBy("COUNT(c.id) DESC")) }; + .orderBy("COUNT(c.id) DESC") }; - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(query); } std::vector> TrackList::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const @@ -216,14 +216,12 @@ namespace lms::db query.groupBy("c.id"); query.orderBy("COUNT(c.id) DESC"); - auto queryRes{ utils::execMultiResultQuery(query) }; - std::map> clustersByType; - for (const Wt::Dbo::ptr& cluster : queryRes) - { - if (clustersByType[cluster->getType()->getId()].size() < size) - clustersByType[cluster->getType()->getId()].push_back(cluster); - } + utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster) + { + if (clustersByType[cluster->getType()->getId()].size() < size) + clustersByType[cluster->getType()->getId()].push_back(cluster); + }); for (const auto& [clusterTypeId, clusters] : clustersByType) res.push_back(clusters); @@ -235,7 +233,7 @@ namespace lms::db { assert(session()); - auto res{ utils::execMultiResultQuery(session()->query>( + auto query{ session()->query>( "SELECT t FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " WHERE " @@ -247,19 +245,19 @@ namespace lms::db .groupBy("t.id") .orderBy("COUNT(*) DESC, RANDOM()") .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1))}; + .offset(offset ? static_cast(*offset) : -1) }; - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(query); } std::vector TrackList::getTrackIds() const { assert(session()); - auto res {utils::execMultiResultQuery(session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") - .where("p.id = ?").bind(getId())) }; + auto query{ session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") + .where("p.id = ?").bind(getId()) }; - return std::vector(res.begin(), res.end()); + return utils::fetchQueryResults(query); } std::chrono::milliseconds TrackList::getDuration() const @@ -268,7 +266,7 @@ namespace lms::db using milli = std::chrono::duration; - return utils::execSingleResultQuery(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") + return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") .where("p_e.tracklist_id = ?").bind(getId())); } @@ -305,6 +303,6 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } } // namespace lms::db diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index e34253b2..cf5b356b 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -44,7 +44,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->query("SELECT COUNT(*) FROM user")); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM user")); } RangeResults User::find(Session& session, const FindParameters& params) @@ -65,17 +65,17 @@ namespace lms::db { session.checkReadTransaction(); - return utils::execSingleResultQuery(session.getDboSession()->find().where("type = ?").bind(UserType::DEMO)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("type = ?").bind(UserType::DEMO)); } User::pointer User::find(Session& session, UserId id) { - return utils::execSingleResultQuery(session.getDboSession()->find().where("id = ?").bind(id)); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } User::pointer User::find(Session& session, std::string_view name) { - return utils::execSingleResultQuery(session.getDboSession()->find() + return utils::fetchQuerySingleResult(session.getDboSession()->find() .where("login_name = ?").bind(name)); } diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index dbd0b6a7..14cca456 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -45,22 +45,74 @@ namespace lms::db::utils } } - template - auto execSingleResultQuery(const Query& query) + template + auto fetchFirstResult(const Wt::Dbo::collection& collection) { - LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ExecSingleResultQuery", "Query", query.asString()); + LMS_SCOPED_TRACE_DETAILED("Database", "FetchFirstResult"); + return collection.begin(); + } + + template + void fetchNextResult(typename Wt::Dbo::collection::const_iterator& it) + { + LMS_SCOPED_TRACE_DETAILED("Database", "FetchNextResult"); + it++; + } + + template + void forEachResult(const Wt::Dbo::collection& collection, Func&& func) + { + typename Wt::Dbo::collection::const_iterator it{ fetchFirstResult(collection) }; + while (it != collection.end()) + { + func(*it); + fetchNextResult(it); + } + } + + template + struct QueryResultType; + + template + struct QueryResultType> + { + using type = ResultType; + }; + + template + void forEachQueryResult(const Query& query, UnaryFunc&& func) + { + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString()); + forEachResult(query.resultList(), std::forward(func)); + } + + template + std::vector fetchQueryResults(const Query& query) + { + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString()); + + auto collection{ query.resultList() }; + return std::vector(collection.begin(), collection.end()); + } + + template + std::vector::type> fetchQueryResults(const Query& query) + { + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString()); + + auto collection{ query.resultList() }; + return std::vector::type>(collection.begin(), collection.end()); + } + + template + auto fetchQuerySingleResult(const Query& query) + { + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString()); return query.resultValue(); } - template - auto execMultiResultQuery(const Query& query) - { - LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ExecMultiResultQuery", "Query", query.asString()); - return query.resultList(); - } - template - RangeResults execRangeQuery(Query& query, std::optional range) + RangeResults execRangeQuery(Query& query, const std::optional range) { RangeResults res; @@ -72,16 +124,12 @@ namespace lms::db::utils res.results.reserve(range->size); } - auto collection{ execMultiResultQuery(query) }; - for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) + // TODO optim useless last copy + res.results = utils::fetchQueryResults(query); + if (range && (res.results.size() == range->size + 1)) { - if (range && res.results.size() == range->size) - { - res.moreResults = true; - break; - } - - res.results.push_back(std::move(*itResult)); + res.moreResults = true; + res.results.pop_back(); } res.range.size = res.results.size(); @@ -89,31 +137,29 @@ namespace lms::db::utils return res; } - template - void execRangeQuery(Query& query, std::optional range, const std::function& func) + template + void forEachQueryRangeResult(Query& query, std::optional range, UnaryFunc&& func) { if (range) applyRange(query, range); - auto collection{ execMultiResultQuery(query) }; - for (const auto& res : collection) - { - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); - func(res); - } + forEachQueryResult(query, std::forward(func)); } - template - void execRangeQuery(Query& query, std::optional range, bool& moreResults, const std::function& func) + template + void forEachQueryRangeResult(Query& query, std::optional range, bool& moreResults, UnaryFunc&& func) { + using ResultType = typename QueryResultType::type; + if (range) applyRange(query, Range{ range->offset, range->size + 1 }); moreResults = false; std::size_t count{}; - auto collection{ execMultiResultQuery(query) }; - for (const auto& res : collection) + const auto collection{ query.resultList() }; + auto it{ fetchFirstResult(collection) }; + while (it != collection.end()) { if (range && (count++ == static_cast(range->size))) { @@ -121,8 +167,8 @@ namespace lms::db::utils break; } - LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRangeForEach"); - func(res); + func(*it); + fetchNextResult(it); } } diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index f12a7397..c9a79bcf 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -124,7 +124,7 @@ namespace lms::db static pointer find(Session& session, ReleaseId id); static void find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function& func, MediaLibraryId library = {}); static RangeResults find(Session& session, const FindParameters& parameters); - static void find(Session& session, const FindParameters& parameters, std::function func); + static void find(Session& session, const FindParameters& parameters, const std::function& func); static RangeResults findIds(Session& session, const FindParameters& parameters); static std::size_t getCount(Session& session, const FindParameters& parameters); static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // not track related diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 6b29414a..0dba7003 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -118,8 +118,8 @@ namespace lms::db static RangeResults findIds(Session& session, const FindParameters& parameters); static RangeResults find(Session& session, const FindParameters& parameters); - static void find(Session& session, const FindParameters& parameters, std::function func); - static void find(Session& session, const FindParameters& parameters, bool& moreResults, std::function func); + static void find(Session& session, const FindParameters& parameters, const std::function& func); + static void find(Session& session, const FindParameters& parameters, bool& moreResults, const std::function& func); static RangeResults findPaths(Session& session, std::optional range = std::nullopt); static RangeResults findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range = std::nullopt); From e2f68356fa7c6fc96e3abecb919228d8883721fd Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 1 Apr 2024 23:10:07 +0200 Subject: [PATCH 22/64] traces: compacted struct + fix bad json export --- src/libs/core/impl/TraceLogger.cpp | 30 ++++++++++++++++++++++-------- src/libs/core/impl/TraceLogger.hpp | 18 +++++++++++++++--- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/libs/core/impl/TraceLogger.cpp b/src/libs/core/impl/TraceLogger.cpp index 5a6be0bc..5d264f0c 100644 --- a/src/libs/core/impl/TraceLogger.cpp +++ b/src/libs/core/impl/TraceLogger.cpp @@ -68,7 +68,7 @@ namespace lms::core::tracing for (Buffer& buffer : _buffers) _freeBuffers.push_back(&buffer); - LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize) << ", entry size = " << sizeof(CompleteEvent) << ", entry count per buffer = " << Buffer::CompleteEventCount); + LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize) << ", entry size = " << sizeof(CompleteEventEntry) << ", entry count per buffer = " << Buffer::CompleteEventCount); } bool TraceLogger::isLevelActive(Level level) const @@ -81,7 +81,12 @@ namespace lms::core::tracing if (!_currentBuffer) _currentBuffer = acquireBuffer(); - _currentBuffer->durationEvents[_currentBuffer->currentDurationIndex] = event; + CompleteEventEntry& entry{ _currentBuffer->durationEvents[_currentBuffer->currentDurationIndex] }; + entry.start = event.start; + entry.duration = event.duration; + entry.name = event.name.c_str(); + entry.category = event.category.c_str(); + entry.arg = event.arg.value_or(invalidHash); // update the index after writing the event, in case another thread wants to dump if (++_currentBuffer->currentDurationIndex == _currentBuffer->durationEvents.size()) @@ -163,7 +168,7 @@ namespace lms::core::tracing // Hence the double representation as the microsecond unit is not precise enough using clockMicro = std::chrono::duration; - const CompleteEvent& event{ buffer.durationEvents[i] }; + const CompleteEventEntry& event{ buffer.durationEvents[i] }; if (first) first = false; @@ -171,24 +176,24 @@ namespace lms::core::tracing os << ", " << std::endl;; os << "\t\t{ "; - os << "\"name\" : \"" << event.name.c_str() << "\", "; - os << "\"cat\" : \"" << event.category.c_str() << "\", "; + os << "\"name\" : \"" << event.name << "\", "; + os << "\"cat\" : \"" << event.category << "\", "; os << "\"pid\": 1, "; os << "\"tid\" : " << threadId << ", "; os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.start - _start).count() << ", "; os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast(event.duration).count() << ", "; os << "\"ph\" : \"X\""; - if (event.arg.has_value()) + if (event.arg != invalidHash) { ArgEntryMap::const_iterator itArgEntry; { std::shared_lock lock{ _argMutex }; - itArgEntry = _argEntries.find(*event.arg); + itArgEntry = _argEntries.find(event.arg); assert(itArgEntry != _argEntries.cend()); } os << ", \"args\" : { \"" << itArgEntry->second.type.c_str() << "\" : \""; - stringUtils::writeJSEscapedString(os, std::string_view{ itArgEntry->second.value }); + stringUtils::writeJsonEscapedString(os, std::string_view{ itArgEntry->second.value }); os << "\" }"; } os << " }"; @@ -198,6 +203,7 @@ namespace lms::core::tracing os << std::endl; os << "\t]," << std::endl; + os << "\t\"meta_registered_arg_count\" : " << getRegisteredArgCount() << ", " << std::endl; os << "\t\"meta_cpu_count\" : " << std::thread::hardware_concurrency() << ", " << std::endl; os << "\t\"meta_build_type\" : "; #ifndef NDEBUG @@ -226,6 +232,7 @@ namespace lms::core::tracing TraceLogger::ArgHashType TraceLogger::registerArg(LiteralString argType, std::string_view argValue) { const ArgHashType hash{ computeArgHash(argType, argValue) }; + assert(hash != invalidHash); { const std::shared_lock lock{ _argMutex }; @@ -255,6 +262,13 @@ namespace lms::core::tracing } } + std::size_t TraceLogger::getRegisteredArgCount() const + { + const std::shared_lock lock{ _argMutex }; + + return _argEntries.size(); + } + std::uint32_t TraceLogger::toTraceThreadId(std::thread::id threadId) { // Pefetto UI does not accept 64bits thread ids diff --git a/src/libs/core/impl/TraceLogger.hpp b/src/libs/core/impl/TraceLogger.hpp index ee81090b..ba90c690 100644 --- a/src/libs/core/impl/TraceLogger.hpp +++ b/src/libs/core/impl/TraceLogger.hpp @@ -44,18 +44,30 @@ namespace lms::core::tracing void dumpCurrentBuffer(std::ostream& os) override; void setThreadName(std::thread::id id, std::string_view threadName) override; ArgHashType registerArg(LiteralString argType, std::string_view argValue) override; + std::size_t getRegisteredArgCount() const; static ArgHashType computeArgHash(LiteralString type, std::string_view value); static std::uint32_t toTraceThreadId(std::thread::id threadId); static constexpr std::size_t BufferSize{ 64 * 1024 }; + // Same as ComplteEvent, but compacted + struct CompleteEventEntry + { + clock::time_point start; + clock::duration duration; + const char* name; + const char* category; + ArgHashType arg; + }; + static constexpr ArgHashType invalidHash{ 0 }; + struct alignas(64) Buffer { - static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEvent) }; + static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEventEntry) }; std::thread::id threadId; - std::array durationEvents; + std::array durationEvents; std::atomic currentDurationIndex{}; }; @@ -68,7 +80,7 @@ namespace lms::core::tracing std::vector _buffers; // allocated once during construction - std::shared_mutex _argMutex; + mutable std::shared_mutex _argMutex; struct ArgEntry { LiteralString type; From 50ecfb13786ed2e81d10a5f5907c83678aa77617 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 2 Apr 2024 23:04:54 +0200 Subject: [PATCH 23/64] More query wrapped + fixed typo --- src/libs/database/impl/Cluster.cpp | 3 ++- src/libs/database/impl/Release.cpp | 10 +++++--- src/libs/database/impl/Track.cpp | 4 +-- .../database/include/database/Cluster.hpp | 2 +- .../database/include/database/Release.hpp | 2 +- src/libs/database/test/Release.cpp | 25 +++++++++++++++++++ src/libs/subsonic/impl/responses/Album.cpp | 2 +- src/libs/subsonic/impl/responses/Genre.cpp | 2 +- 8 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 704a334f..76065a8b 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -26,6 +26,7 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "IdTypeTraits.hpp" +#include "StringViewTraits.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" @@ -202,7 +203,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(std::string{ name })); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(name)); } ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 7c4a844e..8fb77601 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -530,15 +530,17 @@ namespace lms::db return getArtists().size() > 1; } - std::size_t Release::getTracksCount() const + std::size_t Release::getTrackCount() const { - return _tracks.size(); + assert(session()); + return utils::fetchQuerySingleResult(session()->query("SELECT COUNT(t.id) FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?").bind(getId())); } - std::vector> Release::getReleaseTypes() const + std::vector Release::getReleaseTypes() const { // TODO remove? - return std::vector>(_releaseTypes.begin(), _releaseTypes.end()); + return utils::fetchQueryResults(_releaseTypes.find()); } std::vector Release::getReleaseTypeNames() const diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 068620eb..bafa304e 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -316,7 +316,7 @@ namespace lms::db std::vector Track::getClusters() const { - return std::vector(_clusters.begin(), _clusters.end()); + return utils::fetchQueryResults(_clusters.find()); } std::vector Track::getClusterIds() const @@ -490,7 +490,7 @@ namespace lms::db std::vector Track::getArtistLinks() const { - return std::vector(_trackArtistLinks.begin(), _trackArtistLinks.end()); + return utils::fetchQueryResults(_trackArtistLinks.find()); } std::vector> Track::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const diff --git a/src/libs/database/include/database/Cluster.hpp b/src/libs/database/include/database/Cluster.hpp index ae869778..4408d2c2 100644 --- a/src/libs/database/include/database/Cluster.hpp +++ b/src/libs/database/include/database/Cluster.hpp @@ -75,7 +75,7 @@ namespace lms::db // Accessors std::string_view getName() const { return _name; } ObjectPtr getType() const { return _clusterType; } - std::size_t getTracksCount() const { return _trackCount; } + std::size_t getTrackCount() const { return _trackCount; } RangeResults getTracks(std::optional range = std::nullopt) const; std::size_t getReleasesCount() const { return _releaseCount; }; diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index c9a79bcf..2933d481 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -154,7 +154,7 @@ namespace lms::db std::chrono::milliseconds getDuration() const; Wt::WDateTime getLastWritten() const; std::string_view getArtistDisplayName() const { return _artistDisplayName; } - std::size_t getTracksCount() const; + std::size_t getTrackCount() const; std::vector> getReleaseTypes() const; std::vector getReleaseTypeNames() const; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 22540e51..89d3fade 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -910,4 +910,29 @@ namespace lms::db::tests } checkExpectedBitrate(192); // 0 should not be taken into account } + + TEST_F(DatabaseFixture, Release_trackCount) + { + ScopedRelease release1{ session, "MyRelease1" }; + ScopedRelease release2{ session, "MyRelease2" }; + ScopedRelease release3{ session, "MyRelease2" }; + + ScopedTrack track1{ session, "MyTrack1" }; + ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track3{ session, "MyTrack3" }; + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release1.get()); + track3.get().modify()->setRelease(release2.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(release1->getTrackCount(), 2); + EXPECT_EQ(release2->getTrackCount(), 1); + EXPECT_EQ(release3->getTrackCount(), 0); + } + } } \ 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 8e16b33a..163a89b2 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -47,7 +47,7 @@ namespace lms::api::subsonic if (id3) { albumNode.setAttribute("name", release->getName()); - albumNode.setAttribute("songCount", release->getTracksCount()); + albumNode.setAttribute("songCount", release->getTrackCount()); albumNode.setAttribute( "duration", std::chrono::duration_cast( release->getDuration()) diff --git a/src/libs/subsonic/impl/responses/Genre.cpp b/src/libs/subsonic/impl/responses/Genre.cpp index 9f761032..6b4fb015 100644 --- a/src/libs/subsonic/impl/responses/Genre.cpp +++ b/src/libs/subsonic/impl/responses/Genre.cpp @@ -28,7 +28,7 @@ namespace lms::api::subsonic Response::Node clusterNode; clusterNode.setValue(cluster->getName()); - clusterNode.setAttribute("songCount", cluster->getTracksCount()); + clusterNode.setAttribute("songCount", cluster->getTrackCount()); clusterNode.setAttribute("albumCount", cluster->getReleasesCount()); return clusterNode; From 0acef3bfd4da7470beeb3933ef09e5d2d64f373f Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 3 Apr 2024 00:01:19 +0200 Subject: [PATCH 24/64] Cleaned up Useless session --- .../services/scanner/impl/ScannerService.cpp | 11 ++--- .../services/scanner/impl/ScannerService.hpp | 3 -- .../services/scanner/impl/ScannerStats.cpp | 42 +++++++++---------- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 96ef1c41..356cbcbb 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -20,7 +20,6 @@ #include "ScannerService.hpp" #include -#include #include "database/MediaLibrary.hpp" #include "database/TrackFeatures.hpp" @@ -70,7 +69,6 @@ namespace lms::scanner ScannerService::ScannerService(Db& db) : _db{ db } - , _dbSession{ db } { _ioService.setThreadCount(1); @@ -291,7 +289,7 @@ namespace lms::scanner LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size()); - _dbSession.analyze(); + _db.getTLSSession().analyze(); if (!_abortScan) { @@ -358,9 +356,9 @@ namespace lms::scanner newSettings.skipDuplicateMBID = core::Service::get()->getBool("scanner-skip-duplicate-mbid", false); { - auto transaction{ _dbSession.createReadTransaction() }; + auto transaction{ _db.getTLSSession().createReadTransaction() }; - const ScanSettings::pointer scanSettings{ ScanSettings::get(_dbSession) }; + const ScanSettings::pointer scanSettings{ ScanSettings::get(_db.getTLSSession()) }; newSettings.scanVersion = scanSettings->getScanVersion(); newSettings.startTime = scanSettings->getUpdateStartTime(); @@ -373,7 +371,7 @@ namespace lms::scanner [](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; }); } - MediaLibrary::find(_dbSession, [&](const MediaLibrary::pointer& mediaLibrary) + MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) { newSettings.mediaLibraries.push_back(ScannerSettings::MediaLibraryInfo{ mediaLibrary->getId(), mediaLibrary->getPath().lexically_normal() }); }); @@ -409,5 +407,4 @@ namespace lms::scanner if (std::chrono::duration_cast(now - _lastScanInProgressEmit).count() > 1) notifyInProgress(stepStats); } - } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/ScannerService.hpp b/src/libs/services/scanner/impl/ScannerService.hpp index dec1c26c..d0beb160 100644 --- a/src/libs/services/scanner/impl/ScannerService.hpp +++ b/src/libs/services/scanner/impl/ScannerService.hpp @@ -74,11 +74,9 @@ namespace lms::scanner // Helpers void refreshScanSettings(); ScannerSettings readSettings(); - void reloadRecommendationService(); void notifyInProgressIfNeeded(const ScanStepStats& stats); void notifyInProgress(const ScanStepStats& stats); - void reloadSimilarityEngine(ScanStats& stats); std::vector> _scanSteps; @@ -89,7 +87,6 @@ namespace lms::scanner Events _events; std::chrono::system_clock::time_point _lastScanInProgressEmit{}; db::Db& _db; - db::Session _dbSession; mutable std::shared_mutex _statusMutex; State _curState{ State::NotScheduled }; diff --git a/src/libs/services/scanner/impl/ScannerStats.cpp b/src/libs/services/scanner/impl/ScannerStats.cpp index ad68a8fe..b2df60d6 100644 --- a/src/libs/services/scanner/impl/ScannerStats.cpp +++ b/src/libs/services/scanner/impl/ScannerStats.cpp @@ -19,32 +19,28 @@ #include "services/scanner/ScannerStats.hpp" -namespace lms::scanner { - -ScanError::ScanError(const std::filesystem::path& _file, ScanErrorType _error, const std::string& _systemError) -: file {_file}, -error {_error}, -systemError {_systemError} +namespace lms::scanner { -} + ScanError::ScanError(const std::filesystem::path& _file, ScanErrorType _error, const std::string& _systemError) + : file{ _file }, + error{ _error }, + systemError{ _systemError } + { + } -std::size_t -ScanStats::nbFiles() const -{ - return skips + additions + updates; -} + std::size_t ScanStats::nbFiles() const + { + return skips + additions + updates; + } -std::size_t -ScanStats::nbChanges() const -{ - return additions + deletions + updates; -} - -unsigned -ScanStepStats::progress() const -{ - return (processedElems / static_cast(totalElems ? totalElems : 1)) * 100; -} + std::size_t ScanStats::nbChanges() const + { + return additions + deletions + updates; + } + unsigned ScanStepStats::progress() const + { + return (processedElems / static_cast(totalElems ? totalElems : 1)) * 100; + } } // namespace lms::scanner From 8438da9fdef1eb427542bf20ec2fae09c9bfaac4 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 3 Apr 2024 00:02:48 +0200 Subject: [PATCH 25/64] Added db stats in trace metadata --- src/libs/core/impl/TraceLogger.cpp | 34 ++++++++++++++----- src/libs/core/impl/TraceLogger.hpp | 6 ++++ src/libs/core/include/core/ITraceLogger.hpp | 1 + src/libs/database/impl/Session.cpp | 21 ++++++++++++ .../database/include/database/Session.hpp | 1 + src/lms/main.cpp | 2 ++ 6 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/libs/core/impl/TraceLogger.cpp b/src/libs/core/impl/TraceLogger.cpp index 5d264f0c..adddad9d 100644 --- a/src/libs/core/impl/TraceLogger.cpp +++ b/src/libs/core/impl/TraceLogger.cpp @@ -69,6 +69,15 @@ namespace lms::core::tracing _freeBuffers.push_back(&buffer); LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize) << ", entry size = " << sizeof(CompleteEventEntry) << ", entry count per buffer = " << Buffer::CompleteEventCount); + + setMetadata("cpu_count", std::to_string(std::thread::hardware_concurrency())); + setMetadata("build_type", +#ifndef NDEBUG + "debug" +#else + "release" +#endif + ); } bool TraceLogger::isLevelActive(Level level) const @@ -203,15 +212,16 @@ namespace lms::core::tracing os << std::endl; os << "\t]," << std::endl; - os << "\t\"meta_registered_arg_count\" : " << getRegisteredArgCount() << ", " << std::endl; - os << "\t\"meta_cpu_count\" : " << std::thread::hardware_concurrency() << ", " << std::endl; - os << "\t\"meta_build_type\" : "; -#ifndef NDEBUG - os << "\"debug\""; -#else - os << "\"release\""; -#endif - os << std::endl; + { + std::scoped_lock lock{ _metadataMutex }; + for (const auto& [metadata, value] : _metadata) + { + os << "\t\"meta_" << metadata << "\": \""; + stringUtils::writeJsonEscapedString(os, value); + os << "\"," << std::endl; + } + } + os << "\t\"meta_registered_arg_count\" : " << getRegisteredArgCount() << std::endl; os << "}" << std::endl; } @@ -262,6 +272,12 @@ namespace lms::core::tracing } } + void TraceLogger::setMetadata(std::string_view metadata, std::string_view value) + { + const std::scoped_lock lock{ _metadataMutex }; + _metadata[std::string{ metadata }] = value; + } + std::size_t TraceLogger::getRegisteredArgCount() const { const std::shared_lock lock{ _argMutex }; diff --git a/src/libs/core/impl/TraceLogger.hpp b/src/libs/core/impl/TraceLogger.hpp index ba90c690..79cc256b 100644 --- a/src/libs/core/impl/TraceLogger.hpp +++ b/src/libs/core/impl/TraceLogger.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -44,6 +45,8 @@ namespace lms::core::tracing void dumpCurrentBuffer(std::ostream& os) override; void setThreadName(std::thread::id id, std::string_view threadName) override; ArgHashType registerArg(LiteralString argType, std::string_view argValue) override; + void setMetadata(std::string_view metadata, std::string_view value) override; + std::size_t getRegisteredArgCount() const; static ArgHashType computeArgHash(LiteralString type, std::string_view value); @@ -92,6 +95,9 @@ namespace lms::core::tracing std::mutex _threadNameMutex; std::unordered_map _threadNames; + std::mutex _metadataMutex; + std::map _metadata; + std::mutex _mutex; std::deque _freeBuffers; diff --git a/src/libs/core/include/core/ITraceLogger.hpp b/src/libs/core/include/core/ITraceLogger.hpp index dcf87a80..aad89edf 100644 --- a/src/libs/core/include/core/ITraceLogger.hpp +++ b/src/libs/core/include/core/ITraceLogger.hpp @@ -79,6 +79,7 @@ namespace lms::core::tracing virtual void write(const CompleteEvent& entry) = 0; virtual void dumpCurrentBuffer(std::ostream& os) = 0; virtual void setThreadName(std::thread::id id, std::string_view threadName) = 0; + virtual void setMetadata(std::string_view metadata, std::string_view value) = 0; virtual ArgHashType registerArg(LiteralString argType, std::string_view argValue) = 0; }; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 30754d48..4e5fb902 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -200,6 +200,27 @@ namespace lms::db } } + void Session::refreshTracingLoggerStats() + { + auto* traceLogger{ core::Service::get() }; + if (!traceLogger) + return; + + auto& dbSession{ _db.getTLSSession() }; + auto transaction{ dbSession.createReadTransaction() }; + + traceLogger->setMetadata("db_artist_count", std::to_string(db::Artist::getCount(dbSession))); + traceLogger->setMetadata("db_cluster_count", std::to_string(db::Cluster::getCount(dbSession))); + traceLogger->setMetadata("db_cluster_type_count", std::to_string(db::ClusterType::getCount(dbSession))); + traceLogger->setMetadata("db_starred_artist_count", std::to_string(db::StarredArtist::getCount(dbSession))); + traceLogger->setMetadata("db_starred_release_count", std::to_string(db::StarredRelease::getCount(dbSession))); + traceLogger->setMetadata("db_starred_track_count", std::to_string(db::StarredTrack::getCount(dbSession))); + traceLogger->setMetadata("db_track_bookmark_count", std::to_string(db::TrackBookmark::getCount(dbSession))); + traceLogger->setMetadata("db_listen_count", std::to_string(db::Listen::getCount(dbSession))); + traceLogger->setMetadata("db_release_count", std::to_string(db::Release::getCount(dbSession))); + traceLogger->setMetadata("db_track_count", std::to_string(db::Track::getCount(dbSession))); + } + void Session::analyze() { LMS_SCOPED_TRACE_DETAILED("Database", "Analyze"); diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index b9f48f0e..d2d10083 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -88,6 +88,7 @@ namespace lms::db void optimize(); void prepareTables(); // need to run only once at startup + void refreshTracingLoggerStats(); // returning a ptr here to ease further wrapping using operator-> Wt::Dbo::Session* getDboSession() { return &_session; } diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 4380005f..dd6c684c 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -286,6 +286,7 @@ namespace lms // force optimize in case scanner aborted during a large import: // queries may be too slow to even be able to relaunch a scan sing the web interface session.analyze(); + database.getTLSSession().refreshTracingLoggerStats(); } ui::LmsApplicationManager appManager; @@ -320,6 +321,7 @@ namespace lms // Flush cover cache even if no changes: // covers may be external files that changed and we don't keep track of them for now (but we should) coverService->flushCache(); + database.getTLSSession().refreshTracingLoggerStats(); }); core::Service feedbackService{ feedback::createFeedbackService(ioContext, database) }; From aff46065b0f7b30dd92aa58a5e14f14244dbf05b Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Apr 2024 14:10:02 +0200 Subject: [PATCH 26/64] Export the traces using gzip format --- CMakeLists.txt | 2 +- Dockerfile-release | 1 + src/libs/database/impl/Utils.hpp | 3 +-- src/lms/CMakeLists.txt | 5 +++-- src/lms/ui/admin/TracingView.cpp | 14 +++++++++++--- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f06fbc8..7a203733 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ endif() find_package(PkgConfig REQUIRED) find_package(Threads REQUIRED) find_package(Filesystem REQUIRED) -find_package(Boost REQUIRED COMPONENTS system program_options) +find_package(Boost REQUIRED COMPONENTS system program_options iostreams) find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP) pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib) pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++) diff --git a/Dockerfile-release b/Dockerfile-release index df4eaeee..64679344 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -147,6 +147,7 @@ ARG RUNTIME_PACKAGES=" \ lame-libs \ zlib \ boost-filesystem \ + boost-iostreams \ boost-program_options \ boost-system \ boost-thread \ diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 14cca456..56db65d8 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -173,5 +173,4 @@ namespace lms::db::utils } Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); -} - +} \ No newline at end of file diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index b9754984..a21e8098 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -58,6 +58,9 @@ target_include_directories(lms PRIVATE ) target_link_libraries(lms PRIVATE + Boost::iostreams + Wt::Wt + Wt::HTTP lmsav lmsauth lmsdatabase @@ -68,8 +71,6 @@ target_link_libraries(lms PRIVATE lmsservice-cover lmssubsonic lmscore - Wt::Wt - Wt::HTTP ) install(TARGETS lms DESTINATION bin) diff --git a/src/lms/ui/admin/TracingView.cpp b/src/lms/ui/admin/TracingView.cpp index c42f3dbd..91265b77 100644 --- a/src/lms/ui/admin/TracingView.cpp +++ b/src/lms/ui/admin/TracingView.cpp @@ -19,6 +19,9 @@ #include "TracingView.hpp" +#include +#include + #include #include #include @@ -46,9 +49,14 @@ namespace lms::ui void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) { - suggestFileName(core::stringUtils::toISO8601String(Wt::WDateTime::currentDateTime()) + "-traces.json"); - response.setMimeType("application/json"); - _traceLogger.dumpCurrentBuffer(response.out()); + response.setMimeType("application/gzip"); + suggestFileName(core::stringUtils::toISO8601String(Wt::WDateTime::currentDateTime()) + "-traces.json.gz"); + + boost::iostreams::filtering_ostream gzipStream; + gzipStream.push(boost::iostreams::gzip_compressor{}); + gzipStream.push(response.out()); + + _traceLogger.dumpCurrentBuffer(gzipStream); } private: From 9bb7756938bdf7cd968a3b0cf650c8221a9d7d7d Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Apr 2024 15:22:24 +0200 Subject: [PATCH 27/64] Added indexes on artist(id), release(id) and listen(id) --- src/libs/database/impl/Session.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 4e5fb902..d7aba43e 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -146,6 +146,7 @@ namespace lms::db // Indexes { auto transaction{ createWriteTransaction() }; + _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); @@ -155,6 +156,12 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); @@ -181,10 +188,6 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); From d050d039f5de99b8717f36d32ef78af259539cbc Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Apr 2024 23:33:55 +0200 Subject: [PATCH 28/64] Added missing db trace for migration --- src/libs/database/impl/Migration.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index a2f7121a..757fbeea 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -27,6 +27,8 @@ #include "database/User.hpp" #include "core/Exception.hpp" #include "core/ILogger.hpp" +#include "core/ITraceLogger.hpp" +#include "Utils.hpp" namespace lms::db { @@ -43,7 +45,7 @@ namespace lms::db { session.checkWriteTransaction(); - pointer versionInfo{ session.getDboSession()->find() }; + pointer versionInfo{ utils::fetchQuerySingleResult(session.getDboSession()->find()) }; if (!versionInfo) return session.getDboSession()->add(std::make_unique()); @@ -54,7 +56,7 @@ namespace lms::db { session.checkReadTransaction(); - return session.getDboSession()->find(); + return utils::fetchQuerySingleResult(session.getDboSession()->find()); } } @@ -462,6 +464,7 @@ SELECT }; { + LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration"); auto transaction{ session.createWriteTransaction() }; Version version; From d7b5d4190a6e4a841941f66bcad4f9e13eb45154 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Apr 2024 23:40:58 +0200 Subject: [PATCH 29/64] Added missing groupBy when iterating db with a media library filter --- src/libs/database/impl/Artist.cpp | 1 + src/libs/database/impl/Release.cpp | 1 + src/libs/database/test/Artist.cpp | 9 ++++++--- src/libs/database/test/Release.cpp | 9 ++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index cabdfc88..f09c17e0 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -199,6 +199,7 @@ namespace lms::db query.join("track t ON t.id = t_a_l.track_id"); query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); query.where("t.media_library_id = ?").bind(library); + query.groupBy("a.id"); } utils::forEachQueryResult(query, [&](const Artist::pointer& artist) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 8fb77601..8ff75a0b 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -296,6 +296,7 @@ namespace lms::db { query.join("track t ON t.release_id = r.id"); query.where("t.media_library_id = ?").bind(library); + query.groupBy("r.id"); } utils::forEachQueryResult(query, [&](const Release::pointer& release) diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index bff637df..11828a30 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -80,7 +80,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_findByRangedIdBased) { ScopedTrack track1{ session, "MyTrackFile1" }; - ScopedTrack track2{ session, "MyTrackFile2" }; + ScopedTrack track2a{ session, "MyTrackFile2a" }; + ScopedTrack track2b{ session, "MyTrackFile2b" }; ScopedTrack track3{ session, "MyTrackFile3" }; ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist2{ session, "MyArtist2" }; @@ -90,9 +91,11 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track2.get().modify()->setMediaLibrary(library.get()); + track2a.get().modify()->setMediaLibrary(library.get()); + track2b.get().modify()->setMediaLibrary(library.get()); TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2a.get(), artist2.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2b.get(), artist2.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track3.get(), artist3.get(), TrackArtistLinkType::Artist); } diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 89d3fade..9bf48e14 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -77,7 +77,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_findByRangedIdBased) { ScopedTrack track1{ session, "MyTrackFile1" }; - ScopedTrack track2{ session, "MyTrackFile2" }; + ScopedTrack track2a{ session, "MyTrackFile2a" }; + ScopedTrack track2b{ session, "MyTrackFile2b" }; ScopedTrack track3{ session, "MyTrackFile3" }; ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; @@ -87,9 +88,11 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track2.get().modify()->setMediaLibrary(library.get()); + track2a.get().modify()->setMediaLibrary(library.get()); + track2b.get().modify()->setMediaLibrary(library.get()); track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); + track2a.get().modify()->setRelease(release2.get()); + track2b.get().modify()->setRelease(release2.get()); track3.get().modify()->setRelease(release3.get()); } From f394e96b71690ff20dca71f99b04ee5ced16f3bb Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 12 Apr 2024 14:22:18 +0200 Subject: [PATCH 30/64] DB generator: make each generated release in a single media library (more realistic) --- src/tools/db-generator/LmsDbGenerator.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tools/db-generator/LmsDbGenerator.cpp b/src/tools/db-generator/LmsDbGenerator.cpp index 313309be..d59c43f2 100644 --- a/src/tools/db-generator/LmsDbGenerator.cpp +++ b/src/tools/db-generator/LmsDbGenerator.cpp @@ -90,6 +90,10 @@ namespace lms Artist::pointer artist{ generateArtist(context.session) }; + MediaLibrary::pointer mediaLibrary; + if (!context.mediaLibraries.empty()) + mediaLibrary = *core::random::pickRandom(context.mediaLibraries); + for (std::size_t i{}; i < params.trackCountPerRelease; ++i) { Track::pointer track{ context.session.create(params.trackPath) }; @@ -102,8 +106,8 @@ namespace lms track.modify()->setTrackMBID(core::UUID::generate()); track.modify()->setRecordingMBID(core::UUID::generate()); track.modify()->setTotalTrack(params.trackCountPerRelease); - if (!context.mediaLibraries.empty()) - track.modify()->setMediaLibrary(*core::random::pickRandom(context.mediaLibraries)); + if (mediaLibrary) + track.modify()->setMediaLibrary(mediaLibrary); TrackArtistLink::create(context.session, track, artist, TrackArtistLinkType::Artist); TrackArtistLink::create(context.session, track, artist, TrackArtistLinkType::ReleaseArtist); From 5bf9be304b062125d5de70aa9fa8b1b4ff7c372d Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 12 Apr 2024 14:22:57 +0200 Subject: [PATCH 31/64] Added missing index --- src/libs/database/impl/Session.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index d7aba43e..c76026ff 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -179,6 +179,7 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); From e6e8a6eac55e2dc1474d35cbe04604a3ea220097 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 12 Apr 2024 16:58:13 +0200 Subject: [PATCH 32/64] Optimized queries to iterate over artists/releases, when specifying a media library --- src/libs/database/impl/Artist.cpp | 6 ++---- src/libs/database/impl/Release.cpp | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index f09c17e0..ac944733 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -196,10 +196,8 @@ namespace lms::db if (library.isValid()) { - query.join("track t ON t.id = t_a_l.track_id"); - query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); - query.where("t.media_library_id = ?").bind(library); - query.groupBy("a.id"); + // Faster than using joins + query.where("EXISTS (SELECT 1 FROM track_artist_link t_a_l JOIN track t ON t.id = t_a_l.track_id WHERE t_a_l.artist_id = a.id AND t.media_library_id = ?)").bind(library); } utils::forEachQueryResult(query, [&](const Artist::pointer& artist) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 8ff75a0b..4aee67aa 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -294,9 +294,8 @@ namespace lms::db if (library.isValid()) { - query.join("track t ON t.release_id = r.id"); - query.where("t.media_library_id = ?").bind(library); - query.groupBy("r.id"); + // Faster than using joins + query.where("EXISTS (SELECT 1 FROM track t WHERE t.release_id = r.id AND t.media_library_id = ?)").bind(library); } utils::forEachQueryResult(query, [&](const Release::pointer& release) From cd5569db2f69017fbafe72e4326421cc4194ceed Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 13 Apr 2024 14:26:54 +0200 Subject: [PATCH 33/64] Give a chance to construct an ObjectPtr by moving source --- src/libs/database/include/database/Object.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/database/include/database/Object.hpp b/src/libs/database/include/database/Object.hpp index 3c7452b5..5fa33f69 100644 --- a/src/libs/database/include/database/Object.hpp +++ b/src/libs/database/include/database/Object.hpp @@ -31,7 +31,8 @@ namespace lms::db { public: ObjectPtr() = default; - ObjectPtr(Wt::Dbo::ptr obj) : _obj{ obj } {} + ObjectPtr(const Wt::Dbo::ptr& obj) : _obj{ obj } {} + ObjectPtr(Wt::Dbo::ptr&& obj) : _obj{ std::move(obj) } {} const T* operator->() const { return _obj.get(); } operator bool() const { return _obj.get(); } From be600b5e1312ee5367365ddfb37abd2a0413f2d9 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 13 Apr 2024 14:33:06 +0200 Subject: [PATCH 34/64] Small optim when visiting artist links --- src/libs/database/impl/TrackArtistLink.cpp | 16 ++++--- .../include/database/TrackArtistLink.hpp | 2 +- src/libs/subsonic/impl/responses/Song.cpp | 44 +++++++------------ src/lms/ui/explore/ReleaseView.cpp | 16 ++----- src/lms/ui/explore/TrackListHelpers.cpp | 15 ++----- 5 files changed, 36 insertions(+), 57 deletions(-) diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index f3ca0ca9..5667dd40 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -30,11 +30,11 @@ namespace lms::db { namespace { - Wt::Dbo::Query createQuery(Session& session, const TrackArtistLink::FindParameters& params) + Wt::Dbo::Query> createQuery(Session& session, const TrackArtistLink::FindParameters& params) { session.checkReadTransaction(); - auto query{ session.getDboSession()->query("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") }; + auto query{ session.getDboSession()->query>("SELECT t_a_l FROM track_artist_link t_a_l") }; if (params.linkType) query.where("t_a_l.type = ?").bind(*params.linkType); @@ -51,6 +51,8 @@ namespace lms::db if (params.track.isValid()) query.where("t.id = ?").bind(params.track); + query.groupBy("t_a_l.id"); + return query; } } @@ -79,12 +81,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } - RangeResults TrackArtistLink::find(Session& session, const FindParameters& params) + void TrackArtistLink::find(Session& session, const FindParameters& parameters, const std::function& func) { - session.checkReadTransaction(); + const auto query{ createQuery(session, parameters) }; - auto query{ createQuery(session, params) }; - return utils::execRangeQuery(query, params.range); + utils::forEachQueryResult(query, [&](const TrackArtistLink::pointer& link) + { + func(link); + }); } core::EnumSet TrackArtistLink::findUsedTypes(Session& session) diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/database/include/database/TrackArtistLink.hpp index 36af08ea..e3fe7580 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/database/include/database/TrackArtistLink.hpp @@ -62,7 +62,7 @@ namespace lms::db TrackArtistLink() = default; TrackArtistLink(ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type, std::string_view subType); - static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, const std::function&); static pointer find(Session& session, TrackArtistLinkId linkId); static pointer create(Session& session, ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type, std::string_view subType = {}); static core::EnumSet findUsedTypes(Session& session); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 68c8bbc2..8ecb40f9 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -65,7 +65,6 @@ namespace lms::api::subsonic std::string path; // The track path has to be relative from the root - const auto release{ track->getRelease() }; if (release) { @@ -191,40 +190,31 @@ namespace lms::api::subsonic trackResponse.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : ""); } - trackResponse.createEmptyArrayChild("contributors"); { TrackArtistLink::FindParameters params; params.setTrack(track->getId()); - for (const TrackArtistLinkId linkId : TrackArtistLink::find(context.dbSession, params).results) + trackResponse.createEmptyArrayChild("albumartists"); + trackResponse.createEmptyArrayChild("artists"); + trackResponse.createEmptyArrayChild("contributors"); + + TrackArtistLink::find(context.dbSession, params, [&](const TrackArtistLink::pointer& link) { - TrackArtistLink::pointer link{ TrackArtistLink::find(context.dbSession, linkId) }; - // Don't report artists nor release artists as they are set in dedicated fields - if (link && link->getType() != TrackArtistLinkType::Artist && link->getType() != TrackArtistLinkType::ReleaseArtist) - trackResponse.addArrayChild("contributors", createContributorNode(link)); - } + switch (link->getType()) + { + case TrackArtistLinkType::Artist: + trackResponse.addArrayChild("artists", createArtistNode(link->getArtist())); + break; + case TrackArtistLinkType::ReleaseArtist: + trackResponse.addArrayChild("albumartists", createArtistNode(link->getArtist())); + break; + default: + trackResponse.addArrayChild("contributors", createContributorNode(link)); + } + }); } - auto addArtistLinks{ [&](Response::Node::Key nodeName, TrackArtistLinkType type) - { - trackResponse.createEmptyArrayChild(nodeName); - - TrackArtistLink::FindParameters params; - params.setTrack(track->getId()); - params.setLinkType(type); - - for (const TrackArtistLinkId linkId : TrackArtistLink::find(context.dbSession, params).results) - { - TrackArtistLink::pointer link{ TrackArtistLink::find(context.dbSession, linkId) }; - if (link) - trackResponse.addArrayChild(nodeName, createArtistNode(link->getArtist())); - } - } }; - - addArtistLinks("artists", TrackArtistLinkType::Artist); trackResponse.setAttribute("displayArtist", track->getArtistDisplayName()); - - addArtistLinks("albumartists", TrackArtistLinkType::ReleaseArtist); if (release) trackResponse.setAttribute("displayAlbumArtist", release->getArtistDisplayName()); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 841cd234..963bbbe0 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -95,18 +95,10 @@ namespace lms::ui TrackArtistLink::FindParameters params; params.setRelease(releaseId); params.setLinkType(TrackArtistLinkType::Performer); - const auto links{ TrackArtistLink::find(LmsApp->getDbSession(), params) }; - if (links.results.empty()) - return; - - for (const TrackArtistLinkId linkId : links.results) - { - const TrackArtistLink::pointer link{ TrackArtistLink::find(LmsApp->getDbSession(), linkId) }; - if (!link) - continue; - - artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId()); - } + TrackArtistLink::find(LmsApp->getDbSession(), params, [&](const TrackArtistLink::pointer& link) + { + artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId()); + }); }; addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index a5ced047..05cd698c 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -82,18 +82,11 @@ namespace lms::ui::TrackListHelpers TrackArtistLink::FindParameters params; params.setTrack(trackId); params.setLinkType(TrackArtistLinkType::Performer); - const auto links{ TrackArtistLink::find(LmsApp->getDbSession(), params) }; - if (links.results.empty()) - return; - for (const TrackArtistLinkId linkId : links.results) - { - const TrackArtistLink::pointer link{ TrackArtistLink::find(LmsApp->getDbSession(), linkId) }; - if (!link) - continue; - - artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId()); - } + TrackArtistLink::find(LmsApp->getDbSession(), params, [&](const TrackArtistLink::pointer& link) + { + artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId()); + }); }; addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); From b23f38facf4afe10f3d33d1508612b293bae4a79 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Apr 2024 09:37:24 +0200 Subject: [PATCH 35/64] Added relative file path+file size in DB --- src/libs/database/impl/Migration.cpp | 16 ++- src/libs/database/impl/Release.cpp | 2 +- src/libs/database/impl/Session.cpp | 2 +- src/libs/database/impl/Track.cpp | 48 +++---- src/libs/database/include/database/Track.hpp | 64 ++++----- src/libs/database/test/Artist.cpp | 24 ++-- src/libs/database/test/Cluster.cpp | 36 ++--- src/libs/database/test/DatabaseTest.cpp | 4 +- src/libs/database/test/Listen.cpp | 82 ++++++------ src/libs/database/test/Release.cpp | 86 ++++++------ src/libs/database/test/StarredTrack.cpp | 8 +- src/libs/database/test/Track.cpp | 49 ++++--- src/libs/database/test/TrackBookmark.cpp | 2 +- src/libs/database/test/TrackFeatures.cpp | 2 +- src/libs/database/test/TrackList.cpp | 12 +- src/libs/services/cover/impl/CoverService.cpp | 8 +- .../impl/ScanStepCheckDuplicatedDbFiles.cpp | 2 +- .../impl/ScanStepRemoveOrphanDbFiles.cpp | 2 +- .../scanner/impl/ScanStepScanFiles.cpp | 126 +++++++++++++----- .../impl/entrypoints/MediaRetrieval.cpp | 6 +- src/libs/subsonic/impl/responses/Song.cpp | 51 +------ src/lms/ui/admin/ScannerController.cpp | 2 +- src/lms/ui/explore/ReleaseView.cpp | 2 +- src/lms/ui/explore/TrackListHelpers.cpp | 2 +- src/lms/ui/resource/AudioFileResource.cpp | 2 +- .../ui/resource/AudioTranscodingResource.cpp | 2 +- src/lms/ui/resource/DownloadResource.cpp | 4 +- src/tools/db-generator/LmsDbGenerator.cpp | 2 +- 28 files changed, 346 insertions(+), 302 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 757fbeea..9cb0a616 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -34,7 +34,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 54 }; + static constexpr Version LMS_DATABASE_VERSION{ 55 }; } VersionInfo::VersionInfo() @@ -430,6 +430,18 @@ SELECT session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV54(Session& session) + { + // Add file size + relative file path + session.getDboSession()->execute("ALTER TABLE track RENAME COLUMN file_path TO absolute_file_path"); + session.getDboSession()->execute("ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)"); + session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } + + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -461,6 +473,7 @@ SELECT {51, migrateFromV51}, {52, migrateFromV52}, {53, migrateFromV53}, + {54, migrateFromV54}, }; { @@ -487,6 +500,7 @@ SELECT while (version < LMS_DATABASE_VERSION) { + LMS_SCOPED_TRACE_DETAILED("Database", "MigrationStep"); LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "..."); auto itMigrationFunc{ migrationFunctions.find(version) }; diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 4aee67aa..816567b8 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -243,7 +243,7 @@ namespace lms::db return utils::fetchQueryResults(session.getDboSession()->query>("SELECT DISTINCT r from release r") .join("track t ON t.release_id = r.id") .where("r.name = ?").bind(std::string(name, 0, _maxNameLength)) - .where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%")); + .where("t.absolute_file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%")); } Release::pointer Release::find(Session& session, const core::UUID& mbid) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index c76026ff..66df2a14 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -167,7 +167,7 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index bafa304e..1ee75fb4 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -32,6 +32,7 @@ #include "core/ILogger.hpp" #include "IdTypeTraits.hpp" +#include "PathTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" #include "Utils.hpp" @@ -195,14 +196,9 @@ namespace lms::db } } - Track::Track(const std::filesystem::path& p) - : _filePath{ p.string() } + Track::pointer Track::create(Session& session) { - } - - Track::pointer Track::create(Session& session, const std::filesystem::path& p) - { - return session.getDboSession()->add(std::unique_ptr {new Track{ p }}); + return session.getDboSession()->add(std::make_unique()); } std::size_t Track::getCount(Session& session) @@ -216,7 +212,7 @@ namespace lms::db { session.checkReadTransaction(); - return utils::fetchQuerySingleResult(session.getDboSession()->find().where("file_path = ?").bind(p.string())); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("absolute_file_path = ?").bind(p.string())); } Track::pointer Track::find(Session& session, TrackId id) @@ -269,30 +265,6 @@ namespace lms::db .where("recording_mbid = ?").bind(mbid.getAsString())); } - RangeResults Track::findPaths(Session& session, std::optional range) - { - using QueryResultType = std::tuple; - session.checkReadTransaction(); - - // TODO Dbo traits on filesystem - auto query{ session.getDboSession()->query("SELECT id, file_path FROM track") }; - - RangeResults queryResults{ utils::execRangeQuery(query, range) }; - - RangeResults res; - res.range = queryResults.range; - res.moreResults = queryResults.moreResults; - res.results.reserve(queryResults.results.size()); - - std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results), - [](const QueryResultType& queryResult) - { - return PathResult{ std::get(queryResult), std::move(std::get(queryResult)) }; - }); - - return res; - } - RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, std::optional range) { session.checkReadTransaction(); @@ -392,6 +364,18 @@ namespace lms::db return utils::execRangeQuery(query, range); } + void Track::setAbsoluteFilePath(const std::filesystem::path& filePath) + { + assert(filePath.is_absolute()); + _absoluteFilePath = filePath; + } + + void Track::setRelativeFilePath(const std::filesystem::path& filePath) + { + assert(filePath.is_relative()); + _relativeFilePath = filePath; + } + void Track::clearArtistLinks() { _trackArtistLinks.clear(); diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 0dba7003..cc1999de 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -120,7 +120,6 @@ namespace lms::db static RangeResults find(Session& session, const FindParameters& parameters); static void find(Session& session, const FindParameters& parameters, const std::function& func); static void find(Session& session, const FindParameters& parameters, bool& moreResults, const std::function& func); - static RangeResults findPaths(Session& session, std::optional range = std::nullopt); static RangeResults findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range = std::nullopt); @@ -131,7 +130,9 @@ namespace lms::db void setTotalTrack(std::optional totalTrack) { _totalTrack = totalTrack; } void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } - void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } + void setAbsoluteFilePath(const std::filesystem::path& filePath); + void setRelativeFilePath(const std::filesystem::path& filePath); + void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } @@ -154,32 +155,34 @@ namespace lms::db void setClusters(const std::vector>& clusters); void setMediaLibrary(ObjectPtr mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); } - std::size_t getScanVersion() const { return _scanVersion; } - std::optional getTrackNumber() const { return _trackNumber; } - std::optional getTotalTrack() const { return _totalTrack; } - std::optional getDiscNumber() const { return _discNumber; } + std::size_t getScanVersion() const { return _scanVersion; } + std::optional getTrackNumber() const { return _trackNumber; } + std::optional getTotalTrack() const { return _totalTrack; } + std::optional getDiscNumber() const { return _discNumber; } const std::string& getDiscSubtitle() const { return _discSubtitle; } - std::string getName() const { return _name; } - std::filesystem::path getPath() const { return _filePath; } - std::chrono::milliseconds getDuration() const { return _duration; } - std::size_t getBitrate() const { return _bitrate; } + std::string getName() const { return _name; } + const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; } + const std::filesystem::path& getRelativeFilePath() const { return _relativeFilePath; } + long long getFileSize() const { return _fileSize; } + std::chrono::milliseconds getDuration() const { return _duration; } + std::size_t getBitrate() const { return _bitrate; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } const Wt::WDate& getDate() const { return _date; } - std::optional getYear() const { return _year; } + std::optional getYear() const { return _year; } const Wt::WDate& getOriginalDate() const { return _originalDate; } - std::optional getOriginalYear() const { return _originalYear; }; - Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } - Wt::WDateTime getAddedTime() const { return _fileAdded; } - bool hasCover() const { return _hasCover; } - std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } - std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } - std::optional getCopyright() const; - std::optional getCopyrightURL() const; - std::optional getTrackReplayGain() const { return _trackReplayGain; } - std::optional getReleaseReplayGain() const { return _releaseReplayGain; } - std::string_view getArtistDisplayName() const { return _artistDisplayName; } + std::optional getOriginalYear() const { return _originalYear; }; + const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } + const Wt::WDateTime& getAddedTime() const { return _fileAdded; } + bool hasCover() const { return _hasCover; } + std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } + std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } + std::optional getCopyright() const; + std::optional getCopyrightURL() const; + std::optional getTrackReplayGain() const { return _trackReplayGain; } + std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + std::string_view getArtistDisplayName() const { return _artistDisplayName; } // no artistLinkTypes means get all - std::vector> getArtists(core::EnumSet artistLinkTypes) const; // no type means all + std::vector> getArtists(core::EnumSet artistLinkTypes) const; // no type means all std::vector getArtistIds(core::EnumSet artistLinkTypes) const; // no type means all std::vector> getArtistLinks() const; ObjectPtr getRelease() const { return _release; } @@ -204,7 +207,9 @@ namespace lms::db Wt::Dbo::field(a, _year, "year"); Wt::Dbo::field(a, _originalDate, "original_date"); Wt::Dbo::field(a, _originalYear, "original_year"); - Wt::Dbo::field(a, _filePath, "file_path"); + Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path"); + Wt::Dbo::field(a, _relativeFilePath, "relative_file_path"); + Wt::Dbo::field(a, _fileSize, "file_size"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _fileAdded, "file_added"); Wt::Dbo::field(a, _hasCover, "has_cover"); @@ -223,8 +228,7 @@ namespace lms::db private: friend class Session; - Track(const std::filesystem::path& p); - static pointer create(Session& session, const std::filesystem::path& p); + static pointer create(Session& session); static constexpr std::size_t _maxNameLength{ 256 }; static constexpr std::size_t _maxCopyrightLength{ 256 }; @@ -237,12 +241,14 @@ namespace lms::db std::string _discSubtitle; std::string _name; std::chrono::duration _duration{}; - int _bitrate; // in bps + int _bitrate{}; // in bps Wt::WDate _date; std::optional _year; Wt::WDate _originalDate; std::optional _originalYear; - std::string _filePath; + std::filesystem::path _absoluteFilePath; // full path + std::filesystem::path _relativeFilePath; // relative to root (that may be deleted) + long long _fileSize{}; Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileAdded; bool _hasCover{}; @@ -270,5 +276,3 @@ namespace lms::db std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); } } // namespace lms::db - - diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 11828a30..7d06a95d 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -79,10 +79,10 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_findByRangedIdBased) { - ScopedTrack track1{ session, "MyTrackFile1" }; - ScopedTrack track2a{ session, "MyTrackFile2a" }; - ScopedTrack track2b{ session, "MyTrackFile2b" }; - ScopedTrack track3{ session, "MyTrackFile3" }; + ScopedTrack track1{ session }; + ScopedTrack track2a{ session }; + ScopedTrack track2b{ session }; + ScopedTrack track3{ session }; ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist2{ session, "MyArtist2" }; ScopedArtist artist3{ session, "MyArtist3" }; @@ -232,7 +232,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_singleTrack) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; { @@ -302,7 +302,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; ScopedMediaLibrary library{ session }; ScopedMediaLibrary otherLibrary{ session }; @@ -335,7 +335,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; { auto transaction{ session.createWriteTransaction() }; @@ -398,7 +398,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) { - ScopedTrack track{ session, "track" }; + ScopedTrack track{ session }; ScopedArtist artist1{ session, "artist1" }; ScopedArtist artist2{ session, "artist2" }; ASSERT_NE(artist1.getId(), artist2.getId()); @@ -458,7 +458,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_findByName) { ScopedArtist artist{ session, "AAA" }; - ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans + ScopedTrack track{ session }; // filters does not work on orphans { auto transaction{ session.createWriteTransaction() }; @@ -576,8 +576,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_nonReleaseTracks) { ScopedArtist artist{ session, "artist" }; - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedRelease release{ session, "MyRelease" }; { @@ -608,7 +608,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Artist_findByRelease) { ScopedArtist artist{ session, "artist" }; - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedRelease release{ session, "MyRelease" }; { diff --git a/src/libs/database/test/Cluster.cpp b/src/libs/database/test/Cluster.cpp index f332a43c..027cc14b 100644 --- a/src/libs/database/test/Cluster.cpp +++ b/src/libs/database/test/Cluster.cpp @@ -86,7 +86,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Cluster_singleTrack) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedClusterType clusterType{ session, "MyClusterType" }; { @@ -160,7 +160,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" }; @@ -215,7 +215,7 @@ namespace lms::db::tests for (std::size_t i{}; i < 10; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; @@ -270,7 +270,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { - ScopedTrack track{ session, "MyTrackFile" }; + ScopedTrack track{ session }; ScopedRelease release{ session, "MyRelease" }; { @@ -345,7 +345,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { - ScopedTrack track{ session, "MyTrackFile" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; ScopedClusterType clusterType{ session, "MyType" }; ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" }; @@ -406,7 +406,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) { - ScopedTrack track{ session, "MyTrackFile" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; ScopedClusterType clusterType{ session, "MyType" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; @@ -450,7 +450,7 @@ namespace lms::db::tests for (std::size_t i{}; i < nbTracks; ++i) { - tracks.emplace_back(session, "MyTrackFile" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); @@ -486,7 +486,7 @@ namespace lms::db::tests for (std::size_t i{}; i < 10; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; @@ -515,7 +515,7 @@ namespace lms::db::tests for (std::size_t i{}; i < 5; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; @@ -525,7 +525,7 @@ namespace lms::db::tests for (std::size_t i{ 5 }; i < 10; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; @@ -555,7 +555,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedRelease release{ session, "MyRelease" }; ScopedArtist artist{ session, "MyArtist" }; ScopedClusterType clusterType{ session, "MyType" }; @@ -597,7 +597,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedRelease release{ session, "MyRelease" }; ScopedArtist artist{ session, "MyArtist" }; ScopedClusterType clusterType{ session, "MyClusterType" }; @@ -636,7 +636,7 @@ namespace lms::db::tests for (std::size_t i{}; i < 20; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; @@ -669,7 +669,7 @@ namespace lms::db::tests for (std::size_t i{}; i < 20; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; @@ -729,7 +729,7 @@ namespace lms::db::tests std::list tracks; for (std::size_t i{}; i < 10; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; @@ -744,7 +744,7 @@ namespace lms::db::tests cluster1.get().modify()->addTrack(tracks.back().get()); } - tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); + tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist); @@ -810,7 +810,7 @@ namespace lms::db::tests std::list tracks; for (std::size_t i{}; i < 10; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; @@ -825,7 +825,7 @@ namespace lms::db::tests cluster1.get().modify()->addTrack(tracks.back().get()); } - tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); + tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; tracks.back().get().modify()->setRelease(release3.get()); diff --git a/src/libs/database/test/DatabaseTest.cpp b/src/libs/database/test/DatabaseTest.cpp index b46079b2..7d2c4840 100644 --- a/src/libs/database/test/DatabaseTest.cpp +++ b/src/libs/database/test/DatabaseTest.cpp @@ -32,7 +32,7 @@ namespace lms::db::tests for (std::size_t i{}; i < nbTracks; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; @@ -60,7 +60,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedRelease release{ session, "MyRelease" }; ScopedArtist artist{ session, "MyArtist" }; diff --git a/src/libs/database/test/Listen.cpp b/src/libs/database/test/Listen.cpp index b7461df6..f50246d6 100644 --- a/src/libs/database/test/Listen.cpp +++ b/src/libs/database/test/Listen.cpp @@ -26,7 +26,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getAll) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; { @@ -59,7 +59,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_get) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; @@ -93,7 +93,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_get_multi) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}} }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; @@ -112,7 +112,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_get_byDateTime) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime2{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2} }; @@ -145,7 +145,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopArtists) { - ScopedTrack track1{ session, "MyTrack" }; + ScopedTrack track1{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; @@ -162,7 +162,7 @@ namespace lms::db::tests EXPECT_EQ(artists.moreResults, false); } - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track2{ session }; ScopedArtist artist1{ session, "MyArtist1" }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1.addSecs(1) }; @@ -226,9 +226,9 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopArtists_multi) { ScopedUser user{ session, "MyUser" }; - ScopedTrack track1{ session, "MyTrack1" }; + ScopedTrack track1{ session }; ScopedArtist artist1{ session, "MyArtist1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track2{ session }; ScopedArtist artist2{ session, "MyArtist2" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -293,7 +293,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedArtist artist{ session, "MyArtist" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -337,7 +337,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopArtists_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -403,7 +403,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopReleases) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedRelease release{ session, "MyRelease" }; @@ -453,8 +453,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopReleases_multi) { - ScopedTrack track1{ session, "MyTrack" }; - ScopedTrack track2{ session, "MyTrack" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -513,7 +513,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -558,7 +558,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopReleases_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedRelease release{ session, "MyRelease" }; @@ -615,7 +615,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopTracks) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -660,7 +660,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopTracks_artist) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedArtist artist{ session, "MyArtist" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -714,8 +714,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopTrack_multi) { - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -766,7 +766,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopTracks_cluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -805,7 +805,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getTopTracks_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedMediaLibrary library{ session }; @@ -860,7 +860,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentArtists) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedArtist artist{ session, "MyArtist" }; @@ -937,9 +937,9 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) { ScopedUser user{ session, "MyUser" }; - ScopedTrack track1{ session, "MyTrack1" }; + ScopedTrack track1{ session }; ScopedArtist artist1{ session, "MyArtist1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track2{ session }; ScopedArtist artist2{ session, "MyArtist2" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -1003,7 +1003,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedArtist artist{ session, "MyArtist" }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; @@ -1048,7 +1048,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentArtists_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedArtist artist{ session, "MyArtist" }; ScopedMediaLibrary library{ session }; @@ -1086,7 +1086,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentReleases) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedRelease release{ session, "MyRelease" }; @@ -1137,7 +1137,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getMostRecentRelease) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedRelease release{ session, "MyRelease" }; @@ -1189,8 +1189,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) { - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedUser user{ session, "MyUser" }; ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; @@ -1265,7 +1265,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedClusterType clusterType{ session, "MyType" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; @@ -1322,7 +1322,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentReleases_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedRelease release{ session, "MyRelease" }; ScopedMediaLibrary library{ session }; @@ -1362,7 +1362,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentTracks) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; { @@ -1409,7 +1409,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentTracks_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedMediaLibrary library{ session }; ScopedMediaLibrary otherLibrary{ session }; @@ -1448,7 +1448,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getCount_track) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; { @@ -1482,8 +1482,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getCount_release) { - ScopedTrack track1{ session, "MyTrack" }; - ScopedTrack track2{ session, "MyTrack" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedUser user{ session, "MyUser" }; ScopedRelease release{ session, "MyRelease" }; @@ -1530,7 +1530,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getMostRecentTrack) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; { @@ -1576,8 +1576,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentTracks_multi) { - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; @@ -1644,7 +1644,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 9bf48e14..8f78a71d 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -76,10 +76,10 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_findByRangedIdBased) { - ScopedTrack track1{ session, "MyTrackFile1" }; - ScopedTrack track2a{ session, "MyTrackFile2a" }; - ScopedTrack track2b{ session, "MyTrackFile2b" }; - ScopedTrack track3{ session, "MyTrackFile3" }; + ScopedTrack track1{ session }; + ScopedTrack track2a{ session }; + ScopedTrack track2b{ session }; + ScopedTrack track3{ session }; ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release3{ session, "MyRelease3" }; @@ -172,7 +172,7 @@ namespace lms::db::tests ScopedRelease release{ session, "MyRelease" }; { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; { auto transaction{ session.createWriteTransaction() }; @@ -229,7 +229,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_singleTrack_mediaLibrary) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedRelease release{ session, "MyRelease" }; ScopedMediaLibrary library{ session }; ScopedMediaLibrary otherLibrary{ session }; @@ -257,17 +257,17 @@ namespace lms::db::tests { ScopedRelease release1{ session, "MyRelease" }; ScopedRelease release2{ session, "MyRelease" }; - ScopedTrack track1{ session, "MyTrack" }; - ScopedTrack track2{ session, "MyTrack" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; { auto transaction{ session.createWriteTransaction() }; track1.get().modify()->setRelease(release1.get()); - track1.get().modify()->setPath("/tmp/foo/foo.mp3"); + track1.get().modify()->setAbsoluteFilePath("/tmp/foo/foo.mp3"); track2.get().modify()->setRelease(release2.get()); - track2.get().modify()->setPath("/tmp/bar/bar.mp3"); + track2.get().modify()->setAbsoluteFilePath("/tmp/bar/bar.mp3"); } { @@ -296,12 +296,12 @@ namespace lms::db::tests ScopedRelease release6{ session, "_yRelease" }; // filters does not work on orphans - ScopedTrack track1{ session, "MyTrack" }; - ScopedTrack track2{ session, "MyTrack" }; - ScopedTrack track3{ session, "MyTrack" }; - ScopedTrack track4{ session, "MyTrack" }; - ScopedTrack track5{ session, "MyTrack" }; - ScopedTrack track6{ session, "MyTrack" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; + ScopedTrack track4{ session }; + ScopedTrack track5{ session }; + ScopedTrack track6{ session }; { auto transaction{ session.createWriteTransaction() }; @@ -362,7 +362,7 @@ namespace lms::db::tests EXPECT_FALSE(release1->getTotalDisc()); } - ScopedTrack track1{ session, "MyTrack" }; + ScopedTrack track1{ session }; { auto transaction{ session.createWriteTransaction() }; @@ -391,7 +391,7 @@ namespace lms::db::tests EXPECT_EQ(*release1->getTotalDisc(), 6); } - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track2{ session }; { auto transaction{ session.createWriteTransaction() }; @@ -416,7 +416,7 @@ namespace lms::db::tests EXPECT_FALSE(release2->getTotalDisc()); } - ScopedTrack track3{ session, "MyTrack3" }; + ScopedTrack track3{ session }; { auto transaction{ session.createWriteTransaction() }; @@ -443,10 +443,10 @@ namespace lms::db::tests ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; - ScopedTrack track1A{ session, "MyTrack1A" }; - ScopedTrack track1B{ session, "MyTrack1B" }; - ScopedTrack track2A{ session, "MyTrack2A" }; - ScopedTrack track2B{ session, "MyTrack2B" }; + ScopedTrack track1A{ session }; + ScopedTrack track1B{ session }; + ScopedTrack track2A{ session }; + ScopedTrack track2B{ session }; { auto transaction{ session.createReadTransaction() }; @@ -496,10 +496,10 @@ namespace lms::db::tests const Wt::WDate release1Date{ Wt::WDate {1994, 2, 3} }; const Wt::WDate release1OriginalDate{ Wt::WDate {1993, 4, 5} }; - ScopedTrack track1A{ session, "MyTrack1A" }; - ScopedTrack track1B{ session, "MyTrack1B" }; - ScopedTrack track2A{ session, "MyTrack2A" }; - ScopedTrack track2B{ session, "MyTrack2B" }; + ScopedTrack track1A{ session }; + ScopedTrack track1B{ session }; + ScopedTrack track2A{ session }; + ScopedTrack track2B{ session }; { auto transaction{ session.createReadTransaction() }; @@ -548,10 +548,10 @@ namespace lms::db::tests const int release1Year{ 1994 }; const int release1OriginalYear{ 1993 }; - ScopedTrack track1A{ session, "MyTrack1A" }; - ScopedTrack track1B{ session, "MyTrack1B" }; - ScopedTrack track2A{ session, "MyTrack2A" }; - ScopedTrack track2B{ session, "MyTrack2B" }; + ScopedTrack track1A{ session }; + ScopedTrack track1B{ session }; + ScopedTrack track2A{ session }; + ScopedTrack track2B{ session }; { auto transaction{ session.createReadTransaction() }; @@ -597,7 +597,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_writtenAfter) { ScopedRelease release{ session, "MyRelease" }; - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} }; @@ -629,7 +629,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_artist) { ScopedRelease release{ session, "MyRelease" }; - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist2{ session, "MyArtist2" }; { @@ -690,8 +690,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_getDiscCount) { ScopedRelease release{ session, "MyRelease" }; - ScopedTrack track{ session, "MyTrack" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track{ session }; + ScopedTrack track2{ session }; { auto transaction{ session.createReadTransaction() }; @@ -816,8 +816,8 @@ namespace lms::db::tests ScopedRelease release2{ session, "MyRelease2" }; const Wt::WDate release2Date{ Wt::WDate {1994, 2, 3} }; - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ASSERT_LT(release2Date, release1Date); ASSERT_GT(release2Date, release1OriginalDate); @@ -879,9 +879,9 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Release_meanBitrate) { ScopedRelease release1{ session, "MyRelease1" }; - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; - ScopedTrack track3{ session, "MyTrack3" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; auto checkExpectedBitrate = [&](std::size_t bitrate) { @@ -920,9 +920,9 @@ namespace lms::db::tests ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release3{ session, "MyRelease2" }; - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; - ScopedTrack track3{ session, "MyTrack3" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; { auto transaction{ session.createWriteTransaction() }; diff --git a/src/libs/database/test/StarredTrack.cpp b/src/libs/database/test/StarredTrack.cpp index ac21eeba..8ef77997 100644 --- a/src/libs/database/test/StarredTrack.cpp +++ b/src/libs/database/test/StarredTrack.cpp @@ -26,7 +26,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, StarredTrack) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedUser user2{ session, "MyUser2" }; @@ -78,7 +78,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Starredtrack_PendingDestroy) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; @@ -96,8 +96,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, StarredTrack_dateTime) { - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; ScopedUser user{ session, "MyUser" }; ScopedStarredTrack starredTrack1{ session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index c3a0df8f..592e1f2a 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests } } - ScopedTrack track{ session, "MyTrackFile" }; + ScopedTrack track{ session }; { auto transaction{ session.createReadTransaction() }; @@ -65,9 +65,9 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_findByRangedIdBased) { - ScopedTrack track1{ session, "MyTrackFile1" }; - ScopedTrack track2{ session, "MyTrackFile1" }; - ScopedTrack track3{ session, "MyTrackFile1" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; ScopedMediaLibrary library{ session }; ScopedMediaLibrary otherLibrary{ session }; @@ -149,7 +149,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_MediaLibrary) { - ScopedTrack track{ session, "MyTrackFile" }; + ScopedTrack track{ session }; ScopedMediaLibrary library{ session }; ScopedMediaLibrary otherLibrary{ session }; @@ -173,7 +173,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_noMediaLibrary) { - ScopedTrack track{ session, "MyTrackFile" }; + ScopedTrack track{ session }; { auto transaction{ session.createReadTransaction() }; MediaLibrary::pointer mediaLibrary{ track->getMediaLibrary() }; @@ -200,8 +200,8 @@ namespace lms::db::tests TEST_F(DatabaseFixture, MultipleTracks) { - ScopedTrack track1{ session, "MyTrackFile1" }; - ScopedTrack track2{ session, "MyTrackFile2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; { auto transaction{ session.createReadTransaction() }; @@ -214,12 +214,12 @@ namespace lms::db::tests TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) { - ScopedTrack track1{ session, "" }; - ScopedTrack track2{ session, "" }; - ScopedTrack track3{ session, "" }; - ScopedTrack track4{ session, "" }; - ScopedTrack track5{ session, "" }; - ScopedTrack track6{ session, "" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; + ScopedTrack track4{ session }; + ScopedTrack track5{ session }; + ScopedTrack track6{ session }; { auto transaction{ session.createWriteTransaction() }; @@ -260,7 +260,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_date) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; const Wt::WDate date{ 1995, 5, 5 }; const Wt::WDate originalDate{ 1994, 2, 2 }; { @@ -298,7 +298,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, Track_writtenAfter) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} }; @@ -325,4 +325,21 @@ namespace lms::db::tests EXPECT_EQ(tracks.results.size(), 0); } } + + TEST_F(DatabaseFixture, Track_path) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setAbsoluteFilePath("/root/foo/file.path"); + track.get().modify()->setRelativeFilePath("foo/file.path"); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getAbsoluteFilePath(), "/root/foo/file.path"); + EXPECT_EQ(track->getRelativeFilePath(), "foo/file.path"); + } + } } \ No newline at end of file diff --git a/src/libs/database/test/TrackBookmark.cpp b/src/libs/database/test/TrackBookmark.cpp index 5571c92c..91a533be 100644 --- a/src/libs/database/test/TrackBookmark.cpp +++ b/src/libs/database/test/TrackBookmark.cpp @@ -27,7 +27,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, TrackBookmark) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; { diff --git a/src/libs/database/test/TrackFeatures.cpp b/src/libs/database/test/TrackFeatures.cpp index d5b74553..ed42ee29 100644 --- a/src/libs/database/test/TrackFeatures.cpp +++ b/src/libs/database/test/TrackFeatures.cpp @@ -27,7 +27,7 @@ namespace lms::db::tests TEST_F(DatabaseFixture, TrackFeatures) { - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; { diff --git a/src/libs/database/test/TrackList.cpp b/src/libs/database/test/TrackList.cpp index ee2e31d2..5473e13e 100644 --- a/src/libs/database/test/TrackList.cpp +++ b/src/libs/database/test/TrackList.cpp @@ -44,7 +44,7 @@ namespace lms::db::tests ScopedUser user{ session, "MyUser" }; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; { auto transaction{ session.createReadTransaction() }; @@ -79,7 +79,7 @@ namespace lms::db::tests ScopedUser user{ session, "MyUser" }; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; { auto transaction{ session.createReadTransaction() }; @@ -131,7 +131,7 @@ namespace lms::db::tests for (std::size_t i{}; i < 10; ++i) { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; session.create(tracks.back().get(), trackList.get()); @@ -158,7 +158,7 @@ namespace lms::db::tests ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; - ScopedTrack track{ session, "MyTrack" }; + ScopedTrack track{ session }; { auto transaction{ session.createReadTransaction() }; @@ -187,8 +187,8 @@ namespace lms::db::tests { ScopedUser user{ session, "MyUser" }; ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() }; - ScopedTrack track1{ session, "MyTrack" }; - ScopedTrack track2{ session, "MyTrack" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; { auto transaction{ session.createWriteTransaction() }; diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 49859f4e..c3a04176 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -63,7 +63,7 @@ namespace lms::cover res = TrackInfo{}; res->hasCover = track->hasCover(); - res->trackPath = track->getPath(); + res->trackPath = track->getAbsoluteFilePath(); if (const db::Release::pointer & release{ track->getRelease() }) { @@ -366,7 +366,7 @@ namespace lms::cover const Track::pointer& track{ tracks.results.front() }; res = ReleaseInfo{}; res->firstTrackId = track->getId(); - res->releaseDirectory = track->getPath().parent_path(); + res->releaseDirectory = track->getAbsoluteFilePath().parent_path(); } return res; @@ -424,9 +424,9 @@ namespace lms::cover const auto releaseArtists{ Artist::findIds(session, artistFindParams) }; if (releaseArtists.results.size() == 1) - releasePaths.insert(track->getPath().parent_path()); + releasePaths.insert(track->getAbsoluteFilePath().parent_path()); else - multiArtistReleasePaths.insert(track->getPath().parent_path()); + multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path()); }); } diff --git a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp index 938af789..10f30e14 100644 --- a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp @@ -45,7 +45,7 @@ namespace lms::scanner const Track::pointer track{ Track::find(session, trackId) }; if (auto trackMBID{ track->getTrackMBID() }) { - LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName()); + LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getAbsoluteFilePath().string() << " - " << track->getName()); context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID }); context.currentStepStats.processedElems++; _progressCallback(context.currentStepStats); diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp index a0a30185..64009ebe 100644 --- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp @@ -114,7 +114,7 @@ namespace lms::scanner { endReached = false; - if (!checkFile(track->getPath())) + if (!checkFile(track->getAbsoluteFilePath())) tracksToRemove.push_back(track); context.currentStepStats.processedElems++; diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index c2fad403..bdd950ad 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -42,6 +42,66 @@ namespace lms::scanner namespace { + struct FileInfo + { + Wt::WDateTime lastWriteTime; + std::filesystem::path relativePath; + std::size_t fileSize{}; + }; + + Wt::WDateTime retrieveFileGetLastWrite(const std::filesystem::path& file) + { + Wt::WDateTime res; + + try + { + res = core::pathUtils::getLastWriteTime(file); + } + catch (core::LmsException& e) + { + LMS_LOG(DBUPDATER, ERROR, "Cannot get last write time: " << e.what()); + } + + return res; + } + + std::optional retrieveFileInfo(const std::filesystem::path& file, const std::filesystem::path& rootPath) + { + std::optional res; + res.emplace(); + + res->lastWriteTime = retrieveFileGetLastWrite(file); + if (!res->lastWriteTime.isValid()) + { + res.reset(); + return res; + } + + { + std::error_code ec; + res->relativePath = std::filesystem::relative(file, rootPath, ec); + if (ec) + { + LMS_LOG(DBUPDATER, ERROR, "Cannot get relative file path for '" << file.string() << "' from '" << rootPath.string() << "': " << ec.message()); + res.reset(); + return res; + } + } + + { + std::error_code ec; + res->fileSize = std::filesystem::file_size(file, ec); + if (ec) + { + LMS_LOG(DBUPDATER, ERROR, "Cannot get file size for '" << file.string() << "': " << ec.message()); + res.reset(); + return res; + } + } + + return res; + } + Artist::pointer createArtist(Session& session, const metadata::Artist& artistInfo) { Artist::pointer artist{ session.create(artistInfo.name) }; @@ -389,14 +449,11 @@ namespace lms::scanner bool ScanStepScanFiles::checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo) { ScanStats& stats{ context.stats }; - Wt::WDateTime lastWriteTime; - try + + Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) }; + // Should rarely fail as we are currently iterating it + if (!lastWriteTime.isValid()) { - lastWriteTime = core::pathUtils::getLastWriteTime(file); - } - catch (core::LmsException& e) - { - LMS_LOG(DBUPDATER, ERROR, e.what()); stats.skips++; return false; } @@ -416,7 +473,7 @@ namespace lms::scanner ) { // this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan - auto trackMediaLibrary{ track->getMediaLibrary() }; + const auto trackMediaLibrary{ track->getMediaLibrary() }; if (trackMediaLibrary && trackMediaLibrary->getId() == libraryInfo.id) { stats.skips++; @@ -451,6 +508,8 @@ namespace lms::scanner for (const MetaDataScanResult& scanResult : scanResults) { + LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessScanResult"); + if (_abortScan) return; @@ -474,14 +533,10 @@ namespace lms::scanner void ScanStepScanFiles::processFileMetaData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo) { ScanStats& stats{ context.stats }; - Wt::WDateTime lastWriteTime; - try + + const std::optional fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) }; + if (!fileInfo) { - lastWriteTime = core::pathUtils::getLastWriteTime(file); - } - catch (core::LmsException& e) - { - LMS_LOG(DBUPDATER, ERROR, e.what()); stats.skips++; return; } @@ -493,23 +548,23 @@ namespace lms::scanner { std::vector duplicateTracks{ Track::findByMBID(dbSession, *trackMetadata.mbid) }; - // find for existing MBIDs as the file may have just been moved + // find for an existing track MBID as the file may have just been moved if (!track && duplicateTracks.size() == 1) { Track::pointer otherTrack{ duplicateTracks.front() }; std::error_code ec; - if (!std::filesystem::exists(otherTrack->getPath(), ec)) + if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec)) { - LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'"); + LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getAbsoluteFilePath() << "'"); track = otherTrack; - track.modify()->setPath(file); + track.modify()->setAbsoluteFilePath(file); } } // Skip duplicate track MBID if (_settings.skipDuplicateMBID) { - for (Track::pointer otherTrack : duplicateTracks) + for (Track::pointer& otherTrack : duplicateTracks) { // Skip ourselves if (track && track->getId() == otherTrack->getId()) @@ -525,7 +580,7 @@ namespace lms::scanner continue; } - LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')"); + LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getAbsoluteFilePath().string() << "')"); // As this MBID already exists, just remove what we just scanned if (track) { @@ -565,22 +620,21 @@ namespace lms::scanner // If file already exists, update its data // Otherwise, create it + bool added{}; if (!track) { - track = dbSession.create(file); - LMS_LOG(DBUPDATER, DEBUG, "Adding '" << file.string() << "'"); - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG, "Updating '" << file.string() << "'"); - - stats.updates++; + track = dbSession.create(); + track.modify()->setAbsoluteFilePath(file); + added = true; } // Track related data assert(track); + track.modify()->setRelativeFilePath(fileInfo->relativePath); + track.modify()->setFileSize(fileInfo->fileSize); + track.modify()->setLastWriteTime(fileInfo->lastWriteTime); + track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this track.modify()->clearArtistLinks(); // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files @@ -628,7 +682,6 @@ namespace lms::scanner track.modify()->setReleaseReplayGain(trackMetadata.medium ? trackMetadata.medium->replayGain : std::nullopt); track.modify()->setDiscSubtitle(trackMetadata.medium ? trackMetadata.medium->name : ""); track.modify()->setClusters(getOrCreateClusters(dbSession, trackMetadata)); - track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); track.modify()->setDuration(trackMetadata.duration); track.modify()->setBitrate(trackMetadata.bitrate); @@ -657,5 +710,16 @@ namespace lms::scanner track.modify()->setCopyrightURL(trackMetadata.copyrightURL); track.modify()->setTrackReplayGain(trackMetadata.replayGain); track.modify()->setArtistDisplayName(trackMetadata.artistDisplayName); + + if (added) + { + LMS_LOG(DBUPDATER, DEBUG, "Added '" << file.string() << "'"); + stats.additions++; + } + else + { + LMS_LOG(DBUPDATER, DEBUG, "Updated '" << file.string() << "'"); + stats.updates++; + } } } diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index 7d4ef6b2..b0f9b5d3 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -137,7 +137,7 @@ namespace lms::api::subsonic if (!track) throw RequestedDataNotFoundError{}; - parameters.inputParameters.trackPath = track->getPath(); + parameters.inputParameters.trackPath = track->getAbsoluteFilePath(); parameters.inputParameters.duration = track->getDuration(); parameters.estimateContentLength = estimateContentLength; @@ -161,7 +161,7 @@ namespace lms::api::subsonic // same codec => apply max bitrate // otherwise => apply default bitrate (because we can't really compare bitrates between formats) + max bitrate) std::size_t bitrate{}; - if (requestedFormat && isOutputFormatCompatible(track->getPath(), *requestedFormat)) + if (requestedFormat && isOutputFormatCompatible(track->getAbsoluteFilePath(), *requestedFormat)) { if (maxBitRate == 0 || track->getBitrate() <= maxBitRate) { @@ -205,7 +205,7 @@ namespace lms::api::subsonic if (!track) throw RequestedDataNotFoundError{}; - trackPath = track->getPath(); + trackPath = track->getAbsoluteFilePath(); } resourceHandler = av::createRawResourceHandler(trackPath); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 8ecb40f9..3b43b219 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -59,39 +59,6 @@ namespace lms::api::subsonic return ""; } - - std::string getTrackPath(const Track::pointer& track) - { - std::string path; - - // The track path has to be relative from the root - const auto release{ track->getRelease() }; - if (release) - { - auto artists{ release->getReleaseArtists() }; - if (artists.empty()) - artists = release->getArtists(); - - if (artists.size() > 1) - path = "Various Artists/"; - else if (artists.size() == 1) - path = utils::makeNameFilesystemCompatible(artists.front()->getName()) + "/"; - - path += utils::makeNameFilesystemCompatible(track->getRelease()->getName()) + "/"; - } - - if (track->getDiscNumber()) - path += std::to_string(*track->getDiscNumber()) + "-"; - if (track->getTrackNumber()) - path += std::to_string(*track->getTrackNumber()) + "-"; - - path += utils::makeNameFilesystemCompatible(track->getName()); - - if (track->getPath().has_extension()) - path += track->getPath().extension(); - - return path; - } } Response::Node createSongNode(RequestContext& context, const Track::pointer& track, const User::pointer& user) @@ -110,19 +77,13 @@ namespace lms::api::subsonic if (track->getYear()) trackResponse.setAttribute("year", *track->getYear()); trackResponse.setAttribute("playCount", core::Service::get()->getCount(user->getId(), track->getId())); - trackResponse.setAttribute("path", getTrackPath(track)); - { - // TODO, store this in DB - std::error_code ec; - const auto fileSize{ std::filesystem::file_size(track->getPath(), ec) }; - if (!ec) - trackResponse.setAttribute("size", fileSize); - } + trackResponse.setAttribute("path", track->getRelativeFilePath().string()); + trackResponse.setAttribute("size", track->getFileSize()); - if (track->getPath().has_extension()) + if (track->getAbsoluteFilePath().has_extension()) { - auto extension{ track->getPath().extension() }; - trackResponse.setAttribute("suffix", extension.string().substr(1)); + auto extension{ track->getAbsoluteFilePath().extension() }; + trackResponse.setAttribute("suffix", extension.string().substr(1) /* skip leading .*/); } { @@ -157,7 +118,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000)); trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten())); - trackResponse.setAttribute("contentType", av::getMimeType(track->getPath().extension())); + trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension())); if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 82f0c45b..65f08fd6 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -101,7 +101,7 @@ namespace lms::ui if (!track) continue; - response.out() << track->getPath().string(); + response.out() << track->getAbsoluteFilePath().string(); if (auto mbid{ track->getTrackMBID() }) response.out() << " (Track MBID " << mbid->getAsString() << ")"; diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 963bbbe0..7a5a1e12 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -134,7 +134,7 @@ namespace lms::ui // TODO: save in DB and aggregate all this for (const Track::pointer& track : Track::find(LmsApp->getDbSession(), Track::FindParameters{}.setRelease(releaseId).setRange(Range{ 0, 1 })).results) { - if (const auto audioFile{ av::parseAudioFile(track->getPath()) }) + if (const auto audioFile{ av::parseAudioFile(track->getAbsoluteFilePath()) }) { const std::optional audioStream{ audioFile->getBestStreamInfo() }; if (audioStream) diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 05cd698c..b2496405 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -119,7 +119,7 @@ namespace lms::ui::TrackListHelpers } } - if (const auto audioFile{ av::parseAudioFile(track->getPath()) }) + if (const auto audioFile{ av::parseAudioFile(track->getAbsoluteFilePath()) }) { const std::optional audioStream{ audioFile->getBestStreamInfo() }; if (audioStream) diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 012a4216..30310f23 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -48,7 +48,7 @@ namespace lms::ui return std::nullopt; } - return track->getPath(); + return track->getAbsoluteFilePath(); } std::optional getTrackPathFromURLArgs(const Wt::Http::Request& request) diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index 5523362e..7d002ee8 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -146,7 +146,7 @@ namespace lms::ui return std::nullopt; } - parameters.inputParameters.trackPath = track->getPath(); + parameters.inputParameters.trackPath = track->getAbsoluteFilePath(); parameters.inputParameters.duration = track->getDuration(); } diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 047e160a..943ac595 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -119,7 +119,7 @@ namespace lms::ui if (auto trackNumber{ track->getTrackNumber() }) fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - "; - fileName << core::stringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string(); + fileName << core::stringUtils::replaceInString(track->getName(), "/", "_") << track->getAbsoluteFilePath().extension().string(); return fileName.str(); } @@ -152,7 +152,7 @@ namespace lms::ui fileName += releaseName + "/"; fileName += getTrackPathName(track); - files.emplace_back(zip::Entry{ fileName, track->getPath() }); + files.emplace_back(zip::Entry{ fileName, track->getAbsoluteFilePath() }); } return zip::createArchiveZipper(files); diff --git a/src/tools/db-generator/LmsDbGenerator.cpp b/src/tools/db-generator/LmsDbGenerator.cpp index d59c43f2..0b0b0aad 100644 --- a/src/tools/db-generator/LmsDbGenerator.cpp +++ b/src/tools/db-generator/LmsDbGenerator.cpp @@ -96,7 +96,7 @@ namespace lms for (std::size_t i{}; i < params.trackCountPerRelease; ++i) { - Track::pointer track{ context.session.create(params.trackPath) }; + Track::pointer track{ context.session.create() }; track.modify()->setName("Track-" + std::string{ core::UUID::generate().getAsString() }); track.modify()->setDiscNumber(1); From 0cda12a8bfa795fc6732b2d91c78c234fbaebc17 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Apr 2024 16:17:45 +0200 Subject: [PATCH 36/64] Subsonic API: added bitDepth, samplingRate and channelCount, see https://github.com/opensubsonic/open-subsonic-api/pull/83 --- SUBSONIC.md | 6 +++ src/libs/av/impl/AudioFile.cpp | 3 ++ src/libs/av/include/av/IAudioFile.hpp | 3 ++ src/libs/database/impl/Migration.cpp | 13 +++++- src/libs/database/include/database/Track.hpp | 20 +++++++-- src/libs/database/test/Track.cpp | 24 ++++++++++ src/libs/metadata/impl/AvFormatTagReader.cpp | 11 +++++ src/libs/metadata/impl/AvFormatTagReader.hpp | 7 +-- src/libs/metadata/impl/ITagReader.hpp | 6 +-- src/libs/metadata/impl/Parser.cpp | 8 +--- src/libs/metadata/impl/Parser.hpp | 1 - src/libs/metadata/impl/TagLibTagReader.cpp | 45 ++++++++++--------- src/libs/metadata/impl/TagLibTagReader.hpp | 7 ++- .../metadata/include/metadata/IParser.hpp | 12 ++++- src/libs/metadata/test/Parser.cpp | 12 ++++- src/libs/metadata/test/TestTagReader.hpp | 17 +++---- .../scanner/impl/ScanStepScanFiles.cpp | 11 +++-- src/libs/subsonic/impl/responses/Song.cpp | 4 ++ src/tools/metadata/LmsMetadata.cpp | 16 +++++-- 19 files changed, 161 insertions(+), 65 deletions(-) diff --git a/SUBSONIC.md b/SUBSONIC.md index de749e9e..1361440c 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -11,6 +11,7 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l ## Extra fields The following extra fields are implemented: * `Album` response: + * `mediaType` * `played` * `musicBrainzId` * `genres` @@ -22,6 +23,10 @@ The following extra fields are implemented: * `isCompilation` * `discTitles`: discs with no subtitle are omitted * `Child` response: + * `bitDepth` + * `samplingRate` + * `channelCount` + * `mediaType` * `played` * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song * `genres` @@ -33,6 +38,7 @@ The following extra fields are implemented: * `moods` * `replayGain` * `Artist` response: + * `mediaType` * `musicBrainzId` * `sortName` * `roles` diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index 9184e6b6..d6f3e632 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -281,9 +281,12 @@ namespace lms::av res.emplace(); res->index = streamIndex; res->bitrate = static_cast(avstream->codecpar->bit_rate); + res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample); + res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels); res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id); res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id); assert(!res->codecName.empty()); // doc says it is never NULL + res->sampleRate = static_cast(avstream->codecpar->sample_rate); return res; } diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp index 3dc06edb..6d9915c9 100644 --- a/src/libs/av/include/av/IAudioFile.hpp +++ b/src/libs/av/include/av/IAudioFile.hpp @@ -75,6 +75,9 @@ namespace lms::av { size_t index{}; std::size_t bitrate{}; + std::size_t bitsPerSample{}; + std::size_t channelCount{}; + std::size_t sampleRate{}; DecodingCodec codec; std::string codecName; }; diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 9cb0a616..127ca117 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -34,7 +34,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 55 }; + static constexpr Version LMS_DATABASE_VERSION{ 56 }; } VersionInfo::VersionInfo() @@ -441,6 +441,16 @@ SELECT session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV55(Session& session) + { + // Add bitsPerSample, channelCount and sampleRate + session.getDboSession()->execute("ALTER TABLE track ADD bits_per_sample INTEGER NOT NULL DEFAULT(0)"); + session.getDboSession()->execute("ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)"); + session.getDboSession()->execute("ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } void doDbMigration(Session& session) { @@ -474,6 +484,7 @@ SELECT {52, migrateFromV52}, {53, migrateFromV53}, {54, migrateFromV54}, + {55, migrateFromV55}, }; { diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index cc1999de..84cde313 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -133,10 +133,13 @@ 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 setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setAddedTime(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; } + 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; } @@ -164,8 +167,11 @@ namespace lms::db const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; } const std::filesystem::path& getRelativeFilePath() const { return _relativeFilePath; } long long getFileSize() const { return _fileSize; } - std::chrono::milliseconds getDuration() const { return _duration; } std::size_t getBitrate() const { return _bitrate; } + std::size_t getBitsPerSample() const { return _bitsPerSample; } + std::size_t getChannelCount() const { return _channelCount; } + 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; } @@ -203,6 +209,9 @@ namespace lms::db Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _bitrate, "bitrate"); + Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample"); + 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"); @@ -240,8 +249,11 @@ namespace lms::db std::optional _totalTrack{}; std::string _discSubtitle; std::string _name; - std::chrono::duration _duration{}; int _bitrate{}; // in bps + int _bitsPerSample{}; + int _channelCount{}; + std::chrono::duration _duration{}; + int _sampleRate{}; Wt::WDate _date; std::optional _year; Wt::WDate _originalDate; diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 592e1f2a..ec3a6b77 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -342,4 +342,28 @@ namespace lms::db::tests EXPECT_EQ(track->getRelativeFilePath(), "foo/file.path"); } } + + + TEST_F(DatabaseFixture, Track_audioProperties) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setBitrate(128000); + track.get().modify()->setBitsPerSample(16); + track.get().modify()->setDuration(std::chrono::minutes{ 3 }); + track.get().modify()->setChannelCount(2); + track.get().modify()->setSampleRate(44100); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getBitrate(), 128000); + EXPECT_EQ(track->getBitsPerSample(), 16); + EXPECT_EQ(track->getDuration(), std::chrono::minutes{ 3 }); + EXPECT_EQ(track->getChannelCount(), 2); + EXPECT_EQ(track->getSampleRate(), 44100); + } + } } \ No newline at end of file diff --git a/src/libs/metadata/impl/AvFormatTagReader.cpp b/src/libs/metadata/impl/AvFormatTagReader.cpp index 8d487c94..1f4852e7 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.cpp +++ b/src/libs/metadata/impl/AvFormatTagReader.cpp @@ -144,6 +144,17 @@ namespace lms::metadata try { const auto audioFile{ av::parseAudioFile(p) }; + + _audioProperties.duration = audioFile->getContainerInfo().duration; + + const auto bestAudioStream{ audioFile->getBestStreamInfo() }; + if (bestAudioStream) + { + _audioProperties.bitrate = bestAudioStream->bitrate; + _audioProperties.bitsPerSample = bestAudioStream->bitsPerSample; + _audioProperties.channelCount = bestAudioStream->channelCount; + _audioProperties.sampleRate = bestAudioStream->sampleRate; + } _containerInfo = audioFile->getContainerInfo(); _metaDataMap = audioFile->getMetaData(); diff --git a/src/libs/metadata/impl/AvFormatTagReader.hpp b/src/libs/metadata/impl/AvFormatTagReader.hpp index db7bddde..857de190 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.hpp +++ b/src/libs/metadata/impl/AvFormatTagReader.hpp @@ -40,12 +40,9 @@ namespace lms::metadata void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; bool hasEmbeddedCover() const override { return _hasEmbeddedCover; } + const AudioProperties& getAudioProperties() const override { return _audioProperties; } - std::chrono::milliseconds getDuration() const override { return _containerInfo.duration; } - std::size_t getBitrate() const override { return _containerInfo.bitrate; } - std::size_t getBitsPerSample() const override { return 0; } - std::size_t getSampleRate() const override { return 0; } - + AudioProperties _audioProperties; av::IAudioFile::MetadataMap _metaDataMap; av::ContainerInfo _containerInfo; bool _hasEmbeddedCover{}; diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/metadata/impl/ITagReader.hpp index e426b691..ee80933a 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/metadata/impl/ITagReader.hpp @@ -153,10 +153,6 @@ namespace lms::metadata virtual void visitPerformerTags(PerformerVisitor visitor) const = 0; virtual bool hasEmbeddedCover() const = 0; - - virtual std::chrono::milliseconds getDuration() const = 0; - virtual std::size_t getBitrate() const = 0; - virtual std::size_t getBitsPerSample() const = 0; - virtual std::size_t getSampleRate() const = 0; + virtual const AudioProperties& getAudioProperties() const = 0; }; } // namespace lms::metadata diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp index 6a0a30e3..bfdb38e4 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/Parser.cpp @@ -235,18 +235,12 @@ namespace lms::metadata { auto track{ std::make_unique() }; - processAudioProperties(tagReader, *track); + track->audioProperties = tagReader.getAudioProperties(); processTags(tagReader, *track); return track; } - void Parser::processAudioProperties(const ITagReader& tagReader, Track& track) - { - track.duration = tagReader.getDuration(); - track.bitrate = tagReader.getBitrate(); - } - void Parser::processTags(const ITagReader& tagReader, Track& track) { track.hasCover = tagReader.hasEmbeddedCover(); diff --git a/src/libs/metadata/impl/Parser.hpp b/src/libs/metadata/impl/Parser.hpp index ffbd89c1..87bd8f21 100644 --- a/src/libs/metadata/impl/Parser.hpp +++ b/src/libs/metadata/impl/Parser.hpp @@ -37,7 +37,6 @@ namespace lms::metadata void setArtistTagDelimiters(std::span delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); } void setDefaultTagDelimiters(std::span delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); } - void processAudioProperties(const ITagReader& reader, Track& track); void processTags(const ITagReader& reader, Track& track); std::optional getMedium(const ITagReader& tagReader); diff --git a/src/libs/metadata/impl/TagLibTagReader.cpp b/src/libs/metadata/impl/TagLibTagReader.cpp index 340d6a11..8799a353 100644 --- a/src/libs/metadata/impl/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/TagLibTagReader.cpp @@ -21,6 +21,7 @@ #include +#include #include #include #include @@ -198,6 +199,8 @@ namespace lms::metadata throw ParsingFailedException{}; } + computeAudioProperties(); + _propertyMap = _file.file()->properties(); // Some tags may not be known by TagLib @@ -305,6 +308,28 @@ namespace lms::metadata } } + void TagLibTagReader::computeAudioProperties() + { + const TagLib::AudioProperties* properties{ _file.audioProperties() }; + + // Common properties + _audioProperties.bitrate = static_cast(properties->bitrate() * 1000); + _audioProperties.channelCount = static_cast(_file.audioProperties()->channels()); + _audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; + _audioProperties.sampleRate = static_cast(properties->sampleRate()); + + if (const auto * apeProperties{ dynamic_cast(properties) }) + _audioProperties.bitsPerSample = apeProperties->bitsPerSample(); + if (const auto * asfProperties{ dynamic_cast(properties) }) + _audioProperties.bitsPerSample = asfProperties->bitsPerSample(); + else if (const auto * flacProperties{ dynamic_cast(properties) }) + _audioProperties.bitsPerSample = flacProperties->bitsPerSample(); + else if (const auto * mp4Properties{ dynamic_cast(properties) }) + _audioProperties.bitsPerSample = mp4Properties->bitsPerSample(); + else if (const auto * wavePackProperties{ dynamic_cast(properties) }) + _audioProperties.bitsPerSample = wavePackProperties->bitsPerSample(); + } + void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const { auto itTagNames{ tagMapping.find(tag) }; @@ -362,24 +387,4 @@ namespace lms::metadata } } } - - std::chrono::milliseconds TagLibTagReader::getDuration() const - { - return std::chrono::milliseconds{ _file.audioProperties()->lengthInMilliseconds() }; - } - - std::size_t TagLibTagReader::getBitrate() const - { - return static_cast(_file.audioProperties()->bitrate() * 1000); - } - - std::size_t TagLibTagReader::getBitsPerSample() const - { - return 0; // TODO - } - - std::size_t TagLibTagReader::getSampleRate() const - { - return static_cast(_file.audioProperties()->sampleRate()); - } } // namespace lms::metadata diff --git a/src/libs/metadata/impl/TagLibTagReader.hpp b/src/libs/metadata/impl/TagLibTagReader.hpp index 6aaa3476..3ada79af 100644 --- a/src/libs/metadata/impl/TagLibTagReader.hpp +++ b/src/libs/metadata/impl/TagLibTagReader.hpp @@ -38,17 +38,16 @@ namespace lms::metadata TagLibTagReader(const TagLibTagReader&) = delete; TagLibTagReader& operator=(const TagLibTagReader&) = delete; + void computeAudioProperties(); void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; bool hasEmbeddedCover() const override { return _hasEmbeddedCover; } - std::chrono::milliseconds getDuration() const override; - std::size_t getBitrate() const override; - std::size_t getBitsPerSample() const override; - std::size_t getSampleRate() const override; + const AudioProperties& getAudioProperties() const override { return _audioProperties; } const TagLib::FileRef _file; + AudioProperties _audioProperties; TagLib::PropertyMap _propertyMap; // case-insensitive keys bool _hasEmbeddedCover{}; }; diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 568a6287..821b99a9 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -83,8 +83,18 @@ namespace lms::metadata } }; + struct AudioProperties + { + std::size_t bitrate{}; + std::size_t bitsPerSample{}; + std::size_t channelCount{}; + std::chrono::milliseconds duration{}; + std::size_t sampleRate{}; + }; + struct Track { + AudioProperties audioProperties; std::optional mbid; std::optional recordingMBID; std::string title; @@ -96,8 +106,6 @@ namespace lms::metadata std::vector labels; std::vector languages; Tags userExtraTags; - std::chrono::milliseconds duration{}; - std::size_t bitrate{}; std::optional year{}; Wt::WDate date; std::optional originalYear{}; diff --git a/src/libs/metadata/test/Parser.cpp b/src/libs/metadata/test/Parser.cpp index 45ddd48e..3c6bf9db 100644 --- a/src/libs/metadata/test/Parser.cpp +++ b/src/libs/metadata/test/Parser.cpp @@ -88,6 +88,16 @@ namespace lms::metadata std::unique_ptr track{ parser.parse(testTags) }; + // Audio properties + { + const AudioProperties& audioProperties{ testTags.getAudioProperties() }; + EXPECT_EQ(track->audioProperties.bitrate, audioProperties.bitrate); + EXPECT_EQ(track->audioProperties.bitsPerSample, audioProperties.bitsPerSample); + EXPECT_EQ(track->audioProperties.channelCount, audioProperties.channelCount); + EXPECT_EQ(track->audioProperties.duration, audioProperties.duration); + EXPECT_EQ(track->audioProperties.sampleRate, audioProperties.sampleRate); + } + EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418")); EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2"); ASSERT_EQ(track->artists.size(), 2); @@ -97,7 +107,6 @@ namespace lms::metadata EXPECT_EQ(track->artists[1].name, "MyArtist2"); EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName"); EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229")); - EXPECT_EQ(track->bitrate, TestTagReader::trackBitrate); ASSERT_EQ(track->composerArtists.size(), 2); EXPECT_EQ(track->composerArtists[0].name, "MyComposer1"); EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1"); @@ -112,7 +121,6 @@ namespace lms::metadata EXPECT_EQ(track->date.year(), 2020); EXPECT_EQ(track->date.month(), 3); EXPECT_EQ(track->date.day(), 4); - EXPECT_EQ(track->duration, TestTagReader::trackDuration); EXPECT_FALSE(track->hasCover); ASSERT_EQ(track->genres.size(), 2); EXPECT_EQ(track->genres[0], "Genre1"); diff --git a/src/libs/metadata/test/TestTagReader.hpp b/src/libs/metadata/test/TestTagReader.hpp index b7aca082..e7f968f4 100644 --- a/src/libs/metadata/test/TestTagReader.hpp +++ b/src/libs/metadata/test/TestTagReader.hpp @@ -28,10 +28,14 @@ namespace lms::metadata class TestTagReader : public ITagReader { public: - static constexpr std::chrono::milliseconds trackDuration{ 180 }; - static constexpr std::size_t trackBitrate{ 128000 }; - static constexpr std::size_t trackBitsPerSample{ 16 }; - static constexpr std::size_t trackSampleRate{ 44000 }; + static constexpr AudioProperties audioProperties + { + .bitrate = 128000, + .bitsPerSample = 16, + .channelCount = 2, + .duration = std::chrono::seconds{180}, + .sampleRate = 44000, + }; using Tags = std::unordered_map>; using Performers = std::unordered_map>; @@ -73,10 +77,7 @@ namespace lms::metadata bool hasEmbeddedCover() const override { return false; }; - std::chrono::milliseconds getDuration() const override { return trackDuration; } - std::size_t getBitrate() const override { return trackBitrate; } - std::size_t getBitsPerSample() const override { return trackBitsPerSample; } - std::size_t getSampleRate() const override { return trackSampleRate; } + const AudioProperties& getAudioProperties() const override { return audioProperties; } private: const Tags _tags; diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index bdd950ad..cd2606a7 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -593,7 +593,7 @@ namespace lms::scanner } // We estimate this is an audio file if the duration is not null - if (trackMetadata.duration == std::chrono::milliseconds::zero()) + if (trackMetadata.audioProperties.duration == std::chrono::milliseconds::zero()) { LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)"); @@ -631,6 +631,13 @@ namespace lms::scanner // Track related data assert(track); + // Audio properties + track.modify()->setBitrate(trackMetadata.audioProperties.bitrate); + track.modify()->setBitsPerSample(trackMetadata.audioProperties.bitsPerSample); + track.modify()->setChannelCount(trackMetadata.audioProperties.channelCount); + track.modify()->setDuration(trackMetadata.audioProperties.duration); + track.modify()->setSampleRate(trackMetadata.audioProperties.sampleRate); + track.modify()->setRelativeFilePath(fileInfo->relativePath); track.modify()->setFileSize(fileInfo->fileSize); track.modify()->setLastWriteTime(fileInfo->lastWriteTime); @@ -683,8 +690,6 @@ namespace lms::scanner track.modify()->setDiscSubtitle(trackMetadata.medium ? trackMetadata.medium->name : ""); track.modify()->setClusters(getOrCreateClusters(dbSession, trackMetadata)); track.modify()->setName(title); - track.modify()->setDuration(trackMetadata.duration); - track.modify()->setBitrate(trackMetadata.bitrate); track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); track.modify()->setTrackNumber(trackMetadata.position); track.modify()->setDiscNumber(trackMetadata.medium ? trackMetadata.medium->position : std::nullopt); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 3b43b219..f7a8a1aa 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -139,6 +139,10 @@ namespace lms::api::subsonic if (!context.enableOpenSubsonic) return trackResponse; + trackResponse.setAttribute("bitDepth", track->getBitsPerSample()); + trackResponse.setAttribute("samplingRate", track->getSampleRate()); + trackResponse.setAttribute("channelCount", track->getChannelCount()); + trackResponse.setAttribute("mediaType", "song"); { diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 6d6f41da..4a5a836e 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -32,6 +32,17 @@ namespace lms::metadata { + std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) + { + os << "\tBitrate: " << audioProperties.bitrate << " bps" << std::endl; + os << "\tBitsPerSample: " << audioProperties.bitsPerSample << std::endl; + os << "\tChannelCount: " << audioProperties.channelCount << std::endl; + os << "\tDuration: " << std::fixed << std::setprecision(2) << audioProperties.duration.count() / 1000. << "s" << std::endl; + os << "\tSampleRate: " << audioProperties.sampleRate << std::endl; + + return os; + } + std::ostream& operator<<(std::ostream& os, const Artist& artist) { os << artist.name; @@ -107,6 +118,8 @@ namespace lms::metadata std::cout << "Parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; + std::cout << "Audio properties:\n" << track->audioProperties << std::endl; + std::cout << "Parsed metadata:" << std::endl; if (!track->artistDisplayName.empty()) @@ -178,9 +191,6 @@ namespace lms::metadata } } - std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl; - std::cout << "Bitrate: " << track->bitrate << " bps" << std::endl; - if (track->position) std::cout << "Position: " << *track->position << std::endl; From e3a6364429b0f429628ffb27979a6499914efda7 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Apr 2024 23:54:26 +0200 Subject: [PATCH 37/64] Fixed build on ubuntu 22.04 --- src/libs/av/impl/AudioFile.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index d6f3e632..6c4948dc 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -282,7 +282,11 @@ namespace lms::av res->index = streamIndex; res->bitrate = static_cast(avstream->codecpar->bit_rate); res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample); +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100) + res->channelCount = static_cast(avstream->codecpar->channels); +#else res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels); +#endif res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id); res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id); assert(!res->codecName.empty()); // doc says it is never NULL From 29289c6026c2a70d9cbc7cc39980e8de743466d4 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Apr 2024 14:06:23 +0200 Subject: [PATCH 38/64] Upgraded boostrap from 5.1.3 to 5.3.3, ref #444 --- docroot/css/bootstrap.solar.min.css | 16 ++++++++-------- docroot/js/bootstrap.bundle.min.js | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) mode change 100644 => 100755 docroot/js/bootstrap.bundle.min.js diff --git a/docroot/css/bootstrap.solar.min.css b/docroot/css/bootstrap.solar.min.css index 78dceb24..26836e52 100755 --- a/docroot/css/bootstrap.solar.min.css +++ b/docroot/css/bootstrap.solar.min.css @@ -1,12 +1,12 @@ -/*! - * Bootswatch v5.1.3 - * Homepage: https://bootswatch.com - * Copyright 2012-2021 Thomas Park +@charset "UTF-8";/*! + * Bootswatch v5.3.3 (https://bootswatch.com) + * Theme: solar + * Copyright 2012-2024 Thomas Park * Licensed under MIT * Based on Bootstrap *//*! - * Bootstrap v5.1.3 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */@import url(https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap);:root{--bs-blue:#b58900;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#e83e8c;--bs-red:#d33682;--bs-orange:#fd7e14;--bs-yellow:#cb4b16;--bs-green:#2aa198;--bs-teal:#20c997;--bs-cyan:#268bd2;--bs-white:#fff;--bs-gray:#839496;--bs-gray-dark:#073642;--bs-gray-100:#fdf6e3;--bs-gray-200:#eee8d5;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#839496;--bs-gray-700:#495057;--bs-gray-800:#073642;--bs-gray-900:#002b36;--bs-primary:#b58900;--bs-secondary:#839496;--bs-success:#2aa198;--bs-info:#268bd2;--bs-warning:#cb4b16;--bs-danger:#d33682;--bs-light:#fdf6e3;--bs-dark:#073642;--bs-primary-rgb:181,137,0;--bs-secondary-rgb:131,148,150;--bs-success-rgb:42,161,152;--bs-info-rgb:38,139,210;--bs-warning-rgb:203,75,22;--bs-danger-rgb:211,54,130;--bs-light-rgb:253,246,227;--bs-dark-rgb:7,54,66;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:131,148,150;--bs-body-bg-rgb:0,43,54;--bs-font-sans-serif:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#839496;--bs-body-bg:#002b36}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#2aa198;text-decoration:underline}a:hover{color:#2aa198}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#002b36;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#839496;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#839496}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#002b36;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#839496}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{-ms-flex-negative:0;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-sm-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-sm-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-sm-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-sm-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-sm-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-sm-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-md-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-md-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-md-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-md-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-md-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-md-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-lg-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-lg-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-lg-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-lg-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-lg-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-lg-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-xl-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-xl-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-xl-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-xl-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-xl-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-xl-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-xxl-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-xxl-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-xxl-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-xxl-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-xxl-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-xxl-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-xxl-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xxl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xxl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-xxl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xxl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-xxl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-xxl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-xxl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-xxl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-xxl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-xxl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-xxl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-xxl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-xxl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.333333%}.offset-xxl-2{margin-left:16.666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.333333%}.offset-xxl-5{margin-left:41.666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.333333%}.offset-xxl-8{margin-left:66.666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.333333%}.offset-xxl-11{margin-left:91.666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:rgba(255, 255, 255, 0.05);--bs-table-striped-color:#839496;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#839496;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#839496;--bs-table-hover-bg:rgba(255, 255, 255, 0.075);width:100%;margin-bottom:1rem;color:#839496;vertical-align:top;border-color:#073642}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#b58900;--bs-table-striped-bg:#b98f0d;--bs-table-striped-color:#fff;--bs-table-active-bg:#bc951a;--bs-table-active-color:#fff;--bs-table-hover-bg:#bb9213;--bs-table-hover-color:#fff;color:#fff;border-color:#bc951a}.table-secondary{--bs-table-bg:#839496;--bs-table-striped-bg:#89999b;--bs-table-striped-color:#fff;--bs-table-active-bg:#8f9fa1;--bs-table-active-color:#fff;--bs-table-hover-bg:#8c9c9e;--bs-table-hover-color:#fff;color:#fff;border-color:#8f9fa1}.table-success{--bs-table-bg:#2aa198;--bs-table-striped-bg:#35a69d;--bs-table-striped-color:#fff;--bs-table-active-bg:#3faaa2;--bs-table-active-color:#fff;--bs-table-hover-bg:#3aa8a0;--bs-table-hover-color:#fff;color:#fff;border-color:#3faaa2}.table-info{--bs-table-bg:#268bd2;--bs-table-striped-bg:#3191d4;--bs-table-striped-color:#fff;--bs-table-active-bg:#3c97d7;--bs-table-active-color:#fff;--bs-table-hover-bg:#3694d5;--bs-table-hover-color:#fff;color:#fff;border-color:#3c97d7}.table-warning{--bs-table-bg:#cb4b16;--bs-table-striped-bg:#ce5422;--bs-table-striped-color:#fff;--bs-table-active-bg:#d05d2d;--bs-table-active-color:#fff;--bs-table-hover-bg:#cf5927;--bs-table-hover-color:#fff;color:#fff;border-color:#d05d2d}.table-danger{--bs-table-bg:#d33682;--bs-table-striped-bg:#d54088;--bs-table-striped-color:#fff;--bs-table-active-bg:#d74a8f;--bs-table-active-color:#fff;--bs-table-hover-bg:#d6458b;--bs-table-hover-color:#fff;color:#fff;border-color:#d74a8f}.table-light{--bs-table-bg:#fdf6e3;--bs-table-striped-bg:#f0ead8;--bs-table-striped-color:#000;--bs-table-active-bg:#e4ddcc;--bs-table-active-color:#000;--bs-table-hover-bg:#eae4d2;--bs-table-hover-color:#000;color:#000;border-color:#e4ddcc}.table-dark{--bs-table-bg:#073642;--bs-table-striped-bg:#13404b;--bs-table-striped-color:#fff;--bs-table-active-bg:#204a55;--bs-table-active-color:#fff;--bs-table-hover-bg:#1a4550;--bs-table-hover-color:#fff;color:#fff;border-color:#204a55}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#839496}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#073642;background-color:#a9bdbd;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#073642;background-color:#a9bdbd;border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-webkit-input-placeholder{color:#657b83;opacity:1}.form-control::-moz-placeholder{color:#657b83;opacity:1}.form-control:-ms-input-placeholder{color:#657b83;opacity:1}.form-control::-ms-input-placeholder{color:#657b83;opacity:1}.form-control::placeholder{color:#657b83;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#657b83;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;-moz-margin-end:.75rem;margin-inline-end:.75rem;color:#839496;background-color:#073642;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#07333f}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#839496;background-color:#073642;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#07333f}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#839496;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;-moz-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;-moz-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#073642;background-color:#a9bdbd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23073642' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#eee8d5}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #073642}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:rgba(255,255,255,.75);background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid #fff;-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{-webkit-filter:brightness(90%);filter:brightness(90%)}.form-check-input:focus{border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25)}.form-check-input:checked{background-color:#b58900;border-color:#b58900}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#073642;border-color:#073642;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;-webkit-filter:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;-webkit-filter:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #002b36,0 0 0 .25rem rgba(7,54,66,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #002b36,0 0 0 .25rem rgba(7,54,66,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#b58900;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b5c3c6}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#073642;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#b58900;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b5c3c6}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#073642;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;-webkit-transform-origin:0 0;transform-origin:0 0;transition:opacity .1s ease-in-out,-webkit-transform .1s ease-in-out;transition:opacity .1s ease-in-out,transform .1s ease-in-out;transition:opacity .1s ease-in-out,transform .1s ease-in-out,-webkit-transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-webkit-input-placeholder{color:transparent}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control:-ms-input-placeholder{color:transparent}.form-floating>.form-control::-ms-input-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-ms-input-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-ms-input-placeholder)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;-webkit-transform:scale(.85) translateY(-.5rem) translateX(.15rem);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;-webkit-transform:scale(.85) translateY(-.5rem) translateX(.15rem);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#839496;text-align:center;white-space:nowrap;background-color:#073642;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#2aa198}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(42,161,152,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#2aa198;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%232aa198' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#2aa198;box-shadow:0 0 0 .25rem rgba(42,161,152,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#2aa198}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23073642' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%232aa198' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#2aa198;box-shadow:0 0 0 .25rem rgba(42,161,152,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#2aa198}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#2aa198}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(42,161,152,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#2aa198}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#d33682}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(211,54,130,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#d33682;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d33682'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d33682' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#d33682;box-shadow:0 0 0 .25rem rgba(211,54,130,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#d33682}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23073642' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d33682'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d33682' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#d33682;box-shadow:0 0 0 .25rem rgba(211,54,130,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#d33682}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#d33682}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(211,54,130,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#d33682}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#839496;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#839496}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#b58900;border-color:#b58900}.btn-primary:hover{color:#fff;background-color:#9a7400;border-color:#916e00}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#9a7400;border-color:#916e00;box-shadow:0 0 0 .25rem rgba(192,155,38,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#916e00;border-color:#886700}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(192,155,38,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#b58900;border-color:#b58900}.btn-secondary{color:#fff;background-color:#839496;border-color:#839496}.btn-secondary:hover{color:#fff;background-color:#6f7e80;border-color:#697678}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#6f7e80;border-color:#697678;box-shadow:0 0 0 .25rem rgba(150,164,166,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#697678;border-color:#626f71}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(150,164,166,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#839496;border-color:#839496}.btn-success{color:#fff;background-color:#2aa198;border-color:#2aa198}.btn-success:hover{color:#fff;background-color:#248981;border-color:#22817a}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#248981;border-color:#22817a;box-shadow:0 0 0 .25rem rgba(74,175,167,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#22817a;border-color:#207972}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(74,175,167,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#2aa198;border-color:#2aa198}.btn-info{color:#fff;background-color:#268bd2;border-color:#268bd2}.btn-info:hover{color:#fff;background-color:#2076b3;border-color:#1e6fa8}.btn-check:focus+.btn-info,.btn-info:focus{color:#fff;background-color:#2076b3;border-color:#1e6fa8;box-shadow:0 0 0 .25rem rgba(71,156,217,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#1e6fa8;border-color:#1d689e}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(71,156,217,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#268bd2;border-color:#268bd2}.btn-warning{color:#fff;background-color:#cb4b16;border-color:#cb4b16}.btn-warning:hover{color:#fff;background-color:#ad4013;border-color:#a23c12}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#fff;background-color:#ad4013;border-color:#a23c12;box-shadow:0 0 0 .25rem rgba(211,102,57,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#a23c12;border-color:#983811}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,102,57,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#cb4b16;border-color:#cb4b16}.btn-danger{color:#fff;background-color:#d33682;border-color:#d33682}.btn-danger:hover{color:#fff;background-color:#b32e6f;border-color:#a92b68}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#b32e6f;border-color:#a92b68;box-shadow:0 0 0 .25rem rgba(218,84,149,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#a92b68;border-color:#9e2962}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(218,84,149,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#d33682;border-color:#d33682}.btn-light{color:#000;background-color:#fdf6e3;border-color:#fdf6e3}.btn-light:hover{color:#000;background-color:#fdf7e7;border-color:#fdf7e6}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#fdf7e7;border-color:#fdf7e6;box-shadow:0 0 0 .25rem rgba(215,209,193,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#fdf8e9;border-color:#fdf7e6}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(215,209,193,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#fdf6e3;border-color:#fdf6e3}.btn-dark{color:#fff;background-color:#073642;border-color:#073642}.btn-dark:hover{color:#fff;background-color:#062e38;border-color:#062b35}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#062e38;border-color:#062b35;box-shadow:0 0 0 .25rem rgba(44,84,94,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#062b35;border-color:#052932}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(44,84,94,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#073642;border-color:#073642}.btn-outline-primary{color:#b58900;border-color:#b58900}.btn-outline-primary:hover{color:#fff;background-color:#b58900;border-color:#b58900}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(181,137,0,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#b58900;border-color:#b58900}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(181,137,0,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#b58900;background-color:transparent}.btn-outline-secondary{color:#839496;border-color:#839496}.btn-outline-secondary:hover{color:#fff;background-color:#839496;border-color:#839496}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(131,148,150,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#839496;border-color:#839496}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(131,148,150,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#839496;background-color:transparent}.btn-outline-success{color:#2aa198;border-color:#2aa198}.btn-outline-success:hover{color:#fff;background-color:#2aa198;border-color:#2aa198}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(42,161,152,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#2aa198;border-color:#2aa198}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(42,161,152,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#2aa198;background-color:transparent}.btn-outline-info{color:#268bd2;border-color:#268bd2}.btn-outline-info:hover{color:#fff;background-color:#268bd2;border-color:#268bd2}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(38,139,210,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#fff;background-color:#268bd2;border-color:#268bd2}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(38,139,210,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#268bd2;background-color:transparent}.btn-outline-warning{color:#cb4b16;border-color:#cb4b16}.btn-outline-warning:hover{color:#fff;background-color:#cb4b16;border-color:#cb4b16}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(203,75,22,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#fff;background-color:#cb4b16;border-color:#cb4b16}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(203,75,22,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#cb4b16;background-color:transparent}.btn-outline-danger{color:#d33682;border-color:#d33682}.btn-outline-danger:hover{color:#fff;background-color:#d33682;border-color:#d33682}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(211,54,130,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#d33682;border-color:#d33682}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(211,54,130,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d33682;background-color:transparent}.btn-outline-light{color:#fdf6e3;border-color:#fdf6e3}.btn-outline-light:hover{color:#000;background-color:#fdf6e3;border-color:#fdf6e3}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(253,246,227,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#fdf6e3;border-color:#fdf6e3}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(253,246,227,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#fdf6e3;background-color:transparent}.btn-outline-dark{color:#073642;border-color:#073642}.btn-outline-dark:hover{color:#fff;background-color:#073642;border-color:#073642}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(7,54,66,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#073642;border-color:#073642}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(7,54,66,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#073642;background-color:transparent}.btn-link{font-weight:400;color:#2aa198;text-decoration:underline}.btn-link:hover{color:#2aa198}.btn-link.disabled,.btn-link:disabled{color:#839496}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#839496;text-align:left;list-style:none;background-color:#073642;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #002b36}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#839496;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:rgba(255,255,255,.75);background-color:#002b36}.dropdown-item.active,.dropdown-item:active{color:rgba(255,255,255,.75);text-decoration:none;background-color:#073642}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#839496;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#839496}.dropdown-menu-dark{color:#dee2e6;background-color:#073642;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:rgba(255,255,255,.75);background-color:#073642}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:#002b36}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#2aa198;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#2aa198}.nav-link.disabled{color:#839496;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #073642}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#073642;isolation:isolate}.nav-tabs .nav-link.disabled{color:#839496;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:rgba(255,255,255,.75);background-color:#002b36;border-color:#073642}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:rgba(255,255,255,.75);background-color:#073642}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:-ms-flexbox;display:flex;-ms-flex-wrap:inherit;flex-wrap:inherit;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.7)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.4)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.7)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.4);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.4%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.4)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.7)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:rgba(238,232,213,.125);background-clip:border-box;border:1px solid rgba(0,43,54,.95);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(7,54,66,.25);border-bottom:1px solid rgba(0,43,54,.95)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(7,54,66,.25);border-top:1px solid rgba(0,43,54,.95)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-tabs .nav-link.active{background-color:rgba(238,232,213,.125);border-bottom-color:rgba(238,232,213,.125)}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#839496;text-align:left;background-color:#002b36;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#a37b00;background-color:#e6ebec;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23a37b00'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.accordion-button::after{-ms-flex-negative:0;flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23839496'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#002b36;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.375rem .75rem;margin-bottom:1rem;list-style:none;background-color:#073642;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#839496;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#adb5bd}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#2aa198;text-decoration:none;background-color:transparent;border:1px solid #073642;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#2aa198;background-color:#073642;border-color:#073642}.page-link:focus{z-index:3;color:#2aa198;background-color:#eee8d5;outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:rgba(255,255,255,.75);background-color:#073642;border-color:#073642}.page-item.disabled .page-link{color:#073642;pointer-events:none;background-color:transparent;border-color:#073642}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#6d5200;background-color:#f0e7cc;border-color:#e9dcb3}.alert-primary .alert-link{color:#574200}.alert-secondary{color:#4f595a;background-color:#e6eaea;border-color:#dadfe0}.alert-secondary .alert-link{color:#3f4748}.alert-success{color:#19615b;background-color:#d4ecea;border-color:#bfe3e0}.alert-success .alert-link{color:#144e49}.alert-info{color:#17537e;background-color:#d4e8f6;border-color:#bedcf2}.alert-info .alert-link{color:#124265}.alert-warning{color:#7a2d0d;background-color:#f5dbd0;border-color:#efc9b9}.alert-warning .alert-link{color:#62240a}.alert-danger{color:#7f204e;background-color:#f6d7e6;border-color:#f2c3da}.alert-danger .alert-link{color:#661a3e}.alert-light{color:#989488;background-color:#fffdf9;border-color:#fefcf7}.alert-light .alert-link{color:#7a766d}.alert-dark{color:#042028;background-color:#cdd7d9;border-color:#b5c3c6}.alert-dark .alert-link{color:#031a20}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#073642;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#b58900;text-align:center;white-space:nowrap;background-color:#b58900;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#839496;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#fff;text-decoration:none;background-color:#073642}.list-group-item-action:active{color:#839496;background-color:#eee8d5}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#fff;text-decoration:none;background-color:transparent;border:1px solid #073642}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#839496;pointer-events:none;background-color:transparent}.list-group-item.active{z-index:2;color:#fff;background-color:#073642;border-color:#073642}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#6d5200;background-color:#f0e7cc}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#6d5200;background-color:#d8d0b8}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#6d5200;border-color:#6d5200}.list-group-item-secondary{color:#4f595a;background-color:#e6eaea}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#4f595a;background-color:#cfd3d3}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#4f595a;border-color:#4f595a}.list-group-item-success{color:#19615b;background-color:#d4ecea}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#19615b;background-color:#bfd4d3}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#19615b;border-color:#19615b}.list-group-item-info{color:#17537e;background-color:#d4e8f6}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#17537e;background-color:#bfd1dd}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#17537e;border-color:#17537e}.list-group-item-warning{color:#7a2d0d;background-color:#f5dbd0}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#7a2d0d;background-color:#ddc5bb}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7a2d0d;border-color:#7a2d0d}.list-group-item-danger{color:#7f204e;background-color:#f6d7e6}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#7f204e;background-color:#ddc2cf}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#7f204e;border-color:#7f204e}.list-group-item-light{color:#989488;background-color:#fffdf9}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#989488;background-color:#e6e4e0}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#989488;border-color:#989488}.list-group-item-dark{color:#042028;background-color:#cdd7d9}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#042028;background-color:#b9c2c3}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#042028;border-color:#042028}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#fff;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#fff;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(7,54,66,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:.25}.btn-close-white{-webkit-filter:invert(1) grayscale(100%) brightness(200%);filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:#003f50;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.5rem .75rem;color:#839496;background-color:#003f50;background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#073642;background-clip:padding-box;border:1px solid #002b36;border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #002b36;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-negative:0;flex-shrink:0;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #002b36;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#073642;background-clip:padding-box;border:1px solid #002b36;border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:#002b36}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#073642}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:#002b36}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#073642}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:#002b36}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#073642}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #073642}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:#002b36}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#073642}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#073642;border-bottom:1px solid #002b36;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#839496}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{-webkit-filter:invert(1) grayscale(100);filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;max-width:100%;visibility:hidden;background-color:#073642;background-clip:padding-box;outline:0;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{-ms-flex-positive:1;flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid #002b36;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid #002b36;-webkit-transform:translateX(100%);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid #002b36;-webkit-transform:translateY(-100%);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid #002b36;-webkit-transform:translateY(100%);transform:translateY(100%)}.offcanvas.show{-webkit-transform:none;transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0;mask-position:-200% 0}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0;mask-position:-200% 0}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#b58900}.link-primary:focus,.link-primary:hover{color:#916e00}.link-secondary{color:#839496}.link-secondary:focus,.link-secondary:hover{color:#697678}.link-success{color:#2aa198}.link-success:focus,.link-success:hover{color:#22817a}.link-info{color:#268bd2}.link-info:focus,.link-info:hover{color:#1e6fa8}.link-warning{color:#cb4b16}.link-warning:focus,.link-warning:hover{color:#a23c12}.link-danger{color:#d33682}.link-danger:focus,.link-danger:hover{color:#a92b68}.link-light{color:#fdf6e3}.link-light:focus,.link-light:hover{color:#fdf8e9}.link-dark{color:#073642}.link-dark:focus,.link-dark:hover{color:#062b35}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-align:center;align-items:center;-ms-flex-item-align:stretch;align-self:stretch}.vstack{display:-ms-flexbox;display:flex;-ms-flex:1 1 auto;flex:1 1 auto;-ms-flex-direction:column;flex-direction:column;-ms-flex-item-align:stretch;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;-ms-flex-item-align:stretch;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{-webkit-transform:translate(-50%,-50%)!important;transform:translate(-50%,-50%)!important}.translate-middle-x{-webkit-transform:translateX(-50%)!important;transform:translateX(-50%)!important}.translate-middle-y{-webkit-transform:translateY(-50%)!important;transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#b58900!important}.border-secondary{border-color:#839496!important}.border-success{border-color:#2aa198!important}.border-info{border-color:#268bd2!important}.border-warning{border-color:#cb4b16!important}.border-danger{border-color:#d33682!important}.border-light{border-color:#fdf6e3!important}.border-dark{border-color:#073642!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-first{-ms-flex-order:-1!important;order:-1!important}.order-0{-ms-flex-order:0!important;order:0!important}.order-1{-ms-flex-order:1!important;order:1!important}.order-2{-ms-flex-order:2!important;order:2!important}.order-3{-ms-flex-order:3!important;order:3!important}.order-4{-ms-flex-order:4!important;order:4!important}.order-5{-ms-flex-order:5!important;order:5!important}.order-last{-ms-flex-order:6!important;order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#839496!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-sm-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-sm-first{-ms-flex-order:-1!important;order:-1!important}.order-sm-0{-ms-flex-order:0!important;order:0!important}.order-sm-1{-ms-flex-order:1!important;order:1!important}.order-sm-2{-ms-flex-order:2!important;order:2!important}.order-sm-3{-ms-flex-order:3!important;order:3!important}.order-sm-4{-ms-flex-order:4!important;order:4!important}.order-sm-5{-ms-flex-order:5!important;order:5!important}.order-sm-last{-ms-flex-order:6!important;order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-md-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-md-first{-ms-flex-order:-1!important;order:-1!important}.order-md-0{-ms-flex-order:0!important;order:0!important}.order-md-1{-ms-flex-order:1!important;order:1!important}.order-md-2{-ms-flex-order:2!important;order:2!important}.order-md-3{-ms-flex-order:3!important;order:3!important}.order-md-4{-ms-flex-order:4!important;order:4!important}.order-md-5{-ms-flex-order:5!important;order:5!important}.order-md-last{-ms-flex-order:6!important;order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-lg-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-lg-first{-ms-flex-order:-1!important;order:-1!important}.order-lg-0{-ms-flex-order:0!important;order:0!important}.order-lg-1{-ms-flex-order:1!important;order:1!important}.order-lg-2{-ms-flex-order:2!important;order:2!important}.order-lg-3{-ms-flex-order:3!important;order:3!important}.order-lg-4{-ms-flex-order:4!important;order:4!important}.order-lg-5{-ms-flex-order:5!important;order:5!important}.order-lg-last{-ms-flex-order:6!important;order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-xl-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-xl-first{-ms-flex-order:-1!important;order:-1!important}.order-xl-0{-ms-flex-order:0!important;order:0!important}.order-xl-1{-ms-flex-order:1!important;order:1!important}.order-xl-2{-ms-flex-order:2!important;order:2!important}.order-xl-3{-ms-flex-order:3!important;order:3!important}.order-xl-4{-ms-flex-order:4!important;order:4!important}.order-xl-5{-ms-flex-order:5!important;order:5!important}.order-xl-last{-ms-flex-order:6!important;order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:-ms-flexbox!important;display:flex!important}.d-xxl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xxl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xxl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xxl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xxl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xxl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xxl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xxl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xxl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-xxl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xxl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xxl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xxl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xxl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xxl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-xxl-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-xxl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xxl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xxl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xxl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xxl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xxl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xxl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xxl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xxl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xxl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xxl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xxl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xxl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xxl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xxl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xxl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xxl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-xxl-first{-ms-flex-order:-1!important;order:-1!important}.order-xxl-0{-ms-flex-order:0!important;order:0!important}.order-xxl-1{-ms-flex-order:1!important;order:1!important}.order-xxl-2{-ms-flex-order:2!important;order:2!important}.order-xxl-3{-ms-flex-order:3!important;order:3!important}.order-xxl-4{-ms-flex-order:4!important;order:4!important}.order-xxl-5{-ms-flex-order:5!important;order:5!important}.order-xxl-last{-ms-flex-order:6!important;order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-print-none{display:none!important}}.btn-primary{background-color:#b58900}.btn-secondary{background-color:#839496}.btn-success{background-color:#2aa198}.btn-info{background-color:#268bd2}.btn-warning{background-color:#cb4b16}.btn-danger{background-color:#d33682}.btn-light{background-color:#fdf6e3}.btn-dark{background-color:#073642}.alert{border:none;color:#fff}.alert .alert-link,.alert a{color:#fff;text-decoration:underline}.alert-primary{background-color:#b58900}.alert-secondary{background-color:#839496}.alert-success{background-color:#2aa198}.alert-info{background-color:#268bd2}.alert-warning{background-color:#cb4b16}.alert-danger{background-color:#d33682}.alert-light{background-color:#fdf6e3}.alert-dark{background-color:#073642}.alert-light,.alert-light .alert-link,.alert-light a:not(.btn){color:#002b36}.badge.bg-light{color:#073642} \ No newline at end of file + */@import url(https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap);:root,[data-bs-theme=light]{--bs-blue:#b58900;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#e83e8c;--bs-red:#d33682;--bs-orange:#fd7e14;--bs-yellow:#cb4b16;--bs-green:#2aa198;--bs-teal:#20c997;--bs-cyan:#268bd2;--bs-black:#000;--bs-white:#fff;--bs-gray:#839496;--bs-gray-dark:#073642;--bs-gray-100:#fdf6e3;--bs-gray-200:#eee8d5;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#839496;--bs-gray-700:#495057;--bs-gray-800:#073642;--bs-gray-900:#002b36;--bs-primary:#b58900;--bs-secondary:#839496;--bs-success:#2aa198;--bs-info:#268bd2;--bs-warning:#cb4b16;--bs-danger:#d33682;--bs-light:#fdf6e3;--bs-dark:#073642;--bs-primary-rgb:181,137,0;--bs-secondary-rgb:131,148,150;--bs-success-rgb:42,161,152;--bs-info-rgb:38,139,210;--bs-warning-rgb:203,75,22;--bs-danger-rgb:211,54,130;--bs-light-rgb:253,246,227;--bs-dark-rgb:7,54,66;--bs-primary-text-emphasis:#483700;--bs-secondary-text-emphasis:#343b3c;--bs-success-text-emphasis:#11403d;--bs-info-text-emphasis:#0f3854;--bs-warning-text-emphasis:#511e09;--bs-danger-text-emphasis:#541634;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#f0e7cc;--bs-secondary-bg-subtle:#e6eaea;--bs-success-bg-subtle:#d4ecea;--bs-info-bg-subtle:#d4e8f6;--bs-warning-bg-subtle:#f5dbd0;--bs-danger-bg-subtle:#f6d7e6;--bs-light-bg-subtle:#fefbf1;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#e1d099;--bs-secondary-border-subtle:#cdd4d5;--bs-success-border-subtle:#aad9d6;--bs-info-border-subtle:#a8d1ed;--bs-warning-border-subtle:#eab7a2;--bs-danger-border-subtle:#edafcd;--bs-light-border-subtle:#eee8d5;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#839496;--bs-body-color-rgb:131,148,150;--bs-body-bg:#002b36;--bs-body-bg-rgb:0,43,54;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(131, 148, 150, 0.75);--bs-secondary-color-rgb:131,148,150;--bs-secondary-bg:#eee8d5;--bs-secondary-bg-rgb:238,232,213;--bs-tertiary-color:rgba(131, 148, 150, 0.5);--bs-tertiary-color-rgb:131,148,150;--bs-tertiary-bg:#fdf6e3;--bs-tertiary-bg-rgb:253,246,227;--bs-heading-color:inherit;--bs-link-color:#2aa198;--bs-link-color-rgb:42,161,152;--bs-link-decoration:underline;--bs-link-hover-color:#2aa198;--bs-link-hover-color-rgb:42,161,152;--bs-code-color:#e83e8c;--bs-highlight-color:#839496;--bs-highlight-bg:#f5dbd0;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(181, 137, 0, 0.25);--bs-form-valid-color:#2aa198;--bs-form-valid-border-color:#2aa198;--bs-form-invalid-color:#d33682;--bs-form-invalid-border-color:#d33682}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#002b36;--bs-body-bg-rgb:0,43,54;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#073642;--bs-secondary-bg-rgb:7,54,66;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#04313c;--bs-tertiary-bg-rgb:4,49,60;--bs-primary-text-emphasis:#d3b866;--bs-secondary-text-emphasis:#b5bfc0;--bs-success-text-emphasis:#7fc7c1;--bs-info-text-emphasis:#7db9e4;--bs-warning-text-emphasis:#e09373;--bs-danger-text-emphasis:#e586b4;--bs-light-text-emphasis:#fdf6e3;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#241b00;--bs-secondary-bg-subtle:#1a1e1e;--bs-success-bg-subtle:#08201e;--bs-info-bg-subtle:#081c2a;--bs-warning-bg-subtle:#290f04;--bs-danger-bg-subtle:#2a0b1a;--bs-light-bg-subtle:#073642;--bs-dark-bg-subtle:#041b21;--bs-primary-border-subtle:#6d5200;--bs-secondary-border-subtle:#4f595a;--bs-success-border-subtle:#19615b;--bs-info-border-subtle:#17537e;--bs-warning-border-subtle:#7a2d0d;--bs-danger-border-subtle:#7f204e;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#073642;--bs-heading-color:inherit;--bs-link-color:#d3b866;--bs-link-hover-color:#dcc685;--bs-link-color-rgb:211,184,102;--bs-link-hover-color-rgb:220,198,133;--bs-code-color:#f18bba;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#511e09;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#7fc7c1;--bs-form-valid-border-color:#7fc7c1;--bs-form-invalid-color:#e586b4;--bs-form-invalid-border-color:#e586b4}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#839496}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:#fff;--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:#073642;--bs-table-accent-bg:rgba(255, 255, 255, 0.05);--bs-table-striped-color:#fff;--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:#fff;--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:#fff;--bs-table-hover-bg:rgba(255, 255, 255, 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#fff;--bs-table-bg:#b58900;--bs-table-border-color:#c4a133;--bs-table-striped-bg:#b98f0d;--bs-table-striped-color:#fff;--bs-table-active-bg:#bc951a;--bs-table-active-color:#fff;--bs-table-hover-bg:#bb9213;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#fff;--bs-table-bg:#839496;--bs-table-border-color:#9ca9ab;--bs-table-striped-bg:#89999b;--bs-table-striped-color:#fff;--bs-table-active-bg:#8f9fa1;--bs-table-active-color:#fff;--bs-table-hover-bg:#8c9c9e;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#fff;--bs-table-bg:#2aa198;--bs-table-border-color:#55b4ad;--bs-table-striped-bg:#35a69d;--bs-table-striped-color:#fff;--bs-table-active-bg:#3faaa2;--bs-table-active-color:#fff;--bs-table-hover-bg:#3aa8a0;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#fff;--bs-table-bg:#268bd2;--bs-table-border-color:#51a2db;--bs-table-striped-bg:#3191d4;--bs-table-striped-color:#fff;--bs-table-active-bg:#3c97d7;--bs-table-active-color:#fff;--bs-table-hover-bg:#3694d5;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#fff;--bs-table-bg:#cb4b16;--bs-table-border-color:#d56f45;--bs-table-striped-bg:#ce5422;--bs-table-striped-color:#fff;--bs-table-active-bg:#d05d2d;--bs-table-active-color:#fff;--bs-table-hover-bg:#cf5927;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#fff;--bs-table-bg:#d33682;--bs-table-border-color:#dc5e9b;--bs-table-striped-bg:#d54088;--bs-table-striped-color:#fff;--bs-table-active-bg:#d74a8f;--bs-table-active-color:#fff;--bs-table-hover-bg:#d6458b;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#fdf6e3;--bs-table-border-color:#cac5b6;--bs-table-striped-bg:#f0ead8;--bs-table-striped-color:#000;--bs-table-active-bg:#e4ddcc;--bs-table-active-color:#000;--bs-table-hover-bg:#eae4d2;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#073642;--bs-table-border-color:#395e68;--bs-table-striped-bg:#13404b;--bs-table-striped-color:#fff;--bs-table-active-bg:#204a55;--bs-table-active-color:#fff;--bs-table-hover-bg:#1a4550;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#073642;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#adb5bd;background-clip:padding-box;border:var(--bs-border-width) solid rgba(0,0,0,.15);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#073642;background-color:#adb5bd;border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(181,137,0,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:#657b83;opacity:1}.form-control::placeholder{color:#657b83;opacity:1}.form-control:disabled{color:#fdf6e3;background-color:#839496;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#839496;background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#839496;background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23073642' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#073642;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#adb5bd;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid rgba(0,0,0,.15);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(181,137,0,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{color:#495057;background-color:#839496}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #073642}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:rgba(255, 255, 255, 0.75);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid #fff;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#839ba1;outline:0;box-shadow:0 0 0 .25rem rgba(181,137,0,.25)}.form-check-input:checked{background-color:#b58900;border-color:#b58900}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#073642;border-color:#073642;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.75%29'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #002b36,0 0 0 .25rem rgba(181,137,0,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #002b36,0 0 0 .25rem rgba(181,137,0,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#b58900;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b5c3c6}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#073642;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#b58900;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b5c3c6}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#073642;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:#adb5bd;border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:#adb5bd;border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#839496}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:#839496}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#839496;text-align:center;white-space:nowrap;background-color:#073642;border:var(--bs-border-width) solid rgba(0,0,0,.15);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%232aa198' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%232aa198' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d33682'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d33682' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d33682'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d33682' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#b58900;--bs-btn-border-color:#b58900;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#9a7400;--bs-btn-hover-border-color:#916e00;--bs-btn-focus-shadow-rgb:192,155,38;--bs-btn-active-color:#fff;--bs-btn-active-bg:#916e00;--bs-btn-active-border-color:#886700;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#b58900;--bs-btn-disabled-border-color:#b58900}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#839496;--bs-btn-border-color:#839496;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6f7e80;--bs-btn-hover-border-color:#697678;--bs-btn-focus-shadow-rgb:150,164,166;--bs-btn-active-color:#fff;--bs-btn-active-bg:#697678;--bs-btn-active-border-color:#626f71;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#839496;--bs-btn-disabled-border-color:#839496}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#2aa198;--bs-btn-border-color:#2aa198;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#248981;--bs-btn-hover-border-color:#22817a;--bs-btn-focus-shadow-rgb:74,175,167;--bs-btn-active-color:#fff;--bs-btn-active-bg:#22817a;--bs-btn-active-border-color:#207972;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#2aa198;--bs-btn-disabled-border-color:#2aa198}.btn-info{--bs-btn-color:#fff;--bs-btn-bg:#268bd2;--bs-btn-border-color:#268bd2;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#2076b3;--bs-btn-hover-border-color:#1e6fa8;--bs-btn-focus-shadow-rgb:71,156,217;--bs-btn-active-color:#fff;--bs-btn-active-bg:#1e6fa8;--bs-btn-active-border-color:#1d689e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#268bd2;--bs-btn-disabled-border-color:#268bd2}.btn-warning{--bs-btn-color:#fff;--bs-btn-bg:#cb4b16;--bs-btn-border-color:#cb4b16;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#ad4013;--bs-btn-hover-border-color:#a23c12;--bs-btn-focus-shadow-rgb:211,102,57;--bs-btn-active-color:#fff;--bs-btn-active-bg:#a23c12;--bs-btn-active-border-color:#983811;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#cb4b16;--bs-btn-disabled-border-color:#cb4b16}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#d33682;--bs-btn-border-color:#d33682;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#b32e6f;--bs-btn-hover-border-color:#a92b68;--bs-btn-focus-shadow-rgb:218,84,149;--bs-btn-active-color:#fff;--bs-btn-active-bg:#a92b68;--bs-btn-active-border-color:#9e2962;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#d33682;--bs-btn-disabled-border-color:#d33682}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#fdf6e3;--bs-btn-border-color:#fdf6e3;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d7d1c1;--bs-btn-hover-border-color:#cac5b6;--bs-btn-focus-shadow-rgb:215,209,193;--bs-btn-active-color:#000;--bs-btn-active-bg:#cac5b6;--bs-btn-active-border-color:#beb9aa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#fdf6e3;--bs-btn-disabled-border-color:#fdf6e3}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#073642;--bs-btn-border-color:#073642;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#2c545e;--bs-btn-hover-border-color:#204a55;--bs-btn-focus-shadow-rgb:44,84,94;--bs-btn-active-color:#fff;--bs-btn-active-bg:#395e68;--bs-btn-active-border-color:#204a55;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#073642;--bs-btn-disabled-border-color:#073642}.btn-outline-primary{--bs-btn-color:#b58900;--bs-btn-border-color:#b58900;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#b58900;--bs-btn-hover-border-color:#b58900;--bs-btn-focus-shadow-rgb:181,137,0;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b58900;--bs-btn-active-border-color:#b58900;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#b58900;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#b58900;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#839496;--bs-btn-border-color:#839496;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#839496;--bs-btn-hover-border-color:#839496;--bs-btn-focus-shadow-rgb:131,148,150;--bs-btn-active-color:#fff;--bs-btn-active-bg:#839496;--bs-btn-active-border-color:#839496;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#839496;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#839496;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#2aa198;--bs-btn-border-color:#2aa198;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#2aa198;--bs-btn-hover-border-color:#2aa198;--bs-btn-focus-shadow-rgb:42,161,152;--bs-btn-active-color:#fff;--bs-btn-active-bg:#2aa198;--bs-btn-active-border-color:#2aa198;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#2aa198;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#2aa198;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#268bd2;--bs-btn-border-color:#268bd2;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#268bd2;--bs-btn-hover-border-color:#268bd2;--bs-btn-focus-shadow-rgb:38,139,210;--bs-btn-active-color:#fff;--bs-btn-active-bg:#268bd2;--bs-btn-active-border-color:#268bd2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#268bd2;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#268bd2;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#cb4b16;--bs-btn-border-color:#cb4b16;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#cb4b16;--bs-btn-hover-border-color:#cb4b16;--bs-btn-focus-shadow-rgb:203,75,22;--bs-btn-active-color:#fff;--bs-btn-active-bg:#cb4b16;--bs-btn-active-border-color:#cb4b16;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#cb4b16;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#cb4b16;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#d33682;--bs-btn-border-color:#d33682;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d33682;--bs-btn-hover-border-color:#d33682;--bs-btn-focus-shadow-rgb:211,54,130;--bs-btn-active-color:#fff;--bs-btn-active-bg:#d33682;--bs-btn-active-border-color:#d33682;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#d33682;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#d33682;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#fdf6e3;--bs-btn-border-color:#fdf6e3;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#fdf6e3;--bs-btn-hover-border-color:#fdf6e3;--bs-btn-focus-shadow-rgb:253,246,227;--bs-btn-active-color:#000;--bs-btn-active-bg:#fdf6e3;--bs-btn-active-border-color:#fdf6e3;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fdf6e3;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#fdf6e3;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#073642;--bs-btn-border-color:#073642;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#073642;--bs-btn-hover-border-color:#073642;--bs-btn-focus-shadow-rgb:7,54,66;--bs-btn-active-color:#fff;--bs-btn-active-bg:#073642;--bs-btn-active-border-color:#073642;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#073642;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#073642;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#839496;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:74,175,167;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:#073642;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:#002b36;--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:#839496;--bs-dropdown-link-hover-color:rgba(255, 255, 255, 0.75);--bs-dropdown-link-hover-bg:#002b36;--bs-dropdown-link-active-color:rgba(255, 255, 255, 0.75);--bs-dropdown-link-active-bg:#073642;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#839496;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#073642;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:#002b36;--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:rgba(255, 255, 255, 0.75);--bs-dropdown-link-active-bg:#073642;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(181,137,0,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:#073642;--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:#073642;--bs-nav-tabs-link-active-color:rgba(255, 255, 255, 0.75);--bs-nav-tabs-link-active-bg:#002b36;--bs-nav-tabs-link-active-border-color:#073642;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:rgba(255, 255, 255, 0.75);--bs-nav-pills-link-active-bg:#073642}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28131, 148, 150, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:rgba(0, 43, 54, 0.95);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(7, 54, 66, 0.25);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:rgba(238, 232, 213, 0.125);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23839496' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23483700' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(181, 137, 0, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23d3b866'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23d3b866'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0.75rem;--bs-breadcrumb-padding-y:0.375rem;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg:#073642;--bs-breadcrumb-border-radius:0.25rem;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#adb5bd;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:transparent;--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:#073642;--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:#073642;--bs-pagination-hover-border-color:#073642;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(181, 137, 0, 0.25);--bs-pagination-active-color:rgba(255, 255, 255, 0.75);--bs-pagination-active-bg:#073642;--bs-pagination-active-border-color:#073642;--bs-pagination-disabled-color:#073642;--bs-pagination-disabled-bg:transparent;--bs-pagination-disabled-border-color:#073642;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#073642;--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#b58900;--bs-progress-bar-bg:#b58900;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:#fff;--bs-list-group-bg:transparent;--bs-list-group-border-color:#073642;--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#839496;--bs-list-group-action-hover-color:#fff;--bs-list-group-action-hover-bg:#073642;--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:#839496;--bs-list-group-disabled-bg:transparent;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#073642;--bs-list-group-active-border-color:#073642;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#fff;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(181, 137, 0, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:#003f50;--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:rgba(0, 0, 0, 0.2);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:#839496;--bs-toast-header-bg:#003f50;--bs-toast-header-border-color:rgba(0, 0, 0, 0.2);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#073642;--bs-modal-border-color:#002b36;--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:#002b36;--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:#002b36;--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#073642;--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:#002b36;--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:#073642;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:#002b36;--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#fff!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(145,110,0,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(145,110,0,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(145,110,0,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(105,118,120,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(105,118,120,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(105,118,120,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(34,129,122,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(34,129,122,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(34,129,122,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(30,111,168,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(30,111,168,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(30,111,168,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(162,60,18,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(162,60,18,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(162,60,18,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(169,43,104,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(169,43,104,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(169,43,104,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(253,248,233,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(253,248,233,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(253,248,233,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(6,43,53,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(6,43,53,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(6,43,53,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}.btn-primary{background-color:#b58900}.btn-secondary{background-color:#839496}.btn-success{background-color:#2aa198}.btn-info{background-color:#268bd2}.btn-warning{background-color:#cb4b16}.btn-danger{background-color:#d33682}.btn-light{background-color:#fdf6e3}.btn-dark{background-color:#073642}.alert{color:#fff;border:none}.alert .alert-link,.alert a{color:#fff;text-decoration:underline}.alert-primary{background-color:#b58900}.alert-secondary{background-color:#839496}.alert-success{background-color:#2aa198}.alert-info{background-color:#268bd2}.alert-warning{background-color:#cb4b16}.alert-danger{background-color:#d33682}.alert-light{background-color:#fdf6e3}.alert-dark{background-color:#073642}.alert-light,.alert-light .alert-link,.alert-light a:not(.btn){color:#002b36}.badge.bg-light{color:#073642}.tooltip{--bs-tooltip-bg:var(--bs-tertiary-bg);--bs-tooltip-color:var(--bs-emphasis-color)} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/docroot/js/bootstrap.bundle.min.js b/docroot/js/bootstrap.bundle.min.js old mode 100644 new mode 100755 index cc0a2556..04e9185b --- a/docroot/js/bootstrap.bundle.min.js +++ b/docroot/js/bootstrap.bundle.min.js @@ -1,7 +1,7 @@ /*! - * Bootstrap v5.1.3 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); //# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file From 2c3365f1aa6234e33f5d980d4cf56782c8c81d1d Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Apr 2024 14:07:04 +0200 Subject: [PATCH 39/64] Fixed dropdown menu in search view sometimes hidden, fixes #436 --- src/lms/ui/explore/SearchView.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index 00f690f5..aed56f86 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -48,6 +48,7 @@ namespace lms::ui addFunction("tr", &Wt::WTemplate::Functions::tr); _stack = bindNew("results"); + _stack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default // releases _releases = _stack->addNew(Wt::WString::tr("Lms.Explore.Releases.template.container")); From 18b220b241a24150d3ef01cf0d24edcaa8a2bcbc Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Apr 2024 18:50:00 +0200 Subject: [PATCH 40/64] Optimm to get all artists of a track in a single query --- src/libs/database/impl/TrackArtistLink.cpp | 30 ++++++--- .../include/database/TrackArtistLink.hpp | 1 + src/libs/database/test/Artist.cpp | 64 +++++++++++++++++++ .../subsonic/impl/responses/Contributor.cpp | 4 +- .../subsonic/impl/responses/Contributor.hpp | 3 +- src/libs/subsonic/impl/responses/Song.cpp | 11 ++-- 6 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 5667dd40..81ebe025 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -39,19 +39,17 @@ namespace lms::db if (params.linkType) query.where("t_a_l.type = ?").bind(*params.linkType); - if (params.track.isValid() || params.release.isValid()) - query.join("track t ON t.id = t_a_l.track_id"); + if (params.track.isValid()) + query.where("t_a_l.track_id = ?").bind(params.track); if (params.artist.isValid()) - query.join("artist a ON a.id = t_a_l.artist_id"); + query.where("t_a_l.artist_id = ?").bind(params.artist); if (params.release.isValid()) + { + query.join("track t ON t.id = t_a_l.track_id"); query.where("t.release_id = ?").bind(params.release); - - if (params.track.isValid()) - query.where("t.id = ?").bind(params.track); - - query.groupBy("t_a_l.id"); + } return query; } @@ -81,6 +79,22 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } + void TrackArtistLink::find(Session& session, TrackId trackId, const std::function& artist)>& func) + { + session.checkReadTransaction(); + + using ResultType = std::tuple < Wt::Dbo::ptr, Wt::Dbo::ptr>; + + const auto query{ session.getDboSession()->query("SELECT t_a_l, a FROM track_artist_link t_a_l") + .join("artist a ON t_a_l.artist_id = a.id") + .where("t_a_l.track_id = ?").bind(trackId) }; + + utils::forEachQueryResult(query, [&](const ResultType& result) + { + func(std::get>(result), std::get>(result)); + }); + } + void TrackArtistLink::find(Session& session, const FindParameters& parameters, const std::function& func) { const auto query{ createQuery(session, parameters) }; diff --git a/src/libs/database/include/database/TrackArtistLink.hpp b/src/libs/database/include/database/TrackArtistLink.hpp index e3fe7580..216fbf33 100644 --- a/src/libs/database/include/database/TrackArtistLink.hpp +++ b/src/libs/database/include/database/TrackArtistLink.hpp @@ -62,6 +62,7 @@ namespace lms::db TrackArtistLink() = default; TrackArtistLink(ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type, std::string_view subType); + static void find(Session& session, TrackId trackId, const std::function&)>&); static void find(Session& session, const FindParameters& parameters, const std::function&); static pointer find(Session& session, TrackArtistLinkId linkId); static pointer create(Session& session, ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type, std::string_view subType = {}); diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 7d06a95d..bd486544 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -394,6 +394,29 @@ namespace lms::db::tests EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector visitedLinks; + TrackArtistLink::find(session, TrackArtistLink::FindParameters{}.setTrack(track.getId()), [&](const TrackArtistLink::pointer& link) + { + visitedLinks.push_back(link); + }); + ASSERT_EQ(visitedLinks.size(), 3); + EXPECT_EQ(visitedLinks[0]->getArtist()->getId(), artist.getId()); + EXPECT_EQ(visitedLinks[1]->getArtist()->getId(), artist.getId()); + EXPECT_EQ(visitedLinks[2]->getArtist()->getId(), artist.getId()); + + auto containsType = [&](TrackArtistLinkType type) + { + return std::any_of(std::cbegin(visitedLinks), std::cend(visitedLinks), [type](const TrackArtistLink::pointer& link) { return link->getType() == type;}); + }; + + EXPECT_TRUE(containsType(TrackArtistLinkType::Artist)); + EXPECT_TRUE(containsType(TrackArtistLinkType::ReleaseArtist)); + EXPECT_TRUE(containsType(TrackArtistLinkType::Writer)); + } } TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) @@ -453,6 +476,47 @@ namespace lms::db::tests tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist })); EXPECT_EQ(tracks.results.size(), 1); } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector visitedLinks; + TrackArtistLink::find(session, TrackArtistLink::FindParameters{}.setTrack(track.getId()), [&](const TrackArtistLink::pointer& link) + { + visitedLinks.push_back(link); + }); + ASSERT_EQ(visitedLinks.size(), 2); + EXPECT_EQ(visitedLinks[0]->getArtist()->getId(), artist1.getId()); + EXPECT_EQ(visitedLinks[1]->getArtist()->getId(), artist2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector visitedLinks; + TrackArtistLink::find(session, TrackArtistLink::FindParameters{}.setArtist(artist2.getId()), [&](const TrackArtistLink::pointer& link) + { + visitedLinks.push_back(link); + }); + ASSERT_EQ(visitedLinks.size(), 1); + EXPECT_EQ(visitedLinks[0]->getArtist()->getId(), artist2.getId()); + EXPECT_EQ(visitedLinks[0]->getTrack()->getId(), track.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector> visitedEntries; + TrackArtistLink::find(session, track.getId(), [&](const TrackArtistLink::pointer& link, const Artist::pointer& artist) + { + visitedEntries.push_back(std::make_pair(link, artist)); + }); + ASSERT_EQ(visitedEntries.size(), 2); + EXPECT_EQ(visitedEntries[0].first->getArtist()->getId(), artist1.getId()); + EXPECT_EQ(visitedEntries[0].second->getId(), artist1.getId()); + EXPECT_EQ(visitedEntries[1].first->getArtist()->getId(), artist2.getId()); + EXPECT_EQ(visitedEntries[1].second->getId(), artist2.getId()); + } } TEST_F(DatabaseFixture, Artist_findByName) diff --git a/src/libs/subsonic/impl/responses/Contributor.cpp b/src/libs/subsonic/impl/responses/Contributor.cpp index 576e99b2..ad2dc2b0 100644 --- a/src/libs/subsonic/impl/responses/Contributor.cpp +++ b/src/libs/subsonic/impl/responses/Contributor.cpp @@ -26,14 +26,14 @@ namespace lms::api::subsonic { - Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink) + Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink, const db::ObjectPtr& artist) { Response::Node contributorNode; contributorNode.setAttribute("role", utils::toString(trackArtistLink->getType())); if (!trackArtistLink->getSubType().empty()) contributorNode.setAttribute("subRole", trackArtistLink->getSubType()); - contributorNode.addChild("artist", createArtistNode(trackArtistLink->getArtist())); + contributorNode.addChild("artist", createArtistNode(artist)); return contributorNode; } diff --git a/src/libs/subsonic/impl/responses/Contributor.hpp b/src/libs/subsonic/impl/responses/Contributor.hpp index 14b54dde..6c89ed01 100644 --- a/src/libs/subsonic/impl/responses/Contributor.hpp +++ b/src/libs/subsonic/impl/responses/Contributor.hpp @@ -24,10 +24,11 @@ namespace lms::db { + class Artist; class TrackArtistLink; } namespace lms::api::subsonic { - Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink); + Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink, const db::ObjectPtr& artist); } \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index f7a8a1aa..af5c6f9f 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -156,25 +156,22 @@ namespace lms::api::subsonic } { - TrackArtistLink::FindParameters params; - params.setTrack(track->getId()); - trackResponse.createEmptyArrayChild("albumartists"); trackResponse.createEmptyArrayChild("artists"); trackResponse.createEmptyArrayChild("contributors"); - TrackArtistLink::find(context.dbSession, params, [&](const TrackArtistLink::pointer& link) + TrackArtistLink::find(context.dbSession, track->getId(), [&](const TrackArtistLink::pointer& link, const Artist::pointer& artist) { switch (link->getType()) { case TrackArtistLinkType::Artist: - trackResponse.addArrayChild("artists", createArtistNode(link->getArtist())); + trackResponse.addArrayChild("artists", createArtistNode(artist)); break; case TrackArtistLinkType::ReleaseArtist: - trackResponse.addArrayChild("albumartists", createArtistNode(link->getArtist())); + trackResponse.addArrayChild("albumartists", createArtistNode(artist)); break; default: - trackResponse.addArrayChild("contributors", createContributorNode(link)); + trackResponse.addArrayChild("contributors", createContributorNode(link, artist)); } }); } From 90a2ac5680b331f2e92e305018b251e2315eb077 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Apr 2024 19:10:31 +0200 Subject: [PATCH 41/64] CodeFactor review --- src/libs/services/cover/impl/ImageCache.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/services/cover/impl/ImageCache.cpp b/src/libs/services/cover/impl/ImageCache.cpp index 1f9b6550..1409ab1a 100644 --- a/src/libs/services/cover/impl/ImageCache.cpp +++ b/src/libs/services/cover/impl/ImageCache.cpp @@ -58,7 +58,6 @@ namespace lms::cover ++_cacheHits; return it->second; - } void ImageCache::flush() From 7ecaa16b8eecdd38f39de7e910e166d29533099a Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 16 Apr 2024 14:06:49 +0200 Subject: [PATCH 42/64] Added a trace for index creation --- src/libs/database/impl/Session.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 66df2a14..4ea9f792 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -145,6 +145,8 @@ namespace lms::db // Indexes { + LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); + auto transaction{ createWriteTransaction() }; _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); From 78dd0302d0528b00da9e890ff4d19d6f4d8edcc3 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 16 Apr 2024 14:28:30 +0200 Subject: [PATCH 43/64] Removed useless join --- src/libs/database/impl/Release.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 816567b8..eafca66e 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -90,11 +90,13 @@ namespace lms::db if (params.artist.isValid() || params.sortMethod == ReleaseSortMethod::ArtistNameThenName) { - query.join("artist a ON a.id = t_a_l.artist_id") - .join("track_artist_link t_a_l ON t_a_l.track_id = t.id"); + query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id"); if (params.artist.isValid()) - query.where("a.id = ?").bind(params.artist); + query.where("t_a_l.artist_id = ?").bind(params.artist); + + if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName) + query.join("artist a ON a.id = t_a_l.artist_id"); if (!params.trackArtistLinkTypes.empty()) { @@ -117,10 +119,9 @@ namespace lms::db { std::ostringstream oss; oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN artist a ON a.id = t_a_l.artist_id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN track t ON t.release_id = r.id" - " WHERE (a.id = ? AND ("; + " WHERE (t_a_l.artist_id = ? AND ("; query.bind(params.artist); From e92fcb79de014e40da9fd697181738f2c546932e Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 18:32:11 +0200 Subject: [PATCH 44/64] Iterate on clusters to better trace --- src/libs/database/impl/Cluster.cpp | 8 ++++++++ src/libs/subsonic/impl/responses/Album.cpp | 14 +++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 76065a8b..e92d9faa 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -113,6 +113,14 @@ namespace lms::db return utils::execRangeQuery(query, params.range); } + void Cluster::find(Session& session, const FindParameters& params, std::function _func) + { + session.checkReadTransaction(); + auto query{ createQuery>(session, params) }; + + return utils::forEachQueryResult(query, _func); + } + RangeResults Cluster::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 163a89b2..fc3adf03 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -132,9 +132,11 @@ namespace lms::api::subsonic params.setRelease(release->getId()); params.setClusterTypeName(clusterTypeName); - for (const auto& cluster : Cluster::find(context.dbSession, params).results) - albumNode.addArrayValue(field, cluster->getName()); - } }; + Cluster::find(context.dbSession, params, [&](const Cluster::pointer& cluster) + { + albumNode.addArrayValue(field, cluster->getName()); + }); + }}; addClusters("moods", "MOOD"); @@ -146,8 +148,10 @@ namespace lms::api::subsonic params.setRelease(release->getId()); params.setClusterType(genreClusterType->getId()); - for (const auto& cluster : Cluster::find(context.dbSession, params).results) - albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName())); + Cluster::find(context.dbSession, params, [&](const Cluster::pointer& cluster) + { + albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName())); + }); } albumNode.createEmptyArrayChild("artists"); From f7fac19a5c165c6cc1abde30de25aedbe2cee7d6 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 18:37:44 +0200 Subject: [PATCH 45/64] Simplified queries --- src/libs/database/impl/Track.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 1ee75fb4..f2552dbb 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -296,8 +296,9 @@ namespace lms::db assert(session()); const auto query{ session()->query - ("SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") - .where("t.id = ?").bind(getId()) }; + ("SELECT t_c.cluster_id FROM track_cluster t_c") + .where("t_c.track_id = ?").bind(getId()) + .groupBy("t_c.cluster_id") }; return utils::fetchQueryResults(query); } @@ -444,8 +445,7 @@ namespace lms::db std::ostringstream oss; oss << - "SELECT DISTINCT a.id from artist a" - " INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id" + "SELECT t_a_l.artist_id FROM track_artist_link t_a_l" " INNER JOIN track t ON t.id = t_a_l.track_id"; if (!linkTypes.empty()) @@ -468,6 +468,7 @@ namespace lms::db query.bind(type); query.where("t.id = ?").bind(getId()); + query.groupBy("t_a_l.artist_id"); return utils::fetchQueryResults(query); } From ec1a38fa7600da8204dcfaad824dcd2832493e7e Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 18:41:23 +0200 Subject: [PATCH 46/64] Simplified queries --- src/libs/database/impl/Listen.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/database/impl/Listen.cpp b/src/libs/database/impl/Listen.cpp index bd4b6b47..f53f6cdc 100644 --- a/src/libs/database/impl/Listen.cpp +++ b/src/libs/database/impl/Listen.cpp @@ -53,21 +53,18 @@ namespace lms::db if (!params.clusters.empty()) { std::ostringstream oss; - oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" - " INNER JOIN track t ON t.id = t_a_l.track_id" - " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l" + " INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id"; WhereClause clusterClause; for (auto id : params.clusters) { - clusterClause.Or(WhereClause("c.id = ?")); + clusterClause.Or(WhereClause("t_c.cluster_id = ?")); query.bind(id); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; + oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.clusters.size() << ")"; query.where(oss.str()); } From ba1fb4a6d6de487a13759cce24975c7f9ab2f66f Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 18:43:54 +0200 Subject: [PATCH 47/64] Added an index to speedup a query + split for better reading --- src/libs/database/impl/Session.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 4ea9f792..757bdcd3 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -152,22 +152,27 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); @@ -180,21 +185,30 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); } From 87daa5c7675a5f5aded1ed99ed0b9235d3ede9f1 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 19:01:28 +0200 Subject: [PATCH 48/64] Simplified queries + added some hints for the query planner for bad quality indexes --- src/libs/database/impl/Release.cpp | 49 +++++++++++++----------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index eafca66e..9e1f8217 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -148,8 +148,7 @@ namespace lms::db else if (params.clusters.size() > 1) { std::ostringstream oss; - oss << "r.id IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON t.release_id = r.id" + oss << "r.id IN (SELECT DISTINCT t.release_id FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; WhereClause clusterClause; @@ -334,17 +333,14 @@ namespace lms::db { session.checkReadTransaction(); - return utils::fetchQuerySingleResult(createQuery(session, "COUNT(DISTINCT r.id)", params)); + return utils::fetchQuerySingleResult(createQuery(session, "COUNT(r.id)", params)); } std::size_t Release::getDiscCount() const { assert(session()); int res{ utils::fetchQuerySingleResult(session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") - .join("release r ON r.id = t.release_id") - .where("r.id = ?") - .bind(getId())) }; - + .where("t.release_id = ?").bind(getId()))}; return res; } @@ -352,13 +348,10 @@ namespace lms::db { assert(session()); - using ResultType = std::tuple; const auto query{ session()->query("SELECT DISTINCT disc_number, disc_subtitle FROM track t") - .join("release r ON r.id = t.release_id") - .where("r.id = ?") - .orderBy("disc_number") - .bind(getId()) }; + .where("t.release_id = ?").bind(getId()) + .orderBy("disc_number")}; std::vector discs; utils::forEachQueryResult(query, [&](ResultType&& res) @@ -384,8 +377,8 @@ namespace lms::db assert(session()); const char* field{ original ? "original_date" : "date" }; - auto query{ (session()->query(std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?") + auto query{ (session()->query(std::string {"SELECT "} + "t." + field + " FROM track t") + .where("t.release_id = ?") .groupBy(field) .bind(getId())) }; @@ -413,8 +406,8 @@ namespace lms::db assert(session()); const char* field{ original ? "original_year" : "year" }; - auto query{ session()->query>(std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?").bind(getId()) + auto query{ session()->query>(std::string {"SELECT "} + "t." + field + " FROM track t") + .where("t.release_id = ?").bind(getId()) .groupBy(field) }; const auto years{ utils::fetchQueryResults(query) }; @@ -477,12 +470,12 @@ namespace lms::db assert(session()); const auto query{ session()->query>( - "SELECT DISTINCT a FROM artist a" + "SELECT a FROM artist a" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN track t ON t.id = t_a_l.track_id" - " INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?").bind(getId()) - .where("t_a_l.type = ?").bind(linkType) }; + " INNER JOIN track t ON t.id = t_a_l.track_id") + .where("t.release_id = ?").bind(getId()) + .where("+t_a_l.type = ?").bind(linkType) // adding + since the query planner does not a good job when analyze is not performed + .groupBy("a.id") }; return utils::fetchQueryResults(query); } @@ -560,16 +553,16 @@ namespace lms::db using milli = std::chrono::duration; - return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") - .where("r.id = ?").bind(getId())); + return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t") + .where("t.release_id = ?").bind(getId())); } Wt::WDateTime Release::getLastWritten() const { assert(session()); - return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") - .where("r.id = ?").bind(getId())); + 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())); } std::vector> Release::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const @@ -580,13 +573,13 @@ namespace lms::db std::ostringstream oss; - oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id "; + oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id "; - where.And(WhereClause("r.id = ?")).bind(getId().toString()); + where.And(WhereClause("t.release_id = ?")).bind(getId().toString()); { WhereClause clusterClause; for (const ClusterTypeId clusterTypeId : clusterTypeIds) - clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString()); + clusterClause.Or(WhereClause("+c_type.id = ?")).bind(clusterTypeId.toString()); // Exclude this since the query planner does not do a good job when db is not analyzed where.And(clusterClause); } oss << " " << where.get(); From 42315395fd6a6c56678511524aeb13d9d752db46 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 19:04:52 +0200 Subject: [PATCH 49/64] Simplified queries --- src/libs/database/impl/Artist.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index ac944733..d038fbf3 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -97,21 +97,18 @@ namespace lms::db else if (params.clusters.size() > 1) { std::ostringstream oss; - oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" - " INNER JOIN track t ON t.id = t_a_l.track_id" - " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l" + " INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id"; WhereClause clusterClause; for (const ClusterId clusterId : params.clusters) { - clusterClause.Or(WhereClause("c.id = ?")); + clusterClause.Or(WhereClause("t_c.cluster_id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; + oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.clusters.size() << ")"; query.where(oss.str()); } From a5b7e7e5f34e56ae2a4e9ed308da54d9c0357f21 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 17 Apr 2024 19:27:51 +0200 Subject: [PATCH 50/64] Added some hints for the query planner for bad quality indexes --- src/libs/database/impl/Cluster.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index e92d9faa..7c9a3d3f 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -39,7 +39,8 @@ namespace lms::db { session.checkReadTransaction(); - auto query{ session.getDboSession()->query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") }; + auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " FROM cluster c") }; + query.groupBy("c.id"); if (params.track.isValid() || params.release.isValid()) { @@ -56,7 +57,7 @@ namespace lms::db assert(!params.clusterType.isValid() || params.clusterTypeName.empty()); if (params.clusterType.isValid()) - query.where("c.cluster_type_id = ?").bind(params.clusterType); + query.where("+c.cluster_type_id = ?").bind(params.clusterType); // Exclude this since the query planner does not do a good job when db is not analyzed else if (!params.clusterTypeName.empty()) query.where("c_t.name = ?").bind(params.clusterTypeName); From c63f9fa51c2d7ca4efc3fcf433b5322e7a87a555 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 18 Apr 2024 09:06:27 +0200 Subject: [PATCH 51/64] Simplified queries --- src/libs/database/impl/Artist.cpp | 33 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index d038fbf3..7c46386c 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -50,15 +50,28 @@ namespace lms::db || params.clusters.size() == 1 || params.mediaLibrary.isValid()) { - query.join("track t ON t.id = t_a_l.track_id"); query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); } - if (params.linkType) - query.where("t_a_l.type = ?").bind(*params.linkType); + if (params.sortMethod == ArtistSortMethod::LastWritten + || params.writtenAfter.isValid() + || params.release.isValid() + || params.mediaLibrary.isValid()) + { + query.join("track t ON t.id = t_a_l.track_id"); - if (params.writtenAfter.isValid()) - query.where("t.file_last_write > ?").bind(params.writtenAfter); + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (params.release.isValid()) + query.where("t.release_id = ?").bind(params.release); + + if (params.mediaLibrary.isValid()) + query.where("t.media_library_id = ?").bind(params.mediaLibrary); + } + + if (params.linkType) + query.where("+t_a_l.type = ?").bind(*params.linkType); // Exclude this since the query planner does not do a good job when db is not analyzed if (!params.keywords.empty()) { @@ -91,7 +104,7 @@ namespace lms::db if (params.clusters.size() == 1) { - query.join("track_cluster t_c ON t_c.track_id = t.id") + query.join("track_cluster t_c ON t_c.track_id = t_a_l.track_id") .where("t_c.cluster_id = ?").bind(params.clusters.front()); } else if (params.clusters.size() > 1) @@ -114,13 +127,7 @@ namespace lms::db } if (params.track.isValid()) - query.where("t.id = ?").bind(params.track); - - if (params.release.isValid()) - query.where("t.release_id = ?").bind(params.release); - - if (params.mediaLibrary.isValid()) - query.where("t.media_library_id = ?").bind(params.mediaLibrary); + query.where("t_a_l.track_id = ?").bind(params.track); switch (params.sortMethod) { From 1862cd23c2242a4497c4e67f798a2d4dcbc9e537 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 18 Apr 2024 18:46:35 +0200 Subject: [PATCH 52/64] Changed db optimization strategy: now perform a full analyze (only after scan if enough changes has been made, and during application startup). Also perform a vacuum if needed during startup. Added an init screen for the web interface since it can take a while to complete --- approot/messages.xml | 1 + approot/messages_fr.xml | 1 + approot/messages_it.xml | 1 + approot/messages_zh.xml | 1 + src/libs/core/include/core/LiteralString.hpp | 7 + src/libs/database/impl/Db.cpp | 12 +- src/libs/database/impl/Session.cpp | 241 ++++++++++-------- .../database/include/database/Session.hpp | 13 +- src/libs/database/test/Common.cpp | 4 +- src/libs/services/scanner/CMakeLists.txt | 1 + src/libs/services/scanner/impl/IScanStep.hpp | 4 +- .../services/scanner/impl/ScanStepAnalyze.cpp | 53 ++++ .../services/scanner/impl/ScanStepAnalyze.hpp | 36 +++ .../impl/ScanStepCheckDuplicatedDbFiles.hpp | 2 +- .../impl/ScanStepComputeClusterStats.cpp | 1 + .../impl/ScanStepComputeClusterStats.hpp | 2 +- .../scanner/impl/ScanStepDiscoverFiles.hpp | 2 +- .../impl/ScanStepRemoveOrphanDbFiles.hpp | 2 +- .../scanner/impl/ScanStepScanFiles.cpp | 4 - .../scanner/impl/ScanStepScanFiles.hpp | 2 +- .../services/scanner/impl/ScannerService.cpp | 13 +- .../include/services/scanner/ScannerStats.hpp | 3 +- src/lms/CMakeLists.txt | 1 + src/lms/main.cpp | 31 ++- src/lms/ui/LmsInitApplication.cpp | 38 +++ src/lms/ui/LmsInitApplication.hpp | 33 +++ src/lms/ui/admin/ScannerController.cpp | 7 + 27 files changed, 382 insertions(+), 134 deletions(-) create mode 100644 src/libs/services/scanner/impl/ScanStepAnalyze.cpp create mode 100644 src/libs/services/scanner/impl/ScanStepAnalyze.hpp create mode 100644 src/lms/ui/LmsInitApplication.cpp create mode 100644 src/lms/ui/LmsInitApplication.hpp diff --git a/approot/messages.xml b/approot/messages.xml index e78a91e9..c494f326 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -105,6 +105,7 @@ Not scheduled Scheduled on {1} Scanning: step {1}/{2} +Analyzing database... {1}/{2} entries ({3}%)... Checking for duplicate files... {1} files Checking files... {1}% Computing stats... {1}% diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index b0627408..797355a1 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -105,6 +105,7 @@ Non planifié Planifié le {1} En cours de scan : étape {1}/{2} +Analyze de la base de données... {1}/{2} entrées ({3}%)... Vérification des fichiers dupliqués... {1} fichiers Vérification des fichiers... {1}% Calcul des statistiques... {1}% diff --git a/approot/messages_it.xml b/approot/messages_it.xml index aa675ec5..640b1091 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -105,6 +105,7 @@ Non pianificato Pianificato il {1} Scansione: passo {1}/{2} +Analisi del database... {1}/{2} voci ({3}%)... Controllo duplicati... {1} files Controllo file... {1}% Calcolo statistiche... {1}% diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 3a89f689..6a8ac377 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -106,6 +106,7 @@ 计划于 {1} 扫描中: 阶段 {1}/{2} + 检查文件中... {1}% 检索文件中: {1} 文件 diff --git a/src/libs/core/include/core/LiteralString.hpp b/src/libs/core/include/core/LiteralString.hpp index 7c0394bd..11b7f668 100644 --- a/src/libs/core/include/core/LiteralString.hpp +++ b/src/libs/core/include/core/LiteralString.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -41,6 +42,12 @@ namespace lms::core private: std::string_view _str; }; + + inline std::ostream& operator<<(std::ostream& os, const LiteralString& str) + { + os << str.str(); + return os; + } } namespace std diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index d756d1f5..23afed43 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -25,8 +25,9 @@ #include "database/Session.hpp" #include "database/User.hpp" #include "core/IConfig.hpp" -#include "core/Service.hpp" #include "core/ILogger.hpp" +#include "core/ITraceLogger.hpp" +#include "core/Service.hpp" namespace lms::db { @@ -49,7 +50,7 @@ namespace lms::db prepare(); } - ~Connection() + ~Connection() override { // make use of per-connection usage stats to optimize optimize(); @@ -66,16 +67,15 @@ namespace lms::db void prepare() { LMS_LOG(DB, DEBUG, "Setting per-connection settings..."); - executeSql("pragma journal_mode=WAL"); - executeSql("pragma synchronous=normal"); - executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries + executeSql("PRAGMA journal_mode=WAL"); + executeSql("PRAGMA synchronous=normal"); LMS_LOG(DB, DEBUG, "Setting per-connection settings done!"); } void optimize() { LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize..."); - executeSql("pragma optimize"); + executeSql("PRAGMA optimize"); LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete"); } diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 757bdcd3..ded20802 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -46,6 +46,7 @@ #include "EnumSetTraits.hpp" #include "PathTraits.hpp" #include "Migration.hpp" +#include "Utils.hpp" namespace lms::db { @@ -120,7 +121,7 @@ namespace lms::db return ReadTransaction{ _session }; } - void Session::prepareTables() + void Session::prepareTablesIfNeeded() { LMS_LOG(DB, INFO, "Preparing tables..."); @@ -140,127 +141,165 @@ namespace lms::db throw e; } } + } + void Session::migrateIfNeeded() + { Migration::doDbMigration(*this); - // Indexes - { - LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); - - auto transaction{ createWriteTransaction() }; - _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); - - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); - - _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); - - _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); - - _session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); - - _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)"); - - _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); - } - - // Singletons + // TODO: move this elsewhere { auto uniqueTransaction{ createWriteTransaction() }; ScanSettings::init(*this); } } + void Session::createIndexesIfNeeded() + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); + + auto transaction{ createWriteTransaction() }; + _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); + + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); + + _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); + + _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); + + _session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); + + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)"); + + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); + } + + void Session::vacuumIfNeeded() + { + long pageCount{}; + long freeListCount{}; + + { + auto transaction{ createReadTransaction() }; + pageCount = utils::fetchQuerySingleResult(_session.query("SELECT page_count FROM pragma_page_count")); + freeListCount = utils::fetchQuerySingleResult(_session.query("SELECT freelist_count FROM pragma_freelist_count")); + } + + LMS_LOG(DB, INFO, "page stats: page_count = " << pageCount << ", freelist_count = " << freeListCount); + if (freeListCount >= (pageCount / 10)) + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum"); + LMS_LOG(DB, INFO, "Performing vacuum... This may take a while..."); + + // We manually take a lock here since vacuum cannot be inside a transaction + { + std::unique_lock lock{ _db.getMutex() }; + _db.executeSql("VACUUM"); + } + + LMS_LOG(DB, INFO, "Vacuum complete!"); + } + } + void Session::refreshTracingLoggerStats() { auto* traceLogger{ core::Service::get() }; if (!traceLogger) return; - auto& dbSession{ _db.getTLSSession() }; - auto transaction{ dbSession.createReadTransaction() }; + auto transaction{ createReadTransaction() }; - traceLogger->setMetadata("db_artist_count", std::to_string(db::Artist::getCount(dbSession))); - traceLogger->setMetadata("db_cluster_count", std::to_string(db::Cluster::getCount(dbSession))); - traceLogger->setMetadata("db_cluster_type_count", std::to_string(db::ClusterType::getCount(dbSession))); - traceLogger->setMetadata("db_starred_artist_count", std::to_string(db::StarredArtist::getCount(dbSession))); - traceLogger->setMetadata("db_starred_release_count", std::to_string(db::StarredRelease::getCount(dbSession))); - traceLogger->setMetadata("db_starred_track_count", std::to_string(db::StarredTrack::getCount(dbSession))); - traceLogger->setMetadata("db_track_bookmark_count", std::to_string(db::TrackBookmark::getCount(dbSession))); - traceLogger->setMetadata("db_listen_count", std::to_string(db::Listen::getCount(dbSession))); - traceLogger->setMetadata("db_release_count", std::to_string(db::Release::getCount(dbSession))); - traceLogger->setMetadata("db_track_count", std::to_string(db::Track::getCount(dbSession))); + traceLogger->setMetadata("db_artist_count", std::to_string(db::Artist::getCount(*this))); + traceLogger->setMetadata("db_cluster_count", std::to_string(db::Cluster::getCount(*this))); + traceLogger->setMetadata("db_cluster_type_count", std::to_string(db::ClusterType::getCount(*this))); + traceLogger->setMetadata("db_starred_artist_count", std::to_string(db::StarredArtist::getCount(*this))); + traceLogger->setMetadata("db_starred_release_count", std::to_string(db::StarredRelease::getCount(*this))); + traceLogger->setMetadata("db_starred_track_count", std::to_string(db::StarredTrack::getCount(*this))); + traceLogger->setMetadata("db_track_bookmark_count", std::to_string(db::TrackBookmark::getCount(*this))); + traceLogger->setMetadata("db_listen_count", std::to_string(db::Listen::getCount(*this))); + traceLogger->setMetadata("db_release_count", std::to_string(db::Release::getCount(*this))); + traceLogger->setMetadata("db_track_count", std::to_string(db::Track::getCount(*this))); } - void Session::analyze() + void Session::fullAnalyze() { - LMS_SCOPED_TRACE_DETAILED("Database", "Analyze"); - LMS_LOG(DB, INFO, "Analyzing database..."); + LMS_SCOPED_TRACE_OVERVIEW("Database", "Analyze"); + LMS_LOG(DB, INFO, "Performing database analyze... This may take a while..."); + + // first select all the tables and indexes, and then analyze one by one in order to not have a big lock + std::vector entries; + retrieveEntriesToAnalyze(entries); + + for (const std::string& entry : entries) + analyzeEntry(entry); + + LMS_LOG(DB, INFO, "Analyze complete!"); + } + + void Session::retrieveEntriesToAnalyze(std::vector& entryList) + { + auto transaction{ createReadTransaction() }; + entryList = utils::fetchQueryResults(_session.query("SELECT name FROM sqlite_master WHERE type='table' OR type ='index'")); + } + + void Session::analyzeEntry(const std::string& entry) + { + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "AnalyzeEntry", "Entry", entry); + LMS_LOG(DB, DEBUG, "Analyzing " << entry); { auto transaction{ createWriteTransaction() }; - _session.execute("ANALYZE"); + _session.execute("ANALYZE " + entry); } - LMS_LOG(DB, INFO, "Database Analyze complete"); + LMS_LOG(DB, DEBUG, "Analyzing " << entry << ": done!"); } - - void Session::optimize() - { - LMS_SCOPED_TRACE_DETAILED("Database", "Optimize"); - LMS_LOG(DB, INFO, "Optimizing database..."); - { - auto transaction{ createWriteTransaction() }; - _session.execute("PRAGMA optimize"); - } - LMS_LOG(DB, INFO, "Database optimizing complete"); - } - } // namespace lms::db diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index d2d10083..2c8352af 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include "core/ITraceLogger.hpp" #include "core/RecursiveSharedMutex.hpp" #include "database/Object.hpp" @@ -84,10 +86,15 @@ namespace lms::db #endif } - void analyze(); - void optimize(); + // All these methods will acquire transactions + void fullAnalyze(); // helper for retrieveEntriesToAnalyze + analyzeEntry + void retrieveEntriesToAnalyze(std::vector& entryList); + void analyzeEntry(const std::string& entry); - void prepareTables(); // need to run only once at startup + void prepareTablesIfNeeded(); // need to run only once at startup + void migrateIfNeeded(); + void createIndexesIfNeeded(); + void vacuumIfNeeded(); void refreshTracingLoggerStats(); // returning a ptr here to ease further wrapping using operator-> diff --git a/src/libs/database/test/Common.cpp b/src/libs/database/test/Common.cpp index ac3089ab..d78a8557 100644 --- a/src/libs/database/test/Common.cpp +++ b/src/libs/database/test/Common.cpp @@ -60,8 +60,8 @@ namespace lms::db::tests _tmpDb = std::make_unique(); { db::Session s{ _tmpDb->getDb() }; - s.prepareTables(); - s.analyze(); + s.prepareTablesIfNeeded(); + s.createIndexesIfNeeded(); } } diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 2993415a..4da8e7b8 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(lmsscanner SHARED impl/ScannerService.cpp impl/ScannerStats.cpp + impl/ScanStepAnalyze.cpp impl/ScanStepCheckDuplicatedDbFiles.cpp impl/ScanStepComputeClusterStats.cpp impl/ScanStepDiscoverFiles.cpp diff --git a/src/libs/services/scanner/impl/IScanStep.hpp b/src/libs/services/scanner/impl/IScanStep.hpp index e131da4b..553a8203 100644 --- a/src/libs/services/scanner/impl/IScanStep.hpp +++ b/src/libs/services/scanner/impl/IScanStep.hpp @@ -19,9 +19,9 @@ #pragma once -#include #include +#include "core/LiteralString.hpp" #include "services/scanner/ScannerStats.hpp" namespace lms::scanner @@ -32,7 +32,7 @@ namespace lms::scanner virtual ~IScanStep() = default; virtual ScanStep getStep() const = 0; - virtual std::string_view getStepName() const = 0; + virtual core::LiteralString getStepName() const = 0; struct ScanContext { diff --git a/src/libs/services/scanner/impl/ScanStepAnalyze.cpp b/src/libs/services/scanner/impl/ScanStepAnalyze.cpp new file mode 100644 index 00000000..91731641 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepAnalyze.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 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 "ScanStepAnalyze.hpp" + +#include "core/ILogger.hpp" +#include "database/Db.hpp" +#include "database/Session.hpp" + +namespace lms::scanner +{ + void ScanStepAnalyze::process(ScanContext& context) + { + ScanStats& stats{ context.stats }; + + if (stats.nbChanges() > (stats.nbFiles() / 5)) + { + LMS_LOG(DBUPDATER, INFO, "Database changed substantially: triggering full analyze"); + + auto& session{ _db.getTLSSession() }; + + std::vector entries; + session.retrieveEntriesToAnalyze(entries); + context.currentStepStats.totalElems = entries.size(); + + for (const std::string& entry : entries) + { + if (_abortScan) + break; + + _db.getTLSSession().analyzeEntry(entry); + context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); + } + } + } +} diff --git a/src/libs/services/scanner/impl/ScanStepAnalyze.hpp b/src/libs/services/scanner/impl/ScanStepAnalyze.hpp new file mode 100644 index 00000000..9a1763cb --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepAnalyze.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "ScanStepBase.hpp" + +namespace lms::scanner +{ + class ScanStepAnalyze : public ScanStepBase + { + public: + using ScanStepBase::ScanStepBase; + + private: + ScanStep getStep() const override { return ScanStep::Analyze; } + core::LiteralString getStepName() const override { return "Analyze"; } + void process(ScanContext& context) override; + }; +} diff --git a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp index 3599791d..ba02d7ac 100644 --- a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp @@ -29,7 +29,7 @@ namespace lms::scanner using ScanStepBase::ScanStepBase; private: - std::string_view getStepName() const override { return "Checking for duplicated files"; } + core::LiteralString getStepName() const override { return "Checking for duplicated files"; } ScanStep getStep() const override { return ScanStep::CheckingForDuplicateFiles; } void process(ScanContext& context) override; }; diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp index a62d354e..331973fc 100644 --- a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp @@ -79,6 +79,7 @@ namespace lms::scanner } context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); } return true; diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp index 8000f454..43aeaf36 100644 --- a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp @@ -30,7 +30,7 @@ namespace lms::scanner private: ScanStep getStep() const override { return ScanStep::ComputeClusterStats; } - std::string_view getStepName() const override { return "Compute cluster stats"; } + core::LiteralString getStepName() const override { return "Compute cluster stats"; } void process(ScanContext& context) override; }; } diff --git a/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp b/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp index 60f3e240..2a56a3b9 100644 --- a/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp @@ -30,7 +30,7 @@ namespace lms::scanner private: ScanStep getStep() const override { return ScanStep::DiscoveringFiles; } - std::string_view getStepName() const override { return "DiscoveringFiles"; } + core::LiteralString getStepName() const override { return "Discovering files"; } void process(ScanContext& context) override; }; } diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp index 5b56fd90..92c467ad 100644 --- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp @@ -31,7 +31,7 @@ namespace lms::scanner using ScanStepBase::ScanStepBase; private: - std::string_view getStepName() const override { return "Checking orphaned entries"; } + core::LiteralString getStepName() const override { return "Checking orphaned entries"; } ScanStep getStep() const override { return ScanStep::ChekingForMissingFiles; } void process(ScanContext& context) override; diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index cd2606a7..28bd7bc7 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -518,10 +518,6 @@ namespace lms::scanner context.stats.scans++; processFileMetaData(context, scanResult.path, *scanResult.trackMetaData, libraryInfo); - - // optimize the database during scan (if we import a very large database, it may be too late to do it once at end) - if ((context.stats.scans % 1'000) == 0) - _db.getTLSSession().optimize(); } else { diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp index d656ea99..05398a39 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp @@ -40,7 +40,7 @@ namespace lms::scanner private: ScanStep getStep() const override { return ScanStep::ScanningFiles; } - std::string_view getStepName() const override { return "Scanning files"; } + core::LiteralString getStepName() const override { return "Scanning files"; } void process(ScanContext& context) override; bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo); diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 356cbcbb..fc82af62 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -25,15 +25,17 @@ #include "database/TrackFeatures.hpp" #include "database/ScanSettings.hpp" #include "core/Exception.hpp" +#include "core/Path.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" -#include "core/Path.hpp" +#include "core/ITraceLogger.hpp" +#include "ScanStepAnalyze.hpp" #include "ScanStepCheckDuplicatedDbFiles.hpp" +#include "ScanStepComputeClusterStats.hpp" #include "ScanStepDiscoverFiles.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepScanFiles.hpp" -#include "ScanStepComputeClusterStats.hpp" namespace lms::scanner { @@ -259,6 +261,8 @@ namespace lms::scanner void ScannerService::scan(bool forceScan) { + LMS_SCOPED_TRACE_OVERVIEW("Scanner", "Scan"); + _events.scanStarted.emit(); { @@ -278,6 +282,8 @@ namespace lms::scanner for (auto& scanStep : _scanSteps) { + LMS_SCOPED_TRACE_OVERVIEW("Scanner", scanStep->getStepName()); + LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'"); scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() }; @@ -289,8 +295,6 @@ namespace lms::scanner LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size()); - _db.getTLSSession().analyze(); - if (!_abortScan) { stats.stopTime = Wt::WDateTime::currentDateTime(); @@ -348,6 +352,7 @@ namespace lms::scanner _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); } ScannerSettings ScannerService::readSettings() diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index 02094980..ed5dffe8 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -66,8 +66,9 @@ namespace lms::scanner FetchingTrackFeatures, ReloadingSimilarityEngine, ComputeClusterStats, + Analyze, }; - static inline constexpr unsigned ScanProgressStepCount{ 7 }; + static inline constexpr unsigned ScanProgressStepCount{ 8 }; // reduced scan stats struct ScanStepStats diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index a21e8098..2a877826 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(lms ui/Auth.cpp ui/LmsApplication.cpp ui/LmsApplicationManager.cpp + ui/LmsInitApplication.cpp ui/LmsTheme.cpp ui/MediaPlayer.cpp ui/ModalManager.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index dd6c684c..dfe1f0b8 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -40,6 +40,7 @@ #include "subsonic/SubsonicResource.hpp" #include "ui/LmsApplication.hpp" #include "ui/LmsApplicationManager.hpp" +#include "ui/LmsInitApplication.hpp" #include "core/IChildProcessManager.hpp" #include "core/IConfig.hpp" #include "core/IOContextRunner.hpp" @@ -275,20 +276,34 @@ namespace lms Wt::WServer server{ argv[0] }; server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); + // As initialization can take a while (db migration, analyze, etc.), we bind a temporary init entry point to warn the user + server.addEntryPoint(Wt::EntryPointType::Application, + [&](const Wt::WEnvironment& env) + { + return ui::LmsInitApplication::create(env); + }); + + LMS_LOG(MAIN, INFO, "Starting init web server..."); + server.start(); + core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" }; // Connection pool size must be twice the number of threads: we have at least 2 io pools with getThreadCount() each and they all may access the database db::Db database{ config->getPath("working-dir") / "lms.db", getThreadCount() * 2 }; { db::Session session{ database }; - session.prepareTables(); + session.prepareTablesIfNeeded(); + session.createIndexesIfNeeded(); + + // As this may be quite long, we only do it during startup + session.vacuumIfNeeded(); // force optimize in case scanner aborted during a large import: - // queries may be too slow to even be able to relaunch a scan sing the web interface - session.analyze(); + // queries may be too slow to even be able to relaunch a scan using the web interface + session.fullAnalyze(); database.getTLSSession().refreshTracingLoggerStats(); } - + ui::LmsApplicationManager appManager; // Service initialization order is important (reverse-order for deinit) @@ -327,8 +342,12 @@ namespace lms core::Service feedbackService{ feedback::createFeedbackService(ioContext, database) }; core::Service scrobblingService{ scrobbling::createScrobblingService(ioContext, database) }; - std::unique_ptr subsonicResource; + LMS_LOG(MAIN, INFO, "Stopping init web server..."); + server.stop(); + server.removeEntryPoint(""); + + std::unique_ptr subsonicResource; // bind API resources if (config->getBool("api-subsonic", true)) { @@ -345,7 +364,7 @@ namespace lms proxyScannerEventsToApplication(*scannerService, server); - LMS_LOG(MAIN, INFO, "Starting server..."); + LMS_LOG(MAIN, INFO, "Starting init web server..."); server.start(); LMS_LOG(MAIN, INFO, "Now running..."); diff --git a/src/lms/ui/LmsInitApplication.cpp b/src/lms/ui/LmsInitApplication.cpp new file mode 100644 index 00000000..4870b6ee --- /dev/null +++ b/src/lms/ui/LmsInitApplication.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "LmsInitApplication.hpp" + +namespace lms::ui +{ + LmsInitApplication::LmsInitApplication(const Wt::WEnvironment& env) + : Wt::WApplication{ env } + { + enableUpdates(true); + root()->addNew("LMS is initializing. This may take a while, please wait..."); + } + + std::unique_ptr LmsInitApplication::create(const Wt::WEnvironment& env) + { + return std::make_unique(env); + } +} // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/LmsInitApplication.hpp b/src/lms/ui/LmsInitApplication.hpp new file mode 100644 index 00000000..2e6d4a8d --- /dev/null +++ b/src/lms/ui/LmsInitApplication.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace lms::ui +{ + class LmsInitApplication : public Wt::WApplication + { + public: + LmsInitApplication(const Wt::WEnvironment& env); + + static std::unique_ptr create(const Wt::WEnvironment& env); + }; +} // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 65f08fd6..d7eb29a7 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -274,6 +274,13 @@ namespace lms::ui case scanner::ScanStep::ComputeClusterStats: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats") .arg(status.currentScanStepStats->progress())); + + case scanner::ScanStep:: Analyze: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-analyze") + .arg(status.currentScanStepStats->processedElems) + .arg(status.currentScanStepStats->totalElems) + .arg(status.currentScanStepStats->progress())); + break; } break; } From 38d011506972d4ff94a87b01c62e94d9f2b38b8b Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 08:50:22 +0200 Subject: [PATCH 53/64] Added missing break --- src/lms/ui/admin/ScannerController.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index d7eb29a7..c7c7d305 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -274,6 +274,7 @@ namespace lms::ui case scanner::ScanStep::ComputeClusterStats: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats") .arg(status.currentScanStepStats->progress())); + break; case scanner::ScanStep:: Analyze: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-analyze") From 697233dcef33c32b9479a11108f1a1336a8a8a92 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 08:56:48 +0200 Subject: [PATCH 54/64] Fixed results inconsistencies when using search3 --- src/libs/database/impl/Artist.cpp | 7 +++++-- src/libs/database/impl/Release.cpp | 3 +++ src/libs/database/impl/Track.cpp | 3 +++ src/libs/database/include/database/Types.hpp | 7 +++++-- src/libs/database/test/Artist.cpp | 10 +++++----- src/libs/services/scanner/impl/ScanStepAnalyze.cpp | 2 +- src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp | 2 +- src/libs/subsonic/impl/entrypoints/Browsing.cpp | 4 ++-- src/libs/subsonic/impl/entrypoints/Searching.cpp | 3 +++ src/lms/ui/explore/ArtistCollector.cpp | 2 +- 10 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 7c46386c..bb070c5d 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -133,10 +133,13 @@ namespace lms::db { case ArtistSortMethod::None: break; - case ArtistSortMethod::ByName: + case ArtistSortMethod::Id: + query.orderBy("a.id"); + break; + case ArtistSortMethod::Name: query.orderBy("a.name COLLATE NOCASE"); break; - case ArtistSortMethod::BySortName: + case ArtistSortMethod::SortName: query.orderBy("a.sort_name COLLATE NOCASE"); break; case ArtistSortMethod::Random: diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 9e1f8217..23997c67 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -168,6 +168,9 @@ namespace lms::db { case ReleaseSortMethod::None: break; + case ReleaseSortMethod::Id: + query.orderBy("r.id"); + break; case ReleaseSortMethod::Name: query.orderBy("r.name COLLATE NOCASE"); break; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index f2552dbb..5a459397 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -153,6 +153,9 @@ namespace lms::db { case TrackSortMethod::None: break; + case TrackSortMethod::Id: + query.orderBy("t.id"); + break; case TrackSortMethod::LastWritten: query.orderBy("t.file_last_write DESC"); break; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index e9cc5a2a..506355b4 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -108,8 +108,9 @@ namespace lms::db enum class ArtistSortMethod { None, - ByName, - BySortName, + Id, + Name, + SortName, Random, LastWritten, StarredDateDesc, @@ -118,6 +119,7 @@ namespace lms::db enum class ReleaseSortMethod { None, + Id, Name, ArtistNameThenName, Date, @@ -138,6 +140,7 @@ namespace lms::db enum class TrackSortMethod { None, + Id, Random, LastWritten, StarredDateDesc, diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index bd486544..49d3e729 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -588,21 +588,21 @@ namespace lms::db::tests } { - const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) }; + const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::Name)) }; ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], artist1.getId()); EXPECT_EQ(artists.results[1], artist4.getId()); } { - const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; + const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::Name)) }; ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], artist2.getId()); EXPECT_EQ(artists.results[1], artist5.getId()); } { - const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; + const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::Name)) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results[0], artist3.getId()); } @@ -624,8 +624,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) }; - auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) }; + auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Name)) }; + auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::SortName)) }; ASSERT_EQ(allArtistsByName.results.size(), 2); EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); diff --git a/src/libs/services/scanner/impl/ScanStepAnalyze.cpp b/src/libs/services/scanner/impl/ScanStepAnalyze.cpp index 91731641..f7101db9 100644 --- a/src/libs/services/scanner/impl/ScanStepAnalyze.cpp +++ b/src/libs/services/scanner/impl/ScanStepAnalyze.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Emeric Poupon + * Copyright (C) 2024 Emeric Poupon * * This file is part of LMS. * diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 3aecebcd..ec43f5d2 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -197,7 +197,7 @@ namespace lms::api::subsonic { feedback::IFeedbackService::ArtistFindParameters artistFindParams; artistFindParams.setUser(context.userId); - artistFindParams.setSortMethod(ArtistSortMethod::BySortName); + artistFindParams.setSortMethod(ArtistSortMethod::SortName); for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) { if (auto artist{ Artist::find(context.dbSession, artistId) }) diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index c85797c9..19d4777b 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -109,7 +109,7 @@ namespace lms::api::subsonic if (!user) throw UserNotAuthorizedError{}; - parameters.setSortMethod(ArtistSortMethod::BySortName); + parameters.setSortMethod(ArtistSortMethod::SortName); switch (user->getSubsonicArtistListMode()) { case SubsonicArtistListMode::AllArtists: @@ -329,7 +329,7 @@ namespace lms::api::subsonic directoryNode.setAttribute("name", "Music"); // TODO: this does not scale when a lot of artists are present - Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::BySortName), [&](const Artist::pointer& artist) + Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName), [&](const Artist::pointer& artist) { directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */)); }); diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index 502dd28d..6d672428 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -132,6 +132,7 @@ namespace lms::api::subsonic params.setKeywords(keywords); params.setRange(Range{ artistOffset, artistCount }); params.setMediaLibrary(mediaLibrary); + params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) { @@ -197,6 +198,7 @@ namespace lms::api::subsonic params.setKeywords(keywords); params.setRange(Range{ albumOffset, albumCount }); params.setMediaLibrary(mediaLibrary); + params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods Release::find(context.dbSession, params, [&](const Release::pointer& release) { @@ -262,6 +264,7 @@ namespace lms::api::subsonic params.setKeywords(keywords); params.setRange(Range{ songOffset, songCount }); params.setMediaLibrary(mediaLibrary); + params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods Track::find(context.dbSession, params, [&](const Track::pointer& track) { diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 91f464da..d927c86b 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -119,7 +119,7 @@ namespace lms::ui Artist::FindParameters params; params.setClusters(getFilters().getClusterIds()); params.setLinkType(_linkType); - params.setSortMethod(ArtistSortMethod::BySortName); + params.setSortMethod(ArtistSortMethod::SortName); params.setRange(range); { From 27120d9848580faea8421664d69d8e6038e465ed Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 11:19:45 +0200 Subject: [PATCH 55/64] Fixed regression on migration not performed --- src/lms/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lms/main.cpp b/src/lms/main.cpp index dfe1f0b8..6fbc7727 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -293,6 +293,7 @@ namespace lms { db::Session session{ database }; session.prepareTablesIfNeeded(); + session.migrateIfNeeded(); session.createIndexesIfNeeded(); // As this may be quite long, we only do it during startup From 4432def11ff4142c3f7db38491517082194834db Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 11:20:44 +0200 Subject: [PATCH 56/64] Added missing indexes, force index recreation to remove outdated indexes --- src/libs/database/impl/Migration.cpp | 11 ++++++++++- src/libs/database/impl/Session.cpp | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 127ca117..6d53b076 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -34,7 +34,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 56 }; + static constexpr Version LMS_DATABASE_VERSION{ 57 }; } VersionInfo::VersionInfo() @@ -452,6 +452,14 @@ SELECT session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } + 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) + session.getDboSession()->execute("DROP INDEX " + indexName); + } + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -485,6 +493,7 @@ SELECT {53, migrateFromV53}, {54, migrateFromV54}, {55, migrateFromV55}, + {56, migrateFromV56}, }; { diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index ded20802..ba0e366d 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -175,6 +175,7 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_date_time ON listen(user_id, backend, date_time DESC)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); @@ -191,6 +192,8 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_release_year_idx ON track(release_id, year)"); _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); @@ -206,8 +209,10 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); From d6b86a93605cd6f684979ff7035b45c0e903206a Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 12:01:23 +0200 Subject: [PATCH 57/64] Perform vacuum if migration is performed --- src/libs/database/impl/Migration.cpp | 6 +++- src/libs/database/impl/Migration.hpp | 2 +- src/libs/database/impl/Session.cpp | 32 ++++++++++++------- .../database/include/database/Session.hpp | 3 +- src/lms/main.cpp | 7 ++-- 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 6d53b076..426de394 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -460,7 +460,7 @@ SELECT session.getDboSession()->execute("DROP INDEX " + indexName); } - void doDbMigration(Session& session) + bool doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -496,6 +496,7 @@ SELECT {56, migrateFromV56}, }; + bool migrationPerformed{}; { LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration"); auto transaction{ session.createWriteTransaction() }; @@ -530,7 +531,10 @@ SELECT VersionInfo::get(session).modify()->setVersion(++version); LMS_LOG(DB, INFO, "Migration complete to version " << version); + migrationPerformed = true; } } + + return migrationPerformed; } } diff --git a/src/libs/database/impl/Migration.hpp b/src/libs/database/impl/Migration.hpp index 5263e9e7..ac528c8f 100644 --- a/src/libs/database/impl/Migration.hpp +++ b/src/libs/database/impl/Migration.hpp @@ -52,6 +52,6 @@ namespace lms::db namespace Migration { - void doDbMigration(Session& session); + bool doDbMigration(Session& session); // return true if migration was performed } } diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index ba0e366d..342d8cb4 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -143,20 +143,23 @@ namespace lms::db } } - void Session::migrateIfNeeded() + bool Session::migrateSchemaIfNeeded() { - Migration::doDbMigration(*this); + const bool migrationPerformed{ Migration::doDbMigration(*this) }; // TODO: move this elsewhere { auto uniqueTransaction{ createWriteTransaction() }; ScanSettings::init(*this); } + + return migrationPerformed; } void Session::createIndexesIfNeeded() { LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); + LMS_LOG(DB, INFO, "Creating indexes... This may take a while..."); auto transaction{ createWriteTransaction() }; _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); @@ -227,6 +230,8 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); + + LMS_LOG(DB, INFO, "Indexes created!"); } void Session::vacuumIfNeeded() @@ -242,18 +247,21 @@ namespace lms::db LMS_LOG(DB, INFO, "page stats: page_count = " << pageCount << ", freelist_count = " << freeListCount); if (freeListCount >= (pageCount / 10)) + vacuum(); + } + + void Session::vacuum() + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum"); + LMS_LOG(DB, INFO, "Performing vacuum... This may take a while..."); + + // We manually take a lock here since vacuum cannot be inside a transaction { - LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum"); - LMS_LOG(DB, INFO, "Performing vacuum... This may take a while..."); - - // We manually take a lock here since vacuum cannot be inside a transaction - { - std::unique_lock lock{ _db.getMutex() }; - _db.executeSql("VACUUM"); - } - - LMS_LOG(DB, INFO, "Vacuum complete!"); + std::unique_lock lock{ _db.getMutex() }; + _db.executeSql("VACUUM"); } + + LMS_LOG(DB, INFO, "Vacuum complete!"); } void Session::refreshTracingLoggerStats() diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index 2c8352af..7983e5d5 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -92,9 +92,10 @@ namespace lms::db void analyzeEntry(const std::string& entry); void prepareTablesIfNeeded(); // need to run only once at startup - void migrateIfNeeded(); + bool migrateSchemaIfNeeded(); // returns true if migration was performed void createIndexesIfNeeded(); void vacuumIfNeeded(); + void vacuum(); void refreshTracingLoggerStats(); // returning a ptr here to ease further wrapping using operator-> diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 6fbc7727..605114aa 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -293,11 +293,14 @@ namespace lms { db::Session session{ database }; session.prepareTablesIfNeeded(); - session.migrateIfNeeded(); + bool migrationPerformed{ session.migrateSchemaIfNeeded() }; session.createIndexesIfNeeded(); // As this may be quite long, we only do it during startup - session.vacuumIfNeeded(); + if (migrationPerformed) + session.vacuum(); + else + session.vacuumIfNeeded(); // force optimize in case scanner aborted during a large import: // queries may be too slow to even be able to relaunch a scan using the web interface From fd36527681a0301fb68c94f960fc7714d98bc83d Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 14:08:38 +0200 Subject: [PATCH 58/64] Bumped sqlite3 in released docker image --- Dockerfile-release | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile-release b/Dockerfile-release index 64679344..b78366dc 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -31,6 +31,7 @@ ARG BUILD_PACKAGES=" \ libarchive-dev \ libconfig-dev \ taglib-dev \ + sqlite-dev \ gtest-dev" RUN apk add --no-cache --update ${BUILD_PACKAGES} @@ -86,7 +87,7 @@ RUN \ RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF && \ + cmake -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 @@ -153,6 +154,7 @@ ARG RUNTIME_PACKAGES=" \ boost-thread \ libarchive \ libconfig++ \ + sqlite-libs \ taglib" ARG LMS_USER=lms From e16c4edb325107eccad60497931140ab4fb5aaf1 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 18:32:36 +0200 Subject: [PATCH 59/64] Improved query --- src/libs/database/impl/Artist.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index bb070c5d..92a0e307 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -41,7 +41,7 @@ namespace lms::db session.checkReadTransaction(); // TODO remove distinct and use group by - auto query{ session.getDboSession()->query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") }; + auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " FROM artist a") }; if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType @@ -154,6 +154,8 @@ namespace lms::db break; } + query.groupBy("a.id"); + return query; } From abd013ad29f030194b45dda2e710ef8e61b622c4 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Apr 2024 18:38:22 +0200 Subject: [PATCH 60/64] Fixed bad ids --- approot/admin-scannercontroller.xml | 2 +- approot/admin-user.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/approot/admin-scannercontroller.xml b/approot/admin-scannercontroller.xml index 1b971a16..d2660d0e 100644 --- a/approot/admin-scannercontroller.xml +++ b/approot/admin-scannercontroller.xml @@ -18,7 +18,7 @@ ${step-status class="form-control"}
-