Revert "Migrated database stuff"

This reverts commit c41cc77e21.
This commit is contained in:
emeric
2021-10-17 21:08:48 +02:00
parent c41cc77e21
commit 1faea94a24
143 changed files with 387 additions and 358 deletions
+40
View File
@@ -0,0 +1,40 @@
add_library(lmsdatabase SHARED
impl/Artist.cpp
impl/Cluster.cpp
impl/Db.cpp
impl/TrackArtistLink.cpp
impl/TrackFeatures.cpp
impl/TrackList.cpp
impl/Release.cpp
impl/ScanSettings.cpp
impl/Session.cpp
impl/SqlQuery.cpp
impl/Track.cpp
impl/TrackBookmark.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()
+620
View File
@@ -0,0 +1,620 @@
/*
* 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/Logger.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "Traits.hpp"
namespace Database
{
Artist::Artist(const std::string& name, const std::optional<UUID>& MBID)
: _name {std::string(name, 0 , _maxNameLength)},
_sortName {_name},
_MBID {MBID ? MBID->getAsString() : ""}
{
}
std::vector<Artist::pointer>
Artist::getByName(Session& session, const std::string& name)
{
session.checkSharedLocked();
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::getByMBID(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
}
Artist::pointer
Artist::getById(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
bool
Artist::exists(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
}
Artist::pointer
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
session.checkUniqueLocked();
Artist::pointer res {session.getDboSession().add(std::make_unique<Artist>(name, MBID))};
session.getDboSession().flush();
return res;
}
template <typename T>
static
Wt::Dbo::Query<T>
createQuery(Session& session,
const std::string& queryStr,
const std::vector<ClusterId>& clusterIds,
const std::vector<std::string_view>& keywords,
std::optional<TrackArtistLinkType> linkType)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<T>(queryStr)};
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 (linkType)
query.where("t_a_l.type = ?").bind(*linkType);
if (!keywords.empty())
{
std::vector<std::string> clauses;
std::vector<std::string> sortClauses;
for (std::string_view keyword : keywords)
{
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + escapeLikeKeyword(keyword) + "%");
}
for (std::string_view keyword : keywords)
{
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + escapeLikeKeyword(keyword) + "%");
}
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
}
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 (const ClusterId clusterId : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
query.where(oss.str());
}
return query;
}
std::vector<Artist::pointer>
Artist::getAll(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>();
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getAll(Session& session, SortMethod sortMethod)
{
session.checkSharedLocked();
auto query {session.getDboSession().find<Artist>()};
switch (sortMethod)
{
case Artist::SortMethod::None:
break;
case Artist::SortMethod::ByName:
query.orderBy("name COLLATE NOCASE");
break;
case Artist::SortMethod::BySortName:
query.orderBy("sort_name COLLATE NOCASE");
break;
}
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = query;
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)};
switch (sortMethod)
{
case Artist::SortMethod::None:
break;
case Artist::SortMethod::ByName:
query.orderBy("a.name COLLATE NOCASE");
break;
case Artist::SortMethod::BySortName:
query.orderBy("a.sort_name COLLATE NOCASE");
break;
}
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
std::vector<Artist::pointer> res (collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<ArtistId>
Artist::getAllIds(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<ArtistId> res = session.getDboSession().query<ArtistId>("SELECT id FROM artist");
return std::vector<ArtistId>(res.begin(), res.end());
}
std::vector<ArtistId>
Artist::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size)
{
session.checkSharedLocked();
auto query {createQuery<ArtistId>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
Wt::Dbo::collection<ArtistId> res = query
.orderBy("RANDOM()")
.limit(size ? static_cast<int>(*size) : -1);
return std::vector<ArtistId>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getAllOrphans(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res {session.getDboSession().query<Wt::Dbo::ptr<Artist>>("SELECT DISTINCT a 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 std::vector<pointer>(res.begin(), res.end());
}
std::vector<ArtistId>
Artist::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
Wt::Dbo::collection<ArtistId> res = session.getDboSession().query<ArtistId>
("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 track_cluster t_c ON t_c.track_id = t.id")
.limit(limit ? static_cast<int>(*limit) : -1);
return std::vector<ArtistId>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getByClusters(Session& session, const std::vector<ClusterId>& clusters, SortMethod sortMethod)
{
assert(!clusters.empty());
session.checkSharedLocked();
bool more{};
return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more);
}
std::vector<Artist::pointer>
Artist::getByFilter(Session& session,
const std::vector<ClusterId>& clusters,
const std::vector<std::string_view>& keywords,
std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod,
std::optional<Range> range,
bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)};
switch (sortMethod)
{
case Artist::SortMethod::None:
break;
case Artist::SortMethod::ByName:
query.orderBy("a.name COLLATE NOCASE");
break;
case Artist::SortMethod::BySortName:
query.orderBy("a.sort_name COLLATE NOCASE");
break;
}
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
std::vector<pointer> res (collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Artist::pointer>
Artist::getLastWritten(Session& session,
std::optional<Wt::WDateTime> after,
const std::vector<ClusterId>& clusters,
std::optional<TrackArtistLinkType> linkType,
std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
if (after)
query.where("t.file_last_write > ?").bind(*after);
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
.orderBy("t.file_last_write DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
std::vector<pointer> res (collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getStarred(Session& session,
User::pointer user,
const std::vector<ClusterId>& clusters,
std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod,
std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN user_artist_starred uas ON uas.artist_id = a.id"
" INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)";
query.bind(user->getId());
query.where(oss.str());
}
switch (sortMethod)
{
case Artist::SortMethod::None:
break;
case Artist::SortMethod::ByName:
query.orderBy("name COLLATE NOCASE");
break;
case Artist::SortMethod::BySortName:
query.orderBy("sort_name COLLATE NOCASE");
break;
}
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
.groupBy("a.id")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
std::vector<pointer> res (collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
Artist::getReleases(const std::vector<ClusterId>& clusterIds) const
{
assert(session());
WhereClause where;
std::ostringstream oss;
oss << "SELECT DISTINCT r 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";
if (!clusterIds.empty())
{
oss << " 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 = ?")).bind(id.toString());
where.And(clusterClause);
}
where.And(WhereClause("a.id = ?")).bind(getId().toString());
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
oss << " ORDER BY t.date DESC, r.name COLLATE NOCASE";
auto query {session()->query<Wt::Dbo::ptr<Release>>(oss.str())};
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
auto res {query.resultList()};
return std::vector<Release::pointer>(res.begin(), res.end());
}
std::size_t
Artist::getReleaseCount() const
{
assert(session());
int res = session()->query<int>("SELECT COUNT(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 = ?").bind(getId());
return res;
}
std::vector<Track::pointer>
Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
{
assert(session());
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT DISTINCT t FROM track t 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 = ?").bind(getId())
.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number")};
if (linkType)
query.where("t_a_l.type = ?").bind(*linkType);
auto tracks {query.resultList()};
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
}
std::vector<Track::pointer>
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t 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 = ?").bind(getId())
.where("t.release_id is NULL")
.orderBy("t.name")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)};
if (linkType)
query.where("t_a_l.type = ?").bind(*linkType);
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {query.resultList()};
std::vector<Track::pointer> res(tracks.begin(), tracks.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
bool
Artist::hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType) const
{
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t 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 = ?").bind(getId())
.where("t.release_id is NULL")
.orderBy("t.name")};
if (linkType)
query.where("t_a_l.type = ?").bind(*linkType);
return !query.resultList().empty();
}
std::vector<Track::pointer>
Artist::getRandomTracks(std::optional<std::size_t> count) const
{
assert(session());
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {session()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t 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 = ?").bind(getId())
.orderBy("RANDOM()")
.limit(count ? static_cast<int>(*count) : -1)};
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
}
std::vector<Artist::pointer>
Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
{
assert(session());
std::ostringstream oss;
oss <<
"SELECT a FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE "
" t_c.cluster_id IN (SELECT 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 << ")";
}
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>> query {session()->query<Wt::Dbo::ptr<Artist>>(oss.str())
.bind(getId())
.bind(getId())
.groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()")
.limit(range ? static_cast<int>(range->limit) : -1)
.offset(range ? static_cast<int>(range->offset) : -1)};
for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type);
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res {query.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<std::vector<Cluster::pointer>>
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, 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 (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().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 (const 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
+208
View File
@@ -0,0 +1,208 @@
/*
* 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 "SqlQuery.hpp"
#include "Traits.hpp"
namespace Database {
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)
{
session.checkUniqueLocked();
Cluster::pointer res {session.getDboSession().add(std::make_unique<Cluster>(type, name))};
session.getDboSession().flush();
return res;
}
std::vector<Cluster::pointer>
Cluster::getAll(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> res {session.getDboSession().find<Cluster>()};
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
std::vector<Cluster::pointer>
Cluster::getAllOrphans(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<Wt::Dbo::ptr<Cluster>>("SELECT DISTINCT c FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)").resultList()};
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
Cluster::pointer
Cluster::getById(Session& session, ClusterId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
}
void
Cluster::addTrack(ObjectPtr<Track> track)
{
_tracks.insert(getDboPtr(track));
}
std::vector<Track::pointer>
Cluster::getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> limit) const
{
assert(session());
auto res {session()->query<Wt::Dbo::ptr<Track>>("SELECT t 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())
.offset(offset ? static_cast<int>(*offset) : -1)
.limit(limit ? static_cast<int>(*limit) : -1)
.resultList()};
return std::vector<Track::pointer>(res.begin(), res.end());
}
std::vector<TrackId>
Cluster::getTrackIds() const
{
assert(session());
Wt::Dbo::collection<TrackId> res = session()->query<TrackId>("SELECT t_c.track_id FROM track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id")
.where("c.id = ?").bind(getId());
return std::vector<TrackId>(res.begin(), res.end());
}
std::size_t
Cluster::getReleasesCount() const
{
assert(session());
return session()->query<int>("SELECT COUNT(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")
.where("c.id = ?").bind(getId());
}
ClusterType::ClusterType(std::string_view name)
: _name {name}
{
}
std::vector<ClusterType::pointer>
ClusterType::getAllOrphans(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
"SELECT c_t from cluster_type c_t"
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
.where("c.id IS NULL");
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<ClusterType::pointer>
ClusterType::getAllUsed(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
"SELECT DISTINCT c_t from cluster_type c_t")
.join("cluster c ON c_t.id = c.cluster_type_id");
return std::vector<pointer>(res.begin(), res.end());
}
ClusterType::pointer
ClusterType::getByName(Session& session, const std::string& name)
{
session.checkSharedLocked();
return session.getDboSession().find<ClusterType>().where("name = ?").bind(name).resultValue();
}
ClusterType::pointer
ClusterType::getById(Session& session, ClusterTypeId id)
{
session.checkSharedLocked();
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
}
std::vector<ClusterType::pointer>
ClusterType::getAll(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<ClusterType>().resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
ClusterType::pointer
ClusterType::create(Session& session, const std::string& name)
{
session.checkUniqueLocked();
ClusterType::pointer res {session.getDboSession().add(std::make_unique<ClusterType>(name))};
session.getDboSession().flush();
return res;
}
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
+98
View File
@@ -0,0 +1,98 @@
/*
* 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/Logger.hpp"
namespace Database {
// 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();
std::unique_ptr<Wt::Dbo::backend::Sqlite3> connection {std::make_unique<Wt::Dbo::backend::Sqlite3>(dbPath.string())};
// connection->setProperty("show-queries", "true");
connection->executeSql("pragma journal_mode=WAL");
connection->executeSql("pragma synchronous=normal");
auto connectionPool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount);
connectionPool->setTimeout(std::chrono::seconds(10));
_connectionPool = std::move(connectionPool);
}
Db::~Db()
{
LMS_LOG(DB, DEBUG) << "Optimizing db...";
executeSql("pragma optimize");
LMS_LOG(DB, DEBUG) << "Optimizing db DONE";
}
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
+625
View File
@@ -0,0 +1,625 @@
/*
* 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/Logger.hpp"
#include "SqlQuery.hpp"
#include "Traits.hpp"
#include "Utils.hpp"
namespace Database
{
template <typename T>
static
Wt::Dbo::Query<T>
createQuery(Session& session,
const std::string& queryStr,
const std::vector<ClusterId>& clusterIds,
const std::vector<std::string_view>& keywords)
{
auto query {session.getDboSession().query<T>(queryStr)};
query.join("track t ON t.release_id = r.id");
for (std::string_view keyword : keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
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 (const ClusterId clusterId : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
query.where(oss.str());
}
return query;
}
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
: _name {std::string(name, 0 , _maxNameLength)},
_MBID {MBID ? MBID->getAsString() : ""}
{
}
std::vector<Release::pointer>
Release::getByName(Session& session, const std::string& name)
{
session.checkUniqueLocked();
auto res {session.getDboSession()
.find<Release>()
.where("name = ?").bind( std::string(name, 0, _maxNameLength) )
.resultList()};
return std::vector<Release::pointer>(res.begin(), res.end());
}
Release::pointer
Release::getByMBID(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
return session.getDboSession()
.find<Release>()
.where("mbid = ?").bind(std::string {mbid.getAsString()})
.resultValue();;
}
Release::pointer
Release::getById(Session& session, ReleaseId id)
{
session.checkSharedLocked();
return session.getDboSession()
.find<Release>()
.where("id = ?").bind(id)
.resultValue();
}
bool
Release::exists(Session& session, ReleaseId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
}
Release::pointer
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
session.checkSharedLocked();
Release::pointer res {session.getDboSession().add(std::make_unique<Release>(name, MBID))};
session.getDboSession().flush();
return res;
}
std::size_t
Release::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<Release>().resultList().size();
}
std::vector<Release::pointer>
Release::getAll(Session& session, std::optional<Range> range)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<Release>()
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) : -1)
.orderBy("name COLLATE NOCASE")
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<ReleaseId>
Release::getAllIds(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<ReleaseId> res = session.getDboSession().query<ReleaseId>("SELECT id FROM release");
return std::vector<ReleaseId>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>(
"SELECT DISTINCT r 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")
.offset(offset ? static_cast<int>(*offset) : -1)
.limit(size ? static_cast<int>(*size) : -1)
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT DISTINCT r from release r", clusterIds, {})};
auto res {query
.orderBy("RANDOM()")
.limit(size ? static_cast<int>(*size) : -1)
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<ReleaseId>
Release::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
{
session.checkSharedLocked();
auto query {createQuery<ReleaseId>(session, "SELECT DISTINCT r.id from release r", clusterIds, {})};
Wt::Dbo::collection<ReleaseId> res = query
.orderBy("RANDOM()")
.limit(size ? static_cast<int>(*size) : -1);
return std::vector<ReleaseId>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getAllOrphans(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL").resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getLastWritten(Session& session,
std::optional<Wt::WDateTime> after,
const std::vector<ClusterId>& clusterIds,
std::optional<Range> range,
bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, {})};
if (after)
query.where("t.file_last_write > ?").bind(after);
auto collection {query
.orderBy("t.file_last_write DESC")
.groupBy("r.id")
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) + 1: -1)
.resultList()};
std::vector<pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer>
Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range)
{
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>
("SELECT DISTINCT r from release r INNER JOIN track t ON r.id = t.release_id")
.where("t.date >= ?").bind(Wt::WDate {yearFrom, 1, 1})
.where("t.date <= ?").bind(Wt::WDate {yearTo, 12, 31})
.orderBy("t.date, r.name COLLATE NOCASE")
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) : -1)
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getStarred(Session& session,
User::pointer user,
const std::vector<ClusterId>& clusterIds,
std::optional<Range> range,
bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, {})};
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN user_release_starred urs ON urs.release_id = r.id"
" INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)";
query.bind(user->getId());
query.where(oss.str());
}
auto collection {query
.groupBy("r.id")
.orderBy("r.name COLLATE NOCASE")
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) + 1: -1)
.resultList()};
std::vector<pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer>
Release::getByClusters(Session& session, const std::vector<ClusterId>& clusters)
{
assert(!clusters.empty());
session.checkSharedLocked();
bool moreResults;
return getByFilter(session, clusters, {}, std::nullopt, moreResults);
}
std::vector<Release::pointer>
Release::getByFilter(Session& session,
const std::vector<ClusterId>& clusterIds,
const std::vector<std::string_view>& keywords,
std::optional<Range> range,
bool& moreResults)
{
session.checkSharedLocked();
auto collection {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, keywords)
.groupBy("r.id")
.orderBy("r.name COLLATE NOCASE")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<ReleaseId>
Release::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
Wt::Dbo::collection<ReleaseId> res = session.getDboSession().query<ReleaseId>
("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")
.limit(limit ? static_cast<int>(*limit) : -1);
return std::vector<ReleaseId>(res.begin(), res.end());
}
std::optional<std::size_t>
Release::getTotalTrack(void) const
{
assert(session());
int res = session()->query<int>("SELECT COALESCE(MAX(total_track),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.bind(getId());
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
}
std::optional<std::size_t>
Release::getTotalDisc(void) const
{
assert(session());
int res = session()->query<int>("SELECT COALESCE(MAX(total_disc),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.bind(getId());
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
}
std::optional<int>
Release::getReleaseYear(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 => no date
if (dates.empty() || dates.size() > 1)
return std::nullopt;
auto date {dates.front().year()};
if (date > 0)
return date;
return std::nullopt;
}
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 = ?")
.groupBy("copyright_url")
.bind(getId());
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::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());
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 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::vector<Track::pointer>
Release::getTracks(const std::vector<ClusterId>& clusterIds) const
{
assert(session());
WhereClause where;
std::ostringstream oss;
oss << "SELECT t FROM track t INNER JOIN release r ON t.release_id = r.id";
if (!clusterIds.empty())
{
oss << " 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 = ?")).bind(id.toString());
where.And(clusterClause);
}
where.And(WhereClause("r.id = ?")).bind(getId().toString());
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
oss << " ORDER BY t.disc_number,t.track_number";
auto query {session()->query<Wt::Dbo::ptr<Track>>(oss.str())};
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
auto res {query.resultList()};
return std::vector<Track::pointer> (res.begin(), res.end());
}
std::size_t
Release::getTracksCount() const
{
return _tracks.size();
}
Track::pointer
Release::getFirstTrack() const
{
assert(session());
return session()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t")
.join("release r ON t.release_id = r.id")
.where("r.id = ?").bind(getId())
.orderBy("t.disc_number,t.track_number")
.limit(1)
.resultValue();
}
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<ClusterType::pointer>& clusterTypes, 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 (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().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
+147
View File
@@ -0,0 +1,147 @@
/*
* 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/Logger.hpp"
#include "utils/String.hpp"
#include "database/Cluster.hpp"
#include "database/Session.hpp"
namespace {
const std::set<std::string> defaultClusterTypeNames =
{
"GENRE",
"ALBUMGROUPING",
"MOOD",
"ALBUMMOOD",
};
}
namespace Database {
void
ScanSettings::init(Session& session)
{
session.checkUniqueLocked();
pointer settings {get(session)};
if (settings)
return;
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
settings.modify()->setClusterTypes(session, defaultClusterTypeNames );
}
ScanSettings::pointer
ScanSettings::get(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<ScanSettings>().resultValue();
}
std::vector<std::filesystem::path>
ScanSettings::getAudioFileExtensions() const
{
const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")};
return std::vector<std::filesystem::path>(std::cbegin(extensions), std::cend(extensions));
}
void
ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
{
_audioFileExtensions += " " + ext.string();
}
std::vector<ClusterType::pointer>
ScanSettings::getClusterTypes() const
{
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
}
void
ScanSettings::setMediaDirectory(const std::filesystem::path& p)
{
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
}
template <typename It>
std::set<std::string> getNames(It begin, It end)
{
std::set<std::string> names;
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
[](const ClusterType::pointer& clusterType)
{
return clusterType->getName();
});
return names;
}
void
ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames)
{
session.checkUniqueLocked();
bool needRescan {};
// Create any missing cluster type
for (const std::string& clusterTypeName : clusterTypeNames)
{
ClusterType::pointer clusterType {ClusterType::getByName(session, clusterTypeName)};
if (!clusterType)
{
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
clusterType = ClusterType::create(session, clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType));
needRescan = true;
}
}
// Delete no longer existing cluster types
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
{
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
{
LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName();
clusterType.remove();
}
}
if (needRescan)
_scanVersion += 1;
}
void
ScanSettings::incScanVersion()
{
_scanVersion += 1;
}
} // namespace Database
+508
View File
@@ -0,0 +1,508 @@
/*
* 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 <map>
#include <mutex>
#include <thread>
#include <string_view>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/ScanSettings.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackList.hpp"
#include "database/TrackFeatures.hpp"
#include "database/User.hpp"
namespace Database
{
using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {31};
class VersionInfo
{
public:
using pointer = Wt::Dbo::ptr<VersionInfo>;
static VersionInfo::pointer getOrCreate(Session& session)
{
session.checkUniqueLocked();
pointer versionInfo {session.getDboSession().find<VersionInfo>()};
if (!versionInfo)
return session.getDboSession().add(std::make_unique<VersionInfo>());
return versionInfo;
}
static VersionInfo::pointer get(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<VersionInfo>();
}
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};
};
void
Session::doDatabaseMigrationIfNeeded()
{
static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"};
Db::ScopedNoForeignKeys noPragmaKeys {_db};
while (1)
{
auto uniqueTransaction {createUniqueTransaction()};
Version version;
try
{
version = VersionInfo::getOrCreate(*this)->getVersion();
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
if (version == LMS_DATABASE_VERSION)
{
LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!";
return;
}
}
catch (std::exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
throw LmsException {outdatedMsg};
}
LMS_LOG(DB, INFO) << "Migrating database from version " << version << "...";
if (version == 5)
{
_session.execute("DELETE FROM auth_token"); // format has changed
}
else if (version == 6)
{
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 7)
{
_session.execute("DROP TABLE similarity_settings");
_session.execute("DROP TABLE similarity_settings_feature");
_session.execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
}
else if (version == 8)
{
// Better cover handling, need to rescan the whole files
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 9)
{
_session.execute(R"(
CREATE TABLE IF NOT EXISTS "track_bookmark" (
"id" integer primary key autoincrement,
"version" integer not null,
"offset" integer,
"comment" text not null,
"track_id" bigint,
"user_id" bigint,
constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
);)");
}
else if (version == 10)
{
ScanSettings::get(*this).modify()->addAudioFileExtension(".m4b");
ScanSettings::get(*this).modify()->addAudioFileExtension(".alac");
}
else if (version == 11)
{
// Sanitize bad MBID, need to rescan the whole files
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 12)
{
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 13)
{
// Always store UUID in lower case + better WMA parsing
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 14)
{
// SortName now set from metadata
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 15)
{
_session.execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
}
else if (version == 16)
{
_session.execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
_session.execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 17)
{
// Drop colums total_disc/total_track from release
_session.execute(R"(
CREATE TABLE "release_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null,
"mbid" text not null
))");
_session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release");
_session.execute("DROP TABLE release");
_session.execute("ALTER TABLE release_backup RENAME TO release");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 18)
{
_session.execute(R"(
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
"id" integer primary key autoincrement,
"version" integer not null,
"api_enabled" boolean not null,
"artist_list_mode" integer not null
))");
}
else if (version == 19)
{
_session.execute(R"(
CREATE TABLE "user_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"type" integer not null,
"login_name" text not null,
"password_salt" text not null,
"password_hash" text not null,
"last_login" text,
"subsonic_transcode_enable" boolean not null,
"subsonic_transcode_format" integer not null,
"subsonic_transcode_bitrate" integer not null,
"subsonic_artist_list_mode" integer not null,
"ui_theme" integer not null,
"cur_playing_track_pos" integer not null,
"repeat_all" boolean not null,
"radio" boolean not null
))");
_session.execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "}
+ (User::defaultSubsonicTranscodeEnable ? "1" : "0")
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
+ ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
_session.execute("DROP TABLE user");
_session.execute("ALTER TABLE user_backup RENAME TO user");
}
else if (version == 20)
{
_session.execute("DROP TABLE subsonic_settings");
}
else if (version == 21)
{
_session.execute("ALTER TABLE track ADD track_replay_gain REAL");
_session.execute("ALTER TABLE track ADD release_replay_gain REAL");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 22)
{
_session.execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 23)
{
// Better cover detection
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 24)
{
// User's AuthMode
_session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
}
else if (version == 25)
{
// Better cover detection
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 26)
{
// Composer, mixer, etc. support
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 27)
{
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 28)
{
// Drop Auth mode
_session.execute(R"(
CREATE TABLE "user_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"type" integer not null,
"login_name" text not null,
"password_salt" text not null,
"password_hash" text not null,
"last_login" text,
"subsonic_transcode_enable" boolean not null,
"subsonic_transcode_format" integer not null,
"subsonic_transcode_bitrate" integer not null,
"subsonic_artist_list_mode" integer not null,
"ui_theme" integer not null,
"cur_playing_track_pos" integer not null,
"repeat_all" boolean not null,
"radio" boolean not null
))");
_session.execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
_session.execute("DROP TABLE user");
_session.execute("ALTER TABLE user_backup RENAME TO user");
}
else if (version == 29)
{
_session.execute("ALTER TABLE tracklist_entry ADD date_time TEXT");
_session.execute("ALTER TABLE user ADD listenbrainz_token TEXT");
_session.execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultScrobbler)) + ")");
_session.execute("ALTER TABLE track ADD recording_mbid TEXT");
_session.execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__");
// MBID changes
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 30)
{
// drop "year" and "original_year" (rescan needed to convert them into dates)
_session.execute(R"(
CREATE TABLE "track_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"scan_version" integer not null,
"track_number" integer not null,
"disc_number" integer not null,
"name" text not null,
"duration" integer,
"date" integer text,
"original_date" integer text,
"file_path" text not null,
"file_last_write" text,
"file_added" text,
"has_cover" boolean not null,
"mbid" text not null,
"copyright" text not null,
"copyright_url" text not null,
"release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT,
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))");
_session.execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track");
_session.execute("DROP TABLE track");
_session.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(*this).modify()->incScanVersion();
}
else
{
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
throw LmsException { LMS_DATABASE_VERSION > version ? outdatedMsg : "Server binary outdated, please upgrade it to handle this database"};
}
VersionInfo::get(*this).modify()->setVersion(++version);
}
}
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<Release>("release");
_session.mapClass<ScanSettings>("scan_settings");
_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");
}
enum class OwnedLock
{
None,
Shared,
Unique,
};
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
: _lock {mutex},
_transaction {session}
{
}
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
: _lock {mutex},
_transaction {session}
{
}
void
Session::checkUniqueLocked()
{
// assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique);
}
void
Session::checkSharedLocked()
{
// assert(lockDebug[&_db.getMutex()] != OwnedLock::None);
}
UniqueTransaction
Session::createUniqueTransaction()
{
return UniqueTransaction {_db.getMutex(), _session};
}
SharedTransaction
Session::createSharedTransaction()
{
return SharedTransaction {_db.getMutex(), _session};
}
void
Session::prepareTables()
{
// Creation case
try {
_session.createTables();
LMS_LOG(DB, INFO) << "Tables created";
}
catch (Wt::Dbo::Exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
}
doDatabaseMigrationIfNeeded();
// Indexes
{
auto uniqueTransaction {createUniqueTransaction()};
_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_file_last_write_idx ON track(file_last_write)");
_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_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_name_idx ON track_artist_link(name)");
_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_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)");
}
// Initial settings tables
{
auto uniqueTransaction {createUniqueTransaction()};
ScanSettings::init(*this);
}
}
void
Session::optimize()
{
LMS_LOG(DB, DEBUG) << "Optimizing db...";
{
auto uniqueTransaction {createUniqueTransaction()};
_session.execute("ANALYZE");
}
LMS_LOG(DB, DEBUG) << "Optimized db!";
}
} // namespace Database
+199
View File
@@ -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(void) const
{
if (!_clause.empty())
return "WHERE " + _clause;
else
return "";
}
WhereClause&
WhereClause::bind(const std::string& bindArg)
{
assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?')));
_bindArgs.push_back(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);
_statement.sort();
_statement.unique();
return *this;
}
std::string
SelectStatement::get() const
{
std::string res = "SELECT ";
for (std::list<std::string>::const_iterator 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);
}
_clause.sort();
_clause.unique();
return *this;
}
std::string
FromClause::get() const
{
std::ostringstream oss;
if (!_clause.empty())
{
oss << "FROM ";
for (std::list<std::string>::const_iterator it = _clause.begin(); it != _clause.end(); ++it) {
if (it != _clause.begin())
oss << ",";
oss << *it;
}
}
return oss.str();
}
std::string
SqlQuery::get(void) 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();
}
+135
View File
@@ -0,0 +1,135 @@
/*
* 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 <list>
#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(const std::string& arg);
std::string get() const;
const std::list<std::string>& getBindArgs(void) const {return _bindArgs;}
private:
std::string _clause; // WHERE clause
std::list<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::list<std::string> _statement;
};
class FromClause
{
public:
FromClause() {}
FromClause(const std::string& clause);
FromClause& And(const FromClause& clause);
std::string get() const;
private:
std::list<std::string> _clause;
};
class SqlQuery
{
public:
SelectStatement& select(void) { return _selectStatement;}
SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; }
FromClause& from(void) { return _fromClause; }
FromClause& from(const std::string& clause) { _whereClause = WhereClause(clause); return _fromClause; }
InnerJoinClause& innerJoin(void) { return _innerJoinClause; }
WhereClause& where(void) { return _whereClause; }
const WhereClause& where(void) const { return _whereClause; }
GroupByStatement& groupBy(void) { return _groupByStatement; }
const GroupByStatement& groupBy(void) const { return _groupByStatement; }
std::string get(void) 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,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});
}
};
}
+645
View File
@@ -0,0 +1,645 @@
/*
* 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/Logger.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Traits.hpp"
#include "Utils.hpp"
namespace Database {
template <typename T>
static
Wt::Dbo::Query<T>
createQuery(Session& session,
const std::string& queryStr,
const std::vector<ClusterId>& clusterIds,
const std::vector<std::string_view>& keywords)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<T>(queryStr)};
for (std::string_view keyword : keywords)
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
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 (const ClusterId clusterId : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
query.where(oss.str());
}
return query;
}
Track::Track(const std::filesystem::path& p)
: _filePath {p.string()}
{
}
std::size_t
Track::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
}
std::vector<Track::pointer>
Track::getAll(Session& session, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<Track>()
.limit(limit ? static_cast<int>(*limit) : -1)
.resultList()};
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
}
std::vector<Track::pointer>
Track::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
auto collection {query
.orderBy("RANDOM()")
.limit(limit ? static_cast<int>(*limit) + 1: -1)
.resultList()};
return std::vector<pointer>(collection.begin(), collection.end());
}
std::vector<TrackId>
Track::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
auto query {createQuery<TrackId>(session, "SELECT t.id from track t", clusterIds, {})};
Wt::Dbo::collection<TrackId> collection = query
.orderBy("RANDOM()")
.limit(limit ? static_cast<int>(*limit) + 1: -1);
return std::vector<TrackId>(collection.begin(), collection.end());
}
std::vector<TrackId>
Track::getAllIds(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>("SELECT id FROM track");
return std::vector<TrackId>(res.begin(), res.end());
}
Track::pointer
Track::getByPath(Session& session, const std::filesystem::path& p)
{
session.checkSharedLocked();
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
}
Track::pointer
Track::getById(Session& session, TrackId id)
{
session.checkSharedLocked();
return session.getDboSession().find<Track>()
.where("id = ?").bind(id)
.resultValue();
}
bool
Track::exists(Session& session, TrackId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1;
}
std::vector<Track::pointer>
Track::getByRecordingMBID(Session& session, const UUID& mbid)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<Track>()
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()})
.resultList()};
return std::vector<Track::pointer>(res.begin(), res.end());
}
Track::pointer
Track::create(Session& session, const std::filesystem::path& p)
{
session.checkUniqueLocked();
Track::pointer res {session.getDboSession().add(std::make_unique<Track>(p))};
session.getDboSession().flush();
return res;
}
std::vector<std::pair<TrackId, std::filesystem::path>>
Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
{
using QueryResultType = std::tuple<TrackId, std::string>;
session.checkSharedLocked();
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
.limit(size ? static_cast<int>(*size) + 1 : -1)
.offset(offset ? static_cast<int>(*offset) : -1);
std::vector<std::pair<TrackId, std::filesystem::path>> result;
result.reserve(queryRes.size());
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
[](const QueryResultType& queryResult)
{
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
});
return result;
}
std::vector<Track::pointer>
Track::getMBIDDuplicates(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>( "SELECT track 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")
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Track::pointer>
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
if (after)
query.where("t.file_last_write > ?").bind(after);
auto collection {query
.orderBy("t.file_last_write DESC")
.groupBy("t.id")
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) + 1: -1)
.resultList()};
std::vector<pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Track::pointer>
Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>
("SELECT t 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)")
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<TrackId>
Track::getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>
("SELECT t.id FROM track t")
.where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)")
.limit(limit ? static_cast<int>(*limit) : -1);
return std::vector<TrackId>(res.begin(), res.end());
}
std::vector<TrackId>
Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
{
session.checkSharedLocked();
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>
("SELECT DISTINCT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.limit(limit ? static_cast<int>(*limit) : -1);
return std::vector<TrackId>(res.begin(), res.end());
}
std::vector<Track::pointer>
Track::getStarred(Session& session,
ObjectPtr<User> user,
const std::vector<ClusterId>& clusterIds,
std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
{
std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN user_track_starred uts ON uts.track_id = t.id"
" INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)";
query.bind(user->getId().toString());
query.where(oss.str());
}
auto collection {query
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) + 1: -1)
.resultList()};
std::vector<pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
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());
}
bool
Track::hasTrackFeatures() const
{
return (_trackFeatures.lock() != Wt::Dbo::ptr<Database::TrackFeatures> {});
}
std::vector<Track::pointer>
Track::getByFilter(Session& session,
const std::vector<ClusterId>& clusterIds,
const std::vector<std::string_view>& keywords,
std::optional<Range> range,
bool& moreResults)
{
session.checkSharedLocked();
auto collection {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, keywords)
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<pointer> res(collection.begin(), collection.end());
if (range && (res.size() == static_cast<std::size_t>(range->limit) + 1))
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Track::pointer>
Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>("SELECT t from track t")
.join("release r ON t.release_id = r.id")
.where("t.name = ?").bind(trackName)
.where("r.name = ?").bind(releaseName)
.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Track::pointer>
Track::getSimilarTracks(Session& session,
const std::vector<TrackId>& tracks,
std::optional<std::size_t> offset,
std::optional<std::size_t> size)
{
assert(!tracks.empty());
session.checkSharedLocked();
std::ostringstream oss;
for (std::size_t i {}; i < tracks.size(); ++i)
{
if (!oss.str().empty())
oss << ", ";
oss << "?";
}
auto query {session.getDboSession().query<Wt::Dbo::ptr<Track>>(
"SELECT t FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" AND t_c.cluster_id IN (SELECT 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()")
.limit(size ? static_cast<int>(*size) : -1)
.offset(offset ? static_cast<int>(*offset) : -1)};
for (TrackId trackId : tracks)
query.bind(trackId);
for (TrackId trackId : tracks)
query.bind(trackId);
auto res {query.resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Track::pointer>
Track::getByClusters(Session& session, const std::vector<ClusterId>& clusters)
{
assert(!clusters.empty());
session.checkSharedLocked();
bool moreResults;
return getByFilter(session,
clusters,
{}, // keywords
std::nullopt, // range
moreResults);
}
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));
}
void
Track::setFeatures(const ObjectPtr<TrackFeatures>& features)
{
_trackFeatures = getDboPtr(features);
}
std::optional<std::size_t>
Track::getTrackNumber() const
{
return (_trackNumber > 0) ? std::make_optional<std::size_t>(_trackNumber) : std::nullopt;
}
std::optional<std::size_t>
Track::getTotalTrack() const
{
return (_totalTrack > 0) ? std::make_optional<std::size_t>(_totalTrack) : std::nullopt;
}
std::optional<std::size_t>
Track::getDiscNumber() const
{
return (_discNumber > 0) ? std::make_optional<std::size_t>(_discNumber) : std::nullopt;
}
std::optional<std::size_t>
Track::getTotalDisc() const
{
return (_totalDisc > 0) ? std::make_optional<std::size_t>(_totalDisc) : std::nullopt;
}
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 (TrackArtistLinkType type : linkTypes)
{
(void) type;
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 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 (TrackArtistLinkType type : linkTypes)
{
(void) type;
if (!first)
oss << ", ";
oss << "?";
first = false;
}
oss << ")";
}
Wt::Dbo::Query<ArtistId> query {session()->query<ArtistId>(oss.str())
.where("t.id = ?").bind(getId())};
for (TrackArtistLinkType type : linkTypes)
query.bind(type);
Wt::Dbo::collection<ArtistId> res = query;
return std::vector<ArtistId>(std::begin(res), std::end(res));
}
std::vector<TrackArtistLink::pointer>
Track::getArtistLinks() const
{
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
}
ObjectPtr<TrackFeatures>
Track::getTrackFeatures() const
{
return _trackFeatures.lock();
}
std::vector<std::vector<Cluster::pointer>>
Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, 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 (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().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 (auto cluster_list : clusters)
res.push_back(cluster_list.second);
return res;
}
} // namespace Database
@@ -0,0 +1,59 @@
/*
* 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 "Traits.hpp"
namespace Database {
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
: _type {type},
_track {getDboPtr(track)},
_artist {getDboPtr(artist)}
{
}
TrackArtistLink::pointer
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
{
session.checkUniqueLocked();
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type))};
session.getDboSession().flush();
return res;
}
EnumSet<TrackArtistLinkType>
TrackArtistLink::getUsedTypes(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
}
}
+90
View File
@@ -0,0 +1,90 @@
/*
* 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 "Traits.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)
{
session.checkUniqueLocked();
TrackBookmark::pointer res {session.getDboSession().add(std::make_unique<TrackBookmark>(user, track))};
session.getDboSession().flush();
return res;
}
std::vector<TrackBookmark::pointer>
TrackBookmark::getAll(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<TrackBookmark>().resultList()};
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
}
std::vector<TrackBookmark::pointer>
TrackBookmark::getByUser(Session& session, User::pointer user)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<TrackBookmark>()
.where("user_id = ?").bind(user->getId())
.resultList()};
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
}
TrackBookmark::pointer
TrackBookmark::getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackBookmark>()
.where("user_id = ?").bind(user->getId())
.where("track_id = ?").bind(track->getId())
.resultValue();
}
TrackBookmark::pointer
TrackBookmark::getById(Session& session, TrackBookmarkId id)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackBookmark>()
.where("id = ?").bind(id)
.resultValue();
}
} // namespace Database
+88
View File
@@ -0,0 +1,88 @@
/*
* 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/Logger.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)
{
session.checkUniqueLocked();
return session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures));
}
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
{
try
{
std::istringstream iss {_data};
boost::property_tree::ptree root;
boost::property_tree::read_json(iss, root);
FeatureValuesMap res;
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>());
}
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
return {};
}
}
} // namespace Database
+523
View File
@@ -0,0 +1,523 @@
/*
* 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/Logger.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 "Traits.hpp"
namespace Database {
TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user)
: _name {name},
_type {type},
_isPublic {isPublic},
_user {getDboPtr(user)}
{
}
TrackList::pointer
TrackList::create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user)
{
session.checkUniqueLocked();
assert(user);
TrackList::pointer res {session.getDboSession().add( std::make_unique<TrackList>(name, type, isPublic, user) )};
session.getDboSession().flush();
return res;
}
TrackList::pointer
TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr<User> user)
{
session.checkSharedLocked();
assert(user);
return session.getDboSession().find<TrackList>()
.where("name = ?").bind(name)
.where("type = ?").bind(type)
.where("user_id = ?").bind(user->getId()).resultValue();
}
std::vector<TrackList::pointer>
TrackList::getAll(Session& session)
{
session.checkSharedLocked();
auto res = session.getDboSession().find<TrackList>().resultList();
return std::vector<TrackList::pointer>(res.begin(), res.end());
}
std::vector<TrackList::pointer>
TrackList::getAll(Session& session, ObjectPtr<User> user)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<TrackList>()
.where("user_id = ?").bind(user->getId())
.orderBy("name COLLATE NOCASE")
.resultList()};
return std::vector<TrackList::pointer>(res.begin(), res.end());
}
std::vector<TrackList::pointer>
TrackList::getAll(Session& session, ObjectPtr<User> user, Type type)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<TrackList>()
.where("user_id = ?").bind(user->getId())
.where("type = ?").bind(type)
.orderBy("name COLLATE NOCASE")
.resultList()};
return std::vector<TrackList::pointer>(res.begin(), res.end());
}
TrackList::pointer
TrackList::getById(Session& session, TrackListId id)
{
session.checkSharedLocked();
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(pos, 1);
if (!entries.empty())
res = entries.front();
return res;
}
std::vector<TrackListEntry::pointer>
TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
{
assert(session());
auto entries {
session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(getId())
.orderBy("id")
.limit(size ? static_cast<int>(*size) : -1)
.offset(offset ? static_cast<int>(*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(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
.resultValue();
}
static
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>>
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
{
auto query {session.query<Wt::Dbo::ptr<Artist>>(queryStr)};
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
query.join("tracklist p ON p.id = p_e.tracklist_id");
query.where("p.id = ?").bind(tracklistId);
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;
}
static
Wt::Dbo::Query<Wt::Dbo::ptr<Release>>
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds)
{
auto query {session.query<Wt::Dbo::ptr<Release>>(queryStr)};
query.join("track t ON t.release_id = r.id");
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
query.join("tracklist p ON p.id = p_e.tracklist_id");
query.where("p.id = ?").bind(tracklistId);
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;
}
static
Wt::Dbo::Query<Wt::Dbo::ptr<Track>>
createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds)
{
auto query {session.query<Wt::Dbo::ptr<Track>>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")};
query.where("p.id = ?").bind(tracklistId);
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;
}
std::vector<Artist::pointer>
TrackList::getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer>
TrackList::getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Release::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Track::pointer>
TrackList::getTracksReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createTracksQuery(*session(), getId(), clusterIds)
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Track::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
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());
}
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 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();
}
std::vector<Artist::pointer>
TrackList::getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)};
auto collection {query
.orderBy("COUNT(a.id) DESC")
.groupBy("a.id")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Artist::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer>
TrackList::getTopReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)};
auto collection {query
.orderBy("COUNT(r.id) DESC")
.groupBy("r.id")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Release::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Track::pointer>
TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {createTracksQuery(*session(), getId(), clusterIds)};
auto collection {query
.orderBy("COUNT(t.id) DESC")
.groupBy("t.id")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Track::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution
, _track {getDboPtr(track)}
, _tracklist {getDboPtr(tracklist)}
{
assert(_dateTime.isValid());
}
TrackListEntry::pointer
TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
{
session.checkUniqueLocked();
assert(track);
assert(tracklist);
auto res = session.getDboSession().add(std::make_unique<TrackListEntry>( track, tracklist, dateTime));
session.getDboSession().flush();
return res;
}
TrackListEntry::pointer
TrackListEntry::getById(Session& session, TrackListEntryId id)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue();
}
} // namespace Database
+60
View File
@@ -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;
}
};
}
+225
View File
@@ -0,0 +1,225 @@
/*
* 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 "database/TrackList.hpp"
#include "utils/Logger.hpp"
#include "StringViewTraits.hpp"
#include "Traits.hpp"
namespace Database {
AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
: _value {value}
, _expiry {expiry}
, _user {getDboPtr(user)}
{
}
AuthToken::pointer
AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
{
session.checkUniqueLocked();
AuthToken::pointer res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
session.getDboSession().flush();
return res;
}
void
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
{
session.checkUniqueLocked();
session.getDboSession().execute
("DELETE FROM auth_token WHERE expiry < ?").bind(now);
}
AuthToken::pointer
AuthToken::getByValue(Session& session, const std::string& value)
{
session.checkSharedLocked();
return session.getDboSession().find<AuthToken>()
.where("value = ?").bind(value)
.resultValue();
}
static const std::string queuedListName {"__queued_tracks__"};
User::User(std::string_view loginName)
: _loginName {loginName}
{
}
std::vector<User::pointer>
User::getAll(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().find<User>().resultList()};
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<UserId>
User::getAllIds(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<UserId>("SELECT id FROM user").resultList()};
return std::vector<UserId>(res.begin(), res.end());
}
User::pointer
User::getDemo(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
}
std::size_t
User::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
}
User::pointer
User::create(Session& session, std::string_view loginName)
{
session.checkUniqueLocked();
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName))};
TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user);
session.getDboSession().flush();
return user;
}
User::pointer
User::getById(Session& session, UserId id)
{
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
}
User::pointer
User::getByLoginName(Session& session, std::string_view name)
{
return session.getDboSession().find<User>()
.where("login_name = ?").bind(name)
.resultValue();
}
void
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
{
assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend());
_subsonicTranscodeBitrate = bitrate;
}
void
User::clearAuthTokens()
{
_authTokens.clear();
}
TrackList::pointer
User::getQueuedTrackList(Session& session) const
{
assert(self());
session.checkSharedLocked();
return TrackList::get(session, queuedListName, TrackList::Type::Internal, self());
}
void
User::starArtist(ObjectPtr<Artist> artist)
{
if (_starredArtists.count(getDboPtr(artist)) == 0)
_starredArtists.insert(getDboPtr(artist));
}
void
User::unstarArtist(ObjectPtr<Artist> artist)
{
if (_starredArtists.count(getDboPtr(artist)) != 0)
_starredArtists.erase(getDboPtr(artist));
}
bool
User::hasStarredArtist(ObjectPtr<Artist> artist) const
{
return _starredArtists.count(getDboPtr(artist)) != 0;
}
void
User::starRelease(ObjectPtr<Release> release)
{
if (_starredReleases.count(getDboPtr(release)) == 0)
_starredReleases.insert(getDboPtr(release));
}
void
User::unstarRelease(ObjectPtr<Release> release)
{
if (_starredReleases.count(getDboPtr(release)) != 0)
_starredReleases.erase(getDboPtr(release));
}
bool
User::hasStarredRelease(ObjectPtr<Release> release) const
{
return _starredReleases.count(getDboPtr(release)) != 0;
}
void
User::starTrack(ObjectPtr<Track> track)
{
if (_starredTracks.count(getDboPtr(track)) == 0)
_starredTracks.insert(getDboPtr(track));
}
void
User::unstarTrack(ObjectPtr<Track> track)
{
if (_starredTracks.count(getDboPtr(track)) != 0)
_starredTracks.erase(getDboPtr(track));
}
bool
User::hasStarredTrack(ObjectPtr<Track> track) const
{
return _starredTracks.count(getDboPtr(track)) != 0;
}
} // namespace Database
+33
View File
@@ -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/>.
*/
#include "Utils.hpp"
#include "utils/String.hpp"
namespace Database
{
std::string
escapeLikeKeyword(std::string_view keyword)
{
return StringUtils::escapeString(keyword, "%_", escapeChar);
}
} // namespace Database
+33
View File
@@ -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
#include <string>
#include <string_view>
#include <vector>
namespace Database
{
#define ESCAPE_CHAR_STR "\\"
static constexpr char escapeChar {'\\'};
std::string escapeLikeKeyword(std::string_view keywords);
} // namespace Database
@@ -0,0 +1,146 @@
/*
* 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/Types.hpp"
#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp"
namespace Database
{
class Cluster;
class ClusterType;
class Release;
class Session;
class Track;
class TrackArtistLink;
class User;
class Artist : public Object<Artist, ArtistId>
{
public:
enum class SortMethod
{
None,
ByName,
BySortName,
};
Artist() = default;
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
// Accessors
static pointer getByMBID(Session& session, const UUID& MBID);
static pointer getById(Session& session, ArtistId id);
static bool exists(Session& session, ArtistId id);
static std::vector<pointer> getByName(Session& session, const std::string& name); // exact match on name field
static std::vector<pointer> getByClusters(Session& session,
const std::vector<ClusterId>& clusters, // at least one track that belongs to these clusters
SortMethod sortMethod
);
static std::vector<pointer> getByFilter(Session& session,
const std::vector<ClusterId>& clusters, // if non empty, at least one artist that belongs to these clusters
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
SortMethod sortMethod,
std::optional<Range> range,
bool& moreExpected);
static std::vector<pointer> getAll(Session& session);
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
static std::vector<ArtistId> getAllIds(Session& session);
static std::vector<ArtistId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size = {});
static std::vector<pointer> getAllOrphans(Session& session); // No track related
static std::vector<pointer> getLastWritten(Session& session,
std::optional<Wt::WDateTime> after,
const std::vector<ClusterId>& clusters,
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
std::optional<Range>,
bool& moreResults);
static std::vector<ArtistId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session,
ObjectPtr<User> user,
const std::vector<ClusterId>& clusters,
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
SortMethod sortMethod,
std::optional<Range>, bool& moreResults);
// Accessors
const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
std::size_t getReleaseCount() const;
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
std::vector<ObjectPtr<Track>> getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
// No artistLinkTypes means get them all
std::vector<pointer> getSimilarArtists(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<ObjectPtr<ClusterType>> clusterTypes, 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);
// Create
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
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, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxNameLength = 128;
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<User>> _starringUsers; // Users that starred this artist
};
} // namespace Database
@@ -0,0 +1,120 @@
/*
* 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 <string>
#include <string_view>
#include <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "database/Types.hpp"
namespace Database {
class Track;
class ClusterType;
class ScanSettings;
class Session;
class Cluster : public Object<Cluster, ClusterId>
{
public:
Cluster() = default;
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
// Find utility
static std::vector<pointer> getAll(Session& session);
static std::vector<pointer> getAllOrphans(Session& session);
static pointer getById(Session& session, ClusterId id);
// Create utility
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
// Accessors
const std::string& getName() const { return _name; }
ObjectPtr<ClusterType> getType() const { return _clusterType; }
std::size_t getTracksCount() const { return _tracks.size(); }
std::vector<ObjectPtr<Track>> getTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> limit = {}) const;
std::vector<TrackId> getTrackIds() const;
std::size_t getReleasesCount() const;
void addTrack(ObjectPtr<Track> track);
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxNameLength = 128;
std::string _name;
Wt::Dbo::ptr<ClusterType> _clusterType;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
};
class ClusterType : public Object<ClusterType, ClusterTypeId>
{
public:
ClusterType() = default;
ClusterType(std::string_view name);
// Getters
static std::vector<pointer> getAllOrphans(Session& session);
static std::vector<pointer> getAllUsed(Session& session);
static pointer getByName(Session& session, const std::string& name);
static pointer getById(Session& session, ClusterTypeId id);
static std::vector<pointer> getAll(Session& session);
static pointer create(Session& session, const std::string& name);
static void remove(Session& session, const std::string& name);
// Accessors
const std::string& getName(void) 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");
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxNameLength = 128;
std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
Wt::Dbo::ptr<ScanSettings> _scanSettings;
};
} // namespace Database
+101
View File
@@ -0,0 +1,101 @@
/*
* 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);
~Db();
Db(const Db&) = delete;
Db(Db&&) = delete;
Db& operator=(const Db&) = delete;
Db& operator=(Db&&) = delete;
Session& getTLSSession();
private:
friend class Session;
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
class ScopedConnection
{
public:
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
~ScopedConnection();
ScopedConnection(const ScopedConnection& ) = delete;
ScopedConnection(ScopedConnection&& ) = delete;
ScopedConnection& operator=(const ScopedConnection& ) = delete;
ScopedConnection& operator=(ScopedConnection&& ) = delete;
Wt::Dbo::SqlConnection* operator->() const;
private:
Wt::Dbo::SqlConnectionPool& _connectionPool;
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
};
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;
};
void executeSql(const std::string& sql);
RecursiveSharedMutex _sharedMutex;
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
std::mutex _tlsSessionsMutex;
std::vector<std::unique_ptr<Session>> _tlsSessions;
};
} // namespace Database
@@ -0,0 +1,128 @@
/*
* 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 <vector>
#include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include "database/Types.hpp"
#include "utils/UUID.hpp"
namespace Database
{
class Artist;
class Cluster;
class ClusterType;
class Release;
class Session;
class Track;
class User;
class Release : public Object<Release, ReleaseId>
{
public:
Release() = default;
Release(const std::string& name, const std::optional<UUID>& MBID = {});
// Accessors
static std::size_t getCount(Session& session);
static pointer getByMBID(Session& session, const UUID& MBID);
static std::vector<pointer> getByName(Session& session, const std::string& name);
static pointer getById(Session& session, ReleaseId id);
static bool exists(Session& session, ReleaseId id);
static std::vector<pointer> getAllOrphans(Session& session); // no track related
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
static std::vector<ReleaseId> getAllIds(Session& session);
static std::vector<pointer> getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
static std::vector<pointer> getAllRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> size = {});
static std::vector<ReleaseId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> size = {});
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults);
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range = std::nullopt);
static std::vector<pointer> getStarred(Session& session, ObjectPtr<User> user, const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults);
static std::vector<pointer> getByClusters(Session& session, const std::vector<ClusterId>& clusters);
static std::vector<pointer> getByFilter(Session& session,
const std::vector<ClusterId>& clusters, // if non empty, at least one release that belongs to these clusters
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords
std::optional<Range> range,
bool& moreExpected);
static std::vector<ReleaseId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
std::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters = {}) const;
std::size_t getTracksCount() const;
ObjectPtr<Track> getFirstTrack() const;
// 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<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
// Create
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
// Utility functions
std::optional<int> getReleaseYear(bool originalDate = false) const;
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
// Accessors
const std::string& getName() const { return _name; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::size_t> getTotalTrack() const;
std::optional<std::size_t> getTotalDisc() const;
std::chrono::milliseconds getDuration() const;
Wt::WDateTime getLastWritten() const;
// 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;
void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxNameLength {128};
std::string _name;
std::string _MBID;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers; // Users that starred this release
};
} // namespace Database
@@ -0,0 +1,101 @@
/*
* 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 <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WTime.h>
#include "database/Types.hpp"
namespace Database {
class ClusterType;
class Session;
class ScanSettings : 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 RecommendationEngineType
{
Clusters = 0,
Features,
};
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<ObjectPtr<ClusterType>> getClusterTypes() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const;
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
// 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 setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = 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, _recommendationEngineType,"similarity_engine_type");
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
}
private:
int _scanVersion {};
std::string _mediaDirectory;
Wt::WTime _startTime = Wt::WTime {0,0,0};
UpdatePeriod _updatePeriod {UpdatePeriod::Never};
RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters};
std::string _audioFileExtensions {".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus"};
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
};
} // namespace Database
@@ -0,0 +1,87 @@
/*
* 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 <memory>
#include <mutex>
#include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/SqlConnectionPool.h>
#include "utils/RecursiveSharedMutex.hpp"
namespace Database
{
class UniqueTransaction
{
private:
friend class Session;
UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
std::unique_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction;
};
class SharedTransaction
{
private:
friend class Session;
SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
std::shared_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction;
};
class Db;
class Session
{
public:
Session (Db& database);
Session(const Session&) = delete;
Session(Session&&) = delete;
Session& operator=(const Session&) = delete;
Session& operator=(Session&&) = delete;
[[nodiscard]] UniqueTransaction createUniqueTransaction();
[[nodiscard]] SharedTransaction createSharedTransaction();
void checkUniqueLocked();
void checkSharedLocked();
void optimize();
void prepareTables(); // need to run only once at startup
Wt::Dbo::Session& getDboSession() { return _session; }
private:
Session(std::shared_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool);
void doDatabaseMigrationIfNeeded();
Db& _db;
Wt::Dbo::Session _session;
};
} // namespace Database
@@ -0,0 +1,226 @@
/*
* 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 <optional>
#include <string>
#include <string_view>
#include <unordered_set>
#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/Types.hpp"
namespace Database {
class Artist;
class Cluster;
class ClusterType;
class Release;
class Session;
class TrackArtistLink;
class TrackFeatures;
class TrackListEntry;
class TrackStats;
class User;
class Track : public Object<Track, TrackId>
{
public:
Track() = default;
Track(const std::filesystem::path& p);
// Find utility functions
static std::size_t getCount(Session& session);
static pointer getByPath(Session& session, const std::filesystem::path& p);
static pointer getById(Session& session, TrackId id);
static bool exists(Session& session, TrackId id);
static std::vector<pointer> getByRecordingMBID(Session& session, const UUID& MBID);
static std::vector<pointer> getSimilarTracks(Session& session,
const std::vector<TrackId>& trackIds,
std::optional<std::size_t> offset = {},
std::optional<std::size_t> size = {});
static std::vector<pointer> getByClusters(Session& session,
const std::vector<ClusterId>& clusters); // tracks that belong to these clusters
static std::vector<pointer> getByFilter(Session& session,
const std::vector<ClusterId>& clusters, // if non empty, tracks that belong to these clusters
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords
std::optional<Range> range,
bool& moreExpected);
static std::vector<pointer> getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName);
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = std::nullopt);
static std::vector<pointer> getAllRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> limit = std::nullopt);
static std::vector<TrackId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> limit = std::nullopt);
static std::vector<TrackId> getAllIds(Session& session);
static std::vector<std::pair<TrackId, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
static std::vector<pointer> getMBIDDuplicates(Session& session);
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults);
static std::vector<pointer> getAllWithRecordingMBIDAndMissingFeatures(Session& session);
static std::vector<TrackId> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
static std::vector<TrackId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session,
ObjectPtr<User> user,
const std::vector<ClusterId>& clusters,
std::optional<Range> range, bool& hasMore);
// Create utility
static pointer create(Session& session, const std::filesystem::path& p);
// Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(int num) { _trackNumber = num; }
void setDiscNumber(int num) { _discNumber = num; }
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack ? *totalTrack : 0; }
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc ? *totalDisc : 0; }
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
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; }
void clearArtistLinks();
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters );
void setFeatures(const ObjectPtr<TrackFeatures>& features);
std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const;
std::optional<std::size_t> getTotalTrack() const;
std::optional<std::size_t> getDiscNumber() const;
const std::string& getDiscSubtitle() const { return _discSubtitle; }
std::optional<std::size_t> getTotalDisc() const;
std::string getName() const { return _name; }
std::filesystem::path getPath() const { return _filePath; }
std::chrono::milliseconds getDuration() const { return _duration; }
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; }
// no artistLinkTypes means get all
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const;
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
ObjectPtr<Release> getRelease() const { return _release; }
std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<ClusterId> getClusterIds() const;
bool hasTrackFeatures() const;
ObjectPtr<TrackFeatures> getTrackFeatures() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& 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, _discSubtitle, "disc_subtitle");
Wt::Dbo::field(a, _totalTrack, "total_track");
Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration");
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");
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);
Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasOne(a, _trackFeatures);
}
private:
static const std::size_t _maxNameLength = 128;
static const std::size_t _maxCopyrightLength = 128;
static const std::size_t _maxCopyrightURLLength = 128;
int _scanVersion {};
int _trackNumber {};
int _discNumber {};
std::string _discSubtitle;
int _totalTrack {};
int _totalDisc {};
std::string _name;
std::string _artistName;
std::string _releaseName;
std::chrono::duration<int, std::milli> _duration {};
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;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _playlistEntries;
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers;
Wt::Dbo::weak_ptr<TrackFeatures> _trackFeatures;
};
} // namespace database
@@ -0,0 +1,69 @@
/*
* 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 <string>
#include <Wt/Dbo/Dbo.h>
#include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database
{
class Artist;
class Session;
class Track;
class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId>
{
public:
TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
static EnumSet<TrackArtistLinkType> getUsedTypes(Session& session);
ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<Artist> getArtist() const { return _artist; }
TrackArtistLinkType getType() const { return _type; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _type, "name");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
}
private:
TrackArtistLinkType _type;
std::string _name;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Artist> _artist;
};
}
@@ -0,0 +1,80 @@
/*
* 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 <Wt/Dbo/Dbo.h>
#include "database/Types.hpp"
namespace Database {
class Session;
class Track;
class User;
class TrackBookmark : public Object<TrackBookmark, TrackBookmarkId>
{
public:
TrackBookmark () = default;
TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track);
// utility
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
// Find utility functions
static std::vector<pointer> getAll(Session& session);
static std::vector<pointer> getByUser(Session& session, ObjectPtr<User> user);
static pointer getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
static pointer getById(Session& session, TrackBookmarkId id);
// 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:
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,68 @@
/*
* 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 <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <Wt/Dbo/Dbo.h>
#include "database/Types.hpp"
namespace Database {
class Session;
class Track;
using FeatureName = std::string;
using FeatureValues = std::vector<double>;
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
{
public:
TrackFeatures() = default;
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
// Create utility
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
FeatureValues getFeatureValues(const FeatureName& feature) const;
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _data, "data");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
}
private:
std::string _data;
Wt::Dbo::ptr<Track> _track;
};
} // namespace database
@@ -0,0 +1,160 @@
/*
* 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 <set>
#include <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "database/Types.hpp"
namespace Database {
class Artist;
class Cluster;
class Release;
class Session;
class Track;
class TrackListEntry;
class User;
class TrackList : public Object<TrackList, TrackListId>
{
public:
enum class Type
{
Playlist, // user controlled playlists
Internal, // internal usage (current playqueue, history, ...)
};
TrackList() = default;
TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
// Stats utility
std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Release>> getTopReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Track>> getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
// Search utility
static pointer get(Session& session, std::string_view name, Type type, ObjectPtr<User> user);
static pointer getById(Session& session, TrackListId tracklistId);
static std::vector<pointer> getAll(Session& session);
static std::vector<pointer> getAll(Session& session, ObjectPtr<User> user);
static std::vector<pointer> getAll(Session& session, ObjectPtr<User> user, Type type);
// Create utility
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
// Accessors
std::string getName() const { return _name; }
bool isPublic() const { return _isPublic; }
Type 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<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
// Get track bya
std::vector<ObjectPtr<Artist>> getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Release>> getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Track>> getTracksReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<TrackId> getTrackIds() const;
std::chrono::milliseconds getDuration() const;
// Get clusters, order by occurence
std::vector<ObjectPtr<Cluster>> getClusters() 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::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
}
private:
std::string _name;
Type _type {Type::Playlist};
bool _isPublic {false};
Wt::Dbo::ptr<User> _user;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
};
class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
{
public:
TrackListEntry() = default;
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
// find utility
static pointer getById(Session& session, TrackListEntryId id);
// Create utility
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime());
// 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:
Wt::WDateTime _dateTime;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<TrackList> _tracklist;
};
} // namespace Database
@@ -0,0 +1,176 @@
/*
* 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/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 other._id < _id; }
private:
Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()};
};
struct Range
{
std::size_t offset {};
std::size_t limit {};
};
enum class TrackArtistLinkType
{
Artist, // regular artist
Arranger,
Composer,
Conductor,
Lyricist,
Mixer,
Performer,
Producer,
ReleaseArtist,
Remixer,
Writer,
};
// User selectable audio file formats
// Do not change values
enum class AudioFormat
{
MP3 = 1,
OGG_OPUS = 2,
OGG_VORBIS = 3,
WEBM_VORBIS = 4,
MATROSKA_OPUS = 5,
};
using Bitrate = std::uint32_t;
// Do not change enum values!
enum class Scrobbler
{
Internal = 0,
ListenBrainz = 1,
};
// Do not change enum values!
enum class UserType
{
REGULAR = 0,
ADMIN = 1,
DEMO = 2,
};
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(); }
auto modify() { return _obj.modify(); }
void remove() { _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:
// Can get raw dbo ptr only from Objects
template <typename SomeObject>
static
Wt::Dbo::ptr<SomeObject> getDboPtr(ObjectPtr<SomeObject> ptr) { return ptr._obj; }
};
}
// TODO factorize hash with std::enable_if
#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
LMS_DECLARE_IDTYPE(ArtistId)
LMS_DECLARE_IDTYPE(AuthTokenId)
LMS_DECLARE_IDTYPE(ClusterId)
LMS_DECLARE_IDTYPE(ClusterTypeId)
LMS_DECLARE_IDTYPE(ReleaseId)
LMS_DECLARE_IDTYPE(ScanSettingsId)
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
LMS_DECLARE_IDTYPE(TrackBookmarkId)
LMS_DECLARE_IDTYPE(TrackFeaturesId)
LMS_DECLARE_IDTYPE(TrackId)
LMS_DECLARE_IDTYPE(TrackListId)
LMS_DECLARE_IDTYPE(TrackListEntryId)
LMS_DECLARE_IDTYPE(UserId)
+244
View File
@@ -0,0 +1,244 @@
/*
* 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/Types.hpp"
#include "utils/UUID.hpp"
namespace Database {
class Artist;
class Release;
class Session;
class TrackList;
class Track;
class User;
class AuthToken : public Object<AuthToken, AuthTokenId>
{
public:
AuthToken() = default;
AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
// Utility
static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
static pointer getByValue(Session& session, const std::string& value);
static pointer getById(Session& session, AuthTokenId tokenId);
// 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:
std::string _value;
Wt::WDateTime _expiry;
Wt::Dbo::ptr<User> _user;
};
class User : public Object<User, UserId>
{
public:
struct PasswordHash
{
std::string salt;
std::string hash;
};
// Do not change enum values!
enum class UITheme
{
Light = 0,
Dark = 1,
};
// Do not remove values!
static inline const std::set<Bitrate> audioTranscodeAllowedBitrates
{
64000,
96000,
128000,
192000,
320000,
};
// Do not change enum values!
enum class SubsonicArtistListMode
{
AllArtists = 0,
ReleaseArtists = 1,
TrackArtists = 2,
};
static inline const std::size_t MinNameLength {3};
static inline const std::size_t MaxNameLength {15};
static inline const bool defaultSubsonicTranscodeEnable {true};
static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS};
static inline const Bitrate defaultSubsonicTranscodeBitrate {128000};
static inline const UITheme defaultUITheme {UITheme::Dark};
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
static inline const Scrobbler defaultScrobbler {Scrobbler::Internal};
User() = default;
User(std::string_view loginName);
// utility
static pointer create(Session& session, std::string_view loginName);
static pointer getById(Session& session, UserId id);
static pointer getByLoginName(Session& session, std::string_view loginName);
static std::vector<pointer> getAll(Session& session);
static std::vector<UserId> getAllIds(Session& session);
static pointer getDemo(Session& session);
static std::size_t getCount(Session& session);
// accessors
const std::string& getLoginName() const { return _loginName; }
PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; }
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 setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; }
void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; }
void setSubsonicTranscodeBitrate(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 setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; }
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 getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; }
AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; }
Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; }
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; }
Scrobbler getScrobbler() const { return _scrobbler; }
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
ObjectPtr<TrackList> getQueuedTrackList(Session& session) const;
void starArtist(ObjectPtr<Artist> artist);
void unstarArtist(ObjectPtr<Artist> artist);
bool hasStarredArtist(ObjectPtr<Artist> artist) const;
void starRelease(ObjectPtr<Release> release);
void unstarRelease(ObjectPtr<Release> release);
bool hasStarredRelease(ObjectPtr<Release> release) const;
// Stars
void starTrack(ObjectPtr<Track> track);
void unstarTrack(ObjectPtr<Track> track);
bool hasStarredTrack(ObjectPtr<Track> track) const;
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, _subsonicTranscodeEnable, "subsonic_transcode_enable");
Wt::Dbo::field(a, _subsonicTranscodeFormat, "subsonic_transcode_format");
Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
// UI 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, _tracklists, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
}
private:
std::string _loginName;
std::string _passwordSalt;
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme {defaultUITheme};
Scrobbler _scrobbler {defaultScrobbler};
std::string _listenbrainzToken; // Musicbrainz Identifier
// Admin defined settings
UserType _type {UserType::REGULAR};
// User defined settings
SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode};
bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable};
AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat};
int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate};
// User's dynamic data (UI)
int _curPlayingTrackPos {}; // Current track position in queue
bool _repeatAll {};
bool _radio {};
Wt::Dbo::collection<Wt::Dbo::ptr<TrackList>> _tracklists;
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _starredArtists;
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _starredReleases;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _starredTracks;
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
};
} // namespace Databas'
+368
View File
@@ -0,0 +1,368 @@
/*
* 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, SingleArtist)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(Artist::exists(session, 35));
EXPECT_FALSE(Artist::exists(session, 0));
EXPECT_FALSE(Artist::exists(session, 1));
}
ScopedArtist artist {session, "MyArtist"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(artist.get());
EXPECT_FALSE(!artist.get());
EXPECT_EQ(artist.get()->getId(), artist.getId());
EXPECT_TRUE(Artist::exists(session, artist.getId()));
}
{
auto transaction {session.createSharedTransaction()};
auto artists {Artist::getAll(session, Artist::SortMethod::ByName)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
artists = Artist::getAllOrphans(session);
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackSingleArtist)
{
ScopedTrack track {session, "MyTrack"};
ScopedArtist artist {session, "MyArtist"};
{
auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
}
{
auto transaction {session.createSharedTransaction()};
auto artists {track->getArtists({TrackArtistLinkType::Artist})};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
EXPECT_EQ(artist->getReleaseCount(), 0);
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.createUniqueTransaction()};
auto tracks {artist->getTracks()};
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track.getId());
EXPECT_TRUE(artist->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
EXPECT_EQ(artist->getTracks(TrackArtistLinkType::Artist).size(), 1);
}
}
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles)
{
ScopedTrack track {session, "MyTrack"};
ScopedArtist artist {session, "MyArtist"};
{
auto transaction {session.createUniqueTransaction()};
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.createSharedTransaction()};
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
}
{
auto transaction {session.createSharedTransaction()};
bool hasMore{};
EXPECT_EQ(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
EXPECT_TRUE(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty());
}
{
auto transaction {session.createSharedTransaction()};
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);
EXPECT_EQ(artist->getTracks().size(), 1);
EXPECT_EQ(artist->getTracks({TrackArtistLinkType::ReleaseArtist}).size(), 1);
EXPECT_EQ(artist->getTracks({TrackArtistLinkType::Artist}).size(), 1);
EXPECT_EQ(artist->getTracks({TrackArtistLinkType::Writer}).size(), 1);
}
}
TEST_F(DatabaseFixture,SingleTrackMultiArtists)
{
ScopedTrack track {session, "track"};
ScopedArtist artist1 {session, "artist1"};
ScopedArtist artist2 {session, "artist2"};
ASSERT_NE(artist1.getId(), artist2.getId());
{
auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
}
{
auto transaction {session.createSharedTransaction()};
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::getAll(session, Artist::SortMethod::ByName).size(), 2);
EXPECT_EQ(Artist::getAllIds(session).size(), 2);
}
{
auto transaction {session.createUniqueTransaction()};
EXPECT_EQ(artist1->getTracks().front(), track.get());
EXPECT_EQ(artist2->getTracks().front(), track.get());
EXPECT_TRUE(artist1->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
EXPECT_EQ(artist1->getTracks(TrackArtistLinkType::Artist).size(), 1);
EXPECT_TRUE(artist2->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
EXPECT_EQ(artist2->getTracks(TrackArtistLinkType::Artist).size(), 1);
}
}
TEST_F(DatabaseFixture, SingleArtistSearchByName)
{
ScopedArtist artist {session, "AAA"};
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
{
auto transaction {session.createUniqueTransaction()};
artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
bool more {};
EXPECT_TRUE(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more).empty());
const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
ASSERT_EQ(artistsByAAA.size(), 1);
EXPECT_EQ(artistsByAAA.front()->getId(), artist.getId());
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
ASSERT_EQ(artistsByZZZ.size(), 1);
EXPECT_EQ(artistsByZZZ.front()->getId(), artist.getId());
EXPECT_TRUE(Artist::getByName(session, "NNN").empty());
}
}
TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped)
{
ScopedArtist artist1 {session, "MyArtist%"};
ScopedArtist artist2 {session, "%MyArtist"};
ScopedArtist artist3 {session, "%_MyArtist"};
ScopedArtist artist4 {session, "MyArtist%foo"};
ScopedArtist artist5 {session, "foo%MyArtist"};
ScopedArtist artist6 {session, "%AMyArtist"};
{
auto transaction {session.createSharedTransaction()};
{
const auto artists {Artist::getByName(session, "MyArtist%")};
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_TRUE(Artist::getByName(session, "MyArtistFoo").empty());
}
{
const auto artists {Artist::getByName(session, "%MyArtist")};
ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_TRUE(Artist::getByName(session, "FooMyArtist").empty());
}
{
const auto artists {Artist::getByName(session, "%_MyArtist")};
ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_TRUE(Artist::getByName(session, "%CMyArtist").empty());
}
}
// get by filter only works with tracks links...
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
{
auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist3.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist4.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist5.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist6.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
bool more;
{
const auto artists {Artist::getByFilter(session, {}, {"MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
EXPECT_EQ(artists.size(), 6);
}
{
const auto artists {Artist::getByFilter(session, {}, {"MyArtist%"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist1.getId());
EXPECT_EQ(artists[1]->getId(), artist4.getId());
}
{
const auto artists {Artist::getByFilter(session, {}, {"%MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist2.getId());
EXPECT_EQ(artists[1]->getId(), artist5.getId());
}
{
const auto artists {Artist::getByFilter(session, {}, {"_MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0]->getId(), artist3.getId());
}
}
}
TEST_F(DatabaseFixture, MultiArtistsSortMethod)
{
ScopedArtist artistA {session, "artistA"};
ScopedArtist artistB {session, "artistB"};
{
auto transaction {session.createUniqueTransaction()};
artistA.get().modify()->setSortName("sortNameB");
artistB.get().modify()->setSortName("sortNameA");
}
{
auto transaction {session.createSharedTransaction()};
auto allArtistsByName {Artist::getAll(session, Artist::SortMethod::ByName)};
auto allArtistsBySortName {Artist::getAll(session, Artist::SortMethod::BySortName)};
ASSERT_EQ(allArtistsByName.size(), 2);
EXPECT_EQ(allArtistsByName.front()->getId(), artistA.getId());
EXPECT_EQ(allArtistsByName.back()->getId(), artistB.getId());
ASSERT_EQ(allArtistsBySortName.size(), 2);
EXPECT_EQ(allArtistsBySortName.front()->getId(), artistB.getId());
EXPECT_EQ(allArtistsBySortName.back()->getId(), artistA.getId());
}
}
TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks)
{
ScopedArtist artist {session, "artist"};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedRelease release{session, "MyRelease"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt));
bool moreResults;
const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )};
EXPECT_EQ(tracks.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
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.createSharedTransaction()};
bool moreResults;
const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )};
EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt));
EXPECT_FALSE(moreResults);
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track2.getId());
}
}
+18
View File
@@ -0,0 +1,18 @@
add_executable(test-database
Artist.cpp
Cluster.cpp
DatabaseTest.cpp
Release.cpp
Track.cpp
)
target_link_libraries(test-database PRIVATE
lmsdatabase
GTest::GTest
)
if (NOT CMAKE_CROSSCOMPILING)
gtest_discover_tests(test-database)
endif()
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
/*
* 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/Release.hpp"
#include "database/Session.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"
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.createUniqueTransaction()};
auto entity {T::create(_session, std::forward<Args>(args)...)};
EXPECT_TRUE(entity);
_id = entity->getId();
}
~ScopedEntity()
{
auto transaction {_session.createUniqueTransaction()};
auto entity {T::getById(_session, _id)};
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.createSharedTransaction()};
return get();
}
typename T::pointer get()
{
_session.checkSharedLocked();
auto entity {T::getById(_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 ScopedTrackBookmark = ScopedEntity<Database::TrackBookmark>;
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:
Database::Db& getDb() { return _db; }
private:
const std::filesystem::path _tmpFile {std::tmpnam(nullptr)};
ScopedFileDeleter fileDeleter {_tmpFile};
Database::Db _db {_tmpFile};
};
class DatabaseFixture : public ::testing::Test
{
public:
~DatabaseFixture()
{
testDatabaseEmpty();
}
public:
static void SetUpTestCase()
{
_tmpDb = std::make_unique<TmpDatabase>();
{
Database::Session s {_tmpDb->getDb()};
s.prepareTables();
s.optimize();
// remove default created entries
{
auto transaction {s.createUniqueTransaction()};
auto clusterTypes {Database::ClusterType::getAll(s)};
for (auto& clusterType : clusterTypes)
clusterType.remove();
}
}
}
static void TearDownTestCase()
{
_tmpDb.reset();
}
private:
void testDatabaseEmpty()
{
auto uniqueTransaction {session.createUniqueTransaction()};
EXPECT_TRUE(Database::Artist::getAll(session, Database::Artist::SortMethod::ByName).empty());
EXPECT_TRUE(Database::Cluster::getAll(session).empty());
EXPECT_TRUE(Database::ClusterType::getAll(session).empty());
EXPECT_TRUE(Database::Release::getAll(session).empty());
EXPECT_TRUE(Database::Track::getAll(session).empty());
EXPECT_TRUE(Database::TrackBookmark::getAll(session).empty());
EXPECT_TRUE(Database::TrackList::getAll(session).empty());
EXPECT_TRUE(Database::User::getAll(session).empty());
}
static inline std::unique_ptr<TmpDatabase> _tmpDb {};
public:
Database::Session session {_tmpDb->getDb()};
};
+410
View File
@@ -0,0 +1,410 @@
/*
* 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.createUniqueTransaction()};
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
tracks.back().get().modify()->setRelease(release.get());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Release::getAllOrphans(session).empty());
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(artist->getReleaseCount(), 1);
ASSERT_EQ(artist->getReleases().size(), 1);
EXPECT_EQ(artist->getReleases().front()->getId(), release.getId());
EXPECT_EQ(release->getTracks().size(), nbTracks);
}
}
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
{
ScopedTrack track {session, "MyTrack"};
ScopedRelease release {session, "MyRelease"};
ScopedArtist artist {session, "MyArtist"};
{
auto transaction {session.createUniqueTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
track.get().modify()->setRelease(release.get());
}
{
auto transaction {session.createUniqueTransaction()};
auto releases {artist->getReleases()};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId());
EXPECT_EQ(artist->getReleaseCount(), 1);
auto artists {release->getArtists()};
ASSERT_EQ(artists.size(), 1);
ASSERT_EQ(artists.front()->getId(), artist.getId());
}
}
TEST_F(DatabaseFixture, SingleUser)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(User::getAll(session).empty());
EXPECT_TRUE(User::getAllIds(session).empty());
}
ScopedUser user {session, "MyUser"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(user->getQueuedTrackList(session)->getCount(), 0);
EXPECT_EQ(User::getAll(session).size(), 1);
EXPECT_EQ(User::getAllIds(session).size(), 1);
}
}
TEST_F(DatabaseFixture, SingleStarredArtist)
{
ScopedArtist artist {session, "MyArtist"};
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
{
auto transaction {session.createUniqueTransaction()};
EXPECT_FALSE(user->hasStarredArtist(artist.get()));
}
{
auto transaction {session.createUniqueTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
user.get().modify()->starArtist(artist.get());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(user->hasStarredArtist(artist.get()));
bool hasMore {};
auto artists {Artist::getStarred(session, user.get(), {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, hasMore)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId());
EXPECT_FALSE(hasMore);
}
}
TEST_F(DatabaseFixture, SingleStarredRelease)
{
ScopedRelease release {session, "MyRelease"};
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(user->hasStarredRelease(release.get()));
}
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setRelease(release.get());
user.get().modify()->starRelease(release.get());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(user->hasStarredRelease(release.get()));
bool hasMore {};
auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId());
EXPECT_FALSE(hasMore);
}
}
TEST_F(DatabaseFixture, SingleStarredTrack)
{
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
{
auto transaction {session.createUniqueTransaction()};
EXPECT_FALSE(user->hasStarredTrack(track.get()));
}
{
auto transaction {session.createUniqueTransaction()};
user.get().modify()->starTrack(track.get());
}
{
auto transaction {session.createUniqueTransaction()};
EXPECT_TRUE(user->hasStarredTrack(track.get()));
bool hasMore {};
auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track.getId());
EXPECT_FALSE(hasMore);
}
}
TEST_F(DatabaseFixture, SingleTrackList)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
{
auto transaction {session.createSharedTransaction()};
auto trackLists {TrackList::getAll(session, user.get(), TrackList::Type::Playlist)};
ASSERT_EQ(trackLists.size(), 1);
EXPECT_EQ(trackLists.front()->getId(), trackList.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::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.createUniqueTransaction()};
TrackListEntry::create(session, tracks.back().get(), trackList.get());
}
{
auto transaction {session.createSharedTransaction()};
ASSERT_EQ(trackList->getCount(), tracks.size());
const auto trackIds {trackList->getTrackIds()};
for (auto trackId : trackIds)
EXPECT_TRUE(std::any_of(std::cbegin(tracks), std::cend(tracks), [trackId](const ScopedTrack& track) { return track.getId() == trackId; }));
}
}
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedTrack track3 {session, "MyTrack3"};
{
Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get(), now);
TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(-1));
TrackListEntry::create(session, track3.get(), trackList.get(), now.addSecs(1));
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults;
const auto tracks {trackList.get()->getTracksReverse({}, std::nullopt, moreResults)};
ASSERT_EQ(tracks.size(), 3);
EXPECT_EQ(tracks.front()->getId(), track3.getId());
EXPECT_EQ(tracks.back()->getId(), track2.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack1"};
ScopedArtist artist1 {session, "MyArtist1"};
ScopedArtist artist2 {session, "MyArtist2"};
ScopedRelease release1 {session, "MyRelease1"};
ScopedRelease release2 {session, "MyRelease2"};
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
EXPECT_TRUE(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getTracksReverse({}, std::nullopt, moreResults).empty());
}
{
auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get(), now);
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
EXPECT_EQ(tracks.size(), 1);
}
{
auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(1));
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist2.getId());
EXPECT_EQ(artists[1]->getId(), artist1.getId());
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release2.getId());
EXPECT_EQ(releases[1]->getId(), release1.getId());
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track2.getId());
EXPECT_EQ(tracks[1]->getId(), track1.getId());
}
{
auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2));
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist1.getId());
EXPECT_EQ(artists[1]->getId(), artist2.getId());
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release1.getId());
EXPECT_EQ(releases[1]->getId(), release2.getId());
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track1.getId());
EXPECT_EQ(tracks[1]->getId(), track2.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackSingleUserSingleBookmark)
{
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"};
ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()};
{
auto transaction {session.createUniqueTransaction()};
bookmark.get().modify()->setComment("MyComment");
bookmark.get().modify()->setOffset(std::chrono::milliseconds {5});
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(TrackBookmark::getAll(session).size(), 1);
const auto bookmarks {TrackBookmark::getByUser(session, user.get())};
ASSERT_EQ(bookmarks.size(), 1);
EXPECT_EQ(bookmarks.back(), bookmark.get());
}
{
auto transaction {session.createSharedTransaction()};
auto userBookmark {TrackBookmark::getByUser(session, user.get(), track.get())};
ASSERT_TRUE(userBookmark);
EXPECT_EQ(userBookmark, bookmark.get());
EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5});
EXPECT_EQ(userBookmark->getComment(), "MyComment");
}
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+363
View File
@@ -0,0 +1,363 @@
/*
* 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, SingleRelease)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(Release::exists(session, 0));
EXPECT_FALSE(Release::exists(session, 1));
}
ScopedRelease release {session, "MyRelease"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Release::exists(session, release.getId()));
auto releases {Release::getAllOrphans(session)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId());
releases = Release::getAll(session);
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId());
EXPECT_EQ(release->getDuration(), std::chrono::seconds {0});
}
}
TEST_F(DatabaseFixture, SingleTrackSingleRelease)
{
ScopedRelease release {session, "MyRelease"};
{
ScopedTrack track {session, "MyTrack"};
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setRelease(release.get());
track.get().modify()->setName("MyTrackName");
release.get().modify()->setName("MyReleaseName");
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Release::getAllOrphans(session).empty());
EXPECT_EQ(release->getTracksCount(), 1);
ASSERT_EQ(release->getTracks().size(), 1);
EXPECT_EQ(release->getTracks().front()->getId(), track.getId());
}
{
auto transaction {session.createUniqueTransaction()};
ASSERT_TRUE(track->getRelease());
EXPECT_EQ(track->getRelease()->getId(), release.getId());
}
{
auto transaction {session.createUniqueTransaction()};
auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")};
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track.getId());
}
{
auto transaction {session.createUniqueTransaction()};
auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")};
EXPECT_EQ(tracks.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
auto tracks {Track::getByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")};
EXPECT_EQ(tracks.size(), 0);
}
}
{
auto transaction {session.createUniqueTransaction()};
EXPECT_TRUE(release->getTracks().empty());
auto releases {Release::getAllOrphans(session)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.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.createUniqueTransaction()};
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.createSharedTransaction()};
bool more;
{
const auto releases {Release::getByFilter(session, {}, {"Release"}, std::nullopt, more)};
EXPECT_EQ(releases.size(), 6);
}
{
const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)};
EXPECT_EQ(releases.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); }));
}
{
const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)};
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release2.getId());
EXPECT_EQ(releases[1]->getId(), release4.getId());
}
{
const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)};
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release3.getId());
EXPECT_EQ(releases[1]->getId(), release5.getId());
}
{
const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases[0]->getId(), release5.getId());
}
{
const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases[0]->getId(), release4.getId());
}
}
}
TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
{
ScopedRelease release1 {session, "MyRelease"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(release1->getTotalTrack());
EXPECT_FALSE(release1->getTotalDisc());
}
ScopedTrack track1 {session, "MyTrack"};
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setRelease(release1.get());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(release1->getTotalTrack());
EXPECT_FALSE(release1->getTotalDisc());
}
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setTotalTrack(36);
track1.get().modify()->setTotalDisc(6);
}
{
auto transaction {session.createSharedTransaction()};
ASSERT_TRUE(release1->getTotalTrack());
EXPECT_EQ(*release1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 6);
}
ScopedTrack track2 {session, "MyTrack2"};
{
auto transaction {session.createUniqueTransaction()};
track2.get().modify()->setRelease(release1.get());
track2.get().modify()->setTotalTrack(37);
track2.get().modify()->setTotalDisc(67);
}
{
auto transaction {session.createSharedTransaction()};
ASSERT_TRUE(release1->getTotalTrack());
EXPECT_EQ(*release1->getTotalTrack(), 37);
ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 67);
}
ScopedRelease release2 {session, "MyRelease2"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(release2->getTotalTrack());
EXPECT_FALSE(release2->getTotalDisc());
}
ScopedTrack track3 {session, "MyTrack3"};
{
auto transaction {session.createUniqueTransaction()};
track3.get().modify()->setRelease(release2.get());
track3.get().modify()->setTotalTrack(7);
track3.get().modify()->setTotalDisc(5);
}
{
auto transaction {session.createSharedTransaction()};
ASSERT_TRUE(release1->getTotalTrack());
EXPECT_EQ(*release1->getTotalTrack(), 37);
ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 67);
ASSERT_TRUE(release2->getTotalTrack());
EXPECT_EQ(*release2->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.createSharedTransaction()};
EXPECT_FALSE(release1->getFirstTrack());
EXPECT_FALSE(release2->getFirstTrack());
}
{
auto transaction {session.createUniqueTransaction()};
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.createSharedTransaction()};
EXPECT_TRUE(release1->getFirstTrack());
EXPECT_TRUE(release2->getFirstTrack());
EXPECT_EQ(release1->getFirstTrack()->getId(), track1A.getId());
EXPECT_EQ(release2->getFirstTrack()->getId(), 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.createSharedTransaction()};
const auto releases {Release::getByYear(session, 0, 3000)};
EXPECT_EQ(releases.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
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()->getReleaseYear(), release1Date.year());
EXPECT_EQ(release1.get()->getReleaseYear(true), release1OriginalDate.year());
}
{
auto transaction {session.createSharedTransaction()};
auto releases {Release::getByYear(session, 1950, 2000)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
releases = Release::getByYear(session, 1994, 1994);
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
releases = Release::getByYear(session, 1993, 1993);
ASSERT_EQ(releases.size(), 0);
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
* 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, SingleTrack)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(Track::getCount(session), 0);
EXPECT_FALSE(Track::exists(session, 0));
}
ScopedTrack track {session, "MyTrackFile"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(Track::getAll(session).size(), 1);
EXPECT_EQ(Track::getCount(session), 1);
EXPECT_TRUE(Track::exists(session, track.getId()));
auto myTrack {Track::getById(session, track.getId())};
ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId());
}
}
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.createUniqueTransaction()};
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.createSharedTransaction()};
bool more;
{
const auto tracks {Track::getByFilter(session, {}, {"Track"}, std::nullopt, more)};
EXPECT_EQ(tracks.size(), 6);
}
{
const auto tracks {Track::getByFilter(session, {}, {"MyTrack"}, std::nullopt, more)};
EXPECT_EQ(tracks.size(), 5);
EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const Track::pointer& track) { return track->getId() == track6.getId(); }));
}
{
const auto tracks {Track::getByFilter(session, {}, {"MyTrack%"}, std::nullopt, more)};
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track2.getId());
EXPECT_EQ(tracks[1]->getId(), track3.getId());
}
{
const auto tracks {Track::getByFilter(session, {}, {"%MyTrack"}, std::nullopt, more)};
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track4.getId());
EXPECT_EQ(tracks[1]->getId(), track5.getId());
}
}
}
TEST_F(DatabaseFixture, SingleTrackDate)
{
ScopedTrack track {session, "MyTrack"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(track->getYear(), std::nullopt);
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
}
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setDate(Wt::WDate {1995, 5, 5});
track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2});
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(track->getYear(), 1995);
EXPECT_EQ(track->getOriginalYear(), 1994);
}
}