Removed the database lib from services as it is still not a service
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
add_library(lmsdatabase SHARED
|
||||
impl/Artist.cpp
|
||||
impl/AuthToken.cpp
|
||||
impl/Cluster.cpp
|
||||
impl/Db.cpp
|
||||
impl/Listen.cpp
|
||||
impl/Migration.cpp
|
||||
impl/TrackArtistLink.cpp
|
||||
impl/TrackFeatures.cpp
|
||||
impl/TrackList.cpp
|
||||
impl/Release.cpp
|
||||
impl/ScanSettings.cpp
|
||||
impl/Session.cpp
|
||||
impl/StarredArtist.cpp
|
||||
impl/StarredRelease.cpp
|
||||
impl/StarredTrack.cpp
|
||||
impl/SqlQuery.cpp
|
||||
impl/Track.cpp
|
||||
impl/TrackBookmark.cpp
|
||||
impl/TransactionChecker.cpp
|
||||
impl/Types.cpp
|
||||
impl/User.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsdatabase INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsdatabase PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(lmsdatabase PRIVATE
|
||||
Wt::DboSqlite3
|
||||
)
|
||||
|
||||
target_link_libraries(lmsdatabase PUBLIC
|
||||
lmsutils
|
||||
std::filesystem
|
||||
Wt::Dbo
|
||||
)
|
||||
|
||||
install(TARGETS lmsdatabase DESTINATION lib)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "database/Artist.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "EnumSetTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Artist::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") };
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.linkType
|
||||
|| params.track.isValid()
|
||||
|| params.release.isValid()
|
||||
|| params.clusters.size() == 1)
|
||||
{
|
||||
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.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (!params.keywords.empty())
|
||||
{
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<std::string> sortClauses;
|
||||
|
||||
for (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)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
}
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.feedbackBackend);
|
||||
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
||||
.where("s_a.user_id = ?").bind(params.starringUser)
|
||||
.where("s_a.backend = ?").bind(*params.feedbackBackend)
|
||||
.where("s_a.sync_state <> ?").bind(SyncState::PendingRemove);
|
||||
}
|
||||
|
||||
if (params.clusters.size() == 1)
|
||||
{
|
||||
query.join("track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("t_c.cluster_id = ?").bind(params.clusters.front());
|
||||
}
|
||||
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";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
break;
|
||||
case ArtistSortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case ArtistSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case ArtistSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_a.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Artist::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, ArtistId>)
|
||||
itemToSelect = "a.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Artist>>)
|
||||
itemToSelect = "a";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
}
|
||||
|
||||
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name{ std::string(name, 0 , _maxNameLength) },
|
||||
_sortName{ _name },
|
||||
_MBID{ MBID ? MBID->getAsString() : "" }
|
||||
{
|
||||
}
|
||||
|
||||
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Artist> {new Artist{ name, MBID }});
|
||||
}
|
||||
|
||||
std::size_t Artist::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
|
||||
.where("name = ?").bind(std::string{ name, 0, _maxNameLength })
|
||||
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
|
||||
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Artist::pointer Artist::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
|
||||
}
|
||||
|
||||
Artist::pointer Artist::find(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
bool Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession().query<ArtistId>("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<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<ArtistId>(session, params) };
|
||||
return Utils::execQuery<ArtistId>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
|
||||
return Utils::execQuery<Artist::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Artist::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
|
||||
Utils::execQuery(query, params.range, func);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
oss <<
|
||||
"SELECT a.id 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 track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" t_c.cluster_id IN (SELECT DISTINCT c.id 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 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"
|
||||
" WHERE a.id = ?)"
|
||||
" AND a.id <> ?";
|
||||
|
||||
if (!artistLinkTypes.empty())
|
||||
{
|
||||
oss << " AND t_a_l.type IN (";
|
||||
|
||||
bool first{ true };
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
{
|
||||
(void)type;
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
auto query{ session()->query<ArtistId>(oss.str())
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()") };
|
||||
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
|
||||
return Utils::execQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
|
||||
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 artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id";
|
||||
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
|
||||
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>(oss.str());
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (Cluster::pointer cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void Artist::setSortName(const std::string& sortName)
|
||||
{
|
||||
_sortName = std::string(sortName, 0, _maxNameLength);
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/AuthToken.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
: _value {value}
|
||||
, _expiry {expiry}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<AuthToken> {new AuthToken {value, expiry, user}});
|
||||
}
|
||||
|
||||
void
|
||||
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now);
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::find(Session& session, std::string_view value)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value)
|
||||
.resultValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2016 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Cluster::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") };
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
{
|
||||
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
}
|
||||
if (!params.clusterTypeName.empty())
|
||||
query.join("cluster_type c_t ON c_t.id = c.cluster_type_id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
|
||||
if (params.clusterType.isValid())
|
||||
query.where("c.cluster_type_id = ?").bind(params.clusterType);
|
||||
else if (!params.clusterTypeName.empty())
|
||||
query.where("c_t.name = ?").bind(params.clusterTypeName);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Cluster::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, ClusterId>)
|
||||
itemToSelect = "c.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Cluster>>)
|
||||
itemToSelect = "c";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
}
|
||||
|
||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
: _name{ std::string {name, 0, _maxNameLength} },
|
||||
_clusterType{ getDboPtr(type) }
|
||||
{
|
||||
}
|
||||
|
||||
Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster{ type, name }});
|
||||
}
|
||||
|
||||
std::size_t Cluster::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
|
||||
RangeResults<ClusterId> Cluster::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<ClusterId>(session, params) };
|
||||
|
||||
return Utils::execQuery<ClusterId>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) };
|
||||
|
||||
return Utils::execQuery<Cluster::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterId> Cluster::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession().query<ClusterId>("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<ClusterId>(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer Cluster::find(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
std::size_t Cluster::computeTrackCount(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("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();
|
||||
}
|
||||
|
||||
std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("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();
|
||||
}
|
||||
|
||||
void Cluster::addTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
_tracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Cluster::getTracks(std::optional<Range> range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query{ session()->query<TrackId>("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<TrackId>(query, range);
|
||||
}
|
||||
|
||||
ClusterType::ClusterType(std::string_view name)
|
||||
: _name{ name }
|
||||
{
|
||||
}
|
||||
|
||||
ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
|
||||
}
|
||||
|
||||
std::size_t ClusterType::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
"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<ClusterTypeId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||
.join("cluster c ON c_t.id = c.cluster_type_id") };
|
||||
|
||||
return Utils::execQuery<ClusterTypeId>(query, range);
|
||||
}
|
||||
|
||||
ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("name = ?").bind(std::string{ name }).resultValue();
|
||||
}
|
||||
|
||||
ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type") };
|
||||
|
||||
return Utils::execQuery<ClusterTypeId>(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer ClusterType::getCluster(const std::string& name) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
return session()->find<Cluster>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> ClusterType::getClusters() const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
auto res = session()->find<Cluster>()
|
||||
.where("cluster_type_id = ?").bind(getId())
|
||||
.orderBy("name")
|
||||
.resultList();
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Db.hpp"
|
||||
|
||||
#include <Wt/Dbo/FixedSqlConnectionPool.h>
|
||||
#include <Wt/Dbo/backend/Sqlite3.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class Connection : public Wt::Dbo::backend::Sqlite3
|
||||
{
|
||||
public:
|
||||
Connection(const std::filesystem::path& dbPath)
|
||||
: Wt::Dbo::backend::Sqlite3{ dbPath.string() }
|
||||
, _dbPath{ dbPath }
|
||||
{
|
||||
prepare();
|
||||
}
|
||||
|
||||
Connection(const Connection& other)
|
||||
: Wt::Dbo::backend::Sqlite3{ other }
|
||||
, _dbPath{ other._dbPath }
|
||||
{
|
||||
prepare();
|
||||
}
|
||||
|
||||
~Connection()
|
||||
{
|
||||
// make use of per-connection usage stats to optimize
|
||||
optimize();
|
||||
}
|
||||
|
||||
private:
|
||||
Connection& operator=(const Connection&) = delete;
|
||||
|
||||
std::unique_ptr<SqlConnection> clone() const override
|
||||
{
|
||||
return std::make_unique<Connection>(*this);
|
||||
}
|
||||
|
||||
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
|
||||
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
|
||||
}
|
||||
|
||||
void optimize()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize...");
|
||||
executeSql("pragma optimize");
|
||||
LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete");
|
||||
}
|
||||
|
||||
std::filesystem::path _dbPath;
|
||||
};
|
||||
}
|
||||
|
||||
// Session living class handling the database and the login
|
||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath.string());
|
||||
|
||||
auto connection{ std::make_unique<Connection>(dbPath.string()) };
|
||||
if (IConfig * config{ Service<IConfig>::get() })// may not be here on testU
|
||||
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
|
||||
|
||||
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
|
||||
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
||||
|
||||
_connectionPool = std::move(connectionPool);
|
||||
}
|
||||
|
||||
void Db::executeSql(const std::string& sql)
|
||||
{
|
||||
ScopedConnection connection{ *_connectionPool };
|
||||
connection->executeSql(sql);
|
||||
}
|
||||
|
||||
Session& Db::getTLSSession()
|
||||
{
|
||||
static thread_local Session* tlsSession{};
|
||||
|
||||
if (!tlsSession)
|
||||
{
|
||||
auto newSession{ std::make_unique<Session>(*this) };
|
||||
tlsSession = newSession.get();
|
||||
|
||||
{
|
||||
std::scoped_lock lock{ _tlsSessionsMutex };
|
||||
_tlsSessions.push_back(std::move(newSession));
|
||||
}
|
||||
}
|
||||
|
||||
return *tlsSession;
|
||||
}
|
||||
|
||||
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
|
||||
: _connectionPool{ pool }
|
||||
, _connection{ _connectionPool.getConnection() }
|
||||
{
|
||||
}
|
||||
|
||||
Db::ScopedConnection::~ScopedConnection()
|
||||
{
|
||||
_connectionPool.returnConnection(std::move(_connection));
|
||||
}
|
||||
|
||||
Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const
|
||||
{
|
||||
return _connection.get();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include <Wt/Dbo/StdSqlTraits.h>
|
||||
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Wt::Dbo
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
struct sql_value_traits<EnumSet<T>, void> : public sql_value_traits<long long>
|
||||
{
|
||||
using ValueType = typename EnumSet<T>::ValueType;
|
||||
static_assert(sizeof(long long) > sizeof(ValueType));
|
||||
|
||||
static void bind(EnumSet<T> v, SqlStatement *statement, int column, int size)
|
||||
{
|
||||
sql_value_traits<long long>::bind(static_cast<long long>(v.getBitfield()), statement, column, size);
|
||||
}
|
||||
|
||||
static bool read(EnumSet<T>& v, SqlStatement *statement, int column, int size)
|
||||
{
|
||||
long long val;
|
||||
if (sql_value_traits<long long>::read(val, statement, column, size))
|
||||
{
|
||||
v.setBitfield(val);
|
||||
return true;
|
||||
}
|
||||
v.clear();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include <Wt/Dbo/StdSqlTraits.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Wt::Dbo
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
struct sql_value_traits<T, typename std::enable_if<std::is_base_of<Database::IdType, T>::value>::type>
|
||||
{
|
||||
static_assert(!std::is_same_v<Database::IdType, T>, "Cannot use IdType, use derived types");
|
||||
static const bool specialized = true;
|
||||
|
||||
static std::string type(SqlConnection *conn, int size)
|
||||
{
|
||||
return sql_value_traits<typename T::ValueType, void>::type(conn, size);
|
||||
}
|
||||
|
||||
static void bind(const T& v, SqlStatement *statement, int column, int size)
|
||||
{
|
||||
sql_value_traits<typename T::ValueType>::bind(v.getValue(), statement, column, size);
|
||||
}
|
||||
|
||||
static bool read(T& v, SqlStatement *statement, int column, int size)
|
||||
{
|
||||
typename T::ValueType value;
|
||||
if (sql_value_traits<typename T::ValueType>::read(value, statement, column, size))
|
||||
{
|
||||
v = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
v = {};
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
auto query{ session.query<ArtistId>("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")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.backend = ?").bind(backend) };
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
if (!clusterIds.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";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
||||
{
|
||||
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
|
||||
.join("track t ON t.release_id = r.id")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.backend = ?").bind(backend) };
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.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";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (ClusterId id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
||||
{
|
||||
auto query{ session.query<TrackId>("SELECT t.id from track t")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.backend = ?").bind(backend) };
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Database
|
||||
{
|
||||
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
|
||||
, _backend{ backend }
|
||||
, _user{ getDboPtr(user) }
|
||||
, _track{ getDboPtr(track) }
|
||||
{}
|
||||
|
||||
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, backend, dateTime }});
|
||||
}
|
||||
|
||||
std::size_t Listen::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
|
||||
}
|
||||
|
||||
Listen::pointer Listen::find(Session& session, ListenId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ListenId>("SELECT id FROM listen")
|
||||
.orderBy("date_time") };
|
||||
|
||||
if (parameters.user.isValid())
|
||||
query.where("user_id = ?").bind(parameters.user);
|
||||
|
||||
if (parameters.backend)
|
||||
query.where("backend = ?").bind(*parameters.backend);
|
||||
|
||||
if (parameters.syncState)
|
||||
query.where("sync_state = ?").bind(*parameters.syncState);
|
||||
|
||||
return Utils::execQuery<ListenId>(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<Listen>()
|
||||
.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();
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) };
|
||||
|
||||
auto collection{ query
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.groupBy("a.id") };
|
||||
|
||||
return Utils::execQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id") };
|
||||
|
||||
return Utils::execQuery<ReleaseId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id") };
|
||||
|
||||
return Utils::execQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType)
|
||||
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
return Utils::execQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
return Utils::execQuery<ReleaseId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
return Utils::execQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
std::size_t Listen::getCount(Session& session, UserId userId, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("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();
|
||||
}
|
||||
|
||||
std::size_t Listen::getCount(Session& session, UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>(
|
||||
"SELECT IFNULL(MIN(count_result), 0)"
|
||||
" FROM ("
|
||||
" SELECT COUNT(l.track_id) AS count_result"
|
||||
" FROM track t"
|
||||
" LEFT JOIN listen l ON t.id = l.track_id AND l.backend = (SELECT scrobbling_backend FROM user WHERE id = ?) AND l.user_id = ?"
|
||||
" WHERE t.release_id = ?"
|
||||
" GROUP BY t.id)")
|
||||
.bind(userId)
|
||||
.bind(userId)
|
||||
.bind(releaseId)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
// TODO not pending remove?
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("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();
|
||||
}
|
||||
|
||||
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
// TODO not pending remove?
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("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();
|
||||
}
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Migration.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Db.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
VersionInfo::pointer VersionInfo::getOrCreate(Session& session)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
pointer versionInfo{ session.getDboSession().find<VersionInfo>() };
|
||||
if (!versionInfo)
|
||||
return session.getDboSession().add(std::make_unique<VersionInfo>());
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
VersionInfo::pointer VersionInfo::get(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<VersionInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Database::Migration
|
||||
{
|
||||
class ScopedNoForeignKeys
|
||||
{
|
||||
public:
|
||||
ScopedNoForeignKeys(Db& db) : _db{ db }
|
||||
{
|
||||
_db.executeSql("PRAGMA foreign_keys=OFF");
|
||||
}
|
||||
~ScopedNoForeignKeys()
|
||||
{
|
||||
_db.executeSql("PRAGMA foreign_keys=ON");
|
||||
}
|
||||
|
||||
ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete;
|
||||
ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete;
|
||||
ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete;
|
||||
ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete;
|
||||
|
||||
private:
|
||||
Db& _db;
|
||||
};
|
||||
|
||||
static void migrateFromV32(Session& session)
|
||||
{
|
||||
ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
|
||||
}
|
||||
|
||||
static void migrateFromV33(Session& session)
|
||||
{
|
||||
// remove name from track_artist_link
|
||||
// Drop Auth mode
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"type" integer not null,
|
||||
"track_id" bigint,
|
||||
"artist_id" bigint,
|
||||
constraint "fk_track_artist_link_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
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");
|
||||
}
|
||||
|
||||
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<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
||||
session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
||||
session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*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");
|
||||
}
|
||||
|
||||
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
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static void migrateFromV37(Session& session)
|
||||
{
|
||||
// Support Performer tags (via subtypes)
|
||||
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
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
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(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scan_version" integer not null,
|
||||
"track_number" integer,
|
||||
"disc_number" integer,
|
||||
"total_track" integer,
|
||||
"disc_subtitle" text not null,
|
||||
"name" text not null,
|
||||
"duration" integer,
|
||||
"date" text,
|
||||
"original_date" text,
|
||||
"file_path" text not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"has_cover" boolean not null,
|
||||
"mbid" text not null,
|
||||
"recording_mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"track_replay_gain" real,
|
||||
"release_replay_gain" real,
|
||||
"release_id" bigint,
|
||||
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");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static void migrateFromV40(Session& session)
|
||||
{
|
||||
// add artist_display_name in Release and Track
|
||||
session.getDboSession().execute("ALTER TABLE release ADD artist_display_name TEXT");
|
||||
session.getDboSession().execute("ALTER TABLE track ADD artist_display_name TEXT");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// 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("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");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static void migrateFromV44(Session& session)
|
||||
{
|
||||
// add bitrate
|
||||
session.getDboSession().execute("ALTER TABLE track ADD bitrate INTEGER");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
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<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")");
|
||||
}
|
||||
|
||||
void migrateFromV46(Session& session)
|
||||
{
|
||||
// add extra tags to parse
|
||||
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("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
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
void doDbMigration(Session& session)
|
||||
{
|
||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
|
||||
ScopedNoForeignKeys noPragmaKeys{ session.getDb() };
|
||||
|
||||
using MigrationFunction = std::function<void(Session&)>;
|
||||
|
||||
const std::map<unsigned, MigrationFunction> migrationFunctions
|
||||
{
|
||||
{32, migrateFromV32},
|
||||
{33, migrateFromV33},
|
||||
{34, migrateFromV34},
|
||||
{35, migrateFromV35},
|
||||
{36, migrateFromV36},
|
||||
{37, migrateFromV37},
|
||||
{38, migrateFromV38},
|
||||
{39, migrateFromV39},
|
||||
{40, migrateFromV40},
|
||||
{41, migrateFromV41},
|
||||
{42, migrateFromV42},
|
||||
{43, migrateFromV43},
|
||||
{44, migrateFromV44},
|
||||
{45, migrateFromV45},
|
||||
{46, migrateFromV46},
|
||||
};
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
Version version;
|
||||
try
|
||||
{
|
||||
version = VersionInfo::getOrCreate(session)->getVersion();
|
||||
LMS_LOG(DB, INFO, "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
LMS_LOG(DB, ERROR, "Cannot get database version info: " << e.what());
|
||||
throw LmsException{ outdatedMsg };
|
||||
}
|
||||
|
||||
if (version > LMS_DATABASE_VERSION)
|
||||
throw LmsException{ "Server binary outdated, please upgrade it to handle this database" };
|
||||
|
||||
if (version < migrationFunctions.begin()->first)
|
||||
throw LmsException{ outdatedMsg };
|
||||
|
||||
while (version < LMS_DATABASE_VERSION)
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
|
||||
|
||||
auto itMigrationFunc{ migrationFunctions.find(version) };
|
||||
assert(itMigrationFunc != std::cend(migrationFunctions));
|
||||
itMigrationFunc->second(session);
|
||||
|
||||
VersionInfo::get(session).modify()->setVersion(++version);
|
||||
|
||||
LMS_LOG(DB, INFO, "Migration complete to version " << version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 47 };
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<VersionInfo>;
|
||||
|
||||
static VersionInfo::pointer getOrCreate(Session& session);
|
||||
static VersionInfo::pointer get(Session& session);
|
||||
|
||||
Version getVersion() const { return _version; }
|
||||
void setVersion(Version version) { _version = static_cast<int>(version); }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _version, "db_version");
|
||||
}
|
||||
|
||||
private:
|
||||
int _version{ LMS_DATABASE_VERSION };
|
||||
};
|
||||
|
||||
namespace Migration
|
||||
{
|
||||
void doDbMigration(Session& session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Release.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "EnumSetTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
|
||||
{
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT " + std::string{ itemToSelect } + " from release r") };
|
||||
|
||||
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||
|| params.sortMethod == ReleaseSortMethod::Date
|
||||
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|
||||
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.dateRange
|
||||
|| params.artist.isValid()
|
||||
|| params.clusters.size() == 1)
|
||||
{
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
}
|
||||
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (params.dateRange)
|
||||
{
|
||||
query.where("t.date >= ?").bind(params.dateRange->begin);
|
||||
query.where("t.date <= ?").bind(params.dateRange->end);
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.feedbackBackend);
|
||||
query.join("starred_release s_r ON s_r.release_id = r.id")
|
||||
.where("s_r.user_id = ?").bind(params.starringUser)
|
||||
.where("s_r.backend = ?").bind(*params.feedbackBackend)
|
||||
.where("s_r.sync_state <> ?").bind(SyncState::PendingRemove);
|
||||
}
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
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);
|
||||
|
||||
if (!params.trackArtistLinkTypes.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
bool first{ true };
|
||||
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (!params.excludedTrackArtistLinkTypes.empty())
|
||||
{
|
||||
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 (";
|
||||
|
||||
query.bind(params.artist);
|
||||
|
||||
bool first{ true };
|
||||
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
oss << ")))";
|
||||
query.where(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
if (params.clusters.size() == 1)
|
||||
{
|
||||
query.join("track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("t_c.cluster_id = ?").bind(params.clusters.front());
|
||||
}
|
||||
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"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.primaryType)
|
||||
query.where("primary_type = ?").bind(*params.primaryType);
|
||||
if (!params.secondaryTypes.empty())
|
||||
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ReleaseSortMethod::None:
|
||||
break;
|
||||
case ReleaseSortMethod::Name:
|
||||
query.orderBy("r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case ReleaseSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case ReleaseSortMethod::Date:
|
||||
query.orderBy("t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::OriginalDate:
|
||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::OriginalDateDesc:
|
||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_r.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name{ std::string(name, 0 , _maxNameLength) },
|
||||
_MBID{ MBID ? MBID->getAsString() : "" }
|
||||
{
|
||||
}
|
||||
|
||||
Release::pointer Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Release> {new Release{ name, MBID }});
|
||||
}
|
||||
|
||||
std::vector<Release::pointer> Release::find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.query<Wt::Dbo::ptr<Release>>("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() };
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Release::pointer Release::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("mbid = ?").bind(std::string{ mbid.getAsString() })
|
||||
.resultValue();;
|
||||
}
|
||||
|
||||
Release::pointer Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
bool Release::exists(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
std::size_t Release::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
// TODO merge with find
|
||||
auto query{ session.getDboSession().query<ReleaseId>(
|
||||
"SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON r.id = t.release_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") };
|
||||
|
||||
return Utils::execQuery<ReleaseId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ReleaseId>("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<ReleaseId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) };
|
||||
return Utils::execQuery<pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Release::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) };
|
||||
Utils::execQuery<pointer>(query, params.range, func);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<ReleaseId>(session, "DISTINCT r.id", params) };
|
||||
return Utils::execQuery<ReleaseId>(query, params.range);
|
||||
}
|
||||
|
||||
std::size_t Release::getCount(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return createQuery<int>(session, "COUNT(DISTINCT r.id)", params).resultValue();
|
||||
}
|
||||
|
||||
std::size_t Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId()) };
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<DiscInfo> Release::getDiscs() const
|
||||
{
|
||||
assert(session());
|
||||
using ResultType = std::tuple<int, std::string>;
|
||||
auto results{ session()->query<ResultType>("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() };
|
||||
|
||||
std::vector<DiscInfo> discs;
|
||||
for (const auto& res : results)
|
||||
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::get<std::string>(res) });
|
||||
|
||||
return discs;
|
||||
}
|
||||
|
||||
Wt::WDate Release::getReleaseDate() const
|
||||
{
|
||||
return getReleaseDate(false);
|
||||
}
|
||||
|
||||
Wt::WDate Release::getOriginalReleaseDate() const
|
||||
{
|
||||
return getReleaseDate(true);
|
||||
}
|
||||
|
||||
Wt::WDate Release::getReleaseDate(bool original) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const char* field{ original ? "original_date" : "date" };
|
||||
|
||||
auto dates{ session()->query<Wt::WDate>(
|
||||
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() };
|
||||
|
||||
// various dates => invalid date
|
||||
if (dates.empty() || dates.size() > 1)
|
||||
return {};
|
||||
|
||||
return dates.front();
|
||||
}
|
||||
|
||||
std::optional<std::string> Release::getCopyright() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright")
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
// various copyrights => no copyright
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::optional<std::string> Release::getCopyrightURL() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?").bind(getId())
|
||||
.groupBy("copyright_url");
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
// various copyright URLs => no copyright URL
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::size_t Release::getMeanBitrate() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
return session()->query<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t")
|
||||
.where("release_id = ?").bind(getId())
|
||||
.where("bitrate > 0")
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"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() };
|
||||
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
// Select the similar releases using the 5 most used clusters of the release
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Release>>(
|
||||
"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"
|
||||
" WHERE "
|
||||
" t_c.cluster_id IN "
|
||||
"(SELECT DISTINCT c.id 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 release r ON r.id = t.release_id"
|
||||
" WHERE r.id = ?)"
|
||||
" AND r.id <> ?"
|
||||
)
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList() };
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool Release::hasVariousArtists() const
|
||||
{
|
||||
// TODO optimize
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::size_t Release::getTracksCount() const
|
||||
{
|
||||
return _tracks.size();
|
||||
}
|
||||
|
||||
std::chrono::milliseconds Release::getDuration() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
using milli = std::chrono::duration<int, std::milli>;
|
||||
|
||||
Wt::Dbo::Query<milli> query{ session()->query<milli>("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();
|
||||
}
|
||||
|
||||
Wt::WDateTime Release::getLastWritten() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::Query<Wt::WDateTime> query{ session()->query<Wt::WDateTime>("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();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
|
||||
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 ";
|
||||
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()) };
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/ScanSettings.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
ScanSettings::pointer ScanSettings::get(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<ScanSettings>().resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
|
||||
{
|
||||
const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
|
||||
|
||||
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
|
||||
std::sort(std::begin(res), std::end(res));
|
||||
res.erase(std::unique(std::begin(res), std::end(res)), std::end(res));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
|
||||
{
|
||||
_audioFileExtensions += " " + ext.string();
|
||||
}
|
||||
|
||||
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
||||
{
|
||||
return StringUtils::splitString(_extraTagsToScan, ";");
|
||||
}
|
||||
|
||||
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
|
||||
{
|
||||
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
|
||||
}
|
||||
|
||||
void ScanSettings::setExtraTagsToScan(const std::vector<std::string_view>& extraTags)
|
||||
{
|
||||
std::string newTagsToScan{ StringUtils::joinStrings(extraTags, ";") };
|
||||
if (newTagsToScan != _extraTagsToScan)
|
||||
incScanVersion();
|
||||
|
||||
_extraTagsToScan = std::move(newTagsToScan);
|
||||
}
|
||||
|
||||
void ScanSettings::incScanVersion()
|
||||
{
|
||||
_scanVersion += 1;
|
||||
}
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/AuthToken.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
#include "database/StarredRelease.hpp"
|
||||
#include "database/StarredTrack.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/TransactionChecker.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "EnumSetTraits.hpp"
|
||||
#include "Migration.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
WriteTransaction::WriteTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock{ mutex },
|
||||
_transaction{ session }
|
||||
{
|
||||
TransactionChecker::pushWriteTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
WriteTransaction::~WriteTransaction()
|
||||
{
|
||||
TransactionChecker::popWriteTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
ReadTransaction::ReadTransaction(Wt::Dbo::Session& session)
|
||||
: _transaction{ session }
|
||||
{
|
||||
TransactionChecker::pushReadTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
ReadTransaction::~ReadTransaction()
|
||||
{
|
||||
TransactionChecker::popReadTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
Session::Session(Db& db)
|
||||
: _db{ db }
|
||||
{
|
||||
_session.setConnectionPool(_db.getConnectionPool());
|
||||
|
||||
_session.mapClass<VersionInfo>("version_info");
|
||||
_session.mapClass<Artist>("artist");
|
||||
_session.mapClass<AuthToken>("auth_token");
|
||||
_session.mapClass<Cluster>("cluster");
|
||||
_session.mapClass<ClusterType>("cluster_type");
|
||||
_session.mapClass<Listen>("listen");
|
||||
_session.mapClass<Release>("release");
|
||||
_session.mapClass<ScanSettings>("scan_settings");
|
||||
_session.mapClass<StarredArtist>("starred_artist");
|
||||
_session.mapClass<StarredRelease>("starred_release");
|
||||
_session.mapClass<StarredTrack>("starred_track");
|
||||
_session.mapClass<Track>("track");
|
||||
_session.mapClass<TrackBookmark>("track_bookmark");
|
||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||
_session.mapClass<TrackFeatures>("track_features");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||
_session.mapClass<User>("user");
|
||||
}
|
||||
|
||||
WriteTransaction Session::createWriteTransaction()
|
||||
{
|
||||
return WriteTransaction{ _db.getMutex(), _session };
|
||||
}
|
||||
|
||||
ReadTransaction Session::createReadTransaction()
|
||||
{
|
||||
return ReadTransaction{ _session };
|
||||
}
|
||||
|
||||
void Session::prepareTables()
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Preparing tables...");
|
||||
|
||||
// Initial creation case
|
||||
try
|
||||
{
|
||||
_session.createTables();
|
||||
LMS_LOG(DB, INFO, "Tables created");
|
||||
}
|
||||
catch (Wt::Dbo::Exception& e)
|
||||
{
|
||||
LMS_LOG(DB, DEBUG, "Cannot create tables: " << e.what());
|
||||
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
|
||||
{
|
||||
LMS_LOG(DB, ERROR, "Cannot create tables: " << e.what());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
Migration::doDbMigration(*this);
|
||||
|
||||
// Indexes
|
||||
{
|
||||
auto transaction{ createWriteTransaction() };
|
||||
_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_type_name_idx ON cluster_type(name)");
|
||||
_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 track_path_idx ON track(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_original_date_idx ON track(original_date)");
|
||||
_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_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)");
|
||||
_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::analyze()
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Analyzing database...");
|
||||
{
|
||||
auto transaction{ createWriteTransaction() };
|
||||
_session.execute("ANALYZE");
|
||||
}
|
||||
LMS_LOG(DB, INFO, "Database Analyze complete");
|
||||
}
|
||||
|
||||
void Session::optimize()
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Optimizing database...");
|
||||
{
|
||||
auto transaction{ createWriteTransaction() };
|
||||
_session.execute("PRAGMA optimize");
|
||||
}
|
||||
LMS_LOG(DB, INFO, "Database optimizing complete");
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
|
||||
WhereClause&
|
||||
WhereClause::And(const WhereClause& otherClause)
|
||||
{
|
||||
if (!otherClause._clause.empty()) {
|
||||
if (!_clause.empty())
|
||||
_clause += " AND ";
|
||||
_clause += "(" + otherClause._clause + ")";
|
||||
|
||||
// Add associated bind args
|
||||
for (const std::string& otherBindArg : otherClause._bindArgs)
|
||||
{
|
||||
_bindArgs.push_back(otherBindArg);
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
WhereClause&
|
||||
WhereClause::Or(const WhereClause& otherClause)
|
||||
{
|
||||
if (!otherClause._clause.empty()) {
|
||||
if (!_clause.empty())
|
||||
_clause += " OR ";
|
||||
_clause += "(" + otherClause._clause + ")";
|
||||
|
||||
// Add associated bind args
|
||||
for (const std::string& otherBindArg : otherClause._bindArgs)
|
||||
{
|
||||
_bindArgs.push_back(otherBindArg);
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::string
|
||||
WhereClause::get() const
|
||||
{
|
||||
if (!_clause.empty())
|
||||
return "WHERE " + _clause;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
WhereClause&
|
||||
WhereClause::bind(std::string_view bindArg)
|
||||
{
|
||||
assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?')));
|
||||
|
||||
_bindArgs.push_back(std::string{ bindArg });
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
InnerJoinClause::InnerJoinClause(const std::string& clause)
|
||||
:_clause(clause)
|
||||
{
|
||||
}
|
||||
|
||||
InnerJoinClause&
|
||||
InnerJoinClause::And(const InnerJoinClause& clause)
|
||||
{
|
||||
if (!_clause.empty())
|
||||
_clause += " ";
|
||||
|
||||
_clause += "INNER JOIN " + clause._clause;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
SelectStatement::SelectStatement(const std::string& statement)
|
||||
{
|
||||
And(statement);
|
||||
}
|
||||
|
||||
SelectStatement&
|
||||
SelectStatement::And(const std::string& statement)
|
||||
{
|
||||
_statement.push_back(statement);
|
||||
|
||||
std::sort(_statement.begin(), _statement.end());
|
||||
_statement.erase(std::unique(_statement.begin(), _statement.end()), _statement.end());
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::string
|
||||
SelectStatement::get() const
|
||||
{
|
||||
std::string res = "SELECT ";
|
||||
|
||||
for (auto it = _statement.begin(); it != _statement.end(); ++it)
|
||||
{
|
||||
if (it != _statement.begin())
|
||||
res += ",";
|
||||
res += *it;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
GroupByStatement&
|
||||
GroupByStatement::And(const GroupByStatement& statement)
|
||||
{
|
||||
if( _statement.empty() && !statement._statement.empty())
|
||||
_statement = "GROUP BY ";
|
||||
else if (!_statement.empty() && !statement._statement.empty())
|
||||
_statement += ",";
|
||||
|
||||
_statement += statement._statement;
|
||||
return *this;
|
||||
}
|
||||
|
||||
FromClause::FromClause(const std::string& clause)
|
||||
{
|
||||
_clause.push_back(clause);
|
||||
}
|
||||
|
||||
FromClause&
|
||||
FromClause::And(const FromClause& clause)
|
||||
{
|
||||
for (const std::string& fromClause : clause._clause)
|
||||
{
|
||||
_clause.push_back(fromClause);
|
||||
}
|
||||
|
||||
std::sort(_clause.begin(), _clause.end());
|
||||
_clause.erase(std::unique(_clause.begin(), _clause.end()), _clause.end());
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::string
|
||||
FromClause::get() const
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
if (!_clause.empty())
|
||||
{
|
||||
oss << "FROM ";
|
||||
for (auto it = _clause.begin(); it != _clause.end(); ++it) {
|
||||
if (it != _clause.begin())
|
||||
oss << ",";
|
||||
|
||||
oss << *it;
|
||||
}
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string
|
||||
SqlQuery::get() const
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << _selectStatement.get();
|
||||
|
||||
if (!_fromClause.get().empty())
|
||||
oss << " " << _fromClause.get();
|
||||
|
||||
if (!_innerJoinClause.get().empty())
|
||||
oss << " " << _innerJoinClause.get();
|
||||
|
||||
if (!_whereClause.get().empty())
|
||||
oss << " " << _whereClause.get();
|
||||
|
||||
if (!_groupByStatement.get().empty())
|
||||
oss << " " << _groupByStatement.get();
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
class WhereClause
|
||||
{
|
||||
public:
|
||||
WhereClause() {}
|
||||
WhereClause(const std::string& clause) { _clause = clause; }
|
||||
|
||||
WhereClause& And(const WhereClause& clause);
|
||||
WhereClause& Or(const WhereClause& clause);
|
||||
|
||||
// Arguments binding (for each '?' in where clause)
|
||||
WhereClause& bind(std::string_view arg);
|
||||
|
||||
std::string get() const;
|
||||
const std::vector<std::string>& getBindArgs() const {return _bindArgs;}
|
||||
|
||||
private:
|
||||
std::string _clause; // WHERE clause
|
||||
std::vector<std::string> _bindArgs;
|
||||
};
|
||||
|
||||
class InnerJoinClause
|
||||
{
|
||||
public:
|
||||
InnerJoinClause() {}
|
||||
InnerJoinClause(const std::string& clause);
|
||||
|
||||
InnerJoinClause& And(const InnerJoinClause& clause);
|
||||
std::string get() const { return _clause;}
|
||||
|
||||
private:
|
||||
std::string _clause;
|
||||
};
|
||||
|
||||
class GroupByStatement
|
||||
{
|
||||
public:
|
||||
GroupByStatement() {}
|
||||
GroupByStatement(const std::string& statement) { _statement = statement; }
|
||||
|
||||
GroupByStatement& And(const GroupByStatement& statement);
|
||||
|
||||
std::string get() const {return _statement;}
|
||||
|
||||
private:
|
||||
std::string _statement; // SELECT statement
|
||||
};
|
||||
|
||||
class SelectStatement
|
||||
{
|
||||
public:
|
||||
SelectStatement() {};
|
||||
SelectStatement(const std::string& item);
|
||||
|
||||
SelectStatement& And(const std::string& item);
|
||||
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> _statement;
|
||||
};
|
||||
|
||||
class FromClause
|
||||
{
|
||||
public:
|
||||
FromClause() {}
|
||||
FromClause(const std::string& clause);
|
||||
|
||||
FromClause& And(const FromClause& clause);
|
||||
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> _clause;
|
||||
};
|
||||
|
||||
class SqlQuery
|
||||
{
|
||||
public:
|
||||
SelectStatement& select() { return _selectStatement;}
|
||||
SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; }
|
||||
FromClause& from() { return _fromClause; }
|
||||
FromClause& from(const std::string& clause) { _whereClause = WhereClause(clause); return _fromClause; }
|
||||
InnerJoinClause& innerJoin() { return _innerJoinClause; }
|
||||
WhereClause& where() { return _whereClause; }
|
||||
const WhereClause& where() const { return _whereClause; }
|
||||
GroupByStatement& groupBy() { return _groupByStatement; }
|
||||
const GroupByStatement& groupBy() const { return _groupByStatement; }
|
||||
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
SelectStatement _selectStatement; // SELECT statement
|
||||
InnerJoinClause _innerJoinClause; // INNER JOIN
|
||||
FromClause _fromClause; // FROM tables
|
||||
WhereClause _whereClause; // WHERE clause
|
||||
GroupByStatement _groupByStatement; // GROUP BY statement
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/StarredArtist.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
: _backend{ backend }
|
||||
, _artist{ getDboPtr(artist) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
|
||||
}
|
||||
|
||||
std::size_t StarredArtist::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<StarredArtist>>("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();
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredArtist>()
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/StarredRelease.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
: _backend{ backend }
|
||||
, _release{ getDboPtr(release) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
|
||||
}
|
||||
|
||||
std::size_t StarredRelease::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<StarredRelease>>("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();
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredRelease>()
|
||||
.where("release_id = ?").bind(releaseId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/StarredTrack.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
: _backend{ backend }
|
||||
, _track{ getDboPtr(track) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
|
||||
}
|
||||
|
||||
std::size_t StarredTrack::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<StarredTrack>>("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();
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredTrack>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
bool StarredTrack::exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().query<int>("SELECT 1 from starred_track")
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue() == 1;
|
||||
}
|
||||
|
||||
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") };
|
||||
|
||||
if (params.backend)
|
||||
query.where("s_t.backend = ?").bind(*params.backend);
|
||||
if (params.syncState)
|
||||
query.where("s_t.sync_state = ?").bind(*params.syncState);
|
||||
if (params.user.isValid())
|
||||
query.where("s_t.user_id = ?").bind(params.user);
|
||||
|
||||
return Utils::execQuery<StarredTrackId>(query, params.range);
|
||||
}
|
||||
|
||||
void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Wt::Dbo
|
||||
{
|
||||
template<>
|
||||
struct sql_value_traits<std::string_view>
|
||||
{
|
||||
static void bind(std::string_view str, SqlStatement *statement, int column, int /* size */)
|
||||
{
|
||||
statement->bind(column, std::string {str});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2016 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Track::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::string selectStatement{ params.distinct ? "SELECT DISTINCT" : "SELECT" };
|
||||
auto query{ session.getDboSession().query<ResultType>(selectStatement + " " + std::string{ itemToSelect } + " FROM track t") };
|
||||
|
||||
assert(params.keywords.empty() || params.name.empty());
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
if (!params.name.empty())
|
||||
query.where("t.name = ?").bind(params.name);
|
||||
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.feedbackBackend);
|
||||
query.join("starred_track s_t ON s_t.track_id = t.id")
|
||||
.where("s_t.user_id = ?").bind(params.starringUser)
|
||||
.where("s_t.backend = ?").bind(*params.feedbackBackend)
|
||||
.where("s_t.sync_state <> ?").bind(SyncState::PendingRemove);
|
||||
}
|
||||
|
||||
if (params.clusters.size() == 1)
|
||||
{
|
||||
// optim
|
||||
query.join("track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("t_c.cluster_id = ?").bind(params.clusters.front());
|
||||
}
|
||||
else if (params.clusters.size() > 1)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.artist.isValid() || !params.artistName.empty())
|
||||
{
|
||||
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");
|
||||
|
||||
if (params.artist.isValid())
|
||||
query.where("a.id = ?").bind(params.artist);
|
||||
if (!params.artistName.empty())
|
||||
query.where("a.name = ?").bind(params.artistName);
|
||||
|
||||
if (!params.trackArtistLinkTypes.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
bool first{ true };
|
||||
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
query.where(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
assert(!(params.nonRelease && params.release.isValid()));
|
||||
if (params.nonRelease)
|
||||
query.where("t.release_id IS NULL");
|
||||
else if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
else if (!params.releaseName.empty())
|
||||
{
|
||||
query.join("release r ON t.release_id = r.id");
|
||||
query.where("r.name = ?").bind(params.releaseName);
|
||||
}
|
||||
|
||||
if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList)
|
||||
{
|
||||
query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id");
|
||||
query.join("tracklist_entry t_l_e ON t.id = t_l_e.track_id");
|
||||
query.where("t_l.id = ?").bind(params.trackList);
|
||||
}
|
||||
|
||||
if (params.trackNumber)
|
||||
query.where("t.track_number = ?").bind(*params.trackNumber);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case TrackSortMethod::None:
|
||||
break;
|
||||
case TrackSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case TrackSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case TrackSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_t.date_time DESC");
|
||||
break;
|
||||
case TrackSortMethod::Name:
|
||||
query.orderBy("t.name COLLATE NOCASE");
|
||||
break;
|
||||
case TrackSortMethod::DateDescAndRelease:
|
||||
query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number");
|
||||
break;
|
||||
case TrackSortMethod::Release:
|
||||
query.orderBy("t.disc_number,t.track_number");
|
||||
break;
|
||||
case TrackSortMethod::TrackList:
|
||||
assert(params.trackList.isValid());
|
||||
query.orderBy("t_l.id");
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Track::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, TrackId>)
|
||||
itemToSelect = "t.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Track>>)
|
||||
itemToSelect = "t";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
}
|
||||
|
||||
Track::Track(const std::filesystem::path& p)
|
||||
: _filePath{ p.string() }
|
||||
{
|
||||
}
|
||||
|
||||
Track::pointer Track::create(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Track> {new Track{ p }});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Track::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
||||
}
|
||||
|
||||
Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
|
||||
}
|
||||
|
||||
Track::pointer Track::find(Session& session, TrackId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Track>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
bool Track::exists(Session& session, TrackId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer> Track::findByMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().find<Track>()
|
||||
.where("mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
RangeResults<Track::PathResult> Track::findPaths(Session& session, std::optional<Range> range)
|
||||
{
|
||||
using QueryResultType = std::tuple<TrackId, std::string>;
|
||||
session.checkReadTransaction();
|
||||
|
||||
// TODO Dbo traits on filesystem
|
||||
auto query{ session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track") };
|
||||
|
||||
RangeResults<QueryResultType> queryResults{ Utils::execQuery<QueryResultType>(query, range) };
|
||||
|
||||
RangeResults<PathResult> 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<TrackId>(queryResult), std::move(std::get<std::string>(queryResult)) };
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackId>("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<TrackId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackId>("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<TrackId>(query, range);
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> Track::getClusters() const
|
||||
{
|
||||
return std::vector<Cluster::pointer>(_clusters.begin(), _clusters.end());
|
||||
}
|
||||
|
||||
std::vector<ClusterId> Track::getClusterIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<ClusterId>
|
||||
("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() };
|
||||
|
||||
return std::vector<ClusterId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<TrackId>(session, parameters) };
|
||||
return Utils::execQuery<TrackId>(query, parameters.range);
|
||||
}
|
||||
|
||||
RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) };
|
||||
return Utils::execQuery<Track::pointer>(query, parameters.range);
|
||||
}
|
||||
|
||||
void Track::find(Session& session, const FindParameters& params, std::function<void(const Track::pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)};
|
||||
Utils::execQuery(query, params.range, func);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
|
||||
{
|
||||
assert(!tracks.empty());
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::ostringstream oss;
|
||||
for (std::size_t i{}; i < tracks.size(); ++i)
|
||||
{
|
||||
if (!oss.str().empty())
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
}
|
||||
|
||||
auto query{ session.getDboSession().query<TrackId>(
|
||||
"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() + "))"
|
||||
" AND t.id NOT IN (" + oss.str() + ")")
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()") };
|
||||
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
return Utils::execQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
void Track::clearArtistLinks()
|
||||
{
|
||||
_trackArtistLinks.clear();
|
||||
}
|
||||
|
||||
void Track::addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink)
|
||||
{
|
||||
_trackArtistLinks.insert(getDboPtr(artistLink));
|
||||
}
|
||||
|
||||
void Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
|
||||
{
|
||||
_clusters.clear();
|
||||
for (const ObjectPtr<Cluster>& cluster : clusters)
|
||||
_clusters.insert(getDboPtr(cluster));
|
||||
}
|
||||
|
||||
std::optional<int> Track::getYear() const
|
||||
{
|
||||
return (_date.isValid() ? std::make_optional<int>(_date.year()) : std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<int> Track::getOriginalYear() const
|
||||
{
|
||||
return (_originalDate.isValid() ? std::make_optional<int>(_originalDate.year()) : std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<std::string> Track::getCopyright() const
|
||||
{
|
||||
return _copyright != "" ? std::make_optional<std::string>(_copyright) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> Track::getCopyrightURL() const
|
||||
{
|
||||
return _copyrightURL != "" ? std::make_optional<std::string>(_copyrightURL) : std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Track::getArtists(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
oss <<
|
||||
"SELECT a from artist a"
|
||||
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id";
|
||||
|
||||
if (!linkTypes.empty())
|
||||
{
|
||||
oss << " AND t_a_l.type IN (";
|
||||
|
||||
bool first{ true };
|
||||
for ([[maybe_unused]] TrackArtistLinkType type : linkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Artist>>(oss.str()) };
|
||||
for (TrackArtistLinkType type : linkTypes)
|
||||
query.bind(type);
|
||||
|
||||
query.where("t.id = ?").bind(getId());
|
||||
|
||||
auto res{ query.resultList() };
|
||||
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
std::vector<ArtistId> Track::getArtistIds(EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
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"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id";
|
||||
|
||||
if (!linkTypes.empty())
|
||||
{
|
||||
oss << " AND t_a_l.type IN (";
|
||||
|
||||
bool first{ true };
|
||||
for ([[maybe_unused]] TrackArtistLinkType type : linkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
auto query{ session()->query<ArtistId>(oss.str()) };
|
||||
for (TrackArtistLinkType type : linkTypes)
|
||||
query.bind(type);
|
||||
|
||||
query.where("t.id = ?").bind(getId());
|
||||
|
||||
auto res{ query.resultList() };
|
||||
return std::vector<ArtistId>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
std::vector<TrackArtistLink::pointer> Track::getArtistLinks() const
|
||||
{
|
||||
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
|
||||
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";
|
||||
|
||||
where.And(WhereClause("t.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()) };
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clusters;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clusters[cluster->getType()->getId()].size() < size)
|
||||
clusters[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [type, clusters] : clusters)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
namespace Debug
|
||||
{
|
||||
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo)
|
||||
{
|
||||
auto transaction{ trackInfo.session.createReadTransaction() };
|
||||
|
||||
const Track::pointer track{ Track::find(trackInfo.session, trackInfo.trackId) };
|
||||
if (track)
|
||||
{
|
||||
os << track->getName();
|
||||
|
||||
if (const Release::pointer release{ track->getRelease() })
|
||||
os << " [" << release->getName() << "]";
|
||||
for (auto artist : track->getArtists({ TrackArtistLinkType::Artist }))
|
||||
os << " - " << artist->getName();
|
||||
for (auto cluster : track->getClusters())
|
||||
os << " {" << cluster->getType()->getName() << "-" << cluster->getName() << "}";
|
||||
}
|
||||
else
|
||||
{
|
||||
os << "*unknown*";
|
||||
}
|
||||
|
||||
return os;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2016 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackArtistLinkId>("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);
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
|
||||
if (params.artist.isValid())
|
||||
query.join("artist a ON a.id = t_a_l.artist_id");
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
: _type{ type }
|
||||
, _subType{ subType }
|
||||
, _track{ getDboPtr(track) }
|
||||
, _artist{ getDboPtr(artist) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery(session, params) };
|
||||
return Utils::execQuery<TrackArtistLinkId>(query, params.range);
|
||||
}
|
||||
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList() };
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.resultList() };
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TrackBookmark.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackBookmark::TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
: _user{ getDboPtr(user) },
|
||||
_track{ getDboPtr(track) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<TrackBookmark> {new TrackBookmark{ user, track }});
|
||||
}
|
||||
|
||||
std::size_t TrackBookmark::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark");
|
||||
}
|
||||
|
||||
RangeResults<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark")
|
||||
.where("user_id = ?").bind(userId) };
|
||||
|
||||
return Utils::execQuery<TrackBookmarkId>(query, range);
|
||||
}
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TrackFeatures.hpp"
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackFeatures::TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
: _data{ jsonEncodedFeatures },
|
||||
_track{ getDboPtr(track) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<TrackFeatures> {new TrackFeatures{ track, jsonEncodedFeatures }});
|
||||
}
|
||||
|
||||
std::size_t TrackFeatures::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features");
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackFeaturesId>("SELECT id from track_features") };
|
||||
|
||||
return Utils::execQuery<TrackFeaturesId>(query, range);
|
||||
}
|
||||
|
||||
FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
|
||||
{
|
||||
FeatureValuesMap featuresValuesMap{ getFeatureValuesMap({featureNode}) };
|
||||
return std::move(featuresValuesMap[featureNode]);
|
||||
}
|
||||
|
||||
FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
|
||||
{
|
||||
FeatureValuesMap res;
|
||||
|
||||
try
|
||||
{
|
||||
std::istringstream iss{ _data };
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
boost::property_tree::read_json(iss, root);
|
||||
|
||||
for (const FeatureName& featureName : featureNames)
|
||||
{
|
||||
FeatureValues& featureValues{ res[featureName] };
|
||||
|
||||
auto node{ root.get_child(featureName) };
|
||||
|
||||
bool hasChildren = false;
|
||||
for (const auto& child : node.get_child(""))
|
||||
{
|
||||
hasChildren = true;
|
||||
featureValues.push_back(child.second.get_value<double>());
|
||||
}
|
||||
|
||||
if (!hasChildren)
|
||||
featureValues.push_back(node.get_value<double>());
|
||||
}
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
|
||||
res.clear();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "database/TrackList.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
: _name{ name }
|
||||
, _type{ type }
|
||||
, _isPublic{ isPublic }
|
||||
, _creationDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||
, _lastModifiedDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
assert(user);
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
|
||||
}
|
||||
|
||||
std::size_t TrackList::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist");
|
||||
}
|
||||
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
assert(userId.isValid());
|
||||
|
||||
return session.getDboSession().find<TrackList>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("type = ?").bind(type)
|
||||
.where("user_id = ?").bind(userId).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l") };
|
||||
|
||||
if (params.user.isValid())
|
||||
query.where("t_l.user_id = ?").bind(params.user);
|
||||
|
||||
if (params.type)
|
||||
query.where("t_l.type = ?").bind(*params.type);
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
|
||||
query.join("track t ON t.id = t_l_e.track_id");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case TrackListSortMethod::None:
|
||||
break;
|
||||
case TrackListSortMethod::Name:
|
||||
query.orderBy("t_l.name COLLATE NOCASE");
|
||||
break;
|
||||
case TrackListSortMethod::LastModifiedDesc:
|
||||
query.orderBy("t_l.last_modified_date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return Utils::execQuery<TrackListId>(query, params.range);
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, TrackListId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
bool TrackList::isEmpty() const
|
||||
{
|
||||
return _entries.empty();
|
||||
}
|
||||
|
||||
std::size_t TrackList::getCount() const
|
||||
{
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
TrackListEntry::pointer TrackList::getEntry(std::size_t pos) const
|
||||
{
|
||||
TrackListEntry::pointer res;
|
||||
|
||||
auto entries = getEntries(Range{ pos, 1 });
|
||||
if (!entries.empty())
|
||||
res = entries.front();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<TrackListEntry::pointer> TrackList::getEntries(std::optional<Range> range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto entries{
|
||||
session()->find<TrackListEntry>()
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.orderBy("id")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList() };
|
||||
|
||||
return std::vector<TrackListEntry::pointer>(entries.begin(), entries.end());
|
||||
}
|
||||
|
||||
TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
return session()->find<TrackListEntry>()
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.where("track_id = ?").bind(track->getId())
|
||||
.where("date_time = ?").bind(Utils::normalizeDateTime(dateTime))
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> TrackList::getClusters() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Cluster>>("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() };
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
|
||||
if (clusterTypeIds.empty())
|
||||
return res;
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c") };
|
||||
|
||||
query.join("track t ON c.id = t_c.cluster_id")
|
||||
.join("track_cluster t_c ON t_c.track_id = t.id")
|
||||
.join("cluster_type c_type ON c.cluster_type_id = c_type.id")
|
||||
.join("tracklist_entry t_l_e ON t_l_e.track_id = t.id")
|
||||
.join("tracklist t_l ON t_l.id = t_l_e.tracklist_id")
|
||||
.where("t_l.id = ?").bind(getId());
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "c_type.id IN (";
|
||||
bool first{ true };
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
{
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
query.bind(clusterTypeId);
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
query.where(oss.str());
|
||||
}
|
||||
query.groupBy("c.id");
|
||||
query.orderBy("COUNT(c.id) DESC");
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool TrackList::hasTrack(TrackId trackId) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<TrackListEntry::pointer> res = session()->query<TrackListEntry::pointer>("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
|
||||
.where("p_e.track_id = ?").bind(trackId)
|
||||
.where("p.id = ?").bind(getId());
|
||||
|
||||
return res.size() > 0;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer> TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Track>>(
|
||||
"SELECT t FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" (t_c.cluster_id IN (SELECT DISTINCT c.id 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 = ?)"
|
||||
" AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))"
|
||||
)
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackId> TrackList::getTrackIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<TrackId> res = session()->query<TrackId>("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<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::chrono::milliseconds TrackList::getDuration() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
using milli = std::chrono::duration<int, std::milli>;
|
||||
|
||||
Wt::Dbo::Query<milli> query{ session()->query<milli>("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();
|
||||
}
|
||||
|
||||
void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_lastModifiedDateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
|
||||
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
: _dateTime{ Utils::normalizeDateTime(dateTime) }
|
||||
, _track{ getDboPtr(track) }
|
||||
, _tracklist{ getDboPtr(tracklist) }
|
||||
{
|
||||
assert(track);
|
||||
assert(tracklist);
|
||||
}
|
||||
|
||||
TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<TrackListEntry> {new TrackListEntry{ track, tracklist, dateTime }});
|
||||
}
|
||||
|
||||
void TrackListEntry::onPostCreated()
|
||||
{
|
||||
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
||||
}
|
||||
|
||||
void TrackListEntry::onPreRemove()
|
||||
{
|
||||
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
||||
}
|
||||
|
||||
TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TransactionChecker.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#if !defined(NDEBUG)
|
||||
#define LMS_CHECK_TRANSACTION_ACCESSES 1
|
||||
#else
|
||||
#define LMS_CHECK_TRANSACTION_ACCESSES 0
|
||||
#endif
|
||||
|
||||
namespace Database
|
||||
{
|
||||
#if LMS_CHECK_TRANSACTION_ACCESSES
|
||||
namespace
|
||||
{
|
||||
struct StackEntry
|
||||
{
|
||||
TransactionChecker::TransactionType type;
|
||||
Wt::Dbo::Session* session{};
|
||||
};
|
||||
|
||||
static thread_local std::vector<StackEntry> transactionStack;
|
||||
}
|
||||
#endif
|
||||
|
||||
void TransactionChecker::pushWriteTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
pushTransaction(TransactionType::Write, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::pushReadTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
pushTransaction(TransactionType::Read, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::popWriteTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
popTransaction(TransactionType::Write, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::popReadTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
popTransaction(TransactionType::Read, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::pushTransaction([[maybe_unused]] TransactionType type, [[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
#if LMS_CHECK_TRANSACTION_ACCESSES
|
||||
assert(transactionStack.empty() || transactionStack.back().session == &session);
|
||||
transactionStack.push_back(StackEntry{ type, &session });
|
||||
#endif // LMS_CHECK_TRANSACTION_ACCESSES
|
||||
}
|
||||
|
||||
void TransactionChecker::popTransaction([[maybe_unused]] TransactionType type, [[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
#if LMS_CHECK_TRANSACTION_ACCESSES
|
||||
|
||||
assert(!transactionStack.empty());
|
||||
assert(transactionStack.back().type == type);
|
||||
assert(transactionStack.back().session == &session);
|
||||
transactionStack.pop_back();
|
||||
#endif // LMS_CHECK_TRANSACTION_ACCESSES
|
||||
}
|
||||
|
||||
void TransactionChecker::checkWriteTransaction([[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
assert(!transactionStack.empty());
|
||||
assert(transactionStack.back().type == TransactionType::Write);
|
||||
assert(transactionStack.back().session == &session);
|
||||
}
|
||||
|
||||
void TransactionChecker::checkWriteTransaction(Session& session)
|
||||
{
|
||||
checkWriteTransaction(session.getDboSession());
|
||||
}
|
||||
|
||||
void TransactionChecker::checkReadTransaction([[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
assert(!transactionStack.empty());
|
||||
assert(transactionStack.back().session == &session);
|
||||
}
|
||||
|
||||
void TransactionChecker::checkReadTransaction(Session& session)
|
||||
{
|
||||
checkReadTransaction(session.getDboSession());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
static const std::set<Bitrate> allowedAudioBitrates
|
||||
{
|
||||
64000,
|
||||
96000,
|
||||
128000,
|
||||
192000,
|
||||
320000,
|
||||
};
|
||||
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)> func)
|
||||
{
|
||||
for (Bitrate bitrate : allowedAudioBitrates)
|
||||
func(bitrate);
|
||||
}
|
||||
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate)
|
||||
{
|
||||
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
|
||||
}
|
||||
|
||||
DateRange
|
||||
DateRange::fromYearRange(int from, int to)
|
||||
{
|
||||
return DateRange {{from, 1, 1}, {to, 12, 31}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
User::User(std::string_view loginName)
|
||||
: _loginName{ loginName }
|
||||
{
|
||||
}
|
||||
|
||||
User::pointer User::create(Session& session, std::string_view loginName)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<User> {new User{ loginName }});
|
||||
}
|
||||
|
||||
std::size_t User::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||
}
|
||||
|
||||
RangeResults<UserId> User::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<UserId>("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<UserId>(query, params.range);
|
||||
}
|
||||
|
||||
User::pointer User::findDemoUser(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
|
||||
}
|
||||
|
||||
User::pointer User::find(Session& session, UserId id)
|
||||
{
|
||||
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
User::pointer User::find(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate)
|
||||
{
|
||||
assert(isAudioBitrateAllowed(bitrate));
|
||||
_subsonicDefaultTranscodingOutputBitrate = bitrate;
|
||||
}
|
||||
|
||||
void User::clearAuthTokens()
|
||||
{
|
||||
_authTokens.clear();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include "utils/String.hpp"
|
||||
|
||||
namespace Database::Utils
|
||||
{
|
||||
std::string
|
||||
escapeLikeKeyword(std::string_view keyword)
|
||||
{
|
||||
return StringUtils::escapeString(keyword, "%_", escapeChar);
|
||||
}
|
||||
|
||||
Wt::WDateTime
|
||||
normalizeDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
// force second resolution
|
||||
return Wt::WDateTime::fromTime_t(dateTime.toTime_t());
|
||||
}
|
||||
} // namespace Database::Utils
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database::Utils
|
||||
{
|
||||
#define ESCAPE_CHAR_STR "\\"
|
||||
static inline constexpr char escapeChar{ '\\' };
|
||||
std::string escapeLikeKeyword(std::string_view keywords);
|
||||
|
||||
template <typename Query>
|
||||
void applyRange(Query& query, std::optional<Range> range)
|
||||
{
|
||||
if (range)
|
||||
{
|
||||
query.limit(static_cast<int>(range->size));
|
||||
query.offset(static_cast<int>(range->offset));
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ResultType, typename Query>
|
||||
RangeResults<ResultType> execQuery(Query& query, std::optional<Range> range)
|
||||
{
|
||||
RangeResults<ResultType> res;
|
||||
|
||||
if (range)
|
||||
applyRange(query, Range{ range->offset, range->size + 1 });
|
||||
|
||||
auto collection{ query.resultList() };
|
||||
res.results.assign(collection.begin(), collection.end());
|
||||
if (range && res.results.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
// TODO may optim by not actually requesting the last one
|
||||
res.moreResults = true;
|
||||
res.results.pop_back();
|
||||
}
|
||||
else
|
||||
res.moreResults = false;
|
||||
|
||||
res.range.offset = range->offset;
|
||||
res.range.size = res.results.size();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename ResultType, typename Query>
|
||||
void execQuery(Query& query, std::optional<Range> range, std::function<void(const ResultType&)> func)
|
||||
{
|
||||
if (range)
|
||||
applyRange(query, range);
|
||||
|
||||
for (const auto& res : query.resultList())
|
||||
func(res);
|
||||
}
|
||||
|
||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||
} // namespace Database::Utils
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class StarredArtist;
|
||||
class Track;
|
||||
class TrackArtistLink;
|
||||
class User;
|
||||
|
||||
class Artist final : public Object<Artist, ArtistId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
|
||||
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
ArtistSortMethod sortMethod{ ArtistSortMethod::None };
|
||||
std::optional<Range> range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only artists starred by this user
|
||||
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
|
||||
TrackId track; // artists involved in this track
|
||||
ReleaseId release; // artists involved in this release
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
|
||||
Artist() = default;
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static pointer find(Session& session, ArtistId id);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
||||
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related
|
||||
static bool exists(Session& session, ArtistId id);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
||||
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(const std::string& sortName);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t _maxNameLength{ 128 };
|
||||
|
||||
friend class Session;
|
||||
// Create
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(ArtistId)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/AuthTokenId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class User;
|
||||
class AuthToken final : public Object<AuthToken, AuthTokenId>
|
||||
{
|
||||
public:
|
||||
AuthToken() = default;
|
||||
|
||||
// Utility
|
||||
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
|
||||
static pointer find(Session& session, std::string_view value);
|
||||
|
||||
// Accessors
|
||||
const Wt::WDateTime& getExpiry() const { return _expiry; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
const std::string& getValue() const { return _value; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _value, "value");
|
||||
Wt::Dbo::field(a, _expiry, "expiry");
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
|
||||
|
||||
std::string _value;
|
||||
Wt::WDateTime _expiry;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Databas'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(AuthTokenId)
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Track;
|
||||
class ClusterType;
|
||||
class Session;
|
||||
|
||||
class Cluster final : public Object<Cluster, ClusterId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||
std::string clusterTypeName; // if non empty, clusters that belong to this cluster type
|
||||
TrackId track; // if set, clusters involved in this track
|
||||
ReleaseId release; // if set, clusters involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
|
||||
FindParameters& setClusterTypeName(std::string_view _name) { clusterTypeName = _name; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
|
||||
Cluster() = default;
|
||||
|
||||
// Find utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterId> findIds(Session& session, const FindParameters& params);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func);
|
||||
static pointer find(Session& session, ClusterId id);
|
||||
static RangeResults<ClusterId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// May be very slow
|
||||
static std::size_t computeTrackCount(Session& session, ClusterId id);
|
||||
static std::size_t computeReleaseCount(Session& session, ClusterId id);
|
||||
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||
std::size_t getTracksCount() const { return _trackCount; }
|
||||
RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const;
|
||||
std::size_t getReleasesCount() const { return _releaseCount; };
|
||||
|
||||
void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; }
|
||||
void setTrackCount(std::size_t trackCount) { _trackCount = trackCount; }
|
||||
void addTrack(ObjectPtr<Track> track);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
// cached field since queries are too long
|
||||
Wt::Dbo::field(a, _trackCount, "track_count");
|
||||
Wt::Dbo::field(a, _releaseCount, "release_count");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
int _trackCount{};
|
||||
int _releaseCount{};
|
||||
|
||||
Wt::Dbo::ptr<ClusterType> _clusterType;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
|
||||
};
|
||||
|
||||
|
||||
class ClusterType final : public Object<ClusterType, ClusterTypeId>
|
||||
{
|
||||
public:
|
||||
ClusterType() = default;
|
||||
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static pointer find(Session& session, ClusterTypeId id);
|
||||
static RangeResults<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static void remove(Session& session, const std::string& name);
|
||||
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
std::vector<Cluster::pointer> getClusters() const;
|
||||
Cluster::pointer getCluster(const std::string& name) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
ClusterType(std::string_view name);
|
||||
static pointer create(Session& session, std::string_view name);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(ClusterId)
|
||||
LMS_DECLARE_IDTYPE(ClusterTypeId)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
|
||||
#include "utils/RecursiveSharedMutex.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Session;
|
||||
class Db
|
||||
{
|
||||
public:
|
||||
Db(const std::filesystem::path& dbPath, std::size_t connectionCount = 10);
|
||||
|
||||
Session& getTLSSession();
|
||||
|
||||
void executeSql(const std::string& sql);
|
||||
|
||||
private:
|
||||
Db(const Db&) = delete;
|
||||
Db& operator=(const Db&) = delete;
|
||||
|
||||
friend class Session;
|
||||
|
||||
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
||||
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
||||
|
||||
class ScopedConnection
|
||||
{
|
||||
public:
|
||||
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
|
||||
~ScopedConnection();
|
||||
|
||||
Wt::Dbo::SqlConnection* operator->() const;
|
||||
|
||||
private:
|
||||
ScopedConnection(const ScopedConnection&) = delete;
|
||||
ScopedConnection& operator=(const ScopedConnection&) = delete;
|
||||
|
||||
Wt::Dbo::SqlConnectionPool& _connectionPool;
|
||||
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
|
||||
};
|
||||
|
||||
RecursiveSharedMutex _sharedMutex;
|
||||
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
|
||||
|
||||
std::mutex _tlsSessionsMutex;
|
||||
std::vector<std::unique_ptr<Session>> _tlsSessions;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class IdType
|
||||
{
|
||||
public:
|
||||
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
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); }
|
||||
|
||||
ValueType getValue() const { return _id; }
|
||||
|
||||
bool operator==(IdType other) const { return other._id == _id; }
|
||||
bool operator!=(IdType other) const { return !(*this == other); }
|
||||
bool operator<(IdType other) const { return _id < other._id; }
|
||||
bool operator>(IdType other) const { return _id > other._id; }
|
||||
|
||||
private:
|
||||
Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()};
|
||||
};
|
||||
|
||||
#define LMS_DECLARE_IDTYPE(name) \
|
||||
namespace Database { \
|
||||
class name : public IdType \
|
||||
{ \
|
||||
public: \
|
||||
using IdType::IdType; \
|
||||
};\
|
||||
} \
|
||||
namespace std \
|
||||
{ \
|
||||
template<> \
|
||||
class hash<Database::name> \
|
||||
{ \
|
||||
public: \
|
||||
size_t operator()(Database::name id) const \
|
||||
{ \
|
||||
return std::hash<Database::name::ValueType>()(id.getValue()); \
|
||||
} \
|
||||
}; \
|
||||
} // ns std
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/ListenId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class Listen final : public Object<Listen, ListenId>
|
||||
{
|
||||
public:
|
||||
Listen() = default;
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
UserId user;
|
||||
std::optional<ScrobblingBackend> backend;
|
||||
std::optional<SyncState> syncState;
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; }
|
||||
FindParameters& setSyncState(SyncState _syncState) { syncState = _syncState; return *this; }
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
};
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, ListenId id);
|
||||
static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
|
||||
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
|
||||
|
||||
// Stats
|
||||
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static std::size_t getCount(Session& session, UserId userId, TrackId trackId); // for the current backend
|
||||
static std::size_t getCount(Session& session, UserId userId, ReleaseId trackId); // for the current backend
|
||||
|
||||
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId);
|
||||
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId);
|
||||
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
Wt::Dbo::field(a, _backend, "backend"); // TODO rename
|
||||
Wt::Dbo::field(a, _syncState, "sync_state"); // TODO rename
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
|
||||
|
||||
Wt::WDateTime _dateTime;
|
||||
ScrobblingBackend _backend;
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(ListenId)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
#include "database/IdType.hpp"
|
||||
#include "database/TransactionChecker.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
template <typename T>
|
||||
class ObjectPtr
|
||||
{
|
||||
public:
|
||||
ObjectPtr() = default;
|
||||
ObjectPtr(Wt::Dbo::ptr<T> obj) : _obj {obj} {}
|
||||
|
||||
const T* operator->() const { return _obj.get(); }
|
||||
operator bool() const { return _obj.get(); }
|
||||
bool operator!() const { return !_obj.get(); }
|
||||
bool operator==(const ObjectPtr& other) const { return _obj == other._obj; }
|
||||
bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; }
|
||||
|
||||
auto modify() { TransactionChecker::checkWriteTransaction(*_obj.session()); return _obj.modify(); }
|
||||
void remove()
|
||||
{
|
||||
TransactionChecker::checkWriteTransaction(*_obj.session());
|
||||
|
||||
if (_obj->hasOnPreRemove())
|
||||
_obj.modify()->onPreRemove();
|
||||
_obj.remove();
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename, typename> friend class Object;
|
||||
Wt::Dbo::ptr<T> _obj;
|
||||
};
|
||||
|
||||
template <typename T, typename ObjectIdType>
|
||||
class Object : public Wt::Dbo::Dbo<T>
|
||||
{
|
||||
static_assert(std::is_base_of_v<Database::IdType, ObjectIdType>);
|
||||
static_assert(!std::is_same_v<Database::IdType, ObjectIdType>);
|
||||
|
||||
public:
|
||||
using pointer = ObjectPtr<T>;
|
||||
using IdType = ObjectIdType;
|
||||
|
||||
IdType getId() const { return Wt::Dbo::Dbo<T>::self()->Wt::Dbo::template Dbo<T>::id(); }
|
||||
|
||||
// catch some misuses
|
||||
typename Wt::Dbo::dbo_traits<T>::IdType id() const = delete;
|
||||
|
||||
protected:
|
||||
template <typename> friend class ObjectPtr;
|
||||
|
||||
virtual bool hasOnPreRemove() const { return false; }
|
||||
virtual void onPreRemove() {}
|
||||
|
||||
virtual bool hasOnPostCreated() const { return false; }
|
||||
virtual void onPostCreated() {}
|
||||
|
||||
// Can get raw dbo ptr only from Objects
|
||||
template <typename SomeObject>
|
||||
static
|
||||
Wt::Dbo::ptr<SomeObject> getDboPtr(ObjectPtr<SomeObject> ptr) { return ptr._obj; }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class Release final : public Object<Release, ReleaseId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||
ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
|
||||
std::optional<Range> range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
std::optional<DateRange> dateRange;
|
||||
UserId starringUser; // only releases starred by this user
|
||||
std::optional<FeedbackBackend> feedbackBackend; // and for this backend
|
||||
ArtistId artist; // only releases that involved this user
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||
std::optional<ReleaseTypePrimary> primaryType; // if set, matching this primary type
|
||||
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
||||
{
|
||||
artist = _artist;
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Release() = default;
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static bool exists(Session& session, ReleaseId id);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory);
|
||||
static pointer find(Session& session, ReleaseId id);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
||||
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
|
||||
static std::size_t getCount(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related
|
||||
static RangeResults<ReleaseId> findIdsOrderedByArtist(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// Get the cluster of the tracks that belong to this release
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
Wt::WDate getReleaseDate() const;
|
||||
Wt::WDate getOriginalReleaseDate() const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::size_t getMeanBitrate() const;
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||
std::vector<DiscInfo> getDiscs() const;
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
|
||||
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
|
||||
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||
std::size_t getTracksCount() const;
|
||||
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
||||
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
||||
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
||||
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
Wt::Dbo::field(a, _primaryType, "primary_type");
|
||||
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
Wt::WDate getReleaseDate(bool original) const;
|
||||
|
||||
static constexpr std::size_t _maxNameLength{ 128 };
|
||||
|
||||
std::string _name;
|
||||
std::string _MBID;
|
||||
std::optional<int> _totalDisc{};
|
||||
std::optional<ReleaseTypePrimary> _primaryType;
|
||||
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
||||
std::string _artistDisplayName;
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(ReleaseId)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WTime.h>
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
#include "database/Object.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
|
||||
{
|
||||
public:
|
||||
// Do not modify values (just add)
|
||||
enum class UpdatePeriod
|
||||
{
|
||||
Never = 0,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Hourly,
|
||||
};
|
||||
|
||||
// Do not modify values (just add)
|
||||
enum class SimilarityEngineType
|
||||
{
|
||||
Clusters = 0,
|
||||
Features,
|
||||
None,
|
||||
};
|
||||
|
||||
static void init(Session& session);
|
||||
|
||||
static pointer get(Session& session);
|
||||
|
||||
// Getters
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
|
||||
// Setters
|
||||
void addAudioFileExtension(const std::filesystem::path& ext);
|
||||
void setMediaDirectory(const std::filesystem::path& p);
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setExtraTagsToScan(const std::vector<std::string_view>& extraTags);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void incScanVersion();
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _mediaDirectory, "media_directory");
|
||||
Wt::Dbo::field(a, _startTime, "start_time");
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||
}
|
||||
|
||||
private:
|
||||
int _scanVersion{};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
|
||||
std::string _extraTagsToScan;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
|
||||
#include "utils/RecursiveSharedMutex.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TransactionChecker.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class WriteTransaction
|
||||
{
|
||||
public:
|
||||
~WriteTransaction();
|
||||
private:
|
||||
friend class Session;
|
||||
WriteTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
|
||||
|
||||
WriteTransaction(const WriteTransaction&) = delete;
|
||||
WriteTransaction& operator=(const WriteTransaction&) = delete;
|
||||
|
||||
std::unique_lock<RecursiveSharedMutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
class ReadTransaction
|
||||
{
|
||||
public:
|
||||
~ReadTransaction();
|
||||
private:
|
||||
friend class Session;
|
||||
ReadTransaction(Wt::Dbo::Session& session);
|
||||
|
||||
|
||||
ReadTransaction(const ReadTransaction&) = delete;
|
||||
ReadTransaction& operator=(const ReadTransaction&) = delete;
|
||||
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
class Db;
|
||||
class Session
|
||||
{
|
||||
public:
|
||||
Session(Db& database);
|
||||
|
||||
[[nodiscard]] WriteTransaction createWriteTransaction();
|
||||
[[nodiscard]] ReadTransaction createReadTransaction();
|
||||
|
||||
void checkWriteTransaction() { TransactionChecker::checkWriteTransaction(_session); }
|
||||
void checkReadTransaction() { TransactionChecker::checkReadTransaction(_session); }
|
||||
|
||||
void analyze();
|
||||
void optimize();
|
||||
|
||||
void prepareTables(); // need to run only once at startup
|
||||
|
||||
Wt::Dbo::Session& getDboSession() { return _session; }
|
||||
Db& getDb() { return _db; }
|
||||
|
||||
template <typename Object, typename... Args>
|
||||
typename Object::pointer create(Args&&... args)
|
||||
{
|
||||
TransactionChecker::checkWriteTransaction(_session);
|
||||
|
||||
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
|
||||
getDboSession().flush();
|
||||
|
||||
if (res->hasOnPostCreated())
|
||||
res.modify()->onPostCreated();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private:
|
||||
Session(const Session&) = delete;
|
||||
Session& operator=(const Session&) = delete;
|
||||
|
||||
Db& _db;
|
||||
Wt::Dbo::Session _session;
|
||||
};
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/StarredArtistId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
|
||||
{
|
||||
public:
|
||||
StarredArtist() = default;
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredArtistId id);
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId); // current backend
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
FeedbackBackend getFeedbackBackend() const { return _backend; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _backend, "backend");
|
||||
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
|
||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
|
||||
|
||||
FeedbackBackend _backend; // for which backend
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredArtistId)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/StarredReleaseId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Release;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
|
||||
{
|
||||
public:
|
||||
StarredRelease() = default;
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredReleaseId id);
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId); // current feedback backend
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
FeedbackBackend getFeedbackBackend() const { return _backend; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _backend, "backend");
|
||||
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
|
||||
FeedbackBackend _backend; // for which backend
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredReleaseId)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/StarredTrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Track;
|
||||
class Session;
|
||||
class User;
|
||||
|
||||
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
|
||||
{
|
||||
public:
|
||||
StarredTrack() = default;
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<FeedbackBackend> backend; // for this backend
|
||||
std::optional<SyncState> syncState; // and these states
|
||||
UserId user; // and this user
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; }
|
||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
};
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredTrackId id);
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId); // current feedback backend
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
|
||||
static bool exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
|
||||
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
FeedbackBackend getBackend() const { return _backend; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
SyncState getSyncState() const { return _syncState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setSyncState(SyncState state) { _syncState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _backend, "backend");
|
||||
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
|
||||
|
||||
FeedbackBackend _backend; // for which backend
|
||||
SyncState _syncState{ SyncState::PendingAdd };
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredTrackId)
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2016 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <ostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/TrackListId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class TrackArtistLink;
|
||||
class TrackStats;
|
||||
class User;
|
||||
|
||||
class Track final : public Object<Track, TrackId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||
std::string name; // if non empty, must match this name
|
||||
TrackSortMethod sortMethod{ TrackSortMethod::None };
|
||||
std::optional<Range> range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only tracks starred by this user
|
||||
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
|
||||
ArtistId artist; // only tracks that involve this artist
|
||||
std::string artistName; // only tracks that involve this artist name
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
bool nonRelease{}; // only tracks that do not belong to a release
|
||||
ReleaseId release; // matching this release
|
||||
std::string releaseName; // matching this release name
|
||||
TrackListId trackList; // matching this trackList
|
||||
std::optional<int> trackNumber; // matching this track number
|
||||
bool distinct{ true };
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setName(std::string_view _name) { name = _name; return *this; }
|
||||
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
||||
FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
||||
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; }
|
||||
FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; }
|
||||
FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; }
|
||||
FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; }
|
||||
};
|
||||
|
||||
struct PathResult
|
||||
{
|
||||
TrackId trackId;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
Track() = default;
|
||||
|
||||
// Find utility functions
|
||||
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 bool exists(Session& session, TrackId id);
|
||||
static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> findByMBID(Session& session, const UUID& MBID);
|
||||
static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static void find(Session& session, const FindParameters& parameters, std::function<void(const Track::pointer&)> func);
|
||||
static RangeResults<PathResult> findPaths(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// Accessors
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||
void setDiscNumber(std::optional<int> num) { _discNumber = num; }
|
||||
void setTotalTrack(std::optional<int> 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 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 setDate(const Wt::WDate& date) { _date = date; }
|
||||
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
||||
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
||||
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
|
||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
||||
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
|
||||
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
|
||||
std::optional<std::size_t> 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; }
|
||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||
std::optional<int> getYear() const;
|
||||
std::optional<int> getOriginalYear() const;
|
||||
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
||||
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
||||
bool hasCover() const { return _hasCover; }
|
||||
std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); }
|
||||
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||
// no artistLinkTypes means get all
|
||||
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||
Wt::Dbo::field(a, _discNumber, "disc_number");
|
||||
Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _bitrate, "bitrate");
|
||||
Wt::Dbo::field(a, _date, "date");
|
||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||
Wt::Dbo::field(a, _filePath, "file_path");
|
||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||
Wt::Dbo::field(a, _fileAdded, "file_added");
|
||||
Wt::Dbo::field(a, _hasCover, "has_cover");
|
||||
Wt::Dbo::field(a, _trackMBID, "mbid");
|
||||
Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
|
||||
Wt::Dbo::field(a, _copyright, "copyright");
|
||||
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class ::Database::Session;
|
||||
Track(const std::filesystem::path& p);
|
||||
static pointer create(Session& session, const std::filesystem::path& p);
|
||||
|
||||
static constexpr std::size_t _maxNameLength{ 128 };
|
||||
static constexpr std::size_t _maxCopyrightLength{ 128 };
|
||||
static constexpr std::size_t _maxCopyrightURLLength{ 128 };
|
||||
|
||||
int _scanVersion{};
|
||||
std::optional<int> _trackNumber{};
|
||||
std::optional<int> _discNumber{};
|
||||
std::optional<int> _totalTrack{};
|
||||
std::string _discSubtitle;
|
||||
std::string _name;
|
||||
std::chrono::duration<int, std::milli> _duration{};
|
||||
int _bitrate; // in bps
|
||||
Wt::WDate _date;
|
||||
Wt::WDate _originalDate;
|
||||
std::string _filePath;
|
||||
Wt::WDateTime _fileLastWrite;
|
||||
Wt::WDateTime _fileAdded;
|
||||
bool _hasCover{};
|
||||
std::string _trackMBID;
|
||||
std::string _recordingMBID;
|
||||
std::string _copyright;
|
||||
std::string _copyrightURL;
|
||||
std::optional<float> _trackReplayGain;
|
||||
std::optional<float> _releaseReplayGain;
|
||||
std::string _artistDisplayName;
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||
};
|
||||
|
||||
namespace Debug
|
||||
{
|
||||
struct TrackInfo
|
||||
{
|
||||
Session& session;
|
||||
TrackId trackId;
|
||||
};
|
||||
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo);
|
||||
}
|
||||
|
||||
} // namespace database
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2016 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/IdType.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
ArtistId artist; // if set, links involved with this artist
|
||||
ReleaseId release; // if set, artists involved in this release
|
||||
TrackId track; // if set, artists involved in this track
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
};
|
||||
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
||||
|
||||
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
|
||||
static pointer find(Session& session, TrackArtistLinkId linkId);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
||||
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
|
||||
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
std::string_view getSubType() const { return _subType; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _subType, "subtype");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
std::string _subType;
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackBookmarkId)
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class TrackBookmark final : public Object<TrackBookmark, TrackBookmarkId>
|
||||
{
|
||||
public:
|
||||
TrackBookmark () = default;
|
||||
|
||||
// Find utility functions
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, TrackBookmarkId id);
|
||||
static RangeResults<TrackBookmarkId> find(Session& session, UserId userId, std::optional<Range> range = std::nullopt);
|
||||
static pointer find(Session& session, UserId userId, TrackId trackId);
|
||||
|
||||
// Setters
|
||||
void setOffset(std::chrono::milliseconds offset) { _offset = offset; }
|
||||
void setComment(std::string_view comment) { _comment = comment; }
|
||||
|
||||
// Getters
|
||||
std::chrono::milliseconds getOffset() const { return _offset; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _offset, "offset");
|
||||
Wt::Dbo::field(a, _comment, "comment");
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
|
||||
static const std::size_t _maxCommentLength = 128;
|
||||
|
||||
std::chrono::duration<int, std::milli> _offset;
|
||||
std::string _comment;
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
};
|
||||
|
||||
} // namespace database
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackFeaturesId)
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
using FeatureName = std::string;
|
||||
using FeatureValues = std::vector<double>;
|
||||
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
|
||||
|
||||
class TrackFeatures final : public Object<TrackFeatures, TrackFeaturesId>
|
||||
{
|
||||
public:
|
||||
TrackFeatures() = default;
|
||||
|
||||
// Find utilities
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, TrackFeaturesId id);
|
||||
static pointer find(Session& session, TrackId trackId);
|
||||
static RangeResults<TrackFeaturesId> find(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
||||
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
||||
|
||||
// Accessors
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _data, "data");
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
|
||||
std::string _data;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
};
|
||||
|
||||
|
||||
} // namespace database
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackId)
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "database/TrackListId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
class TrackListEntry;
|
||||
class User;
|
||||
|
||||
class TrackList final : public Object<TrackList, TrackListId>
|
||||
{
|
||||
public:
|
||||
TrackList() = default;
|
||||
|
||||
// Search utility
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
||||
std::optional<Range> range;
|
||||
std::optional<TrackListType> type;
|
||||
UserId user; // only tracklists owned by this user
|
||||
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setType(TrackListType _type) { type = _type; return *this; }
|
||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||
FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
};
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId);
|
||||
static pointer find(Session& session, TrackListId tracklistId);
|
||||
static RangeResults<TrackListId> find(Session& session, const FindParameters& params);
|
||||
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
bool isPublic() const { return _isPublic; }
|
||||
TrackListType getType() const { return _type; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
|
||||
// Modifiers
|
||||
void setName(const std::string& name) { _name = name; }
|
||||
void setIsPublic(bool isPublic) { _isPublic = isPublic; }
|
||||
void clear() { _entries.clear(); }
|
||||
|
||||
// Get tracks, ordered by position
|
||||
bool isEmpty() const;
|
||||
std::size_t getCount() const;
|
||||
ObjectPtr<TrackListEntry> getEntry(std::size_t pos) const;
|
||||
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const;
|
||||
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
|
||||
|
||||
RangeResults<ObjectPtr<Artist>> getArtists(const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const;
|
||||
RangeResults<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
||||
RangeResults<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
std::vector<TrackId> getTrackIds() const;
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
|
||||
void setLastModifiedDateTime(const Wt::WDateTime& dateTime);
|
||||
|
||||
// Get clusters, order by occurence
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
|
||||
bool hasTrack(TrackId trackId) const;
|
||||
|
||||
// Ordered from most clusters in common
|
||||
std::vector<ObjectPtr<Track>> getSimilarTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _isPublic, "public");
|
||||
Wt::Dbo::field(a, _creationDateTime, "creation_date_time");
|
||||
Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
|
||||
|
||||
std::string _name;
|
||||
TrackListType _type{ TrackListType::Playlist };
|
||||
bool _isPublic{ false };
|
||||
Wt::WDateTime _creationDateTime;
|
||||
Wt::WDateTime _lastModifiedDateTime;
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
|
||||
};
|
||||
|
||||
class TrackListEntry final : public Object<TrackListEntry, TrackListEntryId>
|
||||
{
|
||||
public:
|
||||
TrackListEntry() = default;
|
||||
|
||||
bool hasOnPostCreated() const override { return true; }
|
||||
void onPostCreated() override;
|
||||
|
||||
bool hasOnPreRemove() const override { return true; }
|
||||
void onPreRemove() override;
|
||||
|
||||
// find utility
|
||||
static pointer getById(Session& session, TrackListEntryId id);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = {});
|
||||
|
||||
Wt::WDateTime _dateTime; // optional date time
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<TrackList> _tracklist;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackListId)
|
||||
LMS_DECLARE_IDTYPE(TrackListEntryId)
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <Wt/Dbo/Session.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class TransactionChecker
|
||||
{
|
||||
public:
|
||||
enum class TransactionType
|
||||
{
|
||||
Read,
|
||||
Write,
|
||||
};
|
||||
|
||||
static void pushWriteTransaction(Wt::Dbo::Session& session);
|
||||
static void pushReadTransaction(Wt::Dbo::Session& session);
|
||||
|
||||
static void popWriteTransaction(Wt::Dbo::Session& session);
|
||||
static void popReadTransaction(Wt::Dbo::Session& session);
|
||||
|
||||
static void checkWriteTransaction(Wt::Dbo::Session& session);
|
||||
static void checkWriteTransaction(Session& session);
|
||||
static void checkReadTransaction(Wt::Dbo::Session& session);
|
||||
static void checkReadTransaction(Session& session);
|
||||
|
||||
private:
|
||||
static void pushTransaction(TransactionType type, Wt::Dbo::Session& session);
|
||||
static void popTransaction(TransactionType type, Wt::Dbo::Session& session);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
// Caution: do not change enum values if they are set!
|
||||
|
||||
// Request:
|
||||
// size = 0 => means we don't want data
|
||||
// Response (via RangeResults)
|
||||
// size => results size
|
||||
struct Range
|
||||
{
|
||||
std::size_t offset{};
|
||||
std::size_t size{};
|
||||
|
||||
bool operator==(const Range& rhs) const { return offset == rhs.offset && size == rhs.size; }
|
||||
};
|
||||
|
||||
// Func must return true to continue iterating
|
||||
template <typename Func>
|
||||
void foreachSubRange(Range range, std::size_t subRangeSize, Func&& func)
|
||||
{
|
||||
assert(subRangeSize > 0);
|
||||
|
||||
Range subRange{ range.offset, std::min(range.size, subRangeSize) };
|
||||
while (subRange.size > 0)
|
||||
{
|
||||
if (!func(subRange))
|
||||
break;
|
||||
|
||||
subRange.offset += subRange.size;
|
||||
subRange.size = std::min(subRangeSize, range.size - (subRange.offset - range.offset));
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
struct RangeResults
|
||||
{
|
||||
Range range;
|
||||
std::vector<T> results;
|
||||
bool moreResults{};
|
||||
|
||||
RangeResults getSubRange(Range subRange)
|
||||
{
|
||||
assert(subRange.offset >= range.offset);
|
||||
|
||||
if (!subRange.size)
|
||||
subRange.size = range.size - (subRange.offset - range.offset);
|
||||
|
||||
subRange.offset = std::min(subRange.offset, range.offset + range.size);
|
||||
subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset);
|
||||
|
||||
RangeResults subResults;
|
||||
|
||||
auto itBegin{ std::cbegin(results) + subRange.offset - range.offset };
|
||||
auto itEnd{ itBegin + subRange.size };
|
||||
subResults.results.reserve(std::distance(itBegin, itEnd));
|
||||
std::copy(itBegin, itEnd, std::back_inserter(subResults.results));
|
||||
|
||||
subResults.range = subRange;
|
||||
if (subRange.offset + subRange.size == range.offset + range.size)
|
||||
subResults.moreResults = moreResults;
|
||||
else
|
||||
subResults.moreResults = true;
|
||||
|
||||
return subResults;
|
||||
}
|
||||
};
|
||||
|
||||
struct DateRange
|
||||
{
|
||||
Wt::WDate begin;
|
||||
Wt::WDate end;
|
||||
|
||||
static DateRange fromYearRange(int from, int to);
|
||||
};
|
||||
|
||||
struct DiscInfo
|
||||
{
|
||||
std::size_t position;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
enum class ArtistSortMethod
|
||||
{
|
||||
None,
|
||||
ByName,
|
||||
BySortName,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class ReleaseSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
Date,
|
||||
OriginalDate,
|
||||
OriginalDateDesc,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class TrackListSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
LastModifiedDesc,
|
||||
};
|
||||
|
||||
enum class TrackSortMethod
|
||||
{
|
||||
None,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
Name,
|
||||
DateDescAndRelease,
|
||||
Release, // order by disc/track number
|
||||
TrackList, // order by asc order in tracklist
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkType
|
||||
{
|
||||
Artist = 0, // regular track artist
|
||||
Arranger = 1,
|
||||
Composer = 2,
|
||||
Conductor = 3,
|
||||
Lyricist = 4,
|
||||
Mixer = 5,
|
||||
Performer = 6,
|
||||
Producer = 7,
|
||||
ReleaseArtist = 8,
|
||||
Remixer = 9,
|
||||
Writer = 10,
|
||||
};
|
||||
|
||||
// User selectable transcoding output formats
|
||||
enum class TranscodingOutputFormat
|
||||
{
|
||||
MP3 = 1,
|
||||
OGG_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
WEBM_VORBIS = 4,
|
||||
MATROSKA_OPUS = 5,
|
||||
};
|
||||
|
||||
using Bitrate = std::uint32_t;
|
||||
// Do not remove values!
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||
|
||||
enum class ScrobblingBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class FeedbackBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class SyncState
|
||||
{
|
||||
PendingAdd = 0,
|
||||
Synchronized = 1,
|
||||
PendingRemove = 2,
|
||||
};
|
||||
|
||||
enum class UserType
|
||||
{
|
||||
REGULAR = 0,
|
||||
ADMIN = 1,
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
enum class UITheme
|
||||
{
|
||||
Light = 0,
|
||||
Dark = 1,
|
||||
};
|
||||
|
||||
enum class SubsonicArtistListMode
|
||||
{
|
||||
AllArtists = 0,
|
||||
ReleaseArtists = 1,
|
||||
TrackArtists = 2,
|
||||
};
|
||||
|
||||
enum class TrackListType
|
||||
{
|
||||
Playlist, // user controlled playlists
|
||||
Internal, // internal usage (current playqueue, history, ...)
|
||||
};
|
||||
|
||||
// as defined in https://musicbrainz.org/doc/Release_Group/Type
|
||||
enum class ReleaseTypePrimary
|
||||
{
|
||||
Album,
|
||||
Single,
|
||||
EP,
|
||||
Broadcast,
|
||||
Other,
|
||||
};
|
||||
|
||||
enum class ReleaseTypeSecondary
|
||||
{
|
||||
Compilation,
|
||||
Soundtrack,
|
||||
Spokenword,
|
||||
Interview,
|
||||
Audiobook,
|
||||
AudioDrama,
|
||||
Live,
|
||||
Remix,
|
||||
DJMix,
|
||||
Mixtape_Street,
|
||||
Demo,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class AuthToken;
|
||||
class Session;
|
||||
|
||||
class User final : public Object<User, UserId>
|
||||
{
|
||||
public:
|
||||
struct PasswordHash
|
||||
{
|
||||
std::string salt;
|
||||
std::string hash;
|
||||
};
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<ScrobblingBackend> scrobblingBackend;
|
||||
std::optional<FeedbackBackend> feedbackBackend;
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; }
|
||||
FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; }
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
};
|
||||
|
||||
static inline constexpr std::size_t MinNameLength{ 3 };
|
||||
static inline constexpr std::size_t MaxNameLength{ 15 };
|
||||
static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false };
|
||||
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
|
||||
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
|
||||
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
|
||||
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
||||
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
|
||||
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
|
||||
|
||||
User() = default;
|
||||
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, UserId id);
|
||||
static pointer find(Session& session, std::string_view loginName);
|
||||
static RangeResults<UserId> find(Session& session, const FindParameters& params);
|
||||
static pointer findDemoUser(Session& session);
|
||||
|
||||
// accessors
|
||||
const std::string& getLoginName() const { return _loginName; }
|
||||
PasswordHash getPasswordHash() const { return PasswordHash{ _passwordSalt, _passwordHash }; }
|
||||
const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
|
||||
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
|
||||
|
||||
// write
|
||||
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
||||
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
|
||||
void setType(UserType type) { _type = type; }
|
||||
void setSubsonicEnableTranscodingByDefault(bool value) { _subsonicEnableTranscodingByDefault = value; }
|
||||
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
|
||||
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
|
||||
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
|
||||
void setRadio(bool val) { _radio = val; }
|
||||
void setRepeatAll(bool val) { _repeatAll = val; }
|
||||
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
||||
void clearAuthTokens();
|
||||
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
|
||||
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
|
||||
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
|
||||
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
|
||||
|
||||
// read
|
||||
bool isAdmin() const { return _type == UserType::ADMIN; }
|
||||
bool isDemo() const { return _type == UserType::DEMO; }
|
||||
UserType getType() const { return _type; }
|
||||
bool getSubsonicEnableTranscodingByDefault() const { return _subsonicEnableTranscodingByDefault; }
|
||||
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
|
||||
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
|
||||
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
|
||||
bool isRepeatAllSet() const { return _repeatAll; }
|
||||
bool isRadioSet() const { return _radio; }
|
||||
UITheme getUITheme() const { return _uiTheme; }
|
||||
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
|
||||
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
|
||||
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
|
||||
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _loginName, "login_name");
|
||||
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
||||
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
||||
Wt::Dbo::field(a, _lastLogin, "last_login");
|
||||
Wt::Dbo::field(a, _subsonicEnableTranscodingByDefault, "subsonic_enable_transcoding_by_default");
|
||||
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputFormat, "subsonic_default_transcode_format");
|
||||
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
|
||||
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
||||
Wt::Dbo::field(a, _uiTheme, "ui_theme");
|
||||
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
|
||||
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
|
||||
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
|
||||
|
||||
// UI player settings
|
||||
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
|
||||
Wt::Dbo::field(a, _repeatAll, "repeat_all");
|
||||
Wt::Dbo::field(a, _radio, "radio");
|
||||
|
||||
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
User(std::string_view loginName);
|
||||
static pointer create(Session& session, std::string_view loginName);
|
||||
|
||||
std::string _loginName;
|
||||
std::string _passwordSalt;
|
||||
std::string _passwordHash;
|
||||
Wt::WDateTime _lastLogin;
|
||||
UITheme _uiTheme{ defaultUITheme };
|
||||
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
|
||||
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
|
||||
std::string _listenbrainzToken; // Musicbrainz Identifier
|
||||
|
||||
// Admin defined settings
|
||||
UserType _type{ UserType::REGULAR };
|
||||
|
||||
// User defined settings
|
||||
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
|
||||
bool _subsonicEnableTranscodingByDefault{ defaultSubsonicEnableTranscodingByDefault };
|
||||
TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat };
|
||||
int _subsonicDefaultTranscodingOutputBitrate{ defaultSubsonicTranscodingOutputBitrate };
|
||||
|
||||
// User's dynamic data (UI)
|
||||
int _curPlayingTrackPos{}; // Current track position in queue
|
||||
bool _repeatAll{};
|
||||
bool _radio{};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
|
||||
};
|
||||
|
||||
} // namespace Databas'
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(UserId)
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, Artist)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_FALSE(Artist::exists(session, 35));
|
||||
EXPECT_FALSE(Artist::exists(session, 0));
|
||||
EXPECT_FALSE(Artist::exists(session, 1));
|
||||
EXPECT_EQ(Artist::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(artist.get());
|
||||
EXPECT_FALSE(!artist.get());
|
||||
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::exists(session, artist.getId()));
|
||||
EXPECT_EQ(Artist::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
artists = Artist::findOrphanIds(session);
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::find(session, Artist::FindParameters {}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front()->getId(), artist.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
Artist::find(session, Artist::FindParameters{}, [&](const Artist::pointer& a)
|
||||
{
|
||||
visited = true;
|
||||
EXPECT_EQ(a->getId(), artist.getId());
|
||||
});
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track.get().modify()->setName("MyTrackName");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
ASSERT_EQ(track->getArtistLinks().size(), 1);
|
||||
auto artistLink{ track->getArtistLinks().front() };
|
||||
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
|
||||
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
|
||||
|
||||
ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1);
|
||||
EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
|
||||
EXPECT_EQ(track->getArtists({}).size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front(), artist.getId());
|
||||
|
||||
ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1);
|
||||
EXPECT_TRUE(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).empty());
|
||||
EXPECT_EQ(track->getArtistIds({}).size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist });
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
EXPECT_EQ(track->getArtistLinks().size(), 3);
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
|
||||
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||
{
|
||||
ScopedTrack track{ session, "track" };
|
||||
ScopedArtist artist1{ session, "artist1" };
|
||||
ScopedArtist artist2{ session, "artist2" };
|
||||
ASSERT_NE(artist1.getId(), artist2.getId());
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 2);
|
||||
EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId())
|
||||
|| (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId()));
|
||||
|
||||
EXPECT_EQ(track->getArtists({}).size(), 2);
|
||||
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
|
||||
EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty());
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist }));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByName)
|
||||
{
|
||||
ScopedArtist artist{ session, "AAA" };
|
||||
ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artist.get().modify()->setSortName("ZZZ");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty());
|
||||
|
||||
const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"A"})) };
|
||||
ASSERT_EQ(artistsByAAA.results.size(), 1);
|
||||
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
|
||||
|
||||
const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters {}.setKeywords({"Z"})) };
|
||||
ASSERT_EQ(artistsByZZZ.results.size(), 1);
|
||||
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::find(session, "NNN").empty());
|
||||
EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByNameEscaped)
|
||||
{
|
||||
ScopedArtist artist1{ session, R"(MyArtist%)" };
|
||||
ScopedArtist artist2{ session, R"(%MyArtist)" };
|
||||
ScopedArtist artist3{ session, R"(%_MyArtist)" };
|
||||
|
||||
ScopedArtist artist4{ session, R"(MyArtist%foo)" };
|
||||
ScopedArtist artist5{ session, R"(foo%MyArtist)" };
|
||||
ScopedArtist artist6{ session, R"(%AMyArtist)" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(MyArtist%)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||
EXPECT_TRUE(Artist::find(session, R"(MyArtistFoo)").empty());
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(%MyArtist)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||
EXPECT_TRUE(Artist::find(session, R"(FooMyArtist)").empty());
|
||||
}
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(%_MyArtist)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist3.getId());
|
||||
EXPECT_TRUE(Artist::find(session, R"(%CMyArtist)").empty());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
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)) };
|
||||
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)) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results[0], artist3.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_sortMethod)
|
||||
{
|
||||
ScopedArtist artistA{ session, "artistA" };
|
||||
ScopedArtist artistB{ session, "artistB" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
artistA.get().modify()->setSortName("sortNameB");
|
||||
artistB.get().modify()->setSortName("sortNameA");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
|
||||
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
|
||||
|
||||
ASSERT_EQ(allArtistsByName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
|
||||
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
|
||||
|
||||
ASSERT_EQ(allArtistsBySortName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
|
||||
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
{
|
||||
ScopedArtist artist{ session, "artist" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
track1.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByRelease)
|
||||
{
|
||||
ScopedArtist artist{ session, "artist" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
add_executable(test-database
|
||||
Artist.cpp
|
||||
Cluster.cpp
|
||||
Common.cpp
|
||||
DatabaseTest.cpp
|
||||
Listen.cpp
|
||||
Release.cpp
|
||||
StarredArtist.cpp
|
||||
StarredRelease.cpp
|
||||
StarredTrack.cpp
|
||||
Track.cpp
|
||||
TrackBookmark.cpp
|
||||
TrackFeatures.cpp
|
||||
TrackList.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test-database PRIVATE
|
||||
lmsdatabase
|
||||
GTest::GTest
|
||||
)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
gtest_discover_tests(test-database)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
EXPECT_EQ(ClusterType::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
EXPECT_EQ(Cluster::getCount(session), 1);
|
||||
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
||||
|
||||
{
|
||||
const auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto clusters{ Cluster::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
}
|
||||
|
||||
auto clusterTypes{ ClusterType::findIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
clusterTypes = ClusterType::findUsed(session);
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
clusterTypes = ClusterType::findOrphanIds(session);
|
||||
EXPECT_TRUE(clusterTypes.results.empty());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
ASSERT_TRUE(ClusterType::findUsed(session).results.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
}
|
||||
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters{ Cluster::findOrphanIds(session) };
|
||||
EXPECT_EQ(clusters.results.size(), 2);
|
||||
EXPECT_TRUE(track->getClusters().empty());
|
||||
EXPECT_TRUE(track->getClusterIds().empty());
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster1.getId());
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters{ Cluster::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster2.getId());
|
||||
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setClusters({ cluster2.getId() }));
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto clusters{ track->getClusters() };
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front()->getId(), cluster1.getId());
|
||||
|
||||
auto clusterIds{ track->getClusterIds() };
|
||||
ASSERT_EQ(clusterIds.size(), 1);
|
||||
EXPECT_EQ(clusterIds.front(), cluster1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
const std::vector<ClusterId> clusterIds{ cluster1.getId(), cluster2.getId() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
|
||||
ASSERT_FALSE(tracks.results.empty());
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_multiTracks)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size());
|
||||
|
||||
for (TrackId trackId : cluster->getTracks().results)
|
||||
{
|
||||
auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) };
|
||||
EXPECT_TRUE(it != std::cend(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DatabaseFixture, ClusterType_singleTrack)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}).results.empty());
|
||||
EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results.empty());
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters {Cluster::findIds(session, Cluster::FindParameters{}).results};
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterType(clusterType.getId())).results;
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results;
|
||||
EXPECT_EQ(clusters.size(), 0);
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("MyClusterType")).results;
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
ScopedCluster unusedCluster{ session, clusterType.lockAndGet(), "MyClusterUnused" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
ASSERT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty());
|
||||
EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0);
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track.get().modify()->setRelease(release.get());
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto clusters{ Cluster::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), unusedCluster.getId());
|
||||
}
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, unusedCluster.getId()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "Cluster2" };
|
||||
ScopedCluster cluster3{ session, clusterType.lockAndGet(), "Cluster3" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getClusters().size(), 1);
|
||||
EXPECT_EQ(track->getClusterIds().size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster2.getId() })).results.empty());
|
||||
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty());
|
||||
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster2.getId() }));
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster1.getId() }));
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
|
||||
{
|
||||
constexpr std::size_t nbTracks{ 10 };
|
||||
constexpr std::size_t nbClusters{ 5 };
|
||||
|
||||
std::list<ScopedTrack> tracks;
|
||||
std::list<ScopedCluster> clusters;
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
|
||||
for (std::size_t i{}; i < nbClusters; ++i)
|
||||
clusters.emplace_back(session, clusterType.lockAndGet(), "MyCluster" + std::to_string(i));
|
||||
|
||||
for (std::size_t i{}; i < nbTracks; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrackFile" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
for (auto& cluster : clusters)
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
std::vector<ClusterId> clusterIds;
|
||||
std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); });
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(clusterIds)) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyClusterType" };
|
||||
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) };
|
||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
{
|
||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
for (std::size_t i{}; i < 5; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i{ 5 }; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.back().getId()}, Range {0, 4}) };
|
||||
EXPECT_EQ(similarTracks.results.size(), 4);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||
}
|
||||
|
||||
{
|
||||
auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) };
|
||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
track.get().modify()->setRelease(release.get());
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster.getId() }));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster1.getId(), cluster2.getId() }));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
std::list<ScopedTrack> tracks;
|
||||
|
||||
for (std::size_t i{}; i < 20; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
session.create<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
|
||||
if (i < 10)
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto similarTracks{ trackList->getSimilarTracks() };
|
||||
EXPECT_EQ(similarTracks.size(), 5);
|
||||
|
||||
for (auto similarTrack : similarTracks)
|
||||
EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::cend(tracks), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); }));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
std::list<ScopedTrack> tracks;
|
||||
|
||||
for (std::size_t i{}; i < 20; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
session.create<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
|
||||
if (i < 10)
|
||||
{
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
else if (i < 15)
|
||||
{
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto similarTracks{ trackList->getSimilarTracks(0, 5) };
|
||||
ASSERT_EQ(similarTracks.size(), 5);
|
||||
|
||||
for (auto similarTrack : similarTracks)
|
||||
EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::next(std::cbegin(tracks), 10), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); }));
|
||||
}
|
||||
|
||||
{
|
||||
const auto similarTracks{ trackList->getSimilarTracks(5, 10) };
|
||||
ASSERT_EQ(similarTracks.size(), 5);
|
||||
|
||||
for (auto similarTrack : similarTracks)
|
||||
EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 10), std::next(std::cbegin(tracks), 15), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); }));
|
||||
}
|
||||
|
||||
EXPECT_TRUE(trackList->getSimilarTracks(10, 10).empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
{
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
ScopedArtist artist3{ session, "MyArtist3" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(artist1->findSimilarArtistIds().results.empty());
|
||||
EXPECT_TRUE(artist2->findSimilarArtistIds().results.empty());
|
||||
EXPECT_TRUE(artist3->findSimilarArtistIds().results.empty());
|
||||
}
|
||||
|
||||
std::list<ScopedTrack> tracks;
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
else
|
||||
{
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size()));
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds() };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::ReleaseArtist}) };
|
||||
EXPECT_EQ(artists.results.empty(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({TrackArtistLinkType::Composer}) };
|
||||
EXPECT_TRUE(artists.results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist2->findSimilarArtistIds() };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||
EXPECT_EQ(artists.results[1], artist3.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
ScopedRelease release3{ session, "MyRelease3" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(release1->getSimilarReleases().empty());
|
||||
EXPECT_TRUE(release2->getSimilarReleases().empty());
|
||||
EXPECT_TRUE(release3->getSimilarReleases().empty());
|
||||
}
|
||||
|
||||
std::list<ScopedTrack> tracks;
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
tracks.back().get().modify()->setRelease(release1.get());
|
||||
else
|
||||
{
|
||||
tracks.back().get().modify()->setRelease(release2.get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size()));
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
tracks.back().get().modify()->setRelease(release3.get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto releases{ release1->getSimilarReleases() };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto releases{ release2->getSimilarReleases() };
|
||||
ASSERT_EQ(releases.size(), 2);
|
||||
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
||||
EXPECT_EQ(releases[1]->getId(), release3.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
#include "database/StarredRelease.hpp"
|
||||
#include "database/StarredTrack.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
TmpDatabase::TmpDatabase()
|
||||
: _tmpFile{ std::tmpnam(nullptr) }
|
||||
, _fileDeleter{ _tmpFile }
|
||||
, _db{ _tmpFile }
|
||||
{
|
||||
}
|
||||
|
||||
Database::Db& TmpDatabase::getDb()
|
||||
{
|
||||
return _db;
|
||||
}
|
||||
|
||||
DatabaseFixture::~DatabaseFixture()
|
||||
{
|
||||
testDatabaseEmpty();
|
||||
}
|
||||
|
||||
void DatabaseFixture::SetUpTestCase()
|
||||
{
|
||||
_tmpDb = std::make_unique<TmpDatabase>();
|
||||
{
|
||||
Database::Session s{ _tmpDb->getDb() };
|
||||
s.prepareTables();
|
||||
s.analyze();
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseFixture::TearDownTestCase()
|
||||
{
|
||||
_tmpDb.reset();
|
||||
}
|
||||
|
||||
void DatabaseFixture::testDatabaseEmpty()
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
EXPECT_EQ(Artist::getCount(session), 0);
|
||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||
EXPECT_EQ(Listen::getCount(session), 0);
|
||||
EXPECT_EQ(Release::getCount(session), 0);
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||
EXPECT_EQ(Track::getCount(session), 0);
|
||||
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||
EXPECT_EQ(User::getCount(session), 0);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Common_subRangeEmpty)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
RangeResults<int> results;
|
||||
results.range = Range{ 0, 0 };
|
||||
results.results = {};
|
||||
results.moreResults = false;
|
||||
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {0, 0}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 0);
|
||||
EXPECT_EQ(subRange.range, Range{});
|
||||
}
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {0, 1}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Common_subRangeForeach)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
Range range;
|
||||
std::size_t subRangeSize;
|
||||
std::vector<Range> expectedSubRanges;
|
||||
};
|
||||
|
||||
TestCase testCases[]
|
||||
{
|
||||
{Range{0, 0}, 1, {}},
|
||||
{Range{1, 0}, 1, {}},
|
||||
{Range{1, 1}, 1, { Range{ 1,1 } }},
|
||||
{Range{1, 3}, 1, { Range{ 1,1 }, Range {2,1}, Range{3,1} }},
|
||||
{Range{0, 100}, 100, { Range{0,100} }},
|
||||
{Range{0, 50}, 100, { Range{0,50} }},
|
||||
{Range{100, 200}, 100, { Range{100,100}, Range{200,100} }},
|
||||
{Range{100, 101}, 100, { Range{100,100}, Range{200,1}}},
|
||||
{Range{1000, 10}, 100, { Range{1000,10} }},
|
||||
{Range{1, 100}, 50, { Range{1,50}, Range{51, 50} }},
|
||||
};
|
||||
|
||||
for (const TestCase& test : testCases)
|
||||
{
|
||||
std::vector<Range> subRanges;
|
||||
foreachSubRange(test.range, test.subRangeSize, [&](Range subRange)
|
||||
{
|
||||
subRanges.push_back(subRange);
|
||||
return true;
|
||||
});
|
||||
|
||||
EXPECT_EQ(subRanges, test.expectedSubRanges) << ", test index = " << std::distance(std::cbegin(testCases), &test);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Common_IdType)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
{
|
||||
const IdType id{};
|
||||
EXPECT_FALSE(id.isValid());
|
||||
}
|
||||
|
||||
{
|
||||
const IdType id{ 0 };
|
||||
EXPECT_TRUE(id.isValid());
|
||||
}
|
||||
|
||||
{
|
||||
const IdType id1{ 0 };
|
||||
const IdType id2{ 0 };
|
||||
EXPECT_EQ(id1, id2);
|
||||
}
|
||||
|
||||
{
|
||||
const IdType id1{ 0 };
|
||||
const IdType id2{ 1 };
|
||||
EXPECT_NE(id1, id2);
|
||||
EXPECT_LT(id1, id2);
|
||||
EXPECT_GT(id2, id1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Common_subRange)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
RangeResults<int> results;
|
||||
results.range = Range{ 0, 2 };
|
||||
results.results = { 5, 6 };
|
||||
results.moreResults = false;
|
||||
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {0, 1}) };
|
||||
EXPECT_TRUE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 1);
|
||||
EXPECT_EQ(subRange.results.front(), 5);
|
||||
}
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {1, 1}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 1);
|
||||
EXPECT_EQ(subRange.results.front(), 6);
|
||||
}
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {0, 2}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 2);
|
||||
EXPECT_EQ(subRange.results.front(), 5);
|
||||
EXPECT_EQ(subRange.results.back(), 6);
|
||||
}
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 2);
|
||||
EXPECT_EQ(subRange.results.front(), 5);
|
||||
EXPECT_EQ(subRange.results.back(), 6);
|
||||
EXPECT_EQ(subRange.range, results.range);
|
||||
}
|
||||
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {1, 0}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 1);
|
||||
EXPECT_EQ(subRange.results.front(), 6);
|
||||
const Range expectedRange{ 1, 1 };
|
||||
EXPECT_EQ(subRange.range, expectedRange);
|
||||
}
|
||||
{
|
||||
auto subRange{ results.getSubRange(Range {3, 2}) };
|
||||
EXPECT_FALSE(subRange.moreResults);
|
||||
ASSERT_EQ(subRange.results.size(), 0);
|
||||
const Range expectedRange{ 2, 0 };
|
||||
EXPECT_EQ(subRange.range, expectedRange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
template <typename T>
|
||||
class ScopedEntity
|
||||
{
|
||||
public:
|
||||
using IdType = typename T::IdType;
|
||||
|
||||
template <typename... Args>
|
||||
ScopedEntity(Database::Session& session, Args&& ...args)
|
||||
: _session {session}
|
||||
{
|
||||
auto transaction {_session.createWriteTransaction()};
|
||||
|
||||
auto entity {_session.create<T>(std::forward<Args>(args)...)};
|
||||
EXPECT_TRUE(entity);
|
||||
_id = entity->getId();
|
||||
}
|
||||
|
||||
~ScopedEntity()
|
||||
{
|
||||
auto transaction {_session.createWriteTransaction()};
|
||||
|
||||
auto entity {T::find(_session, _id)};
|
||||
// could not be here due to "on delete cascade" constraints...
|
||||
if (entity)
|
||||
entity.remove();
|
||||
}
|
||||
|
||||
ScopedEntity(const ScopedEntity&) = delete;
|
||||
ScopedEntity(ScopedEntity&&) = delete;
|
||||
ScopedEntity& operator=(const ScopedEntity&) = delete;
|
||||
ScopedEntity& operator=(ScopedEntity&&) = delete;
|
||||
|
||||
typename T::pointer lockAndGet()
|
||||
{
|
||||
auto transaction {_session.createReadTransaction()};
|
||||
return get();
|
||||
}
|
||||
|
||||
typename T::pointer get()
|
||||
{
|
||||
_session.checkReadTransaction();
|
||||
|
||||
auto entity {T::find(_session, _id)};
|
||||
EXPECT_TRUE(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
typename T::pointer operator->()
|
||||
{
|
||||
return get();
|
||||
}
|
||||
|
||||
IdType getId() const { return _id; }
|
||||
|
||||
private:
|
||||
Database::Session& _session;
|
||||
IdType _id {};
|
||||
};
|
||||
|
||||
using ScopedArtist = ScopedEntity<Database::Artist>;
|
||||
using ScopedCluster = ScopedEntity<Database::Cluster>;
|
||||
using ScopedClusterType = ScopedEntity<Database::ClusterType>;
|
||||
using ScopedRelease = ScopedEntity<Database::Release>;
|
||||
using ScopedTrack = ScopedEntity<Database::Track>;
|
||||
using ScopedTrackList = ScopedEntity<Database::TrackList>;
|
||||
using ScopedUser = ScopedEntity<Database::User>;
|
||||
|
||||
class ScopedFileDeleter final
|
||||
{
|
||||
public:
|
||||
ScopedFileDeleter(const std::filesystem::path& path) : _path {path} {}
|
||||
~ScopedFileDeleter() { std::filesystem::remove(_path); }
|
||||
|
||||
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
|
||||
ScopedFileDeleter(ScopedFileDeleter&&) = delete;
|
||||
ScopedFileDeleter operator=(const ScopedFileDeleter&) = delete;
|
||||
ScopedFileDeleter operator=(ScopedFileDeleter&&) = delete;
|
||||
|
||||
private:
|
||||
const std::filesystem::path _path;
|
||||
};
|
||||
|
||||
class TmpDatabase final
|
||||
{
|
||||
public:
|
||||
TmpDatabase ();
|
||||
|
||||
Database::Db& getDb();
|
||||
|
||||
private:
|
||||
const std::filesystem::path _tmpFile;
|
||||
ScopedFileDeleter _fileDeleter;
|
||||
Database::Db _db;
|
||||
};
|
||||
|
||||
class DatabaseFixture : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
~DatabaseFixture();
|
||||
|
||||
public:
|
||||
static void SetUpTestCase();
|
||||
static void TearDownTestCase();
|
||||
|
||||
private:
|
||||
void testDatabaseEmpty();
|
||||
|
||||
static inline std::unique_ptr<TmpDatabase> _tmpDb {};
|
||||
|
||||
public:
|
||||
Database::Session session {_tmpDb->getDb()};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <list>
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
|
||||
{
|
||||
constexpr std::size_t nbTracks{ 10 };
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedArtist artist{ session, "MyArtst" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
for (std::size_t i{}; i < nbTracks; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
tracks.back().get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
const auto releaseTracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(releaseTracks.results.size(), nbTracks);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
auto artists{ release->getArtists() };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleUser)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(User::find(session, User::FindParameters{}).results.empty());
|
||||
EXPECT_EQ(User::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(User::find(session, User::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(User::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,690 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, Release)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Release::getCount(session), 0);
|
||||
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 0);
|
||||
EXPECT_FALSE(Release::exists(session, 0));
|
||||
EXPECT_FALSE(Release::exists(session, 1));
|
||||
}
|
||||
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Release::getCount(session), 1);
|
||||
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1);
|
||||
EXPECT_TRUE(Release::exists(session, release.getId()));
|
||||
|
||||
{
|
||||
const auto releases{ Release::findOrphanIds(session) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
EXPECT_EQ(release->getDuration(), std::chrono::seconds{ 0 });
|
||||
}
|
||||
|
||||
{
|
||||
const auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front()->getId(), release.getId());
|
||||
}
|
||||
|
||||
{
|
||||
bool visited{};
|
||||
Release::find(session, Release::FindParameters{}, [&](const Release::pointer& r)
|
||||
{
|
||||
visited = true;
|
||||
EXPECT_EQ(r->getId(), release.getId());
|
||||
});
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track.get().modify()->setRelease(release.get());
|
||||
track.get().modify()->setName("MyTrackName");
|
||||
release.get().modify()->setName("MyReleaseName");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
ASSERT_TRUE(track->getRelease());
|
||||
EXPECT_EQ(track->getRelease()->getId(), release.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
|
||||
auto releases{ Release::findOrphanIds(session) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_findByNameAndPath)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
ScopedRelease release2{ session, "MyRelease" };
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track1.get().modify()->setPath("/tmp/foo/foo.mp3");
|
||||
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
track2.get().modify()->setPath("/tmp/bar/bar.mp3");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
{
|
||||
const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto releases{ Release::find(session, "MyRelease", "/tmp/bar") };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
ScopedRelease release2{ session, "MyRelease%" };
|
||||
ScopedRelease release3{ session, "%MyRelease" };
|
||||
ScopedRelease release4{ session, "MyRelease%Foo" };
|
||||
ScopedRelease release5{ session, "Foo%MyRelease" };
|
||||
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" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
track3.get().modify()->setRelease(release3.get());
|
||||
track4.get().modify()->setRelease(release4.get());
|
||||
track5.get().modify()->setRelease(release5.get());
|
||||
track6.get().modify()->setRelease(release6.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"Release"})) };
|
||||
EXPECT_EQ(releases.results.size(), 6);
|
||||
}
|
||||
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"MyRelease"})) };
|
||||
ASSERT_EQ(releases.results.size(), 5);
|
||||
EXPECT_TRUE(std::none_of(std::cbegin(releases.results), std::cend(releases.results), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); }));
|
||||
}
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"MyRelease%"})) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release2.getId());
|
||||
EXPECT_EQ(releases.results[1], release4.getId());
|
||||
}
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"%MyRelease"})) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], release3.getId());
|
||||
EXPECT_EQ(releases.results[1], release5.getId());
|
||||
}
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"Foo%MyRelease"})) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results[0], release5.getId());
|
||||
}
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"MyRelease%Foo"})) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results[0], release4.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setTotalTrack(36);
|
||||
release1.get().modify()->setTotalDisc(6);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 6);
|
||||
}
|
||||
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setTotalTrack(37);
|
||||
release1.get().modify()->setTotalDisc(67);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 67);
|
||||
}
|
||||
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(release2->getTotalDisc());
|
||||
}
|
||||
|
||||
ScopedTrack track3{ session, "MyTrack3" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track3.get().modify()->setRelease(release2.get());
|
||||
track3.get().modify()->setTotalTrack(7);
|
||||
release2.get().modify()->setTotalDisc(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release2->getTotalDisc(), 5);
|
||||
ASSERT_TRUE(track3->getTotalTrack());
|
||||
EXPECT_EQ(*track3->getTotalTrack(), 7);
|
||||
ASSERT_TRUE(release2->getTotalDisc());
|
||||
EXPECT_EQ(*release2->getTotalDisc(), 5);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
|
||||
ScopedTrack track1A{ session, "MyTrack1A" };
|
||||
ScopedTrack track1B{ session, "MyTrack1B" };
|
||||
ScopedTrack track2A{ session, "MyTrack2A" };
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty());
|
||||
EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1A.get().modify()->setRelease(release1.get());
|
||||
track1B.get().modify()->setRelease(release1.get());
|
||||
track2A.get().modify()->setRelease(release2.get());
|
||||
track2B.get().modify()->setRelease(release2.get());
|
||||
|
||||
track1A.get().modify()->setTrackNumber(1);
|
||||
track1B.get().modify()->setTrackNumber(2);
|
||||
|
||||
track2A.get().modify()->setDiscNumber(2);
|
||||
track2A.get().modify()->setTrackNumber(1);
|
||||
track2B.get().modify()->setTrackNumber(2);
|
||||
track2B.get().modify()->setDiscNumber(1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
|
||||
ASSERT_FALSE(tracks.results.empty());
|
||||
EXPECT_EQ(tracks.results.front(), track1A.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) };
|
||||
ASSERT_FALSE(tracks.results.empty());
|
||||
EXPECT_EQ(tracks.results.front(), track2B.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
const Wt::WDate release1Date{ Wt::WDate {1994, 2, 3} };
|
||||
const Wt::WDate release1OriginalDate{ Wt::WDate {1993, 4, 5} };
|
||||
|
||||
ScopedTrack track1A{ session, "MyTrack1A" };
|
||||
ScopedTrack track1B{ session, "MyTrack1B" };
|
||||
ScopedTrack track2A{ session, "MyTrack2A" };
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000))) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1A.get().modify()->setRelease(release1.get());
|
||||
track1B.get().modify()->setRelease(release1.get());
|
||||
track2A.get().modify()->setRelease(release2.get());
|
||||
track2B.get().modify()->setRelease(release2.get());
|
||||
|
||||
|
||||
track1A.get().modify()->setDate(release1Date);
|
||||
track1B.get().modify()->setDate(release1Date);
|
||||
track1A.get().modify()->setOriginalDate(release1OriginalDate);
|
||||
track1B.get().modify()->setOriginalDate(release1OriginalDate);
|
||||
|
||||
EXPECT_EQ(release1.get()->getReleaseDate(), release1Date);
|
||||
EXPECT_EQ(release1.get()->getOriginalReleaseDate(), release1OriginalDate);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000))) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994)));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993)));
|
||||
ASSERT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_writtenAfter)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_artist)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer }));
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId()));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist }));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer }));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist }));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist, TrackArtistLinkType::Composer }));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setDiscNumber(6);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_releaseType)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getPrimaryType(), std::nullopt);
|
||||
EXPECT_EQ(release.get()->getSecondaryTypes(), EnumSet<ReleaseTypeSecondary> {});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
release.get().modify()->setPrimaryType({ ReleaseTypePrimary::Album });
|
||||
release.get().modify()->setSecondaryTypes({ ReleaseTypeSecondary::Compilation });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getPrimaryType(), ReleaseTypePrimary::Album);
|
||||
EXPECT_TRUE(release.get()->getSecondaryTypes().contains(ReleaseTypeSecondary::Compilation));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
const Wt::WDate release1Date{ Wt::WDate {2000, 2, 3} };
|
||||
const Wt::WDate release1OriginalDate{ Wt::WDate {1993, 4, 5} };
|
||||
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
const Wt::WDate release2Date{ Wt::WDate {1994, 2, 3} };
|
||||
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
|
||||
ASSERT_LT(release2Date, release1Date);
|
||||
ASSERT_GT(release2Date, release1OriginalDate);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track1.get().modify()->setOriginalDate(release1OriginalDate);
|
||||
track1.get().modify()->setDate(release1Date);
|
||||
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
track2.get().modify()->setDate(release2Date);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Name)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||
EXPECT_EQ(releases.results.back(), release2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Random)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Date)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results.front(), release2.getId());
|
||||
EXPECT_EQ(releases.results.back(), release1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDate)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||
EXPECT_EQ(releases.results.back(), release2.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDateDesc)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results.front(), release2.getId());
|
||||
EXPECT_EQ(releases.results.back(), release1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_meanBitrate)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track3{ session, "MyTrack3" };
|
||||
|
||||
auto checkExpectedBitrate = [&](std::size_t bitrate)
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release1->getMeanBitrate(), bitrate);
|
||||
};
|
||||
|
||||
checkExpectedBitrate(0);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setBitrate(128);
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
|
||||
checkExpectedBitrate(128);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setBitrate(256);
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
checkExpectedBitrate(192);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track3.get().modify()->setBitrate(0);
|
||||
track3.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
checkExpectedBitrate(192); // 0 should not be taken into account
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
using ScopedStarredArtist = ScopedEntity<Database::StarredArtist>;
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist)
|
||||
{
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredArtist);
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotArtist, Artist::pointer{});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::Internal);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
||||
{
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
starredArtist.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||
{
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredArtist starredArtist1{ session, artist1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
ScopedStarredArtist starredArtist2{ session, artist2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(artists.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
|
||||
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
|
||||
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
#include "database/StarredRelease.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
using ScopedStarredRelease = ScopedEntity<Database::StarredRelease>;
|
||||
|
||||
TEST_F(DatabaseFixture, StarredRelease)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredRelease);
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||
|
||||
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotRelease, StarredRelease::pointer{});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
starredRelease.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredRelease starredRelease1{ session, release1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
ScopedStarredRelease starredRelease2{ session, release2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(releases.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
|
||||
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
|
||||
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
#include "database/StarredTrack.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto starredTrack{ StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredTrack);
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto gotTrack{ StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ StarredTrack::find(session, track->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotRelease, StarredTrack::pointer{});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
{
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredTrack starredTrack1{ session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
ScopedStarredTrack starredTrack2{ session, track2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
|
||||
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(tracks.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, Track)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0);
|
||||
EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0);
|
||||
EXPECT_EQ(Track::getCount(session), 0);
|
||||
EXPECT_FALSE(Track::exists(session, 0));
|
||||
|
||||
{
|
||||
bool visited{};
|
||||
Track::find(session, Track::FindParameters{}, [&](const Track::pointer&) {visited = true;});
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Track::getCount(session), 1);
|
||||
EXPECT_TRUE(Track::exists(session, track.getId()));
|
||||
auto myTrack{ Track::find(session, track.getId()) };
|
||||
ASSERT_TRUE(myTrack);
|
||||
EXPECT_EQ(myTrack->getId(), track.getId());
|
||||
|
||||
{
|
||||
bool visited{};
|
||||
Track::find(session, Track::FindParameters{}, [&](const Track::pointer& t)
|
||||
{
|
||||
visited = true;
|
||||
EXPECT_EQ(t->getId(), track.getId());
|
||||
});
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracks)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrackFile1" };
|
||||
ScopedTrack track2{ session, "MyTrackFile2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(track1.getId() != track2.getId());
|
||||
EXPECT_TRUE(track1.get() != track2.get());
|
||||
EXPECT_FALSE(track1.get() == track2.get());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
||||
{
|
||||
ScopedTrack track1{ session, "" };
|
||||
ScopedTrack track2{ session, "" };
|
||||
ScopedTrack track3{ session, "" };
|
||||
ScopedTrack track4{ session, "" };
|
||||
ScopedTrack track5{ session, "" };
|
||||
ScopedTrack track6{ session, "" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setName("MyTrack");
|
||||
track2.get().modify()->setName("MyTrack%");
|
||||
track3.get().modify()->setName("MyTrack%Foo");
|
||||
track4.get().modify()->setName("%MyTrack");
|
||||
track5.get().modify()->setName("Foo%MyTrack");
|
||||
track6.get().modify()->setName("M_Track");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
|
||||
EXPECT_EQ(tracks.results.size(), 6);
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) };
|
||||
EXPECT_EQ(tracks.results.size(), 5);
|
||||
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||
EXPECT_EQ(tracks.results[1], track3.getId());
|
||||
}
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track4.getId());
|
||||
EXPECT_EQ(tracks.results[1], track5.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_date)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getYear(), std::nullopt);
|
||||
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
|
||||
track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getYear(), 1995);
|
||||
EXPECT_EQ(track->getOriginalYear(), 1994);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_writtenAfter)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "database/TrackBookmark.hpp"
|
||||
|
||||
using ScopedTrackBookmark = ScopedEntity<Database::TrackBookmark>;
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, TrackBookmark)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()};
|
||||
|
||||
{
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
bookmark.get().modify()->setComment("MyComment");
|
||||
bookmark.get().modify()->setOffset(std::chrono::milliseconds {5});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
EXPECT_EQ(TrackBookmark::getCount(session), 1);
|
||||
|
||||
const auto bookmarks {TrackBookmark::find(session, user.getId())};
|
||||
ASSERT_EQ(bookmarks.results.size(), 1);
|
||||
EXPECT_EQ(bookmarks.results.front(), bookmark.getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto userBookmark {TrackBookmark::find(session, user.getId(), track.getId())};
|
||||
ASSERT_TRUE(userBookmark);
|
||||
EXPECT_EQ(userBookmark, bookmark.get());
|
||||
|
||||
EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5});
|
||||
EXPECT_EQ(userBookmark->getComment(), "MyComment");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "database/TrackFeatures.hpp"
|
||||
|
||||
using ScopedTrackFeatures = ScopedEntity<Database::TrackFeatures>;
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, TrackFeatures)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
{
|
||||
auto transaction {session.createReadTransaction()};
|
||||
EXPECT_EQ(TrackFeatures::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackFeatures trackFeatures {session, track.lockAndGet(), ""};
|
||||
|
||||
{
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
EXPECT_EQ(TrackFeatures::getCount(session), 1);
|
||||
|
||||
auto allTrackFeatures {TrackFeatures::find(session)};
|
||||
ASSERT_EQ(allTrackFeatures.results.size(), 1);
|
||||
EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <list>
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackList)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(TrackList::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||
{
|
||||
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" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId()));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
session.create<TrackListEntry>(track.get(), trackList1.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId()));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackList_SortMethod)
|
||||
{
|
||||
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" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name)) };
|
||||
ASSERT_EQ(trackLists.results.size(), 2);
|
||||
EXPECT_EQ(trackLists.results[0], trackList1.getId());
|
||||
EXPECT_EQ(trackLists.results[1], trackList2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} });
|
||||
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) };
|
||||
ASSERT_EQ(trackLists.results.size(), 2);
|
||||
EXPECT_EQ(trackLists.results[0], trackList2.getId());
|
||||
EXPECT_EQ(trackLists.results[1], trackList1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} });
|
||||
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) };
|
||||
ASSERT_EQ(trackLists.results.size(), 2);
|
||||
EXPECT_EQ(trackLists.results[0], trackList1.getId());
|
||||
EXPECT_EQ(trackLists.results[1], trackList2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
std::list<ScopedTrack> tracks;
|
||||
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_EQ(trackList->getCount(), tracks.size());
|
||||
const auto trackIds{ trackList->getTrackIds() };
|
||||
ASSERT_EQ(trackIds.size(), tracks.size());
|
||||
|
||||
// Same order
|
||||
std::size_t i{};
|
||||
for (const ScopedTrack& track : tracks)
|
||||
EXPECT_EQ(track.getId(), trackIds[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
EXPECT_EQ(trackLists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
session.create<TrackListEntry>(track.get(), trackList1.get());
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(trackLists.results.size(), 1);
|
||||
EXPECT_EQ(trackLists.results.front(), trackList1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackList_getEntries)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto entries{ trackList.get()->getEntries() };
|
||||
ASSERT_EQ(entries.size(), 1);
|
||||
EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(track2.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto entries{ trackList.get()->getEntries() };
|
||||
ASSERT_EQ(entries.size(), 2);
|
||||
EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId());
|
||||
EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto entries{ trackList.get()->getEntries(Range {1, 1}) };
|
||||
ASSERT_EQ(entries.size(), 1);
|
||||
EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user