New UI layout and UI improvements. Fixes #50
This commit is contained in:
@@ -44,9 +44,9 @@ namespace CoverArt
|
||||
Release,
|
||||
};
|
||||
|
||||
Type type;
|
||||
Type type;
|
||||
Database::IdType id;
|
||||
std::size_t size;
|
||||
std::size_t size;
|
||||
|
||||
bool operator==(const CacheEntryDesc& other) const
|
||||
{
|
||||
@@ -114,7 +114,7 @@ namespace CoverArt
|
||||
void saveToCache(const CacheEntryDesc& entryDesc, const Image& image);
|
||||
std::optional<Image> loadFromCache(const CacheEntryDesc& entryDesc);
|
||||
|
||||
static inline constexpr std::size_t _maxCacheEntries {300};
|
||||
static inline constexpr std::size_t _maxCacheEntries {1000};
|
||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||
static inline constexpr std::size_t _maxFileSize {10000000};
|
||||
static inline const std::vector<std::filesystem::path> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||
|
||||
@@ -74,91 +74,133 @@ Artist::create(Session& session, const std::string& name, const std::optional<UU
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
getQuery(Session& session,
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
Artist::SortMethod sortMethod)
|
||||
std::optional<TrackArtistLink::Type> linkType)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
WhereClause where;
|
||||
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");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT a FROM artist a";
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
if (!keywords.empty())
|
||||
{
|
||||
WhereClause whereKeywordsName;
|
||||
WhereClause whereKeywordsSortName;
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<std::string> sortClauses;
|
||||
|
||||
for (auto keyword : keywords)
|
||||
for (const std::string& keyword : keywords)
|
||||
{
|
||||
whereKeywordsName.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
whereKeywordsSortName.And(WhereClause("a.sort_name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
clauses.push_back("a.name LIKE ?");
|
||||
query.bind("%%" + keyword + "%%");
|
||||
|
||||
sortClauses.push_back("a.sort_name LIKE ?");
|
||||
query.bind("%%" + keyword + "%%");
|
||||
}
|
||||
|
||||
where.And(whereKeywordsName.Or(whereKeywordsSortName));
|
||||
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
}
|
||||
|
||||
if (!clusterIds.empty() || linkType)
|
||||
{
|
||||
oss << " 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";
|
||||
|
||||
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(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
if (linkType)
|
||||
where.And(WhereClause {"t_a_l.type = ?"}.bind(std::to_string(static_cast<int>(*linkType))));
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
oss << " ORDER BY a.name COLLATE NOCASE";
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
oss << " ORDER BY a.sort_name COLLATE NOCASE";
|
||||
break;
|
||||
}
|
||||
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";
|
||||
|
||||
Wt::Dbo::Query<Artist::pointer> query = session.getDboSession().query<Artist::pointer>( oss.str() );
|
||||
WhereClause clusterClause;
|
||||
for (const IdType clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
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, SortMethod sortMethod, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
Artist::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = getQuery(session, {}, {}, std::nullopt, sortMethod)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
Wt::Dbo::collection<pointer> 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<pointer> 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<Artist::pointer>(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<Artist::pointer> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<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<IdType>
|
||||
Artist::getAllIds(Session& session)
|
||||
{
|
||||
@@ -168,6 +210,21 @@ Artist::getAllIds(Session& session)
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAllOrphans(Session& session)
|
||||
{
|
||||
@@ -197,8 +254,8 @@ Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMe
|
||||
assert(!clusters.empty());
|
||||
|
||||
session.checkSharedLocked();
|
||||
bool more;
|
||||
return getByFilter(session, clusters, {}, {}, sortMethod, {}, {}, more);
|
||||
bool more{};
|
||||
return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more);
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
@@ -207,18 +264,31 @@ Artist::getByFilter(Session& session,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords, linkType, sortMethod)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
auto query {createQuery<Artist::pointer>(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<Artist::pointer> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (size && res.size() == static_cast<std::size_t>(*size) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -230,14 +300,29 @@ Artist::getByFilter(Session& session,
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getLastWritten(Session& session, Wt::WDateTime after, std::optional<std::size_t> limit)
|
||||
Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.getDboSession().query<Artist::pointer>("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")
|
||||
.where("t.file_last_write > ?").bind(after)
|
||||
.groupBy("a.id")
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(*after);
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<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 std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
@@ -427,7 +512,6 @@ void
|
||||
Artist::setSortName(const std::string& sortName)
|
||||
{
|
||||
_sortName = std::string(sortName, 0 , _maxNameLength);
|
||||
LMS_LOG(DB, DEBUG) << "SORT NAME = '" << _sortName << "'";
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -46,9 +46,25 @@ Db::Db(const std::filesystem::path& dbPath)
|
||||
void
|
||||
Db::executeSql(const std::string& sql)
|
||||
{
|
||||
auto connection {_connectionPool->getConnection()};
|
||||
ScopedConnection connection {*_connectionPool};
|
||||
connection->executeSql(sql);
|
||||
_connectionPool->returnConnection(std::move(connection));
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -31,6 +31,45 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
|
||||
for (const std::string& keyword : keywords)
|
||||
query.where("r.name LIKE ?").bind("%%" + 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 IdType 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() : ""}
|
||||
@@ -84,13 +123,13 @@ Release::getCount(Session& session)
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAll(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
Release::getAll(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.orderBy("name COLLATE NOCASE");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
@@ -123,17 +162,34 @@ Release::getAllOrderedByArtist(Session& session, std::optional<std::size_t> offs
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllRandom(Session& session, std::optional<std::size_t> size)
|
||||
Release::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.orderBy("RANDOM()");
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT DISTINCT r from release r", clusterIds,{})};
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Release::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT DISTINCT r.id from release r", clusterIds,{})};
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllOrphans(Session& session)
|
||||
{
|
||||
@@ -145,18 +201,34 @@ Release::getAllOrphans(Session& session)
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> offset, std::optional<std::size_t> limit)
|
||||
Release::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().query<Release::pointer>("SELECT r from release r INNER JOIN track t ON r.id = t.release_id")
|
||||
.where("t.file_last_write > ?").bind(after)
|
||||
.groupBy("r.id")
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Release::pointer> 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);
|
||||
|
||||
auto res {std::vector<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>
|
||||
@@ -173,47 +245,6 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<std
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Release::pointer>
|
||||
getQuery(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT r FROM release r";
|
||||
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("r.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " 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 (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY r.name COLLATE NOCASE";
|
||||
|
||||
Wt::Dbo::Query<Release::pointer> query = session.getDboSession().query<Release::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
{
|
||||
@@ -222,26 +253,27 @@ Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool moreResults;
|
||||
return getByFilter(session, clusters, {}, {}, {}, moreResults);
|
||||
return getByFilter(session, clusters, {}, std::nullopt, moreResults);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
Wt::Dbo::collection<pointer> collection = createQuery<Release::pointer>(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);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (size && res.size() == static_cast<std::size_t>(*size) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
|
||||
@@ -378,6 +378,7 @@ Session::prepareTables()
|
||||
_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)");
|
||||
|
||||
@@ -32,9 +32,46 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
|
||||
for (const std::string& keyword : keywords)
|
||||
query.where("t.name LIKE ?").bind("%%" + 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 IdType 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() )
|
||||
: _filePath {p.string()}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -58,17 +95,34 @@ Track::getAll(Session& session, std::optional<std::size_t> limit)
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAllRandom(Session& session, std::optional<std::size_t> limit)
|
||||
Track::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res {session.getDboSession().find<Track>()
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.orderBy("RANDOM()")};
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
|
||||
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
|
||||
return std::vector<pointer>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Track::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT t.id from track t", clusterIds, {})};
|
||||
|
||||
Wt::Dbo::collection<IdType> collection = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
|
||||
return std::vector<IdType>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<IdType>
|
||||
Track::getAllIds(Session& session)
|
||||
{
|
||||
@@ -147,16 +201,30 @@ Track::getMBIDDuplicates(Session& session)
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> limit)
|
||||
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res = session.getDboSession().find<Track>()
|
||||
.where("file_last_write > ?").bind(after)
|
||||
.orderBy("file_last_write DESC")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Track::pointer> 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);
|
||||
|
||||
auto res {std::vector<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<Track::pointer>
|
||||
@@ -225,66 +293,21 @@ Track::hasTrackFeatures() const
|
||||
return (_trackFeatures.lock() != Database::TrackFeatures::pointer());
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query< Track::pointer >
|
||||
getQuery(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT t FROM track t";
|
||||
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("t.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
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(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY t.name COLLATE NOCASE";
|
||||
|
||||
Wt::Dbo::Query<Track::pointer> query = session.getDboSession().query<Track::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
Wt::Dbo::collection<pointer> collection = createQuery<Track::pointer>(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);
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
|
||||
if (size && (res.size() == static_cast<std::size_t>(*size) + 1))
|
||||
if (range && (res.size() == static_cast<std::size_t>(range->limit) + 1))
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -340,12 +363,10 @@ Track::getByClusters(Session& session,
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool moreResults;
|
||||
|
||||
return getByFilter(session,
|
||||
clusters,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}, // keywords
|
||||
std::nullopt, // range
|
||||
moreResults);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -106,6 +107,23 @@ TrackList::getById(Session& session, IdType id)
|
||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackList::getCount() const
|
||||
{
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackListEntry>
|
||||
TrackList::getEntry(std::size_t pos) const
|
||||
{
|
||||
Wt::Dbo::ptr<TrackListEntry> res;
|
||||
|
||||
auto entries = getEntries(pos, 1);
|
||||
if (!entries.empty())
|
||||
res = entries.front();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>>
|
||||
TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||
@@ -139,22 +157,180 @@ TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<st
|
||||
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackListEntry>
|
||||
TrackList::getEntry(std::size_t pos) const
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType)
|
||||
{
|
||||
Wt::Dbo::ptr<TrackListEntry> res;
|
||||
auto query {session.query<Artist::pointer>(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");
|
||||
|
||||
auto entries = getEntries(pos, 1);
|
||||
if (!entries.empty())
|
||||
res = entries.front();
|
||||
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<Release::pointer>
|
||||
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||
{
|
||||
auto query {session.query<Release::pointer>(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 (auto 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<Track::pointer>
|
||||
createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||
{
|
||||
auto query {session.query<Track::pointer>("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(std::to_string(id));
|
||||
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::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = createArtistsQuery(*session(), "SELECT DISTINCT a from artist a", self()->id(), clusterIds, linkType)
|
||||
.orderBy("p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
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::size_t
|
||||
TrackList::getCount() const
|
||||
std::vector<Release::pointer>
|
||||
TrackList::getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
return _entries.size();
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = createReleasesQuery(*session(), "SELECT DISTINCT r from release r", self()->id(), clusterIds)
|
||||
.orderBy("p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Release::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<Track::pointer>
|
||||
TrackList::getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = createTracksQuery(*session(), self()->id(), clusterIds)
|
||||
.orderBy("p_e.id DESC")
|
||||
.groupBy("t.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Track::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<Wt::Dbo::ptr<Cluster>>
|
||||
@@ -235,48 +411,85 @@ TrackList::getDuration() const
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getTopArtists(std::size_t limit) const
|
||||
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = session()->query<Artist::pointer>("SELECT a 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 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(self()->id())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.limit(static_cast<int>(limit));
|
||||
auto query {createArtistsQuery(*session(), "SELECT a from artist a", self()->id(), clusterIds, linkType)};
|
||||
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Artist::pointer> 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);
|
||||
|
||||
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::getTopReleases(std::size_t limit) const
|
||||
TrackList::getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> res = session()->query<Release::pointer>("SELECT r from release r INNER JOIN track t ON t.release_id = r.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(self()->id())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.limit(static_cast<int>(limit));
|
||||
auto query {createReleasesQuery(*session(), "SELECT r from release r", self()->id(), clusterIds)};
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Release::pointer> 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);
|
||||
|
||||
auto res {std::vector<Release::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<Track::pointer>
|
||||
TrackList::getTopTracks(std::size_t limit) const
|
||||
TrackList::getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res = session()->query<Track::pointer>("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")
|
||||
.where("p.id = ?").bind(self()->id())
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.limit(static_cast<int>(limit));
|
||||
auto query {createTracksQuery(*session(), self()->id(), clusterIds)};
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Track::pointer> 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);
|
||||
|
||||
auto res {std::vector<Track::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;
|
||||
}
|
||||
|
||||
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
|
||||
|
||||
@@ -70,14 +70,21 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
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<IdType> getAllIds(Session& session);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastWritten(Session& session, Wt::WDateTime after, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<Range>,
|
||||
bool& moreResults);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
// Accessors
|
||||
|
||||
@@ -39,6 +39,24 @@ class Db
|
||||
std::shared_mutex& 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:
|
||||
@@ -51,9 +69,13 @@ class Db
|
||||
_db.executeSql("PRAGMA foreign_keys=ON");
|
||||
}
|
||||
|
||||
private:
|
||||
Db& _db;
|
||||
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);
|
||||
|
||||
@@ -52,19 +52,19 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // no track related
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static std::vector<IdType> 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, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
|
||||
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, at least one release that belongs to these clusters
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
|
||||
@@ -68,16 +68,16 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, tracks that belong to these clusters
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<std::pair<IdType, 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, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <set>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "TrackArtistLink.hpp"
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
@@ -51,9 +53,9 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
||||
|
||||
// Stats utility
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(std::size_t limit = 1) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(std::size_t limit = 1) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(std::size_t limit = 1) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
// Search utility
|
||||
static pointer get(Session& session, const std::string& name, Type type, Wt::Dbo::ptr<User> user);
|
||||
@@ -82,6 +84,10 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
std::vector<IdType> getTrackIds() const;
|
||||
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
|
||||
@@ -21,12 +21,19 @@
|
||||
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
namespace Database {
|
||||
namespace Database
|
||||
{
|
||||
using IdType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
static inline bool IdIsValid(IdType id)
|
||||
{
|
||||
return id != Wt::Dbo::dbo_default_traits::invalidId();
|
||||
}
|
||||
|
||||
struct Range
|
||||
{
|
||||
std::size_t offset {};
|
||||
std::size_t limit {};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -677,7 +677,7 @@ handleGetRandomSongsRequest(RequestContext& context)
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
auto tracks {Track::getAllRandom(context.dbSession, size)};
|
||||
auto tracks {Track::getAllRandom(context.dbSession, {}, size)};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
|
||||
@@ -710,16 +710,17 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
if (type == "random")
|
||||
{
|
||||
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
||||
releases = Release::getAllRandom(context.dbSession, size);
|
||||
releases = Release::getAllRandom(context.dbSession, {}, size);
|
||||
}
|
||||
else if (type == "newest")
|
||||
{
|
||||
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
||||
releases = Release::getLastWritten(context.dbSession, after, offset, size);
|
||||
bool moreResults {};
|
||||
releases = Release::getLastWritten(context.dbSession, after, {}, Range {offset, size}, moreResults);
|
||||
}
|
||||
else if (type == "alphabeticalByName")
|
||||
{
|
||||
releases = Release::getAll(context.dbSession, offset, size);
|
||||
releases = Release::getAll(context.dbSession, Range {offset, size});
|
||||
}
|
||||
else if (type == "alphabeticalByArtist")
|
||||
{
|
||||
@@ -748,7 +749,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
if (cluster)
|
||||
{
|
||||
bool more;
|
||||
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more);
|
||||
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -935,7 +936,7 @@ handleGetArtistsRequest(RequestContext& context)
|
||||
{},
|
||||
linkType,
|
||||
Artist::SortMethod::BySortName,
|
||||
{}, {}, more)};
|
||||
std::nullopt, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
|
||||
|
||||
@@ -966,7 +967,8 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
{
|
||||
directoryNode.setAttribute("name", "Music");
|
||||
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
|
||||
bool moreResults{};
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
@@ -1078,7 +1080,7 @@ handleGetIndexesRequest(RequestContext& context)
|
||||
{},
|
||||
linkType,
|
||||
Artist::SortMethod::BySortName,
|
||||
{}, {}, more)};
|
||||
std::nullopt, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
@@ -1296,7 +1298,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
||||
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
||||
|
||||
bool more;
|
||||
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)};
|
||||
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
@@ -1367,19 +1369,19 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
bool more;
|
||||
{
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, artistOffset, artistCount, more)};
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, Range {artistOffset, artistCount}, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto releases {Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)};
|
||||
auto releases {Release::getByFilter(context.dbSession, {}, keywords, Range {albumOffset, albumCount}, more)};
|
||||
for (const Release::pointer& release : releases)
|
||||
searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)};
|
||||
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, Range {songOffset, songCount}, more)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
@@ -24,10 +24,15 @@ namespace Random {
|
||||
RandGenerator& getRandGenerator()
|
||||
{
|
||||
static thread_local std::random_device rd;
|
||||
static thread_local std::mt19937 randGenerator(rd());
|
||||
static thread_local RandGenerator randGenerator(rd());
|
||||
|
||||
return randGenerator;
|
||||
}
|
||||
|
||||
RandGenerator createSeededGenerator(std::size_t seed)
|
||||
{
|
||||
return RandGenerator {seed};
|
||||
}
|
||||
|
||||
} // Random
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Random {
|
||||
using RandGenerator = std::mt19937;
|
||||
RandGenerator& getRandGenerator();
|
||||
|
||||
RandGenerator createSeededGenerator(std::size_t seed);
|
||||
|
||||
template <typename T>
|
||||
T
|
||||
getRandom(T min, T max)
|
||||
|
||||
Reference in New Issue
Block a user